using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Xml.Linq; using Umbraco.Core.Events; using Umbraco.Core.IO; 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; using Umbraco.Core.Persistence.UnitOfWork; using Umbraco.Core.Strings; namespace Umbraco.Core.Services { /// /// Represents the Content Service, which is an easy access to operations involving /// public class ContentService : RepositoryService, IContentService, IContentServiceOperations { private readonly EntityXmlSerializer _entitySerializer = new EntityXmlSerializer(); private readonly IDataTypeService _dataTypeService; private readonly IUserService _userService; private readonly IEnumerable _urlSegmentProviders; private IContentTypeService _contentTypeService; #region Constructors public ContentService( IDatabaseUnitOfWorkProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory, IDataTypeService dataTypeService, IUserService userService, IEnumerable urlSegmentProviders) : base(provider, logger, eventMessagesFactory) { if (dataTypeService == null) throw new ArgumentNullException(nameof(dataTypeService)); if (userService == null) throw new ArgumentNullException(nameof(userService)); if (urlSegmentProviders == null) throw new ArgumentNullException(nameof(urlSegmentProviders)); _dataTypeService = dataTypeService; _userService = userService; _urlSegmentProviders = urlSegmentProviders; } // don't change or remove this, will need it later private IContentTypeService ContentTypeService => _contentTypeService; //// handle circular dependencies //internal IContentTypeService ContentTypeService //{ // get // { // if (_contentTypeService == null) // throw new InvalidOperationException("ContentService.ContentTypeService has not been initialized."); // return _contentTypeService; // } // set { _contentTypeService = value; } //} #endregion #region Count public int CountPublished(string contentTypeAlias = null) { using (var uow = UowProvider.CreateUnitOfWork()) { uow.ReadLock(Constants.Locks.ContentTree); var repo = uow.CreateRepository(); var count = repo.CountPublished(); uow.Complete(); return count; } } public int Count(string contentTypeAlias = null) { using (var uow = UowProvider.CreateUnitOfWork()) { uow.ReadLock(Constants.Locks.ContentTree); var repo = uow.CreateRepository(); var count = repo.Count(contentTypeAlias); uow.Complete(); return count; } } public int CountChildren(int parentId, string contentTypeAlias = null) { using (var uow = UowProvider.CreateUnitOfWork()) { uow.ReadLock(Constants.Locks.ContentTree); var repo = uow.CreateRepository(); var count = repo.CountChildren(parentId, contentTypeAlias); uow.Complete(); return count; } } public int CountDescendants(int parentId, string contentTypeAlias = null) { using (var uow = UowProvider.CreateUnitOfWork()) { uow.ReadLock(Constants.Locks.ContentTree); var repo = uow.CreateRepository(); var count = repo.CountDescendants(parentId, contentTypeAlias); uow.Complete(); return count; } } #endregion #region Permissions /// /// 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. /// /// public void ReplaceContentPermissions(EntityPermissionSet permissionSet) { using (var uow = UowProvider.CreateUnitOfWork()) { uow.WriteLock(Constants.Locks.ContentTree); var repo = uow.CreateRepository(); repo.ReplaceContentPermissions(permissionSet); uow.Complete(); } } /// /// Assigns a single permission to the current content item for the specified user ids /// /// /// /// public void AssignContentPermission(IContent entity, char permission, IEnumerable userIds) { using (var uow = UowProvider.CreateUnitOfWork()) { uow.WriteLock(Constants.Locks.ContentTree); var repo = uow.CreateRepository(); repo.AssignEntityPermission(entity, permission, userIds); uow.Complete(); } } /// /// Gets the list of permissions for the content item /// /// /// public IEnumerable GetPermissionsForEntity(IContent content) { using (var uow = UowProvider.CreateUnitOfWork()) { uow.ReadLock(Constants.Locks.ContentTree); var repo = uow.CreateRepository(); var perms = repo.GetPermissionsForEntity(content.Id); uow.Complete(); return perms; } } #endregion #region Create /// /// Creates an object of a specified content type. /// /// 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. /// /// The name of the content object. /// The identifier of the parent, or -1. /// The alias of the content type. /// The optional id of the user creating the content. /// The content object. public IContent CreateContent(string name, int parentId, string contentTypeAlias, int userId = 0) { 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); CreateContent(null, content, parent, userId, false); return content; } /// /// Creates an object of a specified content type, at root. /// /// 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. /// /// The name of the content object. /// The alias of the content type. /// The optional id of the user creating the content. /// The content object. public IContent CreateContent(string name, string contentTypeAlias, int userId = 0) { // not locking since not saving anything var contentType = GetContentType(contentTypeAlias); if (contentType == null) throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias)); var content = new Content(name, -1, contentType); CreateContent(null, content, null, userId, false); return content; } /// /// Creates an object of a specified content type, under a parent. /// /// 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. /// /// The name of the content object. /// The parent content object. /// The alias of the content type. /// The optional id of the user creating the content. /// The content object. public IContent CreateContent(string name, IContent parent, string contentTypeAlias, int userId = 0) { if (parent == null) throw new ArgumentNullException(nameof(parent)); using (var uow = UowProvider.CreateUnitOfWork()) { // 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); CreateContent(uow, content, parent, userId, false); uow.Complete(); return content; } } /// /// Creates an object of a specified content type. /// /// This method returns a new, persisted, IContent with an identity. /// The name of the content object. /// The identifier of the parent, or -1. /// The alias of the content type. /// The optional id of the user creating the content. /// The content object. public IContent CreateContentWithIdentity(string name, int parentId, string contentTypeAlias, int userId = 0) { using (var uow = UowProvider.CreateUnitOfWork()) { // locking the content tree secures content types too uow.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); CreateContent(uow, content, parent, userId, true); uow.Complete(); return content; } } /// /// Creates an object of a specified content type, under a parent. /// /// This method returns a new, persisted, IContent with an identity. /// The name of the content object. /// The parent content object. /// The alias of the content type. /// The optional id of the user creating the content. /// The content object. public IContent CreateContentWithIdentity(string name, IContent parent, string contentTypeAlias, int userId = 0) { if (parent == null) throw new ArgumentNullException(nameof(parent)); using (var uow = UowProvider.CreateUnitOfWork()) { // locking the content tree secures content types too uow.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); CreateContent(uow, content, parent, userId, true); uow.Complete(); return content; } } private void CreateContent(IDatabaseUnitOfWork uow, Content content, IContent parent, int userId, bool withIdentity) { // NOTE: I really hate the notion of these Creating/Created events - they are so inconsistent, I've only just found // out that in these 'WithIdentity' methods, the Saving/Saved events were not fired, wtf. Anyways, they're added now. var newArgs = parent != null ? new NewEventArgs(content, content.ContentType.Alias, parent) : new NewEventArgs(content, content.ContentType.Alias, -1); if (Creating.IsRaisedEventCancelled(newArgs, this)) { content.WasCancelled = true; return; } content.CreatorId = userId; content.WriterId = userId; if (withIdentity) { if (Saving.IsRaisedEventCancelled(new SaveEventArgs(content), this)) { content.WasCancelled = true; return; } var repo = uow.CreateRepository(); repo.AddOrUpdate(content); repo.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); Saved.RaiseEvent(new SaveEventArgs(content, false), this); } Created.RaiseEvent(new NewEventArgs(content, false, content.ContentType.Alias, parent), this); var msg = withIdentity ? "Content '{0}' was created with Id {1}" : "Content '{0}' was created"; Audit(AuditType.New, string.Format(msg, content.Name, content.Id), content.CreatorId, content.Id); } #endregion #region Get, Has, Is /// /// Gets an object by Id /// /// Id of the Content to retrieve /// public IContent GetById(int id) { using (var uow = UowProvider.CreateUnitOfWork()) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var content = repository.Get(id); uow.Complete(); return content; } } /// /// Gets an object by Id /// /// Ids of the Content to retrieve /// public IEnumerable GetByIds(IEnumerable ids) { var idsA = ids.ToArray(); if (idsA.Length == 0) return Enumerable.Empty(); using (var uow = UowProvider.CreateUnitOfWork()) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var content = repository.GetAll(idsA); uow.Complete(); return content; } } /// /// Gets an object by its 'UniqueId' /// /// Guid key of the Content to retrieve /// public IContent GetById(Guid key) { using (var uow = UowProvider.CreateUnitOfWork()) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var query = repository.Query.Where(x => x.Key == key); var contents = repository.GetByQuery(query); var content = contents.SingleOrDefault(); uow.Complete(); return content; } } /// /// Gets a collection of objects by the Id of the /// /// Id of the /// An Enumerable list of objects public IEnumerable GetContentOfContentType(int id) { using (var uow = UowProvider.CreateUnitOfWork()) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var query = repository.Query.Where(x => x.ContentTypeId == id); var content = repository.GetByQuery(query); uow.Complete(); return content; } } internal IEnumerable GetPublishedContentOfContentType(int id) { using (var uow = UowProvider.CreateUnitOfWork()) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var query = repository.Query.Where(x => x.ContentTypeId == id); var content = repository.GetByPublishedVersion(query); uow.Complete(); return content; } } /// /// Gets a collection of objects by Level /// /// The level to retrieve Content from /// An Enumerable list of objects /// Contrary to most methods, this method filters out trashed content items. public IEnumerable GetByLevel(int level) { using (var uow = UowProvider.CreateUnitOfWork()) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var query = repository.Query.Where(x => x.Level == level && x.Trashed == false); var content = repository.GetByQuery(query); uow.Complete(); return content; } } /// /// Gets a specific version of an item. /// /// Id of the version to retrieve /// An item public IContent GetByVersion(Guid versionId) { using (var uow = UowProvider.CreateUnitOfWork()) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var content = repository.GetByVersion(versionId); uow.Complete(); return content; } } /// /// Gets a collection of an objects versions by Id /// /// /// An Enumerable list of objects public IEnumerable GetVersions(int id) { using (var uow = UowProvider.CreateUnitOfWork()) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var content = repository.GetAllVersions(id); uow.Complete(); return content; } } /// /// Gets a collection of objects, which are ancestors of the current content. /// /// Id of the to retrieve ancestors for /// An Enumerable list of objects public IEnumerable GetAncestors(int id) { // intentionnaly not locking var content = GetById(id); return GetAncestors(content); } /// /// Gets a collection of objects, which are ancestors of the current content. /// /// to retrieve ancestors for /// An Enumerable list of objects public IEnumerable GetAncestors(IContent content) { //null check otherwise we get exceptions if (content.Path.IsNullOrWhiteSpace()) return Enumerable.Empty(); var rootId = Constants.System.Root.ToInvariantString(); 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(); using (var uow = UowProvider.CreateUnitOfWork()) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var ancestors = repository.GetAll(ids); uow.Complete(); return ancestors; } } /// /// Gets a collection of objects by Parent Id /// /// Id of the Parent to retrieve Children from /// An Enumerable list of objects public IEnumerable GetChildren(int id) { using (var uow = UowProvider.CreateUnitOfWork()) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var query = repository.Query.Where(x => x.ParentId == id); var children = repository.GetByQuery(query).OrderBy(x => x.SortOrder); uow.Complete(); return children; } } /// /// Gets a collection of published objects by Parent Id /// /// Id of the Parent to retrieve Children from /// An Enumerable list of published objects public IEnumerable GetPublishedChildren(int id) { using (var uow = UowProvider.CreateUnitOfWork()) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var query = repository.Query.Where(x => x.ParentId == id && x.Published); var children = repository.GetByQuery(query).OrderBy(x => x.SortOrder); uow.Complete(); return children; } } /// /// Gets a collection of objects by Parent Id /// /// Id of the Parent to retrieve Children from /// Page index (zero based) /// Page size /// Total records query would return without paging /// Field to order by /// Direction to order by /// Search text filter /// An Enumerable list of objects public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy, Direction orderDirection, string filter = "") { return GetPagedChildren(id, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, true, filter); } /// /// Gets a collection of objects by Parent Id /// /// Id of the Parent to retrieve Children from /// Page index (zero based) /// Page size /// Total records query would return without paging /// Field to order by /// Direction to order by /// Flag to indicate when ordering by system field /// Search text filter /// An Enumerable list of objects public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy, Direction orderDirection, bool orderBySystemField, string filter) { Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); Mandate.ParameterCondition(pageSize > 0, "pageSize"); using (var uow = UowProvider.CreateUnitOfWork()) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var query = repository.Query; //if the id is System Root, then just get all if (id != Constants.System.Root) query.Where(x => x.ParentId == id); var children = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); uow.Complete(); return children; } } /// /// Gets a collection of objects by Parent Id /// /// Id of the Parent to retrieve Descendants from /// Page number /// Page size /// Total records query would return without paging /// Field to order by /// Direction to order by /// Search text filter /// An Enumerable list of objects public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = "") { return GetPagedDescendants(id, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, true, filter); } /// /// Gets a collection of objects by Parent Id /// /// Id of the Parent to retrieve Descendants from /// Page number /// Page size /// Total records query would return without paging /// Field to order by /// Direction to order by /// Flag to indicate when ordering by system field /// Search text filter /// An Enumerable list of objects public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy, Direction orderDirection, bool orderBySystemField, string filter) { Mandate.ParameterCondition(pageIndex >= 0, nameof(pageIndex)); Mandate.ParameterCondition(pageSize > 0, nameof(pageSize)); using (var uow = UowProvider.CreateUnitOfWork()) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var query = repository.Query; //if the id is System Root, then just get all if (id != Constants.System.Root) query.Where(x => x.Path.SqlContains($",{id},", TextColumnType.NVarchar)); var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); uow.Complete(); return contents; } } /// /// Gets a collection of objects by its name or partial name /// /// Id of the Parent to retrieve Children from /// Full or partial name of the children /// An Enumerable list of objects public IEnumerable GetChildrenByName(int parentId, string name) { using (var uow = UowProvider.CreateUnitOfWork()) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var query = repository.Query.Where(x => x.ParentId == parentId && x.Name.Contains(name)); var children = repository.GetByQuery(query); uow.Complete(); return children; } } /// /// Gets a collection of objects by Parent Id /// /// Id of the Parent to retrieve Descendants from /// An Enumerable list of objects public IEnumerable GetDescendants(int id) { using (var uow = UowProvider.CreateUnitOfWork()) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var content = GetById(id); if (content == null) { uow.Complete(); // else causes rollback return Enumerable.Empty(); } var pathMatch = content.Path + ","; var query = repository.Query.Where(x => x.Id != content.Id && x.Path.StartsWith(pathMatch)); var descendants = repository.GetByQuery(query); uow.Complete(); return descendants; } } /// /// Gets a collection of objects by Parent Id /// /// item to retrieve Descendants from /// An Enumerable list of objects public IEnumerable GetDescendants(IContent content) { using (var uow = UowProvider.CreateUnitOfWork()) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var pathMatch = content.Path + ","; var query = repository.Query.Where(x => x.Id != content.Id && x.Path.StartsWith(pathMatch)); var descendants = repository.GetByQuery(query); uow.Complete(); return descendants; } } /// /// Gets the parent of the current content as an item. /// /// Id of the to retrieve the parent from /// Parent object public IContent GetParent(int id) { // intentionnaly not locking var content = GetById(id); return GetParent(content); } /// /// Gets the parent of the current content as an item. /// /// to retrieve the parent from /// Parent object public IContent GetParent(IContent content) { if (content.ParentId == Constants.System.Root || content.ParentId == Constants.System.RecycleBinContent) return null; return GetById(content.ParentId); } /// /// Gets the published version of an item /// /// Id of the to retrieve version from /// An item public IContent GetPublishedVersion(int id) { var version = GetVersions(id); return version.FirstOrDefault(x => x.Published); } /// /// Gets the published version of a item. /// /// The content item. /// The published version, if any; otherwise, null. public IContent GetPublishedVersion(IContent content) { if (content.Published) return content; return content.HasPublishedVersion ? GetByVersion(content.PublishedVersionGuid) : null; } /// /// Gets a collection of objects, which reside at the first level / root /// /// An Enumerable list of objects public IEnumerable GetRootContent() { using (var uow = UowProvider.CreateUnitOfWork()) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var query = repository.Query.Where(x => x.ParentId == Constants.System.Root); var content = repository.GetByQuery(query); uow.Complete(); return content; } } /// /// Gets all published content items /// /// internal IEnumerable GetAllPublished() { using (var uow = UowProvider.CreateUnitOfWork()) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var query = repository.Query.Where(x => x.Trashed == false); var content = repository.GetByPublishedVersion(query); uow.Complete(); return content; } } /// /// Gets a collection of objects, which has an expiration date less than or equal to today. /// /// An Enumerable list of objects public IEnumerable GetContentForExpiration() { using (var uow = UowProvider.CreateUnitOfWork()) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var query = repository.Query.Where(x => x.Published && x.ExpireDate <= DateTime.Now); var content = repository.GetByQuery(query); uow.Complete(); return content; } } /// /// Gets a collection of objects, which has a release date less than or equal to today. /// /// An Enumerable list of objects public IEnumerable GetContentForRelease() { using (var uow = UowProvider.CreateUnitOfWork()) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var query = repository.Query.Where(x => x.Published == false && x.ReleaseDate <= DateTime.Now); var content = repository.GetByQuery(query); uow.Complete(); return content; } } /// /// Gets a collection of an objects, which resides in the Recycle Bin /// /// An Enumerable list of objects public IEnumerable GetContentInRecycleBin() { using (var uow = UowProvider.CreateUnitOfWork()) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var query = repository.Query.Where(x => x.Path.Contains(Constants.System.RecycleBinContent.ToInvariantString())); var content = repository.GetByQuery(query); uow.Complete(); return content; } } /// /// Checks whether an item has any children /// /// Id of the /// True if the content has any children otherwise False public bool HasChildren(int id) { return CountChildren(id) > 0; } /// /// Checks whether an item has any published versions /// /// Id of the /// True if the content has any published version otherwise False public bool HasPublishedVersion(int id) { using (var uow = UowProvider.CreateUnitOfWork()) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var query = repository.Query.Where(x => x.Published && x.Id == id && x.Trashed == false); var count = repository.Count(query); uow.Complete(); return count > 0; } } /// /// Checks if the passed in can be published based on the anscestors publish state. /// /// to check if anscestors are published /// True if the Content can be published, otherwise False public bool IsPublishable(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 using (var uow = UowProvider.CreateUnitOfWork()) { uow.ReadLock(Constants.Locks.ContentTree); var repo = uow.CreateRepository(); var parent = repo.Get(content.ParentId); if (parent == null) throw new Exception("Out of sync."); // causes rollback var isPublishable = repo.IsPathPublished(parent); uow.Complete(); return isPublishable; } } public bool IsPathPublished(IContent content) { using (var uow = UowProvider.CreateUnitOfWork()) { uow.ReadLock(Constants.Locks.ContentTree); var repo = uow.CreateRepository(); var isPathPublished = repo.IsPathPublished(content); uow.Complete(); return isPathPublished; } } #endregion #region Save, Publish, Unpublish /// /// This will rebuild the xml structures for content in the database. /// /// This is not used for anything /// True if publishing succeeded, otherwise False /// /// This is used for when a document type alias or a document type property is changed, the xml will need to /// be regenerated. /// public bool RePublishAll(int userId = 0) { try { RebuildXmlStructures(); return true; } catch (Exception ex) { Logger.Error("An error occurred executing RePublishAll", ex); return false; } } /// /// This will rebuild the xml structures for content in the database. /// /// /// If specified will only rebuild the xml for the content type's specified, otherwise will update the structure /// for all published content. /// internal void RePublishAll(params int[] contentTypeIds) { try { RebuildXmlStructures(contentTypeIds); } catch (Exception ex) { Logger.Error("An error occurred executing RePublishAll", ex); } } /// /// Saves a single object /// /// The to save /// Optional Id of the User saving the Content /// Optional boolean indicating whether or not to raise events. public void Save(IContent content, int userId = 0, bool raiseEvents = true) { ((IContentServiceOperations) this).Save(content, userId, raiseEvents); } /// /// Saves a single object /// /// The to save /// Optional Id of the User saving the Content /// Optional boolean indicating whether or not to raise events. Attempt IContentServiceOperations.Save(IContent content, int userId, bool raiseEvents) { var evtMsgs = EventMessagesFactory.Get(); if (raiseEvents && Saving.IsRaisedEventCancelled(new SaveEventArgs(content, evtMsgs), this)) return OperationStatus.Attempt.Cancel(evtMsgs); using (var uow = UowProvider.CreateUnitOfWork()) { uow.WriteLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); if (content.HasIdentity == false) content.CreatorId = userId; content.WriterId = userId; // saving the Published version => indicate we are .Saving // saving the Unpublished version => remains .Unpublished if (content.Published) content.ChangePublishedState(PublishedState.Saving); repository.AddOrUpdate(content); repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); uow.Complete(); } if (raiseEvents) Saved.RaiseEvent(new SaveEventArgs(content, false, evtMsgs), this); Audit(AuditType.Save, "Save Content performed by user", userId, content.Id); return OperationStatus.Attempt.Succeed(evtMsgs); } /// /// Saves a collection of objects. /// /// /// If the collection of content contains new objects that references eachother by Id or ParentId, /// then use the overload Save method with a collection of Lazy . /// /// Collection of to save /// Optional Id of the User saving the Content /// Optional boolean indicating whether or not to raise events. public void Save(IEnumerable contents, int userId = 0, bool raiseEvents = true) { ((IContentServiceOperations) this).Save(contents, userId, raiseEvents); } /// /// Saves a collection of objects. /// /// Collection of to save /// Optional Id of the User saving the Content /// Optional boolean indicating whether or not to raise events. Attempt IContentServiceOperations.Save(IEnumerable contents, int userId, bool raiseEvents) { var evtMsgs = EventMessagesFactory.Get(); var contentsA = contents.ToArray(); if (raiseEvents && Saving.IsRaisedEventCancelled(new SaveEventArgs(contentsA, evtMsgs), this)) return OperationStatus.Attempt.Cancel(evtMsgs); using (var uow = UowProvider.CreateUnitOfWork()) { uow.WriteLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); foreach (var content in contentsA) { if (content.HasIdentity == false) content.CreatorId = userId; content.WriterId = userId; // saving the Published version => indicate we are .Saving // saving the Unpublished version => remains .Unpublished if (content.Published) content.ChangePublishedState(PublishedState.Saving); repository.AddOrUpdate(content); repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); } uow.Complete(); } if (raiseEvents) Saved.RaiseEvent(new SaveEventArgs(contentsA, false, evtMsgs), this); Audit(AuditType.Save, "Bulk Save content performed by user", userId == -1 ? 0 : userId, Constants.System.Root); return OperationStatus.Attempt.Succeed(evtMsgs); } /// /// Saves and Publishes a single object /// /// The to save and publish /// Optional Id of the User issueing the publishing /// Optional boolean indicating whether or not to raise save events. /// True if publishing succeeded, otherwise False [Obsolete("Use SaveAndPublishWithStatus instead, that method will provide more detailed information on the outcome")] public bool SaveAndPublish(IContent content, int userId = 0, bool raiseEvents = true) { var result = SaveAndPublishDo(content, userId, raiseEvents); return result.Success; } /// /// Saves and Publishes a single object /// /// The to save and publish /// Optional Id of the User issueing the publishing /// Optional boolean indicating whether or not to raise save events. /// True if publishing succeeded, otherwise False Attempt IContentServiceOperations.SaveAndPublish(IContent content, int userId, bool raiseEvents) { return SaveAndPublishDo(content, userId, raiseEvents); } /// /// Publishes a single object /// /// The to publish /// Optional Id of the User issueing the publishing /// True if publishing succeeded, otherwise False public bool Publish(IContent content, int userId = 0) { var result = SaveAndPublishDo(content, userId); Logger.Info("Call was made to ContentService.Publish, use PublishWithStatus instead since that method will provide more detailed information on the outcome"); return result.Success; } /// /// Publishes a single object /// /// The to publish /// Optional Id of the User issueing the publishing /// The published status attempt Attempt IContentServiceOperations.Publish(IContent content, int userId) { return SaveAndPublishDo(content, userId); } /// /// UnPublishes a single object /// /// The to publish /// Optional Id of the User issueing the publishing /// True if unpublishing succeeded, otherwise False public bool UnPublish(IContent content, int userId = 0) { return ((IContentServiceOperations)this).UnPublish(content, userId).Success; } /// /// UnPublishes a single object /// /// The to publish /// Optional Id of the User issueing the publishing /// True if unpublishing succeeded, otherwise False Attempt IContentServiceOperations.UnPublish(IContent content, int userId) { return UnPublishDo(content, false, userId); } /// /// Saves and Publishes a single object /// /// The to save and publish /// Optional Id of the User issueing the publishing /// Optional boolean indicating whether or not to raise save events. /// True if publishing succeeded, otherwise False public Attempt SaveAndPublishWithStatus(IContent content, int userId = 0, bool raiseEvents = true) { return ((IContentServiceOperations)this).SaveAndPublish(content, userId, raiseEvents); } /// /// Publishes a single object /// /// The to publish /// Optional Id of the User issueing the publishing /// True if publishing succeeded, otherwise False public Attempt PublishWithStatus(IContent content, int userId = 0) { return ((IContentServiceOperations) this).Publish(content, userId); } /// /// Publishes a object and all its children /// /// The to publish along with its children /// Optional Id of the User issueing the publishing /// True if publishing succeeded, otherwise False [Obsolete("Use PublishWithChildrenWithStatus instead, that method will provide more detailed information on the outcome and also allows the includeUnpublished flag")] public bool PublishWithChildren(IContent content, int userId = 0) { // this used to just return false only when the parent content failed, otherwise would // always return true so we'll do the same thing for the moment var result = PublishWithChildrenDo(content, userId, true); // FirstOrDefault() is a pain to use with structs and result contain Attempt structs // so use this code, which is fast and works - and please ReSharper do NOT suggest otherwise // ReSharper disable once LoopCanBeConvertedToQuery foreach (var r in result) if (r.Result.ContentItem.Id == content.Id) return r.Success; return false; } /// /// Publishes a object and all its children /// /// The to publish along with its children /// Optional Id of the User issueing the publishing /// set to true if you want to also publish children that are currently unpublished /// True if publishing succeeded, otherwise False public IEnumerable> PublishWithChildrenWithStatus(IContent content, int userId = 0, bool includeUnpublished = false) { return ((IContentServiceOperations)this).PublishWithChildren(content, userId, includeUnpublished); } /// /// Used to perform scheduled publishing/unpublishing /// public IEnumerable> PerformScheduledPublish() { //TODO: Do I need to move all of this logic to the repo? Or wrap this all in a unit of work? foreach (var d in GetContentForRelease()) { d.ReleaseDate = null; var result = SaveAndPublishWithStatus(d, (int)d.GetWriterProfile(_userService).Id); if (result.Success == false) { if (result.Exception != null) { Logger.Error("Could not published the document (" + d.Id + ") based on it's scheduled release, status result: " + result.Result.StatusType, result.Exception); } else { Logger.Warn("Could not published the document (" + d.Id + ") based on it's scheduled release. Status result: " + result.Result.StatusType); } } yield return result; } foreach (var d in GetContentForExpiration()) { try { d.ExpireDate = null; UnPublish(d, (int)d.GetWriterProfile(_userService).Id); } catch (Exception ee) { Logger.Error($"Error unpublishing node {d.Id}", ee); throw; } } } /// /// Publishes a object and all its children /// /// The to publish along with its children /// Optional Id of the User issueing the publishing /// /// The list of statuses for all published items IEnumerable> IContentServiceOperations.PublishWithChildren(IContent content, int userId, bool includeUnpublished) { return PublishWithChildrenDo(content, userId, includeUnpublished); } #endregion #region Delete /// /// Permanently deletes an object as well as all of its Children. /// /// /// This method will also delete associated media files, child content and possibly associated domains. /// /// Please note that this method will completely remove the Content from the database /// The to delete /// Optional Id of the User deleting the Content public void Delete(IContent content, int userId = 0) { ((IContentServiceOperations)this).Delete(content, userId); } /// /// Permanently deletes an object. /// /// /// This method will also delete associated media files, child content and possibly associated domains. /// /// Please note that this method will completely remove the Content from the database /// The to delete /// Optional Id of the User deleting the Content Attempt IContentServiceOperations.Delete(IContent content, int userId) { var evtMsgs = EventMessagesFactory.Get(); if (Deleting.IsRaisedEventCancelled(new DeleteEventArgs(content, evtMsgs), this)) return OperationStatus.Attempt.Cancel(evtMsgs); using (var uow = UowProvider.CreateUnitOfWork()) { uow.WriteLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); // if it's not trashed yet, and published, we should unpublish // but... UnPublishing event makes no sense (not going to cancel?) and no need to save // just raise the event if (content.Trashed == false && content.HasPublishedVersion) UnPublished.RaiseEvent(new PublishEventArgs(content, false, false), this); DeleteLocked(repository, content); uow.Complete(); } Audit(AuditType.Delete, "Delete Content performed by user", userId, content.Id); return OperationStatus.Attempt.Succeed(evtMsgs); } private void DeleteLocked(IContentRepository repository, IContent content) { // then recursively delete descendants, bottom-up // just repository.Delete + an event var stack = new Stack(); stack.Push(content); var level = 1; while (stack.Count > 0) { var c = stack.Peek(); IContent[] cc; if (c.Level == level) while ((cc = c.Children().ToArray()).Length > 0) { foreach (var ci in cc) stack.Push(ci); c = cc[cc.Length - 1]; } c = stack.Pop(); level = c.Level; repository.Delete(c); var args = new DeleteEventArgs(c, false); // raise event & get flagged files Deleted.RaiseEvent(args, this); IOHelper.DeleteFiles(args.MediaFilesToDelete, // remove flagged files (file, e) => Logger.Error("An error occurred while deleting file attached to nodes: " + file, e)); } } //TODO: // both DeleteVersions methods below have an issue. Sort of. They do NOT take care of files the way // 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. /// /// Permanently deletes versions from an object prior to a specific date. /// This method will never delete the latest version of a content item. /// /// Id of the object to delete versions from /// Latest version date /// Optional Id of the User deleting versions of a Content object public void DeleteVersions(int id, DateTime versionDate, int userId = 0) { if (DeletingVersions.IsRaisedEventCancelled(new DeleteRevisionsEventArgs(id, dateToRetain: versionDate), this)) return; using (var uow = UowProvider.CreateUnitOfWork()) { uow.WriteLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); repository.DeleteVersions(id, versionDate); uow.Complete(); } DeletedVersions.RaiseEvent(new DeleteRevisionsEventArgs(id, false, dateToRetain: versionDate), this); Audit(AuditType.Delete, "Delete Content by version date performed by user", userId, Constants.System.Root); } /// /// Permanently deletes specific version(s) from an object. /// This method will never delete the latest version of a content item. /// /// Id of the object to delete a version from /// Id of the version to delete /// Boolean indicating whether to delete versions prior to the versionId /// Optional Id of the User deleting versions of a Content object public void DeleteVersion(int id, Guid versionId, bool deletePriorVersions, int userId = 0) { if (DeletingVersions.IsRaisedEventCancelled(new DeleteRevisionsEventArgs(id, /*specificVersion:*/ versionId), this)) return; if (deletePriorVersions) { var content = GetByVersion(versionId); DeleteVersions(id, content.UpdateDate, userId); } using (var uow = UowProvider.CreateUnitOfWork()) { uow.WriteLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); repository.DeleteVersion(versionId); uow.Complete(); } DeletedVersions.RaiseEvent(new DeleteRevisionsEventArgs(id, false,/* specificVersion:*/ versionId), this); Audit(AuditType.Delete, "Delete Content by version performed by user", userId, Constants.System.Root); } #endregion #region Move, RecycleBin /// /// Deletes an object by moving it to the Recycle Bin /// /// Move an item to the Recycle Bin will result in the item being unpublished /// The to delete /// Optional Id of the User deleting the Content public void MoveToRecycleBin(IContent content, int userId = 0) { ((IContentServiceOperations)this).MoveToRecycleBin(content, userId); } /// /// Deletes an object by moving it to the Recycle Bin /// /// Move an item to the Recycle Bin will result in the item being unpublished /// The to delete /// Optional Id of the User deleting the Content Attempt IContentServiceOperations.MoveToRecycleBin(IContent content, int userId) { var evtMsgs = EventMessagesFactory.Get(); var moves = new List>(); using (var uow = UowProvider.CreateUnitOfWork()) { uow.WriteLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var originalPath = content.Path; if (Trashing.IsRaisedEventCancelled(new MoveEventArgs(new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent)), this)) return OperationStatus.Attempt.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(repository, content, Constants.System.RecycleBinContent, null, userId, moves, true); uow.Complete(); } var moveInfo = moves .Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId)) .ToArray(); Trashed.RaiseEvent(new MoveEventArgs(false, evtMsgs, moveInfo), this); Audit(AuditType.Move, "Move Content to Recycle Bin performed by user", userId, content.Id); return OperationStatus.Attempt.Succeed(evtMsgs); } /// /// Moves an object to a new location by changing its parent id. /// /// /// If the 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. /// /// The to move /// Id of the Content's new Parent /// Optional Id of the User moving the Content public void Move(IContent content, int parentId, int userId = 0) { // if moving to the recycle bin then use the proper method if (parentId == Constants.System.RecycleBinContent) { MoveToRecycleBin(content, userId); return; } var moves = new List>(); using (var uow = UowProvider.CreateUnitOfWork()) { uow.WriteLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); 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 if (Moving.IsRaisedEventCancelled(new MoveEventArgs(new MoveEventInfo(content, content.Path, parentId)), this)) 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.HasPublishedVersion) { // however, it had been masked when being trashed, so there's no need for // any special event here - just change its state content.ChangePublishedState(PublishedState.Unpublishing); } PerformMoveLocked(repository, content, parentId, parent, userId, moves, trashed); uow.Complete(); } var moveInfo = moves //changes .Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId)) .ToArray(); Moved.RaiseEvent(new MoveEventArgs(false, moveInfo), this); Audit(AuditType.Move, "Move Content performed by user", userId, content.Id); } // MUST be called from within WriteLock // trash indicates whether we are trashing, un-trashing, or not changing anything private void PerformMoveLocked(IContentRepository repository, IContent content, int parentId, IContent parent, int userId, ICollection> 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(); moves.Add(Tuple.Create(content, content.Path)); // capture original path // 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(repository, content, userId, trash); // BUT 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; var descendants = GetDescendants(content); foreach (var descendant in descendants) { moves.Add(Tuple.Create(descendant, descendant.Path)); // capture original path // update path and level since we do not update parentId descendant.Path = paths[descendant.Id] = paths[descendant.ParentId] + "," + descendant.Id; descendant.Level += levelDelta; PerformMoveContentLocked(repository, descendant, userId, trash); } } private static void PerformMoveContentLocked(IContentRepository repository, IContent content, int userId, bool? trash) { if (trash.HasValue) ((ContentBase) content).Trashed = trash.Value; content.WriterId = userId; repository.AddOrUpdate(content); } /// /// Empties the Recycle Bin by deleting all that resides in the bin /// public void EmptyRecycleBin() { var nodeObjectType = new Guid(Constants.ObjectTypes.Document); var deleted = new List(); var evtMsgs = EventMessagesFactory.Get(); // todo - and then? using (var uow = UowProvider.CreateUnitOfWork()) { uow.WriteLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); // 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 if (EmptyingRecycleBin.IsRaisedEventCancelled(new RecycleBinEventArgs(nodeObjectType), this)) return; // causes rollback // emptying the recycle bin means deleting whetever is in there - do it properly! var query = repository.Query.Where(x => x.ParentId == Constants.System.RecycleBinContent); var contents = repository.GetByQuery(query).ToArray(); foreach (var content in contents) { DeleteLocked(repository, content); deleted.Add(content); } EmptiedRecycleBin.RaiseEvent(new RecycleBinEventArgs(nodeObjectType, true), this); uow.Complete(); } Audit(AuditType.Delete, "Empty Content Recycle Bin performed by user", 0, Constants.System.RecycleBinContent); } #endregion #region Others /// /// Copies an 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. /// /// The to copy /// Id of the Content's new Parent /// Boolean indicating whether the copy should be related to the original /// Optional Id of the User copying the Content /// The newly created object public IContent Copy(IContent content, int parentId, bool relateToOriginal, int userId = 0) { return Copy(content, parentId, relateToOriginal, true, userId); } /// /// Copies an 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. /// /// The to copy /// Id of the Content's new Parent /// Boolean indicating whether the copy should be related to the original /// A value indicating whether to recursively copy children. /// Optional Id of the User copying the Content /// The newly created object public IContent Copy(IContent content, int parentId, bool relateToOriginal, bool recursive, int userId = 0) { var copy = content.DeepCloneWithResetIdentities(); copy.ParentId = parentId; if (Copying.IsRaisedEventCancelled(new CopyEventArgs(content, copy, parentId), this)) return null; // fixme - relateToOriginal is ignored?! using (var uow = UowProvider.CreateUnitOfWork()) { uow.WriteLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); // a copy is .Saving and will be .Unpublished if (copy.Published) copy.ChangePublishedState(PublishedState.Saving); // update the create author and last edit author copy.CreatorId = userId; copy.WriterId = userId; // save repository.AddOrUpdate(copy); repository.AddOrUpdatePreviewXml(copy, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); uow.Flush(); // ensure copy has an ID - fixme why? if (recursive) { // process descendants var copyIds = new Dictionary(); copyIds[content.Id] = copy; foreach (var descendant in GetDescendants(content)) { var dcopy = descendant.DeepCloneWithResetIdentities(); //dcopy.ParentId = copyIds[descendant.ParentId]; var descendantParentId = descendant.ParentId; ((Content) dcopy).SetLazyParentId(new Lazy(() => copyIds[descendantParentId].Id)); if (dcopy.Published) dcopy.ChangePublishedState(PublishedState.Saving); dcopy.CreatorId = userId; dcopy.WriterId = userId; repository.AddOrUpdate(dcopy); repository.AddOrUpdatePreviewXml(dcopy, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); copyIds[descendant.Id] = dcopy; } } // fixme tag & tree issue // tags code handling has been removed here // - tags should be handled by the content repository // - a copy is unpublished and therefore has no impact on tags in DB uow.Complete(); } Copied.RaiseEvent(new CopyEventArgs(content, copy, false, parentId, relateToOriginal), this); Audit(AuditType.Copy, "Copy Content performed by user", content.WriterId, content.Id); return copy; } /// /// Sends an to Publication, which executes handlers and events for the 'Send to Publication' action. /// /// The to send to publication /// Optional Id of the User issueing the send to publication /// True if sending publication was succesfull otherwise false public bool SendToPublication(IContent content, int userId = 0) { if (SendingToPublish.IsRaisedEventCancelled(new SendToPublishEventArgs(content), this)) return false; //Save before raising event Save(content, userId); SentToPublish.RaiseEvent(new SendToPublishEventArgs(content, false), this); Audit(AuditType.SendToPublish, "Send to Publish performed by user", content.WriterId, content.Id); return true; } /// /// Rollback an object to a previous version. /// This will create a new version, which is a copy of all the old data. /// /// /// The way data is stored actually only allows us to rollback on properties /// and not data like Name and Alias of the Content. /// /// Id of the being rolled back /// Id of the version to rollback to /// Optional Id of the User issueing the rollback of the Content /// The newly created object public IContent Rollback(int id, Guid versionId, int userId = 0) { var content = GetByVersion(versionId); if (RollingBack.IsRaisedEventCancelled(new RollbackEventArgs(content), this)) return content; content.CreatorId = userId; // need to make sure that the repository is going to save a new version // but if we're not changing anything, the repository would not save anything // so - make sure the property IS dirty, doing a flip-flop with an impossible value content.WriterId = -1; content.WriterId = userId; using (var uow = UowProvider.CreateUnitOfWork()) { uow.WriteLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); // a rolled back version is .Saving and will be .Unpublished content.ChangePublishedState(PublishedState.Saving); repository.AddOrUpdate(content); repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); uow.Complete(); } RolledBack.RaiseEvent(new RollbackEventArgs(content, false), this); Audit(AuditType.RollBack, "Content rollback performed by user", content.WriterId, content.Id); return content; } /// /// Sorts a collection of objects by updating the SortOrder according /// to the ordering of items in the passed in . /// /// /// Using this method will ensure that the Published-state is maintained upon sorting /// so the cache is updated accordingly - as needed. /// /// /// /// /// True if sorting succeeded, otherwise False public bool Sort(IEnumerable items, int userId = 0, bool raiseEvents = true) { var itemsA = items.ToArray(); if (itemsA.Length == 0) return true; if (raiseEvents && Saving.IsRaisedEventCancelled(new SaveEventArgs(itemsA), this)) return false; var published = new List(); var saved = new List(); using (var uow = UowProvider.CreateUnitOfWork()) { uow.WriteLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var sortOrder = 0; 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) { sortOrder++; continue; } // else update content.SortOrder = sortOrder++; content.WriterId = userId; // 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); else if (content.HasPublishedVersion) published.Add(GetByVersion(content.PublishedVersionGuid)); // save saved.Add(content); repository.AddOrUpdate(content); repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); } foreach (var content in published) repository.AddOrUpdateContentXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); uow.Complete(); } if (raiseEvents) Saved.RaiseEvent(new SaveEventArgs(saved, false), this); if (raiseEvents && published.Any()) Published.RaiseEvent(new PublishEventArgs(published, false, false), this); Audit(AuditType.Sort, "Sorting content performed by user", userId, 0); return true; } #endregion #region Internal Methods /// /// Gets a collection of descendants by the first Parent. /// /// item to retrieve Descendants from /// An Enumerable list of objects internal IEnumerable GetPublishedDescendants(IContent content) { using (var uow = UowProvider.CreateUnitOfWork()) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var descendants = GetPublishedDescendantsLocked(repository, content); uow.Complete(); return descendants; } } internal IEnumerable GetPublishedDescendantsLocked(IContentRepository repository, IContent content) { var pathMatch = content.Path + ","; var query = repository.Query.Where(x => x.Id != content.Id && x.Path.StartsWith(pathMatch) /*&& x.Trashed == false*/); var contents = repository.GetByPublishedVersion(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 { content.Id }; foreach (var c in contents) { if (parents.Contains(c.ParentId)) { yield return c; parents.Add(c.Id); } } } #endregion #region Private Methods private void Audit(AuditType type, string message, int userId, int objectId) { using (var uow = UowProvider.CreateUnitOfWork()) { var repo = uow.CreateRepository(); repo.AddOrUpdate(new AuditItem(objectId, message, type, userId)); uow.Complete(); } } /// /// Publishes a object and all its children /// /// The to publish along with its children /// Optional Id of the User issueing the publishing /// If set to true, this will also publish descendants that are completely unpublished, normally this will only publish children that have previously been published /// /// A list of publish statues. If the parent document is not valid or cannot be published because it's parent(s) is not published /// then the list will only contain one status item, otherwise it will contain status items for it and all of it's descendants that /// are to be published. /// private IEnumerable> PublishWithChildrenDo(IContent content, int userId = 0, bool includeUnpublished = false) { if (content == null) throw new ArgumentNullException(nameof(content)); var evtMsgs = EventMessagesFactory.Get(); var publishedItems = new List(); // this is for events Attempt[] attempts; using (var uow = UowProvider.CreateUnitOfWork()) { uow.WriteLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); // fail fast + use in alreadyChecked below to avoid duplicate checks var attempt = EnsurePublishable(content, evtMsgs); if (attempt.Success) attempt = StrategyCanPublish(content, userId, evtMsgs); if (attempt.Success == false) return new[] { attempt }; // causes rollback var contents = new List { content }; //include parent item contents.AddRange(GetDescendants(content)); // publish using the strategy - for descendants, // - published w/out changes: nothing to do // - published w/changes: publish those changes // - unpublished: publish if includeUnpublished, otherwise ignore var alreadyChecked = new[] { content }; attempts = StrategyPublishWithChildren(contents, alreadyChecked, userId, evtMsgs, includeUnpublished).ToArray(); foreach (var status in attempts.Where(x => x.Success).Select(x => x.Result)) { // save them all, even those that are .Success because of (.StatusType == PublishStatusType.SuccessAlreadyPublished) // so we bump the date etc var publishedItem = status.ContentItem; publishedItem.WriterId = userId; repository.AddOrUpdate(publishedItem); repository.AddOrUpdatePreviewXml(publishedItem, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); repository.AddOrUpdateContentXml(publishedItem, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); publishedItems.Add(publishedItem); } uow.Complete(); } Published.RaiseEvent(new PublishEventArgs(publishedItems, false, false), this); Audit(AuditType.Publish, "Publish with Children performed by user", userId, content.Id); return attempts; } /// /// UnPublishes a single object /// /// The to publish /// Optional boolean to avoid having the cache refreshed when calling this Unpublish method. By default this method will update the cache. /// Optional Id of the User issueing the publishing /// True if unpublishing succeeded, otherwise False private Attempt UnPublishDo(IContent content, bool omitCacheRefresh = false, int userId = 0) { // fixme kill omitCacheRefresh! var evtMsgs = EventMessagesFactory.Get(); using (var uow = UowProvider.CreateUnitOfWork()) { uow.WriteLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var newest = GetById(content.Id); // ensure we have the newest version if (content.Version != newest.Version) // but use the original object if it's already the newest version content = newest; if (content.Published == false && content.HasPublishedVersion == false) { uow.Complete(); return Attempt.Succeed(new UnPublishStatus(UnPublishedStatusType.SuccessAlreadyUnPublished, evtMsgs, content)); // already unpublished } // strategy var attempt = StrategyCanUnPublish(content, userId, evtMsgs); if (attempt == false) return attempt; // causes rollback attempt = StrategyUnPublish(content, true, userId, evtMsgs); if (attempt == false) return attempt; // causes rollback content.WriterId = userId; repository.AddOrUpdate(content); // fixme delete xml from database! was in _publishingStrategy.UnPublishingFinalized(content); repository.DeleteContentXml(content); uow.Complete(); } UnPublished.RaiseEvent(new PublishEventArgs(content, false, false), this); return Attempt.Succeed(new UnPublishStatus(UnPublishedStatusType.Success, evtMsgs, content)); } /// /// Saves and Publishes a single object /// /// The to save and publish /// Optional Id of the User issueing the publishing /// Optional boolean indicating whether or not to raise save events. /// True if publishing succeeded, otherwise False private Attempt SaveAndPublishDo(IContent content, int userId = 0, bool raiseEvents = true) { var evtMsgs = EventMessagesFactory.Get(); if (raiseEvents && Saving.IsRaisedEventCancelled(new SaveEventArgs(content), this)) return Attempt.Fail(new PublishStatus(PublishStatusType.FailedCancelledByEvent, evtMsgs, content)); var isNew = content.IsNewEntity(); var previouslyPublished = content.HasIdentity && content.HasPublishedVersion; var status = default(Attempt); using (var uow = UowProvider.CreateUnitOfWork()) { uow.WriteLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); // fixme - EnsurePublishable vs StrategyCanPublish? // EnsurePublishable ensures that path published is ok // StrategyCanPublish ensures other things including valid properties // should we merge or?! // ensure content is publishable, and try to publish status = EnsurePublishable(content, evtMsgs); if (status.Success) { // strategy handles events, and various business rules eg release & expire // dates, trashed status... status = StrategyPublish(content, false, userId, evtMsgs); } // save - always, even if not publishing (this is SaveAndPublish) if (content.HasIdentity == false) content.CreatorId = userId; content.WriterId = userId; repository.AddOrUpdate(content); repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); if (content.Published) repository.AddOrUpdateContentXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); uow.Complete(); } if (status.Success == false) { // fixme what about the saved event? return status; } Published.RaiseEvent(new PublishEventArgs(content, false, false), this); // if was not published and now is... descendants that were 'published' (but // had an unpublished ancestor) are 're-published' ie not explicitely published // but back as 'published' nevertheless if (isNew == false && previouslyPublished == false) { if (HasChildren(content.Id)) { var descendants = GetPublishedDescendants(content).ToArray(); Published.RaiseEvent(new PublishEventArgs(descendants, false, false), this); } } Audit(AuditType.Publish, "Save and Publish performed by user", userId, content.Id); return status; } private Attempt EnsurePublishable(IContent content, EventMessages evtMsgs) { // root content can be published var checkParents = content.ParentId == Constants.System.Root; // trashed content cannot be published if (checkParents == false && content.ParentId != Constants.System.RecycleBinContent) { // ensure all ancestors are published // because content may be new its Path may be null - start with parent var path = content.Path ?? content.Parent().Path; if (path != null) // if parent is also null, give up { var ancestorIds = path.Split(',') .Skip(1) // remove leading "-1" .Reverse() .Select(int.Parse); if (content.Path != null) ancestorIds = ancestorIds.Skip(1); // remove trailing content.Id if (ancestorIds.All(HasPublishedVersion)) checkParents = true; } } if (checkParents == false) { Logger.Info($"Content '{content.Name}' with Id '{content.Id}' could not be published because its parent is not published."); return Attempt.Fail(new PublishStatus(PublishStatusType.FailedPathNotPublished, evtMsgs, content)); } // fixme - should we do it - are we doing it for descendants too? if (content.IsValid() == false) { Logger.Info($"Content '{content.Name}' with Id '{content.Id}' could not be published because of invalid properties."); return Attempt.Fail(new PublishStatus(PublishStatusType.FailedContentInvalid, evtMsgs, content) { InvalidProperties = ((ContentBase)content).LastInvalidProperties }); } return Attempt.Succeed(new PublishStatus(PublishStatusType.Success, evtMsgs, content)); } #endregion #region Event Handlers /// /// Occurs before Delete /// public static event TypedEventHandler> Deleting; /// /// Occurs after Delete /// public static event TypedEventHandler> Deleted; /// /// Occurs before Delete Versions /// public static event TypedEventHandler DeletingVersions; /// /// Occurs after Delete Versions /// public static event TypedEventHandler DeletedVersions; /// /// Occurs before Save /// public static event TypedEventHandler> Saving; /// /// Occurs after Save /// public static event TypedEventHandler> Saved; /// /// Occurs before Create /// [Obsolete("Use the Created event instead, the Creating and Created events both offer the same functionality, Creating event has been deprecated.")] public static event TypedEventHandler> Creating; /// /// Occurs after Create /// /// /// Please note that the Content object has been created, but might not have been saved /// so it does not have an identity yet (meaning no Id has been set). /// public static event TypedEventHandler> Created; /// /// Occurs before Copy /// public static event TypedEventHandler> Copying; /// /// Occurs after Copy /// public static event TypedEventHandler> Copied; /// /// Occurs before Content is moved to Recycle Bin /// public static event TypedEventHandler> Trashing; /// /// Occurs after Content is moved to Recycle Bin /// public static event TypedEventHandler> Trashed; /// /// Occurs before Move /// public static event TypedEventHandler> Moving; /// /// Occurs after Move /// public static event TypedEventHandler> Moved; /// /// Occurs before Rollback /// public static event TypedEventHandler> RollingBack; /// /// Occurs after Rollback /// public static event TypedEventHandler> RolledBack; /// /// Occurs before Send to Publish /// public static event TypedEventHandler> SendingToPublish; /// /// Occurs after Send to Publish /// public static event TypedEventHandler> SentToPublish; /// /// Occurs before the Recycle Bin is emptied /// public static event TypedEventHandler EmptyingRecycleBin; /// /// Occurs after the Recycle Bin has been Emptied /// public static event TypedEventHandler EmptiedRecycleBin; /// /// Occurs before publish /// public static event TypedEventHandler> Publishing; /// /// Occurs after publish /// public static event TypedEventHandler> Published; /// /// Occurs before unpublish /// public static event TypedEventHandler> UnPublishing; /// /// Occurs after unpublish /// public static event TypedEventHandler> UnPublished; #endregion #region Publishing Strategies // prob. want to find nicer names? internal Attempt StrategyCanPublish(IContent content, int userId, EventMessages evtMsgs) { if (Publishing.IsRaisedEventCancelled(new PublishEventArgs(content, evtMsgs), this)) { Logger.Info($"Content '{content.Name}' with Id '{content.Id}' will not be published, the event was cancelled."); return Attempt.Fail(new PublishStatus(PublishStatusType.FailedCancelledByEvent, evtMsgs, content)); } // check if the content is valid if (content.IsValid() == false) { Logger.Info($"Content '{content.Name}' with Id '{content.Id}' could not be published because of invalid properties."); return Attempt.Fail(new PublishStatus(PublishStatusType.FailedContentInvalid, evtMsgs, content) { InvalidProperties = ((ContentBase)content).LastInvalidProperties }); } // check if the Content is Expired if (content.Status == ContentStatus.Expired) { Logger.Info($"Content '{content.Name}' with Id '{content.Id}' has expired and could not be published."); return Attempt.Fail(new PublishStatus(PublishStatusType.FailedHasExpired, evtMsgs, content)); } // check if the Content is Awaiting Release if (content.Status == ContentStatus.AwaitingRelease) { Logger.Info($"Content '{content.Name}' with Id '{content.Id}' is awaiting release and could not be published."); return Attempt.Fail(new PublishStatus(PublishStatusType.FailedAwaitingRelease, evtMsgs, content)); } // check if the Content is Trashed if (content.Status == ContentStatus.Trashed) { Logger.Info($"Content '{content.Name}' with Id '{content.Id}' is trashed and could not be published."); return Attempt.Fail(new PublishStatus(PublishStatusType.FailedIsTrashed, evtMsgs, content)); } return Attempt.Succeed(new PublishStatus(content, evtMsgs)); } internal Attempt StrategyPublish(IContent content, bool alreadyCheckedCanPublish, int userId, EventMessages evtMsgs) { var attempt = alreadyCheckedCanPublish ? Attempt.Succeed(new PublishStatus(content, evtMsgs)) // already know we can : StrategyCanPublish(content, userId, evtMsgs); // else check if (attempt.Success == false) return attempt; // change state to publishing content.ChangePublishedState(PublishedState.Publishing); Logger.Info($"Content '{content.Name}' with Id '{content.Id}' has been published."); return attempt; } /// /// Publishes a list of content items /// /// Contents, ordered by level ASC /// Contents for which we've already checked CanPublish /// /// /// Indicates whether to publish content that is completely unpublished (has no published /// version). If false, will only publish already published content with changes. Also impacts what happens if publishing /// fails (see remarks). /// /// /// Navigate content & descendants top-down and for each, /// - if it is published /// - and unchanged, do nothing /// - else (has changes), publish those changes /// - if it is not published /// - and at top-level, publish /// - or includeUnpublished is true, publish /// - else do nothing & skip the underlying branch /// /// When publishing fails /// - if content has no published version, skip the underlying branch /// - else (has published version), /// - if includeUnpublished is true, process the underlying branch /// - else, do not process the underlying branch /// internal IEnumerable> StrategyPublishWithChildren(IEnumerable contents, IEnumerable alreadyChecked, int userId, EventMessages evtMsgs, bool includeUnpublished = true) { var statuses = new List>(); var alreadyCheckedA = (alreadyChecked ?? Enumerable.Empty()).ToArray(); // list of ids that we exclude because they could not be published var excude = new List(); var topLevel = -1; foreach (var content in contents) { // initialize - content is ordered by level ASC if (topLevel < 0) topLevel = content.Level; if (excude.Contains(content.ParentId)) { // parent is excluded, so exclude content too Logger.Info($"Content '{content.Name}' with Id '{content.Id}' will not be published because it's parent's publishing action failed or was cancelled."); excude.Add(content.Id); // status has been reported for an ancestor and that one is excluded => no status continue; } if (content.Published && content.Level > topLevel) // topLevel we DO want to (re)publish { // newest is published already statuses.Add(Attempt.Succeed(new PublishStatus(PublishStatusType.SuccessAlreadyPublished, evtMsgs, content))); continue; } if (content.HasPublishedVersion) { // newest is published already but we are topLevel, or // newest is not published, but another version is - publish newest var r = StrategyPublish(content, alreadyCheckedA.Contains(content), userId, evtMsgs); if (r.Success == false) { // we tried to publish and it failed, but it already had / still has a published version, // the rule in remarks says that we should skip the underlying branch if includeUnpublished // is false, else process it - not that it makes much sense, but keep it like that for now if (includeUnpublished == false) excude.Add(content.Id); } statuses.Add(r); continue; } if (content.Level == topLevel || includeUnpublished) { // content has no published version, and we want to publish it, either // because it is top-level or because we include unpublished. // if publishing fails, and because content does not have a published // version at all, ensure we do not process its descendants var r = StrategyPublish(content, alreadyCheckedA.Contains(content), userId, evtMsgs); if (r.Success == false) excude.Add(content.Id); statuses.Add(r); continue; } // content has no published version, and we don't want to publish it excude.Add(content.Id); // ignore everything below it // content is not even considered, really => no status } return statuses; } internal Attempt StrategyCanUnPublish(IContent content, int userId, EventMessages evtMsgs) { // fire UnPublishing event if (UnPublishing.IsRaisedEventCancelled(new PublishEventArgs(content, evtMsgs), this)) { Logger.Info($"Content '{content.Name}' with Id '{content.Id}' will not be unpublished, the event was cancelled."); return Attempt.Fail(new UnPublishStatus(UnPublishedStatusType.FailedCancelledByEvent, evtMsgs, content)); } return Attempt.Succeed(new UnPublishStatus(content, evtMsgs)); } internal Attempt StrategyUnPublish(IContent content, bool alreadyCheckedCanUnPublish, int userId, EventMessages evtMsgs) { // content should (is assumed to) be the newest version, which may not be published, // don't know how to test this, so it's not verified var attempt = alreadyCheckedCanUnPublish ? Attempt.Succeed(new UnPublishStatus(content, evtMsgs)) // already know we can : StrategyCanUnPublish(content, userId, evtMsgs); if (attempt.Success == false) return attempt; // if Content has a release date set to before now, it should be removed so it doesn't interrupt an unpublish // otherwise it would remain released == published if (content.ReleaseDate.HasValue && content.ReleaseDate.Value <= DateTime.Now) { content.ReleaseDate = null; Logger.Info($"Content '{content.Name}' with Id '{content.Id}' had its release date removed, because it was unpublished."); } // version is published or unpublished, but content is published // change state to unpublishing content.ChangePublishedState(PublishedState.Unpublishing); Logger.Info($"Content '{content.Name}' with Id '{content.Id}' has been unpublished."); return attempt; } internal IEnumerable> StrategyUnPublish(IEnumerable content, int userId, EventMessages evtMsgs) { return content.Select(x => StrategyUnPublish(x, false, userId, evtMsgs)); } #endregion #region Content Types /// /// Deletes all content of specified type. All children of deleted content is moved to Recycle Bin. /// /// This needs extra care and attention as its potentially a dangerous and extensive operation /// Id of the /// Optional Id of the user issueing the delete operation public void DeleteContentOfType(int contentTypeId, int userId = 0) { //TODO: This currently this is called from the ContentTypeService but that needs to change, // 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 moves = new List>(); using (var uow = UowProvider.CreateUnitOfWork()) { uow.WriteLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); // fixme what about content that has the contenttype as part of its composition? var query = repository.Query.Where(x => x.ContentTypeId == contentTypeId); var contents = repository.GetByQuery(query).ToArray(); if (Deleting.IsRaisedEventCancelled(new DeleteEventArgs(contents), this)) return; // causes rollback // 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 // but... UnPublishing event makes no sense (not going to cancel?) and no need to save // just raise the event if (content.Trashed == false && content.HasPublishedVersion) UnPublished.RaiseEvent(new PublishEventArgs(content, false, false), this); // if current content has children, move them to trash var c = content; var childQuery = repository.Query.Where(x => x.Path.StartsWith(c.Path)); var children = repository.GetByQuery(childQuery); foreach (var child in children.Where(x => x.ContentTypeId != contentTypeId)) { // see MoveToRecycleBin PerformMoveLocked(repository, child, Constants.System.RecycleBinContent, null, userId, moves, true); } // delete content // triggers the deleted event (and handles the files) DeleteLocked(repository, content); } uow.Complete(); } var moveInfos = moves .Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId)) .ToArray(); if (moveInfos.Length > 0) Trashed.RaiseEvent(new MoveEventArgs(false, moveInfos), this); Audit(AuditType.Delete, $"Delete Content of Type {contentTypeId} performed by user", userId, Constants.System.Root); } private IContentType GetContentType(string contentTypeAlias) { using (var uow = UowProvider.CreateUnitOfWork()) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var query = repository.Query.Where(x => x.Alias == contentTypeAlias); var contentType = repository.GetByQuery(query).FirstOrDefault(); if (contentType == null) throw new Exception($"No ContentType matching the passed in Alias: '{contentTypeAlias}' was found"); // causes rollback uow.Complete(); return contentType; } } #endregion #region Xml - Shoud Move! /// /// Returns the persisted content's XML structure /// /// /// public XElement GetContentXml(int contentId) { using (var uow = UowProvider.CreateUnitOfWork()) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var elt = repository.GetContentXml(contentId); uow.Complete(); return elt; } } /// /// Returns the persisted content's preview XML structure /// /// /// /// public XElement GetContentPreviewXml(int contentId, Guid version) { using (var uow = UowProvider.CreateUnitOfWork()) { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var elt = repository.GetContentPreviewXml(contentId, version); uow.Complete(); return elt; } } /// /// Rebuilds all xml content in the cmsContentXml table for all documents /// /// /// Only rebuild the xml structures for the content type ids passed in, if none then rebuilds the structures /// for all content /// public void RebuildXmlStructures(params int[] contentTypeIds) { using (var uow = UowProvider.CreateUnitOfWork()) { uow.WriteLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); repository.RebuildXmlStructures( content => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, content), contentTypeIds: contentTypeIds.Length == 0 ? null : contentTypeIds); uow.Complete(); } Audit(AuditType.Publish, "ContentService.RebuildXmlStructures completed, the xml has been regenerated in the database", 0, Constants.System.Root); } #endregion } }