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:
188
src/Umbraco.Core/Services/ScriptService.cs
Normal file
188
src/Umbraco.Core/Services/ScriptService.cs
Normal 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"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user