Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
d242f6a
Prevent save of partial view file when using runtime production mode,…
AndyButland Feb 2, 2026
c3ec0a8
Display warning when templates and partial views are not editable in …
AndyButland Feb 2, 2026
7407cb3
Share styles.
AndyButland Feb 2, 2026
750452f
Validate at the partial view API whether updates are allowed based on…
AndyButland Feb 2, 2026
0db4abb
Add similar checks for templates, handling case where metadata update…
AndyButland Feb 2, 2026
14ffc97
Add integration tests for verifying behaviour in production mode.
AndyButland Feb 2, 2026
92a356c
Fix the breaking changes on the constructor of the service classes.
AndyButland Feb 2, 2026
dcf442f
Use IOptions (we don't need live updates for this setting).
AndyButland Feb 2, 2026
504a771
Addressed code review feedback.
AndyButland Feb 2, 2026
63e728d
Add IsProductionMode private property on both updated services.
AndyButland Feb 2, 2026
e492c9c
Merge branch 'main' into v17/21564-improvement/prevent-template-editi…
AndyButland Feb 26, 2026
93c7889
Move create template check to validate method.
AndyButland Feb 26, 2026
6c0737d
Remove entity actions create/delete/rename for templates and partial …
AndyButland Feb 26, 2026
f8f83c7
Merge branch 'main' into v17/21564-improvement/prevent-template-editi…
madsrasmussen Mar 2, 2026
6c1ceec
Addressed code review feedback.
AndyButland Mar 2, 2026
acc50da
include server in condition name
madsrasmussen Mar 3, 2026
811d659
move tag to bottom right corner of workspace
madsrasmussen Mar 3, 2026
128af12
introduce info modal
madsrasmussen Mar 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ protected IActionResult PartialViewOperationStatusResult(PartialViewOperationSta
.WithDetail("The partial view name is invalid.")
.Build()),
PartialViewOperationStatus.NotFound => PartialViewNotFound(),
PartialViewOperationStatus.NotAllowedInProductionMode => BadRequest(problemDetailsBuilder
.WithTitle("Not allowed in production mode")
.WithDetail("Partial view modifications are not allowed when running in production mode.")
.Build()),
_ => StatusCode(StatusCodes.Status500InternalServerError, problemDetailsBuilder
.WithTitle("Unknown partial view operation status.")
.Build()),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.Builders;
Expand Down Expand Up @@ -42,6 +42,14 @@ protected IActionResult TemplateOperationStatusResult(TemplateOperationStatus st
.WithTitle("Master template cannot be deleted")
.WithDetail("The master templates cannot be deleted. Please ensure the template is not a master template before you delete.")
.Build()),
TemplateOperationStatus.NotAllowedInProductionMode => BadRequest(problemDetailsBuilder
.WithTitle("Not allowed in production mode")
.WithDetail("Template modifications are not allowed when running in production mode.")
.Build()),
TemplateOperationStatus.ContentChangeNotAllowedInProductionMode => BadRequest(problemDetailsBuilder
.WithTitle("Content change not allowed in production mode")
.WithDetail("Template content changes are not allowed when running in production mode. Metadata updates are permitted.")
.Build()),
_ => StatusCode(StatusCodes.Status500InternalServerError, problemDetailsBuilder
.WithTitle("Unknown template operation status.")
.Build()),
Expand Down
4 changes: 2 additions & 2 deletions src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -328,11 +328,11 @@ private void AddCoreServices()
Services.AddUnique<IContentTypeEditingService, ContentTypeEditingService>();
Services.AddUnique<IMediaTypeEditingService, MediaTypeEditingService>();
Services.AddUnique<IFileService, FileService>();
Services.AddUnique<ITemplateService, TemplateService>();
Services.AddUnique<ITemplateService>(sp => ActivatorUtilities.CreateInstance<TemplateService>(sp));
Services.AddUnique<IScriptService, ScriptService>();
Services.AddUnique<IStylesheetService, StylesheetService>();
Services.AddUnique<IStylesheetFolderService, StylesheetFolderService>();
Services.AddUnique<IPartialViewService, PartialViewService>();
Services.AddUnique<IPartialViewService>(sp => ActivatorUtilities.CreateInstance<PartialViewService>(sp));
Services.AddUnique<IScriptFolderService, ScriptFolderService>();
Services.AddUnique<IPartialViewFolderService, PartialViewFolderService>();
Services.AddUnique<ITemporaryFileService, TemporaryFileService>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,10 @@ public enum PartialViewOperationStatus
/// <summary>
/// The specified partial view was not found.
/// </summary>
NotFound
NotFound,

/// <summary>
/// The operation is not allowed when running in production mode.
/// </summary>
NotAllowedInProductionMode
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,6 @@ public enum TemplateOperationStatus
/// The operation failed because the master template cannot be deleted while it has child templates.
/// </summary>
MasterTemplateCannotBeDeleted,
NotAllowedInProductionMode,
ContentChangeNotAllowedInProductionMode,
}
115 changes: 82 additions & 33 deletions src/Umbraco.Core/Services/PartialViewService.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
Expand All @@ -23,6 +25,14 @@
public class PartialViewService : FileServiceOperationBase<IPartialViewRepository, IPartialView, PartialViewOperationStatus>, IPartialViewService
{
private readonly PartialViewSnippetCollection _snippetCollection;
private readonly IOptions<RuntimeSettings> _runtimeSettings;

// TODO (V18): Remove obsolete constructors and the ActivatorUtilitiesConstructor attribute.
// Also update UmbracoBuilder where this service is registered using:
// Services.AddUnique<IPartialViewService>(sp => ActivatorUtilities.CreateInstance<PartialViewService>(sp));
// We do this to allow the ActivatorUtilitiesConstructor to be used (it's otherwise ignored by AddUnique).
// Revert it to:
// Services.AddUnique<IPartialViewService, PartialViewService>();

/// <summary>
/// Initializes a new instance of the <see cref="PartialViewService" /> class.
Expand All @@ -35,6 +45,8 @@
/// <param name="userIdKeyResolver">The resolver for converting user keys to IDs.</param>
/// <param name="auditService">The service for audit logging.</param>
/// <param name="snippetCollection">The collection of available partial view snippets.</param>
/// <param name="runtimeSettings">The runtime configuration settings.</param>
[ActivatorUtilitiesConstructor]
public PartialViewService(
ICoreScopeProvider provider,
ILoggerFactory loggerFactory,
Expand All @@ -43,22 +55,15 @@
ILogger<StylesheetService> logger,
IUserIdKeyResolver userIdKeyResolver,
IAuditService auditService,
PartialViewSnippetCollection snippetCollection)
PartialViewSnippetCollection snippetCollection,
IOptions<RuntimeSettings> runtimeSettings)
: base(provider, loggerFactory, eventMessagesFactory, repository, logger, userIdKeyResolver, auditService)
=> _snippetCollection = snippetCollection;
{
_snippetCollection = snippetCollection;
_runtimeSettings = runtimeSettings;
}

