using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Xml.Linq; using Umbraco.Core.Auditing; using Umbraco.Core.Events; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Rdbms; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.UnitOfWork; using Umbraco.Core.Publishing; namespace Umbraco.Core.Services { /// /// Represents the Content Service, which is an easy access to operations involving /// public class ContentService : IContentService { private readonly IDatabaseUnitOfWorkProvider _uowProvider; private readonly IPublishingStrategy _publishingStrategy; private readonly RepositoryFactory _repositoryFactory; public ContentService() : this(new RepositoryFactory()) {} public ContentService(RepositoryFactory repositoryFactory) : this(new PetaPocoUnitOfWorkProvider(), repositoryFactory, new PublishingStrategy()) {} public ContentService(IDatabaseUnitOfWorkProvider provider) : this(provider, new RepositoryFactory(), new PublishingStrategy()) { } public ContentService(IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory) : this(provider, repositoryFactory, new PublishingStrategy()) { } public ContentService(IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, IPublishingStrategy publishingStrategy) { _uowProvider = provider; _publishingStrategy = publishingStrategy; _repositoryFactory = repositoryFactory; } /// /// Creates an object using the alias of the /// that this Content is based on. /// /// 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) { IContentType contentType = null; IContent content = null; var uow = _uowProvider.GetUnitOfWork(); using (var repository = _repositoryFactory.CreateContentTypeRepository(uow)) { var query = Query.Builder.Where(x => x.Alias == contentTypeAlias); var contentTypes = repository.GetByQuery(query); if (!contentTypes.Any()) throw new Exception(string.Format("No ContentType matching the passed in Alias: '{0}' was found", contentTypeAlias)); contentType = contentTypes.First(); if (contentType == null) throw new Exception(string.Format("ContentType matching the passed in Alias: '{0}' was null", contentTypeAlias)); } content = new Content(name, parentId, contentType); if (Creating.IsRaisedEventCancelled(new NewEventArgs(content, contentTypeAlias, parentId), this)) return content; content.CreatorId = userId; content.WriterId = userId; Created.RaiseEvent(new NewEventArgs(content, false, contentTypeAlias, parentId), this); Audit.Add(AuditTypes.New, "", content.CreatorId, content.Id); return content; } /// /// Creates an object using the alias of the /// that this Content is based on. /// /// 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) { IContentType contentType = null; IContent content = null; var uow = _uowProvider.GetUnitOfWork(); using (var repository = _repositoryFactory.CreateContentTypeRepository(uow)) { var query = Query.Builder.Where(x => x.Alias == contentTypeAlias); var contentTypes = repository.GetByQuery(query); if (!contentTypes.Any()) throw new Exception(string.Format("No ContentType matching the passed in Alias: '{0}' was found", contentTypeAlias)); contentType = contentTypes.First(); if (contentType == null) throw new Exception(string.Format("ContentType matching the passed in Alias: '{0}' was null", contentTypeAlias)); } content = new Content(name, parent, contentType); if (Creating.IsRaisedEventCancelled(new NewEventArgs(content, contentTypeAlias, parent), this)) return content; content.CreatorId = userId; content.WriterId = userId; Created.RaiseEvent(new NewEventArgs(content, false, contentTypeAlias, parent), this); Audit.Add(AuditTypes.New, "", 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 its 'UniqueId' /// /// Guid key of the Content to retrieve /// public IContent GetById(Guid key) { using (var repository = _repositoryFactory.CreateContentRepository(_uowProvider.GetUnitOfWork())) { var query = Query.Builder.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 = Query.Builder.Where(x => x.ContentTypeId == id); var contents = repository.GetByQuery(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 = Query.Builder.Where(x => x.Level == level); 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 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 = Query.Builder.Where(x => x.ParentId == id); var contents = repository.GetByQuery(query); 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 = Query.Builder.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); 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 query = Query.Builder.Where(x => x.Path.StartsWith(content.Path) && x.Id != content.Id); var contents = repository.GetByQuery(query); return contents; } } /// /// 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 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 = Query.Builder.Where(x => x.ParentId == -1); var contents = repository.GetByQuery(query); return contents; } } /// /// 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 = Query.Builder.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 = Query.Builder.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 = Query.Builder.Where(x => x.ParentId == -20); 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) { using (var repository = _repositoryFactory.CreateContentRepository(_uowProvider.GetUnitOfWork())) { var query = Query.Builder.Where(x => x.ParentId == id); int count = repository.Count(query); return count > 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 repository = _repositoryFactory.CreateContentRepository(_uowProvider.GetUnitOfWork())) { var query = Query.Builder.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); } /// /// Re-Publishes all Content /// /// Optional Id of the User issueing the publishing /// True if publishing succeeded, otherwise False public bool RePublishAll(int userId = 0) { return RePublishAllDo(false, userId); } /// /// 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) { return SaveAndPublishDo(content, false, 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 public bool PublishWithChildren(IContent content, int userId = 0) { return PublishWithChildrenDo(content, false, 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 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 bool SaveAndPublish(IContent content, int userId = 0, bool raiseEvents = true) { return SaveAndPublishDo(content, false, 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) { 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) { if(raiseEvents) { if (Saving.IsRaisedEventCancelled(new SaveEventArgs(contents), this)) return; } var containsNew = contents.Any(x => x.HasIdentity == false); var uow = _uowProvider.GetUnitOfWork(); using (var repository = _repositoryFactory.CreateContentRepository(uow)) { if (containsNew) { foreach (var content in contents) { 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); uow.Commit(); } } else { foreach (var content in contents) { content.WriterId = userId; repository.AddOrUpdate(content); } uow.Commit(); } } if(raiseEvents) Saved.RaiseEvent(new SaveEventArgs(contents, false), this); Audit.Add(AuditTypes.Save, "Bulk Save content performed by user", userId == -1 ? 0 : userId, -1); } /// /// 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 (var uow = _uowProvider.GetUnitOfWork()) { var repository = _repositoryFactory.CreateContentRepository(uow); //NOTE What about content that has the contenttype as part of its composition? var query = Query.Builder.Where(x => x.ContentTypeId == contentTypeId); var contents = repository.GetByQuery(query); 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 = Query.Builder.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.Add(AuditTypes.Delete, string.Format("Delete Content of Type {0} performed by user", contentTypeId), userId, -1); } /// /// 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 public void Delete(IContent content, int userId = 0) { if (Deleting.IsRaisedEventCancelled(new DeleteEventArgs(content), this)) return; //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(); } Deleted.RaiseEvent(new DeleteEventArgs(content, false), this); Audit.Add(AuditTypes.Delete, "Delete Content performed by user", userId, content.Id); } /// /// Permanently deletes versions from an object prior to a specific date. /// /// 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) { //TODO: We should check if we are going to delete the most recent version because if that happens it means the // entity is completely deleted and we should raise the normal Deleting/Deleted event 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.Add(AuditTypes.Delete, "Delete Content by version date performed by user", userId, -1); } /// /// Permanently deletes specific version(s) from an object. /// /// 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) { //TODO: We should check if we are going to delete the most recent version because if that happens it means the // entity is completely deleted and we should raise the normal Deleting/Deleted event if (deletePriorVersions) { var content = GetByVersion(versionId); DeleteVersions(id, content.UpdateDate, userId); } if (DeletingVersions.IsRaisedEventCancelled(new DeleteRevisionsEventArgs(id, specificVersion: versionId), this)) return; 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.Add(AuditTypes.Delete, "Delete Content by version performed by user", userId, -1); } /// /// 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) { if (Trashing.IsRaisedEventCancelled(new MoveEventArgs(content, -20), this)) return; //Make sure that published content is unpublished before being moved to the Recycle Bin if (HasPublishedVersion(content.Id)) { UnPublish(content, userId); } //Unpublish descendents of the content item that is being moved to trash var descendants = GetDescendants(content).ToList(); foreach (var descendant in descendants) { 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) { descendant.WriterId = userId; descendant.ChangeTrashedState(true, descendant.ParentId); repository.AddOrUpdate(descendant); } uow.Commit(); } Trashed.RaiseEvent(new MoveEventArgs(content, false, -20), this); Audit.Add(AuditTypes.Move, "Move Content to Recycle Bin performed by user", userId, content.Id); } /// /// 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) { //This ensures that the correct method is called if this method is used to Move to recycle bin. if (parentId == -20) { MoveToRecycleBin(content, userId); return; } if (Moving.IsRaisedEventCancelled(new MoveEventArgs(content, parentId), this)) return; content.WriterId = userId; 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 != -20) { 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)) { SaveAndPublish(content, userId); } else { Save(content, false, userId, true); using (var uow = _uowProvider.GetUnitOfWork()) { var xml = content.ToXml(); var poco = new ContentXmlDto {NodeId = content.Id, Xml = xml.ToString(SaveOptions.None)}; 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 { Save(content, userId); } //Ensure that Path and Level is updated on children var children = GetChildren(content.Id); if (children.Any()) { foreach (var child in children) { Move(child, content.Id, userId); } } Moved.RaiseEvent(new MoveEventArgs(content, false, parentId), this); Audit.Add(AuditTypes.Move, "Move Content performed by user", userId, content.Id); } /// /// Empties the Recycle Bin by deleting all that resides in the bin /// public void EmptyRecycleBin() { //TODO: Why don't we have a base class to share between MediaService/ContentService as some of this is exacty the same? var uow = _uowProvider.GetUnitOfWork(); using (var repository = _repositoryFactory.CreateContentRepository(uow)) { var query = Query.Builder.Where(x => x.ParentId == -20); var contents = repository.GetByQuery(query); foreach (var content in contents) { if (Deleting.IsRaisedEventCancelled(new DeleteEventArgs(content), this)) continue; repository.Delete(content); Deleted.RaiseEvent(new DeleteEventArgs(content, false), this); } uow.Commit(); } Audit.Add(AuditTypes.Delete, "Empty Recycle Bin performed by user", 0, -20); } /// /// 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 /// Optional Id of the User copying the Content /// The newly created object public IContent Copy(IContent content, int parentId, bool relateToOriginal, int userId = 0) { var copy = ((Content)content).Clone(); 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)) { content.WriterId = userId; repository.AddOrUpdate(copy); uow.Commit(); //Special case for the Upload DataType var uploadDataTypeId = new Guid("5032a6e6-69e3-491d-bb28-cd31cd11086c"); if (content.Properties.Any(x => x.PropertyType.DataTypeId == uploadDataTypeId)) { bool isUpdated = false; var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); //Loop through properties to check if the content contains media that should be deleted foreach (var property in content.Properties.Where(x => x.PropertyType.DataTypeId == uploadDataTypeId && string.IsNullOrEmpty(x.Value.ToString()) == false)) { if (fs.FileExists(IOHelper.MapPath(property.Value.ToString()))) { var currentPath = fs.GetRelativePath(property.Value.ToString()); var propertyId = copy.Properties.First(x => x.Alias == property.Alias).Id; var newPath = fs.GetRelativePath(propertyId, System.IO.Path.GetFileName(currentPath)); fs.CopyFile(currentPath, newPath); copy.SetValue(property.Alias, fs.GetUrl(newPath)); //Copy thumbnails foreach (var thumbPath in fs.GetThumbnails(currentPath)) { var newThumbPath = fs.GetRelativePath(propertyId, System.IO.Path.GetFileName(thumbPath)); fs.CopyFile(thumbPath, newThumbPath); } isUpdated = true; } } if (isUpdated) { repository.AddOrUpdate(copy); uow.Commit(); } } //Special case for the Tags DataType var tagsDataTypeId = new Guid("4023e540-92f5-11dd-ad8b-0800200c9a66"); if (content.Properties.Any(x => x.PropertyType.DataTypeId == tagsDataTypeId)) { 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}); } } } //NOTE This 'Relation' part should eventually be delegated to a RelationService if (relateToOriginal) { RelationType relationType = null; using (var relationTypeRepository = _repositoryFactory.CreateRelationTypeRepository(uow)) { relationType = relationTypeRepository.Get(1); } using (var relationRepository = _repositoryFactory.CreateRelationRepository(uow)) { var relation = new Relation(content.Id, copy.Id, relationType); relationRepository.AddOrUpdate(relation); uow.Commit(); } Audit.Add(AuditTypes.Copy, string.Format("Copied content with Id: '{0}' related to original content with Id: '{1}'", copy.Id, content.Id), copy.WriterId, copy.Id); } //Look for children and copy those as well var children = GetChildren(content.Id); foreach (var child in children) { Copy(child, copy.Id, relateToOriginal, userId); } Copied.RaiseEvent(new CopyEventArgs(content, copy, false, parentId), this); Audit.Add(AuditTypes.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 internal bool SendToPublication(IContent content, int userId = 0) { if (SendingToPublish.IsRaisedEventCancelled(new SendToPublishEventArgs(content), this)) return false; //TODO: Do some stuff here.. RunActionHandlers SentToPublish.RaiseEvent(new SendToPublishEventArgs(content, false), this); Audit.Add(AuditTypes.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; repository.AddOrUpdate(content); uow.Commit(); } RolledBack.RaiseEvent(new RollbackEventArgs(content, false), this); Audit.Add(AuditTypes.RollBack, "Content rollback performed by user", content.WriterId, content.Id); return content; } #region Internal Methods /// /// Internal method to Re-Publishes all Content for legacy purposes. /// /// Optional Id of the User issueing the publishing /// Optional boolean to avoid having the cache refreshed when calling this RePublish method. By default this method will not update the cache. /// True if publishing succeeded, otherwise False internal bool RePublishAll(bool omitCacheRefresh = true, int userId = 0) { return RePublishAllDo(omitCacheRefresh, userId); } /// /// Internal method that Publishes a single object for legacy purposes. /// /// The to publish /// Optional boolean to avoid having the cache refreshed when calling this Publish method. By default this method will not update the cache. /// Optional Id of the User issueing the publishing /// True if publishing succeeded, otherwise False internal bool Publish(IContent content, bool omitCacheRefresh = true, int userId = 0) { return SaveAndPublishDo(content, omitCacheRefresh, userId); } /// /// Internal method that Publishes a object and all its children for legacy purposes. /// /// The to publish along with its children /// Optional boolean to avoid having the cache refreshed when calling this Publish method. By default this method will not update the cache. /// Optional Id of the User issueing the publishing /// True if publishing succeeded, otherwise False internal bool PublishWithChildren(IContent content, bool omitCacheRefresh = true, int userId = 0) { return PublishWithChildrenDo(content, omitCacheRefresh, userId); } /// /// Internal method that UnPublishes a single object for legacy purposes. /// /// The to publish /// Optional boolean to avoid having the cache refreshed when calling this Unpublish method. By default this method will not update the cache. /// Optional Id of the User issueing the publishing /// True if unpublishing succeeded, otherwise False internal bool UnPublish(IContent content, bool omitCacheRefresh = true, int userId = 0) { return UnPublishDo(content, omitCacheRefresh, userId); } /// /// Saves and Publishes a single object /// /// The to save and publish /// Optional boolean to avoid having the cache refreshed when calling this Publish method. By default this method will not update the cache. /// Optional Id of the User issueing the publishing /// Optional boolean indicating whether or not to raise save events. /// True if publishing succeeded, otherwise False internal bool SaveAndPublish(IContent content, bool omitCacheRefresh = true, int userId = 0, bool raiseEvents = true) { return SaveAndPublishDo(content, omitCacheRefresh, userId, raiseEvents); } /// /// 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 = Query.Builder.Where(x => x.Id != content.Id && x.Path.StartsWith(content.Path) && x.Published == true && x.Trashed == false); var contents = repository.GetByQuery(query); return contents; } } #endregion #region Private Methods /// /// Re-Publishes all Content /// /// Optional Id of the User issueing the publishing /// Optional boolean to avoid having the cache refreshed when calling this RePublish method. By default this method will update the cache. /// True if publishing succeeded, otherwise False private bool RePublishAllDo(bool omitCacheRefresh = false, int userId = 0) { var list = new List(); var updated = new List(); //Consider creating a Path query instead of recursive method: //var query = Query.Builder.Where(x => x.Path.StartsWith("-1")); var rootContent = GetRootContent(); foreach (var content in rootContent) { if (content.IsValid()) { list.Add(content); list.AddRange(GetDescendants(content)); } } //Publish and then update the database with new status var published = _publishingStrategy.PublishWithChildren(list, userId); if (published) { var uow = _uowProvider.GetUnitOfWork(); using (var repository = _repositoryFactory.CreateContentRepository(uow)) { //Only loop through content where the Published property has been updated foreach (var item in list.Where(x => ((ICanBeDirty)x).IsPropertyDirty("Published"))) { item.WriterId = userId; repository.AddOrUpdate(item); updated.Add(item); } uow.Commit(); foreach (var c in updated) { var xml = c.ToXml(); var poco = new ContentXmlDto { NodeId = c.Id, Xml = xml.ToString(SaveOptions.None) }; var exists = uow.Database.FirstOrDefault("WHERE nodeId = @Id", new { Id = c.Id }) != null; int result = exists ? uow.Database.Update(poco) : Convert.ToInt32(uow.Database.Insert(poco)); } } //Updating content to published state is finished, so we fire event through PublishingStrategy to have cache updated if (omitCacheRefresh == false) _publishingStrategy.PublishingFinalized(updated, true); } Audit.Add(AuditTypes.Publish, "RePublish All performed by user", userId, -1); return published; } /// /// Publishes a object and all its children /// /// The to publish along with its children /// Optional boolean to avoid having the cache refreshed when calling this Publish method. By default this method will update the cache. /// Optional Id of the User issueing the publishing /// True if publishing succeeded, otherwise False private bool PublishWithChildrenDo(IContent content, bool omitCacheRefresh = false, int userId = 0) { //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 != -1 && content.ParentId != -20 && IsPublishable(content) == false) { LogHelper.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)); return false; } //Content contains invalid property values and can therefore not be published - fire event? if (!content.IsValid()) { LogHelper.Info( string.Format("Content '{0}' with Id '{1}' could not be published because of invalid properties.", content.Name, content.Id)); return false; } //Consider creating a Path query instead of recursive method: //var query = Query.Builder.Where(x => x.Path.StartsWith(content.Path)); var updated = new List(); var list = new List(); list.Add(content); list.AddRange(GetDescendants(content)); //Publish and then update the database with new status var published = _publishingStrategy.PublishWithChildren(list, userId); if (published) { var uow = _uowProvider.GetUnitOfWork(); using (var repository = _repositoryFactory.CreateContentRepository(uow)) { //Only loop through content where the Published property has been updated foreach (var item in list.Where(x => ((ICanBeDirty)x).IsPropertyDirty("Published"))) { item.WriterId = userId; repository.AddOrUpdate(item); updated.Add(item); } uow.Commit(); foreach (var c in updated) { var xml = c.ToXml(); var poco = new ContentXmlDto { NodeId = c.Id, Xml = xml.ToString(SaveOptions.None) }; var exists = uow.Database.FirstOrDefault("WHERE nodeId = @Id", new { Id = c.Id }) != null; int result = exists ? uow.Database.Update(poco) : Convert.ToInt32(uow.Database.Insert(poco)); } } //Save xml to db and call following method to fire event: if (omitCacheRefresh == false) _publishingStrategy.PublishingFinalized(updated, false); Audit.Add(AuditTypes.Publish, "Publish with Children performed by user", userId, content.Id); } return published; } /// /// 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 bool UnPublishDo(IContent content, bool omitCacheRefresh = false, int userId = 0) { var unpublished = _publishingStrategy.UnPublish(content, userId); if (unpublished) { var uow = _uowProvider.GetUnitOfWork(); using (var repository = _repositoryFactory.CreateContentRepository(uow)) { content.WriterId = userId; repository.AddOrUpdate(content); //Remove 'published' xml from the cmsContentXml table for the unpublished content uow.Database.Delete("WHERE nodeId = @Id", new { Id = content.Id }); 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.Add(AuditTypes.UnPublish, "UnPublish performed by user", userId, content.Id); } return unpublished; } /// /// Saves and Publishes a single object /// /// The to save and publish /// Optional boolean to avoid having the cache refreshed when calling this Publish method. By default this method will update the cache. /// 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 bool SaveAndPublishDo(IContent content, bool omitCacheRefresh = false, int userId = 0, bool raiseEvents = true) { if(raiseEvents) { if (Saving.IsRaisedEventCancelled(new SaveEventArgs(content), this)) return false; } //Has this content item previously been published? If so, we don't need to refresh the children var previouslyPublished = HasPublishedVersion(content.Id); //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 != -1 && content.ParentId != -20 && IsPublishable(content) == false) { LogHelper.Info( string.Format( "Content '{0}' with Id '{1}' could not be published because its parent is not published.", content.Name, content.Id)); return false; } //Content contains invalid property values and can therefore not be published - fire event? if (!content.IsValid()) { LogHelper.Info( string.Format( "Content '{0}' with Id '{1}' could not be published because of invalid properties.", content.Name, content.Id)); return false; } //Publish and then update the database with new status bool published = _publishingStrategy.Publish(content, userId); 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 content.WriterId = userId; repository.AddOrUpdate(content); uow.Commit(); if (published) { var xml = content.ToXml(); var poco = new ContentXmlDto { NodeId = content.Id, Xml = xml.ToString(SaveOptions.None) }; 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)); } } if(raiseEvents) Saved.RaiseEvent(new SaveEventArgs(content, false), this); //Save xml to db and call following method to fire event through PublishingStrategy to update cache if (omitCacheRefresh == false) _publishingStrategy.PublishingFinalized(content); //We need to check if children and their publish state to ensure that we 'republish' content that was previously published if (omitCacheRefresh == false && previouslyPublished == false && HasChildren(content.Id)) { var descendants = GetPublishedDescendants(content); _publishingStrategy.PublishingFinalized(descendants, false); } Audit.Add(AuditTypes.Publish, "Save and Publish performed by user", userId, content.Id); return published; } /// /// 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 void Save(IContent content, bool changeState, int userId = 0, bool raiseEvents = true) { if(raiseEvents) { if (Saving.IsRaisedEventCancelled(new SaveEventArgs(content), this)) return; } var uow = _uowProvider.GetUnitOfWork(); using (var repository = _repositoryFactory.CreateContentRepository(uow)) { content.WriterId = userId; //Only change the publish state if the "previous" version was actually published if (changeState && content.Published) content.ChangePublishedState(PublishedState.Saved); repository.AddOrUpdate(content); uow.Commit(); } if(raiseEvents) Saved.RaiseEvent(new SaveEventArgs(content, false), this); Audit.Add(AuditTypes.Save, "Save Content performed by user", userId, content.Id); } /// /// 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 == -20) return false; //We don't check the System Root, so just continue if (id == -1) 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; } #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 /// public static event TypedEventHandler> Creating; /// /// Occurs after Create /// /// /// Please note that the Content object has been created, but not 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; #endregion } }