using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.ContentEditing.Validation; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services.OperationStatus; namespace Umbraco.Cms.Core.Services; internal sealed class ContentEditingService : ContentEditingServiceWithSortingBase, IContentEditingService { private readonly ITemplateService _templateService; private readonly ILogger _logger; public ContentEditingService( IContentService contentService, IContentTypeService contentTypeService, PropertyEditorCollection propertyEditorCollection, IDataTypeService dataTypeService, ITemplateService templateService, ILogger logger, ICoreScopeProvider scopeProvider, IUserIdKeyResolver userIdKeyResolver, ITreeEntitySortingService treeEntitySortingService, IContentValidationService contentValidationService) : base(contentService, contentTypeService, propertyEditorCollection, dataTypeService, logger, scopeProvider, userIdKeyResolver, contentValidationService, treeEntitySortingService) { _templateService = templateService; _logger = logger; } public async Task GetAsync(Guid key) { IContent? content = ContentService.GetById(key); return await Task.FromResult(content); } public async Task> ValidateUpdateAsync(IContent content, ContentUpdateModel updateModel) => await ValidatePropertiesAsync(updateModel, content.ContentType.Key); public async Task> ValidateCreateAsync(ContentCreateModel createModel) => await ValidatePropertiesAsync(createModel, createModel.ContentTypeKey); public async Task> CreateAsync(ContentCreateModel createModel, Guid userKey) { Attempt result = await MapCreate(createModel); if (result.Success == false) { return result; } // the create mapping might succeed, but this doesn't mean the model is valid at property level. // we'll return the actual property validation status if the entire operation succeeds. ContentEditingOperationStatus validationStatus = result.Status; ContentValidationResult validationResult = result.Result.ValidationResult; IContent content = result.Result.Content!; ContentEditingOperationStatus updateTemplateStatus = await UpdateTemplateAsync(content, createModel.TemplateKey); if (updateTemplateStatus != ContentEditingOperationStatus.Success) { return Attempt.FailWithStatus(updateTemplateStatus, new ContentCreateResult { Content = content }); } ContentEditingOperationStatus saveStatus = await Save(content, userKey); return saveStatus == ContentEditingOperationStatus.Success ? Attempt.SucceedWithStatus(validationStatus, new ContentCreateResult { Content = content, ValidationResult = validationResult }) : Attempt.FailWithStatus(saveStatus, new ContentCreateResult { Content = content }); } public async Task> UpdateAsync(IContent content, ContentUpdateModel updateModel, Guid userKey) { Attempt result = await MapUpdate(content, updateModel); if (result.Success == false) { return Attempt.FailWithStatus(result.Status, result.Result); } // the update mapping might succeed, but this doesn't mean the model is valid at property level. // we'll return the actual property validation status if the entire operation succeeds. ContentEditingOperationStatus validationStatus = result.Status; ContentValidationResult validationResult = result.Result.ValidationResult; ContentEditingOperationStatus updateTemplateStatus = await UpdateTemplateAsync(content, updateModel.TemplateKey); if (updateTemplateStatus != ContentEditingOperationStatus.Success) { return Attempt.FailWithStatus(updateTemplateStatus, new ContentUpdateResult { Content = content }); } ContentEditingOperationStatus saveStatus = await Save(content, userKey); return saveStatus == ContentEditingOperationStatus.Success ? Attempt.SucceedWithStatus(validationStatus, new ContentUpdateResult { Content = content, ValidationResult = validationResult }) : Attempt.FailWithStatus(saveStatus, new ContentUpdateResult { Content = content }); } public async Task> MoveToRecycleBinAsync(Guid key, Guid userKey) => await HandleMoveToRecycleBinAsync(key, userKey); public async Task> DeleteFromRecycleBinAsync(Guid key, Guid userKey) => await HandleDeleteAsync(key, userKey, true); public async Task> DeleteAsync(Guid key, Guid userKey) => await HandleDeleteAsync(key, userKey, false); public async Task> MoveAsync(Guid key, Guid? parentKey, Guid userKey) => await HandleMoveAsync(key, parentKey, userKey); public async Task> CopyAsync(Guid key, Guid? parentKey, bool relateToOriginal, bool includeDescendants, Guid userKey) => await HandleCopyAsync(key, parentKey, relateToOriginal, includeDescendants, userKey); public async Task SortAsync(Guid? parentKey, IEnumerable sortingModels, Guid userKey) => await HandleSortAsync(parentKey, sortingModels, userKey); private async Task UpdateTemplateAsync(IContent content, Guid? templateKey) { if (templateKey == null) { content.TemplateId = null; return ContentEditingOperationStatus.Success; } ITemplate? template = await _templateService.GetAsync(templateKey.Value); if (template == null) { return ContentEditingOperationStatus.TemplateNotFound; } IContentType contentType = ContentTypeService.Get(content.ContentTypeId) ?? throw new ArgumentException("The content type was not found", nameof(content)); if (contentType.IsAllowedTemplate(template.Alias) == false) { return ContentEditingOperationStatus.TemplateNotAllowed; } content.TemplateId = template.Id; return ContentEditingOperationStatus.Success; } protected override IContent New(string? name, int parentId, IContentType contentType) => new Content(name, parentId, contentType); protected override OperationResult? Move(IContent content, int newParentId, int userId) => ContentService.Move(content, newParentId, userId); protected override IContent? Copy(IContent content, int newParentId, bool relateToOriginal, bool includeDescendants, int userId) => ContentService.Copy(content, newParentId, relateToOriginal, includeDescendants, userId); protected override OperationResult? MoveToRecycleBin(IContent content, int userId) => ContentService.MoveToRecycleBin(content, userId); protected override OperationResult? Delete(IContent content, int userId) => ContentService.Delete(content, userId); protected override IEnumerable GetPagedChildren(int parentId, int pageIndex, int pageSize, out long total) => ContentService.GetPagedChildren(parentId, pageIndex, pageSize, out total); protected override ContentEditingOperationStatus Sort(IEnumerable items, int userId) { OperationResult result = ContentService.Sort(items, userId); return OperationResultToOperationStatus(result); } private async Task Save(IContent content, Guid userKey) { try { var currentUserId = await GetUserIdAsync(userKey); OperationResult saveResult = ContentService.Save(content, currentUserId); return saveResult.Result switch { // these are the only result states currently expected from Save OperationResultType.Success => ContentEditingOperationStatus.Success, OperationResultType.FailedCancelledByEvent => ContentEditingOperationStatus.CancelledByNotification, // for any other state we'll return "unknown" so we know that we need to amend this _ => ContentEditingOperationStatus.Unknown }; } catch (Exception ex) { _logger.LogError(ex, "Content save operation failed"); return ContentEditingOperationStatus.Unknown; } } }