/// <summary>
/// Initializes a new instance of the <see cref="PartialViewService" /> class.
/// </summary>
/// <param name="provider">The core scope provider for managing database transactions.</param>
/// <param name="loggerFactory">The factory for creating loggers.</param>
/// <param name="eventMessagesFactory">The factory for creating event messages.</param>
/// <param name="repository">The repository for partial view file operations.</param>
/// <param name="logger">The logger instance for logging operations.</param>
/// <param name="userIdKeyResolver">The resolver for converting user keys to IDs.</param>
/// <param name="auditRepository">The repository for audit logging (obsolete).</param>
/// <param name="snippetCollection">The collection of available partial view snippets.</param>
[Obsolete("Use the non-obsolete constructor instead. Scheduled for removal in Umbraco 19.")]
[Obsolete("Use the non-obsolete constructor instead. Scheduled for removal in Umbraco 18.")]
public PartialViewService(
ICoreScopeProvider provider,
ILoggerFactory loggerFactory,
Expand All @@ -76,23 +81,12 @@
logger,
userIdKeyResolver,
StaticServiceProvider.Instance.GetRequiredService<IAuditService>(),
snippetCollection)
snippetCollection,
StaticServiceProvider.Instance.GetRequiredService<IOptions<RuntimeSettings>>())

Check warning on line 85 in src/Umbraco.Core/Services/PartialViewService.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ New issue: Code Duplication

The module contains 3 functions with similar structure: PartialViewService,PartialViewService,PartialViewService. Avoid duplicated, aka copy-pasted, code inside the module. More duplication lowers the code health.

Check notice on line 85 in src/Umbraco.Core/Services/PartialViewService.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

✅ Getting better: Constructor Over-Injection

PartialViewService decreases from 9 to 8 arguments, max arguments = 5. This constructor has too many arguments, indicating an object with low cohesion or missing function argument abstraction. Avoid adding more arguments.
{
}

