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 IUserService _userService; private readonly RepositoryFactory _repositoryFactory; private HttpContextBase _httpContext; 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()) { } internal ContentService(IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, IPublishingStrategy publishingStrategy) { _uowProvider = provider; _publishingStrategy = publishingStrategy; _repositoryFactory = repositoryFactory; } internal ContentService(IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, IPublishingStrategy publishingStrategy, IUserService userService) { _uowProvider = provider; _publishingStrategy = publishingStrategy; _userService = userService; _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 = -1) { 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; SetUser(content, userId); SetWriter(content, userId); Created.RaiseEvent(new NewEventArgs(content, false, contentTypeAlias, parentId), 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)); 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); 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 /// 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 public bool RePublishAll(int userId = -1, bool omitCacheRefresh = false) { //TODO Refactor this so omitCacheRefresh isn't exposed in the public method, but only in an internal one as its purely there for legacy reasons. 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(GetChildrenDeep(content.Id)); } } //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"))) { SetWriter(item, 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 ? 0 : userId, -1); return published; } /// /// Publishes a single object /// /// The to publish /// Optional Id of the User issueing the publishing /// Optional boolean to avoid having the cache refreshed when calling this Publish method. By default this method will update the cache. /// True if publishing succeeded, otherwise False public bool Publish(IContent content, int userId = -1, bool omitCacheRefresh = false) { //TODO Refactor this so omitCacheRefresh isn't exposed in the public method, but only in an internal one as its purely there for legacy reasons. return SaveAndPublish(content, userId, omitCacheRefresh); } /// /// Publishes a object and all its children /// /// The to publish along with its children /// Optional Id of the User issueing the publishing /// Optional boolean to avoid having the cache refreshed when calling this Publish method. By default this method will update the cache. /// True if publishing succeeded, otherwise False public bool PublishWithChildren(IContent content, int userId = -1, bool omitCacheRefresh = false) { //TODO Refactor this so omitCacheRefresh isn't exposed in the public method, but only in an internal one as its purely there for legacy reasons. //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(GetChildrenDeep(content.Id)); //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"))) { SetWriter(item, 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 == -1 ? 0 : userId, content.Id); } return published; } /// /// UnPublishes a single object /// /// The to publish /// Optional Id of the User issueing the publishing /// Optional boolean to avoid having the cache refreshed when calling this Unpublish method. By default this method will update the cache. /// True if unpublishing succeeded, otherwise False public bool UnPublish(IContent content, int userId = -1, bool omitCacheRefresh = false) { //TODO Refactor this so omitCacheRefresh isn't exposed in the public method, but only in an internal one as its purely there for legacy reasons. var unpublished = _publishingStrategy.UnPublish(content, userId); if (unpublished) { var uow = _uowProvider.GetUnitOfWork(); using (var repository = _repositoryFactory.CreateContentRepository(uow)) { 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 == -1 ? 0 : userId, content.Id); } return unpublished; } /// /// Saves and Publishes a single object /// /// The to save and publish /// Optional Id of the User issueing the publishing /// Optional boolean to avoid having the cache refreshed when calling this Publish method. By default this method will update the cache. /// True if publishing succeeded, otherwise False public bool SaveAndPublish(IContent content, int userId = -1, bool omitCacheRefresh = false) { //TODO Refactor this so omitCacheRefresh isn't exposed in the public method, but only in an internal one as its purely there for legacy reasons. if (Saving.IsRaisedEventCancelled(new SaveEventArgs(content), this)) return false; //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 SetWriter(content, 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)); } } 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 (HasChildren(content.Id)) { var children = GetChildrenDeep(content.Id); var shouldBeRepublished = children.Where(child => HasPublishedVersion(child.Id)); if (omitCacheRefresh == false) _publishingStrategy.PublishingFinalized(shouldBeRepublished, false); } Audit.Add(AuditTypes.Publish, "Save and Publish performed by user", userId == -1 ? 0 : userId, content.Id); return published; } /// /// Saves a single object /// /// The to save /// Optional Id of the User saving the Content public void Save(IContent content, int userId = -1) { if (Saving.IsRaisedEventCancelled(new SaveEventArgs(content), this)) return; var uow = _uowProvider.GetUnitOfWork(); using (var repository = _repositoryFactory.CreateContentRepository(uow)) { SetWriter(content, 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(); } Saved.RaiseEvent(new SaveEventArgs(content, false), this); Audit.Add(AuditTypes.Save, "Save Content performed by user", userId == -1 ? 0 : userId, content.Id); } /// /// 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 public void Save(IEnumerable contents, int userId = -1) { 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) { SetWriter(content, 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) { SetWriter(content, userId); repository.AddOrUpdate(content); } uow.Commit(); } } 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 = -1) { 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 ? 0 : 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 = -1) { 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)) { //TODO: Why are we setting a writer if we are just deleting the object? (I'm probably overlooking something here...?) SetWriter(content, userId); repository.Delete(content); uow.Commit(); } Deleted.RaiseEvent(new DeleteEventArgs(content, false), this); Audit.Add(AuditTypes.Delete, "Delete Content performed by user", userId == -1 ? 0 : 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 = -1) { //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 ? 0 : 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 = -1) { //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 ? 0 : 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 = -1) { 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); } //Move children to Recycle Bin before the 'possible parent' is moved there var children = GetChildren(content.Id); foreach (var child in children) { MoveToRecycleBin(child, userId); } var uow = _uowProvider.GetUnitOfWork(); using (var repository = _repositoryFactory.CreateContentRepository(uow)) { SetWriter(content, userId); content.ChangeTrashedState(true); repository.AddOrUpdate(content); uow.Commit(); } Trashed.RaiseEvent(new MoveEventArgs(content, false, -20), this); Audit.Add(AuditTypes.Move, "Move Content to Recycle Bin performed by user", userId == -1 ? 0 : 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 = -1) { //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; SetWriter(content, userId); //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) { SaveAndPublish(content, userId); } else { Save(content, userId); } Moved.RaiseEvent(new MoveEventArgs(content, false, parentId), this); Audit.Add(AuditTypes.Move, "Move Content performed by user", userId == -1 ? 0 : 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 = -1) { var copy = ((Content)content).Clone(); copy.ParentId = parentId; // A copy should never be set to published // automatically even if the original was this.UnPublish(copy); if (Copying.IsRaisedEventCancelled(new CopyEventArgs(content, copy, parentId), this)) return null; var uow = _uowProvider.GetUnitOfWork(); using (var repository = _repositoryFactory.CreateContentRepository(uow)) { SetWriter(content, userId); repository.AddOrUpdate(copy); uow.Commit(); var uploadFieldId = new Guid("5032a6e6-69e3-491d-bb28-cd31cd11086c"); if (content.Properties.Any(x => x.PropertyType.DataTypeId == uploadFieldId)) { 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 == uploadFieldId && 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(); } } } //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 = -1) { 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 = -1) { var content = GetByVersion(versionId); if (RollingBack.IsRaisedEventCancelled(new RollbackEventArgs(content), this)) return content; var uow = _uowProvider.GetUnitOfWork(); using (var repository = _repositoryFactory.CreateContentRepository(uow)) { SetUser(content, userId); SetWriter(content, 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 set the HttpContextBase for testing. /// /// internal void SetHttpContext(HttpContextBase httpContext) { _httpContext = httpContext; } #endregion #region Private Methods /// /// Gets a flat list of decendents of content from parent id /// /// /// Only contains valid objects, which means /// that everything in the returned list can be published. /// If an invalid object is found it will not /// be added to the list neither will its children. /// /// Id of the parent to retrieve children from /// A list of valid that can be published private IEnumerable GetChildrenDeep(int parentId) { var list = new List(); var children = GetChildren(parentId); foreach (var child in children) { if (child.IsValid()) { list.Add(child); list.AddRange(GetChildrenDeep(child.Id)); } } return list; } /// /// 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; } /// /// Updates a content object with the User (id), who created the content. /// /// object to update /// Optional Id of the User private void SetUser(IContent content, int userId) { if (userId > -1) { //If a user id was passed in we use that content.CreatorId = userId; } else if (UserServiceOrContext()) { var profile = _httpContext == null ? _userService.GetCurrentBackOfficeUser() : _userService.GetCurrentBackOfficeUser(_httpContext); content.CreatorId = profile.Id.SafeCast(); } else { //Otherwise we default to Admin user, which should always exist (almost always) content.CreatorId = 0; } } /// /// Updates a content object with a Writer (user id), who updated the content. /// /// object to update /// Optional Id of the Writer private void SetWriter(IContent content, int userId) { if (userId > -1) { //If a user id was passed in we use that content.WriterId = userId; } else if (UserServiceOrContext()) { var profile = _httpContext == null ? _userService.GetCurrentBackOfficeUser() : _userService.GetCurrentBackOfficeUser(_httpContext); content.WriterId = profile.Id.SafeCast(); } else { //Otherwise we default to Admin user, which should always exist (almost always) content.WriterId = 0; } } private bool UserServiceOrContext() { return _userService != null && (HttpContext.Current != null || _httpContext != null); } #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 } }