2024-11-05 06:32:34 +01:00
using Microsoft.Extensions.Logging ;
using Microsoft.Extensions.Options ;
using Umbraco.Cms.Core.Configuration.Models ;
2023-02-20 11:08:22 +01:00
using Umbraco.Cms.Core.Models ;
using Umbraco.Cms.Core.Models.ContentEditing ;
2024-09-18 13:10:15 +02:00
using Umbraco.Cms.Core.Models.Membership ;
2023-02-20 11:08:22 +01:00
using Umbraco.Cms.Core.PropertyEditors ;
using Umbraco.Cms.Core.Scoping ;
using Umbraco.Cms.Core.Services.OperationStatus ;
2024-09-18 13:10:15 +02:00
using Umbraco.Extensions ;
2023-02-20 11:08:22 +01:00
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
{
2024-11-05 06:32:34 +01:00
private readonly PropertyEditorCollection _propertyEditorCollection ;
2023-02-20 11:08:22 +01:00
private readonly ITemplateService _templateService ;
private readonly ILogger < ContentEditingService > _logger ;
2024-09-18 13:10:15 +02:00
private readonly IUserService _userService ;
private readonly ILocalizationService _localizationService ;
private readonly ILanguageService _languageService ;
2024-11-05 06:32:34 +01:00
private readonly ContentSettings _contentSettings ;
2024-09-18 13:10:15 +02:00
public ContentEditingService (
IContentService contentService ,
IContentTypeService contentTypeService ,
PropertyEditorCollection propertyEditorCollection ,
IDataTypeService dataTypeService ,
ITemplateService templateService ,
ILogger < ContentEditingService > logger ,
ICoreScopeProvider scopeProvider ,
IUserIdKeyResolver userIdKeyResolver ,
ITreeEntitySortingService treeEntitySortingService ,
IContentValidationService contentValidationService ,
IUserService userService ,
ILocalizationService localizationService ,
2024-11-05 06:32:34 +01:00
ILanguageService languageService ,
IOptions < ContentSettings > contentSettings )
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
{
2024-11-05 06:32:34 +01:00
_propertyEditorCollection = propertyEditorCollection ;
2023-02-20 11:08:22 +01:00
_templateService = templateService ;
_logger = logger ;
2024-09-18 13:10:15 +02:00
_userService = userService ;
_localizationService = localizationService ;
_languageService = languageService ;
2024-11-05 06:32:34 +01:00
_contentSettings = contentSettings . Value ;
2023-02-20 11:08:22 +01:00
}
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-09-25 15:34:14 +02:00
[Obsolete("Please use the validate update method that is not obsoleted. Will be removed in V16.")]
2024-03-01 10:45:19 +01:00
public async Task < Attempt < ContentValidationResult , ContentEditingOperationStatus > > ValidateUpdateAsync ( Guid key , ContentUpdateModel updateModel )
{
IContent ? content = ContentService . GetById ( key ) ;
return content is not null
2024-03-04 10:07:01 +01:00
? await ValidateCulturesAndPropertiesAsync ( updateModel , content . ContentType . Key )
2024-03-01 10:45:19 +01:00
: Attempt . FailWithStatus ( ContentEditingOperationStatus . NotFound , new ContentValidationResult ( ) ) ;
}
2024-01-31 10:40:58 +01:00
2024-09-25 15:34:14 +02:00
public async Task < Attempt < ContentValidationResult , ContentEditingOperationStatus > > ValidateUpdateAsync ( Guid key , ValidateContentUpdateModel updateModel )
{
IContent ? content = ContentService . GetById ( key ) ;
return content is not null
? await ValidateCulturesAndPropertiesAsync ( updateModel , content . ContentType . Key , updateModel . Cultures )
: Attempt . FailWithStatus ( ContentEditingOperationStatus . NotFound , new ContentValidationResult ( ) ) ;
}
2024-01-31 10:40:58 +01:00
public async Task < Attempt < ContentValidationResult , ContentEditingOperationStatus > > ValidateCreateAsync ( ContentCreateModel createModel )
2024-09-25 15:34:14 +02:00
= > await ValidateCulturesAndPropertiesAsync ( createModel , createModel . ContentTypeKey , createModel . Variants . Select ( variant = > variant . Culture ) ) ;
2024-01-31 10:40:58 +01:00
public async Task < Attempt < ContentCreateResult , ContentEditingOperationStatus > > CreateAsync ( ContentCreateModel createModel , Guid userKey )
2023-02-20 11:08:22 +01:00
{
2024-03-04 10:07:01 +01:00
if ( await ValidateCulturesAsync ( createModel ) is false )
{
return Attempt . FailWithStatus ( ContentEditingOperationStatus . InvalidCulture , new ContentCreateResult ( ) ) ;
}
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 ;
2024-09-18 13:10:15 +02:00
IContent content = await EnsureOnlyAllowedFieldsAreUpdated ( result . Result . Content ! , userKey ) ;
2024-01-31 10:40:58 +01:00
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-09-18 13:10:15 +02:00
/// <summary>
/// A temporary method that ensures the data is sent in is overridden by the original data, in cases where the user do not have permissions to change the data.
/// </summary>
private async Task < IContent > EnsureOnlyAllowedFieldsAreUpdated ( IContent contentWithPotentialUnallowedChanges , Guid userKey )
{
if ( contentWithPotentialUnallowedChanges . ContentType . VariesByCulture ( ) is false )
{
return contentWithPotentialUnallowedChanges ;
}
IContent ? existingContent = await GetAsync ( contentWithPotentialUnallowedChanges . Key ) ;
IUser ? user = await _userService . GetAsync ( userKey ) ;
if ( user is null )
{
return contentWithPotentialUnallowedChanges ;
}
var allowedLanguageIds = user . CalculateAllowedLanguageIds ( _localizationService ) ! ;
var allowedCultures = ( await _languageService . GetIsoCodesByIdsAsync ( allowedLanguageIds ) ) . ToHashSet ( ) ;
2024-11-05 06:32:34 +01:00
ILanguage ? defaultLanguage = await _languageService . GetDefaultLanguageAsync ( ) ;
2024-09-18 13:10:15 +02:00
foreach ( var culture in contentWithPotentialUnallowedChanges . EditedCultures ? ? contentWithPotentialUnallowedChanges . PublishedCultures )
{
if ( allowedCultures . Contains ( culture ) )
{
continue ;
}
// else override the updates values with the original values.
foreach ( IProperty property in contentWithPotentialUnallowedChanges . Properties )
{
2024-11-05 06:32:34 +01:00
// if the property varies by culture, simply overwrite the edited property value with the current property value
if ( property . PropertyType . VariesByCulture ( ) )
2024-09-18 13:10:15 +02:00
{
2024-11-05 06:32:34 +01:00
var currentValue = existingContent ? . Properties . First ( x = > x . Alias = = property . Alias ) . GetValue ( culture , null , false ) ;
property . SetValue ( currentValue , culture , null ) ;
2024-09-18 13:10:15 +02:00
continue ;
}
2024-11-05 06:32:34 +01:00
// if the property does not vary by culture and the data editor supports variance within invariant property values,
// we need perform a merge between the edited property value and the current property value
if ( _propertyEditorCollection . TryGet ( property . PropertyType . PropertyEditorAlias , out IDataEditor ? dataEditor ) & & dataEditor . CanMergePartialPropertyValues ( property . PropertyType ) )
{
var currentValue = existingContent ? . Properties . First ( x = > x . Alias = = property . Alias ) . GetValue ( null , null , false ) ;
var editedValue = contentWithPotentialUnallowedChanges . Properties . First ( x = > x . Alias = = property . Alias ) . GetValue ( null , null , false ) ;
var mergedValue = dataEditor . MergePartialPropertyValueForCulture ( currentValue , editedValue , culture ) ;
// If we are not allowed to edit invariant properties, overwrite the edited property value with the current property value.
if ( _contentSettings . AllowEditInvariantFromNonDefault is false & & culture = = defaultLanguage ? . IsoCode )
{
mergedValue = dataEditor . MergePartialPropertyValueForCulture ( currentValue , mergedValue , null ) ;
}
property . SetValue ( mergedValue , null , null ) ;
}
// If property does not support merging, we still need to overwrite if we are not allowed to edit invariant properties.
else if ( _contentSettings . AllowEditInvariantFromNonDefault is false & & culture = = defaultLanguage ? . IsoCode )
{
var currentValue = existingContent ? . Properties . First ( x = > x . Alias = = property . Alias ) . GetValue ( null , null , false ) ;
property . SetValue ( currentValue , null , null ) ;
}
2024-09-18 13:10:15 +02:00
}
}
2024-11-05 06:32:34 +01:00
return contentWithPotentialUnallowedChanges ;
2024-09-18 13:10:15 +02:00
}
2024-03-01 10:45:19 +01:00
public async Task < Attempt < ContentUpdateResult , ContentEditingOperationStatus > > UpdateAsync ( Guid key , ContentUpdateModel updateModel , Guid userKey )
2023-02-20 11:08:22 +01:00
{
2024-03-01 10:45:19 +01:00
IContent ? content = ContentService . GetById ( key ) ;
if ( content is null )
{
return Attempt . FailWithStatus ( ContentEditingOperationStatus . NotFound , new ContentUpdateResult ( ) ) ;
}
2024-03-04 10:07:01 +01:00
if ( await ValidateCulturesAsync ( updateModel ) is false )
{
return Attempt . FailWithStatus ( ContentEditingOperationStatus . InvalidCulture , new ContentUpdateResult { Content = content } ) ;
}
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 ;
2024-09-18 13:10:15 +02:00
content = await EnsureOnlyAllowedFieldsAreUpdated ( content , userKey ) ;
2024-01-31 10:40:58 +01:00
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
2024-03-04 10:07:01 +01:00
private async Task < Attempt < ContentValidationResult , ContentEditingOperationStatus > > ValidateCulturesAndPropertiesAsync (
ContentEditingModelBase contentEditingModelBase ,
2024-09-25 15:34:14 +02:00
Guid contentTypeKey ,
IEnumerable < string? > ? culturesToValidate = null )
2024-03-04 10:07:01 +01:00
= > await ValidateCulturesAsync ( contentEditingModelBase ) is false
? Attempt . FailWithStatus ( ContentEditingOperationStatus . InvalidCulture , new ContentValidationResult ( ) )
2024-09-25 15:34:14 +02:00
: await ValidatePropertiesAsync ( contentEditingModelBase , contentTypeKey , culturesToValidate ) ;
2024-03-04 10:07:01 +01:00
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 ;
}
}
}