diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DataType/UpdateDataTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DataType/UpdateDataTypeController.cs index 6a2a716a4c..60ca5de703 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DataType/UpdateDataTypeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DataType/UpdateDataTypeController.cs @@ -3,7 +3,6 @@ using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Factories; using Umbraco.Cms.Api.Management.ViewModels.DataType; using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/ByPathPartialViewController.cs b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/ByPathPartialViewController.cs new file mode 100644 index 0000000000..6e51d1bf95 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/ByPathPartialViewController.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.PartialView; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.PartialView; + +public class ByPathPartialViewController : PartialViewControllerBase +{ + private readonly IPartialViewService _partialViewService; + private readonly IUmbracoMapper _mapper; + + public ByPathPartialViewController( + IPartialViewService partialViewService, + IUmbracoMapper mapper) + { + _partialViewService = partialViewService; + _mapper = mapper; + } + + [HttpGet] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PartialViewResponseModel), StatusCodes.Status200OK)] + public async Task ByPath(string path) + { + IPartialView? partialView = await _partialViewService.GetAsync(path); + + return partialView is null + ? NotFound() + : Ok(_mapper.Map(partialView)); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/CreatePartialViewController.cs b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/CreatePartialViewController.cs new file mode 100644 index 0000000000..c72c9aa0c7 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/CreatePartialViewController.cs @@ -0,0 +1,42 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.PartialView; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.PartialView; + +public class CreatePartialViewController : PartialViewControllerBase +{ + private readonly IUmbracoMapper _umbracoMapper; + private readonly IPartialViewService _partialViewService; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + + public CreatePartialViewController( + IUmbracoMapper umbracoMapper, + IPartialViewService partialViewService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + { + _umbracoMapper = umbracoMapper; + _partialViewService = partialViewService; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + } + + [HttpPost] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status201Created)] + public async Task Create(CreatePartialViewRequestModel createRequestModel) + { + PartialViewCreateModel createModel = _umbracoMapper.Map(createRequestModel)!; + + Attempt createAttempt = await _partialViewService.CreateAsync(createModel, CurrentUserKey(_backOfficeSecurityAccessor)); + + return createAttempt.Success + ? CreatedAtAction(controller => nameof(controller.ByPath), new { path = createAttempt.Result!.Path }) + : PartialViewOperationStatusResult(createAttempt.Status); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/DeletePartialViewController.cs b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/DeletePartialViewController.cs new file mode 100644 index 0000000000..47d2bbc4de --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/DeletePartialViewController.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.PartialView; + +public class DeletePartialViewController : PartialViewControllerBase +{ + private readonly IPartialViewService _partialViewService; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + + public DeletePartialViewController( + IPartialViewService partialViewService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + { + _partialViewService = partialViewService; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + } + + [HttpDelete] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task Delete(string path) + { + PartialViewOperationStatus operationStatus = await _partialViewService.DeleteAsync(path, CurrentUserKey(_backOfficeSecurityAccessor)); + + return operationStatus is PartialViewOperationStatus.Success + ? Ok() + : PartialViewOperationStatusResult(operationStatus); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Folder/ByPathPartialViewFolderController.cs b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Folder/ByPathPartialViewFolderController.cs new file mode 100644 index 0000000000..58cf73eb70 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Folder/ByPathPartialViewFolderController.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.PartialView.Folder; + +public class ByPathPartialViewFolderController : PartialViewFolderControllerBase +{ + public ByPathPartialViewFolderController(IUmbracoMapper mapper, IPartialViewFolderService partialViewFolderService) : base(mapper, partialViewFolderService) + { + } + + [HttpGet] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + public Task ByPath(string path) => GetFolderAsync(path); +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Folder/CreatePartialViewFolderController.cs b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Folder/CreatePartialViewFolderController.cs new file mode 100644 index 0000000000..73f2f32c82 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Folder/CreatePartialViewFolderController.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.Folder; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.PartialView.Folder; + +public class CreatePartialViewFolderController : PartialViewFolderControllerBase +{ + public CreatePartialViewFolderController( + IUmbracoMapper mapper, + IPartialViewFolderService partialViewFolderService) + : base(mapper, partialViewFolderService) + { + } + + [HttpPost] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + public Task Create(CreatePathFolderRequestModel model) => CreateAsync(model); +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Folder/DeletePartialViewFolderController.cs b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Folder/DeletePartialViewFolderController.cs new file mode 100644 index 0000000000..8b7b8d54e8 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Folder/DeletePartialViewFolderController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.PartialView.Folder; + +public class DeletePartialViewFolderController : PartialViewFolderControllerBase +{ + public DeletePartialViewFolderController( + IUmbracoMapper mapper, + IPartialViewFolderService partialViewFolderService) + : base(mapper, partialViewFolderService) + { + } + + [HttpDelete] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + public Task Delete(string path) => DeleteAsync(path); +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Folder/PartialViewFolderBaseController.cs b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Folder/PartialViewFolderBaseController.cs new file mode 100644 index 0000000000..a5539dab94 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Folder/PartialViewFolderBaseController.cs @@ -0,0 +1,63 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Common.Builders; +using Umbraco.Cms.Api.Management.Routing; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.PartialView.Folder; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.PartialView}/folder")] +[ApiExplorerSettings(GroupName = "Partial View")] +public class PartialViewFolderControllerBase : PathFolderManagementControllerBase +{ + private readonly IPartialViewFolderService _partialViewFolderService; + + public PartialViewFolderControllerBase( + IUmbracoMapper mapper, + IPartialViewFolderService partialViewFolderService) + : base(mapper) + { + _partialViewFolderService = partialViewFolderService; + } + + protected override Task GetContainerAsync(string path) => _partialViewFolderService.GetAsync(path); + + protected override Task> CreateContainerAsync( + PathContainer container) => + _partialViewFolderService.CreateAsync(container); + + protected override Task> DeleteContainerAsync(string path) => + _partialViewFolderService.DeleteAsync(path); + + protected override IActionResult OperationStatusResult(PartialViewFolderOperationStatus status) => + status switch + { + PartialViewFolderOperationStatus.AlreadyExists => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Folder already exists") + .WithDetail("The folder already exists") + .Build()), + PartialViewFolderOperationStatus.NotEmpty => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Not empty") + .WithDetail("The folder is not empty and can therefore not be deleted.") + .Build()), + PartialViewFolderOperationStatus.NotFound => NotFound(new ProblemDetailsBuilder() + .WithTitle("Not found") + .WithDetail("The specified folder was not found.") + .Build()), + PartialViewFolderOperationStatus.ParentNotFound => NotFound(new ProblemDetailsBuilder() + .WithTitle("Parent not found") + .WithDetail("The parent folder was not found.") + .Build()), + PartialViewFolderOperationStatus.InvalidName => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Invalid name") + .WithDetail("The name specified is not a valid name.") + .Build()), + _ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown partial view folder operation status") + }; +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/PartialViewControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/PartialViewControllerBase.cs new file mode 100644 index 0000000000..1c46734d87 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/PartialViewControllerBase.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Common.Builders; +using Umbraco.Cms.Api.Management.Routing; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.PartialView; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.PartialView}")] +[ApiExplorerSettings(GroupName = "Partial View")] +public class PartialViewControllerBase : ManagementApiControllerBase +{ + protected IActionResult PartialViewOperationStatusResult(PartialViewOperationStatus status) => + status switch + { + PartialViewOperationStatus.Success => Ok(), + PartialViewOperationStatus.AlreadyExists => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Partial view already exists") + .WithDetail("A partial view with the same path already exists") + .Build()), + PartialViewOperationStatus.InvalidFileExtension => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Invalid file extension") + .WithDetail("The file extension is not valid for a partial view.") + .Build()), + PartialViewOperationStatus.ParentNotFound => NotFound(new ProblemDetailsBuilder() + .WithTitle("Parent not found") + .WithDetail("The parent folder was not found.") + .Build()), + PartialViewOperationStatus.PathTooLong => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Path too long") + .WithDetail("The file path is too long.") + .Build()), + PartialViewOperationStatus.InvalidName => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Invalid name") + .WithDetail("The partial view name is invalid.") + .Build()), + PartialViewOperationStatus.NotFound => NotFound(new ProblemDetailsBuilder() + .WithTitle("Partial view not found") + .WithDetail("The partial view was not found.") + .Build()), + _ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown partial view operation status") + }; +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Snippet/ByNameController.cs b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Snippet/ByNameController.cs new file mode 100644 index 0000000000..82489af1df --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Snippet/ByNameController.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.PartialView.Snippets; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Snippets; + +namespace Umbraco.Cms.Api.Management.Controllers.PartialView.Snippet; + +public class ByNameController : PartialViewControllerBase +{ + private readonly IPartialViewService _partialViewService; + private readonly IUmbracoMapper _umbracoMapper; + + public ByNameController(IPartialViewService partialViewService, IUmbracoMapper umbracoMapper) + { + _partialViewService = partialViewService; + _umbracoMapper = umbracoMapper; + } + + [HttpGet("snippet/{name}")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PartialViewSnippetResponseModel), StatusCodes.Status200OK)] + public async Task GetByName(string name) + { + PartialViewSnippet? snippet = await _partialViewService.GetSnippetByNameAsync(name); + + if (snippet is null) + { + return NotFound(); + } + + PartialViewSnippetResponseModel? viewModel = _umbracoMapper.Map(snippet); + + return Ok(viewModel); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Snippet/GetAllController.cs b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Snippet/GetAllController.cs new file mode 100644 index 0000000000..e937157db2 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Snippet/GetAllController.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.ViewModels.PartialView.Snippets; +using Umbraco.Cms.Core.Services; +using Umbraco.New.Cms.Core.Models; + +namespace Umbraco.Cms.Api.Management.Controllers.PartialView.Snippet; + +public class GetAllController : PartialViewControllerBase +{ + private readonly IPartialViewService _partialViewService; + + public GetAllController(IPartialViewService partialViewService) => _partialViewService = partialViewService; + + [HttpGet("snippet")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task GetAll(int skip = 0, int take = 100) + { + PagedModel snippets = await _partialViewService.GetSnippetNamesAsync(skip, take); + + var pageViewModel = new PagedViewModel + { + Total = snippets.Total, + Items = snippets.Items.Select(x => new SnippetItemResponseModel(x)), + }; + + return Ok(pageViewModel); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/UpdatePartialViewController.cs b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/UpdatePartialViewController.cs new file mode 100644 index 0000000000..fdbbeab878 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/UpdatePartialViewController.cs @@ -0,0 +1,41 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.PartialView; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.PartialView; + +public class UpdatePartialViewController : PartialViewControllerBase +{ + private readonly IPartialViewService _partialViewService; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly IUmbracoMapper _mapper; + + public UpdatePartialViewController( + IPartialViewService partialViewService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IUmbracoMapper mapper) + { + _partialViewService = partialViewService; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _mapper = mapper; + } + + [HttpPut] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task Update(UpdatePartialViewRequestModel updateViewModel) + { + PartialViewUpdateModel updateModel = _mapper.Map(updateViewModel)!; + Attempt updateAttempt = await _partialViewService.UpdateAsync(updateModel, CurrentUserKey(_backOfficeSecurityAccessor)); + + return updateAttempt.Success + ? Ok() + : PartialViewOperationStatusResult(updateAttempt.Status); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PathFolderManagementControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/PathFolderManagementControllerBase.cs new file mode 100644 index 0000000000..4597682227 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/PathFolderManagementControllerBase.cs @@ -0,0 +1,60 @@ +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.Folder; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Api.Management.Controllers; + +public abstract class PathFolderManagementControllerBase : ManagementApiControllerBase + where TStatus : Enum +{ + protected readonly IUmbracoMapper Mapper; + + protected PathFolderManagementControllerBase( + IUmbracoMapper mapper) => + Mapper = mapper; + + protected async Task GetFolderAsync(string path) + { + PathContainer? container = await GetContainerAsync(path); + if (container == null) + { + return NotFound(); + } + + PathFolderResponseModel? viewModel = Mapper.Map(container); + return Ok(viewModel); + } + + protected async Task CreateAsync(CreatePathFolderRequestModel requestModel) + { + PathContainer folderModel = Mapper.Map(requestModel)!; + + Attempt attempt = await CreateContainerAsync(folderModel); + if (attempt.Success is false) + { + return OperationStatusResult(attempt.Status); + } + + PathFolderResponseModel? viewModel = Mapper.Map(attempt.Result); + return Ok(viewModel); + } + + protected async Task DeleteAsync(string path) + { + Attempt attempt = await DeleteContainerAsync(path); + + return attempt.Success + ? Ok() + : OperationStatusResult(attempt.Result!); + } + + protected abstract Task GetContainerAsync(string path); + + protected abstract Task> CreateContainerAsync(PathContainer container); + + protected abstract Task> DeleteContainerAsync(string path); + + protected abstract IActionResult OperationStatusResult(TStatus status); +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Script/ByPathScriptController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Script/ByPathScriptController.cs new file mode 100644 index 0000000000..4a6b018314 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Script/ByPathScriptController.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.Script; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.Script; + +public class ByPathScriptController : ScriptControllerBase +{ + private readonly IScriptService _scriptService; + private readonly IUmbracoMapper _mapper; + + public ByPathScriptController( + IScriptService scriptService, + IUmbracoMapper mapper) + { + _scriptService = scriptService; + _mapper = mapper; + } + + [HttpGet] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(ScriptResponseModel), StatusCodes.Status200OK)] + public async Task ByPath(string path) + { + IScript? script = await _scriptService.GetAsync(path); + + return script is null + ? NotFound() + : Ok(_mapper.Map(script)); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Script/CreateScriptController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Script/CreateScriptController.cs new file mode 100644 index 0000000000..3c7a94ce46 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Script/CreateScriptController.cs @@ -0,0 +1,44 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.Script; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.Script; + +public class CreateScriptController : ScriptControllerBase +{ + private readonly IScriptService _scriptService; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly IUmbracoMapper _umbracoMapper; + + public CreateScriptController( + IScriptService scriptService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IUmbracoMapper umbracoMapper) + { + _scriptService = scriptService; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _umbracoMapper = umbracoMapper; + } + + [HttpPost] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status201Created)] + public async Task Create(CreateScriptRequestModel requestModel) + { + Guid currentUserKey = CurrentUserKey(_backOfficeSecurityAccessor); + + ScriptCreateModel createModel = _umbracoMapper.Map(requestModel)!; + + Attempt createAttempt = await _scriptService.CreateAsync(createModel, currentUserKey); + + return createAttempt.Success + ? CreatedAtAction(controller => nameof(controller.ByPath), new { path = createAttempt.Result!.Path }) + : ScriptOperationStatusResult(createAttempt.Status); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Script/DeleteScriptController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Script/DeleteScriptController.cs new file mode 100644 index 0000000000..d98a4d5a25 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Script/DeleteScriptController.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.Script; + +public class DeleteScriptController : ScriptControllerBase +{ + private readonly IScriptService _scriptService; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + + public DeleteScriptController( + IScriptService scriptService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + { + _scriptService = scriptService; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + } + + [HttpDelete] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task Delete(string path) + { + ScriptOperationStatus operationStatus = await _scriptService.DeleteAsync(path, CurrentUserKey(_backOfficeSecurityAccessor)); + + return operationStatus is ScriptOperationStatus.Success + ? Ok() + : ScriptOperationStatusResult(operationStatus); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Script/Folder/ByPathScriptFolderController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Script/Folder/ByPathScriptFolderController.cs new file mode 100644 index 0000000000..fea03112cc --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Script/Folder/ByPathScriptFolderController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.Script.Folder; + +public class ByPathScriptFolderController : ScriptFolderControllerBase +{ + public ByPathScriptFolderController( + IUmbracoMapper mapper, + IScriptFolderService scriptFolderService) + : base(mapper, scriptFolderService) + { + } + + [HttpGet] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + public Task ByPath(string path) => GetFolderAsync(path); +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Script/Folder/CreateScriptFolderController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Script/Folder/CreateScriptFolderController.cs new file mode 100644 index 0000000000..37720ad187 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Script/Folder/CreateScriptFolderController.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.Folder; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.Script.Folder; + +public class CreateScriptFolderController : ScriptFolderControllerBase +{ + public CreateScriptFolderController( + IUmbracoMapper mapper, + IScriptFolderService scriptFolderService) + : base(mapper, scriptFolderService) + { + } + + [HttpPost] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + public Task Create(CreatePathFolderRequestModel model) => CreateAsync(model); +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Script/Folder/DeleteScriptFolderController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Script/Folder/DeleteScriptFolderController.cs new file mode 100644 index 0000000000..1b78471587 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Script/Folder/DeleteScriptFolderController.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.Script.Folder; + +public class DeleteScriptFolderController : ScriptFolderControllerBase +{ + public DeleteScriptFolderController(IUmbracoMapper mapper, IScriptFolderService scriptFolderService) + : base(mapper, scriptFolderService) + { + } + + [HttpDelete] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + public Task Delete(string path) => DeleteAsync(path); +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Script/Folder/ScriptFolderControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Script/Folder/ScriptFolderControllerBase.cs new file mode 100644 index 0000000000..7981112b6e --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Script/Folder/ScriptFolderControllerBase.cs @@ -0,0 +1,63 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Common.Builders; +using Umbraco.Cms.Api.Management.Routing; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.Script.Folder; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.Script}/folder")] +[ApiExplorerSettings(GroupName = "Script")] +public class ScriptFolderControllerBase : PathFolderManagementControllerBase +{ + private readonly IScriptFolderService _scriptFolderService; + + public ScriptFolderControllerBase( + IUmbracoMapper mapper, + IScriptFolderService scriptFolderService) + : base(mapper) + { + _scriptFolderService = scriptFolderService; + } + + protected override Task GetContainerAsync(string path) + => _scriptFolderService.GetAsync(path); + + protected override Task> CreateContainerAsync(PathContainer container) + => _scriptFolderService.CreateAsync(container); + + protected override Task> DeleteContainerAsync(string path) => + _scriptFolderService.DeleteAsync(path); + + protected override IActionResult OperationStatusResult(ScriptFolderOperationStatus status) => + status switch + { + ScriptFolderOperationStatus.AlreadyExists => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Folder already exists") + .WithDetail("The folder already exists") + .Build()), + ScriptFolderOperationStatus.NotEmpty => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Not empty") + .WithDetail("The folder is not empty and can therefore not be deleted.") + .Build()), + ScriptFolderOperationStatus.NotFound => NotFound(new ProblemDetailsBuilder() + .WithTitle("Not found") + .WithDetail("The specified folder was not found.") + .Build()), + ScriptFolderOperationStatus.ParentNotFound => NotFound(new ProblemDetailsBuilder() + .WithTitle("Parent not found") + .WithDetail("The parent folder was not found.") + .Build()), + ScriptFolderOperationStatus.InvalidName => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Invalid name") + .WithDetail("The name specified is not a valid name.") + .Build()), + _ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown script folder operation status") + }; +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Script/ScriptControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Script/ScriptControllerBase.cs new file mode 100644 index 0000000000..a82db8ba8c --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Script/ScriptControllerBase.cs @@ -0,0 +1,50 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Common.Builders; +using Umbraco.Cms.Api.Management.Routing; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.Script; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.Script}")] +[ApiExplorerSettings(GroupName = nameof(Constants.UdiEntityType.Script))] +public class ScriptControllerBase : ManagementApiControllerBase +{ + protected IActionResult ScriptOperationStatusResult(ScriptOperationStatus status) => + status switch + { + ScriptOperationStatus.Success => Ok(), + ScriptOperationStatus.AlreadyExists => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Script already exists") + .WithDetail("A script with the same path already exists") + .Build()), + ScriptOperationStatus.CancelledByNotification => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Cancelled by notification") + .WithDetail("A script notification handler prevented the script operation.") + .Build()), + ScriptOperationStatus.InvalidFileExtension => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Invalid file extension") + .WithDetail("The file extension is not valid for a script.") + .Build()), + ScriptOperationStatus.ParentNotFound => NotFound(new ProblemDetailsBuilder() + .WithTitle("Parent not found") + .WithDetail("The parent folder was not found.") + .Build()), + ScriptOperationStatus.PathTooLong => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Path too long") + .WithDetail("The file path is too long.") + .Build()), + ScriptOperationStatus.NotFound => NotFound(new ProblemDetailsBuilder() + .WithTitle("Script not found") + .WithDetail("The script was not found.") + .Build()), + ScriptOperationStatus.InvalidName => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Invalid name") + .WithDetail("The script name is invalid.") + .Build()), + _ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown script operation status") + }; +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Script/UpdateScriptController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Script/UpdateScriptController.cs new file mode 100644 index 0000000000..ecd5541852 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Script/UpdateScriptController.cs @@ -0,0 +1,41 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.Script; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.Script; + +public class UpdateScriptController : ScriptControllerBase +{ + private readonly IScriptService _scriptService; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly IUmbracoMapper _umbracoMapper; + + public UpdateScriptController( + IScriptService scriptService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IUmbracoMapper umbracoMapper) + { + _scriptService = scriptService; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _umbracoMapper = umbracoMapper; + } + + [HttpPut] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task Update(UpdateScriptRequestModel updateViewModel) + { + ScriptUpdateModel? updateModel = _umbracoMapper.Map(updateViewModel); + Attempt updateAttempt = await _scriptService.UpdateAsync(updateModel!, CurrentUserKey(_backOfficeSecurityAccessor)); + + return updateAttempt.Success + ? Ok() + : ScriptOperationStatusResult(updateAttempt.Status); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/AllStylesheetController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/AllStylesheetController.cs new file mode 100644 index 0000000000..42187fd4b6 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/AllStylesheetController.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.ViewModels.Stylesheet; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet; + +public class AllStylesheetController : StylesheetControllerBase +{ + private readonly IStylesheetService _stylesheetService; + private readonly IUmbracoMapper _umbracoMapper; + + public AllStylesheetController( + IStylesheetService stylesheetService, + IUmbracoMapper umbracoMapper) + { + _stylesheetService = stylesheetService; + _umbracoMapper = umbracoMapper; + } + + [HttpGet("all")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task All(int skip = 0, int take = 100) + { + IStylesheet[] stylesheets = (await _stylesheetService.GetAllAsync()).ToArray(); + + List viewModels = _umbracoMapper.MapEnumerable(stylesheets.Skip(skip).Take(take)); + + return Ok(new PagedViewModel { Items = viewModels, Total = stylesheets.Length }); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/ByPathStylesheetController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/ByPathStylesheetController.cs new file mode 100644 index 0000000000..2a98c2dc22 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/ByPathStylesheetController.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.Stylesheet; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet; + +public class ByPathStylesheetController : StylesheetControllerBase +{ + private readonly IStylesheetService _stylesheetService; + private readonly IUmbracoMapper _umbracoMapper; + + public ByPathStylesheetController( + IStylesheetService stylesheetService, + IUmbracoMapper umbracoMapper) + { + _stylesheetService = stylesheetService; + _umbracoMapper = umbracoMapper; + } + + [HttpGet] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(StylesheetResponseModel), StatusCodes.Status200OK)] + public async Task ByPath(string path) + { + IStylesheet? stylesheet = await _stylesheetService.GetAsync(path); + + if (stylesheet is null) + { + return NotFound(); + } + + StylesheetResponseModel? viewModel = _umbracoMapper.Map(stylesheet); + + return Ok(viewModel); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/CreateStylesheetController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/CreateStylesheetController.cs new file mode 100644 index 0000000000..a31bf7ac12 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/CreateStylesheetController.cs @@ -0,0 +1,42 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.Stylesheet; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet; + +public class CreateStylesheetController : StylesheetControllerBase +{ + private readonly IStylesheetService _stylesheetService; + private readonly IUmbracoMapper _umbracoMapper; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + + public CreateStylesheetController( + IStylesheetService stylesheetService, + IUmbracoMapper umbracoMapper, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + { + _stylesheetService = stylesheetService; + _umbracoMapper = umbracoMapper; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + } + + [HttpPost] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status201Created)] + public async Task Create(CreateStylesheetRequestModel requestModel) + { + StylesheetCreateModel createModel = _umbracoMapper.Map(requestModel)!; + Attempt createAttempt = await _stylesheetService.CreateAsync(createModel, CurrentUserKey(_backOfficeSecurityAccessor)); + + return createAttempt.Success + ? CreatedAtAction(controller => nameof(controller.ByPath), new { path = createAttempt.Result!.Path }) + : StylesheetOperationStatusResult(createAttempt.Status); + } + +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/DeleteStylesheetController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/DeleteStylesheetController.cs new file mode 100644 index 0000000000..6d55819be2 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/DeleteStylesheetController.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet; + +public class DeleteStylesheetController : StylesheetControllerBase +{ + private readonly IStylesheetService _stylesheetService; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + + public DeleteStylesheetController( + IStylesheetService stylesheetService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + { + _stylesheetService = stylesheetService; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + } + + [HttpDelete] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task Delete(string path) + { + StylesheetOperationStatus operationStatus = await _stylesheetService.DeleteAsync(path, CurrentUserKey(_backOfficeSecurityAccessor)); + + return operationStatus is StylesheetOperationStatus.Success + ? Ok() + : StylesheetOperationStatusResult(operationStatus); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/ExtractRichTextRulesController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/ExtractRichTextRulesController.cs new file mode 100644 index 0000000000..967e95b24d --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/ExtractRichTextRulesController.cs @@ -0,0 +1,38 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.RichTextStylesheet; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Strings.Css; + +namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet; + +public class ExtractRichTextRulesController : StylesheetControllerBase +{ + private readonly IRichTextStylesheetService _richTextStylesheetService; + private readonly IUmbracoMapper _umbracoMapper; + + public ExtractRichTextRulesController( + IRichTextStylesheetService richTextStylesheetService, + IUmbracoMapper umbracoMapper) + { + _richTextStylesheetService = richTextStylesheetService; + _umbracoMapper = umbracoMapper; + } + + [HttpPost("rich-text/extract-rules")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(ExtractRichTextStylesheetRulesResponseModel), StatusCodes.Status200OK)] + public async Task ExtractRichTextRules(ExtractRichTextStylesheetRulesRequestModel requestModel) + { + RichTextStylesheetData? model = _umbracoMapper.Map(requestModel); + + IEnumerable rules = await _richTextStylesheetService.ExtractRichTextRules(model!); + + return Ok(new ExtractRichTextStylesheetRulesResponseModel + { + Rules = _umbracoMapper.MapEnumerable(rules), + }); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Folder/ByPathStylesheetFolderController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Folder/ByPathStylesheetFolderController.cs new file mode 100644 index 0000000000..789a844f8c --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Folder/ByPathStylesheetFolderController.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Folder; + +public class ByPathStylesheetFolderController : StylesheetFolderControllerBase +{ + public ByPathStylesheetFolderController(IUmbracoMapper mapper, IStylesheetFolderService stylesheetFolderService) : base(mapper, stylesheetFolderService) + { + } + + [HttpGet] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + public Task ByPath(string path) => GetFolderAsync(path); +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Folder/CreateStylesheetFolderController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Folder/CreateStylesheetFolderController.cs new file mode 100644 index 0000000000..dd97b3f0c7 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Folder/CreateStylesheetFolderController.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.Folder; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Folder; + +public class CreateStylesheetFolderController : StylesheetFolderControllerBase +{ + public CreateStylesheetFolderController(IUmbracoMapper mapper, IStylesheetFolderService stylesheetFolderService) : base(mapper, stylesheetFolderService) + { + } + + + [HttpPost] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + public Task Create(CreatePathFolderRequestModel model) => CreateAsync(model); +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Folder/DeleteStylesheetFolderController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Folder/DeleteStylesheetFolderController.cs new file mode 100644 index 0000000000..d5d0107606 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Folder/DeleteStylesheetFolderController.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Folder; + +public class DeleteStylesheetFolderController : StylesheetFolderControllerBase +{ + public DeleteStylesheetFolderController(IUmbracoMapper mapper, IStylesheetFolderService stylesheetFolderService) : base(mapper, stylesheetFolderService) + { + } + + [HttpDelete] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + public Task Delete(string path) => DeleteAsync(path); +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Folder/StylesheetFolderControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Folder/StylesheetFolderControllerBase.cs new file mode 100644 index 0000000000..943de371e9 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Folder/StylesheetFolderControllerBase.cs @@ -0,0 +1,62 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Common.Builders; +using Umbraco.Cms.Api.Management.Routing; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Folder; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.Stylesheet}/folder")] +[ApiExplorerSettings(GroupName = "Stylesheet")] +public class StylesheetFolderControllerBase : PathFolderManagementControllerBase +{ + private readonly IStylesheetFolderService _stylesheetFolderService; + + public StylesheetFolderControllerBase( + IUmbracoMapper mapper, + IStylesheetFolderService stylesheetFolderService) + : base(mapper) + { + _stylesheetFolderService = stylesheetFolderService; + } + + protected override Task GetContainerAsync(string path) => _stylesheetFolderService.GetAsync(path); + + protected override Task> CreateContainerAsync(PathContainer container) + => _stylesheetFolderService.CreateAsync(container); + + protected override Task> DeleteContainerAsync(string path) + => _stylesheetFolderService.DeleteAsync(path); + + protected override IActionResult OperationStatusResult(StylesheetFolderOperationStatus status) => + status switch + { + StylesheetFolderOperationStatus.AlreadyExists => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Folder already exists") + .WithDetail("The folder already exists") + .Build()), + StylesheetFolderOperationStatus.NotEmpty => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Not empty") + .WithDetail("The folder is not empty and can therefore not be deleted.") + .Build()), + StylesheetFolderOperationStatus.NotFound => NotFound(new ProblemDetailsBuilder() + .WithTitle("Not found") + .WithDetail("The specified folder was not found.") + .Build()), + StylesheetFolderOperationStatus.ParentNotFound => NotFound(new ProblemDetailsBuilder() + .WithTitle("Parent not found") + .WithDetail("The parent folder was not found.") + .Build()), + StylesheetFolderOperationStatus.InvalidName => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Invalid name") + .WithDetail("The name specified is not a valid name.") + .Build()), + _ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown stylesheet folder operation status") + }; +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/GetRichTextRulesByPath.cs b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/GetRichTextRulesByPath.cs new file mode 100644 index 0000000000..3df24e7652 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/GetRichTextRulesByPath.cs @@ -0,0 +1,42 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.RichTextStylesheet; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; +using StylesheetRule = Umbraco.Cms.Core.Strings.Css.StylesheetRule; + +namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet; + +public class GetRichTextRulesByPath : StylesheetControllerBase +{ + private readonly IRichTextStylesheetService _richTextStylesheetService; + private readonly IUmbracoMapper _umbracoMapper; + + public GetRichTextRulesByPath( + IRichTextStylesheetService richTextStylesheetService, + IUmbracoMapper umbracoMapper) + { + _richTextStylesheetService = richTextStylesheetService; + _umbracoMapper = umbracoMapper; + } + + [HttpGet("rich-text/rules")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(RichTextStylesheetRulesResponseModel), StatusCodes.Status200OK)] + public async Task GetByPath(string path) + { + Attempt, StylesheetOperationStatus> rulesAttempt = await _richTextStylesheetService.GetRulesByPathAsync(path); + + if (rulesAttempt.Success is false) + { + return StylesheetOperationStatusResult(rulesAttempt.Status); + } + + return Ok(new RichTextStylesheetRulesResponseModel + { + Rules = _umbracoMapper.MapEnumerable(rulesAttempt.Result) + }); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/InterpolateRichTextRulesController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/InterpolateRichTextRulesController.cs new file mode 100644 index 0000000000..ce8f0077d2 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/InterpolateRichTextRulesController.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.RichTextStylesheet; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet; + +public class InterpolateRichTextRulesController : StylesheetControllerBase +{ + private readonly IRichTextStylesheetService _richTextStylesheetService; + private readonly IUmbracoMapper _umbracoMapper; + + public InterpolateRichTextRulesController( + IRichTextStylesheetService richTextStylesheetService, + IUmbracoMapper umbracoMapper) + { + _richTextStylesheetService = richTextStylesheetService; + _umbracoMapper = umbracoMapper; + } + + [HttpPost("rich-text/interpolate-rules")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(InterpolateRichTextStylesheetResponseModel), StatusCodes.Status200OK)] + public async Task InterpolateRichTextRules(InterpolateRichTextStylesheetRequestModel requestModel) + { + RichTextStylesheetData? model = _umbracoMapper.Map(requestModel); + + var content = await _richTextStylesheetService.InterpolateRichTextRules(model!); + + return Ok(new InterpolateRichTextStylesheetResponseModel + { + Content = content, + }); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/StylesheetControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/StylesheetControllerBase.cs new file mode 100644 index 0000000000..8d5b6bd47f --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/StylesheetControllerBase.cs @@ -0,0 +1,50 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Common.Builders; +using Umbraco.Cms.Api.Management.Routing; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.Stylesheet}")] +[ApiExplorerSettings(GroupName = "Stylesheet")] +public class StylesheetControllerBase : ManagementApiControllerBase +{ + protected IActionResult StylesheetOperationStatusResult(StylesheetOperationStatus status) => + status switch + { + StylesheetOperationStatus.Success => Ok(), + StylesheetOperationStatus.AlreadyExists => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Stylesheet already exists") + .WithDetail("A stylesheet with the same path already exists") + .Build()), + StylesheetOperationStatus.CancelledByNotification => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Cancelled by notification") + .WithDetail("A stylesheet notification handler prevented the stylesheet operation.") + .Build()), + StylesheetOperationStatus.InvalidFileExtension => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Invalid file extension") + .WithDetail("The file extension is not valid for a stylesheet.") + .Build()), + StylesheetOperationStatus.ParentNotFound => NotFound(new ProblemDetailsBuilder() + .WithTitle("Parent not found") + .WithDetail("The parent folder was not found.") + .Build()), + StylesheetOperationStatus.PathTooLong => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Path too long") + .WithDetail("The file path is too long.") + .Build()), + StylesheetOperationStatus.NotFound => NotFound(new ProblemDetailsBuilder() + .WithTitle("Stylesheet not found") + .WithDetail("The stylesheet was not found.") + .Build()), + StylesheetOperationStatus.InvalidName => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Invalid name") + .WithDetail("The stylesheet name is invalid.") + .Build()), + _ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown script operation status"), + }; +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/UpdateStylesheetController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/UpdateStylesheetController.cs new file mode 100644 index 0000000000..4c6157ccd7 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/UpdateStylesheetController.cs @@ -0,0 +1,42 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.Stylesheet; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet; + +public class UpdateStylesheetController : StylesheetControllerBase +{ + private readonly IStylesheetService _stylesheetService; + private readonly IUmbracoMapper _umbracoMapper; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + + public UpdateStylesheetController( + IStylesheetService stylesheetService, + IUmbracoMapper umbracoMapper, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + { + _stylesheetService = stylesheetService; + _umbracoMapper = umbracoMapper; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + } + + [HttpPut] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task Update(UpdateStylesheetRequestModel requestModel) + { + StylesheetUpdateModel updateModel = _umbracoMapper.Map(requestModel)!; + + Attempt updateAttempt = await _stylesheetService.UpdateAsync(updateModel, CurrentUserKey(_backOfficeSecurityAccessor)); + + return updateAttempt.Success + ? Ok() + : StylesheetOperationStatusResult(updateAttempt.Status); + } +} diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/EntityBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/EntityBuilderExtensions.cs index 1c8bfce5c1..eac57897d7 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/EntityBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/EntityBuilderExtensions.cs @@ -8,7 +8,7 @@ namespace Umbraco.Cms.Api.Management.DependencyInjection; internal static class EntityBuilderExtensions { - internal static IUmbracoBuilder AddEntitys(this IUmbracoBuilder builder) + internal static IUmbracoBuilder AddEntities(this IUmbracoBuilder builder) { builder.WithCollectionBuilder() .Add(); diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/PartialViewBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/PartialViewBuilderExtensions.cs new file mode 100644 index 0000000000..fe67aba823 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/PartialViewBuilderExtensions.cs @@ -0,0 +1,16 @@ +using Umbraco.Cms.Api.Management.Mapping.PartialView; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Mapping; + +namespace Umbraco.Cms.Api.Management.DependencyInjection; + +internal static class PartialViewBuilderExtensions +{ + internal static IUmbracoBuilder AddPartialViews(this IUmbracoBuilder builder) + { + builder.WithCollectionBuilder() + .Add(); + + return builder; + } +} diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/PathFolderBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/PathFolderBuilderExtensions.cs new file mode 100644 index 0000000000..a50e3a4609 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/PathFolderBuilderExtensions.cs @@ -0,0 +1,16 @@ +using Umbraco.Cms.Api.Management.Mapping.Folder; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Mapping; + +namespace Umbraco.Cms.Api.Management.DependencyInjection; + +public static class PathFolderBuilderExtensions +{ + internal static IUmbracoBuilder AddPathFolders(this IUmbracoBuilder builder) + { + builder.WithCollectionBuilder() + .Add(); + + return builder; + } +} diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/ScriptBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/ScriptBuilderExtensions.cs new file mode 100644 index 0000000000..5fd8b9fa2c --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/ScriptBuilderExtensions.cs @@ -0,0 +1,16 @@ +using Umbraco.Cms.Api.Management.Mapping.Script; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Mapping; + +namespace Umbraco.Cms.Api.Management.DependencyInjection; + +internal static class ScriptBuilderExtensions +{ + internal static IUmbracoBuilder AddScripts(this IUmbracoBuilder builder) + { + builder.WithCollectionBuilder() + .Add(); + + return builder; + } +} diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/StylesheetBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/StylesheetBuilderExtensions.cs new file mode 100644 index 0000000000..12d6545c78 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/StylesheetBuilderExtensions.cs @@ -0,0 +1,16 @@ +using Umbraco.Cms.Api.Management.Mapping.Stylesheet; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Mapping; + +namespace Umbraco.Cms.Api.Management.DependencyInjection; + +internal static class StylesheetBuilderExtensions +{ + internal static IUmbracoBuilder AddStylesheets(this IUmbracoBuilder builder) + { + builder.WithCollectionBuilder() + .Add(); + + return builder; + } +} diff --git a/src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs b/src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs index 198e1a86e3..2edb694bd0 100644 --- a/src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs +++ b/src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs @@ -46,7 +46,11 @@ public class ManagementApiComposer : IComposer .AddUsers() .AddUserGroups() .AddPackages() - .AddEntitys() + .AddEntities() + .AddPathFolders() + .AddScripts() + .AddPartialViews() + .AddStylesheets() .AddBackOfficeAuthentication(); services diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Folder/PathFolderViewModelMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Folder/PathFolderViewModelMapDefinition.cs new file mode 100644 index 0000000000..0aae43ebc2 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Mapping/Folder/PathFolderViewModelMapDefinition.cs @@ -0,0 +1,28 @@ +using Umbraco.Cms.Api.Management.ViewModels.Folder; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Api.Management.Mapping.Folder; + +public class PathFolderViewModelMapDefinition : IMapDefinition +{ + public void DefineMaps(IUmbracoMapper mapper) + { + mapper.Define((_, _) => new PathFolderResponseModel(), Map); + mapper.Define((_, _) => new PathContainer { Name = string.Empty }, Map); + } + + // Umbraco.Code.MapAll + private void Map(CreatePathFolderRequestModel source, PathContainer target, MapperContext context) + { + target.Name = source.Name; + target.ParentPath = source.ParentPath; + } + + // Umbraco.Code.MapAll + private void Map(PathContainer source, PathFolderResponseModel target, MapperContext context) + { + target.Name = source.Name; + target.ParentPath = source.ParentPath; + } +} diff --git a/src/Umbraco.Cms.Api.Management/Mapping/PartialView/PartialViewViewModelsMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/PartialView/PartialViewViewModelsMapDefinition.cs new file mode 100644 index 0000000000..3f41e02b1b --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Mapping/PartialView/PartialViewViewModelsMapDefinition.cs @@ -0,0 +1,49 @@ +using Umbraco.Cms.Api.Management.ViewModels.PartialView; +using Umbraco.Cms.Api.Management.ViewModels.PartialView.Snippets; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Snippets; + +namespace Umbraco.Cms.Api.Management.Mapping.PartialView; + +public class PartialViewViewModelsMapDefinition : IMapDefinition +{ + public void DefineMaps(IUmbracoMapper mapper) + { + mapper.Define((_, _) => new PartialViewSnippetResponseModel { Name = string.Empty, Content = string.Empty }, Map); + mapper.Define((_, _) => new PartialViewResponseModel { Name = string.Empty, Path = string.Empty, Content = string.Empty }, Map); + mapper.Define((_, _) => new PartialViewCreateModel { Name = string.Empty }, Map); + mapper.Define((_, _) => new PartialViewUpdateModel { Content = string.Empty, ExistingPath = string.Empty, Name = string.Empty }, Map); + } + + // Umbraco.Code.MapAll + private void Map(UpdatePartialViewRequestModel source, PartialViewUpdateModel target, MapperContext context) + { + target.Name = source.Name; + target.Content = source.Content; + target.ExistingPath = source.ExistingPath; + } + + // Umbraco.Code.MapAll + private void Map(IPartialView source, PartialViewResponseModel target, MapperContext context) + { + target.Name = source.Name ?? string.Empty; + target.Content = source.Content ?? string.Empty; + target.Path = source.Path; + } + + // Umbraco.Code.MapAll + private void Map(CreatePartialViewRequestModel source, PartialViewCreateModel target, MapperContext context) + { + target.Name = source.Name; + target.Content = source.Content; + target.ParentPath = source.ParentPath; + } + + // Umbraco.Code.MapAll + private void Map(PartialViewSnippet source, PartialViewSnippetResponseModel target, MapperContext context) + { + target.Name = source.Name; + target.Content = source.Content; + } +} diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Script/ScriptViewModelsMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Script/ScriptViewModelsMapDefinition.cs new file mode 100644 index 0000000000..d6d6fb6fc8 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Mapping/Script/ScriptViewModelsMapDefinition.cs @@ -0,0 +1,39 @@ +using Umbraco.Cms.Api.Management.ViewModels.Script; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Api.Management.Mapping.Script; + +public class ScriptViewModelsMapDefinition : IMapDefinition +{ + public void DefineMaps(IUmbracoMapper mapper) + { + mapper.Define((_, _) => new ScriptCreateModel { Name = string.Empty }, Map); + mapper.Define((_, _) => new ScriptUpdateModel { Name = string.Empty, Content = string.Empty, ExistingPath = string.Empty }, Map); + mapper.Define((_, _) => new ScriptResponseModel { Name = string.Empty, Content = string.Empty }, Map); + } + + // Umbraco.Code.MapAll + private void Map(UpdateScriptRequestModel source, ScriptUpdateModel target, MapperContext context) + { + target.Name = source.Name; + target.Content = source.Content; + target.ExistingPath = source.ExistingPath; + } + + // Umbraco.Code.MapAll + private void Map(IScript source, ScriptResponseModel target, MapperContext context) + { + target.Name = source.Name ?? string.Empty; + target.Content = source.Content ?? string.Empty; + target.Path = source.Path; + } + + // Umbraco.Code.MapAll + private void Map(CreateScriptRequestModel source, ScriptCreateModel target, MapperContext context) + { + target.Name = source.Name; + target.ParentPath = source.ParentPath; + target.Content = source.Content; + } +} diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Stylesheet/StylesheetviewModelsMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Stylesheet/StylesheetviewModelsMapDefinition.cs new file mode 100644 index 0000000000..66272078ff --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Mapping/Stylesheet/StylesheetviewModelsMapDefinition.cs @@ -0,0 +1,74 @@ +using Umbraco.Cms.Api.Management.ViewModels.RichTextStylesheet; +using Umbraco.Cms.Api.Management.ViewModels.Stylesheet; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Strings.Css; + +namespace Umbraco.Cms.Api.Management.Mapping.Stylesheet; + +public class StylesheetViewModelsMapDefinition : IMapDefinition +{ + public void DefineMaps(IUmbracoMapper mapper) + { + mapper.Define((_, _) => new StylesheetResponseModel { Content = string.Empty, Name = string.Empty, Path = string.Empty }, Map); + mapper.Define((_, _) => new StylesheetCreateModel { Name = string.Empty }, Map); + mapper.Define((_, _) => new StylesheetUpdateModel { Content = string.Empty, Name = string.Empty, ExistingPath = string.Empty }, Map); + mapper.Define((_, _) => new RichTextStylesheetData(), Map); + mapper.Define((_, _) => new RichTextStylesheetData(), Map); + mapper.Define((_, _) => new RichTextRuleViewModel { Name = string.Empty, Selector = string.Empty, Styles = string.Empty }, Map); + mapper.Define((_, _) => new StylesheetOverviewResponseModel{ Name = string.Empty, Path = string.Empty }, Map); + } + + // Umbraco.Code.MapAll + private void Map(IStylesheet source, StylesheetOverviewResponseModel target, MapperContext context) + { + target.Name = source.Alias; + target.Path = source.Path; + } + + // Umbraco.Code.MapAll + private void Map(StylesheetRule source, RichTextRuleViewModel target, MapperContext context) + { + target.Name = source.Name; + target.Selector = source.Selector; + target.Styles = source.Styles; + } + + // Umbraco.Code.MapAll -Rules + private void Map(ExtractRichTextStylesheetRulesRequestModel source, RichTextStylesheetData target, MapperContext context) + => target.Content = source.Content; + + // Umbraco.Code.MapAll + private void Map(InterpolateRichTextStylesheetRequestModel source, RichTextStylesheetData target, MapperContext context) + { + target.Content = source.Content; + target.Rules = source.Rules?.Select(x => new StylesheetRule + { + Name = x.Name, Selector = x.Selector, Styles = x.Styles, + }).ToArray() ?? Array.Empty(); + } + + // Umbraco.Code.MapAll + private void Map(UpdateStylesheetRequestModel source, StylesheetUpdateModel target, MapperContext context) + { + target.Content = source.Content; + target.Name = source.Name; + target.ExistingPath = source.ExistingPath; + } + + // Umbraco.Code.MapAll + private void Map(CreateStylesheetRequestModel source, StylesheetCreateModel target, MapperContext context) + { + target.Content = source.Content; + target.Name = source.Name; + target.ParentPath = source.ParentPath; + } + + // Umbraco.Code.MapAll + private void Map(IStylesheet source, StylesheetResponseModel target, MapperContext context) + { + target.Name = source.Name ?? string.Empty; + target.Content = source.Content ?? string.Empty; + target.Path = source.Path; + } +} diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index a6e313dcac..9b3ea7659e 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -4710,6 +4710,183 @@ } } }, + "/umbraco/management/api/v1/partial-view": { + "get": { + "tags": [ + "Partial View" + ], + "operationId": "GetPartialView", + "parameters": [ + { + "name": "path", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/PartialViewResponseModel" + } + ] + } + } + } + } + } + }, + "post": { + "tags": [ + "Partial View" + ], + "operationId": "PostPartialView", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreatePartialViewRequestModel" + } + ] + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "headers": { + "Location": { + "description": "Location of the newly created resource", + "schema": { + "type": "string", + "description": "Location of the newly created resource", + "format": "uri" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Partial View" + ], + "operationId": "DeletePartialView", + "parameters": [ + { + "name": "path", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + }, + "put": { + "tags": [ + "Partial View" + ], + "operationId": "PutPartialView", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/UpdatePartialViewRequestModel" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/umbraco/management/api/v1/partial-view/folder": { + "post": { + "tags": [ + "Partial View" + ], + "operationId": "PostPartialViewFolder", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreatePathFolderRequestModel" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + }, + "delete": { + "tags": [ + "Partial View" + ], + "operationId": "DeletePartialViewFolder", + "parameters": [ + { + "name": "path", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + }, + "get": { + "tags": [ + "Partial View" + ], + "operationId": "GetPartialViewFolder", + "parameters": [ + { + "name": "path", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, "/umbraco/management/api/v1/partial-view/item": { "get": { "tags": [ @@ -4750,6 +4927,80 @@ } } }, + "/umbraco/management/api/v1/partial-view/snippet": { + "get": { + "tags": [ + "Partial View" + ], + "operationId": "GetPartialViewSnippet", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + } + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedSnippetItemResponseModel" + } + } + } + } + } + } + }, + "/umbraco/management/api/v1/partial-view/snippet/{name}": { + "get": { + "tags": [ + "Partial View" + ], + "operationId": "GetPartialViewSnippetByName", + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/PartialViewSnippetResponseModel" + } + ] + } + } + } + } + } + } + }, "/umbraco/management/api/v1/tree/partial-view/children": { "get": { "tags": [ @@ -5564,6 +5815,183 @@ } } }, + "/umbraco/management/api/v1/script": { + "get": { + "tags": [ + "Script" + ], + "operationId": "GetScript", + "parameters": [ + { + "name": "path", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ScriptResponseModel" + } + ] + } + } + } + } + } + }, + "post": { + "tags": [ + "Script" + ], + "operationId": "PostScript", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreateScriptRequestModel" + } + ] + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "headers": { + "Location": { + "description": "Location of the newly created resource", + "schema": { + "type": "string", + "description": "Location of the newly created resource", + "format": "uri" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Script" + ], + "operationId": "DeleteScript", + "parameters": [ + { + "name": "path", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + }, + "put": { + "tags": [ + "Script" + ], + "operationId": "PutScript", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/UpdateScriptRequestModel" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/umbraco/management/api/v1/script/folder": { + "get": { + "tags": [ + "Script" + ], + "operationId": "GetScriptFolder", + "parameters": [ + { + "name": "path", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + }, + "post": { + "tags": [ + "Script" + ], + "operationId": "PostScriptFolder", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreatePathFolderRequestModel" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + }, + "delete": { + "tags": [ + "Script" + ], + "operationId": "DeleteScriptFolder", + "parameters": [ + { + "name": "path", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, "/umbraco/management/api/v1/script/item": { "get": { "tags": [ @@ -6026,6 +6454,223 @@ } } }, + "/umbraco/management/api/v1/stylesheet": { + "get": { + "tags": [ + "Stylesheet" + ], + "operationId": "GetStylesheet", + "parameters": [ + { + "name": "path", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/StylesheetResponseModel" + } + ] + } + } + } + } + } + }, + "post": { + "tags": [ + "Stylesheet" + ], + "operationId": "PostStylesheet", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreateStylesheetRequestModel" + } + ] + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "headers": { + "Location": { + "description": "Location of the newly created resource", + "schema": { + "type": "string", + "description": "Location of the newly created resource", + "format": "uri" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Stylesheet" + ], + "operationId": "DeleteStylesheet", + "parameters": [ + { + "name": "path", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + }, + "put": { + "tags": [ + "Stylesheet" + ], + "operationId": "PutStylesheet", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/UpdateStylesheetRequestModel" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/umbraco/management/api/v1/stylesheet/all": { + "get": { + "tags": [ + "Stylesheet" + ], + "operationId": "GetStylesheetAll", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + } + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedStylesheetOverviewResponseModel" + } + } + } + } + } + } + }, + "/umbraco/management/api/v1/stylesheet/folder": { + "get": { + "tags": [ + "Stylesheet" + ], + "operationId": "GetStylesheetFolder", + "parameters": [ + { + "name": "path", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + }, + "post": { + "tags": [ + "Stylesheet" + ], + "operationId": "PostStylesheetFolder", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreatePathFolderRequestModel" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + }, + "delete": { + "tags": [ + "Stylesheet" + ], + "operationId": "DeleteStylesheetFolder", + "parameters": [ + { + "name": "path", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, "/umbraco/management/api/v1/stylesheet/item": { "get": { "tags": [ @@ -6066,6 +6711,116 @@ } } }, + "/umbraco/management/api/v1/stylesheet/rich-text/extract-rules": { + "post": { + "tags": [ + "Stylesheet" + ], + "operationId": "PostStylesheetRichTextExtractRules", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ExtractRichTextStylesheetRulesRequestModel" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ExtractRichTextStylesheetRulesResponseModel" + } + ] + } + } + } + } + } + } + }, + "/umbraco/management/api/v1/stylesheet/rich-text/interpolate-rules": { + "post": { + "tags": [ + "Stylesheet" + ], + "operationId": "PostStylesheetRichTextInterpolateRules", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/InterpolateRichTextStylesheetRequestModel" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/InterpolateRichTextStylesheetResponseModel" + } + ] + } + } + } + } + } + } + }, + "/umbraco/management/api/v1/stylesheet/rich-text/rules": { + "get": { + "tags": [ + "Stylesheet" + ], + "operationId": "GetStylesheetRichTextRules", + "parameters": [ + { + "name": "path", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/RichTextStylesheetRulesResponseModel" + }, + { + "$ref": "#/components/schemas/ExtractRichTextStylesheetRulesResponseModel" + } + ] + } + } + } + } + } + } + }, "/umbraco/management/api/v1/tree/stylesheet/children": { "get": { "tags": [ @@ -8517,6 +9272,30 @@ ], "additionalProperties": false }, + "CreatePartialViewRequestModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/CreateTextFileViewModelBaseModel" + } + ], + "additionalProperties": false + }, + "CreatePathFolderRequestModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/PathFolderModelBaseModel" + } + ], + "properties": { + "parentPath": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, "CreateRelationTypeRequestModel": { "type": "object", "allOf": [ @@ -8533,6 +9312,24 @@ }, "additionalProperties": false }, + "CreateScriptRequestModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/CreateTextFileViewModelBaseModel" + } + ], + "additionalProperties": false + }, + "CreateStylesheetRequestModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/CreateTextFileViewModelBaseModel" + } + ], + "additionalProperties": false + }, "CreateTemplateRequestModel": { "type": "object", "allOf": [ @@ -8542,6 +9339,21 @@ ], "additionalProperties": false }, + "CreateTextFileViewModelBaseModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/TextFileViewModelBaseModel" + } + ], + "properties": { + "parentPath": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, "CreateUserRequestModel": { "type": "object", "allOf": [ @@ -9285,6 +10097,24 @@ } } }, + "ExtractRichTextStylesheetRulesRequestModel": { + "type": "object", + "properties": { + "content": { + "type": "string" + } + }, + "additionalProperties": false + }, + "ExtractRichTextStylesheetRulesResponseModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/RichTextStylesheetRulesResponseModel" + } + ], + "additionalProperties": false + }, "FieldPresentationModel": { "type": "object", "properties": { @@ -9703,6 +10533,36 @@ }, "additionalProperties": false }, + "InterpolateRichTextStylesheetRequestModel": { + "type": "object", + "properties": { + "content": { + "type": "string", + "nullable": true + }, + "rules": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/RichTextRuleModel" + } + ] + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "InterpolateRichTextStylesheetResponseModel": { + "type": "object", + "properties": { + "content": { + "type": "string" + } + }, + "additionalProperties": false + }, "InviteUserRequestModel": { "type": "object", "allOf": [ @@ -11045,6 +11905,54 @@ }, "additionalProperties": false }, + "PagedSnippetItemResponseModel": { + "required": [ + "items", + "total" + ], + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "items": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/SnippetItemResponseModel" + } + ] + } + } + }, + "additionalProperties": false + }, + "PagedStylesheetOverviewResponseModel": { + "required": [ + "items", + "total" + ], + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "items": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/StylesheetOverviewResponseModel" + } + ] + } + } + }, + "additionalProperties": false + }, "PagedTagResponseModel": { "required": [ "items", @@ -11150,6 +12058,64 @@ ], "additionalProperties": false }, + "PartialViewResponseModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/TextFileResponseModelBaseModel" + } + ], + "additionalProperties": false + }, + "PartialViewSnippetResponseModel": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "content": { + "type": "string" + } + }, + "additionalProperties": false + }, + "PartialViewUpdateModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/TextFileUpdateModel" + } + ], + "additionalProperties": false + }, + "PathFolderModelBaseModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/FolderModelBaseModel" + } + ], + "additionalProperties": false + }, + "PathFolderResponseModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/FolderModelBaseModel" + } + ], + "properties": { + "parentPath": { + "type": "string", + "nullable": true + }, + "path": { + "type": "string", + "readOnly": true + } + }, + "additionalProperties": false + }, "ProblemDetailsModel": { "type": "object", "properties": { @@ -11531,6 +12497,37 @@ }, "additionalProperties": false }, + "RichTextRuleModel": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "selector": { + "type": "string" + }, + "styles": { + "type": "string" + } + }, + "additionalProperties": false + }, + "RichTextStylesheetRulesResponseModel": { + "type": "object", + "properties": { + "rules": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/RichTextRuleModel" + } + ] + } + } + }, + "additionalProperties": false + }, "RuntimeLevelModel": { "enum": [ "Unknown", @@ -11591,6 +12588,33 @@ ], "additionalProperties": false }, + "ScriptResponseModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/TextFileResponseModelBaseModel" + } + ], + "additionalProperties": false + }, + "ScriptUpdateModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/TextFileUpdateModel" + } + ], + "additionalProperties": false + }, + "ScriptViewModelBaseModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/TextFileViewModelBaseModel" + } + ], + "additionalProperties": false + }, "SearchResultResponseModel": { "type": "object", "properties": { @@ -11647,6 +12671,15 @@ }, "additionalProperties": false }, + "SnippetItemResponseModel": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, "StaticFileItemResponseModel": { "type": "object", "allOf": [ @@ -11675,6 +12708,36 @@ ], "additionalProperties": false }, + "StylesheetOverviewResponseModel": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "path": { + "type": "string" + } + }, + "additionalProperties": false + }, + "StylesheetResponseModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/TextFileResponseModelBaseModel" + } + ], + "additionalProperties": false + }, + "StylesheetUpdateModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/TextFileUpdateModel" + } + ], + "additionalProperties": false + }, "TagResponseModel": { "type": "object", "properties": { @@ -11985,6 +13048,47 @@ }, "additionalProperties": false }, + "TextFileResponseModelBaseModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/TextFileViewModelBaseModel" + } + ], + "properties": { + "path": { + "type": "string" + } + }, + "additionalProperties": false + }, + "TextFileUpdateModel": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "content": { + "type": "string" + }, + "existingPath": { + "type": "string" + } + }, + "additionalProperties": false + }, + "TextFileViewModelBaseModel": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "content": { + "type": "string" + } + }, + "additionalProperties": false + }, "TreeItemPresentationModel": { "type": "object", "properties": { @@ -12165,6 +13269,15 @@ }, "additionalProperties": false }, + "UpdatePartialViewRequestModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/TextFileUpdateModel" + } + ], + "additionalProperties": false + }, "UpdateRelationTypeRequestModel": { "type": "object", "allOf": [ @@ -12174,6 +13287,24 @@ ], "additionalProperties": false }, + "UpdateScriptRequestModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/UpdateTextFileViewModelBaseModel" + } + ], + "additionalProperties": false + }, + "UpdateStylesheetRequestModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/UpdateTextFileViewModelBaseModel" + } + ], + "additionalProperties": false + }, "UpdateTemplateRequestModel": { "type": "object", "allOf": [ @@ -12183,6 +13314,20 @@ ], "additionalProperties": false }, + "UpdateTextFileViewModelBaseModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/TextFileViewModelBaseModel" + } + ], + "properties": { + "existingPath": { + "type": "string" + } + }, + "additionalProperties": false + }, "UpdateUserGroupRequestModel": { "type": "object", "allOf": [ diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Folder/CreatePathFolderRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Folder/CreatePathFolderRequestModel.cs new file mode 100644 index 0000000000..b1d180fe7b --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Folder/CreatePathFolderRequestModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.Folder; + +public class CreatePathFolderRequestModel : PathFolderModelBase +{ + public string? ParentPath { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Folder/PathFolderModelBase.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Folder/PathFolderModelBase.cs new file mode 100644 index 0000000000..6ffd78a5de --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Folder/PathFolderModelBase.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.Folder; + +public class PathFolderModelBase : FolderModelBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Folder/PathFolderResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Folder/PathFolderResponseModel.cs new file mode 100644 index 0000000000..12d7993ea0 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Folder/PathFolderResponseModel.cs @@ -0,0 +1,11 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.Folder; + +public class PathFolderResponseModel : FolderModelBase +{ + public string? ParentPath { get; set; } + + public string Path => + string.IsNullOrEmpty(ParentPath) + ? Name + : System.IO.Path.Combine(ParentPath, Name); +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Item/SnippetItemResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Item/SnippetItemResponseModel.cs new file mode 100644 index 0000000000..997597051f --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Item/SnippetItemResponseModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.Item; + +public class SnippetItemResponseModel +{ + public required string Name { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/PartialView/CreatePartialViewRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/PartialView/CreatePartialViewRequestModel.cs new file mode 100644 index 0000000000..6400ca758b --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/PartialView/CreatePartialViewRequestModel.cs @@ -0,0 +1,8 @@ +using Umbraco.Cms.Api.Management.ViewModels.TextFiles; + +namespace Umbraco.Cms.Api.Management.ViewModels.PartialView; + +public class CreatePartialViewRequestModel : CreateTextFileViewModelBase +{ + +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/PartialView/PartialViewResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/PartialView/PartialViewResponseModel.cs new file mode 100644 index 0000000000..410a6af708 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/PartialView/PartialViewResponseModel.cs @@ -0,0 +1,7 @@ +using Umbraco.Cms.Api.Management.ViewModels.TextFiles; + +namespace Umbraco.Cms.Api.Management.ViewModels.PartialView; + +public class PartialViewResponseModel : TextFileResponseModelBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/PartialView/Snippets/PartialViewSnippetsViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/PartialView/Snippets/PartialViewSnippetsViewModel.cs new file mode 100644 index 0000000000..e8faa90f7e --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/PartialView/Snippets/PartialViewSnippetsViewModel.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.PartialView.Snippets; + +public class PartialViewSnippetResponseModel +{ + public required string Name { get; set; } + + public required string Content { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/PartialView/Snippets/SnippetItemResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/PartialView/Snippets/SnippetItemResponseModel.cs new file mode 100644 index 0000000000..f3e20bd179 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/PartialView/Snippets/SnippetItemResponseModel.cs @@ -0,0 +1,19 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Umbraco.Cms.Api.Management.ViewModels.PartialView.Snippets; + +public class SnippetItemResponseModel +{ + public SnippetItemResponseModel() + { + + } + + [SetsRequiredMembers] + public SnippetItemResponseModel(string name) + { + Name = name; + } + + public required string Name { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/PartialView/UpdatePartialViewRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/PartialView/UpdatePartialViewRequestModel.cs new file mode 100644 index 0000000000..4c42ebe0bf --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/PartialView/UpdatePartialViewRequestModel.cs @@ -0,0 +1,7 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Api.Management.ViewModels.PartialView; + +public class UpdatePartialViewRequestModel : TextFileUpdateModel +{ +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/RichTextStylesheet/ExtractRichTextStylesheetRulesRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/RichTextStylesheet/ExtractRichTextStylesheetRulesRequestModel.cs new file mode 100644 index 0000000000..47068d613d --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/RichTextStylesheet/ExtractRichTextStylesheetRulesRequestModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.RichTextStylesheet; + +public class ExtractRichTextStylesheetRulesRequestModel +{ + public required string Content { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/RichTextStylesheet/ExtractRichTextStylesheetRulesResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/RichTextStylesheet/ExtractRichTextStylesheetRulesResponseModel.cs new file mode 100644 index 0000000000..0444544e5d --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/RichTextStylesheet/ExtractRichTextStylesheetRulesResponseModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.RichTextStylesheet; + +public class ExtractRichTextStylesheetRulesResponseModel : RichTextStylesheetRulesResponseModel +{ + +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/RichTextStylesheet/InterpolateRichTextStylesheetRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/RichTextStylesheet/InterpolateRichTextStylesheetRequestModel.cs new file mode 100644 index 0000000000..b9831cd605 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/RichTextStylesheet/InterpolateRichTextStylesheetRequestModel.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.RichTextStylesheet; + +public class InterpolateRichTextStylesheetRequestModel +{ + public string? Content { get; set; } + + public IEnumerable? Rules { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/RichTextStylesheet/InterpolateRichTextStylesheetResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/RichTextStylesheet/InterpolateRichTextStylesheetResponseModel.cs new file mode 100644 index 0000000000..b0a18d652d --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/RichTextStylesheet/InterpolateRichTextStylesheetResponseModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.RichTextStylesheet; + +public class InterpolateRichTextStylesheetResponseModel +{ + public required string Content { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/RichTextStylesheet/RichTextRuleViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/RichTextStylesheet/RichTextRuleViewModel.cs new file mode 100644 index 0000000000..c23b12c199 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/RichTextStylesheet/RichTextRuleViewModel.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.RichTextStylesheet; + +public class RichTextRuleViewModel +{ + public required string Name { get; set; } + + public required string Selector { get; set; } + + public required string Styles { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/RichTextStylesheet/RichTextStylesheetRulesResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/RichTextStylesheet/RichTextStylesheetRulesResponseModel.cs new file mode 100644 index 0000000000..01cd01d8ca --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/RichTextStylesheet/RichTextStylesheetRulesResponseModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.RichTextStylesheet; + +public class RichTextStylesheetRulesResponseModel +{ + public required IEnumerable Rules { get; set; } = Enumerable.Empty(); +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Script/CreateScriptRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Script/CreateScriptRequestModel.cs new file mode 100644 index 0000000000..86d1ddbfb3 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Script/CreateScriptRequestModel.cs @@ -0,0 +1,8 @@ +using Umbraco.Cms.Api.Management.ViewModels.TextFiles; + +namespace Umbraco.Cms.Api.Management.ViewModels.Script; + +public class CreateScriptRequestModel : CreateTextFileViewModelBase +{ + +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Script/ScriptResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Script/ScriptResponseModel.cs new file mode 100644 index 0000000000..d200b36908 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Script/ScriptResponseModel.cs @@ -0,0 +1,7 @@ +using Umbraco.Cms.Api.Management.ViewModels.TextFiles; + +namespace Umbraco.Cms.Api.Management.ViewModels.Script; + +public class ScriptResponseModel : TextFileResponseModelBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Script/ScriptViewModelBase.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Script/ScriptViewModelBase.cs new file mode 100644 index 0000000000..32bebb8d5f --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Script/ScriptViewModelBase.cs @@ -0,0 +1,7 @@ +using Umbraco.Cms.Api.Management.ViewModels.TextFiles; + +namespace Umbraco.Cms.Api.Management.ViewModels.Script; + +public class ScriptViewModelBase : TextFileViewModelBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Script/UpdateScriptRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Script/UpdateScriptRequestModel.cs new file mode 100644 index 0000000000..8ff724bf37 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Script/UpdateScriptRequestModel.cs @@ -0,0 +1,7 @@ +using Umbraco.Cms.Api.Management.ViewModels.TextFiles; + +namespace Umbraco.Cms.Api.Management.ViewModels.Script; + +public class UpdateScriptRequestModel : UpdateTextFileViewModelBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Stylesheet/CreateStylesheetRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Stylesheet/CreateStylesheetRequestModel.cs new file mode 100644 index 0000000000..59c459f644 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Stylesheet/CreateStylesheetRequestModel.cs @@ -0,0 +1,8 @@ +using Umbraco.Cms.Api.Management.ViewModels.TextFiles; + +namespace Umbraco.Cms.Api.Management.ViewModels.Stylesheet; + +public class CreateStylesheetRequestModel : CreateTextFileViewModelBase +{ + +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Stylesheet/StylesheetOverviewResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Stylesheet/StylesheetOverviewResponseModel.cs new file mode 100644 index 0000000000..cd30066c8f --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Stylesheet/StylesheetOverviewResponseModel.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.Stylesheet; + +public class StylesheetOverviewResponseModel +{ + public required string Name { get; set; } + + public required string Path { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Stylesheet/StylesheetResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Stylesheet/StylesheetResponseModel.cs new file mode 100644 index 0000000000..908423a210 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Stylesheet/StylesheetResponseModel.cs @@ -0,0 +1,7 @@ +using Umbraco.Cms.Api.Management.ViewModels.TextFiles; + +namespace Umbraco.Cms.Api.Management.ViewModels.Stylesheet; + +public class StylesheetResponseModel : TextFileResponseModelBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Stylesheet/UpdateStylesheetRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Stylesheet/UpdateStylesheetRequestModel.cs new file mode 100644 index 0000000000..117ea48af5 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Stylesheet/UpdateStylesheetRequestModel.cs @@ -0,0 +1,7 @@ +using Umbraco.Cms.Api.Management.ViewModels.TextFiles; + +namespace Umbraco.Cms.Api.Management.ViewModels.Stylesheet; + +public class UpdateStylesheetRequestModel : UpdateTextFileViewModelBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/TextFiles/CreateTextFileViewModelBase.cs b/src/Umbraco.Cms.Api.Management/ViewModels/TextFiles/CreateTextFileViewModelBase.cs new file mode 100644 index 0000000000..53b50f9a95 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/TextFiles/CreateTextFileViewModelBase.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.TextFiles; + +public class CreateTextFileViewModelBase : TextFileViewModelBase +{ + public string? ParentPath { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/TextFiles/TextFileResponseModelBase.cs b/src/Umbraco.Cms.Api.Management/ViewModels/TextFiles/TextFileResponseModelBase.cs new file mode 100644 index 0000000000..f7e646f5a4 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/TextFiles/TextFileResponseModelBase.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.TextFiles; + +public class TextFileResponseModelBase : TextFileViewModelBase +{ + public string Path { get; set; } = string.Empty; +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/TextFiles/TextFileViewModelBase.cs b/src/Umbraco.Cms.Api.Management/ViewModels/TextFiles/TextFileViewModelBase.cs new file mode 100644 index 0000000000..708950a8f7 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/TextFiles/TextFileViewModelBase.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.TextFiles; + +public class TextFileViewModelBase +{ + public required string Name { get; set; } + + public required string Content { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/TextFiles/UpdateTextFileViewModelBase.cs b/src/Umbraco.Cms.Api.Management/ViewModels/TextFiles/UpdateTextFileViewModelBase.cs new file mode 100644 index 0000000000..28871ad6f6 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/TextFiles/UpdateTextFileViewModelBase.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.TextFiles; + +public class UpdateTextFileViewModelBase : TextFileViewModelBase +{ + public required string ExistingPath { get; set; } + +} diff --git a/src/Umbraco.Core/CompatibilitySuppressions.xml b/src/Umbraco.Core/CompatibilitySuppressions.xml index 8ebf513b60..c78386e04b 100644 --- a/src/Umbraco.Core/CompatibilitySuppressions.xml +++ b/src/Umbraco.Core/CompatibilitySuppressions.xml @@ -1,5 +1,4 @@  - CP0001 @@ -918,6 +917,20 @@ lib/net7.0/Umbraco.Core.dll true + + CP0006 + M:Umbraco.Cms.Core.Persistence.Repositories.IFileWithFoldersRepository.FolderExists(System.String) + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + + + CP0006 + M:Umbraco.Cms.Core.Persistence.Repositories.IFileWithFoldersRepository.FolderHasContent(System.String) + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + CP0006 M:Umbraco.Cms.Core.Persistence.Repositories.IPropertyTypeUsageRepository.ContentTypeExistAsync(System.Guid) diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index a269e4681f..48c474794e 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -306,6 +306,13 @@ namespace Umbraco.Cms.Core.DependencyInjection Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); diff --git a/src/Umbraco.Core/Models/PartialViewCreateModel.cs b/src/Umbraco.Core/Models/PartialViewCreateModel.cs new file mode 100644 index 0000000000..2b7bfe9417 --- /dev/null +++ b/src/Umbraco.Core/Models/PartialViewCreateModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Core.Models; + +public class PartialViewCreateModel : TextFileCreateModel +{ + +} diff --git a/src/Umbraco.Core/Models/PartialViewUpdateModel.cs b/src/Umbraco.Core/Models/PartialViewUpdateModel.cs new file mode 100644 index 0000000000..29ac501843 --- /dev/null +++ b/src/Umbraco.Core/Models/PartialViewUpdateModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Core.Models; + +public class PartialViewUpdateModel : TextFileUpdateModel +{ + +} diff --git a/src/Umbraco.Core/Models/PathContainer.cs b/src/Umbraco.Core/Models/PathContainer.cs new file mode 100644 index 0000000000..8a9a857077 --- /dev/null +++ b/src/Umbraco.Core/Models/PathContainer.cs @@ -0,0 +1,25 @@ +namespace Umbraco.Cms.Core.Models; + +/// +/// A container using a path as its identity. +/// +public class PathContainer +{ + /// + /// The name of the container + /// + public required string Name { get; set; } + + /// + /// The path to the parent of the container + /// + public string? ParentPath { get; set; } + + /// + /// The path of the container. + /// + public string Path => + string.IsNullOrEmpty(ParentPath) + ? Name + : System.IO.Path.Combine(ParentPath, Name); +} diff --git a/src/Umbraco.Core/Models/RichTextStylesheetData.cs b/src/Umbraco.Core/Models/RichTextStylesheetData.cs new file mode 100644 index 0000000000..58bfa4ec4a --- /dev/null +++ b/src/Umbraco.Core/Models/RichTextStylesheetData.cs @@ -0,0 +1,10 @@ +using Umbraco.Cms.Core.Strings.Css; + +namespace Umbraco.Cms.Core.Models; + +public class RichTextStylesheetData +{ + public string? Content { get; set; } + + public StylesheetRule[] Rules { get; set; } = Array.Empty(); +} diff --git a/src/Umbraco.Core/Models/ScriptCreateModel.cs b/src/Umbraco.Core/Models/ScriptCreateModel.cs new file mode 100644 index 0000000000..7e7f2a7c3b --- /dev/null +++ b/src/Umbraco.Core/Models/ScriptCreateModel.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Core.Models; + +public class ScriptCreateModel : TextFileCreateModel +{ +} diff --git a/src/Umbraco.Core/Models/ScriptUpdateModel.cs b/src/Umbraco.Core/Models/ScriptUpdateModel.cs new file mode 100644 index 0000000000..44dc8cdcb8 --- /dev/null +++ b/src/Umbraco.Core/Models/ScriptUpdateModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Core.Models; + +public class ScriptUpdateModel : TextFileUpdateModel +{ + +} diff --git a/src/Umbraco.Core/Models/StylesheetCreateModel.cs b/src/Umbraco.Core/Models/StylesheetCreateModel.cs new file mode 100644 index 0000000000..daac696958 --- /dev/null +++ b/src/Umbraco.Core/Models/StylesheetCreateModel.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Core.Models; + +public class StylesheetCreateModel : TextFileCreateModel +{ +} diff --git a/src/Umbraco.Core/Models/StylesheetUpdateModel.cs b/src/Umbraco.Core/Models/StylesheetUpdateModel.cs new file mode 100644 index 0000000000..84aead8f0f --- /dev/null +++ b/src/Umbraco.Core/Models/StylesheetUpdateModel.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Core.Models; + +public class StylesheetUpdateModel : TextFileUpdateModel +{ +} diff --git a/src/Umbraco.Core/Models/TextFileCreateModel.cs b/src/Umbraco.Core/Models/TextFileCreateModel.cs new file mode 100644 index 0000000000..8d05f6aa62 --- /dev/null +++ b/src/Umbraco.Core/Models/TextFileCreateModel.cs @@ -0,0 +1,15 @@ +namespace Umbraco.Cms.Core.Models; + +public class TextFileCreateModel +{ + public required string Name { get; set; } + + public string? ParentPath { get; set; } + + public string? Content { get; set; } + + public string FilePath => + ParentPath is null + ? Name + : Path.Combine(ParentPath, Name); +} diff --git a/src/Umbraco.Core/Models/TextFileUpdateModel.cs b/src/Umbraco.Core/Models/TextFileUpdateModel.cs new file mode 100644 index 0000000000..2b9c07aa87 --- /dev/null +++ b/src/Umbraco.Core/Models/TextFileUpdateModel.cs @@ -0,0 +1,19 @@ +namespace Umbraco.Cms.Core.Models; + +public class TextFileUpdateModel +{ + /// + /// The new name of the file. + /// + public required string Name { get; set; } + + /// + /// The new content of the file. + /// + public required string Content { get; set; } + + /// + /// The path of the file to update. + /// + public required string ExistingPath { get; set; } +} diff --git a/src/Umbraco.Core/Persistence/Repositories/IFileWithFoldersRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IFileWithFoldersRepository.cs index 9914e49b26..7f2916fb85 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IFileWithFoldersRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IFileWithFoldersRepository.cs @@ -5,4 +5,8 @@ public interface IFileWithFoldersRepository void AddFolder(string folderPath); void DeleteFolder(string folderPath); + + bool FolderExists(string folderPath); + + bool FolderHasContent(string folderPath); } diff --git a/src/Umbraco.Core/Services/FileService.cs b/src/Umbraco.Core/Services/FileService.cs index 01a0521050..021deed4f5 100644 --- a/src/Umbraco.Core/Services/FileService.cs +++ b/src/Umbraco.Core/Services/FileService.cs @@ -101,6 +101,7 @@ public class FileService : RepositoryService, IFileService #region Stylesheets /// + [Obsolete("Please use IStylesheetService for stylesheet operations - will be removed in Umbraco 15")] public IEnumerable GetStylesheets(params string[] paths) { using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) @@ -113,6 +114,7 @@ public class FileService : RepositoryService, IFileService _auditRepository.Save(new AuditItem(objectId, type, userId, entityType)); /// + [Obsolete("Please use IStylesheetService for stylesheet operations - will be removed in Umbraco 15")] public IStylesheet? GetStylesheet(string? path) { using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) @@ -122,6 +124,7 @@ public class FileService : RepositoryService, IFileService } /// + [Obsolete("Please use IStylesheetService for stylesheet operations - will be removed in Umbraco 15")] public void SaveStylesheet(IStylesheet? stylesheet, int? userId = null) { if (stylesheet is null) @@ -150,6 +153,7 @@ public class FileService : RepositoryService, IFileService } /// + [Obsolete("Please use IStylesheetService for stylesheet operations - will be removed in Umbraco 15")] public void DeleteStylesheet(string path, int? userId) { using (ICoreScope scope = ScopeProvider.CreateCoreScope()) @@ -181,6 +185,7 @@ public class FileService : RepositoryService, IFileService } /// + [Obsolete("Please use IStylesheetService for stylesheet operations - will be removed in Umbraco 15")] public void CreateStyleSheetFolder(string folderPath) { using (ICoreScope scope = ScopeProvider.CreateCoreScope()) @@ -191,6 +196,7 @@ public class FileService : RepositoryService, IFileService } /// + [Obsolete("Please use IStylesheetFolderService for stylesheet folder operations - will be removed in Umbraco 15")] public void DeleteStyleSheetFolder(string folderPath) { using (ICoreScope scope = ScopeProvider.CreateCoreScope()) @@ -201,6 +207,7 @@ public class FileService : RepositoryService, IFileService } /// + [Obsolete("Please use IStylesheetService for stylesheet operations - will be removed in Umbraco 15")] public Stream GetStylesheetFileContentStream(string filepath) { using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) @@ -210,6 +217,7 @@ public class FileService : RepositoryService, IFileService } /// + [Obsolete("Please use IStylesheetService for stylesheet operations - will be removed in Umbraco 15")] public void SetStylesheetFileContent(string filepath, Stream content) { using (ICoreScope scope = ScopeProvider.CreateCoreScope()) @@ -220,6 +228,7 @@ public class FileService : RepositoryService, IFileService } /// + [Obsolete("Please use IStylesheetService for stylesheet operations - will be removed in Umbraco 15")] public long GetStylesheetFileSize(string filepath) { using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) @@ -233,6 +242,7 @@ public class FileService : RepositoryService, IFileService #region Scripts /// + [Obsolete("Please use IScriptService for script operations - will be removed in Umbraco 15")] public IEnumerable GetScripts(params string[] names) { using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) @@ -242,6 +252,7 @@ public class FileService : RepositoryService, IFileService } /// + [Obsolete("Please use IScriptService for script operations - will be removed in Umbraco 15")] public IScript? GetScript(string? name) { using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) @@ -251,6 +262,7 @@ public class FileService : RepositoryService, IFileService } /// + [Obsolete("Please use IScriptService for script operations - will be removed in Umbraco 15")] public void SaveScript(IScript? script, int? userId) { if (userId is null) @@ -283,6 +295,7 @@ public class FileService : RepositoryService, IFileService } /// + [Obsolete("Please use IScriptService for script operations - will be removed in Umbraco 15")] public void DeleteScript(string path, int? userId = null) { using (ICoreScope scope = ScopeProvider.CreateCoreScope()) @@ -313,6 +326,7 @@ public class FileService : RepositoryService, IFileService } /// + [Obsolete("Please use IScriptFolderService for script folder operations - will be removed in Umbraco 15")] public void CreateScriptFolder(string folderPath) { using (ICoreScope scope = ScopeProvider.CreateCoreScope()) @@ -323,6 +337,7 @@ public class FileService : RepositoryService, IFileService } /// + [Obsolete("Please use IScriptFolderService for script folder operations - will be removed in Umbraco 15")] public void DeleteScriptFolder(string folderPath) { using (ICoreScope scope = ScopeProvider.CreateCoreScope()) @@ -333,6 +348,7 @@ public class FileService : RepositoryService, IFileService } /// + [Obsolete("Please use IScriptService for script operations - will be removed in Umbraco 15")] public Stream GetScriptFileContentStream(string filepath) { using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) @@ -342,6 +358,7 @@ public class FileService : RepositoryService, IFileService } /// + [Obsolete("Please use IScriptService for script operations - will be removed in Umbraco 15")] public void SetScriptFileContent(string filepath, Stream content) { using (ICoreScope scope = ScopeProvider.CreateCoreScope()) @@ -352,6 +369,7 @@ public class FileService : RepositoryService, IFileService } /// + [Obsolete("Please use IScriptService for script operations - will be removed in Umbraco 15")] public long GetScriptFileSize(string filepath) { using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) @@ -571,6 +589,7 @@ public class FileService : RepositoryService, IFileService #region Partial Views + [Obsolete("Please use IPartialViewService for partial view operations - will be removed in Umbraco 15")] public IEnumerable GetPartialViewSnippetNames(params string[] filterNames) { var snippetProvider = @@ -590,6 +609,7 @@ public class FileService : RepositoryService, IFileService return empty.Union(files.Except(empty)).WhereNotNull(); } + [Obsolete("Please use IPartialViewFolderService for partial view folder operations - will be removed in Umbraco 15")] public void DeletePartialViewFolder(string folderPath) { using (ICoreScope scope = ScopeProvider.CreateCoreScope()) @@ -608,6 +628,7 @@ public class FileService : RepositoryService, IFileService } } + [Obsolete("Please use IPartialViewService for partial view operations - will be removed in Umbraco 15")] public IEnumerable GetPartialViews(params string[] names) { using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) @@ -616,6 +637,7 @@ public class FileService : RepositoryService, IFileService } } + [Obsolete("Please use IPartialViewService for partial view operations - will be removed in Umbraco 15")] public IPartialView? GetPartialView(string path) { using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) @@ -632,12 +654,14 @@ public class FileService : RepositoryService, IFileService } } + [Obsolete("Please use IPartialViewService for partial view operations - will be removed in Umbraco 15")] public Attempt CreatePartialView(IPartialView partialView, string? snippetName = null, int? userId = Constants.Security.SuperUserId) => CreatePartialViewMacro(partialView, PartialViewType.PartialView, snippetName, userId); public Attempt CreatePartialViewMacro(IPartialView partialView, string? snippetName = null, int? userId = Constants.Security.SuperUserId) => CreatePartialViewMacro(partialView, PartialViewType.PartialViewMacro, snippetName, userId); + [Obsolete("Please use IPartialViewService for partial view operations - will be removed in Umbraco 15")] public bool DeletePartialView(string path, int? userId = null) => DeletePartialViewMacro(path, PartialViewType.PartialView, userId); @@ -719,6 +743,7 @@ public class FileService : RepositoryService, IFileService public bool DeletePartialViewMacro(string path, int? userId = null) => DeletePartialViewMacro(path, PartialViewType.PartialViewMacro, userId); + [Obsolete("Please use IPartialViewService for partial view operations - will be removed in Umbraco 15")] public Attempt SavePartialView(IPartialView partialView, int? userId = null) => SavePartialView(partialView, PartialViewType.PartialView, userId); @@ -757,6 +782,7 @@ public class FileService : RepositoryService, IFileService public Attempt SavePartialViewMacro(IPartialView partialView, int? userId = null) => SavePartialView(partialView, PartialViewType.PartialViewMacro, userId); + [Obsolete("Please use IPartialViewFolderService for partial view folder operations - will be removed in Umbraco 15")] public void CreatePartialViewFolder(string folderPath) { using (ICoreScope scope = ScopeProvider.CreateCoreScope()) @@ -772,6 +798,7 @@ public class FileService : RepositoryService, IFileService return headerMatch.Replace(contents, string.Empty); } + [Obsolete("Please use IPartialViewService for partial view operations - will be removed in Umbraco 15")] private Attempt SavePartialView(IPartialView partialView, PartialViewType partialViewType, int? userId = null) { using (ICoreScope scope = ScopeProvider.CreateCoreScope()) @@ -823,6 +850,7 @@ public class FileService : RepositoryService, IFileService } /// + [Obsolete("Please use IPartialViewService for partial view operations - will be removed in Umbraco 15")] public Stream GetPartialViewFileContentStream(string filepath) { using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) @@ -845,6 +873,7 @@ public class FileService : RepositoryService, IFileService } /// + [Obsolete("Please use IPartialViewService for partial view operations - will be removed in Umbraco 15")] public void SetPartialViewFileContent(string filepath, Stream content) { using (ICoreScope scope = ScopeProvider.CreateCoreScope()) @@ -855,6 +884,7 @@ public class FileService : RepositoryService, IFileService } /// + [Obsolete("Please use IPartialViewService for partial view operations - will be removed in Umbraco 15")] public long GetPartialViewFileSize(string filepath) { using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) diff --git a/src/Umbraco.Core/Services/FileServiceBase.cs b/src/Umbraco.Core/Services/FileServiceBase.cs new file mode 100644 index 0000000000..7860c29f70 --- /dev/null +++ b/src/Umbraco.Core/Services/FileServiceBase.cs @@ -0,0 +1,77 @@ +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Persistence; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Services; + +public abstract class FileServiceBase : RepositoryService, IBasicFileService + where TRepository : IFileRepository, IReadRepository + where TEntity : IFile +{ + public TRepository Repository { get; } + + public FileServiceBase( + ICoreScopeProvider provider, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + TRepository repository) + : base(provider, loggerFactory, eventMessagesFactory) + { + Repository = repository; + } + + protected abstract string[] AllowedFileExtensions { get; } + + protected virtual bool HasValidFileExtension(string fileName) + => AllowedFileExtensions.Contains(Path.GetExtension(fileName)); + + protected virtual bool HasValidFileName(string fileName) + { + if (fileName.ContainsAny(Path.GetInvalidFileNameChars())) + { + return false; + } + + return true; + } + + /// + public Task GetAsync(string path) + { + using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true); + return Task.FromResult(Repository.Get(path)); + } + + /// + public Task> GetAllAsync(params string[] paths) + { + using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true); + return Task.FromResult(Repository.GetMany(paths)); + } + + /// + public Task GetContentStreamAsync(string path) + { + using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true); + return Task.FromResult(Repository.GetFileContentStream(path)); + } + + /// + public Task SetContentStreamAsync(string path, Stream content) + { + using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true); + Repository.SetFileContent(path, content); + return Task.CompletedTask; + } + + /// + public Task GetFileSizeAsync(string path) + { + using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true); + return Task.FromResult(Repository.GetFileSize(path)); + } +} diff --git a/src/Umbraco.Core/Services/IBasicFileService.cs b/src/Umbraco.Core/Services/IBasicFileService.cs new file mode 100644 index 0000000000..018408e159 --- /dev/null +++ b/src/Umbraco.Core/Services/IBasicFileService.cs @@ -0,0 +1,41 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Services; + +public interface IBasicFileService where TEntity : IFile +{ + /// + /// Gets by path. + /// + /// The path to get from. + /// , or null if not found + Task GetAsync(string path); + + /// + /// Gets all by path, or all if no paths are specified. + /// + /// Optional paths of to get. + /// IEnumerable of + Task> GetAllAsync(params string[] paths); + + /// + /// Get the content of a file as a stream. + /// + /// The path to the file. + /// A stream containing the contents of the file. + Task GetContentStreamAsync(string path); + + /// + /// Set the content of a file from a stream. + /// + /// The path to the file. + /// The desired content of the file as a stream. + Task SetContentStreamAsync(string path, Stream content); + + /// + /// + /// + /// + /// + Task GetFileSizeAsync(string path); +} diff --git a/src/Umbraco.Core/Services/IFileService.cs b/src/Umbraco.Core/Services/IFileService.cs index 53d7d004b2..8bf4e0a605 100644 --- a/src/Umbraco.Core/Services/IFileService.cs +++ b/src/Umbraco.Core/Services/IFileService.cs @@ -11,10 +11,12 @@ public interface IFileService : IService [Obsolete("Please use SnippetCollection.GetPartialViewSnippetNames() or SnippetCollection.GetPartialViewMacroSnippetNames() instead. Scheduled for removal in V12.")] IEnumerable GetPartialViewSnippetNames(params string[] filterNames); + [Obsolete("Please use IPartialViewFolderService for partial view folder operations - will be removed in Umbraco 15")] void CreatePartialViewFolder(string folderPath); void CreatePartialViewMacroFolder(string folderPath); + [Obsolete("Please use IPartialViewFolderService for partial view folder operations - will be removed in Umbraco 15")] void DeletePartialViewFolder(string folderPath); void DeletePartialViewMacroFolder(string folderPath); @@ -23,20 +25,25 @@ public interface IFileService : IService /// Gets a list of all objects /// /// An enumerable list of objects + [Obsolete("Please use IPartialViewService for partial view operations - will be removed in Umbraco 15")] IEnumerable GetPartialViews(params string[] names); + [Obsolete("Please use IPartialViewService for partial view operations - will be removed in Umbraco 15")] IPartialView? GetPartialView(string path); IPartialView? GetPartialViewMacro(string path); + [Obsolete("Please use IPartialViewService for partial view operations - will be removed in Umbraco 15")] Attempt CreatePartialView(IPartialView partialView, string? snippetName = null, int? userId = Constants.Security.SuperUserId); Attempt CreatePartialViewMacro(IPartialView partialView, string? snippetName = null, int? userId = Constants.Security.SuperUserId); + [Obsolete("Please use IPartialViewService for partial view operations - will be removed in Umbraco 15")] bool DeletePartialView(string path, int? userId = null); bool DeletePartialViewMacro(string path, int? userId = null); + [Obsolete("Please use IPartialViewService for partial view operations - will be removed in Umbraco 15")] Attempt SavePartialView(IPartialView partialView, int? userId = null); Attempt SavePartialViewMacro(IPartialView partialView, int? userId = null); @@ -46,6 +53,7 @@ public interface IFileService : IService /// /// The filesystem path to the partial view. /// The content of the partial view. + [Obsolete("Please use IPartialViewService for partial view operations - will be removed in Umbraco 15")] Stream GetPartialViewFileContentStream(string filepath); /// @@ -53,6 +61,7 @@ public interface IFileService : IService /// /// The filesystem path to the partial view. /// The content of the partial view. + [Obsolete("Please use IPartialViewService for partial view operations - will be removed in Umbraco 15")] void SetPartialViewFileContent(string filepath, Stream content); /// @@ -60,6 +69,7 @@ public interface IFileService : IService /// /// The filesystem path to the partial view. /// The size of the partial view. + [Obsolete("Please use IPartialViewService for partial view operations - will be removed in Umbraco 15")] long GetPartialViewFileSize(string filepath); /// @@ -87,6 +97,7 @@ public interface IFileService : IService /// Gets a list of all objects /// /// An enumerable list of objects + [Obsolete("Please use IStylesheetService for stylesheet operations - will be removed in Umbraco 15")] IEnumerable GetStylesheets(params string[] paths); /// @@ -94,6 +105,7 @@ public interface IFileService : IService /// /// Path of the stylesheet incl. extension /// A object + [Obsolete("Please use IStylesheetService for stylesheet operations - will be removed in Umbraco 15")] IStylesheet? GetStylesheet(string? path); /// @@ -101,6 +113,7 @@ public interface IFileService : IService /// /// to save /// Optional id of the user saving the stylesheet + [Obsolete("Please use IStylesheetService for stylesheet operations - will be removed in Umbraco 15")] void SaveStylesheet(IStylesheet? stylesheet, int? userId = null); /// @@ -108,6 +121,7 @@ public interface IFileService : IService /// /// Name incl. extension of the Stylesheet to delete /// Optional id of the user deleting the stylesheet + [Obsolete("Please use IStylesheetService for stylesheet operations - will be removed in Umbraco 15")] void DeleteStylesheet(string path, int? userId = null); /// @@ -115,12 +129,14 @@ public interface IFileService : IService /// /// /// + [Obsolete("Please use IStylesheetFolderService for stylesheet folder operations - will be removed in Umbraco 15")] void CreateStyleSheetFolder(string folderPath); /// /// Deletes a folder for style sheets /// /// + [Obsolete("Please use IStylesheetFolderService for stylesheet folder operations - will be removed in Umbraco 15")] void DeleteStyleSheetFolder(string folderPath); /// @@ -128,6 +144,7 @@ public interface IFileService : IService /// /// The filesystem path to the stylesheet. /// The content of the stylesheet. + [Obsolete("Please use IStylesheetService for stylesheet operations - will be removed in Umbraco 15")] Stream GetStylesheetFileContentStream(string filepath); /// @@ -135,6 +152,7 @@ public interface IFileService : IService /// /// The filesystem path to the stylesheet. /// The content of the stylesheet. + [Obsolete("Please use IStylesheetService for stylesheet operations - will be removed in Umbraco 15")] void SetStylesheetFileContent(string filepath, Stream content); /// @@ -142,12 +160,14 @@ public interface IFileService : IService /// /// The filesystem path to the stylesheet. /// The size of the stylesheet. + [Obsolete("Please use IStylesheetService for stylesheet operations - will be removed in Umbraco 15")] long GetStylesheetFileSize(string filepath); /// /// Gets a list of all objects /// /// An enumerable list of objects + [Obsolete("Please use IScriptService for script operations - will be removed in Umbraco 15")] IEnumerable GetScripts(params string[] names); /// @@ -155,6 +175,7 @@ public interface IFileService : IService /// /// Name of the script incl. extension /// A object + [Obsolete("Please use IScriptService for script operations - will be removed in Umbraco 15")] IScript? GetScript(string? name); /// @@ -162,6 +183,7 @@ public interface IFileService : IService /// /// to save /// Optional id of the user saving the script + [Obsolete("Please use IScriptService for script operations - will be removed in Umbraco 15")] void SaveScript(IScript? script, int? userId = Constants.Security.SuperUserId); /// @@ -169,6 +191,7 @@ public interface IFileService : IService /// /// Name incl. extension of the Script to delete /// Optional id of the user deleting the script + [Obsolete("Please use IScriptService for script operations - will be removed in Umbraco 15")] void DeleteScript(string path, int? userId = null); /// @@ -176,12 +199,14 @@ public interface IFileService : IService /// /// /// + [Obsolete("Please use IScriptFolderService for script folder operations - will be removed in Umbraco 15")] void CreateScriptFolder(string folderPath); /// /// Deletes a folder for scripts /// /// + [Obsolete("Please use IScriptFolderService for script folder operations - will be removed in Umbraco 15")] void DeleteScriptFolder(string folderPath); /// @@ -189,6 +214,7 @@ public interface IFileService : IService /// /// The filesystem path to the script. /// The content of the script file. + [Obsolete("Please use IScriptService for script operations - will be removed in Umbraco 15")] Stream GetScriptFileContentStream(string filepath); /// @@ -196,6 +222,7 @@ public interface IFileService : IService /// /// The filesystem path to the script. /// The content of the script file. + [Obsolete("Please use IScriptService for script operations - will be removed in Umbraco 15")] void SetScriptFileContent(string filepath, Stream content); /// @@ -203,18 +230,21 @@ public interface IFileService : IService /// /// The filesystem path to the script file. /// The size of the script file. + [Obsolete("Please use IScriptService for script operations - will be removed in Umbraco 15")] long GetScriptFileSize(string filepath); /// /// Gets a list of all objects /// /// An enumerable list of objects + [Obsolete("Please use ITemplateService for template operations - will be removed in Umbraco 15")] IEnumerable GetTemplates(params string[] aliases); /// /// Gets a list of all objects /// /// An enumerable list of objects + [Obsolete("Please use ITemplateService for template operations - will be removed in Umbraco 15")] IEnumerable GetTemplates(int masterTemplateId); /// @@ -222,6 +252,7 @@ public interface IFileService : IService /// /// The alias of the template. /// The object matching the alias, or null. + [Obsolete("Please use ITemplateService for template operations - will be removed in Umbraco 15")] ITemplate? GetTemplate(string? alias); /// @@ -229,6 +260,7 @@ public interface IFileService : IService /// /// The identifier of the template. /// The object matching the identifier, or null. + [Obsolete("Please use ITemplateService for template operations - will be removed in Umbraco 15")] ITemplate? GetTemplate(int id); /// @@ -236,6 +268,7 @@ public interface IFileService : IService /// /// The guid identifier of the template. /// The object matching the identifier, or null. + [Obsolete("Please use ITemplateService for template operations - will be removed in Umbraco 15")] ITemplate? GetTemplate(Guid id); /// @@ -243,6 +276,7 @@ public interface IFileService : IService /// /// /// + [Obsolete("Please use ITemplateService for template operations - will be removed in Umbraco 15")] IEnumerable GetTemplateDescendants(int masterTemplateId); /// @@ -250,6 +284,7 @@ public interface IFileService : IService /// /// to save /// Optional id of the user saving the template + [Obsolete("Please use ITemplateService for template operations - will be removed in Umbraco 15")] void SaveTemplate(ITemplate template, int userId = Constants.Security.SuperUserId); /// @@ -261,11 +296,13 @@ public interface IFileService : IService /// /// The template created /// + [Obsolete("Please use ITemplateService for template operations - will be removed in Umbraco 15")] Attempt?> CreateTemplateForContentType( string contentTypeAlias, string? contentTypeName, int userId = Constants.Security.SuperUserId); + [Obsolete("Please use ITemplateService for template operations - will be removed in Umbraco 15")] ITemplate CreateTemplateWithIdentity(string? name, string? alias, string? content, ITemplate? masterTemplate = null, int userId = Constants.Security.SuperUserId); /// @@ -273,6 +310,7 @@ public interface IFileService : IService /// /// Alias of the to delete /// Optional id of the user deleting the template + [Obsolete("Please use ITemplateService for template operations - will be removed in Umbraco 15")] void DeleteTemplate(string alias, int userId = Constants.Security.SuperUserId); /// @@ -280,6 +318,7 @@ public interface IFileService : IService /// /// List of to save /// Optional id of the user + [Obsolete("Please use ITemplateService for template operations - will be removed in Umbraco 15")] void SaveTemplate(IEnumerable templates, int userId = Constants.Security.SuperUserId); /// @@ -287,6 +326,7 @@ public interface IFileService : IService /// /// The filesystem path to the template. /// The content of the template. + [Obsolete("Please use ITemplateService for template operations - will be removed in Umbraco 15")] Stream GetTemplateFileContentStream(string filepath); /// @@ -294,6 +334,7 @@ public interface IFileService : IService /// /// The filesystem path to the template. /// The content of the template. + [Obsolete("Please use ITemplateService for template operations - will be removed in Umbraco 15")] void SetTemplateFileContent(string filepath, Stream content); /// @@ -301,6 +342,7 @@ public interface IFileService : IService /// /// The filesystem path to the template. /// The size of the template. + [Obsolete("Please use ITemplateService for template operations - will be removed in Umbraco 15")] long GetTemplateFileSize(string filepath); /// diff --git a/src/Umbraco.Core/Services/IPartialViewFolderService.cs b/src/Umbraco.Core/Services/IPartialViewFolderService.cs new file mode 100644 index 0000000000..6607068a54 --- /dev/null +++ b/src/Umbraco.Core/Services/IPartialViewFolderService.cs @@ -0,0 +1,8 @@ +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Core.Services; + +public interface IPartialViewFolderService : IPathFolderService +{ + +} diff --git a/src/Umbraco.Core/Services/IPartialViewService.cs b/src/Umbraco.Core/Services/IPartialViewService.cs new file mode 100644 index 0000000000..1fe40fa625 --- /dev/null +++ b/src/Umbraco.Core/Services/IPartialViewService.cs @@ -0,0 +1,49 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.New.Cms.Core.Models; +using PartialViewSnippet = Umbraco.Cms.Core.Snippets.PartialViewSnippet; + +namespace Umbraco.Cms.Core.Services; + +public interface IPartialViewService : IBasicFileService +{ + + /// + /// Deletes a partial view. + /// + /// The path of the partial view to delete. + /// The key of the user performing the operation. + /// An operation status. + Task DeleteAsync(string path, Guid performingUserKey); + + /// + /// Gets the name of all the available partial view snippets. + /// + /// Amount to skip. + /// Amount to take. + /// + Task> GetSnippetNamesAsync(int skip, int take); + + /// + /// Gets a partial view snippet by name, returns null if not found. + /// + /// The name of the snippet to get. + /// The partial view snippet, null if not found. + Task GetSnippetByNameAsync(string name); + + /// + /// Creates a new partial view. + /// + /// containing the information about the partial view being created. + /// The key of the user performing the operation. + /// An attempt indicating if the operation was a success as well as a more detailed . + Task> CreateAsync(PartialViewCreateModel createModel, Guid performingUserKey); + + /// + /// Updates an existing partial view. + /// + /// A with the changes. + /// The key of the user performing the operation. + /// An attempt indicating if the operation was a success as well as a more detailed . + Task> UpdateAsync(PartialViewUpdateModel updateModel, Guid performingUserKey); +} diff --git a/src/Umbraco.Core/Services/IPathFolderService.cs b/src/Umbraco.Core/Services/IPathFolderService.cs new file mode 100644 index 0000000000..e40482a354 --- /dev/null +++ b/src/Umbraco.Core/Services/IPathFolderService.cs @@ -0,0 +1,12 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Services; + +public interface IPathFolderService where TStatus : Enum +{ + Task GetAsync(string path); + + Task> CreateAsync(PathContainer container); + + Task> DeleteAsync(string path); +} diff --git a/src/Umbraco.Core/Services/IRichTextStylesheetService.cs b/src/Umbraco.Core/Services/IRichTextStylesheetService.cs new file mode 100644 index 0000000000..239a0b1afb --- /dev/null +++ b/src/Umbraco.Core/Services/IRichTextStylesheetService.cs @@ -0,0 +1,14 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Core.Strings.Css; + +namespace Umbraco.Cms.Core.Services; + +public interface IRichTextStylesheetService +{ + Task InterpolateRichTextRules(RichTextStylesheetData data); + + Task> ExtractRichTextRules(RichTextStylesheetData data); + + Task, StylesheetOperationStatus>> GetRulesByPathAsync(string path); +} diff --git a/src/Umbraco.Core/Services/IScriptFolderService.cs b/src/Umbraco.Core/Services/IScriptFolderService.cs new file mode 100644 index 0000000000..d81cb9a057 --- /dev/null +++ b/src/Umbraco.Core/Services/IScriptFolderService.cs @@ -0,0 +1,8 @@ +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Core.Services; + +public interface IScriptFolderService : IPathFolderService +{ + +} diff --git a/src/Umbraco.Core/Services/IScriptService.cs b/src/Umbraco.Core/Services/IScriptService.cs new file mode 100644 index 0000000000..f6fbb277e3 --- /dev/null +++ b/src/Umbraco.Core/Services/IScriptService.cs @@ -0,0 +1,31 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Core.Services; + +public interface IScriptService : IBasicFileService +{ + /// + /// Creates a new script. + /// + /// containing the information about the script being created. + /// The key of the user performing the operation. + /// An attempt indicating if the operation was a success as well as a more detailed . + Task> CreateAsync(ScriptCreateModel createModel, Guid performingUserKey); + + /// + /// Updates an existing script. + /// + /// A with the changes. + /// The key of the user performing the operation. + /// An attempt indicating if the operation was a success as well as a more detailed . + Task> UpdateAsync(ScriptUpdateModel updateModel, Guid performingUserKey); + + /// + /// Deletes a Script. + /// + /// The path of the script to delete. + /// The key of the user performing the operation. + /// An operation status. + Task DeleteAsync(string path, Guid performingUserKey); +} diff --git a/src/Umbraco.Core/Services/IStylesheetFolderService.cs b/src/Umbraco.Core/Services/IStylesheetFolderService.cs new file mode 100644 index 0000000000..52b7c57c23 --- /dev/null +++ b/src/Umbraco.Core/Services/IStylesheetFolderService.cs @@ -0,0 +1,8 @@ +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Core.Services; + +public interface IStylesheetFolderService : IPathFolderService +{ + +} diff --git a/src/Umbraco.Core/Services/IStylesheetService.cs b/src/Umbraco.Core/Services/IStylesheetService.cs new file mode 100644 index 0000000000..974774058e --- /dev/null +++ b/src/Umbraco.Core/Services/IStylesheetService.cs @@ -0,0 +1,31 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Core.Services; + +public interface IStylesheetService : IBasicFileService +{ + /// + /// Creates a new stylesheet. + /// + /// containing the information about the stylesheet being created. + /// The key of the user performing the operation. + /// An attempt indicating if the operation was a success as well as a more detailed . + Task> CreateAsync(StylesheetCreateModel createModel, Guid performingUserKey); + + /// + /// Updates an existing stylesheet. + /// + /// A with the changes. + /// The key of the user performing the operation. + /// An attempt indicating if the operation was a success as well as a more detailed . + Task> UpdateAsync(StylesheetUpdateModel updateModel, Guid performingUserKey); + + /// + /// Deletes a stylesheet. + /// + /// The path of the stylesheet to delete. + /// The key of the user performing the operation. + /// An operation status. + Task DeleteAsync(string path, Guid performingUserKey); +} diff --git a/src/Umbraco.Core/Services/OperationStatus/PartialViewFolderOperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus/PartialViewFolderOperationStatus.cs new file mode 100644 index 0000000000..f00bba2ae7 --- /dev/null +++ b/src/Umbraco.Core/Services/OperationStatus/PartialViewFolderOperationStatus.cs @@ -0,0 +1,11 @@ +namespace Umbraco.Cms.Core.Services.OperationStatus; + +public enum PartialViewFolderOperationStatus +{ + Success, + AlreadyExists, + NotFound, + NotEmpty, + ParentNotFound, + InvalidName, +} diff --git a/src/Umbraco.Core/Services/OperationStatus/PartialViewOperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus/PartialViewOperationStatus.cs new file mode 100644 index 0000000000..f057da529e --- /dev/null +++ b/src/Umbraco.Core/Services/OperationStatus/PartialViewOperationStatus.cs @@ -0,0 +1,13 @@ +namespace Umbraco.Cms.Core.Services.OperationStatus; + +public enum PartialViewOperationStatus +{ + Success, + AlreadyExists, + ParentNotFound, + InvalidName, + InvalidFileExtension, + PathTooLong, + CancelledByNotification, + NotFound +} diff --git a/src/Umbraco.Core/Services/OperationStatus/ScriptFolderOperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus/ScriptFolderOperationStatus.cs new file mode 100644 index 0000000000..4af83c0b5e --- /dev/null +++ b/src/Umbraco.Core/Services/OperationStatus/ScriptFolderOperationStatus.cs @@ -0,0 +1,11 @@ +namespace Umbraco.Cms.Core.Services.OperationStatus; + +public enum ScriptFolderOperationStatus +{ + Success, + AlreadyExists, + NotFound, + NotEmpty, + ParentNotFound, + InvalidName, +} diff --git a/src/Umbraco.Core/Services/OperationStatus/ScriptOperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus/ScriptOperationStatus.cs new file mode 100644 index 0000000000..a6a2bd8cdc --- /dev/null +++ b/src/Umbraco.Core/Services/OperationStatus/ScriptOperationStatus.cs @@ -0,0 +1,13 @@ +namespace Umbraco.Cms.Core.Services.OperationStatus; + +public enum ScriptOperationStatus +{ + Success, + AlreadyExists, + CancelledByNotification, + InvalidFileExtension, + ParentNotFound, + PathTooLong, + InvalidName, + NotFound, +} diff --git a/src/Umbraco.Core/Services/OperationStatus/StylesheetFolderOperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus/StylesheetFolderOperationStatus.cs new file mode 100644 index 0000000000..87b1bb9bf2 --- /dev/null +++ b/src/Umbraco.Core/Services/OperationStatus/StylesheetFolderOperationStatus.cs @@ -0,0 +1,11 @@ +namespace Umbraco.Cms.Core.Services.OperationStatus; + +public enum StylesheetFolderOperationStatus +{ + Success, + AlreadyExists, + NotFound, + NotEmpty, + ParentNotFound, + InvalidName, +} diff --git a/src/Umbraco.Core/Services/OperationStatus/StylesheetOperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus/StylesheetOperationStatus.cs new file mode 100644 index 0000000000..64fca90303 --- /dev/null +++ b/src/Umbraco.Core/Services/OperationStatus/StylesheetOperationStatus.cs @@ -0,0 +1,13 @@ +namespace Umbraco.Cms.Core.Services.OperationStatus; + +public enum StylesheetOperationStatus +{ + Success, + AlreadyExists, + CancelledByNotification, + InvalidFileExtension, + ParentNotFound, + PathTooLong, + InvalidName, + NotFound, +} diff --git a/src/Umbraco.Core/Services/PartialViewFolderService.cs b/src/Umbraco.Core/Services/PartialViewFolderService.cs new file mode 100644 index 0000000000..0d6201a859 --- /dev/null +++ b/src/Umbraco.Core/Services/PartialViewFolderService.cs @@ -0,0 +1,64 @@ +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Services; + +public class PartialViewFolderService : PathFolderServiceBase, IPartialViewFolderService +{ + private readonly IPartialViewRepository _partialViewRepository; + + public PartialViewFolderService( + ICoreScopeProvider provider, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + IPartialViewRepository partialViewRepository) + : base(provider, loggerFactory, eventMessagesFactory) + { + _partialViewRepository = partialViewRepository; + } + + protected override IPartialViewRepository Repository => _partialViewRepository; + + protected override PartialViewFolderOperationStatus SuccessStatus => PartialViewFolderOperationStatus.Success; + + protected override Task> ValidateCreateAsync(PathContainer container) + { + if(container.Name.ContainsAny(Path.GetInvalidFileNameChars())) + { + return Task.FromResult(Attempt.Fail(PartialViewFolderOperationStatus.InvalidName)); + } + + if(_partialViewRepository.FolderExists(container.Path)) + { + return Task.FromResult(Attempt.Fail(PartialViewFolderOperationStatus.AlreadyExists)); + } + + if(string.IsNullOrWhiteSpace(container.ParentPath) is false && + _partialViewRepository.FolderExists(container.ParentPath) is false) + { + return Task.FromResult(Attempt.Fail(PartialViewFolderOperationStatus.ParentNotFound)); + } + + return Task.FromResult(Attempt.Succeed(PartialViewFolderOperationStatus.Success)); + } + + protected override Task> ValidateDeleteAsync(string path) + { + if (_partialViewRepository.FolderExists(path) is false) + { + return Task.FromResult(Attempt.Fail(PartialViewFolderOperationStatus.NotFound)); + } + + if (_partialViewRepository.FolderHasContent(path)) + { + return Task.FromResult(Attempt.Fail(PartialViewFolderOperationStatus.NotEmpty)); + } + + return Task.FromResult(Attempt.Succeed(PartialViewFolderOperationStatus.Success)); + } +} diff --git a/src/Umbraco.Core/Services/PartialViewService.cs b/src/Umbraco.Core/Services/PartialViewService.cs new file mode 100644 index 0000000000..0280e3e01d --- /dev/null +++ b/src/Umbraco.Core/Services/PartialViewService.cs @@ -0,0 +1,227 @@ +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Core.Snippets; +using Umbraco.New.Cms.Core.Models; +using PartialViewSnippet = Umbraco.Cms.Core.Snippets.PartialViewSnippet; + +namespace Umbraco.Cms.Core.Services; + +public class PartialViewService : FileServiceBase, IPartialViewService +{ + private readonly PartialViewSnippetCollection _snippetCollection; + private readonly IUserIdKeyResolver _userIdKeyResolver; + private readonly ILogger _logger; + private readonly IAuditRepository _auditRepository; + + protected override string[] AllowedFileExtensions { get; } = { ".cshtml" }; + + public PartialViewService( + ICoreScopeProvider provider, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + PartialViewSnippetCollection snippetCollection, + IPartialViewRepository partialViewRepository, + IUserIdKeyResolver userIdKeyResolver, + ILogger logger, + IAuditRepository auditRepository) + : base(provider, loggerFactory, eventMessagesFactory, partialViewRepository) + { + _snippetCollection = snippetCollection; + _userIdKeyResolver = userIdKeyResolver; + _logger = logger; + _auditRepository = auditRepository; + } + + /// + public async Task DeleteAsync(string path, Guid performingUserKey) + { + using ICoreScope scope = ScopeProvider.CreateCoreScope(); + + IPartialView? partialView = Repository.Get(path); + if (partialView is null) + { + return PartialViewOperationStatus.NotFound; + } + + EventMessages eventMessages = EventMessagesFactory.Get(); + + var deletingNotification = new PartialViewDeletingNotification(partialView, eventMessages); + if (await scope.Notifications.PublishCancelableAsync(deletingNotification)) + { + return PartialViewOperationStatus.CancelledByNotification; + } + + Repository.Delete(partialView); + + scope.Notifications.Publish( + new PartialViewDeletedNotification(partialView, eventMessages).WithStateFrom(deletingNotification)); + + await AuditAsync(AuditType.Delete, performingUserKey); + return PartialViewOperationStatus.Success; + } + + /// + public Task> GetSnippetNamesAsync(int skip, int take) + { + using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true); + + string[] names = _snippetCollection.GetNames().ToArray(); + var total = names.Length; + + IEnumerable snippets = names + .Skip(skip) + .Take(take); + + return Task.FromResult(new PagedModel(total, snippets)); + } + + /// + public Task GetSnippetByNameAsync(string name) + { + using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true); + + // A bit weird but the "Name" of the snippet is the file name and extensions + // However when getting the content it's just the name without the extension + var fileName = name + ".cshtml"; + if (_snippetCollection.Any(x => x.Name == fileName) is false) + { + return Task.FromResult(null); + } + + var content = _snippetCollection.GetContentFromName(name); + var snippet = new PartialViewSnippet(name, content); + + return Task.FromResult(snippet); + } + + /// + public async Task> CreateAsync(PartialViewCreateModel createModel, Guid performingUserKey) + { + using ICoreScope scope = ScopeProvider.CreateCoreScope(); + + try + { + PartialViewOperationStatus validationResult = ValidateCreate(createModel); + if (validationResult is not PartialViewOperationStatus.Success) + { + return Attempt.FailWithStatus(validationResult, null); + } + } + catch (PathTooLongException exception) + { + _logger.LogError(exception, "The partial view path was too long"); + return Attempt.FailWithStatus(PartialViewOperationStatus.PathTooLong, null); + } + + var partialView = new PartialView(PartialViewType.PartialView, createModel.FilePath) { Content = createModel.Content }; + + EventMessages eventMessages = EventMessagesFactory.Get(); + var savingNotification = new PartialViewSavingNotification(partialView, eventMessages); + if (await scope.Notifications.PublishCancelableAsync(savingNotification)) + { + return Attempt.FailWithStatus( + PartialViewOperationStatus.CancelledByNotification, null); + } + + Repository.Save(partialView); + scope.Notifications.Publish(new PartialViewSavedNotification(partialView, eventMessages).WithStateFrom(savingNotification)); + await AuditAsync(AuditType.Save, performingUserKey); + + scope.Complete(); + return Attempt.SucceedWithStatus(PartialViewOperationStatus.Success, partialView); + } + + private PartialViewOperationStatus ValidateCreate(PartialViewCreateModel createModel) + { + if (Repository.Exists(createModel.FilePath)) + { + return PartialViewOperationStatus.AlreadyExists; + } + + if (string.IsNullOrWhiteSpace(createModel.ParentPath) is false && + Repository.FolderExists(createModel.ParentPath) is false) + { + return PartialViewOperationStatus.ParentNotFound; + } + + if (HasValidFileName(createModel.Name) is false) + { + return PartialViewOperationStatus.InvalidName; + } + + if (HasValidFileExtension(createModel.FilePath) is false) + { + return PartialViewOperationStatus.InvalidFileExtension; + } + + return PartialViewOperationStatus.Success; + } + + /// + public async Task> UpdateAsync(PartialViewUpdateModel updateModel, Guid performingUserKey) + { + using ICoreScope scope = ScopeProvider.CreateCoreScope(); + IPartialView? partialView = Repository.Get(updateModel.ExistingPath); + + if (partialView is null) + { + return Attempt.FailWithStatus(PartialViewOperationStatus.NotFound, null); + } + + PartialViewOperationStatus validationResult = ValidateUpdate(updateModel); + if (validationResult is not PartialViewOperationStatus.Success) + { + return Attempt.FailWithStatus(validationResult, null); + } + + partialView.Content = updateModel.Content; + if (partialView.Name != updateModel.Name) + { + var newPath = partialView.Path.Replace(partialView.Name!, updateModel.Name); + partialView.Path = newPath; + } + + EventMessages eventMessages = EventMessagesFactory.Get(); + var savingNotification = new PartialViewSavingNotification(partialView, eventMessages); + if (await scope.Notifications.PublishCancelableAsync(savingNotification)) + { + return Attempt.FailWithStatus(PartialViewOperationStatus.CancelledByNotification, null); + } + + Repository.Save(partialView); + scope.Notifications.Publish(new PartialViewSavedNotification(partialView, eventMessages).WithStateFrom(savingNotification)); + + await AuditAsync(AuditType.Save, performingUserKey); + + scope.Complete(); + return Attempt.SucceedWithStatus(PartialViewOperationStatus.Success, partialView); + } + + private PartialViewOperationStatus ValidateUpdate(PartialViewUpdateModel updateModel) + { + if (HasValidFileExtension(updateModel.Name) is false) + { + return PartialViewOperationStatus.InvalidFileExtension; + } + + if (HasValidFileName(updateModel.Name) is false) + { + return PartialViewOperationStatus.InvalidName; + } + + return PartialViewOperationStatus.Success; + } + + private async Task AuditAsync(AuditType type, Guid performingUserKey) + { + int userId = await _userIdKeyResolver.GetAsync(performingUserKey); + + // We're passing -1 here, because we don't have an entity id to pass in, as files on disc are not entities + _auditRepository.Save(new AuditItem(-1, type, userId, "PartialView")); + } +} diff --git a/src/Umbraco.Core/Services/PathFolderServiceBase.cs b/src/Umbraco.Core/Services/PathFolderServiceBase.cs new file mode 100644 index 0000000000..62f07c2e51 --- /dev/null +++ b/src/Umbraco.Core/Services/PathFolderServiceBase.cs @@ -0,0 +1,90 @@ +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; + +namespace Umbraco.Cms.Core.Services; + +public abstract class PathFolderServiceBase : + RepositoryService, + IPathFolderService + where TRepo: IFileWithFoldersRepository + where TStatus : Enum +{ + protected PathFolderServiceBase(ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory) : base(provider, loggerFactory, eventMessagesFactory) + { + } + + protected abstract TRepo Repository { get; } + + protected abstract TStatus SuccessStatus { get; } + + public virtual Task GetAsync(string path) + { + using ICoreScope scope = ScopeProvider.CreateCoreScope(); + + // There's not much we can actually get when it's a folder, so it more a matter of ensuring the folder exists and returning a model. + if (Repository.FolderExists(path) is false) + { + return Task.FromResult(null); + } + + PathContainer model = CreateFromPath(path); + + scope.Complete(); + return Task.FromResult(model); + } + + public async Task> CreateAsync(PathContainer container) + { + using ICoreScope scope = ScopeProvider.CreateCoreScope(); + + Attempt validationResult = await ValidateCreateAsync(container); + if (validationResult.Success is false) + { + return Attempt.FailWithStatus(validationResult.Result!, null); + } + + Repository.AddFolder(container.Path); + + scope.Complete(); + + return Attempt.SucceedWithStatus(SuccessStatus, CreateFromPath(container.Path)); + } + + protected abstract Task> ValidateCreateAsync(PathContainer container); + + public async Task> DeleteAsync(string path) + { + using ICoreScope scope = ScopeProvider.CreateCoreScope(); + + Attempt validationResult = await ValidateDeleteAsync(path); + if (validationResult.Success is false) + { + return Attempt.Fail(validationResult.Result); + } + + Repository.DeleteFolder(path); + + scope.Complete(); + + return Attempt.Succeed(SuccessStatus); + } + + protected abstract Task> ValidateDeleteAsync(string path); + + private PathContainer CreateFromPath(string path) + { + var parentPath = Path.GetDirectoryName(path); + var parentPathLength = string.IsNullOrEmpty(parentPath) ? 0 : parentPath.Length + 1; + + var model = new PathContainer + { + Name = path.Remove(0, parentPathLength), + ParentPath = parentPath, + }; + + return model; + } +} diff --git a/src/Umbraco.Core/Services/RichTextStylesheetService.cs b/src/Umbraco.Core/Services/RichTextStylesheetService.cs new file mode 100644 index 0000000000..46fde91d0a --- /dev/null +++ b/src/Umbraco.Core/Services/RichTextStylesheetService.cs @@ -0,0 +1,73 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Core.Strings.Css; + +namespace Umbraco.Cms.Core.Services; + +public class RichTextStylesheetService : IRichTextStylesheetService +{ + private readonly IStylesheetService _stylesheetService; + + public RichTextStylesheetService(IStylesheetService stylesheetService) + { + _stylesheetService = stylesheetService; + } + + public Task InterpolateRichTextRules(RichTextStylesheetData data) + { + // First we remove all existing rules, from the content, in case some of the rules has changed or been removed + // This way we can simply re-add them. + StylesheetRule[] existingRules = string.IsNullOrWhiteSpace(data.Content) + ? Array.Empty() + : StylesheetHelper.ParseRules(data.Content).ToArray(); + + foreach (StylesheetRule rule in existingRules) + { + // Setting the rule to null will delete it from the content. + data.Content = StylesheetHelper.ReplaceRule(data.Content, rule.Name, null); + } + + data.Content = data.Content?.TrimEnd(Constants.CharArrays.LineFeedCarriageReturn); + + // Now we can re-add all the rules. + if (data.Rules.Any()) + { + foreach (StylesheetRule rule in data.Rules) + { + data.Content = StylesheetHelper.AppendRule( + data.Content, + rule); + } + + data.Content += Environment.NewLine; + } + + return Task.FromResult(data.Content ?? string.Empty); + } + + public Task> ExtractRichTextRules(RichTextStylesheetData data) + { + if (string.IsNullOrWhiteSpace(data.Content)) + { + return Task.FromResult(Enumerable.Empty()); + } + + return Task.FromResult(StylesheetHelper.ParseRules(data.Content)); + } + + public async Task, StylesheetOperationStatus>> GetRulesByPathAsync(string path) + { + IStylesheet? stylesheet = await _stylesheetService.GetAsync(path); + + if (stylesheet is null) + { + return Attempt.FailWithStatus(StylesheetOperationStatus.NotFound, Enumerable.Empty()); + } + + IEnumerable rules = stylesheet.Properties is null + ? Enumerable.Empty() + : stylesheet.Properties.Select(x => new StylesheetRule { Name = x.Name, Selector = x.Alias, Styles = x.Value }); + + return Attempt.SucceedWithStatus(StylesheetOperationStatus.Success, rules); + } +} diff --git a/src/Umbraco.Core/Services/ScriptFolderService.cs b/src/Umbraco.Core/Services/ScriptFolderService.cs new file mode 100644 index 0000000000..272253a0e3 --- /dev/null +++ b/src/Umbraco.Core/Services/ScriptFolderService.cs @@ -0,0 +1,64 @@ +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Services; + +public class ScriptFolderService : PathFolderServiceBase, IScriptFolderService +{ + private readonly IScriptRepository _scriptRepository; + + public ScriptFolderService( + ICoreScopeProvider provider, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + IScriptRepository scriptRepository) + : base(provider, loggerFactory, eventMessagesFactory) + { + _scriptRepository = scriptRepository; + } + + protected override IScriptRepository Repository => _scriptRepository; + + protected override ScriptFolderOperationStatus SuccessStatus => ScriptFolderOperationStatus.Success; + + protected override Task> ValidateCreateAsync(PathContainer container) + { + if (container.Name.ContainsAny(Path.GetInvalidFileNameChars())) + { + return Task.FromResult(Attempt.Fail(ScriptFolderOperationStatus.InvalidName)); + } + + if (_scriptRepository.FolderExists(container.Path)) + { + return Task.FromResult(Attempt.Fail(ScriptFolderOperationStatus.AlreadyExists)); + } + + if (string.IsNullOrWhiteSpace(container.ParentPath) is false && + _scriptRepository.FolderExists(container.ParentPath) is false) + { + return Task.FromResult(Attempt.Fail(ScriptFolderOperationStatus.ParentNotFound)); + } + + return Task.FromResult(Attempt.Succeed(ScriptFolderOperationStatus.Success)); + } + + protected override Task> ValidateDeleteAsync(string path) + { + if(_scriptRepository.FolderExists(path) is false) + { + return Task.FromResult(Attempt.Fail(ScriptFolderOperationStatus.NotFound)); + } + + if (_scriptRepository.FolderHasContent(path)) + { + return Task.FromResult(Attempt.Fail(ScriptFolderOperationStatus.NotEmpty)); + } + + return Task.FromResult(Attempt.Succeed(ScriptFolderOperationStatus.Success)); + } +} diff --git a/src/Umbraco.Core/Services/ScriptService.cs b/src/Umbraco.Core/Services/ScriptService.cs new file mode 100644 index 0000000000..83b8a37a2c --- /dev/null +++ b/src/Umbraco.Core/Services/ScriptService.cs @@ -0,0 +1,188 @@ +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Core.Services; + +public class ScriptService : FileServiceBase, IScriptService +{ + private readonly IAuditRepository _auditRepository; + private readonly IUserIdKeyResolver _userIdKeyResolver; + private readonly ILogger _logger; + + protected override string[] AllowedFileExtensions { get; } = { ".js" }; + + public ScriptService( + ICoreScopeProvider provider, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + IScriptRepository scriptRepository, + IAuditRepository auditRepository, + IUserIdKeyResolver userIdKeyResolver, + ILogger logger) + : base(provider, loggerFactory, eventMessagesFactory, scriptRepository) + { + _auditRepository = auditRepository; + _userIdKeyResolver = userIdKeyResolver; + _logger = logger; + } + + /// + public async Task DeleteAsync(string path, Guid performingUserKey) + { + using ICoreScope scope = ScopeProvider.CreateCoreScope(); + + IScript? script = Repository.Get(path); + if (script is null) + { + return ScriptOperationStatus.NotFound; + } + + EventMessages eventMessages = EventMessagesFactory.Get(); + + var deletingNotification = new ScriptDeletingNotification(script, eventMessages); + if (await scope.Notifications.PublishCancelableAsync(deletingNotification)) + { + return ScriptOperationStatus.CancelledByNotification; + } + + Repository.Delete(script); + + scope.Notifications.Publish( + new ScriptDeletedNotification(script, eventMessages).WithStateFrom(deletingNotification)); + + await AuditAsync(AuditType.Delete, performingUserKey); + + scope.Complete(); + return ScriptOperationStatus.Success; + } + + /// + public async Task> CreateAsync(ScriptCreateModel createModel, Guid performingUserKey) + { + using ICoreScope scope = ScopeProvider.CreateCoreScope(); + + try + { + ScriptOperationStatus validationResult = await ValidateCreateAsync(createModel); + if (validationResult is not ScriptOperationStatus.Success) + { + return Attempt.FailWithStatus(validationResult, null); + } + } + catch (PathTooLongException exception) + { + _logger.LogError(exception, "The script path was too long"); + return Attempt.FailWithStatus(ScriptOperationStatus.PathTooLong, null); + } + + var script = new Script(createModel.FilePath) { Content = createModel.Content }; + + EventMessages eventMessages = EventMessagesFactory.Get(); + var savingNotification = new ScriptSavingNotification(script, eventMessages); + if (await scope.Notifications.PublishCancelableAsync(savingNotification)) + { + return Attempt.FailWithStatus(ScriptOperationStatus.CancelledByNotification, null); + } + + Repository.Save(script); + + scope.Notifications.Publish(new ScriptSavedNotification(script, eventMessages).WithStateFrom(savingNotification)); + await AuditAsync(AuditType.Save, performingUserKey); + + scope.Complete(); + return Attempt.SucceedWithStatus(ScriptOperationStatus.Success, script); + } + + private Task ValidateCreateAsync(ScriptCreateModel createModel) + { + if (Repository.Exists(createModel.FilePath)) + { + return Task.FromResult(ScriptOperationStatus.AlreadyExists); + } + + if (string.IsNullOrWhiteSpace(createModel.ParentPath) is false && + Repository.FolderExists(createModel.ParentPath) is false) + { + return Task.FromResult(ScriptOperationStatus.ParentNotFound); + } + + if(HasValidFileName(createModel.Name) is false) + { + return Task.FromResult(ScriptOperationStatus.InvalidName); + } + + if (HasValidFileExtension(createModel.FilePath) is false) + { + return Task.FromResult(ScriptOperationStatus.InvalidFileExtension); + } + + return Task.FromResult(ScriptOperationStatus.Success); + } + + /// + public async Task> UpdateAsync(ScriptUpdateModel updateModel, Guid performingUserKey) + { + using ICoreScope scope = ScopeProvider.CreateCoreScope(); + IScript? script = Repository.Get(updateModel.ExistingPath); + + if (script is null) + { + return Attempt.FailWithStatus(ScriptOperationStatus.NotFound, null); + } + + ScriptOperationStatus validationResult = ValidateUpdate(updateModel); + if (validationResult is not ScriptOperationStatus.Success) + { + return Attempt.FailWithStatus(validationResult, null); + } + + script.Content = updateModel.Content; + if (script.Name != updateModel.Name) + { + // Name has been updated, so we need to update the path as well + var newPath = script.Path.Replace(script.Name!, updateModel.Name); + script.Path = newPath; + } + + EventMessages eventMessages = EventMessagesFactory.Get(); + var savingNotification = new ScriptSavingNotification(script, eventMessages); + if (await scope.Notifications.PublishCancelableAsync(savingNotification)) + { + return Attempt.FailWithStatus(ScriptOperationStatus.CancelledByNotification, null); + } + + Repository.Save(script); + scope.Notifications.Publish(new ScriptSavedNotification(script, eventMessages).WithStateFrom(savingNotification)); + + await AuditAsync(AuditType.Save, performingUserKey); + + scope.Complete(); + return Attempt.SucceedWithStatus(ScriptOperationStatus.Success, script); + } + + private ScriptOperationStatus ValidateUpdate(ScriptUpdateModel updateModel) + { + if (HasValidFileExtension(updateModel.Name) is false) + { + return ScriptOperationStatus.InvalidFileExtension; + } + + if (HasValidFileName(updateModel.Name) is false) + { + return ScriptOperationStatus.InvalidName; + } + + return ScriptOperationStatus.Success; + } + + private async Task AuditAsync(AuditType type, Guid performingUserKey) + { + int userId = await _userIdKeyResolver.GetAsync(performingUserKey); + _auditRepository.Save(new AuditItem(-1, type, userId, "Script")); + } +} diff --git a/src/Umbraco.Core/Services/StylesheetFolderService.cs b/src/Umbraco.Core/Services/StylesheetFolderService.cs new file mode 100644 index 0000000000..bcbd51ca9c --- /dev/null +++ b/src/Umbraco.Core/Services/StylesheetFolderService.cs @@ -0,0 +1,62 @@ +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Services; + +public class StylesheetFolderService : PathFolderServiceBase, IStylesheetFolderService +{ + private readonly IStylesheetRepository _stylesheetRepository; + + public StylesheetFolderService( + ICoreScopeProvider provider, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + IStylesheetRepository stylesheetRepository) + : base(provider, loggerFactory, eventMessagesFactory) => + _stylesheetRepository = stylesheetRepository; + + protected override IStylesheetRepository Repository => _stylesheetRepository; + + protected override StylesheetFolderOperationStatus SuccessStatus => StylesheetFolderOperationStatus.Success; + + protected override Task> ValidateCreateAsync(PathContainer container) + { + if (container.Name.ContainsAny(Path.GetInvalidFileNameChars())) + { + return Task.FromResult(Attempt.Fail(StylesheetFolderOperationStatus.InvalidName)); + } + + if (_stylesheetRepository.FolderExists(container.Path)) + { + return Task.FromResult(Attempt.Fail(StylesheetFolderOperationStatus.AlreadyExists)); + } + + if (string.IsNullOrWhiteSpace(container.ParentPath) is false && + _stylesheetRepository.FolderExists(container.ParentPath) is false) + { + return Task.FromResult(Attempt.Fail(StylesheetFolderOperationStatus.ParentNotFound)); + } + + return Task.FromResult(Attempt.Succeed(StylesheetFolderOperationStatus.Success)); + } + + protected override Task> ValidateDeleteAsync(string path) + { + if(_stylesheetRepository.FolderExists(path) is false) + { + return Task.FromResult(Attempt.Fail(StylesheetFolderOperationStatus.NotFound)); + } + + if (_stylesheetRepository.FolderHasContent(path)) + { + return Task.FromResult(Attempt.Fail(StylesheetFolderOperationStatus.NotEmpty)); + } + + return Task.FromResult(Attempt.Succeed(StylesheetFolderOperationStatus.Success)); + } +} diff --git a/src/Umbraco.Core/Services/StylesheetService.cs b/src/Umbraco.Core/Services/StylesheetService.cs new file mode 100644 index 0000000000..b3f88f3409 --- /dev/null +++ b/src/Umbraco.Core/Services/StylesheetService.cs @@ -0,0 +1,186 @@ +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Core.Services; + +public class StylesheetService : FileServiceBase, IStylesheetService +{ + private readonly ILogger _logger; + private readonly IUserIdKeyResolver _userIdKeyResolver; + private readonly IAuditRepository _auditRepository; + + protected override string[] AllowedFileExtensions { get; } = { ".css" }; + + public StylesheetService( + ICoreScopeProvider provider, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + IStylesheetRepository stylesheetRepository, + ILogger logger, + IUserIdKeyResolver userIdKeyResolver, + IAuditRepository auditRepository) + : base(provider, loggerFactory, eventMessagesFactory, stylesheetRepository) + { + _logger = logger; + _userIdKeyResolver = userIdKeyResolver; + _auditRepository = auditRepository; + } + + /// + public async Task DeleteAsync(string path, Guid performingUserKey) + { + using ICoreScope scope = ScopeProvider.CreateCoreScope(); + + IStylesheet? stylesheet = Repository.Get(path); + if (stylesheet is null) + { + return StylesheetOperationStatus.NotFound; + } + + EventMessages eventMessages = EventMessagesFactory.Get(); + var deletingNotification = new StylesheetDeletingNotification(stylesheet, eventMessages); + if (await scope.Notifications.PublishCancelableAsync(deletingNotification)) + { + return StylesheetOperationStatus.CancelledByNotification; + } + + Repository.Delete(stylesheet); + + scope.Notifications.Publish(new StylesheetDeletedNotification(stylesheet, eventMessages).WithStateFrom(deletingNotification)); + await AuditAsync(AuditType.Delete, performingUserKey); + + scope.Complete(); + return StylesheetOperationStatus.Success; + } + + /// + public async Task> CreateAsync(StylesheetCreateModel createModel, Guid performingUserKey) + { + using ICoreScope scope = ScopeProvider.CreateCoreScope(); + + try + { + StylesheetOperationStatus validationResult = ValidateCreate(createModel); + if (validationResult is not StylesheetOperationStatus.Success) + { + return Attempt.FailWithStatus(validationResult, null); + } + } + catch (PathTooLongException exception) + { + _logger.LogError(exception, "The stylesheet path was too long"); + return Attempt.FailWithStatus(StylesheetOperationStatus.PathTooLong, null); + } + + var stylesheet = new Stylesheet(createModel.FilePath) { Content = createModel.Content }; + + EventMessages eventMessages = EventMessagesFactory.Get(); + var savingNotification = new StylesheetSavingNotification(stylesheet, eventMessages); + if (await scope.Notifications.PublishCancelableAsync(savingNotification)) + { + return Attempt.FailWithStatus(StylesheetOperationStatus.CancelledByNotification, null); + } + + Repository.Save(stylesheet); + + scope.Notifications.Publish(new StylesheetSavedNotification(stylesheet, eventMessages).WithStateFrom(savingNotification)); + await AuditAsync(AuditType.Save, performingUserKey); + + scope.Complete(); + return Attempt.SucceedWithStatus(StylesheetOperationStatus.Success, stylesheet); + } + + private StylesheetOperationStatus ValidateCreate(StylesheetCreateModel createModel) + { + if (Repository.Exists(createModel.FilePath)) + { + return StylesheetOperationStatus.AlreadyExists; + } + + if (string.IsNullOrWhiteSpace(createModel.ParentPath) is false + && Repository.Exists(createModel.ParentPath) is false) + { + return StylesheetOperationStatus.ParentNotFound; + } + + if (HasValidFileName(createModel.Name) is false) + { + return StylesheetOperationStatus.InvalidName; + } + + if (HasValidFileExtension(createModel.FilePath) is false) + { + return StylesheetOperationStatus.InvalidFileExtension; + } + + return StylesheetOperationStatus.Success; + } + + /// + public async Task> UpdateAsync(StylesheetUpdateModel updateModel, Guid performingUserKey) + { + using ICoreScope scope = ScopeProvider.CreateCoreScope(); + + IStylesheet? stylesheet = Repository.Get(updateModel.ExistingPath); + + if (stylesheet is null) + { + return Attempt.FailWithStatus(StylesheetOperationStatus.NotFound, null); + } + + StylesheetOperationStatus validationResult = ValidateUpdate(updateModel); + if (validationResult is not StylesheetOperationStatus.Success) + { + return Attempt.FailWithStatus(validationResult, null); + } + + stylesheet.Content = updateModel.Content; + if (stylesheet.Name != updateModel.Name) + { + // Name has been updated, so we need to update the path as well + var newPath = stylesheet.Path.Replace(stylesheet.Name!, updateModel.Name); + stylesheet.Path = newPath; + } + + EventMessages eventMessages = EventMessagesFactory.Get(); + var savingNotification = new StylesheetSavingNotification(stylesheet, eventMessages); + if (await scope.Notifications.PublishCancelableAsync(savingNotification)) + { + return Attempt.FailWithStatus(StylesheetOperationStatus.CancelledByNotification, null); + } + + Repository.Save(stylesheet); + + scope.Notifications.Publish(new StylesheetSavedNotification(stylesheet, eventMessages).WithStateFrom(savingNotification)); + await AuditAsync(AuditType.Save, performingUserKey); + + scope.Complete(); + return Attempt.SucceedWithStatus(StylesheetOperationStatus.Success, stylesheet); + } + + private StylesheetOperationStatus ValidateUpdate(StylesheetUpdateModel updateModel) + { + if (HasValidFileExtension(updateModel.Name) is false) + { + return StylesheetOperationStatus.InvalidFileExtension; + } + + if (HasValidFileName(updateModel.Name) is false) + { + return StylesheetOperationStatus.InvalidName; + } + + return StylesheetOperationStatus.Success; + } + + private async Task AuditAsync(AuditType type, Guid performingUserKey) + { + var userId = await _userIdKeyResolver.GetAsync(performingUserKey); + _auditRepository.Save(new AuditItem(-1, type, userId, "Stylesheet")); + } +} diff --git a/src/Umbraco.Core/Services/TemplateService.cs b/src/Umbraco.Core/Services/TemplateService.cs index fdf950567b..458776ed3e 100644 --- a/src/Umbraco.Core/Services/TemplateService.cs +++ b/src/Umbraco.Core/Services/TemplateService.cs @@ -1,6 +1,4 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.DependencyInjection; +using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; @@ -39,27 +37,6 @@ public class TemplateService : RepositoryService, ITemplateService _userIdKeyResolver = userIdKeyResolver; } - [Obsolete("Please use ctor that takes all parameters, scheduled for removal in v15")] - public TemplateService( - ICoreScopeProvider provider, - ILoggerFactory loggerFactory, - IEventMessagesFactory eventMessagesFactory, - IShortStringHelper shortStringHelper, - ITemplateRepository templateRepository, - IAuditRepository auditRepository, - ITemplateContentParserService templateContentParserService) - : this( - provider, - loggerFactory, - eventMessagesFactory, - shortStringHelper, - templateRepository, - auditRepository, - templateContentParserService, - StaticServiceProvider.Instance.GetRequiredService()) - { - } - /// public async Task> CreateForContentTypeAsync( string contentTypeAlias, string? contentTypeName, Guid userKey) diff --git a/src/Umbraco.Core/Snippets/PartialViewSnippet.cs b/src/Umbraco.Core/Snippets/PartialViewSnippet.cs new file mode 100644 index 0000000000..deec00ed1a --- /dev/null +++ b/src/Umbraco.Core/Snippets/PartialViewSnippet.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Cms.Core.Snippets; + +public class PartialViewSnippet : Snippet +{ + public PartialViewSnippet(string name, string content) : base(name, content) + { + } +} diff --git a/src/Umbraco.Core/Snippets/PartialViewSnippetCollection.cs b/src/Umbraco.Core/Snippets/PartialViewSnippetCollection.cs index 7b07d554c0..f20e0fd3e8 100644 --- a/src/Umbraco.Core/Snippets/PartialViewSnippetCollection.cs +++ b/src/Umbraco.Core/Snippets/PartialViewSnippetCollection.cs @@ -42,7 +42,7 @@ namespace Umbraco.Cms.Core.Snippets string partialViewHeader = "@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage"; - var snippet = this.Where(x => x.Name.Equals(snippetName + ".cshtml")).FirstOrDefault(); + ISnippet? snippet = this.Where(x => x.Name.Equals(snippetName + ".cshtml")).FirstOrDefault(); // Try and get the snippet path if (snippet is null) diff --git a/src/Umbraco.Core/Snippets/Snippet.cs b/src/Umbraco.Core/Snippets/Snippet.cs index bcb03b6d11..290d24bdf7 100644 --- a/src/Umbraco.Core/Snippets/Snippet.cs +++ b/src/Umbraco.Core/Snippets/Snippet.cs @@ -3,6 +3,7 @@ namespace Umbraco.Cms.Core.Snippets public class Snippet : ISnippet { public string Name { get; } + public string Content { get; } public Snippet(string name, string content) diff --git a/src/Umbraco.Infrastructure/CompatibilitySuppressions.xml b/src/Umbraco.Infrastructure/CompatibilitySuppressions.xml index df53a955b6..f62e4b12df 100644 --- a/src/Umbraco.Infrastructure/CompatibilitySuppressions.xml +++ b/src/Umbraco.Infrastructure/CompatibilitySuppressions.xml @@ -1,5 +1,4 @@  - CP0001 diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/FileRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/FileRepository.cs index a6e9d517c5..9ddedf7f80 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/FileRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/FileRepository.cs @@ -1,3 +1,4 @@ +using System.Reflection; using System.Text; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; @@ -18,6 +19,10 @@ internal abstract class FileRepository : IReadRepository PersistDeletedItem(new Folder(folderPath)); + public virtual bool FolderExists(string folderPath) => FileSystem?.DirectoryExists(folderPath) is true; + + public virtual bool FolderHasContent(string folderPath) => FileSystem?.GetFiles(folderPath).Any() is true || FileSystem?.GetDirectories(folderPath).Any() is true; + public Stream GetFileContentStream(string filepath) { if (FileSystem?.FileExists(filepath) == false) diff --git a/src/Umbraco.Web.BackOffice/CompatibilitySuppressions.xml b/src/Umbraco.Web.BackOffice/CompatibilitySuppressions.xml index e60e5c2b68..2811562e53 100644 --- a/src/Umbraco.Web.BackOffice/CompatibilitySuppressions.xml +++ b/src/Umbraco.Web.BackOffice/CompatibilitySuppressions.xml @@ -1,5 +1,4 @@  - CP0002 diff --git a/src/Umbraco.Web.Common/CompatibilitySuppressions.xml b/src/Umbraco.Web.Common/CompatibilitySuppressions.xml index 95a26a3f01..af97dca9af 100644 --- a/src/Umbraco.Web.Common/CompatibilitySuppressions.xml +++ b/src/Umbraco.Web.Common/CompatibilitySuppressions.xml @@ -1,5 +1,4 @@  - CP0002 diff --git a/tests/Umbraco.Tests.Common/CompatibilitySuppressions.xml b/tests/Umbraco.Tests.Common/CompatibilitySuppressions.xml index a709113936..657f05d2d7 100644 --- a/tests/Umbraco.Tests.Common/CompatibilitySuppressions.xml +++ b/tests/Umbraco.Tests.Common/CompatibilitySuppressions.xml @@ -1,5 +1,4 @@  - CP0002 diff --git a/tests/Umbraco.Tests.Integration/CompatibilitySuppressions.xml b/tests/Umbraco.Tests.Integration/CompatibilitySuppressions.xml index 2ff5f38afb..bdfa11824e 100644 --- a/tests/Umbraco.Tests.Integration/CompatibilitySuppressions.xml +++ b/tests/Umbraco.Tests.Integration/CompatibilitySuppressions.xml @@ -1,5 +1,4 @@  - CP0002