2023-02-20 11:08:22 +01:00
|
|
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
|
|
using Umbraco.Cms.Core.Models;
|
|
|
|
|
|
using Umbraco.Cms.Core.Models.ContentEditing;
|
2024-01-31 10:40:58 +01:00
|
|
|
|
using Umbraco.Cms.Core.Models.ContentEditing.Validation;
|
2023-02-20 11:08:22 +01:00
|
|
|
|
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
|
2024-02-05 06:42:07 +01:00
|
|
|
|
: ContentEditingServiceWithSortingBase<IContent, IContentType, IContentService, IContentTypeService>, IContentEditingService
|
2023-02-20 11:08:22 +01:00
|
|
|
|
{
|
|
|
|
|
|
private readonly ITemplateService _templateService;
|
|
|
|
|
|
private readonly ILogger<ContentEditingService> _logger;
|
|
|
|
|
|
|
|
|
|
|
|
public ContentEditingService(
|
|
|
|
|
|
IContentService contentService,
|
|
|
|
|
|
IContentTypeService contentTypeService,
|
|
|
|
|
|
PropertyEditorCollection propertyEditorCollection,
|
|
|
|
|
|
IDataTypeService dataTypeService,
|
|
|
|
|
|
ITemplateService templateService,
|
|
|
|
|
|
ILogger<ContentEditingService> logger,
|
2023-03-21 12:41:20 +01:00
|
|
|
|
ICoreScopeProvider scopeProvider,
|
2023-08-28 08:56:57 +02:00
|
|
|
|
IUserIdKeyResolver userIdKeyResolver,
|
2024-01-31 10:40:58 +01:00
|
|
|
|
ITreeEntitySortingService treeEntitySortingService,
|
|
|
|
|
|
IContentValidationService contentValidationService)
|
2024-02-05 06:42:07 +01:00
|
|
|
|
: base(contentService, contentTypeService, propertyEditorCollection, dataTypeService, logger, scopeProvider, userIdKeyResolver, contentValidationService, treeEntitySortingService)
|
2023-02-20 11:08:22 +01:00
|
|
|
|
{
|
|
|
|
|
|
_templateService = templateService;
|
|
|
|
|
|
_logger = logger;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-08-28 08:56:57 +02:00
|
|
|
|
public async Task<IContent?> GetAsync(Guid key)
|
2023-02-21 09:46:23 +01:00
|
|
|
|
{
|
2023-08-28 08:56:57 +02:00
|
|
|
|
IContent? content = ContentService.GetById(key);
|
2023-02-21 09:46:23 +01:00
|
|
|
|
return await Task.FromResult(content);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-01-31 10:40:58 +01:00
|
|
|
|
public async Task<Attempt<ContentValidationResult, ContentEditingOperationStatus>> ValidateUpdateAsync(IContent content, ContentUpdateModel updateModel)
|
|
|
|
|
|
=> await ValidatePropertiesAsync(updateModel, content.ContentType.Key);
|
|
|
|
|
|
|
|
|
|
|
|
public async Task<Attempt<ContentValidationResult, ContentEditingOperationStatus>> ValidateCreateAsync(ContentCreateModel createModel)
|
|
|
|
|
|
=> await ValidatePropertiesAsync(createModel, createModel.ContentTypeKey);
|
|
|
|
|
|
|
|
|
|
|
|
public async Task<Attempt<ContentCreateResult, ContentEditingOperationStatus>> CreateAsync(ContentCreateModel createModel, Guid userKey)
|
2023-02-20 11:08:22 +01:00
|
|
|
|
{
|
2024-01-31 10:40:58 +01:00
|
|
|
|
Attempt<ContentCreateResult, ContentEditingOperationStatus> result = await MapCreate<ContentCreateResult>(createModel);
|
2023-02-20 11:08:22 +01:00
|
|
|
|
if (result.Success == false)
|
|
|
|
|
|
{
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-01-31 10:40:58 +01:00
|
|
|
|
// 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)
|
2023-02-20 11:08:22 +01:00
|
|
|
|
{
|
2024-01-31 10:40:58 +01:00
|
|
|
|
return Attempt.FailWithStatus(updateTemplateStatus, new ContentCreateResult { Content = content });
|
2023-02-20 11:08:22 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-01-31 10:40:58 +01:00
|
|
|
|
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 });
|
2023-02-20 11:08:22 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-01-31 10:40:58 +01:00
|
|
|
|
public async Task<Attempt<ContentUpdateResult, ContentEditingOperationStatus>> UpdateAsync(IContent content, ContentUpdateModel updateModel, Guid userKey)
|
2023-02-20 11:08:22 +01:00
|
|
|
|
{
|
2024-01-31 10:40:58 +01:00
|
|
|
|
Attempt<ContentUpdateResult, ContentEditingOperationStatus> result = await MapUpdate<ContentUpdateResult>(content, updateModel);
|
2023-02-20 11:08:22 +01:00
|
|
|
|
if (result.Success == false)
|
|
|
|
|
|
{
|
2024-01-31 10:40:58 +01:00
|
|
|
|
return Attempt.FailWithStatus(result.Status, result.Result);
|
2023-02-20 11:08:22 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-01-31 10:40:58 +01:00
|
|
|
|
// 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)
|
2023-02-20 11:08:22 +01:00
|
|
|
|
{
|
2024-01-31 10:40:58 +01:00
|
|
|
|
return Attempt.FailWithStatus(updateTemplateStatus, new ContentUpdateResult { Content = content });
|
2023-02-20 11:08:22 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-01-31 10:40:58 +01:00
|
|
|
|
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 });
|
2023-02-20 11:08:22 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2023-08-28 08:56:57 +02:00
|
|
|
|
public async Task<Attempt<IContent?, ContentEditingOperationStatus>> MoveToRecycleBinAsync(Guid key, Guid userKey)
|
|
|
|
|
|
=> await HandleMoveToRecycleBinAsync(key, userKey);
|
2023-02-20 11:08:22 +01:00
|
|
|
|
|
2024-01-22 15:58:18 +01:00
|
|
|
|
public async Task<Attempt<IContent?, ContentEditingOperationStatus>> DeleteFromRecycleBinAsync(Guid key, Guid userKey)
|
|
|
|
|
|
=> await HandleDeleteAsync(key, userKey, true);
|
|
|
|
|
|
|
2023-08-28 08:56:57 +02:00
|
|
|
|
public async Task<Attempt<IContent?, ContentEditingOperationStatus>> DeleteAsync(Guid key, Guid userKey)
|
2024-01-22 15:58:18 +01:00
|
|
|
|
=> await HandleDeleteAsync(key, userKey, false);
|
2023-02-20 11:08:22 +01:00
|
|
|
|
|
2023-08-28 08:56:57 +02:00
|
|
|
|
public async Task<Attempt<IContent?, ContentEditingOperationStatus>> MoveAsync(Guid key, Guid? parentKey, Guid userKey)
|
|
|
|
|
|
=> await HandleMoveAsync(key, parentKey, userKey);
|
2023-04-14 09:44:52 +02:00
|
|
|
|
|
2024-02-22 17:37:27 +01:00
|
|
|
|
public async Task<Attempt<IContent?, ContentEditingOperationStatus>> RestoreAsync(Guid key, Guid? parentKey, Guid userKey)
|
|
|
|
|
|
=> await HandleMoveAsync(key, parentKey, userKey, true);
|
|
|
|
|
|
|
2023-08-28 08:56:57 +02:00
|
|
|
|
public async Task<Attempt<IContent?, ContentEditingOperationStatus>> CopyAsync(Guid key, Guid? parentKey, bool relateToOriginal, bool includeDescendants, Guid userKey)
|
|
|
|
|
|
=> await HandleCopyAsync(key, parentKey, relateToOriginal, includeDescendants, userKey);
|
2023-04-14 09:44:52 +02:00
|
|
|
|
|
2023-08-28 08:56:57 +02:00
|
|
|
|
public async Task<ContentEditingOperationStatus> SortAsync(Guid? parentKey, IEnumerable<SortingModel> sortingModels, Guid userKey)
|
|
|
|
|
|
=> await HandleSortAsync(parentKey, sortingModels, userKey);
|
2023-02-20 11:08:22 +01:00
|
|
|
|
|
|
|
|
|
|
private async Task<ContentEditingOperationStatus> 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-08-28 08:56:57 +02:00
|
|
|
|
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<IContent> GetPagedChildren(int parentId, int pageIndex, int pageSize, out long total)
|
|
|
|
|
|
=> ContentService.GetPagedChildren(parentId, pageIndex, pageSize, out total);
|
|
|
|
|
|
|
|
|
|
|
|
protected override ContentEditingOperationStatus Sort(IEnumerable<IContent> items, int userId)
|
|
|
|
|
|
{
|
|
|
|
|
|
OperationResult result = ContentService.Sort(items, userId);
|
|
|
|
|
|
return OperationResultToOperationStatus(result);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-04-14 09:44:52 +02:00
|
|
|
|
private async Task<ContentEditingOperationStatus> Save(IContent content, Guid userKey)
|
2023-02-20 11:08:22 +01:00
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2023-04-14 09:44:52 +02:00
|
|
|
|
var currentUserId = await GetUserIdAsync(userKey);
|
2023-03-21 12:41:20 +01:00
|
|
|
|
OperationResult saveResult = ContentService.Save(content, currentUserId);
|
2023-02-20 11:08:22 +01:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|