2017-12-28 09:18:09 +01:00
using System ;
using System.Collections.Generic ;
using System.Globalization ;
using System.Linq ;
using Umbraco.Core.Events ;
using Umbraco.Core.Exceptions ;
using Umbraco.Core.Logging ;
using Umbraco.Core.Models ;
using Umbraco.Core.Models.Membership ;
using Umbraco.Core.Persistence.DatabaseModelDefinitions ;
using Umbraco.Core.Persistence.Querying ;
using Umbraco.Core.Persistence.Repositories ;
2019-02-21 14:46:14 +11:00
using Umbraco.Core.PropertyEditors ;
2017-12-28 09:18:09 +01:00
using Umbraco.Core.Scoping ;
using Umbraco.Core.Services.Changes ;
namespace Umbraco.Core.Services.Implement
{
/// <summary>
/// Implements the content service.
/// </summary>
2018-11-27 09:44:08 +01:00
public class ContentService : RepositoryService , IContentService
2017-12-28 09:18:09 +01:00
{
private readonly IDocumentRepository _documentRepository ;
private readonly IEntityRepository _entityRepository ;
private readonly IAuditRepository _auditRepository ;
private readonly IContentTypeRepository _contentTypeRepository ;
private readonly IDocumentBlueprintRepository _documentBlueprintRepository ;
2018-05-08 00:37:41 +10:00
private readonly ILanguageRepository _languageRepository ;
2017-12-28 09:18:09 +01:00
private IQuery < IContent > _queryNotTrashed ;
2019-02-21 14:46:14 +11:00
//TODO: The non-lazy object should be injected
private readonly Lazy < PropertyValidationService > _propertyValidationService = new Lazy < PropertyValidationService > ( ( ) = > new PropertyValidationService ( ) ) ;
2017-12-28 09:18:09 +01:00
#region Constructors
public ContentService ( IScopeProvider provider , ILogger logger ,
2019-01-31 14:03:09 +01:00
IEventMessagesFactory eventMessagesFactory ,
2017-12-28 09:18:09 +01:00
IDocumentRepository documentRepository , IEntityRepository entityRepository , IAuditRepository auditRepository ,
2018-05-08 00:37:41 +10:00
IContentTypeRepository contentTypeRepository , IDocumentBlueprintRepository documentBlueprintRepository , ILanguageRepository languageRepository )
2017-12-28 09:18:09 +01:00
: base ( provider , logger , eventMessagesFactory )
{
_documentRepository = documentRepository ;
_entityRepository = entityRepository ;
_auditRepository = auditRepository ;
_contentTypeRepository = contentTypeRepository ;
_documentBlueprintRepository = documentBlueprintRepository ;
2018-05-08 00:37:41 +10:00
_languageRepository = languageRepository ;
2017-12-28 09:18:09 +01:00
}
#endregion
#region Static queries
// lazy-constructed because when the ctor runs, the query factory may not be ready
private IQuery < IContent > QueryNotTrashed = > _queryNotTrashed ? ? ( _queryNotTrashed = Query < IContent > ( ) . Where ( x = > x . Trashed = = false ) ) ;
#endregion
#region Count
public int CountPublished ( string contentTypeAlias = null )
{
using ( var scope = ScopeProvider . CreateScope ( autoComplete : true ) )
{
scope . ReadLock ( Constants . Locks . ContentTree ) ;
2018-03-22 17:41:13 +01:00
return _documentRepository . CountPublished ( contentTypeAlias ) ;
2017-12-28 09:18:09 +01:00
}
}
public int Count ( string contentTypeAlias = null )
{
using ( var scope = ScopeProvider . CreateScope ( autoComplete : true ) )
{
scope . ReadLock ( Constants . Locks . ContentTree ) ;
return _documentRepository . Count ( contentTypeAlias ) ;
}
}
public int CountChildren ( int parentId , string contentTypeAlias = null )
{
using ( var scope = ScopeProvider . CreateScope ( autoComplete : true ) )
{
scope . ReadLock ( Constants . Locks . ContentTree ) ;
return _documentRepository . CountChildren ( parentId , contentTypeAlias ) ;
}
}
public int CountDescendants ( int parentId , string contentTypeAlias = null )
{
using ( var scope = ScopeProvider . CreateScope ( autoComplete : true ) )
{
scope . ReadLock ( Constants . Locks . ContentTree ) ;
return _documentRepository . CountDescendants ( parentId , contentTypeAlias ) ;
}
}
#endregion
#region Permissions
/// <summary>
/// Used to bulk update the permissions set for a content item. This will replace all permissions
/// assigned to an entity with a list of user id & permission pairs.
/// </summary>
/// <param name="permissionSet"></param>
public void SetPermissions ( EntityPermissionSet permissionSet )
{
using ( var scope = ScopeProvider . CreateScope ( ) )
{
scope . WriteLock ( Constants . Locks . ContentTree ) ;
_documentRepository . ReplaceContentPermissions ( permissionSet ) ;
scope . Complete ( ) ;
}
}
/// <summary>
/// Assigns a single permission to the current content item for the specified group ids
/// </summary>
/// <param name="entity"></param>
/// <param name="permission"></param>
/// <param name="groupIds"></param>
public void SetPermission ( IContent entity , char permission , IEnumerable < int > groupIds )
{
using ( var scope = ScopeProvider . CreateScope ( ) )
{
scope . WriteLock ( Constants . Locks . ContentTree ) ;
_documentRepository . AssignEntityPermission ( entity , permission , groupIds ) ;
scope . Complete ( ) ;
}
}
/// <summary>
/// Returns implicit/inherited permissions assigned to the content item for all user groups
/// </summary>
/// <param name="content"></param>
/// <returns></returns>
public EntityPermissionCollection GetPermissions ( IContent content )
{
using ( var scope = ScopeProvider . CreateScope ( autoComplete : true ) )
{
scope . ReadLock ( Constants . Locks . ContentTree ) ;
return _documentRepository . GetPermissionsForEntity ( content . Id ) ;
}
}
#endregion
#region Create
/// <summary>
/// Creates an <see cref="IContent"/> object using the alias of the <see cref="IContentType"/>
/// that this Content should based on.
/// </summary>
/// <remarks>
/// Note that using this method will simply return a new IContent without any identity
/// as it has not yet been persisted. It is intended as a shortcut to creating new content objects
/// that does not invoke a save operation against the database.
/// </remarks>
/// <param name="name">Name of the Content object</param>
/// <param name="parentId">Id of Parent for the new Content</param>
/// <param name="contentTypeAlias">Alias of the <see cref="IContentType"/></param>
/// <param name="userId">Optional id of the user creating the content</param>
/// <returns><see cref="IContent"/></returns>
2019-02-06 14:01:14 +00:00
public IContent Create ( string name , Guid parentId , string contentTypeAlias , int userId = Constants . Security . SuperUserId )
2017-12-28 09:18:09 +01:00
{
2019-01-26 09:42:14 -05:00
// TODO: what about culture?
2018-05-02 14:52:00 +10:00
2017-12-28 09:18:09 +01:00
var parent = GetById ( parentId ) ;
return Create ( name , parent , contentTypeAlias , userId ) ;
}
/// <summary>
/// Creates an <see cref="IContent"/> object of a specified content type.
/// </summary>
/// <remarks>This method simply returns a new, non-persisted, IContent without any identity. It
/// is intended as a shortcut to creating new content objects that does not invoke a save
/// operation against the database.
/// </remarks>
/// <param name="name">The name of the content object.</param>
/// <param name="parentId">The identifier of the parent, or -1.</param>
/// <param name="contentTypeAlias">The alias of the content type.</param>
/// <param name="userId">The optional id of the user creating the content.</param>
/// <returns>The content object.</returns>
2019-02-06 14:01:14 +00:00
public IContent Create ( string name , int parentId , string contentTypeAlias , int userId = Constants . Security . SuperUserId )
2017-12-28 09:18:09 +01:00
{
2019-01-26 09:42:14 -05:00
// TODO: what about culture?
2018-05-02 14:52:00 +10:00
2017-12-28 09:18:09 +01:00
var contentType = GetContentType ( contentTypeAlias ) ;
if ( contentType = = null )
throw new ArgumentException ( "No content type with that alias." , nameof ( contentTypeAlias ) ) ;
var parent = parentId > 0 ? GetById ( parentId ) : null ;
if ( parentId > 0 & & parent = = null )
throw new ArgumentException ( "No content with that id." , nameof ( parentId ) ) ;
var content = new Content ( name , parentId , contentType ) ;
using ( var scope = ScopeProvider . CreateScope ( ) )
{
2019-02-21 14:46:14 +11:00
CreateContent ( scope , content , userId , false ) ;
2017-12-28 09:18:09 +01:00
scope . Complete ( ) ;
}
return content ;
}
/// <summary>
/// Creates an <see cref="IContent"/> object of a specified content type, under a parent.
/// </summary>
/// <remarks>This method simply returns a new, non-persisted, IContent without any identity. It
/// is intended as a shortcut to creating new content objects that does not invoke a save
/// operation against the database.
/// </remarks>
/// <param name="name">The name of the content object.</param>
/// <param name="parent">The parent content object.</param>
/// <param name="contentTypeAlias">The alias of the content type.</param>
/// <param name="userId">The optional id of the user creating the content.</param>
/// <returns>The content object.</returns>
2019-02-06 14:01:14 +00:00
public IContent Create ( string name , IContent parent , string contentTypeAlias , int userId = Constants . Security . SuperUserId )
2017-12-28 09:18:09 +01:00
{
2019-01-26 09:42:14 -05:00
// TODO: what about culture?
2018-05-02 14:52:00 +10:00
2017-12-28 09:18:09 +01:00
if ( parent = = null ) throw new ArgumentNullException ( nameof ( parent ) ) ;
using ( var scope = ScopeProvider . CreateScope ( ) )
{
// not locking since not saving anything
var contentType = GetContentType ( contentTypeAlias ) ;
if ( contentType = = null )
throw new ArgumentException ( "No content type with that alias." , nameof ( contentTypeAlias ) ) ; // causes rollback
var content = new Content ( name , parent , contentType ) ;
2019-02-21 14:46:14 +11:00
CreateContent ( scope , content , userId , false ) ;
2017-12-28 09:18:09 +01:00
scope . Complete ( ) ;
return content ;
}
}
/// <summary>
/// Creates an <see cref="IContent"/> object of a specified content type.
/// </summary>
/// <remarks>This method returns a new, persisted, IContent with an identity.</remarks>
/// <param name="name">The name of the content object.</param>
/// <param name="parentId">The identifier of the parent, or -1.</param>
/// <param name="contentTypeAlias">The alias of the content type.</param>
/// <param name="userId">The optional id of the user creating the content.</param>
/// <returns>The content object.</returns>
2019-02-06 14:01:14 +00:00
public IContent CreateAndSave ( string name , int parentId , string contentTypeAlias , int userId = Constants . Security . SuperUserId )
2017-12-28 09:18:09 +01:00
{
2019-01-26 09:42:14 -05:00
// TODO: what about culture?
2018-05-02 14:52:00 +10:00
2017-12-28 09:18:09 +01:00
using ( var scope = ScopeProvider . CreateScope ( ) )
{
// locking the content tree secures content types too
scope . WriteLock ( Constants . Locks . ContentTree ) ;
var contentType = GetContentType ( contentTypeAlias ) ; // + locks
if ( contentType = = null )
throw new ArgumentException ( "No content type with that alias." , nameof ( contentTypeAlias ) ) ; // causes rollback
var parent = parentId > 0 ? GetById ( parentId ) : null ; // + locks
if ( parentId > 0 & & parent = = null )
throw new ArgumentException ( "No content with that id." , nameof ( parentId ) ) ; // causes rollback
var content = parentId > 0 ? new Content ( name , parent , contentType ) : new Content ( name , parentId , contentType ) ;
2019-02-21 14:46:14 +11:00
CreateContent ( scope , content , userId , true ) ;
2017-12-28 09:18:09 +01:00
scope . Complete ( ) ;
return content ;
}
}
/// <summary>
/// Creates an <see cref="IContent"/> object of a specified content type, under a parent.
/// </summary>
/// <remarks>This method returns a new, persisted, IContent with an identity.</remarks>
/// <param name="name">The name of the content object.</param>
/// <param name="parent">The parent content object.</param>
/// <param name="contentTypeAlias">The alias of the content type.</param>
/// <param name="userId">The optional id of the user creating the content.</param>
/// <returns>The content object.</returns>
2019-02-06 14:01:14 +00:00
public IContent CreateAndSave ( string name , IContent parent , string contentTypeAlias , int userId = Constants . Security . SuperUserId )
2017-12-28 09:18:09 +01:00
{
2019-01-26 09:42:14 -05:00
// TODO: what about culture?
2018-05-02 14:52:00 +10:00
2017-12-28 09:18:09 +01:00
if ( parent = = null ) throw new ArgumentNullException ( nameof ( parent ) ) ;
using ( var scope = ScopeProvider . CreateScope ( ) )
{
// locking the content tree secures content types too
scope . WriteLock ( Constants . Locks . ContentTree ) ;
var contentType = GetContentType ( contentTypeAlias ) ; // + locks
if ( contentType = = null )
throw new ArgumentException ( "No content type with that alias." , nameof ( contentTypeAlias ) ) ; // causes rollback
var content = new Content ( name , parent , contentType ) ;
2019-02-21 14:46:14 +11:00
CreateContent ( scope , content , userId , true ) ;
2017-12-28 09:18:09 +01:00
scope . Complete ( ) ;
return content ;
}
}
2019-02-21 14:46:14 +11:00
private void CreateContent ( IScope scope , IContent content , int userId , bool withIdentity )
2017-12-28 09:18:09 +01:00
{
content . CreatorId = userId ;
content . WriterId = userId ;
if ( withIdentity )
{
2019-02-06 16:10:20 +11:00
var evtMsgs = EventMessagesFactory . Get ( ) ;
2018-01-10 12:48:51 +01:00
// if saving is cancelled, content remains without an identity
2019-02-06 16:10:20 +11:00
var saveEventArgs = new ContentSavingEventArgs ( content , evtMsgs ) ;
2019-02-21 14:46:14 +11:00
if ( scope . Events . DispatchCancelable ( Saving , this , saveEventArgs , nameof ( Saving ) ) )
2017-12-28 09:18:09 +01:00
return ;
_documentRepository . Save ( content ) ;
2019-02-21 14:46:14 +11:00
scope . Events . Dispatch ( Saved , this , saveEventArgs . ToContentSavedEventArgs ( ) , nameof ( Saved ) ) ;
2017-12-28 09:18:09 +01:00
scope . Events . Dispatch ( TreeChanged , this , new TreeChange < IContent > ( content , TreeChangeTypes . RefreshNode ) . ToEventArgs ( ) ) ;
}
if ( withIdentity = = false )
return ;
2018-10-18 22:47:12 +11:00
Audit ( AuditType . New , content . CreatorId , content . Id , $"Content '{content.Name}' was created with Id {content.Id}" ) ;
2017-12-28 09:18:09 +01:00
}
#endregion
#region Get , Has , Is
/// <summary>
/// Gets an <see cref="IContent"/> object by Id
/// </summary>
/// <param name="id">Id of the Content to retrieve</param>
/// <returns><see cref="IContent"/></returns>
public IContent GetById ( int id )
{
using ( var scope = ScopeProvider . CreateScope ( autoComplete : true ) )
{
scope . ReadLock ( Constants . Locks . ContentTree ) ;
return _documentRepository . Get ( id ) ;
}
}
/// <summary>
/// Gets an <see cref="IContent"/> object by Id
/// </summary>
/// <param name="ids">Ids of the Content to retrieve</param>
/// <returns><see cref="IContent"/></returns>
public IEnumerable < IContent > GetByIds ( IEnumerable < int > ids )
{
var idsA = ids . ToArray ( ) ;
if ( idsA . Length = = 0 ) return Enumerable . Empty < IContent > ( ) ;
using ( var scope = ScopeProvider . CreateScope ( autoComplete : true ) )
{
scope . ReadLock ( Constants . Locks . ContentTree ) ;
var items = _documentRepository . GetMany ( idsA ) ;
var index = items . ToDictionary ( x = > x . Id , x = > x ) ;
return idsA . Select ( x = > index . TryGetValue ( x , out var c ) ? c : null ) . WhereNotNull ( ) ;
}
}
/// <summary>
/// Gets an <see cref="IContent"/> object by its 'UniqueId'
/// </summary>
/// <param name="key">Guid key of the Content to retrieve</param>
/// <returns><see cref="IContent"/></returns>
public IContent GetById ( Guid key )
{
using ( var scope = ScopeProvider . CreateScope ( autoComplete : true ) )
{
scope . ReadLock ( Constants . Locks . ContentTree ) ;
return _documentRepository . Get ( key ) ;
}
}
/// <summary>
/// Gets <see cref="IContent"/> objects by Ids
/// </summary>
/// <param name="ids">Ids of the Content to retrieve</param>
/// <returns><see cref="IContent"/></returns>
public IEnumerable < IContent > GetByIds ( IEnumerable < Guid > ids )
{
var idsA = ids . ToArray ( ) ;
if ( idsA . Length = = 0 ) return Enumerable . Empty < IContent > ( ) ;
using ( var scope = ScopeProvider . CreateScope ( autoComplete : true ) )
{
scope . ReadLock ( Constants . Locks . ContentTree ) ;
var items = _documentRepository . GetMany ( idsA ) ;
var index = items . ToDictionary ( x = > x . Key , x = > x ) ;
return idsA . Select ( x = > index . TryGetValue ( x , out var c ) ? c : null ) . WhereNotNull ( ) ;
}
}
2018-11-01 00:05:17 +11:00
/// <inheritdoc />
2018-11-01 10:28:53 +11:00
public IEnumerable < IContent > GetPagedOfType ( int contentTypeId , long pageIndex , int pageSize , out long totalRecords
, IQuery < IContent > filter = null , Ordering ordering = null )
2017-12-28 09:18:09 +01:00
{
2018-11-07 22:18:43 +11:00
if ( pageIndex < 0 ) throw new ArgumentOutOfRangeException ( nameof ( pageIndex ) ) ;
2018-10-30 17:32:27 +11:00
if ( pageSize < = 0 ) throw new ArgumentOutOfRangeException ( nameof ( pageSize ) ) ;
if ( ordering = = null )
ordering = Ordering . By ( "sortOrder" ) ;
2017-12-28 09:18:09 +01:00
using ( var scope = ScopeProvider . CreateScope ( autoComplete : true ) )
{
scope . ReadLock ( Constants . Locks . ContentTree ) ;
2018-10-30 17:32:27 +11:00
return _documentRepository . GetPage (
Query < IContent > ( ) . Where ( x = > x . ContentTypeId = = contentTypeId ) ,
pageIndex , pageSize , out totalRecords , filter , ordering ) ;
2017-12-28 09:18:09 +01:00
}
}
2018-11-01 00:05:17 +11:00
/// <inheritdoc />
public IEnumerable < IContent > GetPagedOfTypes ( int [ ] contentTypeIds , long pageIndex , int pageSize , out long totalRecords , IQuery < IContent > filter , Ordering ordering = null )
2017-12-28 09:18:09 +01:00
{
2018-11-01 00:05:17 +11:00
if ( pageIndex < 0 ) throw new ArgumentOutOfRangeException ( nameof ( pageIndex ) ) ;
if ( pageSize < = 0 ) throw new ArgumentOutOfRangeException ( nameof ( pageSize ) ) ;
if ( ordering = = null )
ordering = Ordering . By ( "sortOrder" ) ;
2017-12-28 09:18:09 +01:00
using ( var scope = ScopeProvider . CreateScope ( autoComplete : true ) )
{
scope . ReadLock ( Constants . Locks . ContentTree ) ;
2018-11-01 00:05:17 +11:00
return _documentRepository . GetPage (
Query < IContent > ( ) . Where ( x = > contentTypeIds . Contains ( x . ContentTypeId ) ) ,
pageIndex , pageSize , out totalRecords , filter , ordering ) ;
2017-12-28 09:18:09 +01:00
}
}
/// <summary>
/// Gets a collection of <see cref="IContent"/> objects by Level
/// </summary>
/// <param name="level">The level to retrieve Content from</param>
/// <returns>An Enumerable list of <see cref="IContent"/> objects</returns>
/// <remarks>Contrary to most methods, this method filters out trashed content items.</remarks>
public IEnumerable < IContent > GetByLevel ( int level )
{
using ( var scope = ScopeProvider . CreateScope ( autoComplete : true ) )
{
scope . ReadLock ( Constants . Locks . ContentTree ) ;
var query = Query < IContent > ( ) . Where ( x = > x . Level = = level & & x . Trashed = = false ) ;
return _documentRepository . Get ( query ) ;
}
}
/// <summary>
/// Gets a specific version of an <see cref="IContent"/> item.
/// </summary>
/// <param name="versionId">Id of the version to retrieve</param>
/// <returns>An <see cref="IContent"/> item</returns>
public IContent GetVersion ( int versionId )
{
using ( var scope = ScopeProvider . CreateScope ( autoComplete : true ) )
{
scope . ReadLock ( Constants . Locks . ContentTree ) ;
return _documentRepository . GetVersion ( versionId ) ;
}
}
/// <summary>
/// Gets a collection of an <see cref="IContent"/> objects versions by Id
/// </summary>
/// <param name="id"></param>
/// <returns>An Enumerable list of <see cref="IContent"/> objects</returns>
public IEnumerable < IContent > GetVersions ( int id )
{
using ( var scope = ScopeProvider . CreateScope ( autoComplete : true ) )
{
scope . ReadLock ( Constants . Locks . ContentTree ) ;
return _documentRepository . GetAllVersions ( id ) ;
}
}
2018-10-22 08:45:30 +02:00
/// <summary>
/// Gets a collection of an <see cref="IContent"/> objects versions by Id
/// </summary>
/// <returns>An Enumerable list of <see cref="IContent"/> objects</returns>
public IEnumerable < IContent > GetVersionsSlim ( int id , int skip , int take )
{
using ( var scope = ScopeProvider . CreateScope ( autoComplete : true ) )
{
scope . ReadLock ( Constants . Locks . ContentTree ) ;
return _documentRepository . GetAllVersionsSlim ( id , skip , take ) ;
}
}
2017-12-28 09:18:09 +01:00
/// <summary>
/// Gets a list of all version Ids for the given content item ordered so latest is first
/// </summary>
/// <param name="id"></param>
/// <param name="maxRows">The maximum number of rows to return</param>
/// <returns></returns>
public IEnumerable < int > GetVersionIds ( int id , int maxRows )
{
using ( var scope = ScopeProvider . CreateScope ( autoComplete : true ) )
{
return _documentRepository . GetVersionIds ( id , maxRows ) ;
}
}
/// <summary>
/// Gets a collection of <see cref="IContent"/> objects, which are ancestors of the current content.
/// </summary>
/// <param name="id">Id of the <see cref="IContent"/> to retrieve ancestors for</param>
/// <returns>An Enumerable list of <see cref="IContent"/> objects</returns>
public IEnumerable < IContent > GetAncestors ( int id )
{
2019-01-22 18:03:39 -05:00
// intentionally not locking
2017-12-28 09:18:09 +01:00
var content = GetById ( id ) ;
return GetAncestors ( content ) ;
}
/// <summary>
/// Gets a collection of <see cref="IContent"/> objects, which are ancestors of the current content.
/// </summary>
/// <param name="content"><see cref="IContent"/> to retrieve ancestors for</param>
/// <returns>An Enumerable list of <see cref="IContent"/> objects</returns>
public IEnumerable < IContent > GetAncestors ( IContent content )
{
//null check otherwise we get exceptions
if ( content . Path . IsNullOrWhiteSpace ( ) ) return Enumerable . Empty < IContent > ( ) ;
2019-03-29 12:06:23 +00:00
var rootId = Constants . System . RootString ;
2017-12-28 09:18:09 +01:00
var ids = content . Path . Split ( ',' )
. Where ( x = > x ! = rootId & & x ! = content . Id . ToString ( CultureInfo . InvariantCulture ) ) . Select ( int . Parse ) . ToArray ( ) ;
if ( ids . Any ( ) = = false )
return new List < IContent > ( ) ;
using ( var scope = ScopeProvider . CreateScope ( autoComplete : true ) )
{
scope . ReadLock ( Constants . Locks . ContentTree ) ;
return _documentRepository . GetMany ( ids ) ;
}
}
/// <summary>
/// Gets a collection of published <see cref="IContent"/> objects by Parent Id
/// </summary>
/// <param name="id">Id of the Parent to retrieve Children from</param>
/// <returns>An Enumerable list of published <see cref="IContent"/> objects</returns>
public IEnumerable < IContent > GetPublishedChildren ( int id )
{
using ( var scope = ScopeProvider . CreateScope ( autoComplete : true ) )
{
scope . ReadLock ( Constants . Locks . ContentTree ) ;
var query = Query < IContent > ( ) . Where ( x = > x . ParentId = = id & & x . Published ) ;
return _documentRepository . Get ( query ) . OrderBy ( x = > x . SortOrder ) ;
}
}
2018-09-18 11:53:33 +02:00
/// <inheritdoc />
2017-12-28 09:18:09 +01:00
public IEnumerable < IContent > GetPagedChildren ( int id , long pageIndex , int pageSize , out long totalChildren ,
2018-11-01 10:28:53 +11:00
IQuery < IContent > filter = null , Ordering ordering = null )
2017-12-28 09:18:09 +01:00
{
if ( pageIndex < 0 ) throw new ArgumentOutOfRangeException ( nameof ( pageIndex ) ) ;
if ( pageSize < = 0 ) throw new ArgumentOutOfRangeException ( nameof ( pageSize ) ) ;
2018-09-18 11:53:33 +02:00
if ( ordering = = null )
ordering = Ordering . By ( "sortOrder" ) ;
2017-12-28 09:18:09 +01:00
using ( var scope = ScopeProvider . CreateScope ( autoComplete : true ) )
{
scope . ReadLock ( Constants . Locks . ContentTree ) ;
2018-09-18 11:53:33 +02:00
var query = Query < IContent > ( ) . Where ( x = > x . ParentId = = id ) ;
return _documentRepository . GetPage ( query , pageIndex , pageSize , out totalChildren , filter , ordering ) ;
2017-12-28 09:18:09 +01:00
}
}
2018-09-18 11:53:33 +02:00
/// <inheritdoc />
2018-11-01 00:05:17 +11:00
public IEnumerable < IContent > GetPagedDescendants ( int id , long pageIndex , int pageSize , out long totalChildren ,
2018-11-01 10:28:53 +11:00
IQuery < IContent > filter = null , Ordering ordering = null )
2017-12-28 09:18:09 +01:00
{
2018-11-01 00:05:17 +11:00
if ( ordering = = null )
ordering = Ordering . By ( "Path" ) ;
2017-12-28 09:18:09 +01:00
using ( var scope = ScopeProvider . CreateScope ( autoComplete : true ) )
{
scope . ReadLock ( Constants . Locks . ContentTree ) ;
//if the id is System Root, then just get all
if ( id ! = Constants . System . Root )
{
var contentPath = _entityRepository . GetAllPaths ( Constants . ObjectTypes . Document , id ) . ToArray ( ) ;
if ( contentPath . Length = = 0 )
{
totalChildren = 0 ;
return Enumerable . Empty < IContent > ( ) ;
}
2018-11-01 00:05:17 +11:00
return GetPagedDescendantsLocked ( contentPath [ 0 ] . Path , pageIndex , pageSize , out totalChildren , filter , ordering ) ;
2017-12-28 09:18:09 +01:00
}
2018-11-01 00:05:17 +11:00
return GetPagedDescendantsLocked ( null , pageIndex , pageSize , out totalChildren , filter , ordering ) ;
2017-12-28 09:18:09 +01:00
}
}
2018-11-01 00:05:17 +11:00
private IEnumerable < IContent > GetPagedDescendantsLocked ( string contentPath , long pageIndex , int pageSize , out long totalChildren ,
IQuery < IContent > filter , Ordering ordering )
2017-12-28 09:18:09 +01:00
{
2018-10-31 22:38:58 +11:00
if ( pageIndex < 0 ) throw new ArgumentOutOfRangeException ( nameof ( pageIndex ) ) ;
if ( pageSize < = 0 ) throw new ArgumentOutOfRangeException ( nameof ( pageSize ) ) ;
2018-11-01 00:05:17 +11:00
if ( ordering = = null ) throw new ArgumentNullException ( nameof ( ordering ) ) ;
2017-12-28 09:18:09 +01:00
2018-10-31 23:11:37 +11:00
var query = Query < IContent > ( ) ;
if ( ! contentPath . IsNullOrWhiteSpace ( ) )
query . Where ( x = > x . Path . SqlStartsWith ( $"{contentPath}," , TextColumnType . NVarchar ) ) ;
2017-12-28 09:18:09 +01:00
2018-11-01 00:05:17 +11:00
return _documentRepository . GetPage ( query , pageIndex , pageSize , out totalChildren , filter , ordering ) ;
2017-12-28 09:18:09 +01:00
}
/// <summary>
/// Gets the parent of the current content as an <see cref="IContent"/> item.
/// </summary>
/// <param name="id">Id of the <see cref="IContent"/> to retrieve the parent from</param>
/// <returns>Parent <see cref="IContent"/> object</returns>
public IContent GetParent ( int id )
{
2019-01-22 18:03:39 -05:00
// intentionally not locking
2017-12-28 09:18:09 +01:00
var content = GetById ( id ) ;
return GetParent ( content ) ;
}
/// <summary>
/// Gets the parent of the current content as an <see cref="IContent"/> item.
/// </summary>
/// <param name="content"><see cref="IContent"/> to retrieve the parent from</param>
/// <returns>Parent <see cref="IContent"/> object</returns>
public IContent GetParent ( IContent content )
{
if ( content . ParentId = = Constants . System . Root | | content . ParentId = = Constants . System . RecycleBinContent )
return null ;
return GetById ( content . ParentId ) ;
}
/// <summary>
/// Gets a collection of <see cref="IContent"/> objects, which reside at the first level / root
/// </summary>
/// <returns>An Enumerable list of <see cref="IContent"/> objects</returns>
public IEnumerable < IContent > GetRootContent ( )
{
using ( var scope = ScopeProvider . CreateScope ( autoComplete : true ) )
{
scope . ReadLock ( Constants . Locks . ContentTree ) ;
var query = Query < IContent > ( ) . Where ( x = > x . ParentId = = Constants . System . Root ) ;
return _documentRepository . Get ( query ) ;
}
}
/// <summary>
/// Gets all published content items
/// </summary>
/// <returns></returns>
internal IEnumerable < IContent > GetAllPublished ( )
{
using ( var scope = ScopeProvider . CreateScope ( autoComplete : true ) )
{
scope . ReadLock ( Constants . Locks . ContentTree ) ;
return _documentRepository . Get ( QueryNotTrashed ) ;
}
}
2018-11-07 19:42:49 +11:00
/// <inheritdoc />
public IEnumerable < IContent > GetContentForExpiration ( DateTime date )
2017-12-28 09:18:09 +01:00
{
using ( var scope = ScopeProvider . CreateScope ( autoComplete : true ) )
{
scope . ReadLock ( Constants . Locks . ContentTree ) ;
2018-11-07 19:42:49 +11:00
return _documentRepository . GetContentForExpiration ( date ) ;
2017-12-28 09:18:09 +01:00
}
}
2018-11-07 19:42:49 +11:00
/// <inheritdoc />
public IEnumerable < IContent > GetContentForRelease ( DateTime date )
2017-12-28 09:18:09 +01:00
{
using ( var scope = ScopeProvider . CreateScope ( autoComplete : true ) )
{
scope . ReadLock ( Constants . Locks . ContentTree ) ;
2018-11-07 19:42:49 +11:00
return _documentRepository . GetContentForRelease ( date ) ;
2017-12-28 09:18:09 +01:00
}
}
/// <summary>
/// Gets a collection of an <see cref="IContent"/> objects, which resides in the Recycle Bin
/// </summary>
/// <returns>An Enumerable list of <see cref="IContent"/> objects</returns>
2018-11-01 10:22:45 +11:00
public IEnumerable < IContent > GetPagedContentInRecycleBin ( long pageIndex , int pageSize , out long totalRecords ,
IQuery < IContent > filter = null , Ordering ordering = null )
2017-12-28 09:18:09 +01:00
{
using ( var scope = ScopeProvider . CreateScope ( autoComplete : true ) )
{
2018-11-01 10:22:45 +11:00
if ( ordering = = null )
ordering = Ordering . By ( "Path" ) ;
2017-12-28 09:18:09 +01:00
scope . ReadLock ( Constants . Locks . ContentTree ) ;
2018-03-22 17:41:13 +01:00
var query = Query < IContent > ( ) . Where ( x = > x . Path . StartsWith ( Constants . System . RecycleBinContentPathPrefix ) ) ;
2018-11-01 10:22:45 +11:00
return _documentRepository . GetPage ( query , pageIndex , pageSize , out totalRecords , filter , ordering ) ;
2017-12-28 09:18:09 +01:00
}
}
/// <summary>
/// Checks whether an <see cref="IContent"/> item has any children
/// </summary>
/// <param name="id">Id of the <see cref="IContent"/></param>
/// <returns>True if the content has any children otherwise False</returns>
public bool HasChildren ( int id )
{
return CountChildren ( id ) > 0 ;
}
/// <summary>
2019-01-22 18:03:39 -05:00
/// Checks if the passed in <see cref="IContent"/> can be published based on the ancestors publish state.
2017-12-28 09:18:09 +01:00
/// </summary>
2019-01-22 18:03:39 -05:00
/// <param name="content"><see cref="IContent"/> to check if ancestors are published</param>
2017-12-28 09:18:09 +01:00
/// <returns>True if the Content can be published, otherwise False</returns>
public bool IsPathPublishable ( IContent content )
{
// fast
if ( content . ParentId = = Constants . System . Root ) return true ; // root content is always publishable
if ( content . Trashed ) return false ; // trashed content is never publishable
// not trashed and has a parent: publishable if the parent is path-published
var parent = GetById ( content . ParentId ) ;
return parent = = null | | IsPathPublished ( parent ) ;
}
public bool IsPathPublished ( IContent content )
{
using ( var scope = ScopeProvider . CreateScope ( autoComplete : true ) )
{
scope . ReadLock ( Constants . Locks . ContentTree ) ;
return _documentRepository . IsPathPublished ( content ) ;
}
}
#endregion
#region Save , Publish , Unpublish
/// <inheritdoc />
2019-02-06 14:01:14 +00:00
public OperationResult Save ( IContent content , int userId = Constants . Security . SuperUserId , bool raiseEvents = true )
2017-12-28 09:18:09 +01:00
{
2018-05-07 23:22:52 +10:00
var publishedState = content . PublishedState ;
2017-12-28 09:18:09 +01:00
if ( publishedState ! = PublishedState . Published & & publishedState ! = PublishedState . Unpublished )
2018-06-22 21:03:47 +02:00
throw new InvalidOperationException ( "Cannot save (un)publishing content, use the dedicated SavePublished method." ) ;
2017-12-28 09:18:09 +01:00
var evtMsgs = EventMessagesFactory . Get ( ) ;
using ( var scope = ScopeProvider . CreateScope ( ) )
{
2019-02-06 16:10:20 +11:00
var saveEventArgs = new ContentSavingEventArgs ( content , evtMsgs ) ;
2019-02-21 14:46:14 +11:00
if ( raiseEvents & & scope . Events . DispatchCancelable ( Saving , this , saveEventArgs , nameof ( Saving ) ) )
2017-12-28 09:18:09 +01:00
{
scope . Complete ( ) ;
return OperationResult . Cancel ( evtMsgs ) ;
}
scope . WriteLock ( Constants . Locks . ContentTree ) ;
if ( content . HasIdentity = = false )
content . CreatorId = userId ;
content . WriterId = userId ;
2018-10-19 13:24:36 +11:00
//track the cultures that have changed
var culturesChanging = content . ContentType . VariesByCulture ( )
2019-02-05 14:13:03 +11:00
? content . CultureInfos . Values . Where ( x = > x . IsDirty ( ) ) . Select ( x = > x . Culture ) . ToList ( )
2018-10-19 13:24:36 +11:00
: null ;
2019-01-27 01:17:32 -05:00
// TODO: Currently there's no way to change track which variant properties have changed, we only have change
2018-10-19 13:24:36 +11:00
// tracking enabled on all values on the Property which doesn't allow us to know which variants have changed.
// in this particular case, determining which cultures have changed works with the above with names since it will
// have always changed if it's been saved in the back office but that's not really fail safe.
2017-12-28 09:18:09 +01:00
_documentRepository . Save ( content ) ;
if ( raiseEvents )
{
2019-02-21 14:46:14 +11:00
scope . Events . Dispatch ( Saved , this , saveEventArgs . ToContentSavedEventArgs ( ) , nameof ( Saved ) ) ;
2017-12-28 09:18:09 +01:00
}
2018-06-22 21:03:47 +02:00
var changeType = TreeChangeTypes . RefreshNode ;
2017-12-28 09:18:09 +01:00
scope . Events . Dispatch ( TreeChanged , this , new TreeChange < IContent > ( content , changeType ) . ToEventArgs ( ) ) ;
2018-10-18 22:47:12 +11:00
2018-10-19 13:24:36 +11:00
if ( culturesChanging ! = null )
2018-10-30 14:49:25 +11:00
{
var langs = string . Join ( ", " , _languageRepository . GetMany ( )
. Where ( x = > culturesChanging . InvariantContains ( x . IsoCode ) )
. Select ( x = > x . CultureName ) ) ;
2018-10-31 22:38:58 +11:00
Audit ( AuditType . SaveVariant , userId , content . Id , $"Saved languages: {langs}" , langs ) ;
2018-11-07 22:18:43 +11:00
}
2018-10-18 22:47:12 +11:00
else
Audit ( AuditType . Save , userId , content . Id ) ;
2017-12-28 09:18:09 +01:00
scope . Complete ( ) ;
}
return OperationResult . Succeed ( evtMsgs ) ;
}
/// <inheritdoc />
2019-02-06 14:01:14 +00:00
public OperationResult Save ( IEnumerable < IContent > contents , int userId = Constants . Security . SuperUserId , bool raiseEvents = true )
2017-12-28 09:18:09 +01:00
{
var evtMsgs = EventMessagesFactory . Get ( ) ;
var contentsA = contents . ToArray ( ) ;
using ( var scope = ScopeProvider . CreateScope ( ) )
{
2019-02-06 16:10:20 +11:00
var saveEventArgs = new ContentSavingEventArgs ( contentsA , evtMsgs ) ;
2019-02-21 14:46:14 +11:00
if ( raiseEvents & & scope . Events . DispatchCancelable ( Saving , this , saveEventArgs , nameof ( Saving ) ) )
2017-12-28 09:18:09 +01:00
{
scope . Complete ( ) ;
return OperationResult . Cancel ( evtMsgs ) ;
}
2018-06-22 21:03:47 +02:00
var treeChanges = contentsA . Select ( x = > new TreeChange < IContent > ( x , TreeChangeTypes . RefreshNode ) ) ;
2017-12-28 09:18:09 +01:00
scope . WriteLock ( Constants . Locks . ContentTree ) ;
foreach ( var content in contentsA )
{
if ( content . HasIdentity = = false )
content . CreatorId = userId ;
content . WriterId = userId ;
_documentRepository . Save ( content ) ;
}
if ( raiseEvents )
{
2019-02-21 14:46:14 +11:00
scope . Events . Dispatch ( Saved , this , saveEventArgs . ToContentSavedEventArgs ( ) , nameof ( Saved ) ) ;
2017-12-28 09:18:09 +01:00
}
scope . Events . Dispatch ( TreeChanged , this , treeChanges . ToEventArgs ( ) ) ;
2018-10-18 22:47:12 +11:00
Audit ( AuditType . Save , userId = = - 1 ? 0 : userId , Constants . System . Root , "Saved multiple content" ) ;
2017-12-28 09:18:09 +01:00
scope . Complete ( ) ;
}
return OperationResult . Succeed ( evtMsgs ) ;
}
/// <inheritdoc />
2019-02-06 14:01:14 +00:00
public PublishResult SaveAndPublish ( IContent content , string culture = "*" , int userId = Constants . Security . SuperUserId , bool raiseEvents = true )
2017-12-28 09:18:09 +01:00
{
var evtMsgs = EventMessagesFactory . Get ( ) ;
2018-06-22 21:03:47 +02:00
var publishedState = content . PublishedState ;
if ( publishedState ! = PublishedState . Published & & publishedState ! = PublishedState . Unpublished )
2019-02-04 16:55:35 +11:00
throw new InvalidOperationException ( $"Cannot save-and-publish (un)publishing content, use the dedicated {nameof(CommitDocumentChanges)} method." ) ;
2018-06-22 21:03:47 +02:00
// cannot accept invariant (null or empty) culture for variant content type
// cannot accept a specific culture for invariant content type (but '*' is ok)
if ( content . ContentType . VariesByCulture ( ) )
2017-12-28 09:18:09 +01:00
{
2018-06-22 21:03:47 +02:00
if ( culture . IsNullOrWhiteSpace ( ) )
throw new NotSupportedException ( "Invariant culture is not supported by variant content types." ) ;
2017-12-28 09:18:09 +01:00
}
2018-06-22 21:03:47 +02:00
else
2017-12-28 09:18:09 +01:00
{
2018-06-22 21:03:47 +02:00
if ( ! culture . IsNullOrWhiteSpace ( ) & & culture ! = "*" )
throw new NotSupportedException ( $"Culture \" { culture } \ " is not supported by invariant content types." ) ;
}
2017-12-28 09:18:09 +01:00
2019-02-21 14:46:14 +11:00
using ( var scope = ScopeProvider . CreateScope ( ) )
{
scope . WriteLock ( Constants . Locks . ContentTree ) ;
2017-12-28 09:18:09 +01:00
2019-02-21 14:46:14 +11:00
var saveEventArgs = new ContentSavingEventArgs ( content , evtMsgs ) ;
if ( raiseEvents & & scope . Events . DispatchCancelable ( Saving , this , saveEventArgs , nameof ( Saving ) ) )
return new PublishResult ( PublishResultType . FailedPublishCancelledByEvent , evtMsgs , content ) ;
2019-02-20 16:05:42 +01:00
2019-02-21 14:46:14 +11:00
Property [ ] invalidProperties ;
// if culture is specific, first publish the invariant values, then publish the culture itself.
// if culture is '*', then publish them all (including variants)
// explicitly SaveAndPublish a specific culture also publishes invariant values
if ( ! culture . IsNullOrWhiteSpace ( ) & & culture ! = "*" )
{
// publish the invariant values
var publishInvariant = content . PublishCulture ( null ) ;
if ( ! publishInvariant )
return new PublishResult ( PublishResultType . FailedPublishContentInvalid , evtMsgs , content ) ;
//validate the property values
if ( ! _propertyValidationService . Value . IsPropertyDataValid ( content , out invalidProperties ) )
return new PublishResult ( PublishResultType . FailedPublishContentInvalid , evtMsgs , content )
{
InvalidProperties = invalidProperties
} ;
}
// publish the culture(s)
var publishCulture = content . PublishCulture ( culture ) ;
if ( ! publishCulture )
return new PublishResult ( PublishResultType . FailedPublishContentInvalid , evtMsgs , content ) ;
//validate the property values
if ( ! _propertyValidationService . Value . IsPropertyDataValid ( content , out invalidProperties ) )
2019-02-20 16:05:42 +01:00
return new PublishResult ( PublishResultType . FailedPublishContentInvalid , evtMsgs , content )
{
2019-02-21 14:46:14 +11:00
InvalidProperties = invalidProperties
2019-02-20 16:05:42 +01:00
} ;
2019-02-21 14:46:14 +11:00
var result = CommitDocumentChangesInternal ( scope , content , saveEventArgs , userId , raiseEvents ) ;
scope . Complete ( ) ;
return result ;
2018-06-22 21:03:47 +02:00
}
2019-02-04 16:55:35 +11:00
}
/// <inheritdoc />
public PublishResult SaveAndPublish ( IContent content , string [ ] cultures , int userId = 0 , bool raiseEvents = true )
{
if ( content = = null ) throw new ArgumentNullException ( nameof ( content ) ) ;
if ( cultures = = null ) throw new ArgumentNullException ( nameof ( cultures ) ) ;
2019-02-21 14:46:14 +11:00
using ( var scope = ScopeProvider . CreateScope ( ) )
2019-02-04 16:55:35 +11:00
{
2019-02-21 14:46:14 +11:00
scope . WriteLock ( Constants . Locks . ContentTree ) ;
var evtMsgs = EventMessagesFactory . Get ( ) ;
var saveEventArgs = new ContentSavingEventArgs ( content , evtMsgs ) ;
if ( raiseEvents & & scope . Events . DispatchCancelable ( Saving , this , saveEventArgs , nameof ( Saving ) ) )
return new PublishResult ( PublishResultType . FailedPublishCancelledByEvent , evtMsgs , content ) ;
var varies = content . ContentType . VariesByCulture ( ) ;
if ( cultures . Length = = 0 )
{
//no cultures specified and doesn't vary, so publish it, else nothing to publish
return ! varies
? SaveAndPublish ( content , userId : userId , raiseEvents : raiseEvents )
: new PublishResult ( PublishResultType . FailedPublishNothingToPublish , evtMsgs , content ) ;
}
if ( cultures . Select ( content . PublishCulture ) . Any ( isValid = > ! isValid ) )
return new PublishResult ( PublishResultType . FailedPublishContentInvalid , evtMsgs , content ) ;
2019-02-04 16:55:35 +11:00
2019-02-21 14:46:14 +11:00
//validate the property values
if ( ! _propertyValidationService . Value . IsPropertyDataValid ( content , out var invalidProperties ) )
return new PublishResult ( PublishResultType . FailedPublishContentInvalid , evtMsgs , content )
{
InvalidProperties = invalidProperties
} ;
2019-02-04 16:55:35 +11:00
2019-02-21 14:46:14 +11:00
var result = CommitDocumentChangesInternal ( scope , content , saveEventArgs , userId , raiseEvents ) ;
scope . Complete ( ) ;
return result ;
}
2018-06-22 21:03:47 +02:00
}
2017-12-28 09:18:09 +01:00
2018-06-22 21:03:47 +02:00
/// <inheritdoc />
2019-02-06 14:01:14 +00:00
public PublishResult Unpublish ( IContent content , string culture = "*" , int userId = Constants . Security . SuperUserId )
2018-06-22 21:03:47 +02:00
{
2019-02-05 01:58:18 +11:00
if ( content = = null ) throw new ArgumentNullException ( nameof ( content ) ) ;
2018-06-22 21:03:47 +02:00
var evtMsgs = EventMessagesFactory . Get ( ) ;
2017-12-28 09:18:09 +01:00
2018-07-05 17:14:11 +02:00
culture = culture . NullOrWhiteSpaceAsNull ( ) ;
2018-06-22 21:03:47 +02:00
var publishedState = content . PublishedState ;
if ( publishedState ! = PublishedState . Published & & publishedState ! = PublishedState . Unpublished )
2019-02-04 16:55:35 +11:00
throw new InvalidOperationException ( $"Cannot save-and-publish (un)publishing content, use the dedicated {nameof(CommitDocumentChanges)} method." ) ;
2017-12-28 09:18:09 +01:00
2018-06-22 21:03:47 +02:00
// cannot accept invariant (null or empty) culture for variant content type
// cannot accept a specific culture for invariant content type (but '*' is ok)
if ( content . ContentType . VariesByCulture ( ) )
{
2018-07-05 17:14:11 +02:00
if ( culture = = null )
2018-06-22 21:03:47 +02:00
throw new NotSupportedException ( "Invariant culture is not supported by variant content types." ) ;
}
else
{
2018-07-05 17:14:11 +02:00
if ( culture ! = null & & culture ! = "*" )
2018-06-22 21:03:47 +02:00
throw new NotSupportedException ( $"Culture \" { culture } \ " is not supported by invariant content types." ) ;
}
2017-12-28 09:18:09 +01:00
2018-06-22 21:03:47 +02:00
// if the content is not published, nothing to do
if ( ! content . Published )
2018-11-07 19:42:49 +11:00
return new PublishResult ( PublishResultType . SuccessUnpublishAlready , evtMsgs , content ) ;
2017-12-28 09:18:09 +01:00
2019-02-21 14:46:14 +11:00
using ( var scope = ScopeProvider . CreateScope ( ) )
2018-06-22 21:03:47 +02:00
{
2019-02-21 14:46:14 +11:00
scope . WriteLock ( Constants . Locks . ContentTree ) ;
var saveEventArgs = new ContentSavingEventArgs ( content , evtMsgs ) ;
if ( scope . Events . DispatchCancelable ( Saving , this , saveEventArgs , nameof ( Saving ) ) )
return new PublishResult ( PublishResultType . FailedPublishCancelledByEvent , evtMsgs , content ) ;
// all cultures = unpublish whole
if ( culture = = "*" | | ( ! content . ContentType . VariesByCulture ( ) & & culture = = null ) )
{
content . PublishedState = PublishedState . Unpublishing ;
}
else
{
// If the culture we want to unpublish was already unpublished, nothing to do.
// To check for that we need to lookup the persisted content item
var persisted = content . HasIdentity ? GetById ( content . Id ) : null ;
if ( persisted ! = null & & ! persisted . IsCulturePublished ( culture ) )
return new PublishResult ( PublishResultType . SuccessUnpublishAlready , evtMsgs , content ) ;
2019-02-05 01:58:18 +11:00
2019-02-21 14:46:14 +11:00
// unpublish the culture
content . UnpublishCulture ( culture ) ;
}
2017-12-28 09:18:09 +01:00
2019-02-21 14:46:14 +11:00
var result = CommitDocumentChangesInternal ( scope , content , saveEventArgs , userId ) ;
scope . Complete ( ) ;
return result ;
2018-06-22 21:03:47 +02:00
}
2017-12-28 09:18:09 +01:00
}
2019-02-05 01:22:52 +11:00
/// <summary>
/// Saves a document and publishes/unpublishes any pending publishing changes made to the document.
/// </summary>
/// <remarks>
2019-02-21 14:46:14 +11:00
/// <para>
/// This MUST NOT be called from within this service, this used to be a public API and must only be used outside of this service.
/// Internally in this service, calls must be made to CommitDocumentChangesInternal
/// </para>
2019-02-21 14:13:37 +01:00
///
2019-02-05 01:22:52 +11:00
/// <para>This is the underlying logic for both publishing and unpublishing any document</para>
2019-02-21 14:46:14 +11:00
/// <para>Pending publishing/unpublishing changes on a document are made with calls to <see cref="ContentRepositoryExtensions.PublishCulture"/> and
/// <see cref="ContentRepositoryExtensions.UnpublishCulture"/>.</para>
2019-02-05 01:22:52 +11:00
/// <para>When publishing or unpublishing a single culture, or all cultures, use <see cref="SaveAndPublish"/>
/// and <see cref="Unpublish"/>. But if the flexibility to both publish and unpublish in a single operation is required
2019-02-21 14:46:14 +11:00
/// then this method needs to be used in combination with <see cref="ContentRepositoryExtensions.PublishCulture"/> and <see cref="ContentRepositoryExtensions.UnpublishCulture"/>
2019-02-05 01:22:52 +11:00
/// on the content itself - this prepares the content, but does not commit anything - and then, invoke
/// <see cref="CommitDocumentChanges"/> to actually commit the changes to the database.</para>
/// <para>The document is *always* saved, even when publishing fails.</para>
/// </remarks>
2019-02-21 14:46:14 +11:00
internal PublishResult CommitDocumentChanges ( IContent content ,
int userId = Constants . Security . SuperUserId , bool raiseEvents = true )
2018-11-06 15:24:55 +01:00
{
using ( var scope = ScopeProvider . CreateScope ( ) )
{
2019-02-21 14:46:14 +11:00
var evtMsgs = EventMessagesFactory . Get ( ) ;
2018-11-06 15:24:55 +01:00
scope . WriteLock ( Constants . Locks . ContentTree ) ;
2019-02-21 14:46:14 +11:00
var saveEventArgs = new ContentSavingEventArgs ( content , evtMsgs ) ;
if ( raiseEvents & & scope . Events . DispatchCancelable ( Saving , this , saveEventArgs , nameof ( Saving ) ) )
return new PublishResult ( PublishResultType . FailedPublishCancelledByEvent , evtMsgs , content ) ;
var result = CommitDocumentChangesInternal ( scope , content , saveEventArgs , userId , raiseEvents ) ;
2018-11-06 15:24:55 +01:00
scope . Complete ( ) ;
return result ;
}
}
2019-02-21 14:46:14 +11:00
private PublishResult CommitDocumentChangesInternal ( IScope scope , IContent content ,
ContentSavingEventArgs saveEventArgs ,
int userId = Constants . Security . SuperUserId , bool raiseEvents = true , bool branchOne = false , bool branchRoot = false )
2017-12-28 09:18:09 +01:00
{
2019-02-21 14:46:14 +11:00
if ( scope = = null ) throw new ArgumentNullException ( nameof ( scope ) ) ;
if ( content = = null ) throw new ArgumentNullException ( nameof ( content ) ) ;
if ( saveEventArgs = = null ) throw new ArgumentNullException ( nameof ( saveEventArgs ) ) ;
var evtMsgs = saveEventArgs . Messages ;
2019-02-21 14:13:37 +01:00
2018-06-22 21:03:47 +02:00
PublishResult publishResult = null ;
2018-11-07 19:42:49 +11:00
PublishResult unpublishResult = null ;
2018-06-22 21:03:47 +02:00
// nothing set = republish it all
if ( content . PublishedState ! = PublishedState . Publishing & & content . PublishedState ! = PublishedState . Unpublishing )
2019-02-06 16:10:20 +11:00
content . PublishedState = PublishedState . Publishing ;
2017-12-28 09:18:09 +01:00
2018-07-05 17:07:40 +02:00
// state here is either Publishing or Unpublishing
2018-11-08 16:33:19 +01:00
// (even though, Publishing to unpublish a culture may end up unpublishing everything)
2018-07-05 17:07:40 +02:00
var publishing = content . PublishedState = = PublishedState . Publishing ;
var unpublishing = content . PublishedState = = PublishedState . Unpublishing ;
2018-11-07 19:42:49 +11:00
var variesByCulture = content . ContentType . VariesByCulture ( ) ;
//track cultures that are being published, changed, unpublished
IReadOnlyList < string > culturesPublishing = null ;
IReadOnlyList < string > culturesUnpublishing = null ;
IReadOnlyList < string > culturesChanging = variesByCulture
2019-02-05 14:13:03 +11:00
? content . CultureInfos . Values . Where ( x = > x . IsDirty ( ) ) . Select ( x = > x . Culture ) . ToList ( )
2018-11-07 19:42:49 +11:00
: null ;
2018-10-18 22:47:12 +11:00
2018-11-06 15:24:55 +01:00
var isNew = ! content . HasIdentity ;
var changeType = isNew ? TreeChangeTypes . RefreshNode : TreeChangeTypes . RefreshBranch ;
var previouslyPublished = content . HasIdentity & & content . Published ;
2018-06-22 21:03:47 +02:00
2018-11-06 15:24:55 +01:00
if ( publishing )
{
2018-11-07 22:18:43 +11:00
culturesUnpublishing = content . GetCulturesUnpublishing ( ) ;
culturesPublishing = variesByCulture
2019-02-05 14:13:03 +11:00
? content . PublishCultureInfos . Values . Where ( x = > x . IsDirty ( ) ) . Select ( x = > x . Culture ) . ToList ( )
2018-11-07 22:18:43 +11:00
: null ;
2018-11-07 19:42:49 +11:00
2018-11-07 22:18:43 +11:00
// ensure that the document can be published, and publish handling events, business rules, etc
2019-02-06 16:10:20 +11:00
publishResult = StrategyCanPublish ( scope , content , /*checkPath:*/ ( ! branchOne | | branchRoot ) , culturesPublishing , culturesUnpublishing , evtMsgs , saveEventArgs ) ;
2018-11-06 15:24:55 +01:00
if ( publishResult . Success )
2018-11-07 22:18:43 +11:00
{
// note: StrategyPublish flips the PublishedState to Publishing!
2019-02-06 16:10:20 +11:00
publishResult = StrategyPublish ( content , culturesPublishing , culturesUnpublishing , evtMsgs ) ;
2018-11-07 22:18:43 +11:00
}
else
{
2018-11-13 12:05:02 +01:00
// in a branch, just give up
if ( branchOne & & ! branchRoot )
return publishResult ;
//check for mandatory culture missing, and then unpublish document as a whole
2018-11-07 22:18:43 +11:00
if ( publishResult . Result = = PublishResultType . FailedPublishMandatoryCultureMissing )
2018-11-06 21:33:24 +11:00
{
2018-11-07 22:18:43 +11:00
publishing = false ;
unpublishing = content . Published ; // if not published yet, nothing to do
2018-11-06 21:33:24 +11:00
2018-11-07 22:18:43 +11:00
// we may end up in a state where we won't publish nor unpublish
2018-11-08 16:33:19 +01:00
// keep going, though, as we want to save anyways
2018-11-07 22:18:43 +11:00
}
2018-11-06 21:33:24 +11:00
2018-11-13 12:05:02 +01:00
// reset published state from temp values (publishing, unpublishing) to original value
2019-02-06 17:28:48 +01:00
// (published, unpublished) in order to save the document, unchanged - yes, this is odd,
// but: (a) it means we don't reproduce the PublishState logic here and (b) setting the
// PublishState to anything other than Publishing or Unpublishing - which is precisely
// what we want to do here - throws
2019-02-21 14:13:37 +01:00
content . Published = content . Published ;
2017-12-28 09:18:09 +01:00
}
2018-11-07 22:18:43 +11:00
}
2017-12-28 09:18:09 +01:00
2018-11-13 12:05:02 +01:00
if ( unpublishing ) // won't happen in a branch
2018-11-06 15:24:55 +01:00
{
var newest = GetById ( content . Id ) ; // ensure we have the newest version - in scope
2018-11-13 12:05:02 +01:00
if ( content . VersionId ! = newest . VersionId )
return new PublishResult ( PublishResultType . FailedPublishConcurrencyViolation , evtMsgs , content ) ;
2018-06-22 21:03:47 +02:00
2018-11-06 15:24:55 +01:00
if ( content . Published )
2018-06-12 17:05:37 +02:00
{
2018-11-06 15:24:55 +01:00
// ensure that the document can be unpublished, and unpublish
2018-06-22 21:03:47 +02:00
// handling events, business rules, etc
2018-11-06 15:24:55 +01:00
// note: StrategyUnpublish flips the PublishedState to Unpublishing!
// note: This unpublishes the entire document (not different variants)
2019-02-06 16:10:20 +11:00
unpublishResult = StrategyCanUnpublish ( scope , content , evtMsgs ) ;
2018-11-06 15:24:55 +01:00
if ( unpublishResult . Success )
2018-11-07 22:18:43 +11:00
unpublishResult = StrategyUnpublish ( scope , content , userId , evtMsgs ) ;
else
{
2018-11-13 12:05:02 +01:00
// reset published state from temp values (publishing, unpublishing) to original value
2019-02-06 17:28:48 +01:00
// (published, unpublished) in order to save the document, unchanged - yes, this is odd,
// but: (a) it means we don't reproduce the PublishState logic here and (b) setting the
// PublishState to anything other than Publishing or Unpublishing - which is precisely
// what we want to do here - throws
2019-02-06 16:10:20 +11:00
content . Published = content . Published ;
2018-06-22 21:03:47 +02:00
}
2018-11-07 22:18:43 +11:00
}
2018-11-06 15:24:55 +01:00
else
2018-05-07 23:22:52 +10:00
{
2018-11-06 15:24:55 +01:00
// already unpublished - optimistic concurrency collision, really,
// and I am not sure at all what we should do, better die fast, else
// we may end up corrupting the db
throw new InvalidOperationException ( "Concurrency collision." ) ;
2018-05-07 23:22:52 +10:00
}
2018-11-06 15:24:55 +01:00
}
2017-12-28 09:18:09 +01:00
2018-11-06 15:24:55 +01:00
// save, always
if ( content . HasIdentity = = false )
content . CreatorId = userId ;
content . WriterId = userId ;
2017-12-28 09:18:09 +01:00
2018-11-06 15:24:55 +01:00
// saving does NOT change the published version, unless PublishedState is Publishing or Unpublishing
_documentRepository . Save ( content ) ;
2017-12-28 09:18:09 +01:00
2018-11-06 15:24:55 +01:00
// raise the Saved event, always
if ( raiseEvents )
{
2019-02-21 14:46:14 +11:00
scope . Events . Dispatch ( Saved , this , saveEventArgs . ToContentSavedEventArgs ( ) , nameof ( Saved ) ) ;
2018-11-06 15:24:55 +01:00
}
2018-06-12 17:05:37 +02:00
2018-11-13 12:05:02 +01:00
if ( unpublishing ) // we have tried to unpublish - won't happen in a branch
2018-11-06 15:24:55 +01:00
{
if ( unpublishResult . Success ) // and succeeded, trigger events
2018-06-22 21:03:47 +02:00
{
2018-11-06 15:24:55 +01:00
// events and audit
scope . Events . Dispatch ( Unpublished , this , new PublishEventArgs < IContent > ( content , false , false ) , "Unpublished" ) ;
scope . Events . Dispatch ( TreeChanged , this , new TreeChange < IContent > ( content , TreeChangeTypes . RefreshBranch ) . ToEventArgs ( ) ) ;
2018-11-07 19:42:49 +11:00
2018-11-07 22:18:43 +11:00
if ( culturesUnpublishing ! = null )
{
//If we are here, it means we tried unpublishing a culture but it was mandatory so now everything is unpublished
var langs = string . Join ( ", " , _languageRepository . GetMany ( )
. Where ( x = > culturesUnpublishing . InvariantContains ( x . IsoCode ) )
. Select ( x = > x . CultureName ) ) ;
Audit ( AuditType . UnpublishVariant , userId , content . Id , $"Unpublished languages: {langs}" , langs ) ;
//log that the whole content item has been unpublished due to mandatory culture unpublished
Audit ( AuditType . Unpublish , userId , content . Id , "Unpublished (mandatory language unpublished)" ) ;
}
else
Audit ( AuditType . Unpublish , userId , content . Id ) ;
2018-11-07 19:42:49 +11:00
2018-11-07 22:18:43 +11:00
return new PublishResult ( PublishResultType . SuccessUnpublish , evtMsgs , content ) ;
2018-06-22 21:03:47 +02:00
}
2018-05-07 23:22:52 +10:00
2018-11-06 15:24:55 +01:00
// or, failed
scope . Events . Dispatch ( TreeChanged , this , new TreeChange < IContent > ( content , changeType ) . ToEventArgs ( ) ) ;
2018-11-07 22:18:43 +11:00
return new PublishResult ( PublishResultType . FailedUnpublish , evtMsgs , content ) ; // bah
2018-11-06 15:24:55 +01:00
}
2018-05-07 23:22:52 +10:00
2018-11-06 15:24:55 +01:00
if ( publishing ) // we have tried to publish
{
if ( publishResult . Success ) // and succeeded, trigger events
2018-06-22 21:03:47 +02:00
{
2018-11-06 15:24:55 +01:00
if ( isNew = = false & & previouslyPublished = = false )
changeType = TreeChangeTypes . RefreshBranch ; // whole branch
2018-06-22 21:03:47 +02:00
2018-11-06 15:24:55 +01:00
// invalidate the node/branch
2018-11-15 14:58:10 +01:00
if ( ! branchOne ) // for branches, handled by SaveAndPublishBranch
{
2018-11-13 12:05:02 +01:00
scope . Events . Dispatch ( TreeChanged , this , new TreeChange < IContent > ( content , changeType ) . ToEventArgs ( ) ) ;
2019-02-06 16:10:20 +11:00
scope . Events . Dispatch ( Published , this , saveEventArgs . ToContentPublishedEventArgs ( ) , nameof ( Published ) ) ;
2018-11-15 14:58:10 +01:00
}
2018-06-22 21:03:47 +02:00
2019-02-06 16:10:20 +11:00
// it was not published and now is... descendants that were 'published' (but
2019-01-22 18:03:39 -05:00
// had an unpublished ancestor) are 're-published' ie not explicitly published
2018-11-06 15:24:55 +01:00
// but back as 'published' nevertheless
2018-11-13 12:05:02 +01:00
if ( ! branchOne & & isNew = = false & & previouslyPublished = = false & & HasChildren ( content . Id ) )
2018-06-22 21:03:47 +02:00
{
2018-11-06 15:24:55 +01:00
var descendants = GetPublishedDescendantsLocked ( content ) . ToArray ( ) ;
2019-02-06 16:10:20 +11:00
scope . Events . Dispatch ( Published , this , new ContentPublishedEventArgs ( descendants , false , evtMsgs ) , "Published" ) ;
2018-11-06 15:24:55 +01:00
}
2018-06-22 21:03:47 +02:00
2018-11-07 22:18:43 +11:00
switch ( publishResult . Result )
{
case PublishResultType . SuccessPublish :
Audit ( AuditType . Publish , userId , content . Id ) ;
break ;
case PublishResultType . SuccessPublishCulture :
if ( culturesPublishing ! = null )
{
var langs = string . Join ( ", " , _languageRepository . GetMany ( )
. Where ( x = > culturesPublishing . InvariantContains ( x . IsoCode ) )
. Select ( x = > x . CultureName ) ) ;
Audit ( AuditType . PublishVariant , userId , content . Id , $"Published languages: {langs}" , langs ) ;
}
break ;
case PublishResultType . SuccessUnpublishCulture :
if ( culturesUnpublishing ! = null )
{
var langs = string . Join ( ", " , _languageRepository . GetMany ( )
. Where ( x = > culturesUnpublishing . InvariantContains ( x . IsoCode ) )
. Select ( x = > x . CultureName ) ) ;
Audit ( AuditType . UnpublishVariant , userId , content . Id , $"Unpublished languages: {langs}" , langs ) ;
}
break ;
}
2018-10-18 22:47:12 +11:00
2018-11-07 22:09:51 +11:00
return publishResult ;
}
2018-11-07 22:18:43 +11:00
}
2018-06-22 21:03:47 +02:00
2018-11-13 12:05:02 +01:00
// should not happen
if ( branchOne & & ! branchRoot )
throw new Exception ( "panic" ) ;
2018-11-07 22:18:43 +11:00
//if publishing didn't happen or if it has failed, we still need to log which cultures were saved
2018-11-13 12:05:02 +01:00
if ( ! branchOne & & ( publishResult = = null | | ! publishResult . Success ) )
2018-11-07 22:18:43 +11:00
{
if ( culturesChanging ! = null )
{
var langs = string . Join ( ", " , _languageRepository . GetMany ( )
. Where ( x = > culturesChanging . InvariantContains ( x . IsoCode ) )
. Select ( x = > x . CultureName ) ) ;
Audit ( AuditType . SaveVariant , userId , content . Id , $"Saved languages: {langs}" , langs ) ;
2018-11-07 19:42:49 +11:00
}
2018-11-07 22:18:43 +11:00
else
2018-11-13 12:05:02 +01:00
{
2018-11-07 22:18:43 +11:00
Audit ( AuditType . Save , userId , content . Id ) ;
2018-11-13 12:05:02 +01:00
}
2018-11-07 22:18:43 +11:00
}
2018-06-22 21:03:47 +02:00
2018-11-07 22:18:43 +11:00
// or, failed
2018-11-06 15:24:55 +01:00
scope . Events . Dispatch ( TreeChanged , this , new TreeChange < IContent > ( content , changeType ) . ToEventArgs ( ) ) ;
2018-11-07 22:18:43 +11:00
return publishResult ;
2017-12-28 09:18:09 +01:00
}
/// <inheritdoc />
2018-11-07 21:32:12 +11:00
public IEnumerable < PublishResult > PerformScheduledPublish ( DateTime date )
2018-11-13 11:24:30 +01:00
= > PerformScheduledPublishInternal ( date ) . ToList ( ) ;
// beware! this method yields results, so the returned IEnumerable *must* be
// enumerated for anything to happen - dangerous, so private + exposed via
// the public method above, which forces ToList().
private IEnumerable < PublishResult > PerformScheduledPublishInternal ( DateTime date )
2017-12-28 09:18:09 +01:00
{
2018-11-07 19:42:49 +11:00
var evtMsgs = EventMessagesFactory . Get ( ) ;
2017-12-28 09:18:09 +01:00
using ( var scope = ScopeProvider . CreateScope ( ) )
{
scope . WriteLock ( Constants . Locks . ContentTree ) ;
2018-11-13 11:24:30 +01:00
foreach ( var d in _documentRepository . GetContentForRelease ( date ) )
2017-12-28 09:18:09 +01:00
{
PublishResult result ;
2018-11-07 19:42:49 +11:00
if ( d . ContentType . VariesByCulture ( ) )
2017-12-28 09:18:09 +01:00
{
2018-11-07 19:42:49 +11:00
//find which cultures have pending schedules
2018-11-14 09:16:22 +01:00
var pendingCultures = d . ContentSchedule . GetPending ( ContentScheduleAction . Release , date )
2018-11-07 19:42:49 +11:00
. Select ( x = > x . Culture )
2018-11-07 21:32:12 +11:00
. Distinct ( )
. ToList ( ) ;
2018-11-07 22:18:43 +11:00
2019-02-21 14:46:14 +11:00
if ( pendingCultures . Count = = 0 )
break ; //shouldn't happen but no point in continuing if there's nothing there
var saveEventArgs = new ContentSavingEventArgs ( d , evtMsgs ) ;
if ( scope . Events . DispatchCancelable ( Saving , this , saveEventArgs , nameof ( Saving ) ) )
yield return new PublishResult ( PublishResultType . FailedPublishCancelledByEvent , evtMsgs , d ) ;
2018-11-13 11:24:30 +01:00
var publishing = true ;
foreach ( var culture in pendingCultures )
2018-11-07 19:42:49 +11:00
{
//Clear this schedule for this culture
2018-11-14 09:16:22 +01:00
d . ContentSchedule . Clear ( culture , ContentScheduleAction . Release , date ) ;
2018-11-02 14:55:34 +11:00
2018-11-13 11:24:30 +01:00
if ( d . Trashed ) continue ; // won't publish
2018-11-13 17:51:59 +11:00
2019-02-21 14:46:14 +11:00
//publish the culture values and validate the property values, if validation fails, log the invalid properties so the develeper has an idea of what has failed
Property [ ] invalidProperties = null ;
var tryPublish = d . PublishCulture ( culture ) & & _propertyValidationService . Value . IsPropertyDataValid ( d , out invalidProperties ) ;
if ( invalidProperties ! = null & & invalidProperties . Length > 0 )
Logger . Warn < ContentService > ( "Scheduled publishing will fail for document {DocumentId} and culture {Culture} because of invalid properties {InvalidProperties}" ,
d . Id , culture , string . Join ( "," , invalidProperties . Select ( x = > x . Alias ) ) ) ;
publishing & = tryPublish ; //set the culture to be published
2018-11-13 11:24:30 +01:00
if ( ! publishing ) break ; // no point continuing
2018-11-07 21:32:12 +11:00
}
2018-11-13 11:24:30 +01:00
if ( d . Trashed )
result = new PublishResult ( PublishResultType . FailedPublishIsTrashed , evtMsgs , d ) ;
else if ( ! publishing )
2019-02-21 14:46:14 +11:00
result = new PublishResult ( PublishResultType . FailedPublishContentInvalid , evtMsgs , d ) ;
2018-11-13 11:24:30 +01:00
else
2019-02-21 14:46:14 +11:00
result = CommitDocumentChangesInternal ( scope , d , saveEventArgs , d . WriterId ) ;
2018-11-13 11:24:30 +01:00
if ( result . Success = = false )
Logger . Error < ContentService > ( null , "Failed to publish document id={DocumentId}, reason={Reason}." , d . Id , result . Result ) ;
yield return result ;
2017-12-28 09:18:09 +01:00
}
2018-11-07 19:42:49 +11:00
else
2017-12-28 09:18:09 +01:00
{
2018-11-07 19:42:49 +11:00
//Clear this schedule
2018-11-14 09:16:22 +01:00
d . ContentSchedule . Clear ( ContentScheduleAction . Release , date ) ;
2018-11-13 11:24:30 +01:00
result = d . Trashed
? new PublishResult ( PublishResultType . FailedPublishIsTrashed , evtMsgs , d )
: SaveAndPublish ( d , userId : d . WriterId ) ;
2018-11-13 17:51:59 +11:00
2018-11-07 19:42:49 +11:00
if ( result . Success = = false )
Logger . Error < ContentService > ( null , "Failed to publish document id={DocumentId}, reason={Reason}." , d . Id , result . Result ) ;
2018-11-13 11:24:30 +01:00
2018-11-07 21:32:12 +11:00
yield return result ;
2017-12-28 09:18:09 +01:00
}
}
2018-11-07 19:42:49 +11:00
2018-11-13 11:24:30 +01:00
foreach ( var d in _documentRepository . GetContentForExpiration ( date ) )
2017-12-28 09:18:09 +01:00
{
2018-11-07 19:42:49 +11:00
PublishResult result ;
if ( d . ContentType . VariesByCulture ( ) )
2017-12-28 09:18:09 +01:00
{
2018-11-07 19:42:49 +11:00
//find which cultures have pending schedules
2018-11-14 09:16:22 +01:00
var pendingCultures = d . ContentSchedule . GetPending ( ContentScheduleAction . Expire , date )
2018-11-07 19:42:49 +11:00
. Select ( x = > x . Culture )
2018-11-07 21:32:12 +11:00
. Distinct ( )
. ToList ( ) ;
2018-11-02 14:55:34 +11:00
2019-02-21 14:46:14 +11:00
if ( pendingCultures . Count = = 0 )
break ; //shouldn't happen but no point in continuing if there's nothing there
var saveEventArgs = new ContentSavingEventArgs ( d , evtMsgs ) ;
if ( scope . Events . DispatchCancelable ( Saving , this , saveEventArgs , nameof ( Saving ) ) )
yield return new PublishResult ( PublishResultType . FailedPublishCancelledByEvent , evtMsgs , d ) ;
2018-11-07 19:42:49 +11:00
foreach ( var c in pendingCultures )
{
//Clear this schedule for this culture
2018-11-14 09:16:22 +01:00
d . ContentSchedule . Clear ( c , ContentScheduleAction . Expire , date ) ;
2018-11-07 19:42:49 +11:00
//set the culture to be published
d . UnpublishCulture ( c ) ;
}
2019-02-21 14:46:14 +11:00
result = CommitDocumentChangesInternal ( scope , d , saveEventArgs , d . WriterId ) ;
if ( result . Success = = false )
Logger . Error < ContentService > ( null , "Failed to publish document id={DocumentId}, reason={Reason}." , d . Id , result . Result ) ;
yield return result ;
2017-12-28 09:18:09 +01:00
}
2018-11-07 19:42:49 +11:00
else
2017-12-28 09:18:09 +01:00
{
2018-11-07 19:42:49 +11:00
//Clear this schedule
2018-11-14 09:16:22 +01:00
d . ContentSchedule . Clear ( ContentScheduleAction . Expire , date ) ;
2018-11-07 19:42:49 +11:00
result = Unpublish ( d , userId : d . WriterId ) ;
if ( result . Success = = false )
Logger . Error < ContentService > ( null , "Failed to unpublish document id={DocumentId}, reason={Reason}." , d . Id , result . Result ) ;
2018-11-07 21:32:12 +11:00
yield return result ;
2017-12-28 09:18:09 +01:00
}
2018-11-07 19:42:49 +11:00
2018-11-07 22:18:43 +11:00
2017-12-28 09:18:09 +01:00
}
2018-11-13 11:24:30 +01:00
_documentRepository . ClearSchedule ( date ) ;
2018-11-13 17:51:59 +11:00
2017-12-28 09:18:09 +01:00
scope . Complete ( ) ;
}
}
2019-02-21 14:13:37 +01:00
// utility 'PublishCultures' func used by SaveAndPublishBranch
2019-02-21 14:46:14 +11:00
private bool SaveAndPublishBranch_PublishCultures ( IContent content , HashSet < string > culturesToPublish )
2018-11-16 13:30:42 +01:00
{
2019-02-21 14:46:14 +11:00
//TODO: This does not support being able to return invalid property details to bubble up to the UI
2018-11-16 13:30:42 +01:00
// variant content type - publish specified cultures
// invariant content type - publish only the invariant culture
2019-02-21 14:46:14 +11:00
return content . ContentType . VariesByCulture ( )
? culturesToPublish . All ( culture = > content . PublishCulture ( culture ) & & _propertyValidationService . Value . IsPropertyDataValid ( content , out _ ) )
: content . PublishCulture ( ) & & _propertyValidationService . Value . IsPropertyDataValid ( content , out _ ) ;
2018-11-16 13:30:42 +01:00
}
2019-02-21 14:13:37 +01:00
// utility 'ShouldPublish' func used by SaveAndPublishBranch
private HashSet < string > SaveAndPublishBranch_ShouldPublish ( ref HashSet < string > cultures , string c , bool published , bool edited , bool isRoot , bool force )
2018-11-16 13:30:42 +01:00
{
// if published, republish
if ( published )
{
if ( cultures = = null ) cultures = new HashSet < string > ( ) ; // empty means 'already published'
if ( edited ) cultures . Add ( c ) ; // <culture> means 'republish this culture'
return cultures ;
}
// if not published, publish if force/root else do nothing
if ( ! force & & ! isRoot ) return cultures ; // null means 'nothing to do'
if ( cultures = = null ) cultures = new HashSet < string > ( ) ;
cultures . Add ( c ) ; // <culture> means 'publish this culture'
return cultures ;
}
2017-12-28 09:18:09 +01:00
/// <inheritdoc />
2019-02-06 14:01:14 +00:00
public IEnumerable < PublishResult > SaveAndPublishBranch ( IContent content , bool force , string culture = "*" , int userId = Constants . Security . SuperUserId )
2017-12-28 09:18:09 +01:00
{
2018-06-22 21:03:47 +02:00
// note: EditedValue and PublishedValue are objects here, so it is important to .Equals()
// and not to == them, else we would be comparing references, and that is a bad thing
2017-12-28 09:18:09 +01:00
2018-11-16 13:30:42 +01:00
// determines whether the document is edited, and thus needs to be published,
// for the specified culture (it may be edited for other cultures and that
// should not trigger a publish).
2018-11-16 16:39:27 +11:00
2018-11-16 13:30:42 +01:00
// determines cultures to be published
// can be: null (content is not impacted), an empty set (content is impacted but already published), or cultures
HashSet < string > ShouldPublish ( IContent c )
2018-11-06 15:24:55 +01:00
{
2018-11-08 16:33:19 +01:00
var isRoot = c . Id = = content . Id ;
2018-11-16 13:30:42 +01:00
HashSet < string > culturesToPublish = null ;
2018-11-08 16:33:19 +01:00
2018-11-16 13:30:42 +01:00
if ( ! c . ContentType . VariesByCulture ( ) ) // invariant content type
2019-02-21 14:13:37 +01:00
return SaveAndPublishBranch_ShouldPublish ( ref culturesToPublish , "*" , c . Published , c . Edited , isRoot , force ) ;
2018-11-15 13:25:21 +01:00
2018-11-16 13:30:42 +01:00
if ( culture ! = "*" ) // variant content type, specific culture
2019-02-21 14:13:37 +01:00
return SaveAndPublishBranch_ShouldPublish ( ref culturesToPublish , culture , c . IsCulturePublished ( culture ) , c . IsCultureEdited ( culture ) , isRoot , force ) ;
2018-11-16 13:30:42 +01:00
// variant content type, all cultures
if ( c . Published )
2018-11-06 15:24:55 +01:00
{
2018-11-16 13:30:42 +01:00
// then some (and maybe all) cultures will be 'already published' (unless forcing),
// others will have to 'republish this culture'
foreach ( var x in c . AvailableCultures )
2019-02-21 14:13:37 +01:00
SaveAndPublishBranch_ShouldPublish ( ref culturesToPublish , x , c . IsCulturePublished ( x ) , c . IsCultureEdited ( x ) , isRoot , force ) ;
2018-11-16 13:30:42 +01:00
return culturesToPublish ;
2018-11-06 15:24:55 +01:00
}
2018-11-16 13:30:42 +01:00
// if not published, publish if force/root else do nothing
return force | | isRoot
? new HashSet < string > { "*" } // "*" means 'publish all'
: null ; // null means 'nothing to do'
2018-11-06 15:24:55 +01:00
}
2017-12-28 09:18:09 +01:00
2018-11-16 13:30:42 +01:00
return SaveAndPublishBranch ( content , force , ShouldPublish , SaveAndPublishBranch_PublishCultures , userId ) ;
2017-12-28 09:18:09 +01:00
}
2018-11-06 15:24:55 +01:00
/// <inheritdoc />
2019-02-06 14:01:14 +00:00
public IEnumerable < PublishResult > SaveAndPublishBranch ( IContent content , bool force , string [ ] cultures , int userId = Constants . Security . SuperUserId )
2018-10-26 14:38:30 +02:00
{
// note: EditedValue and PublishedValue are objects here, so it is important to .Equals()
// and not to == them, else we would be comparing references, and that is a bad thing
cultures = cultures ? ? Array . Empty < string > ( ) ;
2018-11-16 13:30:42 +01:00
// determines cultures to be published
// can be: null (content is not impacted), an empty set (content is impacted but already published), or cultures
HashSet < string > ShouldPublish ( IContent c )
2018-10-26 14:38:30 +02:00
{
2018-11-08 16:33:19 +01:00
var isRoot = c . Id = = content . Id ;
2018-11-16 13:30:42 +01:00
HashSet < string > culturesToPublish = null ;
2018-11-06 15:24:55 +01:00
2018-11-16 13:30:42 +01:00
if ( ! c . ContentType . VariesByCulture ( ) ) // invariant content type
2019-02-21 14:13:37 +01:00
return SaveAndPublishBranch_ShouldPublish ( ref culturesToPublish , "*" , c . Published , c . Edited , isRoot , force ) ;
2018-10-26 14:38:30 +02:00
2018-11-16 13:30:42 +01:00
// variant content type, specific cultures
if ( c . Published )
2018-11-16 16:39:27 +11:00
{
2018-11-16 13:30:42 +01:00
// then some (and maybe all) cultures will be 'already published' (unless forcing),
// others will have to 'republish this culture'
foreach ( var x in cultures )
2019-02-21 14:13:37 +01:00
SaveAndPublishBranch_ShouldPublish ( ref culturesToPublish , x , c . IsCulturePublished ( x ) , c . IsCultureEdited ( x ) , isRoot , force ) ;
2018-11-16 13:30:42 +01:00
return culturesToPublish ;
2018-11-16 16:39:27 +11:00
}
2018-11-16 13:30:42 +01:00
// if not published, publish if force/root else do nothing
return force | | isRoot
? new HashSet < string > ( cultures ) // means 'publish specified cultures'
: null ; // null means 'nothing to do'
2018-10-26 14:38:30 +02:00
}
2018-11-16 13:30:42 +01:00
return SaveAndPublishBranch ( content , force , ShouldPublish , SaveAndPublishBranch_PublishCultures , userId ) ;
2018-10-26 14:38:30 +02:00
}
2019-02-20 16:05:42 +01:00
internal IEnumerable < PublishResult > SaveAndPublishBranch ( IContent document , bool force ,
2018-11-16 13:30:42 +01:00
Func < IContent , HashSet < string > > shouldPublish ,
Func < IContent , HashSet < string > , bool > publishCultures ,
2019-02-06 14:01:14 +00:00
int userId = Constants . Security . SuperUserId )
2017-12-28 09:18:09 +01:00
{
2018-11-16 13:30:42 +01:00
if ( shouldPublish = = null ) throw new ArgumentNullException ( nameof ( shouldPublish ) ) ;
if ( publishCultures = = null ) throw new ArgumentNullException ( nameof ( publishCultures ) ) ;
2017-12-28 09:18:09 +01:00
var evtMsgs = EventMessagesFactory . Get ( ) ;
var results = new List < PublishResult > ( ) ;
var publishedDocuments = new List < IContent > ( ) ;
using ( var scope = ScopeProvider . CreateScope ( ) )
{
scope . WriteLock ( Constants . Locks . ContentTree ) ;
if ( ! document . HasIdentity )
2018-11-06 15:24:55 +01:00
throw new InvalidOperationException ( "Cannot not branch-publish a new document." ) ;
2017-12-28 09:18:09 +01:00
2019-02-06 16:10:20 +11:00
var publishedState = document . PublishedState ;
2017-12-28 09:18:09 +01:00
if ( publishedState = = PublishedState . Publishing )
2018-11-06 15:24:55 +01:00
throw new InvalidOperationException ( "Cannot mix PublishCulture and SaveAndPublishBranch." ) ;
2017-12-28 09:18:09 +01:00
// deal with the branch root - if it fails, abort
2019-02-21 14:13:37 +01:00
var result = SaveAndPublishBranchItem ( scope , document , shouldPublish , publishCultures , true , publishedDocuments , evtMsgs , userId ) ;
2018-11-16 16:39:27 +11:00
if ( result ! = null )
{
results . Add ( result ) ;
if ( ! result . Success ) return results ;
}
2017-12-28 09:18:09 +01:00
// deal with descendants
// if one fails, abort its branch
var exclude = new HashSet < int > ( ) ;
2018-10-18 22:47:12 +11:00
2018-11-06 15:24:55 +01:00
int count ;
2018-10-31 18:01:39 +11:00
var page = 0 ;
2018-11-06 15:24:55 +01:00
const int pageSize = 100 ;
do
2017-12-28 09:18:09 +01:00
{
2018-11-06 15:24:55 +01:00
count = 0 ;
// important to order by Path ASC so make it explicit in case defaults change
// ReSharper disable once RedundantArgumentDefaultValue
foreach ( var d in GetPagedDescendants ( document . Id , page , pageSize , out _ , ordering : Ordering . By ( "Path" , Direction . Ascending ) ) )
2017-12-28 09:18:09 +01:00
{
2018-11-06 15:24:55 +01:00
count + + ;
// if parent is excluded, exclude child too
if ( exclude . Contains ( d . ParentId ) )
2018-10-31 18:01:39 +11:00
{
exclude . Add ( d . Id ) ;
continue ;
}
2017-12-28 09:18:09 +01:00
2018-11-06 15:24:55 +01:00
// no need to check path here, parent has to be published here
2019-02-21 14:13:37 +01:00
result = SaveAndPublishBranchItem ( scope , d , shouldPublish , publishCultures , false , publishedDocuments , evtMsgs , userId ) ;
2018-11-16 16:39:27 +11:00
if ( result ! = null )
{
results . Add ( result ) ;
if ( result . Success ) continue ;
}
2017-12-28 09:18:09 +01:00
2018-11-06 15:24:55 +01:00
// if we could not publish the document, cut its branch
2018-10-31 18:01:39 +11:00
exclude . Add ( d . Id ) ;
}
2018-11-06 15:24:55 +01:00
page + + ;
} while ( count > 0 ) ;
2017-12-28 09:18:09 +01:00
2018-10-18 22:47:12 +11:00
Audit ( AuditType . Publish , userId , document . Id , "Branch published" ) ;
2017-12-28 09:18:09 +01:00
2018-11-15 14:58:10 +01:00
// trigger events for the entire branch
2019-02-06 17:28:48 +01:00
// (SaveAndPublishBranchOne does *not* do it)
2018-11-15 14:58:10 +01:00
scope . Events . Dispatch ( TreeChanged , this , new TreeChange < IContent > ( document , TreeChangeTypes . RefreshBranch ) . ToEventArgs ( ) ) ;
2019-02-06 16:10:20 +11:00
scope . Events . Dispatch ( Published , this , new ContentPublishedEventArgs ( publishedDocuments , false , evtMsgs ) , nameof ( Published ) ) ;
2018-11-15 14:58:10 +01:00
2017-12-28 09:18:09 +01:00
scope . Complete ( ) ;
}
return results ;
}
2018-11-06 15:24:55 +01:00
// shouldPublish: a function determining whether the document has changes that need to be published
// note - 'force' is handled by 'editing'
// publishValues: a function publishing values (using the appropriate PublishCulture calls)
2019-02-21 14:13:37 +01:00
private PublishResult SaveAndPublishBranchItem ( IScope scope , IContent document ,
2018-11-16 13:30:42 +01:00
Func < IContent , HashSet < string > > shouldPublish ,
Func < IContent , HashSet < string > , bool > publishCultures ,
2018-11-08 16:33:19 +01:00
bool isRoot ,
2018-11-06 15:24:55 +01:00
ICollection < IContent > publishedDocuments ,
2017-12-28 09:18:09 +01:00
EventMessages evtMsgs , int userId )
{
2018-11-16 13:30:42 +01:00
var culturesToPublish = shouldPublish ( document ) ;
if ( culturesToPublish = = null ) // null = do not include
return null ;
if ( culturesToPublish . Count = = 0 ) // empty = already published
2018-11-16 16:39:27 +11:00
return new PublishResult ( PublishResultType . SuccessPublishAlready , evtMsgs , document ) ;
2018-11-16 13:30:42 +01:00
2019-02-21 14:46:14 +11:00
var saveEventArgs = new ContentSavingEventArgs ( document , evtMsgs ) ;
if ( scope . Events . DispatchCancelable ( Saving , this , saveEventArgs , nameof ( Saving ) ) )
return new PublishResult ( PublishResultType . FailedPublishCancelledByEvent , evtMsgs , document ) ;
2017-12-28 09:18:09 +01:00
// publish & check if values are valid
2018-11-16 13:30:42 +01:00
if ( ! publishCultures ( document , culturesToPublish ) )
2019-02-20 16:05:42 +01:00
{
//TODO: Based on this callback behavior there is no way to know which properties may have been invalid if this failed, see other results of FailedPublishContentInvalid
2018-11-07 19:42:49 +11:00
return new PublishResult ( PublishResultType . FailedPublishContentInvalid , evtMsgs , document ) ;
2019-02-21 14:13:37 +01:00
}
2017-12-28 09:18:09 +01:00
2019-02-21 14:46:14 +11:00
var result = CommitDocumentChangesInternal ( scope , document , saveEventArgs , userId , branchOne : true , branchRoot : isRoot ) ;
2018-11-15 13:25:21 +01:00
if ( result . Success )
publishedDocuments . Add ( document ) ;
return result ;
2017-12-28 09:18:09 +01:00
}
#endregion
#region Delete
/// <inheritdoc />
public OperationResult Delete ( IContent content , int userId )
{
var evtMsgs = EventMessagesFactory . Get ( ) ;
using ( var scope = ScopeProvider . CreateScope ( ) )
{
var deleteEventArgs = new DeleteEventArgs < IContent > ( content , evtMsgs ) ;
2018-05-02 14:52:00 +10:00
if ( scope . Events . DispatchCancelable ( Deleting , this , deleteEventArgs , nameof ( Deleting ) ) )
2017-12-28 09:18:09 +01:00
{
scope . Complete ( ) ;
return OperationResult . Cancel ( evtMsgs ) ;
}
scope . WriteLock ( Constants . Locks . ContentTree ) ;
// if it's not trashed yet, and published, we should unpublish
2018-10-03 14:27:48 +02:00
// but... Unpublishing event makes no sense (not going to cancel?) and no need to save
2017-12-28 09:18:09 +01:00
// just raise the event
if ( content . Trashed = = false & & content . Published )
2018-10-03 14:27:48 +02:00
scope . Events . Dispatch ( Unpublished , this , new PublishEventArgs < IContent > ( content , false , false ) , nameof ( Unpublished ) ) ;
2017-12-28 09:18:09 +01:00
DeleteLocked ( scope , content ) ;
scope . Events . Dispatch ( TreeChanged , this , new TreeChange < IContent > ( content , TreeChangeTypes . Remove ) . ToEventArgs ( ) ) ;
2018-10-18 22:47:12 +11:00
Audit ( AuditType . Delete , userId , content . Id ) ;
2017-12-28 09:18:09 +01:00
scope . Complete ( ) ;
}
return OperationResult . Succeed ( evtMsgs ) ;
}
private void DeleteLocked ( IScope scope , IContent content )
{
2018-10-31 23:11:37 +11:00
void DoDelete ( IContent c )
2017-12-28 09:18:09 +01:00
{
_documentRepository . Delete ( c ) ;
var args = new DeleteEventArgs < IContent > ( c , false ) ; // raise event & get flagged files
2018-05-02 14:52:00 +10:00
scope . Events . Dispatch ( Deleted , this , args , nameof ( Deleted ) ) ;
2017-12-28 09:18:09 +01:00
2018-10-26 15:06:53 +02:00
// media files deleted by QueuingEventDispatcher
2017-12-28 09:18:09 +01:00
}
2018-10-31 23:11:37 +11:00
const int pageSize = 500 ;
var page = 0 ;
var total = long . MaxValue ;
while ( page * pageSize < total )
{
//get descendants - ordered from deepest to shallowest
2018-11-01 00:05:17 +11:00
var descendants = GetPagedDescendants ( content . Id , page , pageSize , out total , ordering : Ordering . By ( "Path" , Direction . Descending ) ) ;
2018-10-31 23:11:37 +11:00
foreach ( var c in descendants )
DoDelete ( c ) ;
}
DoDelete ( content ) ;
2017-12-28 09:18:09 +01:00
}
2019-01-27 01:17:32 -05:00
//TODO: both DeleteVersions methods below have an issue. Sort of. They do NOT take care of files the way
2017-12-28 09:18:09 +01:00
// Delete does - for a good reason: the file may be referenced by other, non-deleted, versions. BUT,
// if that's not the case, then the file will never be deleted, because when we delete the content,
// the version referencing the file will not be there anymore. SO, we can leak files.
/// <summary>
/// Permanently deletes versions from an <see cref="IContent"/> object prior to a specific date.
/// This method will never delete the latest version of a content item.
/// </summary>
/// <param name="id">Id of the <see cref="IContent"/> object to delete versions from</param>
/// <param name="versionDate">Latest version date</param>
/// <param name="userId">Optional Id of the User deleting versions of a Content object</param>
2019-02-06 14:01:14 +00:00
public void DeleteVersions ( int id , DateTime versionDate , int userId = Constants . Security . SuperUserId )
2017-12-28 09:18:09 +01:00
{
using ( var scope = ScopeProvider . CreateScope ( ) )
{
var deleteRevisionsEventArgs = new DeleteRevisionsEventArgs ( id , dateToRetain : versionDate ) ;
if ( scope . Events . DispatchCancelable ( DeletingVersions , this , deleteRevisionsEventArgs ) )
{
scope . Complete ( ) ;
return ;
}
scope . WriteLock ( Constants . Locks . ContentTree ) ;
_documentRepository . DeleteVersions ( id , versionDate ) ;
deleteRevisionsEventArgs . CanCancel = false ;
scope . Events . Dispatch ( DeletedVersions , this , deleteRevisionsEventArgs ) ;
2018-10-18 22:47:12 +11:00
Audit ( AuditType . Delete , userId , Constants . System . Root , "Delete (by version date)" ) ;
2017-12-28 09:18:09 +01:00
scope . Complete ( ) ;
}
}
/// <summary>
/// Permanently deletes specific version(s) from an <see cref="IContent"/> object.
/// This method will never delete the latest version of a content item.
/// </summary>
/// <param name="id">Id of the <see cref="IContent"/> object to delete a version from</param>
/// <param name="versionId">Id of the version to delete</param>
/// <param name="deletePriorVersions">Boolean indicating whether to delete versions prior to the versionId</param>
/// <param name="userId">Optional Id of the User deleting versions of a Content object</param>
2019-02-06 14:01:14 +00:00
public void DeleteVersion ( int id , int versionId , bool deletePriorVersions , int userId = Constants . Security . SuperUserId )
2017-12-28 09:18:09 +01:00
{
using ( var scope = ScopeProvider . CreateScope ( ) )
{
if ( scope . Events . DispatchCancelable ( DeletingVersions , this , new DeleteRevisionsEventArgs ( id , /*specificVersion:*/ versionId ) ) )
{
scope . Complete ( ) ;
return ;
}
if ( deletePriorVersions )
{
var content = GetVersion ( versionId ) ;
DeleteVersions ( id , content . UpdateDate , userId ) ;
}
scope . WriteLock ( Constants . Locks . ContentTree ) ;
var c = _documentRepository . Get ( id ) ;
if ( c . VersionId ! = versionId ) // don't delete the current version
_documentRepository . DeleteVersion ( versionId ) ;
scope . Events . Dispatch ( DeletedVersions , this , new DeleteRevisionsEventArgs ( id , false , /* specificVersion:*/ versionId ) ) ;
2018-10-18 22:47:12 +11:00
Audit ( AuditType . Delete , userId , Constants . System . Root , "Delete (by version)" ) ;
2017-12-28 09:18:09 +01:00
scope . Complete ( ) ;
}
}
#endregion
#region Move , RecycleBin
/// <inheritdoc />
public OperationResult MoveToRecycleBin ( IContent content , int userId )
{
var evtMsgs = EventMessagesFactory . Get ( ) ;
var moves = new List < Tuple < IContent , string > > ( ) ;
using ( var scope = ScopeProvider . CreateScope ( ) )
{
scope . WriteLock ( Constants . Locks . ContentTree ) ;
var originalPath = content . Path ;
var moveEventInfo = new MoveEventInfo < IContent > ( content , originalPath , Constants . System . RecycleBinContent ) ;
var moveEventArgs = new MoveEventArgs < IContent > ( evtMsgs , moveEventInfo ) ;
2018-04-11 09:05:02 +10:00
if ( scope . Events . DispatchCancelable ( Trashing , this , moveEventArgs , nameof ( Trashing ) ) )
2017-12-28 09:18:09 +01:00
{
scope . Complete ( ) ;
return OperationResult . Cancel ( evtMsgs ) ; // causes rollback
}
// if it's published we may want to force-unpublish it - that would be backward-compatible... but...
// making a radical decision here: trashing is equivalent to moving under an unpublished node so
// it's NOT unpublishing, only the content is now masked - allowing us to restore it if wanted
//if (content.HasPublishedVersion)
//{ }
PerformMoveLocked ( content , Constants . System . RecycleBinContent , null , userId , moves , true ) ;
scope . Events . Dispatch ( TreeChanged , this , new TreeChange < IContent > ( content , TreeChangeTypes . RefreshBranch ) . ToEventArgs ( ) ) ;
var moveInfo = moves
. Select ( x = > new MoveEventInfo < IContent > ( x . Item1 , x . Item2 , x . Item1 . ParentId ) )
. ToArray ( ) ;
moveEventArgs . CanCancel = false ;
moveEventArgs . MoveInfoCollection = moveInfo ;
2018-04-11 09:05:02 +10:00
scope . Events . Dispatch ( Trashed , this , moveEventArgs , nameof ( Trashed ) ) ;
2018-10-18 22:47:12 +11:00
Audit ( AuditType . Move , userId , content . Id , "Moved to recycle bin" ) ;
2017-12-28 09:18:09 +01:00
scope . Complete ( ) ;
}
return OperationResult . Succeed ( evtMsgs ) ;
}
/// <summary>
/// Moves an <see cref="IContent"/> object to a new location by changing its parent id.
/// </summary>
/// <remarks>
/// If the <see cref="IContent"/> object is already published it will be
/// published after being moved to its new location. Otherwise it'll just
/// be saved with a new parent id.
/// </remarks>
/// <param name="content">The <see cref="IContent"/> to move</param>
/// <param name="parentId">Id of the Content's new Parent</param>
/// <param name="userId">Optional Id of the User moving the Content</param>
2019-02-06 14:01:14 +00:00
public void Move ( IContent content , int parentId , int userId = Constants . Security . SuperUserId )
2017-12-28 09:18:09 +01:00
{
// if moving to the recycle bin then use the proper method
if ( parentId = = Constants . System . RecycleBinContent )
{
MoveToRecycleBin ( content , userId ) ;
return ;
}
var moves = new List < Tuple < IContent , string > > ( ) ;
using ( var scope = ScopeProvider . CreateScope ( ) )
{
scope . WriteLock ( Constants . Locks . ContentTree ) ;
var parent = parentId = = Constants . System . Root ? null : GetById ( parentId ) ;
if ( parentId ! = Constants . System . Root & & ( parent = = null | | parent . Trashed ) )
throw new InvalidOperationException ( "Parent does not exist or is trashed." ) ; // causes rollback
var moveEventInfo = new MoveEventInfo < IContent > ( content , content . Path , parentId ) ;
var moveEventArgs = new MoveEventArgs < IContent > ( moveEventInfo ) ;
2018-05-31 15:43:39 +10:00
if ( scope . Events . DispatchCancelable ( Moving , this , moveEventArgs , nameof ( Moving ) ) )
2017-12-28 09:18:09 +01:00
{
scope . Complete ( ) ;
return ; // causes rollback
}
// if content was trashed, and since we're not moving to the recycle bin,
// indicate that the trashed status should be changed to false, else just
// leave it unchanged
var trashed = content . Trashed ? false : ( bool? ) null ;
// if the content was trashed under another content, and so has a published version,
// it cannot move back as published but has to be unpublished first - that's for the
// root content, everything underneath will retain its published status
if ( content . Trashed & & content . Published )
{
// however, it had been masked when being trashed, so there's no need for
// any special event here - just change its state
2019-02-06 16:10:20 +11:00
content . PublishedState = PublishedState . Unpublishing ;
2017-12-28 09:18:09 +01:00
}
PerformMoveLocked ( content , parentId , parent , userId , moves , trashed ) ;
scope . Events . Dispatch ( TreeChanged , this , new TreeChange < IContent > ( content , TreeChangeTypes . RefreshBranch ) . ToEventArgs ( ) ) ;
var moveInfo = moves //changes
. Select ( x = > new MoveEventInfo < IContent > ( x . Item1 , x . Item2 , x . Item1 . ParentId ) )
. ToArray ( ) ;
moveEventArgs . MoveInfoCollection = moveInfo ;
moveEventArgs . CanCancel = false ;
2018-05-31 15:43:39 +10:00
scope . Events . Dispatch ( Moved , this , moveEventArgs , nameof ( Moved ) ) ;
2018-10-18 22:47:12 +11:00
Audit ( AuditType . Move , userId , content . Id ) ;
2017-12-28 09:18:09 +01:00
scope . Complete ( ) ;
}
}
// MUST be called from within WriteLock
// trash indicates whether we are trashing, un-trashing, or not changing anything
private void PerformMoveLocked ( IContent content , int parentId , IContent parent , int userId ,
ICollection < Tuple < IContent , string > > moves ,
bool? trash )
{
content . WriterId = userId ;
content . ParentId = parentId ;
// get the level delta (old pos to new pos)
var levelDelta = parent = = null
? 1 - content . Level + ( parentId = = Constants . System . RecycleBinContent ? 1 : 0 )
: parent . Level + 1 - content . Level ;
var paths = new Dictionary < int , string > ( ) ;
moves . Add ( Tuple . Create ( content , content . Path ) ) ; // capture original path
2018-10-31 22:38:58 +11:00
//need to store the original path to lookup descendants based on it below
var originalPath = content . Path ;
2017-12-28 09:18:09 +01:00
// these will be updated by the repo because we changed parentId
//content.Path = (parent == null ? "-1" : parent.Path) + "," + content.Id;
//content.SortOrder = ((ContentRepository) repository).NextChildSortOrder(parentId);
//content.Level += levelDelta;
PerformMoveContentLocked ( content , userId , trash ) ;
// if uow is not immediate, content.Path will be updated only when the UOW commits,
// and because we want it now, we have to calculate it by ourselves
//paths[content.Id] = content.Path;
paths [ content . Id ] = ( parent = = null ? ( parentId = = Constants . System . RecycleBinContent ? "-1,-20" : "-1" ) : parent . Path ) + "," + content . Id ;
2018-10-31 18:01:39 +11:00
const int pageSize = 500 ;
var page = 0 ;
var total = long . MaxValue ;
2018-11-07 22:18:43 +11:00
while ( page * pageSize < total )
2017-12-28 09:18:09 +01:00
{
2018-11-01 00:05:17 +11:00
var descendants = GetPagedDescendantsLocked ( originalPath , page + + , pageSize , out total , null , Ordering . By ( "Path" , Direction . Ascending ) ) ;
2018-10-31 18:01:39 +11:00
foreach ( var descendant in descendants )
{
moves . Add ( Tuple . Create ( descendant , descendant . Path ) ) ; // capture original path
2017-12-28 09:18:09 +01:00
2018-10-31 18:01:39 +11:00
// update path and level since we do not update parentId
descendant . Path = paths [ descendant . Id ] = paths [ descendant . ParentId ] + "," + descendant . Id ;
descendant . Level + = levelDelta ;
PerformMoveContentLocked ( descendant , userId , trash ) ;
}
2017-12-28 09:18:09 +01:00
}
2018-11-07 22:18:43 +11:00
2017-12-28 09:18:09 +01:00
}
private void PerformMoveContentLocked ( IContent content , int userId , bool? trash )
{
2018-11-07 22:18:43 +11:00
if ( trash . HasValue ) ( ( ContentBase ) content ) . Trashed = trash . Value ;
2017-12-28 09:18:09 +01:00
content . WriterId = userId ;
_documentRepository . Save ( content ) ;
}
/// <summary>
/// Empties the Recycle Bin by deleting all <see cref="IContent"/> that resides in the bin
/// </summary>
2018-04-04 13:11:12 +10:00
public OperationResult EmptyRecycleBin ( )
2017-12-28 09:18:09 +01:00
{
var nodeObjectType = Constants . ObjectTypes . Document ;
var deleted = new List < IContent > ( ) ;
2018-04-04 13:11:12 +10:00
var evtMsgs = EventMessagesFactory . Get ( ) ;
2017-12-28 09:18:09 +01:00
using ( var scope = ScopeProvider . CreateScope ( ) )
{
scope . WriteLock ( Constants . Locks . ContentTree ) ;
// v7 EmptyingRecycleBin and EmptiedRecycleBin events are greatly simplified since
// each deleted items will have its own deleting/deleted events. so, files and such
// are managed by Delete, and not here.
// no idea what those events are for, keep a simplified version
2018-04-04 13:11:12 +10:00
var recycleBinEventArgs = new RecycleBinEventArgs ( nodeObjectType , evtMsgs ) ;
2017-12-28 09:18:09 +01:00
if ( scope . Events . DispatchCancelable ( EmptyingRecycleBin , this , recycleBinEventArgs ) )
{
scope . Complete ( ) ;
2018-04-04 13:11:12 +10:00
return OperationResult . Cancel ( evtMsgs ) ;
2017-12-28 09:18:09 +01:00
}
2019-01-22 18:03:39 -05:00
// emptying the recycle bin means deleting whatever is in there - do it properly!
2017-12-28 09:18:09 +01:00
var query = Query < IContent > ( ) . Where ( x = > x . ParentId = = Constants . System . RecycleBinContent ) ;
var contents = _documentRepository . Get ( query ) . ToArray ( ) ;
foreach ( var content in contents )
{
DeleteLocked ( scope , content ) ;
deleted . Add ( content ) ;
}
recycleBinEventArgs . CanCancel = false ;
recycleBinEventArgs . RecycleBinEmptiedSuccessfully = true ; // oh my?!
scope . Events . Dispatch ( EmptiedRecycleBin , this , recycleBinEventArgs ) ;
scope . Events . Dispatch ( TreeChanged , this , deleted . Select ( x = > new TreeChange < IContent > ( x , TreeChangeTypes . Remove ) ) . ToEventArgs ( ) ) ;
2018-10-18 22:47:12 +11:00
Audit ( AuditType . Delete , 0 , Constants . System . RecycleBinContent , "Recycle bin emptied" ) ;
2017-12-28 09:18:09 +01:00
scope . Complete ( ) ;
}
2018-04-04 13:11:12 +10:00
return OperationResult . Succeed ( evtMsgs ) ;
2017-12-28 09:18:09 +01:00
}
#endregion
#region Others
/// <summary>
/// Copies an <see cref="IContent"/> object by creating a new Content object of the same type and copies all data from the current
/// to the new copy which is returned. Recursively copies all children.
/// </summary>
/// <param name="content">The <see cref="IContent"/> to copy</param>
/// <param name="parentId">Id of the Content's new Parent</param>
/// <param name="relateToOriginal">Boolean indicating whether the copy should be related to the original</param>
/// <param name="userId">Optional Id of the User copying the Content</param>
/// <returns>The newly created <see cref="IContent"/> object</returns>
2019-02-06 14:01:14 +00:00
public IContent Copy ( IContent content , int parentId , bool relateToOriginal , int userId = Constants . Security . SuperUserId )
2017-12-28 09:18:09 +01:00
{
return Copy ( content , parentId , relateToOriginal , true , userId ) ;
}
/// <summary>
/// Copies an <see cref="IContent"/> object by creating a new Content object of the same type and copies all data from the current
/// to the new copy which is returned.
/// </summary>
/// <param name="content">The <see cref="IContent"/> to copy</param>
/// <param name="parentId">Id of the Content's new Parent</param>
/// <param name="relateToOriginal">Boolean indicating whether the copy should be related to the original</param>
/// <param name="recursive">A value indicating whether to recursively copy children.</param>
/// <param name="userId">Optional Id of the User copying the Content</param>
/// <returns>The newly created <see cref="IContent"/> object</returns>
2019-02-06 14:01:14 +00:00
public IContent Copy ( IContent content , int parentId , bool relateToOriginal , bool recursive , int userId = Constants . Security . SuperUserId )
2017-12-28 09:18:09 +01:00
{
var copy = content . DeepCloneWithResetIdentities ( ) ;
copy . ParentId = parentId ;
using ( var scope = ScopeProvider . CreateScope ( ) )
{
var copyEventArgs = new CopyEventArgs < IContent > ( content , copy , true , parentId , relateToOriginal ) ;
if ( scope . Events . DispatchCancelable ( Copying , this , copyEventArgs ) )
{
scope . Complete ( ) ;
return null ;
}
// note - relateToOriginal is not managed here,
// it's just part of the Copied event args so the RelateOnCopyHandler knows what to do
// meaning that the event has to trigger for every copied content including descendants
var copies = new List < Tuple < IContent , IContent > > ( ) ;
scope . WriteLock ( Constants . Locks . ContentTree ) ;
// a copy is not published (but not really unpublishing either)
// update the create author and last edit author
if ( copy . Published )
2019-02-06 16:10:20 +11:00
copy . Published = false ;
2017-12-28 09:18:09 +01:00
copy . CreatorId = userId ;
copy . WriterId = userId ;
//get the current permissions, if there are any explicit ones they need to be copied
var currentPermissions = GetPermissions ( content ) ;
currentPermissions . RemoveWhere ( p = > p . IsDefaultPermissions ) ;
// save and flush because we need the ID for the recursive Copying events
_documentRepository . Save ( copy ) ;
//add permissions
if ( currentPermissions . Count > 0 )
{
var permissionSet = new ContentPermissionSet ( copy , currentPermissions ) ;
_documentRepository . AddOrUpdatePermissions ( permissionSet ) ;
}
// keep track of copies
copies . Add ( Tuple . Create ( content , copy ) ) ;
var idmap = new Dictionary < int , int > { [ content . Id ] = copy . Id } ;
if ( recursive ) // process descendants
{
2018-10-31 18:01:39 +11:00
const int pageSize = 500 ;
var page = 0 ;
var total = long . MaxValue ;
2018-11-07 22:18:43 +11:00
while ( page * pageSize < total )
2017-12-28 09:18:09 +01:00
{
2018-10-31 18:01:39 +11:00
var descendants = GetPagedDescendants ( content . Id , page + + , pageSize , out total ) ;
foreach ( var descendant in descendants )
{
// if parent has not been copied, skip, else gets its copy id
if ( idmap . TryGetValue ( descendant . ParentId , out parentId ) = = false ) continue ;
2017-12-28 09:18:09 +01:00
2018-10-31 18:01:39 +11:00
var descendantCopy = descendant . DeepCloneWithResetIdentities ( ) ;
descendantCopy . ParentId = parentId ;
2017-12-28 09:18:09 +01:00
2018-10-31 18:01:39 +11:00
if ( scope . Events . DispatchCancelable ( Copying , this , new CopyEventArgs < IContent > ( descendant , descendantCopy , parentId ) ) )
continue ;
2017-12-28 09:18:09 +01:00
2018-10-31 18:01:39 +11:00
// a copy is not published (but not really unpublishing either)
// update the create author and last edit author
if ( descendantCopy . Published )
2019-02-06 16:10:20 +11:00
descendantCopy . Published = false ;
2018-10-31 18:01:39 +11:00
descendantCopy . CreatorId = userId ;
descendantCopy . WriterId = userId ;
2017-12-28 09:18:09 +01:00
2018-10-31 18:01:39 +11:00
// save and flush (see above)
_documentRepository . Save ( descendantCopy ) ;
2017-12-28 09:18:09 +01:00
2018-10-31 18:01:39 +11:00
copies . Add ( Tuple . Create ( descendant , descendantCopy ) ) ;
idmap [ descendant . Id ] = descendantCopy . Id ;
}
2017-12-28 09:18:09 +01:00
}
}
// not handling tags here, because
// - tags should be handled by the content repository
// - a copy is unpublished and therefore has no impact on tags in DB
scope . Events . Dispatch ( TreeChanged , this , new TreeChange < IContent > ( copy , TreeChangeTypes . RefreshBranch ) . ToEventArgs ( ) ) ;
foreach ( var x in copies )
scope . Events . Dispatch ( Copied , this , new CopyEventArgs < IContent > ( x . Item1 , x . Item2 , false , x . Item2 . ParentId , relateToOriginal ) ) ;
2018-10-18 22:47:12 +11:00
Audit ( AuditType . Copy , userId , content . Id ) ;
2017-12-28 09:18:09 +01:00
scope . Complete ( ) ;
}
return copy ;
}
/// <summary>
/// Sends an <see cref="IContent"/> to Publication, which executes handlers and events for the 'Send to Publication' action.
/// </summary>
/// <param name="content">The <see cref="IContent"/> to send to publication</param>
2019-01-22 18:03:39 -05:00
/// <param name="userId">Optional Id of the User issuing the send to publication</param>
/// <returns>True if sending publication was successful otherwise false</returns>
2019-02-06 14:01:14 +00:00
public bool SendToPublication ( IContent content , int userId = Constants . Security . SuperUserId )
2017-12-28 09:18:09 +01:00
{
using ( var scope = ScopeProvider . CreateScope ( ) )
{
var sendToPublishEventArgs = new SendToPublishEventArgs < IContent > ( content ) ;
if ( scope . Events . DispatchCancelable ( SendingToPublish , this , sendToPublishEventArgs ) )
{
scope . Complete ( ) ;
return false ;
}
2018-10-19 13:24:36 +11:00
//track the cultures changing for auditing
var culturesChanging = content . ContentType . VariesByCulture ( )
2019-02-05 14:13:03 +11:00
? string . Join ( "," , content . CultureInfos . Values . Where ( x = > x . IsDirty ( ) ) . Select ( x = > x . Culture ) )
2018-10-19 13:24:36 +11:00
: null ;
2018-11-13 11:59:27 +01:00
2019-01-27 01:17:32 -05:00
// TODO: Currently there's no way to change track which variant properties have changed, we only have change
2018-10-19 13:24:36 +11:00
// tracking enabled on all values on the Property which doesn't allow us to know which variants have changed.
// in this particular case, determining which cultures have changed works with the above with names since it will
// have always changed if it's been saved in the back office but that's not really fail safe.
2017-12-28 09:18:09 +01:00
//Save before raising event
2018-10-19 13:24:36 +11:00
var saveResult = Save ( content , userId ) ;
2017-12-28 09:18:09 +01:00
2018-11-13 11:59:27 +01:00
// always complete (but maybe return a failed status)
scope . Complete ( ) ;
2018-10-18 22:47:12 +11:00
2018-11-13 11:59:27 +01:00
if ( ! saveResult . Success )
return saveResult . Success ;
2018-10-18 22:47:12 +11:00
2018-11-13 11:59:27 +01:00
sendToPublishEventArgs . CanCancel = false ;
scope . Events . Dispatch ( SentToPublish , this , sendToPublishEventArgs ) ;
if ( culturesChanging ! = null )
Audit ( AuditType . SendToPublishVariant , userId , content . Id , $"Send To Publish for cultures: {culturesChanging}" , culturesChanging ) ;
else
Audit ( AuditType . SendToPublish , content . WriterId , content . Id ) ;
2017-12-28 09:18:09 +01:00
2018-10-19 13:24:36 +11:00
return saveResult . Success ;
2017-12-28 09:18:09 +01:00
}
}
/// <summary>
/// Sorts a collection of <see cref="IContent"/> objects by updating the SortOrder according
2018-03-22 17:41:13 +01:00
/// to the ordering of items in the passed in <paramref name="items"/>.
2017-12-28 09:18:09 +01:00
/// </summary>
/// <remarks>
/// Using this method will ensure that the Published-state is maintained upon sorting
/// so the cache is updated accordingly - as needed.
/// </remarks>
/// <param name="items"></param>
/// <param name="userId"></param>
/// <param name="raiseEvents"></param>
2018-11-06 14:19:10 +01:00
/// <returns>Result indicating what action was taken when handling the command.</returns>
2019-02-06 14:01:14 +00:00
public OperationResult Sort ( IEnumerable < IContent > items , int userId = Constants . Security . SuperUserId , bool raiseEvents = true )
2017-12-28 09:18:09 +01:00
{
2018-10-24 23:55:55 +11:00
var evtMsgs = EventMessagesFactory . Get ( ) ;
2017-12-28 09:18:09 +01:00
var itemsA = items . ToArray ( ) ;
2018-10-24 23:55:55 +11:00
if ( itemsA . Length = = 0 ) return new OperationResult ( OperationResultType . NoOperation , evtMsgs ) ;
2017-12-28 09:18:09 +01:00
using ( var scope = ScopeProvider . CreateScope ( ) )
{
2018-03-22 17:41:13 +01:00
scope . WriteLock ( Constants . Locks . ContentTree ) ;
2017-12-28 09:18:09 +01:00
2018-10-24 23:55:55 +11:00
var ret = Sort ( scope , itemsA , userId , evtMsgs , raiseEvents ) ;
2018-03-22 17:41:13 +01:00
scope . Complete ( ) ;
return ret ;
}
}
2017-12-28 09:18:09 +01:00
2018-03-22 17:41:13 +01:00
/// <summary>
/// Sorts a collection of <see cref="IContent"/> objects by updating the SortOrder according
/// to the ordering of items identified by the <paramref name="ids"/>.
/// </summary>
/// <remarks>
/// Using this method will ensure that the Published-state is maintained upon sorting
/// so the cache is updated accordingly - as needed.
/// </remarks>
/// <param name="ids"></param>
/// <param name="userId"></param>
/// <param name="raiseEvents"></param>
2018-11-06 14:19:10 +01:00
/// <returns>Result indicating what action was taken when handling the command.</returns>
2019-02-06 14:01:14 +00:00
public OperationResult Sort ( IEnumerable < int > ids , int userId = Constants . Security . SuperUserId , bool raiseEvents = true )
2018-03-22 17:41:13 +01:00
{
2018-10-24 23:55:55 +11:00
var evtMsgs = EventMessagesFactory . Get ( ) ;
2018-03-22 17:41:13 +01:00
var idsA = ids . ToArray ( ) ;
2018-10-24 23:55:55 +11:00
if ( idsA . Length = = 0 ) return new OperationResult ( OperationResultType . NoOperation , evtMsgs ) ;
2017-12-28 09:18:09 +01:00
2018-03-22 17:41:13 +01:00
using ( var scope = ScopeProvider . CreateScope ( ) )
{
scope . WriteLock ( Constants . Locks . ContentTree ) ;
var itemsA = GetByIds ( idsA ) . ToArray ( ) ;
2017-12-28 09:18:09 +01:00
2018-10-24 23:55:55 +11:00
var ret = Sort ( scope , itemsA , userId , evtMsgs , raiseEvents ) ;
2018-03-22 17:41:13 +01:00
scope . Complete ( ) ;
return ret ;
}
}
2017-12-28 09:18:09 +01:00
2018-10-24 23:55:55 +11:00
private OperationResult Sort ( IScope scope , IContent [ ] itemsA , int userId , EventMessages evtMsgs , bool raiseEvents )
2018-03-22 17:41:13 +01:00
{
2019-02-06 16:10:20 +11:00
var saveEventArgs = new ContentSavingEventArgs ( itemsA , evtMsgs ) ;
2018-10-24 23:55:55 +11:00
if ( raiseEvents )
{
//raise cancelable sorting event
if ( scope . Events . DispatchCancelable ( Saving , this , saveEventArgs , nameof ( Sorting ) ) )
return OperationResult . Cancel ( evtMsgs ) ;
//raise saving event (this one cannot be canceled)
saveEventArgs . CanCancel = false ;
scope . Events . Dispatch ( Saving , this , saveEventArgs , nameof ( Saving ) ) ;
}
2017-12-28 09:18:09 +01:00
2018-03-22 17:41:13 +01:00
var published = new List < IContent > ( ) ;
var saved = new List < IContent > ( ) ;
var sortOrder = 0 ;
2017-12-28 09:18:09 +01:00
2018-03-22 17:41:13 +01:00
foreach ( var content in itemsA )
{
// if the current sort order equals that of the content we don't
// need to update it, so just increment the sort order and continue.
if ( content . SortOrder = = sortOrder )
2017-12-28 09:18:09 +01:00
{
2018-03-22 17:41:13 +01:00
sortOrder + + ;
continue ;
2017-12-28 09:18:09 +01:00
}
2018-03-22 17:41:13 +01:00
// else update
content . SortOrder = sortOrder + + ;
content . WriterId = userId ;
2017-12-28 09:18:09 +01:00
2018-03-22 17:41:13 +01:00
// if it's published, register it, no point running StrategyPublish
// since we're not really publishing it and it cannot be cancelled etc
if ( content . Published )
published . Add ( content ) ;
2017-12-28 09:18:09 +01:00
2018-03-22 17:41:13 +01:00
// save
saved . Add ( content ) ;
_documentRepository . Save ( content ) ;
}
2017-12-28 09:18:09 +01:00
2018-03-22 17:41:13 +01:00
if ( raiseEvents )
{
2019-02-06 16:10:20 +11:00
var savedEventsArgs = saveEventArgs . ToContentSavedEventArgs ( ) ;
2018-10-24 23:55:55 +11:00
//first saved, then sorted
2019-02-06 16:10:20 +11:00
scope . Events . Dispatch ( Saved , this , savedEventsArgs , nameof ( Saved ) ) ;
scope . Events . Dispatch ( Sorted , this , savedEventsArgs , nameof ( Sorted ) ) ;
2017-12-28 09:18:09 +01:00
}
2018-03-22 17:41:13 +01:00
scope . Events . Dispatch ( TreeChanged , this , saved . Select ( x = > new TreeChange < IContent > ( x , TreeChangeTypes . RefreshNode ) ) . ToEventArgs ( ) ) ;
if ( raiseEvents & & published . Any ( ) )
2019-02-06 16:10:20 +11:00
scope . Events . Dispatch ( Published , this , new ContentPublishedEventArgs ( published , false , evtMsgs ) , "Published" ) ;
2018-03-22 17:41:13 +01:00
2018-10-25 14:28:12 +02:00
Audit ( AuditType . Sort , userId , 0 , "Sorting content performed by user" ) ;
2018-10-24 23:55:55 +11:00
return OperationResult . Succeed ( evtMsgs ) ;
2017-12-28 09:18:09 +01:00
}
#endregion
#region Internal Methods
/// <summary>
/// Gets a collection of <see cref="IContent"/> descendants by the first Parent.
/// </summary>
/// <param name="content"><see cref="IContent"/> item to retrieve Descendants from</param>
/// <returns>An Enumerable list of <see cref="IContent"/> objects</returns>
internal IEnumerable < IContent > GetPublishedDescendants ( IContent content )
{
using ( var scope = ScopeProvider . CreateScope ( autoComplete : true ) )
{
scope . ReadLock ( Constants . Locks . ContentTree ) ;
return GetPublishedDescendantsLocked ( content ) . ToArray ( ) ; // ToArray important in uow!
}
}
internal IEnumerable < IContent > GetPublishedDescendantsLocked ( IContent content )
{
var pathMatch = content . Path + "," ;
var query = Query < IContent > ( ) . Where ( x = > x . Id ! = content . Id & & x . Path . StartsWith ( pathMatch ) /*&& x.Trashed == false*/ ) ;
var contents = _documentRepository . Get ( query ) ;
// beware! contents contains all published version below content
// including those that are not directly published because below an unpublished content
// these must be filtered out here
var parents = new List < int > { content . Id } ;
foreach ( var c in contents )
{
if ( parents . Contains ( c . ParentId ) )
{
yield return c ;
parents . Add ( c . Id ) ;
}
}
}
#endregion
#region Private Methods
2018-10-19 13:24:36 +11:00
private void Audit ( AuditType type , int userId , int objectId , string message = null , string parameters = null )
2017-12-28 09:18:09 +01:00
{
2018-10-19 13:24:36 +11:00
_auditRepository . Save ( new AuditItem ( objectId , type , userId , ObjectTypes . GetName ( UmbracoObjectTypes . Document ) , message , parameters ) ) ;
2017-12-28 09:18:09 +01:00
}
#endregion
#region Event Handlers
/// <summary>
/// Occurs before Delete
/// </summary>
public static event TypedEventHandler < IContentService , DeleteEventArgs < IContent > > Deleting ;
/// <summary>
/// Occurs after Delete
/// </summary>
public static event TypedEventHandler < IContentService , DeleteEventArgs < IContent > > Deleted ;
/// <summary>
/// Occurs before Delete Versions
/// </summary>
public static event TypedEventHandler < IContentService , DeleteRevisionsEventArgs > DeletingVersions ;
/// <summary>
/// Occurs after Delete Versions
/// </summary>
public static event TypedEventHandler < IContentService , DeleteRevisionsEventArgs > DeletedVersions ;
2018-10-24 23:55:55 +11:00
/// <summary>
/// Occurs before Sorting
/// </summary>
public static event TypedEventHandler < IContentService , SaveEventArgs < IContent > > Sorting ;
/// <summary>
/// Occurs after Sorting
/// </summary>
public static event TypedEventHandler < IContentService , SaveEventArgs < IContent > > Sorted ;
2017-12-28 09:18:09 +01:00
/// <summary>
/// Occurs before Save
/// </summary>
2019-02-06 16:10:20 +11:00
public static event TypedEventHandler < IContentService , ContentSavingEventArgs > Saving ;
2017-12-28 09:18:09 +01:00
/// <summary>
/// Occurs after Save
/// </summary>
2019-02-06 16:10:20 +11:00
public static event TypedEventHandler < IContentService , ContentSavedEventArgs > Saved ;
2017-12-28 09:18:09 +01:00
/// <summary>
/// Occurs before Copy
/// </summary>
public static event TypedEventHandler < IContentService , CopyEventArgs < IContent > > Copying ;
/// <summary>
/// Occurs after Copy
/// </summary>
public static event TypedEventHandler < IContentService , CopyEventArgs < IContent > > Copied ;
/// <summary>
/// Occurs before Content is moved to Recycle Bin
/// </summary>
public static event TypedEventHandler < IContentService , MoveEventArgs < IContent > > Trashing ;
/// <summary>
/// Occurs after Content is moved to Recycle Bin
/// </summary>
public static event TypedEventHandler < IContentService , MoveEventArgs < IContent > > Trashed ;
/// <summary>
/// Occurs before Move
/// </summary>
public static event TypedEventHandler < IContentService , MoveEventArgs < IContent > > Moving ;
/// <summary>
/// Occurs after Move
/// </summary>
public static event TypedEventHandler < IContentService , MoveEventArgs < IContent > > Moved ;
/// <summary>
/// Occurs before Rollback
/// </summary>
public static event TypedEventHandler < IContentService , RollbackEventArgs < IContent > > RollingBack ;
/// <summary>
/// Occurs after Rollback
/// </summary>
public static event TypedEventHandler < IContentService , RollbackEventArgs < IContent > > RolledBack ;
/// <summary>
/// Occurs before Send to Publish
/// </summary>
public static event TypedEventHandler < IContentService , SendToPublishEventArgs < IContent > > SendingToPublish ;
/// <summary>
/// Occurs after Send to Publish
/// </summary>
public static event TypedEventHandler < IContentService , SendToPublishEventArgs < IContent > > SentToPublish ;
/// <summary>
/// Occurs before the Recycle Bin is emptied
/// </summary>
public static event TypedEventHandler < IContentService , RecycleBinEventArgs > EmptyingRecycleBin ;
/// <summary>
/// Occurs after the Recycle Bin has been Emptied
/// </summary>
public static event TypedEventHandler < IContentService , RecycleBinEventArgs > EmptiedRecycleBin ;
/// <summary>
/// Occurs before publish
/// </summary>
2019-02-06 16:10:20 +11:00
public static event TypedEventHandler < IContentService , ContentPublishingEventArgs > Publishing ;
2017-12-28 09:18:09 +01:00
/// <summary>
/// Occurs after publish
/// </summary>
2019-02-06 16:10:20 +11:00
public static event TypedEventHandler < IContentService , ContentPublishedEventArgs > Published ;
2017-12-28 09:18:09 +01:00
/// <summary>
/// Occurs before unpublish
/// </summary>
2018-10-03 14:27:48 +02:00
public static event TypedEventHandler < IContentService , PublishEventArgs < IContent > > Unpublishing ;
2017-12-28 09:18:09 +01:00
/// <summary>
/// Occurs after unpublish
/// </summary>
2018-10-03 14:27:48 +02:00
public static event TypedEventHandler < IContentService , PublishEventArgs < IContent > > Unpublished ;
2017-12-28 09:18:09 +01:00
/// <summary>
/// Occurs after change.
/// </summary>
internal static event TypedEventHandler < IContentService , TreeChange < IContent > . EventArgs > TreeChanged ;
/// <summary>
/// Occurs after a blueprint has been saved.
/// </summary>
public static event TypedEventHandler < IContentService , SaveEventArgs < IContent > > SavedBlueprint ;
/// <summary>
/// Occurs after a blueprint has been deleted.
/// </summary>
public static event TypedEventHandler < IContentService , DeleteEventArgs < IContent > > DeletedBlueprint ;
#endregion
#region Publishing Strategies
2018-11-06 21:33:24 +11:00
/// <summary>
/// Ensures that a document can be published
/// </summary>
/// <param name="scope"></param>
/// <param name="content"></param>
/// <param name="checkPath"></param>
2019-02-06 16:10:20 +11:00
/// <param name="culturesUnpublishing"></param>
2018-11-06 21:33:24 +11:00
/// <param name="evtMsgs"></param>
2019-02-06 16:10:20 +11:00
/// <param name="culturesPublishing"></param>
/// <param name="savingEventArgs"></param>
2018-11-06 21:33:24 +11:00
/// <returns></returns>
2019-02-06 16:10:20 +11:00
private PublishResult StrategyCanPublish ( IScope scope , IContent content , bool checkPath , IReadOnlyList < string > culturesPublishing , IReadOnlyCollection < string > culturesUnpublishing , EventMessages evtMsgs , ContentSavingEventArgs savingEventArgs )
2018-11-07 22:18:43 +11:00
{
2017-12-28 09:18:09 +01:00
// raise Publishing event
2019-02-06 16:10:20 +11:00
if ( scope . Events . DispatchCancelable ( Publishing , this , savingEventArgs . ToContentPublishingEventArgs ( ) ) )
2017-12-28 09:18:09 +01:00
{
2018-09-11 08:44:58 +01:00
Logger . Info < ContentService > ( "Document {ContentName} (id={ContentId}) cannot be published: {Reason}" , content . Name , content . Id , "publishing was cancelled" ) ;
2018-11-07 19:42:49 +11:00
return new PublishResult ( PublishResultType . FailedPublishCancelledByEvent , evtMsgs , content ) ;
2017-12-28 09:18:09 +01:00
}
2018-11-06 21:33:24 +11:00
var variesByCulture = content . ContentType . VariesByCulture ( ) ;
2018-11-08 16:33:19 +01:00
2018-11-07 19:42:49 +11:00
//First check if mandatory languages fails, if this fails it will mean anything that the published flag on the document will
// be changed to Unpublished and any culture currently published will not be visible.
2018-11-06 21:33:24 +11:00
if ( variesByCulture )
{
2018-11-08 12:28:53 +11:00
if ( content . Published & & culturesPublishing . Count = = 0 & & culturesUnpublishing . Count = = 0 ) // no published cultures = cannot be published
2018-11-07 21:32:12 +11:00
return new PublishResult ( PublishResultType . FailedPublishNothingToPublish , evtMsgs , content ) ;
2018-11-07 22:18:43 +11:00
// missing mandatory culture = cannot be published
2018-11-07 21:32:12 +11:00
var mandatoryCultures = _languageRepository . GetMany ( ) . Where ( x = > x . IsMandatory ) . Select ( x = > x . IsoCode ) ;
2018-11-08 16:33:19 +01:00
var mandatoryMissing = mandatoryCultures . Any ( x = > ! content . PublishedCultures . Contains ( x , StringComparer . OrdinalIgnoreCase ) ) ;
2018-11-07 21:32:12 +11:00
if ( mandatoryMissing )
2018-11-07 19:42:49 +11:00
return new PublishResult ( PublishResultType . FailedPublishMandatoryCultureMissing , evtMsgs , content ) ;
2018-11-06 21:33:24 +11:00
2018-11-07 22:18:43 +11:00
if ( culturesPublishing . Count = = 0 & & culturesUnpublishing . Count > 0 )
return new PublishResult ( PublishResultType . SuccessUnpublishCulture , evtMsgs , content ) ;
2018-11-06 21:33:24 +11:00
}
2017-12-28 09:18:09 +01:00
// ensure that the document has published values
// either because it is 'publishing' or because it already has a published version
2019-02-06 16:10:20 +11:00
if ( content . PublishedState ! = PublishedState . Publishing & & content . PublishedVersionId = = 0 )
2017-12-28 09:18:09 +01:00
{
2018-09-11 08:44:58 +01:00
Logger . Info < ContentService > ( "Document {ContentName} (id={ContentId}) cannot be published: {Reason}" , content . Name , content . Id , "document does not have published values" ) ;
2018-11-07 21:32:12 +11:00
return new PublishResult ( PublishResultType . FailedPublishNothingToPublish , evtMsgs , content ) ;
2017-12-28 09:18:09 +01:00
}
2018-11-07 19:42:49 +11:00
//loop over each culture publishing - or string.Empty for invariant
2018-11-07 22:18:43 +11:00
foreach ( var culture in culturesPublishing ? ? ( new [ ] { string . Empty } ) )
2017-12-28 09:18:09 +01:00
{
2018-11-06 21:33:24 +11:00
// ensure that the document status is correct
// note: culture will be string.Empty for invariant
switch ( content . GetStatus ( culture ) )
{
case ContentStatus . Expired :
if ( ! variesByCulture )
Logger . Info < ContentService > ( "Document {ContentName} (id={ContentId}) cannot be published: {Reason}" , content . Name , content . Id , "document has expired" ) ;
else
Logger . Info < ContentService > ( "Document {ContentName} (id={ContentId}) culture {Culture} cannot be published: {Reason}" , content . Name , content . Id , culture , "document culture has expired" ) ;
2018-11-07 19:42:49 +11:00
return new PublishResult ( ! variesByCulture ? PublishResultType . FailedPublishHasExpired : PublishResultType . FailedPublishCultureHasExpired , evtMsgs , content ) ;
2017-12-28 09:18:09 +01:00
2018-11-06 21:33:24 +11:00
case ContentStatus . AwaitingRelease :
if ( ! variesByCulture )
Logger . Info < ContentService > ( "Document {ContentName} (id={ContentId}) cannot be published: {Reason}" , content . Name , content . Id , "document is awaiting release" ) ;
else
Logger . Info < ContentService > ( "Document {ContentName} (id={ContentId}) culture {Culture} cannot be published: {Reason}" , content . Name , content . Id , culture , "document is culture awaiting release" ) ;
2018-11-07 19:42:49 +11:00
return new PublishResult ( ! variesByCulture ? PublishResultType . FailedPublishAwaitingRelease : PublishResultType . FailedPublishCultureAwaitingRelease , evtMsgs , content ) ;
2017-12-28 09:18:09 +01:00
2018-11-06 21:33:24 +11:00
case ContentStatus . Trashed :
Logger . Info < ContentService > ( "Document {ContentName} (id={ContentId}) cannot be published: {Reason}" , content . Name , content . Id , "document is trashed" ) ;
2018-11-07 19:42:49 +11:00
return new PublishResult ( PublishResultType . FailedPublishIsTrashed , evtMsgs , content ) ;
2018-11-06 21:33:24 +11:00
}
2017-12-28 09:18:09 +01:00
}
2018-11-07 19:42:49 +11:00
if ( checkPath )
2017-12-28 09:18:09 +01:00
{
2018-11-07 19:42:49 +11:00
// check if the content can be path-published
// root content can be published
// else check ancestors - we know we are not trashed
var pathIsOk = content . ParentId = = Constants . System . Root | | IsPathPublished ( GetParent ( content ) ) ;
if ( ! pathIsOk )
{
Logger . Info < ContentService > ( "Document {ContentName} (id={ContentId}) cannot be published: {Reason}" , content . Name , content . Id , "parent is not published" ) ;
return new PublishResult ( PublishResultType . FailedPublishPathNotPublished , evtMsgs , content ) ;
}
2017-12-28 09:18:09 +01:00
}
2018-11-08 12:49:37 +11:00
//If we are both publishing and unpublishing cultures, then return a mixed status
if ( variesByCulture & & culturesPublishing . Count > 0 & & culturesUnpublishing . Count > 0 )
return new PublishResult ( PublishResultType . SuccessMixedCulture , evtMsgs , content ) ;
2018-11-07 22:18:43 +11:00
2017-12-28 09:18:09 +01:00
return new PublishResult ( evtMsgs , content ) ;
}
2018-11-06 21:33:24 +11:00
/// <summary>
/// Publishes a document
/// </summary>
/// <param name="content"></param>
2019-02-06 16:10:20 +11:00
/// <param name="culturesUnpublishing"></param>
2018-11-06 21:33:24 +11:00
/// <param name="evtMsgs"></param>
2019-02-06 16:10:20 +11:00
/// <param name="culturesPublishing"></param>
2018-11-06 21:33:24 +11:00
/// <returns></returns>
/// <remarks>
/// It is assumed that all publishing checks have passed before calling this method like <see cref="StrategyCanPublish"/>
/// </remarks>
2019-02-06 16:10:20 +11:00
private PublishResult StrategyPublish ( IContent content ,
IReadOnlyCollection < string > culturesPublishing , IReadOnlyCollection < string > culturesUnpublishing ,
2018-11-07 19:42:49 +11:00
EventMessages evtMsgs )
2017-12-28 09:18:09 +01:00
{
// change state to publishing
2019-02-06 16:10:20 +11:00
content . PublishedState = PublishedState . Publishing ;
2017-12-28 09:18:09 +01:00
2018-11-07 19:42:49 +11:00
//if this is a variant then we need to log which cultures have been published/unpublished and return an appropriate result
if ( content . ContentType . VariesByCulture ( ) )
{
2018-11-08 12:49:37 +11:00
if ( content . Published & & culturesUnpublishing . Count = = 0 & & culturesPublishing . Count = = 0 )
return new PublishResult ( PublishResultType . FailedPublishNothingToPublish , evtMsgs , content ) ;
2018-11-07 19:42:49 +11:00
if ( culturesUnpublishing . Count > 0 )
Logger . Info < ContentService > ( "Document {ContentName} (id={ContentId}) cultures: {Cultures} have been unpublished." ,
content . Name , content . Id , string . Join ( "," , culturesUnpublishing ) ) ;
2018-11-08 12:49:37 +11:00
if ( culturesPublishing . Count > 0 )
Logger . Info < ContentService > ( "Document {ContentName} (id={ContentId}) cultures: {Cultures} have been published." ,
2018-11-07 19:42:49 +11:00
content . Name , content . Id , string . Join ( "," , culturesPublishing ) ) ;
2018-11-08 12:49:37 +11:00
if ( culturesUnpublishing . Count > 0 & & culturesPublishing . Count > 0 )
return new PublishResult ( PublishResultType . SuccessMixedCulture , evtMsgs , content ) ;
if ( culturesUnpublishing . Count > 0 & & culturesPublishing . Count = = 0 )
return new PublishResult ( PublishResultType . SuccessUnpublishCulture , evtMsgs , content ) ;
2018-11-07 19:42:49 +11:00
return new PublishResult ( PublishResultType . SuccessPublishCulture , evtMsgs , content ) ;
}
2018-09-11 08:44:58 +01:00
Logger . Info < ContentService > ( "Document {ContentName} (id={ContentId}) has been published." , content . Name , content . Id ) ;
2018-11-07 19:42:49 +11:00
return new PublishResult ( evtMsgs , content ) ;
2017-12-28 09:18:09 +01:00
}
2018-11-06 21:33:24 +11:00
/// <summary>
/// Ensures that a document can be unpublished
/// </summary>
/// <param name="scope"></param>
/// <param name="content"></param>
/// <param name="evtMsgs"></param>
/// <returns></returns>
2019-02-06 16:10:20 +11:00
private PublishResult StrategyCanUnpublish ( IScope scope , IContent content , EventMessages evtMsgs )
2017-12-28 09:18:09 +01:00
{
2018-10-03 14:27:48 +02:00
// raise Unpublishing event
if ( scope . Events . DispatchCancelable ( Unpublishing , this , new PublishEventArgs < IContent > ( content , evtMsgs ) ) )
2017-12-28 09:18:09 +01:00
{
2018-09-11 08:44:58 +01:00
Logger . Info < ContentService > ( "Document {ContentName} (id={ContentId}) cannot be unpublished: unpublishing was cancelled." , content . Name , content . Id ) ;
2018-11-07 19:42:49 +11:00
return new PublishResult ( PublishResultType . FailedUnpublishCancelledByEvent , evtMsgs , content ) ;
2017-12-28 09:18:09 +01:00
}
2018-11-07 19:42:49 +11:00
return new PublishResult ( PublishResultType . SuccessUnpublish , evtMsgs , content ) ;
2017-12-28 09:18:09 +01:00
}
2018-11-06 21:33:24 +11:00
/// <summary>
2018-11-08 16:33:19 +01:00
/// Unpublishes a document
2018-11-06 21:33:24 +11:00
/// </summary>
/// <param name="scope"></param>
/// <param name="content"></param>
/// <param name="userId"></param>
/// <param name="evtMsgs"></param>
/// <returns></returns>
/// <remarks>
/// It is assumed that all unpublishing checks have passed before calling this method like <see cref="StrategyCanUnpublish"/>
/// </remarks>
2018-11-07 19:42:49 +11:00
private PublishResult StrategyUnpublish ( IScope scope , IContent content , int userId , EventMessages evtMsgs )
2017-12-28 09:18:09 +01:00
{
2018-11-07 19:42:49 +11:00
var attempt = new PublishResult ( PublishResultType . SuccessUnpublish , evtMsgs , content ) ;
2017-12-28 09:18:09 +01:00
if ( attempt . Success = = false )
return attempt ;
2018-11-02 14:55:34 +11:00
// if the document has any release dates set to before now,
// they should be removed so they don't interrupt an unpublish
2017-12-28 09:18:09 +01:00
// otherwise it would remain released == published
2018-11-02 14:55:34 +11:00
2018-11-14 09:16:22 +01:00
var pastReleases = content . ContentSchedule . GetPending ( ContentScheduleAction . Expire , DateTime . Now ) ;
2018-11-02 14:55:34 +11:00
foreach ( var p in pastReleases )
2018-11-07 22:18:43 +11:00
content . ContentSchedule . Remove ( p ) ;
if ( pastReleases . Count > 0 )
2018-09-11 08:44:58 +01:00
Logger . Info < ContentService > ( "Document {ContentName} (id={ContentId}) had its release date removed, because it was unpublished." , content . Name , content . Id ) ;
2017-12-28 09:18:09 +01:00
// change state to unpublishing
2019-02-06 16:10:20 +11:00
content . PublishedState = PublishedState . Unpublishing ;
2017-12-28 09:18:09 +01:00
2018-09-11 08:44:58 +01:00
Logger . Info < ContentService > ( "Document {ContentName} (id={ContentId}) has been unpublished." , content . Name , content . Id ) ;
2017-12-28 09:18:09 +01:00
return attempt ;
}
#endregion
#region Content Types
/// <summary>
/// Deletes all content of specified type. All children of deleted content is moved to Recycle Bin.
/// </summary>
/// <remarks>
/// <para>This needs extra care and attention as its potentially a dangerous and extensive operation.</para>
/// <para>Deletes content items of the specified type, and only that type. Does *not* handle content types
/// inheritance and compositions, which need to be managed outside of this method.</para>
/// </remarks>
2019-02-06 16:10:20 +11:00
/// <param name="contentTypeIds">Id of the <see cref="IContentType"/></param>
2019-01-22 18:03:39 -05:00
/// <param name="userId">Optional Id of the user issuing the delete operation</param>
2019-02-06 14:01:14 +00:00
public void DeleteOfTypes ( IEnumerable < int > contentTypeIds , int userId = Constants . Security . SuperUserId )
2017-12-28 09:18:09 +01:00
{
2019-01-27 01:17:32 -05:00
// TODO: This currently this is called from the ContentTypeService but that needs to change,
2017-12-28 09:18:09 +01:00
// if we are deleting a content type, we should just delete the data and do this operation slightly differently.
// This method will recursively go lookup every content item, check if any of it's descendants are
// of a different type, move them to the recycle bin, then permanently delete the content items.
// The main problem with this is that for every content item being deleted, events are raised...
// which we need for many things like keeping caches in sync, but we can surely do this MUCH better.
var changes = new List < TreeChange < IContent > > ( ) ;
var moves = new List < Tuple < IContent , string > > ( ) ;
var contentTypeIdsA = contentTypeIds . ToArray ( ) ;
// using an immediate uow here because we keep making changes with
// PerformMoveLocked and DeleteLocked that must be applied immediately,
// no point queuing operations
//
using ( var scope = ScopeProvider . CreateScope ( ) )
{
scope . WriteLock ( Constants . Locks . ContentTree ) ;
var query = Query < IContent > ( ) . WhereIn ( x = > x . ContentTypeId , contentTypeIdsA ) ;
var contents = _documentRepository . Get ( query ) . ToArray ( ) ;
2018-05-02 14:52:00 +10:00
if ( scope . Events . DispatchCancelable ( Deleting , this , new DeleteEventArgs < IContent > ( contents ) , nameof ( Deleting ) ) )
2017-12-28 09:18:09 +01:00
{
scope . Complete ( ) ;
return ;
}
// order by level, descending, so deepest first - that way, we cannot move
// a content of the deleted type, to the recycle bin (and then delete it...)
foreach ( var content in contents . OrderByDescending ( x = > x . ParentId ) )
{
// if it's not trashed yet, and published, we should unpublish
2018-10-03 14:27:48 +02:00
// but... Unpublishing event makes no sense (not going to cancel?) and no need to save
2017-12-28 09:18:09 +01:00
// just raise the event
if ( content . Trashed = = false & & content . Published )
2018-10-03 14:27:48 +02:00
scope . Events . Dispatch ( Unpublished , this , new PublishEventArgs < IContent > ( content , false , false ) , nameof ( Unpublished ) ) ;
2017-12-28 09:18:09 +01:00
// if current content has children, move them to trash
var c = content ;
var childQuery = Query < IContent > ( ) . Where ( x = > x . ParentId = = c . Id ) ;
var children = _documentRepository . Get ( childQuery ) ;
foreach ( var child in children )
{
// see MoveToRecycleBin
PerformMoveLocked ( child , Constants . System . RecycleBinContent , null , userId , moves , true ) ;
changes . Add ( new TreeChange < IContent > ( content , TreeChangeTypes . RefreshBranch ) ) ;
}
// delete content
// triggers the deleted event (and handles the files)
DeleteLocked ( scope , content ) ;
changes . Add ( new TreeChange < IContent > ( content , TreeChangeTypes . Remove ) ) ;
}
var moveInfos = moves
. Select ( x = > new MoveEventInfo < IContent > ( x . Item1 , x . Item2 , x . Item1 . ParentId ) )
. ToArray ( ) ;
if ( moveInfos . Length > 0 )
2018-05-31 15:43:39 +10:00
scope . Events . Dispatch ( Trashed , this , new MoveEventArgs < IContent > ( false , moveInfos ) , nameof ( Trashed ) ) ;
2017-12-28 09:18:09 +01:00
scope . Events . Dispatch ( TreeChanged , this , changes . ToEventArgs ( ) ) ;
2018-10-18 22:47:12 +11:00
Audit ( AuditType . Delete , userId , Constants . System . Root , $"Delete content of type {string.Join(" , ", contentTypeIdsA)}" ) ;
2017-12-28 09:18:09 +01:00
scope . Complete ( ) ;
}
}
/// <summary>
/// Deletes all content items of specified type. All children of deleted content item is moved to Recycle Bin.
/// </summary>
/// <remarks>This needs extra care and attention as its potentially a dangerous and extensive operation</remarks>
/// <param name="contentTypeId">Id of the <see cref="IContentType"/></param>
/// <param name="userId">Optional id of the user deleting the media</param>
2019-02-06 14:01:14 +00:00
public void DeleteOfType ( int contentTypeId , int userId = Constants . Security . SuperUserId )
2017-12-28 09:18:09 +01:00
{
DeleteOfTypes ( new [ ] { contentTypeId } , userId ) ;
}
private IContentType GetContentType ( IScope scope , string contentTypeAlias )
{
if ( string . IsNullOrWhiteSpace ( contentTypeAlias ) ) throw new ArgumentNullOrEmptyException ( nameof ( contentTypeAlias ) ) ;
scope . ReadLock ( Constants . Locks . ContentTypes ) ;
var query = Query < IContentType > ( ) . Where ( x = > x . Alias = = contentTypeAlias ) ;
var contentType = _contentTypeRepository . Get ( query ) . FirstOrDefault ( ) ;
if ( contentType = = null )
throw new Exception ( $"No ContentType matching the passed in Alias: '{contentTypeAlias}' was found" ) ; // causes rollback
return contentType ;
}
private IContentType GetContentType ( string contentTypeAlias )
{
if ( string . IsNullOrWhiteSpace ( contentTypeAlias ) ) throw new ArgumentNullOrEmptyException ( nameof ( contentTypeAlias ) ) ;
using ( var scope = ScopeProvider . CreateScope ( autoComplete : true ) )
{
return GetContentType ( scope , contentTypeAlias ) ;
}
}
#endregion
#region Blueprints
public IContent GetBlueprintById ( int id )
{
using ( var scope = ScopeProvider . CreateScope ( autoComplete : true ) )
{
scope . ReadLock ( Constants . Locks . ContentTree ) ;
var blueprint = _documentBlueprintRepository . Get ( id ) ;
if ( blueprint ! = null )
2019-02-06 16:10:20 +11:00
blueprint . Blueprint = true ;
2017-12-28 09:18:09 +01:00
return blueprint ;
}
}
public IContent GetBlueprintById ( Guid id )
{
using ( var scope = ScopeProvider . CreateScope ( autoComplete : true ) )
{
scope . ReadLock ( Constants . Locks . ContentTree ) ;
var blueprint = _documentBlueprintRepository . Get ( id ) ;
if ( blueprint ! = null )
2019-02-06 16:10:20 +11:00
blueprint . Blueprint = true ;
2017-12-28 09:18:09 +01:00
return blueprint ;
}
}
2019-02-06 14:01:14 +00:00
public void SaveBlueprint ( IContent content , int userId = Constants . Security . SuperUserId )
2017-12-28 09:18:09 +01:00
{
//always ensure the blueprint is at the root
if ( content . ParentId ! = - 1 )
content . ParentId = - 1 ;
2019-02-06 16:10:20 +11:00
content . Blueprint = true ;
2017-12-28 09:18:09 +01:00
using ( var scope = ScopeProvider . CreateScope ( ) )
{
scope . WriteLock ( Constants . Locks . ContentTree ) ;
if ( content . HasIdentity = = false )
{
content . CreatorId = userId ;
}
content . WriterId = userId ;
_documentBlueprintRepository . Save ( content ) ;
scope . Events . Dispatch ( SavedBlueprint , this , new SaveEventArgs < IContent > ( content ) , "SavedBlueprint" ) ;
scope . Complete ( ) ;
}
}
2019-02-06 14:01:14 +00:00
public void DeleteBlueprint ( IContent content , int userId = Constants . Security . SuperUserId )
2017-12-28 09:18:09 +01:00
{
using ( var scope = ScopeProvider . CreateScope ( ) )
{
scope . WriteLock ( Constants . Locks . ContentTree ) ;
_documentBlueprintRepository . Delete ( content ) ;
2018-05-02 14:52:00 +10:00
scope . Events . Dispatch ( DeletedBlueprint , this , new DeleteEventArgs < IContent > ( content ) , nameof ( DeletedBlueprint ) ) ;
2017-12-28 09:18:09 +01:00
scope . Complete ( ) ;
}
}
2019-01-11 08:06:12 +01:00
private static readonly string [ ] ArrayOfOneNullString = { null } ;
2019-02-06 14:01:14 +00:00
public IContent CreateContentFromBlueprint ( IContent blueprint , string name , int userId = Constants . Security . SuperUserId )
2017-12-28 09:18:09 +01:00
{
if ( blueprint = = null ) throw new ArgumentNullException ( nameof ( blueprint ) ) ;
2019-02-27 18:54:53 +01:00
var contentType = GetContentType ( blueprint . ContentType . Alias ) ;
2017-12-28 09:18:09 +01:00
var content = new Content ( name , - 1 , contentType ) ;
content . Path = string . Concat ( content . ParentId . ToString ( ) , "," , content . Id ) ;
content . CreatorId = userId ;
content . WriterId = userId ;
2019-01-08 15:23:05 +01:00
var now = DateTime . Now ;
2019-02-05 14:13:03 +11:00
var cultures = blueprint . CultureInfos . Count > 0 ? blueprint . CultureInfos . Values . Select ( x = > x . Culture ) : ArrayOfOneNullString ;
2019-01-08 15:23:05 +01:00
foreach ( var culture in cultures )
{
foreach ( var property in blueprint . Properties )
{
2019-03-12 08:17:09 +01:00
if ( property . PropertyType . VariesByCulture ( ) )
{
content . SetValue ( property . Alias , property . GetValue ( culture ) , culture ) ;
}
else
{
content . SetValue ( property . Alias , property . GetValue ( ) ) ;
}
2019-01-08 15:23:05 +01:00
}
content . Name = blueprint . Name ;
2019-01-10 07:34:28 +01:00
if ( ! string . IsNullOrEmpty ( culture ) )
{
content . SetCultureInfo ( culture , blueprint . GetCultureName ( culture ) , now ) ;
}
2019-01-08 15:23:05 +01:00
}
2017-12-28 09:18:09 +01:00
return content ;
}
public IEnumerable < IContent > GetBlueprintsForContentTypes ( params int [ ] contentTypeId )
{
using ( var scope = ScopeProvider . CreateScope ( autoComplete : true ) )
{
var query = Query < IContent > ( ) ;
if ( contentTypeId . Length > 0 )
{
query . Where ( x = > contentTypeId . Contains ( x . ContentTypeId ) ) ;
}
return _documentBlueprintRepository . Get ( query ) . Select ( x = >
{
2019-02-06 16:10:20 +11:00
x . Blueprint = true ;
2017-12-28 09:18:09 +01:00
return x ;
} ) ;
}
}
2019-02-06 14:01:14 +00:00
public void DeleteBlueprintsOfTypes ( IEnumerable < int > contentTypeIds , int userId = Constants . Security . SuperUserId )
2018-03-22 17:41:13 +01:00
{
using ( var scope = ScopeProvider . CreateScope ( ) )
{
scope . WriteLock ( Constants . Locks . ContentTree ) ;
var contentTypeIdsA = contentTypeIds . ToArray ( ) ;
var query = Query < IContent > ( ) ;
if ( contentTypeIdsA . Length > 0 )
query . Where ( x = > contentTypeIdsA . Contains ( x . ContentTypeId ) ) ;
var blueprints = _documentBlueprintRepository . Get ( query ) . Select ( x = >
{
2019-02-06 16:10:20 +11:00
x . Blueprint = true ;
2018-03-22 17:41:13 +01:00
return x ;
} ) . ToArray ( ) ;
foreach ( var blueprint in blueprints )
{
_documentBlueprintRepository . Delete ( blueprint ) ;
}
2018-05-02 14:52:00 +10:00
scope . Events . Dispatch ( DeletedBlueprint , this , new DeleteEventArgs < IContent > ( blueprints ) , nameof ( DeletedBlueprint ) ) ;
2018-03-22 17:41:13 +01:00
scope . Complete ( ) ;
}
}
2019-02-06 14:01:14 +00:00
public void DeleteBlueprintsOfType ( int contentTypeId , int userId = Constants . Security . SuperUserId )
2018-03-22 17:41:13 +01:00
{
DeleteBlueprintsOfTypes ( new [ ] { contentTypeId } , userId ) ;
}
2017-12-28 09:18:09 +01:00
#endregion
2018-10-18 14:24:32 +01:00
#region Rollback
2019-02-06 14:01:14 +00:00
public OperationResult Rollback ( int id , int versionId , string culture = "*" , int userId = Constants . Security . SuperUserId )
2018-10-18 14:24:32 +01:00
{
var evtMsgs = EventMessagesFactory . Get ( ) ;
2018-11-07 22:18:43 +11:00
2018-10-18 14:24:32 +01:00
//Get the current copy of the node
var content = GetById ( id ) ;
//Get the version
var version = GetVersion ( versionId ) ;
//Good ole null checks
if ( content = = null | | version = = null )
{
return new OperationResult ( OperationResultType . FailedCannot , evtMsgs ) ;
}
2018-10-18 15:51:22 +01:00
//Store the result of doing the save of content for the rollback
OperationResult rollbackSaveResult ;
2018-10-18 14:24:32 +01:00
2018-10-18 15:51:22 +01:00
using ( var scope = ScopeProvider . CreateScope ( ) )
2018-10-18 14:24:32 +01:00
{
2018-10-18 15:51:22 +01:00
var rollbackEventArgs = new RollbackEventArgs < IContent > ( content ) ;
//Emit RollingBack event aka before
2018-10-19 12:47:04 +02:00
if ( scope . Events . DispatchCancelable ( RollingBack , this , rollbackEventArgs ) )
{
scope . Complete ( ) ;
return OperationResult . Cancel ( evtMsgs ) ;
}
2018-10-18 15:25:11 +01:00
2018-10-18 15:51:22 +01:00
//Copy the changes from the version
content . CopyFrom ( version , culture ) ;
//Save the content for the rollback
rollbackSaveResult = Save ( content , userId ) ;
//Depending on the save result - is what we log & audit along with what we return
if ( rollbackSaveResult . Success = = false )
{
//Log the error/warning
Logger . Error < ContentService > ( "User '{UserId}' was unable to rollback content '{ContentId}' to version '{VersionId}'" , userId , id , versionId ) ;
}
else
{
//Emit RolledBack event aka after
2018-10-19 12:47:04 +02:00
rollbackEventArgs . CanCancel = false ;
2018-10-18 15:51:22 +01:00
scope . Events . Dispatch ( RolledBack , this , rollbackEventArgs ) ;
//Logging & Audit message
Logger . Info < ContentService > ( "User '{UserId}' rolled back content '{ContentId}' to version '{VersionId}'" , userId , id , versionId ) ;
2018-10-23 15:04:41 +02:00
Audit ( AuditType . RollBack , userId , id , $"Content '{content.Name}' was rolled back to version '{versionId}'" ) ;
2018-10-18 15:51:22 +01:00
}
2018-11-07 22:18:43 +11:00
2018-10-18 15:51:22 +01:00
scope . Complete ( ) ;
2018-10-18 14:24:32 +01:00
}
return rollbackSaveResult ;
}
#endregion
2017-12-28 09:18:09 +01:00
}
}