New backoffice: Add new codefile controllers and services (#14157)

* Add scaffolding

* Entities not Entitys

* Remove unececary obsoleted constructor

* Implement create script

* Return a simplified ScriptFile instead of IScript

That file abstraction seems way too bloated, containing a lot of stuff that's not relevant for files, such as IDs and keys.

* Use IScript instead of custom return model

* Add validation when creating script

* Add Get script endpoint

* Add response types

* Add Delete

* Throw if user key not found

* Remove unused maapper

* Add update endpoint

* Add Get by path

* Add create folder endpoint

* Don't pass performingUserId to folder creation

* Remove update folder

* Add delete folder endpoint

* Use specific ScriptFolderOperationStatus instead of ScriptOperationStatus

* Add OperationStatusResult

* Check folder for invalid name

* Check name for invalid characters

* Add partial view snippet endpoint

* Start working on CreatePartialView

* Add create partial view endpoint

* Retrieve key from audit method

* Add operation status results

* Add Get endpoint

* Return 201 when creating

* Add update partial view endpoint

* Add delete endpoint

* Add response types

* Add folder base implementation

* Add folder endpoints

* User property for allowed file extensions

* Rename async method to async

* Break snippet into endpoint in two

* Make content non-nullable

* Remove IService

* Add get by path

* Add viewmodels

* Add create and update models

* Add create stylesheet

* Add update endpoint

* Rename StylesheetControllerBase to StylesheetControllerBase

* Add stylesheet delete

* Rename controller bases

* Add stylesheet folders

* Add status results

* Add response types to folders

* Add richtext rules endpoints

* Add Get all endpoint

* Add get rules by path endpoint

* Aling validates so they're not async

These are private methods, so there's no reason to make them preemptively async

* Add template obsoletions to interface

* Add stream methods

This is evidently used by deploy 🤷

* Obsolete stylesheet operations

* Add get and getall across all services

* Obsolete script operations

* Obsolete old partial view methods

* Add some method docs

* Add compatibility suppression

* Update OpenApi.json

* Rename action

* formatting

* Fix import

* add expression body

* Invert if

* Move base on own line

* Rename file

* Rename to all

* Change to stylesheet instead of script

* Add Umbraco.Code.MapAll to map definitions

* Add comment about auditing

* use publish cancelable async

* use expression body

* formatting

* fix to use pattern matching

---------

Co-authored-by: Zeegaan <nge@umbraco.dk>
This commit is contained in:
Mole
2023-04-26 13:47:47 +02:00
committed by GitHub
parent 17fef47450
commit b411452f79
122 changed files with 4381 additions and 34 deletions

View File

@@ -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<IScriptRepository, IScript>, IScriptService
{
private readonly IAuditRepository _auditRepository;
private readonly IUserIdKeyResolver _userIdKeyResolver;
private readonly ILogger<ScriptService> _logger;
protected override string[] AllowedFileExtensions { get; } = { ".js" };
public ScriptService(
ICoreScopeProvider provider,
ILoggerFactory loggerFactory,
IEventMessagesFactory eventMessagesFactory,
IScriptRepository scriptRepository,
IAuditRepository auditRepository,
IUserIdKeyResolver userIdKeyResolver,
ILogger<ScriptService> logger)
: base(provider, loggerFactory, eventMessagesFactory, scriptRepository)
{
_auditRepository = auditRepository;
_userIdKeyResolver = userIdKeyResolver;
_logger = logger;
}
/// <inheritdoc />
public async Task<ScriptOperationStatus> 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;
}
/// <inheritdoc />
public async Task<Attempt<IScript?, ScriptOperationStatus>> 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<IScript?, ScriptOperationStatus>(validationResult, null);
}
}
catch (PathTooLongException exception)
{
_logger.LogError(exception, "The script path was too long");
return Attempt.FailWithStatus<IScript?, ScriptOperationStatus>(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<IScript?, ScriptOperationStatus>(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<IScript?, ScriptOperationStatus>(ScriptOperationStatus.Success, script);
}
private Task<ScriptOperationStatus> 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);
}
/// <inheritdoc />
public async Task<Attempt<IScript?, ScriptOperationStatus>> UpdateAsync(ScriptUpdateModel updateModel, Guid performingUserKey)
{
using ICoreScope scope = ScopeProvider.CreateCoreScope();
IScript? script = Repository.Get(updateModel.ExistingPath);
if (script is null)
{
return Attempt.FailWithStatus<IScript?, ScriptOperationStatus>(ScriptOperationStatus.NotFound, null);
}
ScriptOperationStatus validationResult = ValidateUpdate(updateModel);
if (validationResult is not ScriptOperationStatus.Success)
{
return Attempt.FailWithStatus<IScript?, ScriptOperationStatus>(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<IScript?, ScriptOperationStatus>(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<IScript?, ScriptOperationStatus>(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"));
}
}