using System; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; using System.Linq; using System.Threading; using System.Xml.Linq; using Umbraco.Core.Auditing; using Umbraco.Core.Configuration; using Umbraco.Core.Events; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Models.Rdbms; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Persistence.UnitOfWork; using Umbraco.Core.Publishing; 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 IPublishingStrategy _publishingStrategy; private readonly EntityXmlSerializer _entitySerializer = new EntityXmlSerializer(); private readonly IDataTypeService _dataTypeService; private readonly IUserService _userService; private readonly IEnumerable _urlSegmentProviders; //Support recursive locks because some of the methods that require locking call other methods that require locking. //for example, the Move method needs to be locked but this calls the Save method which also needs to be locked. private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); public ContentService( IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, ILogger logger, IEventMessagesFactory eventMessagesFactory, IPublishingStrategy publishingStrategy, IDataTypeService dataTypeService, IUserService userService, IEnumerable urlSegmentProviders) : base(provider, repositoryFactory, logger, eventMessagesFactory) { if (publishingStrategy == null) throw new ArgumentNullException("publishingStrategy"); if (dataTypeService == null) throw new ArgumentNullException("dataTypeService"); if (userService == null) throw new ArgumentNullException("userService"); if (urlSegmentProviders == null) throw new ArgumentNullException("urlSegmentProviders"); _publishingStrategy = publishingStrategy; _dataTypeService = dataTypeService; _userService = userService; _urlSegmentProviders = urlSegmentProviders; } public int CountPublished(string contentTypeAlias = null) { var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateContentRepository(uow)) { return repository.CountPublished(); } } public int Count(string contentTypeAlias = null) { var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateContentRepository(uow)) { return repository.Count(contentTypeAlias); } } public int CountChildren(int parentId, string contentTypeAlias = null) { var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateContentRepository(uow)) { return repository.CountChildren(parentId, contentTypeAlias); } } public int CountDescendants(int parentId, string contentTypeAlias = null) { var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateContentRepository(uow)) { return repository.CountDescendants(parentId, contentTypeAlias); } } /// /// 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) { var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateContentRepository(uow)) { repository.ReplaceContentPermissions(permissionSet); } } /// /// Assigns a single permission to the current content item for the specified user ids /// /// /// /// public void AssignContentPermission(IContent entity, char permission, IEnumerable userIds) { var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateContentRepository(uow)) { repository.AssignEntityPermission(entity, permission, userIds); } } /// /// Gets the list of permissions for the content item /// /// /// public IEnumerable GetPermissionsForEntity(IContent content) { var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateContentRepository(uow)) { return repository.GetPermissionsForEntity(content.Id); } } /// /// Creates an object using the alias of the /// that this Content should based on. /// /// /// 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. /// /// Name of the Content object /// Id of Parent for the new Content /// Alias of the /// Optional id of the user creating the content /// public IContent CreateContent(string name, int parentId, string contentTypeAlias, int userId = 0) { var contentType = FindContentTypeByAlias(contentTypeAlias); var content = new Content(name, parentId, contentType); var parent = GetById(content.ParentId); content.Path = string.Concat(parent.IfNotNull(x => x.Path, content.ParentId.ToString()), ",", content.Id); if (Creating.IsRaisedEventCancelled(new NewEventArgs(content, contentTypeAlias, parentId), this)) { content.WasCancelled = true; return content; } content.CreatorId = userId; content.WriterId = userId; Created.RaiseEvent(new NewEventArgs(content, false, contentTypeAlias, parentId), this); var uow = UowProvider.GetUnitOfWork(); using (var auditRepo = RepositoryFactory.CreateAuditRepository(uow)) { auditRepo.AddOrUpdate(new AuditItem(content.Id, string.Format("Content '{0}' was created", name), AuditType.New, content.CreatorId)); uow.Commit(); } return content; } /// /// Creates an object using the alias of the /// that this Content should based on. /// /// /// 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. /// /// Name of the Content object /// Parent object for the new Content /// Alias of the /// Optional id of the user creating the content /// public IContent CreateContent(string name, IContent parent, string contentTypeAlias, int userId = 0) { if (parent == null) throw new ArgumentNullException("parent"); var contentType = FindContentTypeByAlias(contentTypeAlias); var content = new Content(name, parent, contentType); content.Path = string.Concat(parent.Path, ",", content.Id); if (Creating.IsRaisedEventCancelled(new NewEventArgs(content, contentTypeAlias, parent), this)) { content.WasCancelled = true; return content; } content.CreatorId = userId; content.WriterId = userId; Created.RaiseEvent(new NewEventArgs(content, false, contentTypeAlias, parent), this); Audit(AuditType.New, string.Format("Content '{0}' was created", name), content.CreatorId, content.Id); return content; } /// /// Creates and saves an object using the alias of the /// that this Content should based on. /// /// /// This method returns an object that has been persisted to the database /// and therefor has an identity. /// /// Name of the Content object /// Id of Parent for the new Content /// Alias of the /// Optional id of the user creating the content /// public IContent CreateContentWithIdentity(string name, int parentId, string contentTypeAlias, int userId = 0) { var contentType = FindContentTypeByAlias(contentTypeAlias); var content = new Content(name, parentId, contentType); //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. if (Creating.IsRaisedEventCancelled(new NewEventArgs(content, contentTypeAlias, parentId), this)) { content.WasCancelled = true; return content; } if (Saving.IsRaisedEventCancelled(new SaveEventArgs(content), this)) { content.WasCancelled = true; return content; } var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateContentRepository(uow)) { content.CreatorId = userId; content.WriterId = userId; repository.AddOrUpdate(content); //Generate a new preview repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); uow.Commit(); } Saved.RaiseEvent(new SaveEventArgs(content, false), this); Created.RaiseEvent(new NewEventArgs(content, false, contentTypeAlias, parentId), this); Audit(AuditType.New, string.Format("Content '{0}' was created with Id {1}", name, content.Id), content.CreatorId, content.Id); return content; } /// /// Creates and saves an object using the alias of the /// that this Content should based on. /// /// /// This method returns an object that has been persisted to the database /// and therefor has an identity. /// /// Name of the Content object /// Parent object for the new Content /// Alias of the /// Optional id of the user creating the content /// public IContent CreateContentWithIdentity(string name, IContent parent, string contentTypeAlias, int userId = 0) { if (parent == null) throw new ArgumentNullException("parent"); var contentType = FindContentTypeByAlias(contentTypeAlias); var content = new Content(name, parent, contentType); //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. if (Creating.IsRaisedEventCancelled(new NewEventArgs(content, contentTypeAlias, parent), this)) { content.WasCancelled = true; return content; } if (Saving.IsRaisedEventCancelled(new SaveEventArgs(content), this)) { content.WasCancelled = true; return content; } var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateContentRepository(uow)) { content.CreatorId = userId; content.WriterId = userId; repository.AddOrUpdate(content); //Generate a new preview repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); uow.Commit(); } Saved.RaiseEvent(new SaveEventArgs(content, false), this); Created.RaiseEvent(new NewEventArgs(content, false, contentTypeAlias, parent), this); Audit(AuditType.New, string.Format("Content '{0}' was created with Id {1}", name, content.Id), content.CreatorId, content.Id); return content; } /// /// Gets an object by Id /// /// Id of the Content to retrieve /// public IContent GetById(int id) { using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) { return repository.Get(id); } } /// /// Gets an object by Id /// /// Ids of the Content to retrieve /// public IEnumerable GetByIds(IEnumerable ids) { if (ids.Any() == false) return Enumerable.Empty(); using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) { return repository.GetAll(ids.ToArray()); } } /// /// Gets an object by its 'UniqueId' /// /// Guid key of the Content to retrieve /// public IContent GetById(Guid key) { using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) { var query = repository.Query.Where(x => x.Key == key); var contents = repository.GetByQuery(query); return contents.SingleOrDefault(); } } /// /// 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 repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) { var query = repository.Query.Where(x => x.ContentTypeId == id); var contents = repository.GetByQuery(query); return contents; } } internal IEnumerable GetPublishedContentOfContentType(int id) { using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) { var query = repository.Query.Where(x => x.ContentTypeId == id); var contents = repository.GetByPublishedVersion(query); return contents; } } /// /// Gets a collection of objects by Level /// /// The level to retrieve Content from /// An Enumerable list of objects public IEnumerable GetByLevel(int level) { using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) { var query = repository.Query.Where(x => x.Level == level && !x.Path.StartsWith(Constants.System.RecycleBinContent.ToInvariantString())); var contents = repository.GetByQuery(query); return contents; } } /// /// Gets a specific version of an item. /// /// Id of the version to retrieve /// An item public IContent GetByVersion(Guid versionId) { using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) { return repository.GetByVersion(versionId); } } /// /// Gets a collection of an objects versions by Id /// /// /// An Enumerable list of objects public IEnumerable GetVersions(int id) { using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) { var versions = repository.GetAllVersions(id); return versions; } } /// /// 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) { 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 ids = content.Path.Split(',').Where(x => x != Constants.System.Root.ToInvariantString() && x != content.Id.ToString(CultureInfo.InvariantCulture)).Select(int.Parse).ToArray(); if (ids.Any() == false) return new List(); using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) { return repository.GetAll(ids); } } /// /// 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 repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) { var query = repository.Query.Where(x => x.ParentId == id); var contents = repository.GetByQuery(query).OrderBy(x => x.SortOrder); return contents; } } [Obsolete("Use the overload with 'long' parameter types instead")] [EditorBrowsable(EditorBrowsableState.Never)] public IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy, Direction orderDirection, string filter = "") { Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); Mandate.ParameterCondition(pageSize > 0, "pageSize"); using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) { //if the id is System Root, then just get all if (id != Constants.System.Root) { repository.Query.Where(x => x.ParentId == id); } long total; var contents = repository.GetPagedResultsByQuery(repository.Query, pageIndex, pageSize, out total, orderBy, orderDirection, filter); totalChildren = Convert.ToInt32(total); return contents; } } /// /// 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 = "") { Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); Mandate.ParameterCondition(pageSize > 0, "pageSize"); using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) { 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 contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, filter); return contents; } } [Obsolete("Use the overload with 'long' parameter types instead")] [EditorBrowsable(EditorBrowsableState.Never)] public IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = "") { Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); Mandate.ParameterCondition(pageSize > 0, "pageSize"); using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) { //if the id is System Root, then just get all if (id != Constants.System.Root) { repository.Query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); } long total; var contents = repository.GetPagedResultsByQuery(repository.Query, pageIndex, pageSize, out total, orderBy, orderDirection, filter); totalChildren = Convert.ToInt32(total); return contents; } } /// /// 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 = "") { Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); Mandate.ParameterCondition(pageSize > 0, "pageSize"); using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) { 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(string.Format(",{0},", id), TextColumnType.NVarchar)); } var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, filter); 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 repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) { var query = repository.Query.Where(x => x.ParentId == parentId && x.Name.Contains(name)); var contents = repository.GetByQuery(query); return contents; } } /// /// 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) { var content = GetById(id); if (content == null) { return Enumerable.Empty(); } return GetDescendants(content); } /// /// 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 repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) { var pathMatch = content.Path + ","; var query = repository.Query.Where(x => x.Path.StartsWith(pathMatch) && x.Id != content.Id); var contents = repository.GetByQuery(query); return contents; } } /// /// 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) { 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 == true); } /// /// 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 repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) { var query = repository.Query.Where(x => x.ParentId == Constants.System.Root); var contents = repository.GetByQuery(query); return contents; } } /// /// Gets all published content items /// /// internal IEnumerable GetAllPublished() { using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) { var query = repository.Query.Where(x => x.Trashed == false); return repository.GetByPublishedVersion(query); } } /// /// 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 repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) { var query = repository.Query.Where(x => x.Published == true && x.ExpireDate <= DateTime.Now); var contents = repository.GetByQuery(query); return contents; } } /// /// 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 repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) { var query = repository.Query.Where(x => x.Published == false && x.ReleaseDate <= DateTime.Now); var contents = repository.GetByQuery(query); return contents; } } /// /// Gets a collection of an objects, which resides in the Recycle Bin /// /// An Enumerable list of objects public IEnumerable GetContentInRecycleBin() { using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) { var query = repository.Query.Where(x => x.Path.Contains(Constants.System.RecycleBinContent.ToInvariantString())); var contents = repository.GetByQuery(query); return contents; } } /// /// 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; } internal int CountChildren(int id) { using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) { var query = repository.Query.Where(x => x.ParentId == id); var count = repository.Count(query); return count; } } /// /// 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 repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) { var query = repository.Query.Where(x => x.Published == true && x.Id == id && x.Trashed == false); int count = repository.Count(query); 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) { //If the passed in content has yet to be saved we "fallback" to checking the Parent //because if the Parent is publishable then the current content can be Saved and Published if (content.HasIdentity == false) { IContent parent = GetById(content.ParentId); return IsPublishable(parent, true); } return IsPublishable(content, false); } /// /// 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); } } /// /// 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 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); } /// /// 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); } /// /// 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(); using (new WriteLock(Locker)) { var originalPath = content.Path; if (Trashing.IsRaisedEventCancelled( new MoveEventArgs(evtMsgs, new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent)), this)) { return Attempt.Fail(OperationStatus.Cancelled(evtMsgs)); } var moveInfo = new List> { new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent) }; //Make sure that published content is unpublished before being moved to the Recycle Bin if (HasPublishedVersion(content.Id)) { //TODO: this shouldn't be a 'sub operation', and if it needs to be it cannot raise events and cannot be cancelled! UnPublish(content, userId); } //Unpublish descendents of the content item that is being moved to trash var descendants = GetDescendants(content).OrderBy(x => x.Level).ToList(); foreach (var descendant in descendants) { //TODO: this shouldn't be a 'sub operation', and if it needs to be it cannot raise events and cannot be cancelled! UnPublish(descendant, userId); } var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateContentRepository(uow)) { content.WriterId = userId; content.ChangeTrashedState(true); repository.AddOrUpdate(content); //Loop through descendants to update their trash state, but ensuring structure by keeping the ParentId foreach (var descendant in descendants) { moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); descendant.WriterId = userId; descendant.ChangeTrashedState(true, descendant.ParentId); repository.AddOrUpdate(descendant); } uow.Commit(); } Trashed.RaiseEvent(new MoveEventArgs(false, evtMsgs, moveInfo.ToArray()), this); Audit(AuditType.Move, "Move Content to Recycle Bin performed by user", userId, content.Id); return Attempt.Succeed(OperationStatus.Success(evtMsgs)); } } /// /// 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); } /// /// 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) { var result = PublishWithChildrenDo(content, userId, true); //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 if (result.All(x => x.Result.ContentItem.Id != content.Id)) return false; return result.Single(x => x.Result.ContentItem.Id == content.Id).Success; } /// /// 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); } /// /// 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; } /// /// 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 public Attempt SaveAndPublishWithStatus(IContent content, int userId = 0, bool raiseEvents = true) { return ((IContentServiceOperations)this).SaveAndPublish(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. public void Save(IContent content, int userId = 0, bool raiseEvents = true) { ((IContentServiceOperations)this).Save(content, 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 asArray = contents.ToArray(); var evtMsgs = EventMessagesFactory.Get(); if (raiseEvents) { if (Saving.IsRaisedEventCancelled( new SaveEventArgs(asArray, evtMsgs), this)) { return Attempt.Fail(OperationStatus.Cancelled(evtMsgs)); } } using (new WriteLock(Locker)) { var containsNew = asArray.Any(x => x.HasIdentity == false); var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateContentRepository(uow)) { if (containsNew) { foreach (var content in asArray) { content.WriterId = userId; //Only change the publish state if the "previous" version was actually published if (content.Published) content.ChangePublishedState(PublishedState.Saved); repository.AddOrUpdate(content); //add or update preview repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); } } else { foreach (var content in asArray) { content.WriterId = userId; repository.AddOrUpdate(content); //add or update preview repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); } } uow.Commit(); } if (raiseEvents) Saved.RaiseEvent(new SaveEventArgs(asArray, false, evtMsgs), this); Audit(AuditType.Save, "Bulk Save content performed by user", userId == -1 ? 0 : userId, Constants.System.Root); return Attempt.Succeed(OperationStatus.Success(evtMsgs)); } } /// /// 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(); using (new WriteLock(Locker)) { if (Deleting.IsRaisedEventCancelled( new DeleteEventArgs(content, evtMsgs), this)) { return Attempt.Fail(OperationStatus.Cancelled(evtMsgs)); } //Make sure that published content is unpublished before being deleted if (HasPublishedVersion(content.Id)) { UnPublish(content, userId); } //Delete children before deleting the 'possible parent' var children = GetChildren(content.Id); foreach (var child in children) { Delete(child, userId); } var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateContentRepository(uow)) { repository.Delete(content); uow.Commit(); var args = new DeleteEventArgs(content, false, evtMsgs); Deleted.RaiseEvent(args, this); //remove any flagged media files repository.DeleteMediaFiles(args.MediaFilesToDelete); } Audit(AuditType.Delete, "Delete Content performed by user", userId, content.Id); return Attempt.Succeed(OperationStatus.Success(evtMsgs)); } } /// /// 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); } /// /// 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) { return Save(content, true, userId, raiseEvents); } /// /// 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); } /// /// 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) { using (new WriteLock(Locker)) { using (var uow = UowProvider.GetUnitOfWork()) { var repository = RepositoryFactory.CreateContentRepository(uow); //NOTE 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; foreach (var content in contents.OrderByDescending(x => x.ParentId)) { //Look for children of current content and move that to trash before the current content is deleted var c = content; var childQuery = repository.Query.Where(x => x.Path.StartsWith(c.Path)); var children = repository.GetByQuery(childQuery); foreach (var child in children) { if (child.ContentType.Id != contentTypeId) MoveToRecycleBin(child, userId); } //Permantly delete the content Delete(content, userId); } } Audit(AuditType.Delete, string.Format("Delete Content of Type {0} performed by user", contentTypeId), userId, Constants.System.Root); } } /// /// 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 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; var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateContentRepository(uow)) { repository.DeleteVersions(id, versionDate); uow.Commit(); } 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) { using (new WriteLock(Locker)) { if (DeletingVersions.IsRaisedEventCancelled(new DeleteRevisionsEventArgs(id, specificVersion: versionId), this)) return; if (deletePriorVersions) { var content = GetByVersion(versionId); DeleteVersions(id, content.UpdateDate, userId); } var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateContentRepository(uow)) { repository.DeleteVersion(versionId); uow.Commit(); } DeletedVersions.RaiseEvent(new DeleteRevisionsEventArgs(id, false, specificVersion: versionId), this); Audit(AuditType.Delete, "Delete Content by version performed by user", userId, Constants.System.Root); } } /// /// 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); } /// /// 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) { using (new WriteLock(Locker)) { //This ensures that the correct method is called if this method is used to Move to recycle bin. if (parentId == Constants.System.RecycleBinContent) { MoveToRecycleBin(content, userId); return; } if (Moving.IsRaisedEventCancelled( new MoveEventArgs( new MoveEventInfo(content, content.Path, parentId)), this)) { return; } //used to track all the moved entities to be given to the event var moveInfo = new List>(); //call private method that does the recursive moving PerformMove(content, parentId, userId, moveInfo); Moved.RaiseEvent(new MoveEventArgs(false, moveInfo.ToArray()), this); Audit(AuditType.Move, "Move Content performed by user", userId, content.Id); } } /// /// Empties the Recycle Bin by deleting all that resides in the bin /// public void EmptyRecycleBin() { using (new WriteLock(Locker)) { Dictionary> entities; List files; bool success; var nodeObjectType = new Guid(Constants.ObjectTypes.Document); using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) { //Create a dictionary of ids -> dictionary of property aliases + values entities = repository.GetEntitiesInRecycleBin() .ToDictionary( key => key.Id, val => (IEnumerable)val.Properties); files = ((ContentRepository)repository).GetFilesInRecycleBinForUploadField(); if (EmptyingRecycleBin.IsRaisedEventCancelled(new RecycleBinEventArgs(nodeObjectType, entities, files), this)) return; success = repository.EmptyRecycleBin(); EmptiedRecycleBin.RaiseEvent(new RecycleBinEventArgs(nodeObjectType, entities, files, success), this); if (success) repository.DeleteMediaFiles(files); } } Audit(AuditType.Delete, "Empty Content Recycle Bin performed by user", 0, Constants.System.RecycleBinContent); } /// /// 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) { //TODO: This all needs to be managed correctly so that the logic is submitted in one // transaction, the CRUD needs to be moved to the repo using (new WriteLock(Locker)) { var copy = content.DeepCloneWithResetIdentities(); copy.ParentId = parentId; // A copy should never be set to published automatically even if the original was. copy.ChangePublishedState(PublishedState.Unpublished); if (Copying.IsRaisedEventCancelled(new CopyEventArgs(content, copy, parentId), this)) return null; var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateContentRepository(uow)) { // Update the create author and last edit author copy.CreatorId = userId; copy.WriterId = userId; repository.AddOrUpdate(copy); //add or update a preview repository.AddOrUpdatePreviewXml(copy, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); uow.Commit(); //Special case for the associated tags //TODO: Move this to the repository layer in a single transaction! //don't copy tags data in tags table if the item is in the recycle bin if (parentId != Constants.System.RecycleBinContent) { var tags = uow.Database.Fetch("WHERE nodeId = @Id", new { Id = content.Id }); foreach (var tag in tags) { uow.Database.Insert(new TagRelationshipDto { NodeId = copy.Id, TagId = tag.TagId, PropertyTypeId = tag.PropertyTypeId }); } } } if (recursive) { //Look for children and copy those as well var children = GetChildren(content.Id); foreach (var child in children) { //TODO: This shouldn't recurse back to this method, it should be done in a private method // that doesn't have a nested lock and so we can perform the entire operation in one commit. Copy(child, copy.Id, relateToOriginal, true, userId); } } 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; var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateContentRepository(uow)) { content.WriterId = userId; content.CreatorId = userId; content.ChangePublishedState(PublishedState.Unpublished); repository.AddOrUpdate(content); //add or update a preview repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); uow.Commit(); } 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) { if (raiseEvents) { if (Saving.IsRaisedEventCancelled(new SaveEventArgs(items), this)) return false; } var shouldBePublished = new List(); var shouldBeSaved = new List(); var asArray = items.ToArray(); using (new WriteLock(Locker)) { var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateContentRepository(uow)) { int i = 0; foreach (var content in asArray) { //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 == i) { i++; continue; } content.SortOrder = i; content.WriterId = userId; i++; if (content.Published) { //TODO: This should not be an inner operation, but if we do this, it cannot raise events and cannot be cancellable! var published = _publishingStrategy.Publish(content, userId); shouldBePublished.Add(content); } else shouldBeSaved.Add(content); repository.AddOrUpdate(content); //add or update a preview repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); } foreach (var content in shouldBePublished) { //Create and Save ContentXml DTO repository.AddOrUpdateContentXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); } uow.Commit(); } } if (raiseEvents) Saved.RaiseEvent(new SaveEventArgs(asArray, false), this); if (shouldBePublished.Any()) { //TODO: This should not be an inner operation, but if we do this, it cannot raise events and cannot be cancellable! _publishingStrategy.PublishingFinalized(shouldBePublished, false); } Audit(AuditType.Sort, "Sorting content performed by user", userId, 0); return true; } /// /// 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) { var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateContentRepository(uow)) { repository.RebuildXmlStructures( content => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, content), contentTypeIds: contentTypeIds.Length == 0 ? null : contentTypeIds); uow.Commit(); } Audit(AuditType.Publish, "ContentService.RebuildXmlStructures completed, the xml has been regenerated in the database", 0, Constants.System.Root); } #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 repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) { var query = repository.Query.Where(x => x.Id != content.Id && x.Path.StartsWith(content.Path) && x.Trashed == false); var contents = repository.GetByPublishedVersion(query); return contents; } } #endregion #region Private Methods private void Audit(AuditType type, string message, int userId, int objectId) { var uow = UowProvider.GetUnitOfWork(); using (var auditRepo = RepositoryFactory.CreateAuditRepository(uow)) { auditRepo.AddOrUpdate(new AuditItem(objectId, message, type, userId)); uow.Commit(); } } private void PerformMove(IContent content, int parentId, int userId, ICollection> moveInfo) { //add a tracking item to use in the Moved event moveInfo.Add(new MoveEventInfo(content, content.Path, parentId)); content.WriterId = userId; if (parentId == Constants.System.Root) { content.Path = string.Concat(Constants.System.Root, ",", content.Id); content.Level = 1; } else { var parent = GetById(parentId); content.Path = string.Concat(parent.Path, ",", content.Id); content.Level = parent.Level + 1; } //If Content is being moved away from Recycle Bin, its state should be un-trashed if (content.Trashed && parentId != Constants.System.RecycleBinContent) { content.ChangeTrashedState(false, parentId); } else { content.ParentId = parentId; } //If Content is published, it should be (re)published from its new location if (content.Published) { //If Content is Publishable its saved and published //otherwise we save the content without changing the publish state, and generate new xml because the Path, Level and Parent has changed. if (IsPublishable(content)) { //TODO: This is raising events, probably not desirable as this costs performance for event listeners like Examine SaveAndPublish(content, userId); } else { //TODO: This is raising events, probably not desirable as this costs performance for event listeners like Examine Save(content, false, userId); //TODO: This shouldn't be here! This needs to be part of the repository logic but in order to fix this we need to // change how this method calls "Save" as it needs to save using an internal method using (var uow = UowProvider.GetUnitOfWork()) { var xml = _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, content); var poco = new ContentXmlDto { NodeId = content.Id, Xml = xml.ToDataString() }; var exists = uow.Database.FirstOrDefault("WHERE nodeId = @Id", new { Id = content.Id }) != null; int result = exists ? uow.Database.Update(poco) : Convert.ToInt32(uow.Database.Insert(poco)); } } } else { //TODO: This is raising events, probably not desirable as this costs performance for event listeners like Examine Save(content, userId); } //Ensure that Path and Level is updated on children var children = GetChildren(content.Id).ToArray(); if (children.Any()) { foreach (var child in children) { PerformMove(child, content.Id, userId, moveInfo); } } } /// /// 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("content"); var evtMsgs = EventMessagesFactory.Get(); using (new WriteLock(Locker)) { var result = new List>(); //Check if parent is published (although not if its a root node) - if parent isn't published this Content cannot be published if (content.ParentId != Constants.System.Root && content.ParentId != Constants.System.RecycleBinContent && IsPublishable(content) == false) { Logger.Info( string.Format( "Content '{0}' with Id '{1}' could not be published because its parent or one of its ancestors is not published.", content.Name, content.Id)); result.Add(Attempt.Fail(new PublishStatus(content, PublishStatusType.FailedPathNotPublished, evtMsgs))); return result; } //Content contains invalid property values and can therefore not be published - fire event? if (!content.IsValid()) { Logger.Info( string.Format("Content '{0}' with Id '{1}' could not be published because of invalid properties.", content.Name, content.Id)); result.Add( Attempt.Fail( new PublishStatus(content, PublishStatusType.FailedContentInvalid, evtMsgs) { InvalidProperties = ((ContentBase)content).LastInvalidProperties })); return result; } //Consider creating a Path query instead of recursive method: //var query = repository.Query.Where(x => x.Path.StartsWith(content.Path)); var updated = new List(); var list = new List(); list.Add(content); //include parent item list.AddRange(GetDescendants(content)); var internalStrategy = (PublishingStrategy)_publishingStrategy; //Publish and then update the database with new status var publishedOutcome = internalStrategy.PublishWithChildrenInternal(list, userId, includeUnpublished).ToArray(); var published = publishedOutcome .Where(x => x.Success || x.Result.StatusType == PublishStatusType.SuccessAlreadyPublished) // ensure proper order (for events) - cannot publish a child before its parent! .OrderBy(x => x.Result.ContentItem.Level) .ThenBy(x => x.Result.ContentItem.SortOrder); var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateContentRepository(uow)) { //NOTE The Publish with subpages-dialog was used more as a republish-type-thing, so we'll have to include PublishStatusType.SuccessAlreadyPublished //in the updated-list, so the Published event is triggered with the expected set of pages and the xml is updated. foreach (var item in published) { item.Result.ContentItem.WriterId = userId; repository.AddOrUpdate(item.Result.ContentItem); //add or update a preview repository.AddOrUpdatePreviewXml(item.Result.ContentItem, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); //add or update the published xml repository.AddOrUpdateContentXml(item.Result.ContentItem, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); updated.Add(item.Result.ContentItem); } uow.Commit(); } //Save xml to db and call following method to fire event: _publishingStrategy.PublishingFinalized(updated, false); Audit(AuditType.Publish, "Publish with Children performed by user", userId, content.Id); return publishedOutcome; } } /// /// 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) { 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; var evtMsgs = EventMessagesFactory.Get(); var published = content.Published ? content : GetPublishedVersion(content.Id); // get the published version if (published == null) { return Attempt.Succeed(new UnPublishStatus(content, UnPublishedStatusType.SuccessAlreadyUnPublished, evtMsgs)); // already unpublished } var unpublished = _publishingStrategy.UnPublish(content, userId); if (unpublished == false) return Attempt.Fail(new UnPublishStatus(content, UnPublishedStatusType.FailedCancelledByEvent, evtMsgs)); var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateContentRepository(uow)) { content.WriterId = userId; repository.AddOrUpdate(content); // is published is not newest, reset the published flag on published version if (published.Version != content.Version) repository.ClearPublished(published); repository.DeleteContentXml(content); uow.Commit(); } //Delete xml from db? and call following method to fire event through PublishingStrategy to update cache if (omitCacheRefresh == false) _publishingStrategy.UnPublishingFinalized(content); Audit(AuditType.UnPublish, "UnPublish performed by user", userId, content.Id); return Attempt.Succeed(new UnPublishStatus(content, UnPublishedStatusType.Success, 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 private Attempt SaveAndPublishDo(IContent content, int userId = 0, bool raiseEvents = true) { var evtMsgs = EventMessagesFactory.Get(); if (raiseEvents) { if (Saving.IsRaisedEventCancelled( new SaveEventArgs(content, evtMsgs), this)) { return Attempt.Fail(new PublishStatus(content, PublishStatusType.FailedCancelledByEvent, evtMsgs)); } } using (new WriteLock(Locker)) { //Has this content item previously been published? If so, we don't need to refresh the children var previouslyPublished = content.HasIdentity && HasPublishedVersion(content.Id); //content might not have an id var publishStatus = new PublishStatus(content, PublishStatusType.Success, evtMsgs); //initially set to success //Check if parent is published (although not if its a root node) - if parent isn't published this Content cannot be published publishStatus.StatusType = CheckAndLogIsPublishable(content); //if it is not successful, then check if the props are valid if ((int)publishStatus.StatusType < 10) { //Content contains invalid property values and can therefore not be published - fire event? publishStatus.StatusType = CheckAndLogIsValid(content); //set the invalid properties (if there are any) publishStatus.InvalidProperties = ((ContentBase)content).LastInvalidProperties; } //if we're still successful, then publish using the strategy if (publishStatus.StatusType == PublishStatusType.Success) { var internalStrategy = (PublishingStrategy)_publishingStrategy; //Publish and then update the database with new status var publishResult = internalStrategy.PublishInternal(content, userId); //set the status type to the publish result publishStatus.StatusType = publishResult.Result.StatusType; } //we are successfully published if our publishStatus is still Successful bool published = publishStatus.StatusType == PublishStatusType.Success; var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateContentRepository(uow)) { //Since this is the Save and Publish method, the content should be saved even though the publish fails or isn't allowed if (content.HasIdentity == false) { content.CreatorId = userId; } content.WriterId = userId; repository.AddOrUpdate(content); //Generate a new preview repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); if (published) { //Content Xml repository.AddOrUpdateContentXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); } uow.Commit(); } if (raiseEvents) Saved.RaiseEvent(new SaveEventArgs(content, false, evtMsgs), this); //Save xml to db and call following method to fire event through PublishingStrategy to update cache if (published) { _publishingStrategy.PublishingFinalized(content); } //We need to check if children and their publish state to ensure that we 'republish' content that was previously published if (published && previouslyPublished == false && HasChildren(content.Id)) { var descendants = GetPublishedDescendants(content); _publishingStrategy.PublishingFinalized(descendants, false); } Audit(AuditType.Publish, "Save and Publish performed by user", userId, content.Id); return Attempt.If(publishStatus.StatusType == PublishStatusType.Success, publishStatus); } } /// /// Saves a single object /// /// The to save /// Boolean indicating whether or not to change the Published state upon saving /// Optional Id of the User saving the Content /// Optional boolean indicating whether or not to raise events. private Attempt Save(IContent content, bool changeState, int userId = 0, bool raiseEvents = true) { var evtMsgs = EventMessagesFactory.Get(); if (raiseEvents) { if (Saving.IsRaisedEventCancelled( new SaveEventArgs(content, evtMsgs), this)) { return Attempt.Fail(OperationStatus.Cancelled(evtMsgs)); } } using (new WriteLock(Locker)) { var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateContentRepository(uow)) { if (content.HasIdentity == false) { content.CreatorId = userId; } content.WriterId = userId; //Only change the publish state if the "previous" version was actually published or marked as unpublished if (changeState && (content.Published || ((Content)content).PublishedState == PublishedState.Unpublished)) content.ChangePublishedState(PublishedState.Saved); repository.AddOrUpdate(content); //Generate a new preview repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); uow.Commit(); } if (raiseEvents) Saved.RaiseEvent(new SaveEventArgs(content, false, evtMsgs), this); Audit(AuditType.Save, "Save Content performed by user", userId, content.Id); return Attempt.Succeed(OperationStatus.Success(evtMsgs)); } } /// /// Checks if the passed in can be published based on the anscestors publish state. /// /// /// Check current is only used when falling back to checking the Parent of non-saved content, as /// non-saved content doesn't have a valid path yet. /// /// to check if anscestors are published /// Boolean indicating whether the passed in content should also be checked for published versions /// True if the Content can be published, otherwise False private bool IsPublishable(IContent content, bool checkCurrent) { var ids = content.Path.Split(',').Select(int.Parse).ToList(); foreach (var id in ids) { //If Id equals that of the recycle bin we return false because nothing in the bin can be published if (id == Constants.System.RecycleBinContent) return false; //We don't check the System Root, so just continue if (id == Constants.System.Root) continue; //If the current id equals that of the passed in content and if current shouldn't be checked we skip it. if (checkCurrent == false && id == content.Id) continue; //Check if the content for the current id is published - escape the loop if we encounter content that isn't published var hasPublishedVersion = HasPublishedVersion(id); if (hasPublishedVersion == false) return false; } return true; } private PublishStatusType CheckAndLogIsPublishable(IContent content) { //Check if parent is published (although not if its a root node) - if parent isn't published this Content cannot be published if (content.ParentId != Constants.System.Root && content.ParentId != Constants.System.RecycleBinContent && IsPublishable(content) == false) { Logger.Info( string.Format( "Content '{0}' with Id '{1}' could not be published because its parent is not published.", content.Name, content.Id)); return PublishStatusType.FailedPathNotPublished; } return PublishStatusType.Success; } private PublishStatusType CheckAndLogIsValid(IContent content) { //Content contains invalid property values and can therefore not be published - fire event? if (content.IsValid() == false) { Logger.Info( string.Format( "Content '{0}' with Id '{1}' could not be published because of invalid properties.", content.Name, content.Id)); return PublishStatusType.FailedContentInvalid; } return PublishStatusType.Success; } private IContentType FindContentTypeByAlias(string contentTypeAlias) { using (var repository = RepositoryFactory.CreateContentTypeRepository(UowProvider.GetUnitOfWork())) { var query = repository.Query.Where(x => x.Alias == contentTypeAlias); var types = repository.GetByQuery(query); if (types.Any() == false) throw new Exception( string.Format("No ContentType matching the passed in Alias: '{0}' was found", contentTypeAlias)); var contentType = types.First(); if (contentType == null) throw new Exception(string.Format("ContentType matching the passed in Alias: '{0}' was null", contentTypeAlias)); return contentType; } } #endregion #region Proxy Event Handlers /// /// Occurs before publish. /// /// Proxy to the real event on the public static event TypedEventHandler> Publishing { add { PublishingStrategy.Publishing += value; } remove { PublishingStrategy.Publishing -= value; } } /// /// Occurs after publish. /// /// Proxy to the real event on the public static event TypedEventHandler> Published { add { PublishingStrategy.Published += value; } remove { PublishingStrategy.Published -= value; } } /// /// Occurs before unpublish. /// /// Proxy to the real event on the public static event TypedEventHandler> UnPublishing { add { PublishingStrategy.UnPublishing += value; } remove { PublishingStrategy.UnPublishing -= value; } } /// /// Occurs after unpublish. /// /// Proxy to the real event on the public static event TypedEventHandler> UnPublished { add { PublishingStrategy.UnPublished += value; } remove { PublishingStrategy.UnPublished -= value; } } #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; #endregion } }