Skip to content
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Management.ViewModels.Template;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
Expand All @@ -18,18 +22,34 @@ public class CreateTemplateController : TemplateControllerBase
{
private readonly ITemplateService _templateService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IOptions<RuntimeSettings> _runtimeSettings;

/// <summary>
/// Initializes a new instance of the <see cref="CreateTemplateController"/> class.
/// </summary>
/// <param name="templateService">An instance of <see cref="ITemplateService"/> used to manage templates.</param>
/// <param name="backOfficeSecurityAccessor">An instance of <see cref="IBackOfficeSecurityAccessor"/> used to access back office security information.</param>
/// <param name="runtimeSettings">The runtime configuration settings.</param>
[ActivatorUtilitiesConstructor]
public CreateTemplateController(
ITemplateService templateService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IOptions<RuntimeSettings> runtimeSettings)
{
_templateService = templateService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_runtimeSettings = runtimeSettings;
}

[Obsolete("Use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
public CreateTemplateController(
ITemplateService templateService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
: this(
templateService,
backOfficeSecurityAccessor,
StaticServiceProvider.Instance.GetRequiredService<IOptions<RuntimeSettings>>())
{
}

/// <summary>
Expand All @@ -47,6 +67,11 @@ public CreateTemplateController(
[EndpointDescription("Creates a new template with the configuration specified in the request model.")]
public async Task<IActionResult> Create(CancellationToken cancellationToken, CreateTemplateRequestModel requestModel)
{
if (_runtimeSettings.Value.Mode == RuntimeMode.Production)
{
return TemplateOperationStatusResult(TemplateOperationStatus.NotAllowedInProductionMode);
}
Comment thread
AndyButland marked this conversation as resolved.

Attempt<ITemplate, TemplateOperationStatus> result = await _templateService.CreateAsync(
requestModel.Name,
requestModel.Alias,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
Expand All @@ -17,16 +21,34 @@ public class DeleteTemplateController : TemplateControllerBase
{
private readonly ITemplateService _templateService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IOptions<RuntimeSettings> _runtimeSettings;

/// <summary>
/// Initializes a new instance of the <see cref="DeleteTemplateController"/> class, responsible for handling template deletion operations.
/// </summary>
/// <param name="templateService">The service used to manage templates.</param>
/// <param name="backOfficeSecurityAccessor">Provides access to back office security features.</param>
public DeleteTemplateController(ITemplateService templateService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
/// <param name="runtimeSettings">The runtime configuration settings.</param>
[ActivatorUtilitiesConstructor]
public DeleteTemplateController(
ITemplateService templateService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IOptions<RuntimeSettings> runtimeSettings)
{
_templateService = templateService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_runtimeSettings = runtimeSettings;
}

[Obsolete("Use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
public DeleteTemplateController(
ITemplateService templateService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
: this(
templateService,
backOfficeSecurityAccessor,
StaticServiceProvider.Instance.GetRequiredService<IOptions<RuntimeSettings>>())
{
}

/// <summary>
Expand All @@ -44,6 +66,11 @@ public DeleteTemplateController(ITemplateService templateService, IBackOfficeSec
[EndpointDescription("Deletes a template identified by the provided Id.")]
public async Task<IActionResult> Delete(CancellationToken cancellationToken, Guid id)
{
if (_runtimeSettings.Value.Mode == RuntimeMode.Production)
{
return TemplateOperationStatusResult(TemplateOperationStatus.NotAllowedInProductionMode);
}
Comment thread
AndyButland marked this conversation as resolved.

Attempt<ITemplate?, TemplateOperationStatus> result = await _templateService.DeleteAsync(id, CurrentUserKey(_backOfficeSecurityAccessor));
return result.Success
? Ok()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Management.ViewModels.Template;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
Expand All @@ -20,21 +24,39 @@ public class UpdateTemplateController : TemplateControllerBase
private readonly ITemplateService _templateService;
private readonly IUmbracoMapper _umbracoMapper;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IOptions<RuntimeSettings> _runtimeSettings;

/// <summary>
/// Initializes a new instance of the <see cref="UpdateTemplateController"/> class, which manages update operations for templates in the Umbraco CMS.
/// </summary>
/// <param name="templateService">Service used to perform operations on templates.</param>
/// <param name="umbracoMapper">Mapper used to convert between domain models and API models.</param>
/// <param name="backOfficeSecurityAccessor">Accessor for back office security context.</param>
/// <param name="runtimeSettings">The runtime configuration settings.</param>
[ActivatorUtilitiesConstructor]
public UpdateTemplateController(
ITemplateService templateService,
IUmbracoMapper umbracoMapper,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IOptions<RuntimeSettings> runtimeSettings)
{
_templateService = templateService;
_umbracoMapper = umbracoMapper;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_runtimeSettings = runtimeSettings;
}

[Obsolete("Use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
public UpdateTemplateController(
ITemplateService templateService,
IUmbracoMapper umbracoMapper,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
: this(
templateService,
umbracoMapper,
backOfficeSecurityAccessor,
StaticServiceProvider.Instance.GetRequiredService<IOptions<RuntimeSettings>>())
{
}

/// <summary>
Expand Down Expand Up @@ -62,7 +84,13 @@ public async Task<IActionResult> Update(
return TemplateNotFound();
}

// In production mode, block updates if the content is being changed.
var existingContent = template.Content;
template = _umbracoMapper.Map(requestModel, template);
if (_runtimeSettings.Value.Mode == RuntimeMode.Production && existingContent != template.Content)
{
return TemplateOperationStatusResult(TemplateOperationStatus.ContentChangeNotAllowedInProductionMode);
}
Comment thread
AndyButland marked this conversation as resolved.

Attempt<ITemplate, TemplateOperationStatus> result = await _templateService.UpdateAsync(template, CurrentUserKey(_backOfficeSecurityAccessor));

Expand Down
1 change: 0 additions & 1 deletion src/Umbraco.Core/Services/PartialViewService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,6 @@ public PartialViewService(
/// <inheritdoc />
protected override string[] AllowedFileExtensions { get; } = { ".cshtml" };

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

/// <inheritdoc />
Expand Down
41 changes: 6 additions & 35 deletions src/Umbraco.Core/Services/TemplateService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,8 @@
private readonly ITemplateRepository _templateRepository;
private readonly IAuditService _auditService;
private readonly ITemplateContentParserService _templateContentParserService;
private readonly IOptions<RuntimeSettings> _runtimeSettings;

// TODO (V18): Remove obsolete constructors and the ActivatorUtilitiesConstructor attribute.
// TODO (V19): Remove obsolete constructors and the ActivatorUtilitiesConstructor attribute.
// Also update UmbracoBuilder where this service is registered using:
// Services.AddUnique<ITemplateService>(sp => ActivatorUtilities.CreateInstance<TemplateService>(sp));
// We do this to allow the ActivatorUtilitiesConstructor to be used (it's otherwise ignored by AddUnique).
Expand All @@ -43,7 +42,6 @@
/// <param name="templateRepository">The repository for template data access.</param>
/// <param name="auditService">The audit service for recording audit entries.</param>
/// <param name="templateContentParserService">The service for parsing template content.</param>
/// <param name="runtimeSettings">The runtime configuration settings.</param>
[ActivatorUtilitiesConstructor]
public TemplateService(
ICoreScopeProvider provider,
Expand All @@ -52,35 +50,33 @@
IShortStringHelper shortStringHelper,
ITemplateRepository templateRepository,
IAuditService auditService,
ITemplateContentParserService templateContentParserService,
IOptions<RuntimeSettings> runtimeSettings)
ITemplateContentParserService templateContentParserService)
: base(provider, loggerFactory, eventMessagesFactory)
{
_shortStringHelper = shortStringHelper;
_templateRepository = templateRepository;
_auditService = auditService;
_templateContentParserService = templateContentParserService;

Check notice on line 59 in src/Umbraco.Core/Services/TemplateService.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

✅ Getting better: Constructor Over-Injection

TemplateService decreases from 8 to 7 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.
_runtimeSettings = runtimeSettings;
}

[Obsolete("Use the non-obsolete constructor instead. Scheduled for removal in Umbraco 18.")]
[Obsolete("Use the constructor without the runtimeSettings parameter. Scheduled for removal in Umbraco 19.")]
public TemplateService(
ICoreScopeProvider provider,
ILoggerFactory loggerFactory,
IEventMessagesFactory eventMessagesFactory,
IShortStringHelper shortStringHelper,
ITemplateRepository templateRepository,
IAuditService auditService,
ITemplateContentParserService templateContentParserService)
ITemplateContentParserService templateContentParserService,
IOptions<RuntimeSettings> runtimeSettings)
: this(
provider,
loggerFactory,
eventMessagesFactory,
shortStringHelper,
templateRepository,
auditService,
templateContentParserService,
StaticServiceProvider.Instance.GetRequiredService<IOptions<RuntimeSettings>>())
templateContentParserService)
{
}

Expand Down Expand Up @@ -129,8 +125,6 @@
{
}

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

/// <inheritdoc />
[Obsolete("Use the overload that includes name and alias parameters instead. Scheduled for removal in Umbraco 19.")]
public async Task<Attempt<ITemplate, TemplateOperationStatus>> CreateForContentTypeAsync(
Expand Down Expand Up @@ -218,11 +212,6 @@
/// <returns>The operation status indicating the result of the validation.</returns>
private async Task<TemplateOperationStatus> ValidateCreateAsync(ITemplate templateToCreate)
{
if (IsProductionMode)
{
return TemplateOperationStatus.NotAllowedInProductionMode;
}

ITemplate? existingTemplate = await GetAsync(templateToCreate.Alias);
if (existingTemplate is not null)
{
Expand Down Expand Up @@ -319,19 +308,6 @@
return TemplateOperationStatus.TemplateNotFound;
}

// In production mode, block updates if the content is being changed.
if (IsProductionMode)
{
// Reuse existingTemplate if keys match (same template), otherwise fetch by key.
ITemplate? existingByKey = existingTemplate?.Key == templateToUpdate.Key
? existingTemplate
: await GetAsync(templateToUpdate.Key);
if (existingByKey is not null && existingByKey.Content != templateToUpdate.Content)
{
return TemplateOperationStatus.ContentChangeNotAllowedInProductionMode;
}
}

return TemplateOperationStatus.Success;
}

Expand Down Expand Up @@ -580,11 +556,6 @@
/// <returns>An attempt result containing the deleted template and operation status.</returns>
private async Task<Attempt<ITemplate?, TemplateOperationStatus>> DeleteAsync(Func<Task<ITemplate?>> getTemplate, Guid userKey)
{
if (IsProductionMode)
{
return Attempt.FailWithStatus<ITemplate?, TemplateOperationStatus>(TemplateOperationStatus.NotAllowedInProductionMode, null);
}

using (ICoreScope scope = ScopeProvider.CreateCoreScope())
{
ITemplate? template = await getTemplate();
Comment thread
AndyButland marked this conversation as resolved.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -690,8 +690,12 @@ protected override void PersistDeletedItem(ITemplate entity)
Database.Execute(delete, new { id = GetEntityId(entity) });
}

var viewName = string.Concat(entity.Alias, ".cshtml");
_viewsFileSystem?.DeleteFile(viewName);
// Only delete file when not in production runtime mode
if (_runtimeSettings.CurrentValue.Mode != RuntimeMode.Production)
{
var viewName = string.Concat(entity.Alias, ".cshtml");
_viewsFileSystem?.DeleteFile(viewName);
}
Comment thread
Migaroez marked this conversation as resolved.

entity.DeleteDate = DateTime.UtcNow;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System.Linq.Expressions;
using System.Net;
using System.Net.Http.Json;
using NUnit.Framework;
using Umbraco.Cms.Api.Management.Controllers.Template;
using Umbraco.Cms.Api.Management.ViewModels.Template;
using Umbraco.Cms.Core;

namespace Umbraco.Cms.Tests.Integration.ManagementApi.Template;

public class CreateTemplateControllerProductionModeTests : ManagementApiTest<CreateTemplateController>
{
protected override Expression<Func<CreateTemplateController, object>> MethodSelector { get; set; }
= x => x.Create(CancellationToken.None, null);

[SetUp]
public override void Setup()
{
InMemoryConfiguration[Constants.Configuration.ConfigRuntimeMode] = "Production";
InMemoryConfiguration["Umbraco:CMS:WebRouting:UmbracoApplicationUrl"] = "https://localhost";
base.Setup();
Authenticate().GetAwaiter().GetResult();
}

private async Task Authenticate()
=> await AuthenticateClientAsync(Client, "admin@test.com", "1234567890", true);
Comment thread
AndyButland marked this conversation as resolved.

[Test]
public async Task Create_Returns_Bad_Request()
{
CreateTemplateRequestModel createModel = new()
{
Name = Guid.NewGuid().ToString(),
Alias = Guid.NewGuid().ToString("N"),
Content = "<h1>Test</h1>",
};

var response = await Client.PostAsync(Url, JsonContent.Create(createModel));

Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode);
}
}
Loading
Loading