/// <summary>
/// Initializes a new instance of the <see cref="PartialViewService" /> class.
/// </summary>
/// <param name="provider">The core scope provider for managing database transactions.</param>
/// <param name="loggerFactory">The factory for creating loggers.</param>
/// <param name="eventMessagesFactory">The factory for creating event messages.</param>
/// <param name="repository">The repository for partial view file operations.</param>
/// <param name="logger">The logger instance for logging operations.</param>
/// <param name="userIdKeyResolver">The resolver for converting user keys to IDs.</param>
/// <param name="auditService">The service for audit logging.</param>
/// <param name="auditRepository">The repository for audit logging (obsolete).</param>
/// <param name="snippetCollection">The collection of available partial view snippets.</param>
[Obsolete("Use the non-obsolete constructor instead. Scheduled for removal in Umbraco 19.")]
[Obsolete("Use the non-obsolete constructor instead. Scheduled for removal in Umbraco 18.")]
public PartialViewService(
ICoreScopeProvider provider,
ILoggerFactory loggerFactory,
Expand All @@ -111,13 +105,40 @@
logger,
userIdKeyResolver,
auditService,
snippetCollection)
snippetCollection,
StaticServiceProvider.Instance.GetRequiredService<IOptions<RuntimeSettings>>())
{
}

[Obsolete("Use the non-obsolete constructor instead. Scheduled for removal in Umbraco 18.")]
public PartialViewService(
ICoreScopeProvider provider,
ILoggerFactory loggerFactory,
IEventMessagesFactory eventMessagesFactory,
IPartialViewRepository repository,
ILogger<StylesheetService> logger,
IUserIdKeyResolver userIdKeyResolver,
IAuditService auditService,
PartialViewSnippetCollection snippetCollection)
: this(
provider,
loggerFactory,
eventMessagesFactory,
repository,
logger,
userIdKeyResolver,
auditService,
snippetCollection,
StaticServiceProvider.Instance.GetRequiredService<IOptions<RuntimeSettings>>())
{
}

/// <inheritdoc />
protected override string[] AllowedFileExtensions { get; } = { ".cshtml" };

/// <inheritdoc />
private bool IsProductionMode => _runtimeSettings.Value.Mode == RuntimeMode.Production;

/// <inheritdoc />
protected override PartialViewOperationStatus Success => PartialViewOperationStatus.Success;

Expand Down Expand Up @@ -189,17 +210,45 @@

/// <inheritdoc />
public async Task<PartialViewOperationStatus> DeleteAsync(string path, Guid userKey)
=> await HandleDeleteAsync(path, userKey);
{
if (IsProductionMode)
{
return PartialViewOperationStatus.NotAllowedInProductionMode;
}

return await HandleDeleteAsync(path, userKey);
}

/// <inheritdoc />
public async Task<Attempt<IPartialView?, PartialViewOperationStatus>> CreateAsync(PartialViewCreateModel createModel, Guid userKey)
=> await HandleCreateAsync(createModel.Name, createModel.ParentPath, createModel.Content, userKey);
{
if (IsProductionMode)
{
return Attempt.FailWithStatus<IPartialView?, PartialViewOperationStatus>(PartialViewOperationStatus.NotAllowedInProductionMode, null);
}

return await HandleCreateAsync(createModel.Name, createModel.ParentPath, createModel.Content, userKey);
}

/// <inheritdoc />
public async Task<Attempt<IPartialView?, PartialViewOperationStatus>> UpdateAsync(string path, PartialViewUpdateModel updateModel, Guid userKey)
=> await HandleUpdateAsync(path, updateModel.Content, userKey);
{
if (IsProductionMode)
{
return Attempt.FailWithStatus<IPartialView?, PartialViewOperationStatus>(PartialViewOperationStatus.NotAllowedInProductionMode, null);
}

return await HandleUpdateAsync(path, updateModel.Content, userKey);
}

/// <inheritdoc />
public async Task<Attempt<IPartialView?, PartialViewOperationStatus>> RenameAsync(string path, PartialViewRenameModel renameModel, Guid userKey)
=> await HandleRenameAsync(path, renameModel.Name, userKey);
{
if (IsProductionMode)
{
return Attempt.FailWithStatus<IPartialView?, PartialViewOperationStatus>(PartialViewOperationStatus.NotAllowedInProductionMode, null);
}

return await HandleRenameAsync(path, renameModel.Name, userKey);
}
}
Loading
Loading