From 7d0d5b9c939a9d9578c9e112a2f1d90bac6df777 Mon Sep 17 00:00:00 2001 From: Shannon Deminick Date: Thu, 19 Sep 2019 18:46:02 +1000 Subject: [PATCH 01/17] U4-9912 Previewing without changing creates new version (#1943) --- src/Umbraco.Core/Services/ContentService.cs | 5894 +++++++++--------- src/Umbraco.Web/Editors/ContentController.cs | 2442 ++++---- 2 files changed, 4172 insertions(+), 4164 deletions(-) diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 2994bcc4f6..0b50eb1f65 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -1,2945 +1,2949 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text.RegularExpressions; -using System.Threading; -using System.Xml; -using System.Xml.Linq; -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.UnitOfWork; -using Umbraco.Core.Publishing; - -namespace Umbraco.Core.Services -{ - /// - /// Represents the Content Service, which is an easy access to operations involving - /// - public class ContentService : ScopeRepositoryService, IContentService, IContentServiceOperations - { - private readonly IPublishingStrategy2 _publishingStrategy; - private readonly EntityXmlSerializer _entitySerializer = new EntityXmlSerializer(); - private readonly IDataTypeService _dataTypeService; - private readonly IUserService _userService; - - //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); - private static readonly Regex AnchorRegex = new Regex("", RegexOptions.Compiled); - - public ContentService( - IDatabaseUnitOfWorkProvider provider, - RepositoryFactory repositoryFactory, - ILogger logger, - IEventMessagesFactory eventMessagesFactory, - IDataTypeService dataTypeService, - IUserService userService) - : base(provider, repositoryFactory, logger, eventMessagesFactory) - { - if (dataTypeService == null) throw new ArgumentNullException("dataTypeService"); - if (userService == null) throw new ArgumentNullException("userService"); - _publishingStrategy = new PublishingStrategy(UowProvider.ScopeProvider, eventMessagesFactory, logger); - _dataTypeService = dataTypeService; - _userService = userService; - } - - #region Static Queries - - private IQuery _notTrashedQuery; - - #endregion - - public int CountPublished(string contentTypeAlias = null) - { - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateContentRepository(uow); - return repository.CountPublished(contentTypeAlias); - } - } - - public int Count(string contentTypeAlias = null) - { - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateContentRepository(uow); - return repository.Count(contentTypeAlias); - } - } - - public int CountChildren(int parentId, string contentTypeAlias = null) - { - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateContentRepository(uow); - return repository.CountChildren(parentId, contentTypeAlias); - } - } - - public int CountDescendants(int parentId, string contentTypeAlias = null) - { - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - 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) - { - using (var uow = UowProvider.GetUnitOfWork()) - { - var repository = RepositoryFactory.CreateContentRepository(uow); - repository.ReplaceContentPermissions(permissionSet); - uow.Commit(); - } - } - - /// - /// Assigns a single permission to the current content item for the specified group ids - /// - /// - /// - /// - public void AssignContentPermission(IContent entity, char permission, IEnumerable groupIds) - { - using (var uow = UowProvider.GetUnitOfWork()) - { - var repository = RepositoryFactory.CreateContentRepository(uow); - repository.AssignEntityPermission(entity, permission, groupIds); - uow.Commit(); - } - } - - /// - /// Returns implicit/inherited permissions assigned to the content item for all user groups - /// - /// - /// - public EntityPermissionCollection GetPermissionsForEntity(IContent content) - { - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - 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, Guid parentId, string contentTypeAlias, int userId = 0) - { - var parent = GetById(parentId); - return CreateContent(name, parent, contentTypeAlias, userId); - } - - /// - /// 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); - - using (var uow = UowProvider.GetUnitOfWork()) - { - var newEventArgs = new NewEventArgs(content, contentTypeAlias, parentId); - if (uow.Events.DispatchCancelable(Creating, this, newEventArgs)) - { - uow.Commit(); - content.WasCancelled = true; - return content; - } - - content.CreatorId = userId; - content.WriterId = userId; - newEventArgs.CanCancel = false; - uow.Events.Dispatch(Created, this, newEventArgs); - - 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); - - using (var uow = UowProvider.GetUnitOfWork()) - { - var newEventArgs = new NewEventArgs(content, contentTypeAlias, parent); - if (uow.Events.DispatchCancelable(Creating, this, newEventArgs)) - { - uow.Commit(); - content.WasCancelled = true; - return content; - } - - content.CreatorId = userId; - content.WriterId = userId; - newEventArgs.CanCancel = false; - uow.Events.Dispatch(Created, this, newEventArgs); - - uow.Commit(); - } - - 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); - - using (var uow = UowProvider.GetUnitOfWork()) - { - //NOTE: I really hate the notion of these Creating/Created events - they are so inconsistent, I've only just found - // out that in these 'WithIdentity' methods, the Saving/Saved events were not fired, wtf. Anyways, they're added now. - var newEventArgs = new NewEventArgs(content, contentTypeAlias, parentId); - if (uow.Events.DispatchCancelable(Creating, this, newEventArgs)) - { - uow.Commit(); - content.WasCancelled = true; - return content; - } - - var saveEventArgs = new SaveEventArgs(content); - if (uow.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) - { - uow.Commit(); - content.WasCancelled = true; - return content; - } - - var repository = RepositoryFactory.CreateContentRepository(uow); - content.CreatorId = userId; - content.WriterId = userId; - - repository.AddOrUpdate(content); - repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); - saveEventArgs.CanCancel = false; - uow.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); - newEventArgs.CanCancel = false; - uow.Events.Dispatch(Created, this, newEventArgs); - - Audit(uow, AuditType.New, string.Format("Content '{0}' was created with Id {1}", name, content.Id), content.CreatorId, content.Id); - uow.Commit(); - } - - return content; - } - - public IEnumerable GetAnchorValuesFromRTEs(int id) - { - var result = new List(); - - var content = GetById(id); - - foreach (var contentProperty in content.Properties) - { - if (contentProperty.PropertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.TinyMCEAlias)) - { - var value = contentProperty.Value?.ToString(); - - if (!string.IsNullOrEmpty(value)) - { - result.AddRange(GetAnchorValuesFromRTEContent(value)); - } - - } - } - - - return result; - } - - - public IEnumerable GetAnchorValuesFromRTEContent(string rteContent) - { - var result = new List(); - - var matches = AnchorRegex.Matches(rteContent); - - foreach (Match match in matches) - { - result.Add(match.Value.Split('\"')[1]); - } - - return result; - } - - /// - /// 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); - - using (var uow = UowProvider.GetUnitOfWork()) - { - //NOTE: I really hate the notion of these Creating/Created events - they are so inconsistent, I've only just found - // out that in these 'WithIdentity' methods, the Saving/Saved events were not fired, wtf. Anyways, they're added now. - var newEventArgs = new NewEventArgs(content, contentTypeAlias, parent); - if (uow.Events.DispatchCancelable(Creating, this, newEventArgs)) - { - uow.Commit(); - content.WasCancelled = true; - return content; - } - - var saveEventArgs = new SaveEventArgs(content); - if (uow.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) - { - uow.Commit(); - content.WasCancelled = true; - return content; - } - - var repository = RepositoryFactory.CreateContentRepository(uow); - content.CreatorId = userId; - content.WriterId = userId; - - repository.AddOrUpdate(content); - repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); - saveEventArgs.CanCancel = false; - uow.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); - newEventArgs.CanCancel = false; - uow.Events.Dispatch(Created, this, newEventArgs); - - Audit(uow, AuditType.New, string.Format("Content '{0}' was created with Id {1}", name, content.Id), content.CreatorId, content.Id); - uow.Commit(); - } - - return content; - } - - /// - /// Gets an object by Id - /// - /// Id of the Content to retrieve - /// - public IContent GetById(int id) - { - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateContentRepository(uow); - return repository.Get(id); - } - } - - /// - /// Gets objects by Ids - /// - /// Ids of the Content to retrieve - /// - public IEnumerable GetByIds(IEnumerable ids) - { - var idsArray = ids.ToArray(); - if (idsArray.Length == 0) return Enumerable.Empty(); - - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateContentRepository(uow); - - // ensure that the result has the order based on the ids passed in - var result = repository.GetAll(idsArray); - var content = result.ToDictionary(x => x.Id, x => x); - - var sortedResult = idsArray.Select(x => - { - IContent c; - return content.TryGetValue(x, out c) ? c : null; - }).WhereNotNull(); - - return sortedResult; - } - } - - /// - /// Gets objects by Ids - /// - /// Ids of the Content to retrieve - /// - public IEnumerable GetByIds(IEnumerable ids) - { - var idsArray = ids.ToArray(); - if (idsArray.Length == 0) return Enumerable.Empty(); - - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateContentRepository(uow); - - // ensure that the result has the order based on the ids passed in - var result = repository.GetAll(idsArray); - var content = result.ToDictionary(x => x.Key, x => x); - - var sortedResult = idsArray.Select(x => - { - IContent c; - return content.TryGetValue(x, out c) ? c : null; - }).WhereNotNull(); - - return sortedResult; - } - } - - /// - /// Gets an object by its 'UniqueId' - /// - /// Guid key of the Content to retrieve - /// - public IContent GetById(Guid key) - { - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateContentRepository(uow); - return repository.Get(key); - } - } - - /// - /// Gets a collection of objects by the Id of the - /// - /// Id of the - /// An Enumerable list of objects - public IEnumerable GetContentOfContentType(int id) - { - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateContentRepository(uow); - var query = Query.Builder.Where(x => x.ContentTypeId == id); - return repository.GetByQuery(query); - } - } - - internal IEnumerable GetPublishedContentOfContentType(int id) - { - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateContentRepository(uow); - var query = Query.Builder.Where(x => x.ContentTypeId == id); - return repository.GetByPublishedVersion(query); - } - } - - /// - /// 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 uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateContentRepository(uow); - var query = Query.Builder.Where(x => x.Level == level && x.Path.StartsWith(Constants.System.RecycleBinContent.ToInvariantString()) == false); - return repository.GetByQuery(query); - } - } - - /// - /// Gets a specific version of an item. - /// - /// Id of the version to retrieve - /// An item - public IContent GetByVersion(Guid versionId) - { - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateContentRepository(uow); - 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 uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateContentRepository(uow); - return repository.GetAllVersions(id); - } - } - - /// - /// Gets a list of all version Ids for the given content item ordered so latest is first - /// - /// - /// The maximum number of rows to return - /// - public IEnumerable GetVersionIds(int id, int maxRows) - { - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateContentRepository(uow); - return repository.GetVersionIds(id, maxRows); - } - } - - /// - /// 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 uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateContentRepository(uow); - 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 uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateContentRepository(uow); - var query = Query.Builder.Where(x => x.ParentId == id); - return repository.GetByQuery(query).OrderBy(x => x.SortOrder); - } - } - - [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 = "") - { - long total; - var result = GetPagedChildren(id, Convert.ToInt64(pageIndex), pageSize, out total, orderBy, orderDirection, true, filter); - totalChildren = Convert.ToInt32(total); - return result; - } - - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Children from - /// Page index (zero based) - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Search text filter - /// An Enumerable list of objects - public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, - string orderBy, Direction orderDirection, string filter = "") - { - return GetPagedChildren(id, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, true, filter); - } - - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Children from - /// Page index (zero based) - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Flag to indicate when ordering by system field - /// Search text filter - /// An Enumerable list of objects - public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, - string orderBy, Direction orderDirection, bool orderBySystemField, string filter) - { - Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); - Mandate.ParameterCondition(pageSize > 0, "pageSize"); - - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateContentRepository(uow); - - var query = Query.Builder; - // always check for a parent - else it will also get decendants (and then you should use the GetPagedDescendants method) - query.Where(x => x.ParentId == id); - - IQuery filterQuery = null; - if (filter.IsNullOrWhiteSpace() == false) - { - filterQuery = Query.Builder.Where(x => x.Name.Contains(filter)); - } - return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filterQuery); - } - } - - [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 = "") - { - long total; - var result = GetPagedDescendants(id, Convert.ToInt64(pageIndex), pageSize, out total, orderBy, orderDirection, true, filter); - totalChildren = Convert.ToInt32(total); - return result; - } - - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Descendants from - /// Page number - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Search text filter - /// An Enumerable list of objects - public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = "") - { - return GetPagedDescendants(id, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, true, filter); - } - - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Descendants from - /// Page number - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Flag to indicate when ordering by system field - /// Search text filter - /// An Enumerable list of objects - public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy, Direction orderDirection, bool orderBySystemField, string filter) - { - Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); - Mandate.ParameterCondition(pageSize > 0, "pageSize"); - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateContentRepository(uow); - - // get query - if the id is System Root, then just get all - var query = Query.Builder; - if (id != Constants.System.Root) - { - var entityRepository = RepositoryFactory.CreateEntityRepository(uow); - var contentPath = entityRepository.GetAllPaths(Constants.ObjectTypes.DocumentGuid, id).ToArray(); - if (contentPath.Length == 0) - { - totalChildren = 0; - return Enumerable.Empty(); - } - query.Where(x => x.Path.SqlStartsWith(string.Format("{0},", contentPath[0].Path), TextColumnType.NVarchar)); - } - - - // get filter - IQuery filterQuery = null; - if (filter.IsNullOrWhiteSpace() == false) - filterQuery = Query.Builder.Where(x => x.Name.Contains(filter)); - - return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filterQuery); - } - } - - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Descendants from - /// Page number - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Flag to indicate when ordering by system field - /// Search filter - /// An Enumerable list of objects - public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy, Direction orderDirection, bool orderBySystemField, IQuery filter) - { - Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); - Mandate.ParameterCondition(pageSize > 0, "pageSize"); - - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateContentRepository(uow); - - // get query - if the id is System Root, then just get all - var query = Query.Builder; - if (id != Constants.System.Root) - { - var entityRepository = RepositoryFactory.CreateEntityRepository(uow); - var contentPath = entityRepository.GetAllPaths(Constants.ObjectTypes.DocumentGuid, id).ToArray(); - if (contentPath.Length == 0) - { - totalChildren = 0; - return Enumerable.Empty(); - } - query.Where(x => x.Path.SqlStartsWith(string.Format("{0},", contentPath[0].Path), TextColumnType.NVarchar)); - } - - return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); - } - } - - /// - /// Gets a collection of objects by its name or partial name - /// - /// Id of the Parent to retrieve Children from - /// Full or partial name of the children - /// An Enumerable list of objects - public IEnumerable GetChildrenByName(int parentId, string name) - { - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateContentRepository(uow); - - var query = Query.Builder.Where(x => x.ParentId == parentId && x.Name.Contains(name)); - return repository.GetByQuery(query); - } - } - - /// - /// 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 content == null ? Enumerable.Empty() : 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) - { - //This is a check to ensure that the path is correct for this entity to avoid problems like: http://issues.umbraco.org/issue/U4-9336 due to data corruption - if (content.ValidatePath() == false) - throw new InvalidDataException(string.Format("The content item {0} has an invalid path: {1} with parentID: {2}", content.Id, content.Path, content.ParentId)); - - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateContentRepository(uow); - - var pathMatch = content.Path + ","; - var query = Query.Builder.Where(x => x.Path.StartsWith(pathMatch) && x.Id != content.Id); - return repository.GetByQuery(query); - } - } - - /// - /// 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); - } - - /// - /// Gets the published version of a item. - /// - /// The content item. - /// The published version, if any; otherwise, null. - public IContent GetPublishedVersion(IContent content) - { - if (content.Published) return content; - return content.HasPublishedVersion - ? GetByVersion(content.PublishedVersionGuid) - : null; - } - - /// - /// Gets a collection of objects, which reside at the first level / root - /// - /// An Enumerable list of objects - public IEnumerable GetRootContent() - { - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateContentRepository(uow); - - var query = Query.Builder.Where(x => x.ParentId == Constants.System.Root); - return repository.GetByQuery(query); - } - } - - /// - /// Gets all published content items - /// - /// - internal IEnumerable GetAllPublished() - { - //create it once if it is needed (no need for locking here) - if (_notTrashedQuery == null) - { - _notTrashedQuery = Query.Builder.Where(x => x.Trashed == false); - } - - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateContentRepository(uow); - return repository.GetByPublishedVersion(_notTrashedQuery); - } - } - - /// - /// Gets a collection of objects, which has an expiration date less than or equal to today. - /// - /// An Enumerable list of objects - public IEnumerable GetContentForExpiration() - { - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateContentRepository(uow); - var query = Query.Builder.Where(x => x.ExpireDate <= DateTime.Now); - return repository.GetByQuery(query).Where(x => x.HasPublishedVersion); - } - } - - /// - /// Gets a collection of objects, which has a release date less than or equal to today. - /// - /// An Enumerable list of objects - public IEnumerable GetContentForRelease() - { - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateContentRepository(uow); - var query = Query.Builder.Where(x => x.Published == false && x.ReleaseDate <= DateTime.Now); - return repository.GetByQuery(query); - } - } - - /// - /// Gets a collection of an objects, which resides in the Recycle Bin - /// - /// An Enumerable list of objects - public IEnumerable GetContentInRecycleBin() - { - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateContentRepository(uow); - var query = Query.Builder.Where(x => x.Path.StartsWith(Constants.System.RecycleBinContentPathPrefix)); - return repository.GetByQuery(query); - } - } - - /// - /// 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 uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateContentRepository(uow); - var query = Query.Builder.Where(x => x.ParentId == id); - return repository.Count(query); - } - } - - /// - /// Checks whether an item has any published versions - /// - /// Id of the - /// True if the content has any published version otherwise False - public bool HasPublishedVersion(int id) - { - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateContentRepository(uow); - var query = Query.Builder.Where(x => x.Published == true && x.Id == id && x.Trashed == false); - return repository.Count(query) > 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) - { - int[] ids; - if (content.HasIdentity) - { - // get ids from path (we have identity) - // skip the first one that has to be -1 - and we don't care - // skip the last one that has to be "this" - and it's ok to stop at the parent - ids = content.Path.Split(',').Skip(1).SkipLast().Select(int.Parse).ToArray(); - } - else - { - // no path yet (no identity), have to move up to parent - // skip the first one that has to be -1 - and we don't care - // don't skip the last one that is "parent" - var parent = GetById(content.ParentId); - if (parent == null) return false; - ids = parent.Path.Split(',').Skip(1).Select(int.Parse).ToArray(); - } - if (ids.Length == 0) - return false; - - // if the first one is recycle bin, fail fast - if (ids[0] == Constants.System.RecycleBinContent) - return false; - - // fixme - move to repository? - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var sql = new Sql(@" - SELECT id - FROM umbracoNode - JOIN cmsDocument ON umbracoNode.id=cmsDocument.nodeId AND cmsDocument.published=@0 - WHERE umbracoNode.trashed=@1 AND umbracoNode.id IN (@2)", - true, false, ids); - var x = uow.Database.Fetch(sql); - return ids.Length == x.Count; - } - } - - /// - /// 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) - { - return MoveToRecycleBinDo(content, userId, false); - } - - /// - /// 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 - /// - /// A boolean indicating to ignore this item's descendant list from also being moved to the recycle bin. This is required for the DeleteContentOfTypes method - /// because it has already looked up all descendant nodes that will need to be recycled - /// TODO: Fix all of this, it will require a reasonable refactor and most of this stuff should be done at the repo level instead of service sub operations - /// - private Attempt MoveToRecycleBinDo(IContent content, int userId, bool ignoreDescendants) - { - var evtMsgs = EventMessagesFactory.Get(); - using (new WriteLock(Locker)) - { - using (var uow = UowProvider.GetUnitOfWork()) - { - //Hack: this ensures that the entity's path is valid and if not it fixes/persists it - //see: http://issues.umbraco.org/issue/U4-9336 - content.EnsureValidPath(Logger, entity => GetById(entity.ParentId), QuickUpdate); - var originalPath = content.Path; - var moveEventInfo = new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent); - var moveEventArgs = new MoveEventArgs(evtMsgs, moveEventInfo); - if (uow.Events.DispatchCancelable(Trashing, this, moveEventArgs, "Trashing")) - { - uow.Commit(); - return OperationStatus.Cancelled(evtMsgs); - } - var moveInfo = new List> - { - moveEventInfo - }; - - //get descendents to process of the content item that is being moved to trash - must be done before changing the state below - //must be processed with shallowest levels first - var descendants = ignoreDescendants ? Enumerable.Empty() : GetDescendants(content).OrderBy(x => x.Level); - - //Do the updates for this item - var repository = RepositoryFactory.CreateContentRepository(uow); - //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); - } - 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) - { - //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); - descendant.WriterId = userId; - descendant.ChangeTrashedState(true, descendant.ParentId); - repository.AddOrUpdate(descendant); - - moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); - } - - moveEventArgs.CanCancel = false; - moveEventArgs.MoveInfoCollection = moveInfo; - uow.Events.Dispatch(Trashed, this, moveEventArgs, "Trashed"); - - Audit(uow, AuditType.Move, "Move Content to Recycle Bin performed by user", userId, content.Id); - uow.Commit(); - } - - return 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) - { - var attempt = ((IContentServiceOperations)this).UnPublish(content, userId); - LogHelper.Debug(string.Format("Result of unpublish attempt: {0}", attempt.Result.StatusType)); - return attempt.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); - } - - public IContent GetBlueprintById(int id) - { - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateContentBlueprintRepository(uow); - var blueprint = repository.Get(id); - if (blueprint != null) - ((Content) blueprint).IsBlueprint = true; - return blueprint; - } - } - - public IContent GetBlueprintById(Guid id) - { - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateContentBlueprintRepository(uow); - var blueprint = repository.Get(id); - if (blueprint != null) - ((Content)blueprint).IsBlueprint = true; - return blueprint; - } - } - - public void SaveBlueprint(IContent content, int userId = 0) - { - //always ensure the blueprint is at the root - if (content.ParentId != -1) - content.ParentId = -1; - - ((Content) content).IsBlueprint = true; - - using (new WriteLock(Locker)) - { - using (var uow = UowProvider.GetUnitOfWork()) - { - if (string.IsNullOrWhiteSpace(content.Name)) - { - throw new ArgumentException("Cannot save content blueprint with empty name."); - } - - var repository = RepositoryFactory.CreateContentBlueprintRepository(uow); - - if (content.HasIdentity == false) - { - content.CreatorId = userId; - } - content.WriterId = userId; - - repository.AddOrUpdate(content); - - uow.Events.Dispatch(SavedBlueprint, this, new SaveEventArgs(content), "SavedBlueprint"); - - uow.Commit(); - } - } - } - - public void DeleteBlueprint(IContent content, int userId = 0) - { - using (new WriteLock(Locker)) - { - using (var uow = UowProvider.GetUnitOfWork()) - { - var repository = RepositoryFactory.CreateContentBlueprintRepository(uow); - repository.Delete(content); - uow.Events.Dispatch(DeletedBlueprint, this, new DeleteEventArgs(content), "DeletedBlueprint"); - uow.Commit(); - } - } - } - - public IContent CreateContentFromBlueprint(IContent blueprint, string name, int userId = 0) - { - if (blueprint == null) throw new ArgumentNullException("blueprint"); - - var contentType = blueprint.ContentType; - var content = new Content(name, -1, contentType); - content.Path = string.Concat(content.ParentId.ToString(), ",", content.Id); - - content.CreatorId = userId; - content.WriterId = userId; - - foreach (var property in blueprint.Properties) - content.SetValue(property.Alias, property.Value); - - return content; - } - - public void DeleteBlueprintsOfTypes(IEnumerable contentTypeIds, int userId = 0) - { - using (new WriteLock(Locker)) - using (var uow = UowProvider.GetUnitOfWork()) - { - var repository = RepositoryFactory.CreateContentBlueprintRepository(uow); - - var contentTypeIdsA = contentTypeIds.ToArray(); - var query = new Query(); - if (contentTypeIdsA.Length > 0) - { - query.Where(x => contentTypeIdsA.Contains(x.ContentTypeId)); - } - var blueprints = repository.GetByQuery(query).Select(x => - { - ((Content) x).IsBlueprint = true; - return x; - }).ToArray(); - - foreach (var blueprint in blueprints) - { - repository.Delete(blueprint); - } - - uow.Events.Dispatch(DeletedBlueprint, this, new DeleteEventArgs(blueprints), "DeletedBlueprint"); - uow.Commit(); - } - } - - public void DeleteBlueprintsOfType(int contentTypeId, int userId = 0) - { - DeleteBlueprintsOfTypes(new[] {contentTypeId}, 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. - 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(); - - using (var uow = UowProvider.GetUnitOfWork()) - { - var saveEventArgs = new SaveEventArgs(asArray, evtMsgs); - if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) - { - uow.Commit(); - return OperationStatus.Cancelled(evtMsgs); - } - - // todo - understand what's a lock in a scope? - // (though, these locks are refactored in v8) - using (new WriteLock(Locker)) - { - var containsNew = asArray.Any(x => x.HasIdentity == false); - 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, 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, c)); - } - } - } - - if (raiseEvents) - { - saveEventArgs.CanCancel = false; - uow.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); - } - - Audit(uow, AuditType.Save, "Bulk Save content performed by user", userId == -1 ? 0 : userId, Constants.System.Root); - uow.Commit(); - - return 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)) - { - using (var uow = UowProvider.GetUnitOfWork()) - { - var deleteEventArgs = new DeleteEventArgs(content, evtMsgs); - if (uow.Events.DispatchCancelable(Deleting, this, deleteEventArgs, "Deleting")) - { - uow.Commit(); - return 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 repository = RepositoryFactory.CreateContentRepository(uow); - - repository.Delete(content); - - deleteEventArgs.CanCancel = false; - uow.Events.Dispatch(Deleted, this, deleteEventArgs, "Deleted"); - - Audit(uow, AuditType.Delete, "Delete Content performed by user", userId, content.Id); - uow.Commit(); - } - - return 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 the specified types. All Descendants of deleted content that is not of these types is moved to Recycle Bin. - /// - /// Id of the - /// Optional Id of the user issueing the delete operation - public void DeleteContentOfTypes(IEnumerable contentTypeIds, int userId = 0) - { - using (new WriteLock(Locker)) - using (var uow = UowProvider.GetUnitOfWork()) - { - var repository = RepositoryFactory.CreateContentRepository(uow); - - //track the 'root' items of the collection of nodes discovered to delete, we need to use - //these items to lookup descendants that are not of this doc type so they can be transfered - //to the recycle bin - IDictionary rootItems; - var contentToDelete = this.TrackDeletionsForDeleteContentOfTypes(contentTypeIds, repository, out rootItems).ToArray(); - - if (uow.Events.DispatchCancelable(Deleting, this, new DeleteEventArgs(contentToDelete), "Deleting")) - { - uow.Commit(); - return; - } - - //Determine the items that will need to be recycled (that are children of these content items but not of these content types) - var contentToRecycle = this.TrackTrashedForDeleteContentOfTypes(contentTypeIds, rootItems, repository); - - //move each item to the bin starting with the deepest items - foreach (var child in contentToRecycle.OrderByDescending(x => x.Level)) - { - MoveToRecycleBinDo(child, userId, true); - } - - foreach (var content in contentToDelete) - { - Delete(content, userId); - } - - Audit(uow, AuditType.Delete, - string.Format("Delete Content of Types {0} performed by user", string.Join(",", contentTypeIds)), - userId, Constants.System.Root); - uow.Commit(); - } - } - - /// - /// 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) - { - DeleteContentOfTypes(new[] {contentTypeId}, userId); - } - - /// - /// 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) - { - using (var uow = UowProvider.GetUnitOfWork()) - { - var deleteRevisionsEventArgs = new DeleteRevisionsEventArgs(id, dateToRetain: versionDate); - if (uow.Events.DispatchCancelable(DeletingVersions, this, deleteRevisionsEventArgs, "DeletingVersions")) - { - uow.Commit(); - return; - } - - var repository = RepositoryFactory.CreateContentRepository(uow); - repository.DeleteVersions(id, versionDate); - deleteRevisionsEventArgs.CanCancel = false; - uow.Events.Dispatch(DeletedVersions, this, deleteRevisionsEventArgs, "DeletedVersions"); - - Audit(uow, AuditType.Delete, "Delete Content by version date performed by user", userId, Constants.System.Root); - uow.Commit(); - } - } - - /// - /// 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)) - { - using (var uow = UowProvider.GetUnitOfWork()) - { - if (uow.Events.DispatchCancelable(DeletingVersions, this, new DeleteRevisionsEventArgs(id, specificVersion: versionId), "DeletingVersions")) - { - uow.Commit(); - return; - } - - if (deletePriorVersions) - { - var content = GetByVersion(versionId); - DeleteVersions(id, content.UpdateDate, userId); - } - - var repository = RepositoryFactory.CreateContentRepository(uow); - repository.DeleteVersion(versionId); - - uow.Events.Dispatch(DeletedVersions, this, new DeleteRevisionsEventArgs(id, false, specificVersion: versionId), "DeletedVersions"); - - Audit(uow, AuditType.Delete, "Delete Content by version performed by user", userId, Constants.System.Root); - uow.Commit(); - } - } - } - - /// - /// 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; - } - - using (var uow = UowProvider.GetUnitOfWork()) - { - var moveEventInfo = new MoveEventInfo(content, content.Path, parentId); - var moveEventArgs = new MoveEventArgs(moveEventInfo); - if (uow.Events.DispatchCancelable(Moving, this, moveEventArgs, "Moving")) - { - uow.Commit(); - 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); - - moveEventArgs.MoveInfoCollection = moveInfo; - moveEventArgs.CanCancel = false; - uow.Events.Dispatch(Moved, this, moveEventArgs, "Moved"); - - Audit(uow, AuditType.Move, "Move Content performed by user", userId, content.Id); - uow.Commit(); - } - } - } - - /// - /// Empties the Recycle Bin by deleting all that resides in the bin - /// - [EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("Use EmptyRecycleBin with explicit indication of user ID instead")] - public void EmptyRecycleBin() => EmptyRecycleBin(0); - - /// - /// Empties the Recycle Bin by deleting all that resides in the bin - /// - /// Optional Id of the User emptying the Recycle Bin - public void EmptyRecycleBin(int userId = 0) - { - using (new WriteLock(Locker)) - { - var nodeObjectType = Constants.ObjectTypes.DocumentGuid; - - using (var uow = UowProvider.GetUnitOfWork()) - { - var repository = RepositoryFactory.CreateContentRepository(uow); - - //Create a dictionary of ids -> dictionary of property aliases + values - var entities = repository.GetEntitiesInRecycleBin() - .ToDictionary( - key => key.Id, - val => (IEnumerable)val.Properties); - - var files = ((ContentRepository)repository).GetFilesInRecycleBinForUploadField(); - - var recycleBinEventArgs = new RecycleBinEventArgs(nodeObjectType, entities, files); - if (uow.Events.DispatchCancelable(EmptyingRecycleBin, this, recycleBinEventArgs)) - { - uow.Commit(); - return; - } - - var success = repository.EmptyRecycleBin(); - recycleBinEventArgs.CanCancel = false; - recycleBinEventArgs.RecycleBinEmptiedSuccessfully = success; - uow.Events.Dispatch(EmptiedRecycleBin, this, recycleBinEventArgs); - - Audit(uow, AuditType.Delete, "Empty Content Recycle Bin performed by user", userId, Constants.System.RecycleBinContent); - uow.Commit(); - } - } - } - - /// - /// 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); - - using (var uow = UowProvider.GetUnitOfWork()) - { - var copyEventArgs = new CopyEventArgs(content, copy, true, parentId, relateToOriginal); - if (uow.Events.DispatchCancelable(Copying, this, copyEventArgs)) - { - uow.Commit(); - return null; - } - - var repository = RepositoryFactory.CreateContentRepository(uow); - - // Update the create author and last edit author - copy.CreatorId = userId; - copy.WriterId = userId; - - //get the current permissions, if there are any explicit ones they need to be copied - var currentPermissions = GetPermissionsForEntity(content); - currentPermissions.RemoveWhere(p => p.IsDefaultPermissions); - - repository.AddOrUpdate(copy); - repository.AddOrUpdatePreviewXml(copy, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); - - //add permissions - if (currentPermissions.Count > 0) - { - var permissionSet = new ContentPermissionSet(copy, currentPermissions); - repository.AddOrUpdatePermissions(permissionSet); - } - - uow.Commit(); // todo - this should flush, not 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 - }); - } - uow.Commit(); // todo - this should flush, not commit - - 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); - } - } - copyEventArgs.CanCancel = false; - uow.Events.Dispatch(Copied, this, copyEventArgs); - Audit(uow, AuditType.Copy, "Copy Content performed by user", userId, content.Id); - uow.Commit(); - } - - 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) - { - using (var uow = UowProvider.GetUnitOfWork()) - { - var sendToPublishEventArgs = new SendToPublishEventArgs(content); - if (uow.Events.DispatchCancelable(SendingToPublish, this, sendToPublishEventArgs)) - { - uow.Commit(); - return false; - } - - //Save before raising event - Save(content, userId); - sendToPublishEventArgs.CanCancel = false; - uow.Events.Dispatch(SentToPublish, this, sendToPublishEventArgs); - - Audit(uow, AuditType.SendToPublish, "Send to Publish performed by user", content.WriterId, content.Id); - uow.Commit(); - - 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); - - using (var uow = UowProvider.GetUnitOfWork()) - { - var rollbackEventArgs = new RollbackEventArgs(content); - if (uow.Events.DispatchCancelable(RollingBack, this, rollbackEventArgs)) - { - uow.Commit(); - return content; - } - - var repository = RepositoryFactory.CreateContentRepository(uow); - - content.WriterId = userId; - content.CreatorId = userId; - content.ChangePublishedState(PublishedState.Unpublished); - - repository.AddOrUpdate(content); - repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); - rollbackEventArgs.CanCancel = false; - uow.Events.Dispatch(RolledBack, this, rollbackEventArgs); - - Audit(uow, AuditType.RollBack, "Content rollback performed by user", content.WriterId, content.Id); - uow.Commit(); - } - - return content; - } - - /// - /// Sorts a collection of objects by updating the SortOrder according - /// to the ordering of items in the passed in . - /// - /// - /// Using this method will ensure that the Published-state is maintained upon sorting - /// so the cache is updated accordingly - as needed. - /// - /// - /// - /// - /// True if sorting succeeded, otherwise False - public bool Sort(IEnumerable items, int userId = 0, bool raiseEvents = true) - { - var shouldBePublished = new List(); - var shouldBeSaved = new List(); - - using (new WriteLock(Locker)) - { - using (var uow = UowProvider.GetUnitOfWork()) - { - var asArray = items.ToArray(); - var saveEventArgs = new SaveEventArgs(asArray); - if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) - { - uow.Commit(); - return false; - } - - var repository = RepositoryFactory.CreateContentRepository(uow); - - var 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(uow, content, userId).Success; - shouldBePublished.Add(content); - } - else - shouldBeSaved.Add(content); - - repository.AddOrUpdate(content); - //add or update a preview - repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); - } - - foreach (var content in shouldBePublished) - { - //Create and Save ContentXml DTO - repository.AddOrUpdateContentXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); - } - - if (raiseEvents) - { - saveEventArgs.CanCancel = false; - uow.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); - } - - 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(uow, shouldBePublished, false); - } - - Audit(uow, AuditType.Sort, "Sorting content performed by user", userId, 0); - uow.Commit(); - } - } - - return true; - } - - /// - /// Sorts a collection of objects by updating the SortOrder according - /// to the ordering of node Ids 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(int[] ids, int userId = 0, bool raiseEvents = true) - { - var shouldBePublished = new List(); - var shouldBeSaved = new List(); - - using (new WriteLock(Locker)) - { - var allContent = GetByIds(ids).ToDictionary(x => x.Id, x => x); - if (allContent.Any() == false) - { - return false; - } - var items = ids.Select(x => allContent[x]).ToArray(); - - using (var uow = UowProvider.GetUnitOfWork()) - { - var saveEventArgs = new SaveEventArgs(items); - if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) - { - uow.Commit(); - return false; - } - - var repository = RepositoryFactory.CreateContentRepository(uow); - - var i = 0; - foreach (var content in items) - { - //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(uow, content, userId).Success; - shouldBePublished.Add(content); - } - else - shouldBeSaved.Add(content); - - repository.AddOrUpdate(content); - //add or update a preview - repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); - } - - foreach (var content in shouldBePublished) - { - //Create and Save ContentXml DTO - repository.AddOrUpdateContentXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); - } - - if (raiseEvents) - { - saveEventArgs.CanCancel = false; - uow.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); - } - - 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(uow, shouldBePublished, false); - } - - Audit(uow, AuditType.Sort, "Sort child items performed by user", userId, items.First().ParentId); - uow.Commit(); - } - } - - return true; - } - - public IEnumerable GetBlueprintsForContentTypes(params int[] documentTypeIds) - { - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateContentBlueprintRepository(uow); - - var query = new Query(); - if (documentTypeIds.Length > 0) - { - query.Where(x => documentTypeIds.Contains(x.ContentTypeId)); - } - return repository.GetByQuery(query).Select(x => - { - ((Content) x).IsBlueprint = true; - return x; - }); - } - } - - /// - /// Gets paged content descendants as XML by path - /// - /// Path starts with - /// Page number - /// Page size - /// Total records the query would return without paging - /// A paged enumerable of XML entries of content items - public IEnumerable GetPagedXmlEntries(string path, long pageIndex, int pageSize, out long totalRecords) - { - Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); - Mandate.ParameterCondition(pageSize > 0, "pageSize"); - - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateContentRepository(uow); - var contents = repository.GetPagedXmlEntriesByPath(path, pageIndex, pageSize, - //This order by is VERY important! This allows us to figure out what is implicitly not published, see ContentRepository.BuildXmlCache and - // UmbracoContentIndexer.PerformIndexAll which uses the logic based on this sort order - new[] { "level", "parentID", "sortOrder" }, - out totalRecords); - return contents; - } - } - - /// - /// This builds the Xml document used for the XML cache - /// - /// - public XmlDocument BuildXmlCache() - { - using (var uow = UowProvider.GetUnitOfWork()) - { - var repository = RepositoryFactory.CreateContentRepository(uow); - var result = repository.BuildXmlCache(); - uow.Commit(); - return result; - } - } - - public XmlDocument BuildPreviewXmlCache() - { - using (var uow = UowProvider.GetUnitOfWork()) - { - var repository = RepositoryFactory.CreateContentRepository(uow); - var result = repository.BuildPreviewXmlCache(); - uow.Commit(); - return result; - } - } - - /// - /// Rebuilds all xml content in the cmsContentXml table for all documents - /// - /// - /// Only rebuild the xml structures for the content type ids passed in, if none then rebuilds the structures - /// for all content - /// - public void RebuildXmlStructures(params int[] contentTypeIds) - { - using (var uow = UowProvider.GetUnitOfWork()) - { - var repository = RepositoryFactory.CreateContentRepository(uow); - - repository.RebuildXmlStructures( - content => _entitySerializer.Serialize(this, _dataTypeService, _userService, content), - contentTypeIds: contentTypeIds.Length == 0 ? null : contentTypeIds); - - Audit(uow, AuditType.Publish, "ContentService.RebuildXmlStructures completed, the xml has been regenerated in the database", 0, Constants.System.Root); - uow.Commit(); - } - } - - #region Internal Methods - - /// - /// Gets a collection of descendants by the first Parent. - /// - /// item to retrieve Descendants from - /// An Enumerable list of objects - internal IEnumerable GetPublishedDescendants(IContent content) - { - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateContentRepository(uow); - - var query = Query.Builder.Where(x => x.Id != content.Id && x.Path.StartsWith(content.Path) && x.Trashed == false); - return repository.GetByPublishedVersion(query); - } - } - - #endregion - - #region Private Methods - - /// - /// Hack: This is used to fix some data if an entity's properties are invalid/corrupt - /// - /// - private void QuickUpdate(IContent content) - { - if (content == null) throw new ArgumentNullException("content"); - if (content.HasIdentity == false) throw new InvalidOperationException("Cannot update an entity without an Identity"); - - using (var uow = UowProvider.GetUnitOfWork()) - { - var repository = RepositoryFactory.CreateContentRepository(uow); - repository.AddOrUpdate(content); - uow.Commit(); - } - } - - private void Audit(IScopeUnitOfWork uow, AuditType type, string message, int userId, int objectId) - { - var auditRepo = RepositoryFactory.CreateAuditRepository(uow); - auditRepo.AddOrUpdate(new AuditItem(objectId, message, type, userId)); - } - - //TODO: All of this needs to be moved to the repository - 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, 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)); - uow.Commit(); - } - } - } - 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)) - { - //Hack: this ensures that the entity's path is valid and if not it fixes/persists it - //see: http://issues.umbraco.org/issue/U4-9336 - content.EnsureValidPath(Logger, entity => GetById(entity.ParentId), QuickUpdate); - - 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 = Query.Builder.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; - - using (var uow = UowProvider.GetUnitOfWork()) - { - //Publish and then update the database with new status - var publishedOutcome = internalStrategy.PublishWithChildren(uow, 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 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, c)); - //add or update the published xml - repository.AddOrUpdateContentXml(item.Result.ContentItem, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); - updated.Add(item.Result.ContentItem); - } - - //Save xml to db and call following method to fire event: - _publishingStrategy.PublishingFinalized(uow, updated, false); - - Audit(uow, AuditType.Publish, "Publish with Children performed by user", userId, content.Id); - uow.Commit(); - - 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 - } - - using (var uow = UowProvider.GetUnitOfWork()) - { - var unpublished = _publishingStrategy.UnPublish(uow, content, userId); - if (unpublished == false) - { - uow.Commit(); - return Attempt.Fail(new UnPublishStatus(content, UnPublishedStatusType.FailedCancelledByEvent, evtMsgs)); - } - - 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); - - //Delete xml from db? and call following method to fire event through PublishingStrategy to update cache - if (omitCacheRefresh == false) - _publishingStrategy.UnPublishingFinalized(uow, content); - - Audit(uow, AuditType.UnPublish, "UnPublish performed by user", userId, content.Id); - uow.Commit(); - } - - 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(); - - using (new WriteLock(Locker)) - { - using (var uow = UowProvider.GetUnitOfWork()) - { - var saveEventArgs = new SaveEventArgs(content, evtMsgs); - if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) - { - uow.Commit(); - return Attempt.Fail(new PublishStatus(content, PublishStatusType.FailedCancelledByEvent, evtMsgs)); - } - - //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) - { - //Publish and then update the database with new status - var publishResult = _publishingStrategy.Publish(uow, 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 repository = RepositoryFactory.CreateContentRepository(uow); - - if (published == false) - { - content.ChangePublishedState(PublishedState.Saved); - } - //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); - repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); - - if (published) - { - //Content Xml - repository.AddOrUpdateContentXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); - } - - if (raiseEvents) - { - saveEventArgs.CanCancel = false; - uow.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); - } - - //Save xml to db and call following method to fire event through PublishingStrategy to update cache - if (published) - { - _publishingStrategy.PublishingFinalized(uow, 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)) - { - //TODO: Horrible for performance if there are lots of descendents! We should page if anything but this is crazy - var descendants = GetPublishedDescendants(content); - _publishingStrategy.PublishingFinalized(uow, descendants, false); - } - - uow.Commit(); - - if (publishStatus.StatusType == PublishStatusType.Success) - { - Audit(uow, AuditType.Publish, "Save and Publish performed by user", userId, content.Id); - } - else - { - Audit(uow, AuditType.Save, "Save performed by user", userId, content.Id); - } - uow.Commit(); - - 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(); - - using (new WriteLock(Locker)) - { - using (var uow = UowProvider.GetUnitOfWork()) - { - var saveEventArgs = new SaveEventArgs(content, evtMsgs); - if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) - { - uow.Commit(); - return OperationStatus.Cancelled(evtMsgs); - } - - if (string.IsNullOrWhiteSpace(content.Name)) - { - throw new ArgumentException("Cannot save content with empty name."); - } - - 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, c)); - - if (raiseEvents) - { - saveEventArgs.CanCancel = false; - uow.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); - } - - Audit(uow, AuditType.Save, "Save Content performed by user", userId, content.Id); - uow.Commit(); - } - - return OperationStatus.Success(evtMsgs); - } - } - - 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; - } - - if (content.ExpireDate.HasValue && content.ExpireDate.Value > DateTime.MinValue && DateTime.Now > content.ExpireDate.Value) - { - Logger.Info( - string.Format( - "Content '{0}' with Id '{1}' has expired and could not be published.", - content.Name, content.Id)); - return PublishStatusType.FailedHasExpired; - } - - if (content.ReleaseDate.HasValue && content.ReleaseDate.Value > DateTime.MinValue && content.ReleaseDate.Value > DateTime.Now) - { - Logger.Info( - string.Format( - "Content '{0}' with Id '{1}' is awaiting release and could not be published.", - content.Name, content.Id)); - return PublishStatusType.FailedAwaitingRelease; - } - - 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 uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateContentTypeRepository(uow); - - var query = Query.Builder.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; - - /// - /// Occurs after a blueprint has been saved. - /// - public static event TypedEventHandler> SavedBlueprint; - - /// - /// Occurs after a blueprint has been deleted. - /// - public static event TypedEventHandler> DeletedBlueprint; - - #endregion - } -} +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using System.Xml; +using System.Xml.Linq; +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.UnitOfWork; +using Umbraco.Core.Publishing; + +namespace Umbraco.Core.Services +{ + /// + /// Represents the Content Service, which is an easy access to operations involving + /// + public class ContentService : ScopeRepositoryService, IContentService, IContentServiceOperations + { + private readonly IPublishingStrategy2 _publishingStrategy; + private readonly EntityXmlSerializer _entitySerializer = new EntityXmlSerializer(); + private readonly IDataTypeService _dataTypeService; + private readonly IUserService _userService; + + //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); + private static readonly Regex AnchorRegex = new Regex("", RegexOptions.Compiled); + + public ContentService( + IDatabaseUnitOfWorkProvider provider, + RepositoryFactory repositoryFactory, + ILogger logger, + IEventMessagesFactory eventMessagesFactory, + IDataTypeService dataTypeService, + IUserService userService) + : base(provider, repositoryFactory, logger, eventMessagesFactory) + { + if (dataTypeService == null) throw new ArgumentNullException("dataTypeService"); + if (userService == null) throw new ArgumentNullException("userService"); + _publishingStrategy = new PublishingStrategy(UowProvider.ScopeProvider, eventMessagesFactory, logger); + _dataTypeService = dataTypeService; + _userService = userService; + } + + #region Static Queries + + private IQuery _notTrashedQuery; + + #endregion + + public int CountPublished(string contentTypeAlias = null) + { + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + return repository.CountPublished(contentTypeAlias); + } + } + + public int Count(string contentTypeAlias = null) + { + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + return repository.Count(contentTypeAlias); + } + } + + public int CountChildren(int parentId, string contentTypeAlias = null) + { + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + return repository.CountChildren(parentId, contentTypeAlias); + } + } + + public int CountDescendants(int parentId, string contentTypeAlias = null) + { + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + 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) + { + using (var uow = UowProvider.GetUnitOfWork()) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + repository.ReplaceContentPermissions(permissionSet); + uow.Commit(); + } + } + + /// + /// Assigns a single permission to the current content item for the specified group ids + /// + /// + /// + /// + public void AssignContentPermission(IContent entity, char permission, IEnumerable groupIds) + { + using (var uow = UowProvider.GetUnitOfWork()) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + repository.AssignEntityPermission(entity, permission, groupIds); + uow.Commit(); + } + } + + /// + /// Returns implicit/inherited permissions assigned to the content item for all user groups + /// + /// + /// + public EntityPermissionCollection GetPermissionsForEntity(IContent content) + { + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + 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, Guid parentId, string contentTypeAlias, int userId = 0) + { + var parent = GetById(parentId); + return CreateContent(name, parent, contentTypeAlias, userId); + } + + /// + /// 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); + + using (var uow = UowProvider.GetUnitOfWork()) + { + var newEventArgs = new NewEventArgs(content, contentTypeAlias, parentId); + if (uow.Events.DispatchCancelable(Creating, this, newEventArgs)) + { + uow.Commit(); + content.WasCancelled = true; + return content; + } + + content.CreatorId = userId; + content.WriterId = userId; + newEventArgs.CanCancel = false; + uow.Events.Dispatch(Created, this, newEventArgs); + + 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); + + using (var uow = UowProvider.GetUnitOfWork()) + { + var newEventArgs = new NewEventArgs(content, contentTypeAlias, parent); + if (uow.Events.DispatchCancelable(Creating, this, newEventArgs)) + { + uow.Commit(); + content.WasCancelled = true; + return content; + } + + content.CreatorId = userId; + content.WriterId = userId; + newEventArgs.CanCancel = false; + uow.Events.Dispatch(Created, this, newEventArgs); + + uow.Commit(); + } + + 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); + + using (var uow = UowProvider.GetUnitOfWork()) + { + //NOTE: I really hate the notion of these Creating/Created events - they are so inconsistent, I've only just found + // out that in these 'WithIdentity' methods, the Saving/Saved events were not fired, wtf. Anyways, they're added now. + var newEventArgs = new NewEventArgs(content, contentTypeAlias, parentId); + if (uow.Events.DispatchCancelable(Creating, this, newEventArgs)) + { + uow.Commit(); + content.WasCancelled = true; + return content; + } + + var saveEventArgs = new SaveEventArgs(content); + if (uow.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) + { + uow.Commit(); + content.WasCancelled = true; + return content; + } + + var repository = RepositoryFactory.CreateContentRepository(uow); + content.CreatorId = userId; + content.WriterId = userId; + + repository.AddOrUpdate(content); + repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + saveEventArgs.CanCancel = false; + uow.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); + newEventArgs.CanCancel = false; + uow.Events.Dispatch(Created, this, newEventArgs); + + Audit(uow, AuditType.New, string.Format("Content '{0}' was created with Id {1}", name, content.Id), content.CreatorId, content.Id); + uow.Commit(); + } + + return content; + } + + public IEnumerable GetAnchorValuesFromRTEs(int id) + { + var result = new List(); + + var content = GetById(id); + + foreach (var contentProperty in content.Properties) + { + if (contentProperty.PropertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.TinyMCEAlias)) + { + var value = contentProperty.Value?.ToString(); + + if (!string.IsNullOrEmpty(value)) + { + result.AddRange(GetAnchorValuesFromRTEContent(value)); + } + + } + } + + + return result; + } + + + public IEnumerable GetAnchorValuesFromRTEContent(string rteContent) + { + var result = new List(); + + var matches = AnchorRegex.Matches(rteContent); + + foreach (Match match in matches) + { + result.Add(match.Value.Split('\"')[1]); + } + + return result; + } + + /// + /// 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); + + using (var uow = UowProvider.GetUnitOfWork()) + { + //NOTE: I really hate the notion of these Creating/Created events - they are so inconsistent, I've only just found + // out that in these 'WithIdentity' methods, the Saving/Saved events were not fired, wtf. Anyways, they're added now. + var newEventArgs = new NewEventArgs(content, contentTypeAlias, parent); + if (uow.Events.DispatchCancelable(Creating, this, newEventArgs)) + { + uow.Commit(); + content.WasCancelled = true; + return content; + } + + var saveEventArgs = new SaveEventArgs(content); + if (uow.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) + { + uow.Commit(); + content.WasCancelled = true; + return content; + } + + var repository = RepositoryFactory.CreateContentRepository(uow); + content.CreatorId = userId; + content.WriterId = userId; + + repository.AddOrUpdate(content); + repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + saveEventArgs.CanCancel = false; + uow.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); + newEventArgs.CanCancel = false; + uow.Events.Dispatch(Created, this, newEventArgs); + + Audit(uow, AuditType.New, string.Format("Content '{0}' was created with Id {1}", name, content.Id), content.CreatorId, content.Id); + uow.Commit(); + } + + return content; + } + + /// + /// Gets an object by Id + /// + /// Id of the Content to retrieve + /// + public IContent GetById(int id) + { + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + return repository.Get(id); + } + } + + /// + /// Gets objects by Ids + /// + /// Ids of the Content to retrieve + /// + public IEnumerable GetByIds(IEnumerable ids) + { + var idsArray = ids.ToArray(); + if (idsArray.Length == 0) return Enumerable.Empty(); + + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + + // ensure that the result has the order based on the ids passed in + var result = repository.GetAll(idsArray); + var content = result.ToDictionary(x => x.Id, x => x); + + var sortedResult = idsArray.Select(x => + { + IContent c; + return content.TryGetValue(x, out c) ? c : null; + }).WhereNotNull(); + + return sortedResult; + } + } + + /// + /// Gets objects by Ids + /// + /// Ids of the Content to retrieve + /// + public IEnumerable GetByIds(IEnumerable ids) + { + var idsArray = ids.ToArray(); + if (idsArray.Length == 0) return Enumerable.Empty(); + + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + + // ensure that the result has the order based on the ids passed in + var result = repository.GetAll(idsArray); + var content = result.ToDictionary(x => x.Key, x => x); + + var sortedResult = idsArray.Select(x => + { + IContent c; + return content.TryGetValue(x, out c) ? c : null; + }).WhereNotNull(); + + return sortedResult; + } + } + + /// + /// Gets an object by its 'UniqueId' + /// + /// Guid key of the Content to retrieve + /// + public IContent GetById(Guid key) + { + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + return repository.Get(key); + } + } + + /// + /// Gets a collection of objects by the Id of the + /// + /// Id of the + /// An Enumerable list of objects + public IEnumerable GetContentOfContentType(int id) + { + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + var query = Query.Builder.Where(x => x.ContentTypeId == id); + return repository.GetByQuery(query); + } + } + + internal IEnumerable GetPublishedContentOfContentType(int id) + { + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + var query = Query.Builder.Where(x => x.ContentTypeId == id); + return repository.GetByPublishedVersion(query); + } + } + + /// + /// 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 uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + var query = Query.Builder.Where(x => x.Level == level && x.Path.StartsWith(Constants.System.RecycleBinContent.ToInvariantString()) == false); + return repository.GetByQuery(query); + } + } + + /// + /// Gets a specific version of an item. + /// + /// Id of the version to retrieve + /// An item + public IContent GetByVersion(Guid versionId) + { + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + 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 uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + return repository.GetAllVersions(id); + } + } + + /// + /// Gets a list of all version Ids for the given content item ordered so latest is first + /// + /// + /// The maximum number of rows to return + /// + public IEnumerable GetVersionIds(int id, int maxRows) + { + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + return repository.GetVersionIds(id, maxRows); + } + } + + /// + /// 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 uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + 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 uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + var query = Query.Builder.Where(x => x.ParentId == id); + return repository.GetByQuery(query).OrderBy(x => x.SortOrder); + } + } + + [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 = "") + { + long total; + var result = GetPagedChildren(id, Convert.ToInt64(pageIndex), pageSize, out total, orderBy, orderDirection, true, filter); + totalChildren = Convert.ToInt32(total); + return result; + } + + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Children from + /// Page index (zero based) + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Search text filter + /// An Enumerable list of objects + public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, + string orderBy, Direction orderDirection, string filter = "") + { + return GetPagedChildren(id, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, true, filter); + } + + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Children from + /// Page index (zero based) + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Flag to indicate when ordering by system field + /// Search text filter + /// An Enumerable list of objects + public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, + string orderBy, Direction orderDirection, bool orderBySystemField, string filter) + { + Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); + Mandate.ParameterCondition(pageSize > 0, "pageSize"); + + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + + var query = Query.Builder; + // always check for a parent - else it will also get decendants (and then you should use the GetPagedDescendants method) + query.Where(x => x.ParentId == id); + + IQuery filterQuery = null; + if (filter.IsNullOrWhiteSpace() == false) + { + filterQuery = Query.Builder.Where(x => x.Name.Contains(filter)); + } + return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filterQuery); + } + } + + [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 = "") + { + long total; + var result = GetPagedDescendants(id, Convert.ToInt64(pageIndex), pageSize, out total, orderBy, orderDirection, true, filter); + totalChildren = Convert.ToInt32(total); + return result; + } + + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Descendants from + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Search text filter + /// An Enumerable list of objects + public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = "") + { + return GetPagedDescendants(id, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, true, filter); + } + + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Descendants from + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Flag to indicate when ordering by system field + /// Search text filter + /// An Enumerable list of objects + public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy, Direction orderDirection, bool orderBySystemField, string filter) + { + Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); + Mandate.ParameterCondition(pageSize > 0, "pageSize"); + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + + // get query - if the id is System Root, then just get all + var query = Query.Builder; + if (id != Constants.System.Root) + { + var entityRepository = RepositoryFactory.CreateEntityRepository(uow); + var contentPath = entityRepository.GetAllPaths(Constants.ObjectTypes.DocumentGuid, id).ToArray(); + if (contentPath.Length == 0) + { + totalChildren = 0; + return Enumerable.Empty(); + } + query.Where(x => x.Path.SqlStartsWith(string.Format("{0},", contentPath[0].Path), TextColumnType.NVarchar)); + } + + + // get filter + IQuery filterQuery = null; + if (filter.IsNullOrWhiteSpace() == false) + filterQuery = Query.Builder.Where(x => x.Name.Contains(filter)); + + return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filterQuery); + } + } + + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Descendants from + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Flag to indicate when ordering by system field + /// Search filter + /// An Enumerable list of objects + public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy, Direction orderDirection, bool orderBySystemField, IQuery filter) + { + Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); + Mandate.ParameterCondition(pageSize > 0, "pageSize"); + + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + + // get query - if the id is System Root, then just get all + var query = Query.Builder; + if (id != Constants.System.Root) + { + var entityRepository = RepositoryFactory.CreateEntityRepository(uow); + var contentPath = entityRepository.GetAllPaths(Constants.ObjectTypes.DocumentGuid, id).ToArray(); + if (contentPath.Length == 0) + { + totalChildren = 0; + return Enumerable.Empty(); + } + query.Where(x => x.Path.SqlStartsWith(string.Format("{0},", contentPath[0].Path), TextColumnType.NVarchar)); + } + + return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); + } + } + + /// + /// Gets a collection of objects by its name or partial name + /// + /// Id of the Parent to retrieve Children from + /// Full or partial name of the children + /// An Enumerable list of objects + public IEnumerable GetChildrenByName(int parentId, string name) + { + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + + var query = Query.Builder.Where(x => x.ParentId == parentId && x.Name.Contains(name)); + return repository.GetByQuery(query); + } + } + + /// + /// 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 content == null ? Enumerable.Empty() : 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) + { + //This is a check to ensure that the path is correct for this entity to avoid problems like: http://issues.umbraco.org/issue/U4-9336 due to data corruption + if (content.ValidatePath() == false) + throw new InvalidDataException(string.Format("The content item {0} has an invalid path: {1} with parentID: {2}", content.Id, content.Path, content.ParentId)); + + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + + var pathMatch = content.Path + ","; + var query = Query.Builder.Where(x => x.Path.StartsWith(pathMatch) && x.Id != content.Id); + return repository.GetByQuery(query); + } + } + + /// + /// 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); + } + + /// + /// Gets the published version of a item. + /// + /// The content item. + /// The published version, if any; otherwise, null. + public IContent GetPublishedVersion(IContent content) + { + if (content.Published) return content; + return content.HasPublishedVersion + ? GetByVersion(content.PublishedVersionGuid) + : null; + } + + /// + /// Gets a collection of objects, which reside at the first level / root + /// + /// An Enumerable list of objects + public IEnumerable GetRootContent() + { + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + + var query = Query.Builder.Where(x => x.ParentId == Constants.System.Root); + return repository.GetByQuery(query); + } + } + + /// + /// Gets all published content items + /// + /// + internal IEnumerable GetAllPublished() + { + //create it once if it is needed (no need for locking here) + if (_notTrashedQuery == null) + { + _notTrashedQuery = Query.Builder.Where(x => x.Trashed == false); + } + + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + return repository.GetByPublishedVersion(_notTrashedQuery); + } + } + + /// + /// Gets a collection of objects, which has an expiration date less than or equal to today. + /// + /// An Enumerable list of objects + public IEnumerable GetContentForExpiration() + { + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + var query = Query.Builder.Where(x => x.ExpireDate <= DateTime.Now); + return repository.GetByQuery(query).Where(x => x.HasPublishedVersion); + } + } + + /// + /// Gets a collection of objects, which has a release date less than or equal to today. + /// + /// An Enumerable list of objects + public IEnumerable GetContentForRelease() + { + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + var query = Query.Builder.Where(x => x.Published == false && x.ReleaseDate <= DateTime.Now); + return repository.GetByQuery(query); + } + } + + /// + /// Gets a collection of an objects, which resides in the Recycle Bin + /// + /// An Enumerable list of objects + public IEnumerable GetContentInRecycleBin() + { + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + var query = Query.Builder.Where(x => x.Path.StartsWith(Constants.System.RecycleBinContentPathPrefix)); + return repository.GetByQuery(query); + } + } + + /// + /// 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 uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + var query = Query.Builder.Where(x => x.ParentId == id); + return repository.Count(query); + } + } + + /// + /// Checks whether an item has any published versions + /// + /// Id of the + /// True if the content has any published version otherwise False + public bool HasPublishedVersion(int id) + { + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + var query = Query.Builder.Where(x => x.Published == true && x.Id == id && x.Trashed == false); + return repository.Count(query) > 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) + { + int[] ids; + if (content.HasIdentity) + { + // get ids from path (we have identity) + // skip the first one that has to be -1 - and we don't care + // skip the last one that has to be "this" - and it's ok to stop at the parent + ids = content.Path.Split(',').Skip(1).SkipLast().Select(int.Parse).ToArray(); + } + else + { + // no path yet (no identity), have to move up to parent + // skip the first one that has to be -1 - and we don't care + // don't skip the last one that is "parent" + var parent = GetById(content.ParentId); + if (parent == null) return false; + ids = parent.Path.Split(',').Skip(1).Select(int.Parse).ToArray(); + } + if (ids.Length == 0) + return false; + + // if the first one is recycle bin, fail fast + if (ids[0] == Constants.System.RecycleBinContent) + return false; + + // fixme - move to repository? + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var sql = new Sql(@" + SELECT id + FROM umbracoNode + JOIN cmsDocument ON umbracoNode.id=cmsDocument.nodeId AND cmsDocument.published=@0 + WHERE umbracoNode.trashed=@1 AND umbracoNode.id IN (@2)", + true, false, ids); + var x = uow.Database.Fetch(sql); + return ids.Length == x.Count; + } + } + + /// + /// 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) + { + return MoveToRecycleBinDo(content, userId, false); + } + + /// + /// 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 + /// + /// A boolean indicating to ignore this item's descendant list from also being moved to the recycle bin. This is required for the DeleteContentOfTypes method + /// because it has already looked up all descendant nodes that will need to be recycled + /// TODO: Fix all of this, it will require a reasonable refactor and most of this stuff should be done at the repo level instead of service sub operations + /// + private Attempt MoveToRecycleBinDo(IContent content, int userId, bool ignoreDescendants) + { + var evtMsgs = EventMessagesFactory.Get(); + using (new WriteLock(Locker)) + { + using (var uow = UowProvider.GetUnitOfWork()) + { + //Hack: this ensures that the entity's path is valid and if not it fixes/persists it + //see: http://issues.umbraco.org/issue/U4-9336 + content.EnsureValidPath(Logger, entity => GetById(entity.ParentId), QuickUpdate); + var originalPath = content.Path; + var moveEventInfo = new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent); + var moveEventArgs = new MoveEventArgs(evtMsgs, moveEventInfo); + if (uow.Events.DispatchCancelable(Trashing, this, moveEventArgs, "Trashing")) + { + uow.Commit(); + return OperationStatus.Cancelled(evtMsgs); + } + var moveInfo = new List> + { + moveEventInfo + }; + + //get descendents to process of the content item that is being moved to trash - must be done before changing the state below + //must be processed with shallowest levels first + var descendants = ignoreDescendants ? Enumerable.Empty() : GetDescendants(content).OrderBy(x => x.Level); + + //Do the updates for this item + var repository = RepositoryFactory.CreateContentRepository(uow); + //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); + } + 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) + { + //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); + descendant.WriterId = userId; + descendant.ChangeTrashedState(true, descendant.ParentId); + repository.AddOrUpdate(descendant); + + moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); + } + + moveEventArgs.CanCancel = false; + moveEventArgs.MoveInfoCollection = moveInfo; + uow.Events.Dispatch(Trashed, this, moveEventArgs, "Trashed"); + + Audit(uow, AuditType.Move, "Move Content to Recycle Bin performed by user", userId, content.Id); + uow.Commit(); + } + + return 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) + { + var attempt = ((IContentServiceOperations)this).UnPublish(content, userId); + LogHelper.Debug(string.Format("Result of unpublish attempt: {0}", attempt.Result.StatusType)); + return attempt.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); + } + + public IContent GetBlueprintById(int id) + { + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentBlueprintRepository(uow); + var blueprint = repository.Get(id); + if (blueprint != null) + ((Content) blueprint).IsBlueprint = true; + return blueprint; + } + } + + public IContent GetBlueprintById(Guid id) + { + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentBlueprintRepository(uow); + var blueprint = repository.Get(id); + if (blueprint != null) + ((Content)blueprint).IsBlueprint = true; + return blueprint; + } + } + + public void SaveBlueprint(IContent content, int userId = 0) + { + //always ensure the blueprint is at the root + if (content.ParentId != -1) + content.ParentId = -1; + + ((Content) content).IsBlueprint = true; + + using (new WriteLock(Locker)) + { + using (var uow = UowProvider.GetUnitOfWork()) + { + if (string.IsNullOrWhiteSpace(content.Name)) + { + throw new ArgumentException("Cannot save content blueprint with empty name."); + } + + var repository = RepositoryFactory.CreateContentBlueprintRepository(uow); + + if (content.HasIdentity == false) + { + content.CreatorId = userId; + } + content.WriterId = userId; + + repository.AddOrUpdate(content); + + uow.Events.Dispatch(SavedBlueprint, this, new SaveEventArgs(content), "SavedBlueprint"); + + uow.Commit(); + } + } + } + + public void DeleteBlueprint(IContent content, int userId = 0) + { + using (new WriteLock(Locker)) + { + using (var uow = UowProvider.GetUnitOfWork()) + { + var repository = RepositoryFactory.CreateContentBlueprintRepository(uow); + repository.Delete(content); + uow.Events.Dispatch(DeletedBlueprint, this, new DeleteEventArgs(content), "DeletedBlueprint"); + uow.Commit(); + } + } + } + + public IContent CreateContentFromBlueprint(IContent blueprint, string name, int userId = 0) + { + if (blueprint == null) throw new ArgumentNullException("blueprint"); + + var contentType = blueprint.ContentType; + var content = new Content(name, -1, contentType); + content.Path = string.Concat(content.ParentId.ToString(), ",", content.Id); + + content.CreatorId = userId; + content.WriterId = userId; + + foreach (var property in blueprint.Properties) + content.SetValue(property.Alias, property.Value); + + return content; + } + + public void DeleteBlueprintsOfTypes(IEnumerable contentTypeIds, int userId = 0) + { + using (new WriteLock(Locker)) + using (var uow = UowProvider.GetUnitOfWork()) + { + var repository = RepositoryFactory.CreateContentBlueprintRepository(uow); + + var contentTypeIdsA = contentTypeIds.ToArray(); + var query = new Query(); + if (contentTypeIdsA.Length > 0) + { + query.Where(x => contentTypeIdsA.Contains(x.ContentTypeId)); + } + var blueprints = repository.GetByQuery(query).Select(x => + { + ((Content) x).IsBlueprint = true; + return x; + }).ToArray(); + + foreach (var blueprint in blueprints) + { + repository.Delete(blueprint); + } + + uow.Events.Dispatch(DeletedBlueprint, this, new DeleteEventArgs(blueprints), "DeletedBlueprint"); + uow.Commit(); + } + } + + public void DeleteBlueprintsOfType(int contentTypeId, int userId = 0) + { + DeleteBlueprintsOfTypes(new[] {contentTypeId}, 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. + 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(); + + using (var uow = UowProvider.GetUnitOfWork()) + { + var saveEventArgs = new SaveEventArgs(asArray, evtMsgs); + if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) + { + uow.Commit(); + return OperationStatus.Cancelled(evtMsgs); + } + + // todo - understand what's a lock in a scope? + // (though, these locks are refactored in v8) + using (new WriteLock(Locker)) + { + var containsNew = asArray.Any(x => x.HasIdentity == false); + 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, 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, c)); + } + } + } + + if (raiseEvents) + { + saveEventArgs.CanCancel = false; + uow.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); + } + + Audit(uow, AuditType.Save, "Bulk Save content performed by user", userId == -1 ? 0 : userId, Constants.System.Root); + uow.Commit(); + + return 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)) + { + using (var uow = UowProvider.GetUnitOfWork()) + { + var deleteEventArgs = new DeleteEventArgs(content, evtMsgs); + if (uow.Events.DispatchCancelable(Deleting, this, deleteEventArgs, "Deleting")) + { + uow.Commit(); + return 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 repository = RepositoryFactory.CreateContentRepository(uow); + + repository.Delete(content); + + deleteEventArgs.CanCancel = false; + uow.Events.Dispatch(Deleted, this, deleteEventArgs, "Deleted"); + + Audit(uow, AuditType.Delete, "Delete Content performed by user", userId, content.Id); + uow.Commit(); + } + + return 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 the specified types. All Descendants of deleted content that is not of these types is moved to Recycle Bin. + /// + /// Id of the + /// Optional Id of the user issueing the delete operation + public void DeleteContentOfTypes(IEnumerable contentTypeIds, int userId = 0) + { + using (new WriteLock(Locker)) + using (var uow = UowProvider.GetUnitOfWork()) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + + //track the 'root' items of the collection of nodes discovered to delete, we need to use + //these items to lookup descendants that are not of this doc type so they can be transfered + //to the recycle bin + IDictionary rootItems; + var contentToDelete = this.TrackDeletionsForDeleteContentOfTypes(contentTypeIds, repository, out rootItems).ToArray(); + + if (uow.Events.DispatchCancelable(Deleting, this, new DeleteEventArgs(contentToDelete), "Deleting")) + { + uow.Commit(); + return; + } + + //Determine the items that will need to be recycled (that are children of these content items but not of these content types) + var contentToRecycle = this.TrackTrashedForDeleteContentOfTypes(contentTypeIds, rootItems, repository); + + //move each item to the bin starting with the deepest items + foreach (var child in contentToRecycle.OrderByDescending(x => x.Level)) + { + MoveToRecycleBinDo(child, userId, true); + } + + foreach (var content in contentToDelete) + { + Delete(content, userId); + } + + Audit(uow, AuditType.Delete, + string.Format("Delete Content of Types {0} performed by user", string.Join(",", contentTypeIds)), + userId, Constants.System.Root); + uow.Commit(); + } + } + + /// + /// 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) + { + DeleteContentOfTypes(new[] {contentTypeId}, userId); + } + + /// + /// 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) + { + using (var uow = UowProvider.GetUnitOfWork()) + { + var deleteRevisionsEventArgs = new DeleteRevisionsEventArgs(id, dateToRetain: versionDate); + if (uow.Events.DispatchCancelable(DeletingVersions, this, deleteRevisionsEventArgs, "DeletingVersions")) + { + uow.Commit(); + return; + } + + var repository = RepositoryFactory.CreateContentRepository(uow); + repository.DeleteVersions(id, versionDate); + deleteRevisionsEventArgs.CanCancel = false; + uow.Events.Dispatch(DeletedVersions, this, deleteRevisionsEventArgs, "DeletedVersions"); + + Audit(uow, AuditType.Delete, "Delete Content by version date performed by user", userId, Constants.System.Root); + uow.Commit(); + } + } + + /// + /// 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)) + { + using (var uow = UowProvider.GetUnitOfWork()) + { + if (uow.Events.DispatchCancelable(DeletingVersions, this, new DeleteRevisionsEventArgs(id, specificVersion: versionId), "DeletingVersions")) + { + uow.Commit(); + return; + } + + if (deletePriorVersions) + { + var content = GetByVersion(versionId); + DeleteVersions(id, content.UpdateDate, userId); + } + + var repository = RepositoryFactory.CreateContentRepository(uow); + repository.DeleteVersion(versionId); + + uow.Events.Dispatch(DeletedVersions, this, new DeleteRevisionsEventArgs(id, false, specificVersion: versionId), "DeletedVersions"); + + Audit(uow, AuditType.Delete, "Delete Content by version performed by user", userId, Constants.System.Root); + uow.Commit(); + } + } + } + + /// + /// 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; + } + + using (var uow = UowProvider.GetUnitOfWork()) + { + var moveEventInfo = new MoveEventInfo(content, content.Path, parentId); + var moveEventArgs = new MoveEventArgs(moveEventInfo); + if (uow.Events.DispatchCancelable(Moving, this, moveEventArgs, "Moving")) + { + uow.Commit(); + 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); + + moveEventArgs.MoveInfoCollection = moveInfo; + moveEventArgs.CanCancel = false; + uow.Events.Dispatch(Moved, this, moveEventArgs, "Moved"); + + Audit(uow, AuditType.Move, "Move Content performed by user", userId, content.Id); + uow.Commit(); + } + } + } + + /// + /// Empties the Recycle Bin by deleting all that resides in the bin + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use EmptyRecycleBin with explicit indication of user ID instead")] + public void EmptyRecycleBin() => EmptyRecycleBin(0); + + /// + /// Empties the Recycle Bin by deleting all that resides in the bin + /// + /// Optional Id of the User emptying the Recycle Bin + public void EmptyRecycleBin(int userId = 0) + { + using (new WriteLock(Locker)) + { + var nodeObjectType = Constants.ObjectTypes.DocumentGuid; + + using (var uow = UowProvider.GetUnitOfWork()) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + + //Create a dictionary of ids -> dictionary of property aliases + values + var entities = repository.GetEntitiesInRecycleBin() + .ToDictionary( + key => key.Id, + val => (IEnumerable)val.Properties); + + var files = ((ContentRepository)repository).GetFilesInRecycleBinForUploadField(); + + var recycleBinEventArgs = new RecycleBinEventArgs(nodeObjectType, entities, files); + if (uow.Events.DispatchCancelable(EmptyingRecycleBin, this, recycleBinEventArgs)) + { + uow.Commit(); + return; + } + + var success = repository.EmptyRecycleBin(); + recycleBinEventArgs.CanCancel = false; + recycleBinEventArgs.RecycleBinEmptiedSuccessfully = success; + uow.Events.Dispatch(EmptiedRecycleBin, this, recycleBinEventArgs); + + Audit(uow, AuditType.Delete, "Empty Content Recycle Bin performed by user", userId, Constants.System.RecycleBinContent); + uow.Commit(); + } + } + } + + /// + /// 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); + + using (var uow = UowProvider.GetUnitOfWork()) + { + var copyEventArgs = new CopyEventArgs(content, copy, true, parentId, relateToOriginal); + if (uow.Events.DispatchCancelable(Copying, this, copyEventArgs)) + { + uow.Commit(); + return null; + } + + var repository = RepositoryFactory.CreateContentRepository(uow); + + // Update the create author and last edit author + copy.CreatorId = userId; + copy.WriterId = userId; + + //get the current permissions, if there are any explicit ones they need to be copied + var currentPermissions = GetPermissionsForEntity(content); + currentPermissions.RemoveWhere(p => p.IsDefaultPermissions); + + repository.AddOrUpdate(copy); + repository.AddOrUpdatePreviewXml(copy, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + + //add permissions + if (currentPermissions.Count > 0) + { + var permissionSet = new ContentPermissionSet(copy, currentPermissions); + repository.AddOrUpdatePermissions(permissionSet); + } + + uow.Commit(); // todo - this should flush, not 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 + }); + } + uow.Commit(); // todo - this should flush, not commit + + 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); + } + } + copyEventArgs.CanCancel = false; + uow.Events.Dispatch(Copied, this, copyEventArgs); + Audit(uow, AuditType.Copy, "Copy Content performed by user", userId, content.Id); + uow.Commit(); + } + + 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) + { + using (var uow = UowProvider.GetUnitOfWork()) + { + var sendToPublishEventArgs = new SendToPublishEventArgs(content); + if (uow.Events.DispatchCancelable(SendingToPublish, this, sendToPublishEventArgs)) + { + uow.Commit(); + return false; + } + + //Save before raising event + Save(content, userId); + sendToPublishEventArgs.CanCancel = false; + uow.Events.Dispatch(SentToPublish, this, sendToPublishEventArgs); + + Audit(uow, AuditType.SendToPublish, "Send to Publish performed by user", content.WriterId, content.Id); + uow.Commit(); + + 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); + + using (var uow = UowProvider.GetUnitOfWork()) + { + var rollbackEventArgs = new RollbackEventArgs(content); + if (uow.Events.DispatchCancelable(RollingBack, this, rollbackEventArgs)) + { + uow.Commit(); + return content; + } + + var repository = RepositoryFactory.CreateContentRepository(uow); + + content.WriterId = userId; + content.CreatorId = userId; + content.ChangePublishedState(PublishedState.Unpublished); + + repository.AddOrUpdate(content); + repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + rollbackEventArgs.CanCancel = false; + uow.Events.Dispatch(RolledBack, this, rollbackEventArgs); + + Audit(uow, AuditType.RollBack, "Content rollback performed by user", content.WriterId, content.Id); + uow.Commit(); + } + + return content; + } + + /// + /// Sorts a collection of objects by updating the SortOrder according + /// to the ordering of items in the passed in . + /// + /// + /// Using this method will ensure that the Published-state is maintained upon sorting + /// so the cache is updated accordingly - as needed. + /// + /// + /// + /// + /// True if sorting succeeded, otherwise False + public bool Sort(IEnumerable items, int userId = 0, bool raiseEvents = true) + { + var shouldBePublished = new List(); + var shouldBeSaved = new List(); + + using (new WriteLock(Locker)) + { + using (var uow = UowProvider.GetUnitOfWork()) + { + var asArray = items.ToArray(); + var saveEventArgs = new SaveEventArgs(asArray); + if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) + { + uow.Commit(); + return false; + } + + var repository = RepositoryFactory.CreateContentRepository(uow); + + var 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(uow, content, userId).Success; + shouldBePublished.Add(content); + } + else + shouldBeSaved.Add(content); + + repository.AddOrUpdate(content); + //add or update a preview + repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + } + + foreach (var content in shouldBePublished) + { + //Create and Save ContentXml DTO + repository.AddOrUpdateContentXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + } + + if (raiseEvents) + { + saveEventArgs.CanCancel = false; + uow.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); + } + + 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(uow, shouldBePublished, false); + } + + Audit(uow, AuditType.Sort, "Sorting content performed by user", userId, 0); + uow.Commit(); + } + } + + return true; + } + + /// + /// Sorts a collection of objects by updating the SortOrder according + /// to the ordering of node Ids 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(int[] ids, int userId = 0, bool raiseEvents = true) + { + var shouldBePublished = new List(); + var shouldBeSaved = new List(); + + using (new WriteLock(Locker)) + { + var allContent = GetByIds(ids).ToDictionary(x => x.Id, x => x); + if (allContent.Any() == false) + { + return false; + } + var items = ids.Select(x => allContent[x]).ToArray(); + + using (var uow = UowProvider.GetUnitOfWork()) + { + var saveEventArgs = new SaveEventArgs(items); + if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) + { + uow.Commit(); + return false; + } + + var repository = RepositoryFactory.CreateContentRepository(uow); + + var i = 0; + foreach (var content in items) + { + //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(uow, content, userId).Success; + shouldBePublished.Add(content); + } + else + shouldBeSaved.Add(content); + + repository.AddOrUpdate(content); + //add or update a preview + repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + } + + foreach (var content in shouldBePublished) + { + //Create and Save ContentXml DTO + repository.AddOrUpdateContentXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + } + + if (raiseEvents) + { + saveEventArgs.CanCancel = false; + uow.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); + } + + 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(uow, shouldBePublished, false); + } + + Audit(uow, AuditType.Sort, "Sort child items performed by user", userId, items.First().ParentId); + uow.Commit(); + } + } + + return true; + } + + public IEnumerable GetBlueprintsForContentTypes(params int[] documentTypeIds) + { + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentBlueprintRepository(uow); + + var query = new Query(); + if (documentTypeIds.Length > 0) + { + query.Where(x => documentTypeIds.Contains(x.ContentTypeId)); + } + return repository.GetByQuery(query).Select(x => + { + ((Content) x).IsBlueprint = true; + return x; + }); + } + } + + /// + /// Gets paged content descendants as XML by path + /// + /// Path starts with + /// Page number + /// Page size + /// Total records the query would return without paging + /// A paged enumerable of XML entries of content items + public IEnumerable GetPagedXmlEntries(string path, long pageIndex, int pageSize, out long totalRecords) + { + Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); + Mandate.ParameterCondition(pageSize > 0, "pageSize"); + + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + var contents = repository.GetPagedXmlEntriesByPath(path, pageIndex, pageSize, + //This order by is VERY important! This allows us to figure out what is implicitly not published, see ContentRepository.BuildXmlCache and + // UmbracoContentIndexer.PerformIndexAll which uses the logic based on this sort order + new[] { "level", "parentID", "sortOrder" }, + out totalRecords); + return contents; + } + } + + /// + /// This builds the Xml document used for the XML cache + /// + /// + public XmlDocument BuildXmlCache() + { + using (var uow = UowProvider.GetUnitOfWork()) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + var result = repository.BuildXmlCache(); + uow.Commit(); + return result; + } + } + + public XmlDocument BuildPreviewXmlCache() + { + using (var uow = UowProvider.GetUnitOfWork()) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + var result = repository.BuildPreviewXmlCache(); + uow.Commit(); + return result; + } + } + + /// + /// Rebuilds all xml content in the cmsContentXml table for all documents + /// + /// + /// Only rebuild the xml structures for the content type ids passed in, if none then rebuilds the structures + /// for all content + /// + public void RebuildXmlStructures(params int[] contentTypeIds) + { + using (var uow = UowProvider.GetUnitOfWork()) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + + repository.RebuildXmlStructures( + content => _entitySerializer.Serialize(this, _dataTypeService, _userService, content), + contentTypeIds: contentTypeIds.Length == 0 ? null : contentTypeIds); + + Audit(uow, AuditType.Publish, "ContentService.RebuildXmlStructures completed, the xml has been regenerated in the database", 0, Constants.System.Root); + uow.Commit(); + } + } + + #region Internal Methods + + /// + /// Gets a collection of descendants by the first Parent. + /// + /// item to retrieve Descendants from + /// An Enumerable list of objects + internal IEnumerable GetPublishedDescendants(IContent content) + { + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + + var query = Query.Builder.Where(x => x.Id != content.Id && x.Path.StartsWith(content.Path) && x.Trashed == false); + return repository.GetByPublishedVersion(query); + } + } + + #endregion + + #region Private Methods + + /// + /// Hack: This is used to fix some data if an entity's properties are invalid/corrupt + /// + /// + private void QuickUpdate(IContent content) + { + if (content == null) throw new ArgumentNullException("content"); + if (content.HasIdentity == false) throw new InvalidOperationException("Cannot update an entity without an Identity"); + + using (var uow = UowProvider.GetUnitOfWork()) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + repository.AddOrUpdate(content); + uow.Commit(); + } + } + + private void Audit(IScopeUnitOfWork uow, AuditType type, string message, int userId, int objectId) + { + var auditRepo = RepositoryFactory.CreateAuditRepository(uow); + auditRepo.AddOrUpdate(new AuditItem(objectId, message, type, userId)); + } + + //TODO: All of this needs to be moved to the repository + 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, 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)); + uow.Commit(); + } + } + } + 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)) + { + //Hack: this ensures that the entity's path is valid and if not it fixes/persists it + //see: http://issues.umbraco.org/issue/U4-9336 + content.EnsureValidPath(Logger, entity => GetById(entity.ParentId), QuickUpdate); + + 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 = Query.Builder.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; + + using (var uow = UowProvider.GetUnitOfWork()) + { + //Publish and then update the database with new status + var publishedOutcome = internalStrategy.PublishWithChildren(uow, 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 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, c)); + //add or update the published xml + repository.AddOrUpdateContentXml(item.Result.ContentItem, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + updated.Add(item.Result.ContentItem); + } + + //Save xml to db and call following method to fire event: + _publishingStrategy.PublishingFinalized(uow, updated, false); + + Audit(uow, AuditType.Publish, "Publish with Children performed by user", userId, content.Id); + uow.Commit(); + + 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 + } + + using (var uow = UowProvider.GetUnitOfWork()) + { + var unpublished = _publishingStrategy.UnPublish(uow, content, userId); + if (unpublished == false) + { + uow.Commit(); + return Attempt.Fail(new UnPublishStatus(content, UnPublishedStatusType.FailedCancelledByEvent, evtMsgs)); + } + + 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); + + //Delete xml from db? and call following method to fire event through PublishingStrategy to update cache + if (omitCacheRefresh == false) + _publishingStrategy.UnPublishingFinalized(uow, content); + + Audit(uow, AuditType.UnPublish, "UnPublish performed by user", userId, content.Id); + uow.Commit(); + } + + 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(); + + using (new WriteLock(Locker)) + { + using (var uow = UowProvider.GetUnitOfWork()) + { + var saveEventArgs = new SaveEventArgs(content, evtMsgs); + if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) + { + uow.Commit(); + return Attempt.Fail(new PublishStatus(content, PublishStatusType.FailedCancelledByEvent, evtMsgs)); + } + + //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) + { + //Publish and then update the database with new status + var publishResult = _publishingStrategy.Publish(uow, 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 repository = RepositoryFactory.CreateContentRepository(uow); + + if (published == false) + { + content.ChangePublishedState(PublishedState.Saved); + } + //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); + repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + + if (published) + { + //Content Xml + repository.AddOrUpdateContentXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + } + + if (raiseEvents) + { + saveEventArgs.CanCancel = false; + uow.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); + } + + //Save xml to db and call following method to fire event through PublishingStrategy to update cache + if (published) + { + _publishingStrategy.PublishingFinalized(uow, 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)) + { + //TODO: Horrible for performance if there are lots of descendents! We should page if anything but this is crazy + var descendants = GetPublishedDescendants(content); + _publishingStrategy.PublishingFinalized(uow, descendants, false); + } + + uow.Commit(); + + if (publishStatus.StatusType == PublishStatusType.Success) + { + Audit(uow, AuditType.Publish, "Save and Publish performed by user", userId, content.Id); + } + else + { + Audit(uow, AuditType.Save, "Save performed by user", userId, content.Id); + } + uow.Commit(); + + 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(); + + using (new WriteLock(Locker)) + { + var currVersion = content.Version; + + using (var uow = UowProvider.GetUnitOfWork()) + { + var saveEventArgs = new SaveEventArgs(content, evtMsgs); + if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) + { + uow.Commit(); + return OperationStatus.Cancelled(evtMsgs); + } + + if (string.IsNullOrWhiteSpace(content.Name)) + { + throw new ArgumentException("Cannot save content with empty name."); + } + + 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, c)); + + if (raiseEvents) + { + saveEventArgs.CanCancel = false; + uow.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); + } + + Audit(uow, AuditType.Save, "Save Content performed by user", userId, content.Id); + uow.Commit(); + } + + return currVersion == content.Version + ? OperationStatus.NoOperation(evtMsgs) + : OperationStatus.Success(evtMsgs); + } + } + + 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; + } + + if (content.ExpireDate.HasValue && content.ExpireDate.Value > DateTime.MinValue && DateTime.Now > content.ExpireDate.Value) + { + Logger.Info( + string.Format( + "Content '{0}' with Id '{1}' has expired and could not be published.", + content.Name, content.Id)); + return PublishStatusType.FailedHasExpired; + } + + if (content.ReleaseDate.HasValue && content.ReleaseDate.Value > DateTime.MinValue && content.ReleaseDate.Value > DateTime.Now) + { + Logger.Info( + string.Format( + "Content '{0}' with Id '{1}' is awaiting release and could not be published.", + content.Name, content.Id)); + return PublishStatusType.FailedAwaitingRelease; + } + + 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 uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateContentTypeRepository(uow); + + var query = Query.Builder.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; + + /// + /// Occurs after a blueprint has been saved. + /// + public static event TypedEventHandler> SavedBlueprint; + + /// + /// Occurs after a blueprint has been deleted. + /// + public static event TypedEventHandler> DeletedBlueprint; + + #endregion + } +} diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 1ac8318f9c..8bb7af0077 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -1,1219 +1,1223 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Web.Http; -using System.Web.Http.Controllers; -using System.Web.Http.ModelBinding; -using AutoMapper; -using Umbraco.Core; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Core.Models.Membership; -using Umbraco.Core.Persistence.DatabaseModelDefinitions; -using Umbraco.Core.Publishing; -using Umbraco.Core.Services; -using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Models.Mapping; -using Umbraco.Web.Mvc; -using Umbraco.Web.WebApi; -using Umbraco.Web.WebApi.Binders; -using Umbraco.Web.WebApi.Filters; -using Umbraco.Core.Events; -using Constants = Umbraco.Core.Constants; -using umbraco.cms.businesslogic; -using System.Collections; -using umbraco; - -namespace Umbraco.Web.Editors -{ - /// - /// The API controller used for editing content - /// - /// - /// This controller is decorated with the UmbracoApplicationAuthorizeAttribute which means that any user requesting - /// access to ALL of the methods on this controller will need access to the content application. - /// - [PluginController("UmbracoApi")] - [UmbracoApplicationAuthorize(Constants.Applications.Content)] - [ContentControllerConfiguration] - public class ContentController : ContentControllerBase - { - /// - /// Constructor - /// - public ContentController() - : this(UmbracoContext.Current) - { - } - - /// - /// Constructor - /// - /// - public ContentController(UmbracoContext umbracoContext) - : base(umbracoContext) - { - } - - /// - /// Configures this controller with a custom action selector - /// - private class ContentControllerConfigurationAttribute : Attribute, IControllerConfiguration - { - public void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor) - { - controllerSettings.Services.Replace(typeof(IHttpActionSelector), new ParameterSwapControllerActionSelector( - new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetNiceUrl", "id", typeof(int), typeof(Guid), typeof(Udi)), - new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetById", "id", typeof(int), typeof(Guid), typeof(Udi)) - )); - } - } - - /// - /// Return content for the specified ids - /// - /// - /// - [FilterAllowedOutgoingContent(typeof(IEnumerable))] - public IEnumerable GetByIds([FromUri]int[] ids) - { - var foundContent = Services.ContentService.GetByIds(ids); - return foundContent.Select(content => AutoMapperExtensions.MapWithUmbracoContext(content, UmbracoContext)); - } - - /// - /// Updates the permissions for a content item for a particular user group - /// - /// - /// - /// - /// Permission check is done for letter 'R' which is for which the user must have access to to update - /// - [EnsureUserPermissionForContent("saveModel.ContentId", 'R')] - public IEnumerable PostSaveUserGroupPermissions(UserGroupPermissionsSave saveModel) - { - if (saveModel.ContentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - - //TODO: Should non-admins be alowed to set granular permissions? - - var content = Services.ContentService.GetById(saveModel.ContentId); - if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - - //current permissions explicitly assigned to this content item - var contentPermissions = Services.ContentService.GetPermissionsForEntity(content) - .ToDictionary(x => x.UserGroupId, x => x); - - var allUserGroups = Services.UserService.GetAllUserGroups().ToArray(); - - //loop through each user group - foreach (var userGroup in allUserGroups) - { - //check if there's a permission set posted up for this user group - IEnumerable groupPermissions; - if (saveModel.AssignedPermissions.TryGetValue(userGroup.Id, out groupPermissions)) - { - //create a string collection of the assigned letters - var groupPermissionCodes = groupPermissions.ToArray(); - - //check if there are no permissions assigned for this group save model, if that is the case we want to reset the permissions - //for this group/node which will go back to the defaults - if (groupPermissionCodes.Length == 0) - { - Services.UserService.RemoveUserGroupPermissions(userGroup.Id, content.Id); - } - //check if they are the defaults, if so we should just remove them if they exist since it's more overhead having them stored - else if (userGroup.Permissions.UnsortedSequenceEqual(groupPermissionCodes)) - { - //only remove them if they are actually currently assigned - if (contentPermissions.ContainsKey(userGroup.Id)) - { - //remove these permissions from this node for this group since the ones being assigned are the same as the defaults - Services.UserService.RemoveUserGroupPermissions(userGroup.Id, content.Id); - } - } - //if they are different we need to update, otherwise there's nothing to update - else if (contentPermissions.ContainsKey(userGroup.Id) == false || contentPermissions[userGroup.Id].AssignedPermissions.UnsortedSequenceEqual(groupPermissionCodes) == false) - { - - Services.UserService.ReplaceUserGroupPermissions(userGroup.Id, groupPermissionCodes.Select(x => x[0]), content.Id); - } - } - } - - return GetDetailedPermissions(content, allUserGroups); - } - - /// - /// Returns the user group permissions for user groups assigned to this node - /// - /// - /// - /// - /// Permission check is done for letter 'R' which is for which the user must have access to to view - /// - [EnsureUserPermissionForContent("contentId", 'R')] - public IEnumerable GetDetailedPermissions(int contentId) - { - if (contentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - var content = Services.ContentService.GetById(contentId); - if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - - //TODO: Should non-admins be able to see detailed permissions? - - var allUserGroups = Services.UserService.GetAllUserGroups(); - - return GetDetailedPermissions(content, allUserGroups); - } - - private IEnumerable GetDetailedPermissions(IContent content, IEnumerable allUserGroups) - { - //get all user groups and map their default permissions to the AssignedUserGroupPermissions model. - //we do this because not all groups will have true assigned permissions for this node so if they don't have assigned permissions, we need to show the defaults. - - var defaultPermissionsByGroup = Mapper.Map>(allUserGroups).ToArray(); - - var defaultPermissionsAsDictionary = defaultPermissionsByGroup - .ToDictionary(x => Convert.ToInt32(x.Id), x => x); - - //get the actual assigned permissions - var assignedPermissionsByGroup = Services.ContentService.GetPermissionsForEntity(content).ToArray(); - - //iterate over assigned and update the defaults with the real values - foreach (var assignedGroupPermission in assignedPermissionsByGroup) - { - var defaultUserGroupPermissions = defaultPermissionsAsDictionary[assignedGroupPermission.UserGroupId]; - - //clone the default permissions model to the assigned ones - defaultUserGroupPermissions.AssignedPermissions = AssignedUserGroupPermissions.ClonePermissions(defaultUserGroupPermissions.DefaultPermissions); - - //since there is custom permissions assigned to this node for this group, we need to clear all of the default permissions - //and we'll re-check it if it's one of the explicitly assigned ones - foreach (var permission in defaultUserGroupPermissions.AssignedPermissions.SelectMany(x => x.Value)) - { - permission.Checked = false; - permission.Checked = assignedGroupPermission.AssignedPermissions.Contains(permission.PermissionCode, StringComparer.InvariantCulture); - } - - } - - return defaultPermissionsByGroup; - } - - /// - /// Returns an item to be used to display the recycle bin for content - /// - /// - public ContentItemDisplay GetRecycleBin() - { - var display = new ContentItemDisplay - { - Id = Constants.System.RecycleBinContent, - Alias = "recycleBin", - ParentId = -1, - Name = Services.TextService.Localize("general/recycleBin"), - ContentTypeAlias = "recycleBin", - CreateDate = DateTime.Now, - IsContainer = true, - Path = "-1," + Constants.System.RecycleBinContent - }; - - TabsAndPropertiesResolver.AddListView(display, "content", Services.DataTypeService, Services.TextService); - - return display; - } - - public ContentItemDisplay GetBlueprintById(int id) - { - var foundContent = Services.ContentService.GetBlueprintById(id); - if (foundContent == null) - { - HandleContentNotFound(id); - } - - var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); - - SetupBlueprint(content, foundContent); - - return content; - } - - private static void SetupBlueprint(ContentItemDisplay content, IContent persistedContent) - { - content.AllowPreview = false; - - //set a custom path since the tree that renders this has the content type id as the parent - content.Path = string.Format("-1,{0},{1}", persistedContent.ContentTypeId, content.Id); - - content.AllowedActions = new[] { "A" }; - content.IsBlueprint = true; - - var excludeProps = new[] { "_umb_urls", "_umb_releasedate", "_umb_expiredate", "_umb_template" }; - var propsTab = content.Tabs.Last(); - propsTab.Properties = propsTab.Properties - .Where(p => excludeProps.Contains(p.Alias) == false); - } - - /// - /// Gets the content json for the content id - /// - /// - /// - [OutgoingEditorModelEvent] - [EnsureUserPermissionForContent("id")] - public ContentItemDisplay GetById(int id) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - if (foundContent == null) - { - HandleContentNotFound(id); - } - - var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); - return content; - } - - /// - /// Gets the content json for the content id - /// - /// - /// - [OutgoingEditorModelEvent] - [EnsureUserPermissionForContent("id")] - public ContentItemDisplay GetById(Guid id) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - if (foundContent == null) - { - HandleContentNotFound(id); - } - - var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); - return content; - } - - /// - /// Gets the content json for the content id - /// - /// - /// - [OutgoingEditorModelEvent] - [EnsureUserPermissionForContent("id")] - public ContentItemDisplay GetById(Udi id) - { - var guidUdi = id as GuidUdi; - if (guidUdi != null) - { - return GetById(guidUdi.Guid); - } - - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - [EnsureUserPermissionForContent("id")] - public ContentItemDisplay GetWithTreeDefinition(int id) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - if (foundContent == null) - { - HandleContentNotFound(id); - } - - var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); - return content; - } - - /// - /// Gets an empty content item for the - /// - /// - /// - /// - /// If this is a container type, we'll remove the umbContainerView tab for a new item since - /// it cannot actually list children if it doesn't exist yet. - /// - [OutgoingEditorModelEvent] - public ContentItemDisplay GetEmpty(string contentTypeAlias, int parentId) - { - var contentType = Services.ContentTypeService.GetContentType(contentTypeAlias); - if (contentType == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - var emptyContent = Services.ContentService.CreateContent("", parentId, contentType.Alias, UmbracoUser.Id); - var mapped = AutoMapperExtensions.MapWithUmbracoContext(emptyContent, UmbracoContext); - // translate the content type name if applicable - mapped.ContentTypeName = Services.TextService.UmbracoDictionaryTranslate(mapped.ContentTypeName); - // if your user type doesn't have access to the Settings section it would not get this property mapped - if(mapped.DocumentType != null) - mapped.DocumentType.Name = Services.TextService.UmbracoDictionaryTranslate(mapped.DocumentType.Name); - - //remove this tab if it exists: umbContainerView - var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == Constants.Conventions.PropertyGroups.ListViewGroupName); - mapped.Tabs = mapped.Tabs.Except(new[] { containerTab }); - return mapped; - } - - [OutgoingEditorModelEvent] - public ContentItemDisplay GetEmpty(int blueprintId, int parentId) - { - var blueprint = Services.ContentService.GetBlueprintById(blueprintId); - if (blueprint == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - blueprint.Id = 0; - blueprint.Name = string.Empty; - blueprint.ParentId = parentId; - - var mapped = Mapper.Map(blueprint); - - //remove this tab if it exists: umbContainerView - var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == Constants.Conventions.PropertyGroups.ListViewGroupName); - mapped.Tabs = mapped.Tabs.Except(new[] { containerTab }); - return mapped; - } - - /// - /// Gets the Url for a given node ID - /// - /// - /// - public HttpResponseMessage GetNiceUrl(int id) - { - var url = Umbraco.NiceUrl(id); - var response = Request.CreateResponse(HttpStatusCode.OK); - response.Content = new StringContent(url, Encoding.UTF8, "application/json"); - return response; - } - - /// - /// Gets the Url for a given node ID - /// - /// - /// - public HttpResponseMessage GetNiceUrl(Guid id) - { - var url = Umbraco.UrlProvider.GetUrl(id); - var response = Request.CreateResponse(HttpStatusCode.OK); - response.Content = new StringContent(url, Encoding.UTF8, "application/json"); - return response; - } - - /// - /// Gets the Url for a given node ID - /// - /// - /// - public HttpResponseMessage GetNiceUrl(Udi id) - { - var guidUdi = id as GuidUdi; - if (guidUdi != null) - { - return GetNiceUrl(guidUdi.Guid); - } - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - /// - /// Gets the children for the content id passed in - /// - /// - [FilterAllowedOutgoingContent(typeof(IEnumerable>), "Items")] - public PagedResult> GetChildren( - int id, - int pageNumber = 0, //TODO: This should be '1' as it's not the index - int pageSize = 0, - string orderBy = "SortOrder", - Direction orderDirection = Direction.Ascending, - bool orderBySystemField = true, - string filter = "") - { - return GetChildren(id, null, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter); - } - - /// - /// Gets the children for the content id passed in - /// - /// - [FilterAllowedOutgoingContent(typeof(IEnumerable>), "Items")] - public PagedResult> GetChildren( - int id, - string includeProperties, - int pageNumber = 0, //TODO: This should be '1' as it's not the index - int pageSize = 0, - string orderBy = "SortOrder", - Direction orderDirection = Direction.Ascending, - bool orderBySystemField = true, - string filter = "") - { - long totalChildren; - IContent[] children; - if (pageNumber > 0 && pageSize > 0) - { - children = Services.ContentService - .GetPagedChildren(id, (pageNumber - 1), pageSize, out totalChildren - , orderBy, orderDirection, orderBySystemField, filter).ToArray(); - } - else - { - children = Services.ContentService.GetChildren(id).ToArray(); - totalChildren = children.Length; - } - - if (totalChildren == 0) - { - return new PagedResult>(0, 0, 0); - } - - var pagedResult = new PagedResult>(totalChildren, pageNumber, pageSize); - pagedResult.Items = children.Select(content => - Mapper.Map>(content, - opts => - { - // if there's a list of property aliases to map - we will make sure to store this in the mapping context. - if (String.IsNullOrWhiteSpace(includeProperties) == false) - { - opts.Items["IncludeProperties"] = includeProperties.Split(new[] { ", ", "," }, StringSplitOptions.RemoveEmptyEntries); - } - })); - - return pagedResult; - } - - [Obsolete("Dont use this, it is incorrectly named, use HasPermission instead")] - public bool GetHasPermission(string permissionToCheck, int nodeId) - { - return HasPermission(permissionToCheck, nodeId); - } - - /// - /// Returns permissions for all nodes passed in for the current user - /// TODO: This should be moved to the CurrentUserController? - /// - /// - /// - [HttpPost] - public Dictionary GetPermissions(int[] nodeIds) - { - var permissions = Services.UserService - .GetPermissions(Security.CurrentUser, nodeIds); - - var permissionsDictionary = new Dictionary(); - foreach (var nodeId in nodeIds) - { - var aggregatePerms = permissions.GetAllPermissions(nodeId).ToArray(); - permissionsDictionary.Add(nodeId, aggregatePerms); - } - - return permissionsDictionary; - } - - /// - /// Checks a nodes permission for the current user - /// TODO: This should be moved to the CurrentUserController? - /// - /// - /// - /// - [HttpGet] - public bool HasPermission(string permissionToCheck, int nodeId) - { - var p = Services.UserService.GetPermissions(Security.CurrentUser, nodeId).GetAllPermissions(); - if (p.Contains(permissionToCheck.ToString(CultureInfo.InvariantCulture))) - { - return true; - } - - return false; - } - - /// - /// Creates a blueprint from a content item - /// - /// The content id to copy - /// The name of the blueprint - /// - [HttpPost] - public SimpleNotificationModel CreateBlueprintFromContent([FromUri]int contentId, [FromUri]string name) - { - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", "name"); - - var content = Services.ContentService.GetById(contentId); - if (content == null) - throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - - EnsureUniqueName(name, content, "name"); - - var blueprint = Services.ContentService.CreateContentFromBlueprint(content, name, Security.CurrentUser.Id); - - Services.ContentService.SaveBlueprint(blueprint, Security.CurrentUser.Id); - - var notificationModel = new SimpleNotificationModel(); - notificationModel.AddSuccessNotification( - Services.TextService.Localize("blueprints/createdBlueprintHeading"), - Services.TextService.Localize("blueprints/createdBlueprintMessage", new[] { content.Name }) - ); - - return notificationModel; - } - - private void EnsureUniqueName(string name, IContent content, string modelName) - { - var existing = Services.ContentService.GetBlueprintsForContentTypes(content.ContentTypeId); - if (existing.Any(x => x.Name == name && x.Id != content.Id)) - { - ModelState.AddModelError(modelName, Services.TextService.Localize("blueprints/duplicateBlueprintMessage")); - throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); - } - } - - /// - /// Saves content - /// - /// - [FileUploadCleanupFilter] - [ContentPostValidate] - public ContentItemDisplay PostSaveBlueprint( - [ModelBinder(typeof(BlueprintItemBinder))] ContentItemSave contentItem) - { - var contentItemDisplay = PostSaveInternal(contentItem, - content => - { - EnsureUniqueName(content.Name, content, "Name"); - - Services.ContentService.SaveBlueprint(contentItem.PersistedContent, Security.CurrentUser.Id); - //we need to reuse the underlying logic so return the result that it wants - return Attempt.Succeed(new OperationStatus(OperationStatusType.Success, new EventMessages())); - }); - SetupBlueprint(contentItemDisplay, contentItemDisplay.PersistedContent); - - return contentItemDisplay; - } - - /// - /// Saves content - /// - /// - [FileUploadCleanupFilter] - [ContentPostValidate] - [OutgoingEditorModelEvent] - public ContentItemDisplay PostSave( - [ModelBinder(typeof(ContentItemBinder))] - ContentItemSave contentItem) - { - return PostSaveInternal(contentItem, - content => Services.ContentService.WithResult().Save(contentItem.PersistedContent, Security.CurrentUser.Id)); - } - - private ContentItemDisplay PostSaveInternal(ContentItemSave contentItem, Func> saveMethod) - { - //Recent versions of IE/Edge may send in the full clientside file path instead of just the file name. - //To ensure similar behavior across all browsers no matter what they do - we strip the FileName property of all - //uploaded files to being *only* the actual file name (as it should be). - if (contentItem.UploadedFiles != null && contentItem.UploadedFiles.Any()) - { - foreach (var file in contentItem.UploadedFiles) - { - file.FileName = Path.GetFileName(file.FileName); - } - } - - //If we've reached here it means: - // * Our model has been bound - // * and validated - // * any file attachments have been saved to their temporary location for us to use - // * we have a reference to the DTO object and the persisted object - // * Permissions are valid - MapPropertyValues(contentItem); - - //We need to manually check the validation results here because: - // * We still need to save the entity even if there are validation value errors - // * Depending on if the entity is new, and if there are non property validation errors (i.e. the name is null) - // then we cannot continue saving, we can only display errors - // * If there are validation errors and they were attempting to publish, we can only save, NOT publish and display - // a message indicating this - if (ModelState.IsValid == false) - { - if (ValidationHelper.ModelHasRequiredForPersistenceErrors(contentItem) && IsCreatingAction(contentItem.Action)) - { - //ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue! - // add the modelstate to the outgoing object and throw a validation message - var forDisplay = AutoMapperExtensions.MapWithUmbracoContext(contentItem.PersistedContent, UmbracoContext); - forDisplay.Errors = ModelState.ToErrorDictionary(); - throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); - - } - - //if the model state is not valid we cannot publish so change it to save - switch (contentItem.Action) - { - case ContentSaveAction.Publish: - contentItem.Action = ContentSaveAction.Save; - break; - case ContentSaveAction.PublishNew: - contentItem.Action = ContentSaveAction.SaveNew; - break; - } - } - - //initialize this to successful - var publishStatus = Attempt.Succeed(); - var wasCancelled = false; - - if (contentItem.Action == ContentSaveAction.Save || contentItem.Action == ContentSaveAction.SaveNew) - { - //save the item - var saveResult = saveMethod(contentItem.PersistedContent); - - wasCancelled = saveResult.Success == false && saveResult.Result.StatusType == OperationStatusType.FailedCancelledByEvent; - } - else if (contentItem.Action == ContentSaveAction.SendPublish || contentItem.Action == ContentSaveAction.SendPublishNew) - { - var sendResult = Services.ContentService.SendToPublication(contentItem.PersistedContent, Security.CurrentUser.Id); - wasCancelled = sendResult == false; - } - else - { - //publish the item and check if it worked, if not we will show a diff msg below - publishStatus = Services.ContentService.SaveAndPublishWithStatus(contentItem.PersistedContent, Security.CurrentUser.Id); - wasCancelled = publishStatus.Result.StatusType == PublishStatusType.FailedCancelledByEvent; - } - - //return the updated model - var display = AutoMapperExtensions.MapWithUmbracoContext(contentItem.PersistedContent, UmbracoContext); - - //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403 - HandleInvalidModelState(display); - - //put the correct msgs in - switch (contentItem.Action) - { - case ContentSaveAction.Save: - case ContentSaveAction.SaveNew: - if (wasCancelled == false) - { - display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentSavedHeader"), - contentItem.ReleaseDate.HasValue - ? Services.TextService.Localize("speechBubbles/editContentSavedWithReleaseDateText", new [] { contentItem.ReleaseDate.Value.ToLongDateString(), contentItem.ReleaseDate.Value.ToShortTimeString() }) - : Services.TextService.Localize("speechBubbles/editContentSavedText") - ); - } - else - { - AddCancelMessage(display); - } - break; - case ContentSaveAction.SendPublish: - case ContentSaveAction.SendPublishNew: - if (wasCancelled == false) - { - display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentSendToPublish"), - Services.TextService.Localize("speechBubbles/editContentSendToPublishText")); - } - else - { - AddCancelMessage(display); - } - break; - case ContentSaveAction.Publish: - case ContentSaveAction.PublishNew: - ShowMessageForPublishStatus(publishStatus.Result, display, contentItem.ExpireDate); - break; - } - - //If the item is new and the operation was cancelled, we need to return a different - // status code so the UI can handle it since it won't be able to redirect since there - // is no Id to redirect to! - if (wasCancelled && IsCreatingAction(contentItem.Action)) - { - throw new HttpResponseException(Request.CreateValidationErrorResponse(display)); - } - - display.PersistedContent = contentItem.PersistedContent; - - return display; - } - - /// - /// Publishes a document with a given ID - /// - /// - /// - /// - /// The CanAccessContentAuthorize attribute will deny access to this method if the current user - /// does not have Publish access to this node. - /// - /// - [EnsureUserPermissionForContent("id", 'U')] - public HttpResponseMessage PostPublishById(int id) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - - if (foundContent == null) - { - return HandleContentNotFound(id, false); - } - - var publishResult = Services.ContentService.PublishWithStatus(foundContent, Security.CurrentUser.Id); - if (publishResult.Success == false) - { - var notificationModel = new SimpleNotificationModel(); - ShowMessageForPublishStatus(publishResult.Result, notificationModel, foundContent.ExpireDate); - return Request.CreateValidationErrorResponse(notificationModel); - } - - //return ok - return Request.CreateResponse(HttpStatusCode.OK); - - } - - [HttpDelete] - [HttpPost] - public HttpResponseMessage DeleteBlueprint(int id) - { - var found = Services.ContentService.GetBlueprintById(id); - - if (found == null) - { - return HandleContentNotFound(id, false); - } - - Services.ContentService.DeleteBlueprint(found); - - return Request.CreateResponse(HttpStatusCode.OK); - } - - /// - /// Moves an item to the recycle bin, if it is already there then it will permanently delete it - /// - /// - /// - /// - /// The CanAccessContentAuthorize attribute will deny access to this method if the current user - /// does not have Delete access to this node. - /// - [EnsureUserPermissionForContent("id", 'D')] - [HttpDelete] - [HttpPost] - public HttpResponseMessage DeleteById(int id) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - - if (foundContent == null) - { - return HandleContentNotFound(id, false); - } - - //if the current item is in the recycle bin - if (foundContent.IsInRecycleBin() == false) - { - var moveResult = Services.ContentService.WithResult().MoveToRecycleBin(foundContent, Security.CurrentUser.Id); - if (moveResult == false) - { - //returning an object of INotificationModel will ensure that any pending - // notification messages are added to the response. - return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); - } - } - else - { - var deleteResult = Services.ContentService.WithResult().Delete(foundContent, Security.CurrentUser.Id); - if (deleteResult == false) - { - //returning an object of INotificationModel will ensure that any pending - // notification messages are added to the response. - return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); - } - } - - return Request.CreateResponse(HttpStatusCode.OK); - } - - /// - /// Empties the recycle bin - /// - /// - /// - /// attributed with EnsureUserPermissionForContent to verify the user has access to the recycle bin - /// - [HttpDelete] - [HttpPost] - [EnsureUserPermissionForContent(Constants.System.RecycleBinContent, 'D')] - public HttpResponseMessage EmptyRecycleBin() - { - Services.ContentService.EmptyRecycleBin(Security.CurrentUser.Id); - - return Request.CreateNotificationSuccessResponse(Services.TextService.Localize("defaultdialogs/recycleBinIsEmpty")); - } - - /// - /// Change the sort order for content - /// - /// - /// - [EnsureUserPermissionForContent("sorted.ParentId", 'S')] - public HttpResponseMessage PostSort(ContentSortOrder sorted) - { - if (sorted == null) - { - return Request.CreateResponse(HttpStatusCode.NotFound); - } - - //if there's nothing to sort just return ok - if (sorted.IdSortOrder.Length == 0) - { - return Request.CreateResponse(HttpStatusCode.OK); - } - - try - { - var contentService = Services.ContentService; - - // Save content with new sort order and update content xml in db accordingly - if (contentService.Sort(sorted.IdSortOrder, Security.CurrentUser.Id) == false) - { - LogHelper.Warn("Content sorting failed, this was probably caused by an event being cancelled"); - return Request.CreateValidationErrorResponse("Content sorting failed, this was probably caused by an event being cancelled"); - } - return Request.CreateResponse(HttpStatusCode.OK); - } - catch (Exception ex) - { - LogHelper.Error("Could not update content sort order", ex); - throw; - } - } - - /// - /// Change the sort order for media - /// - /// - /// - [EnsureUserPermissionForContent("move.ParentId", 'M')] - public HttpResponseMessage PostMove(MoveOrCopy move) - { - var toMove = ValidateMoveOrCopy(move); - - Services.ContentService.Move(toMove, move.ParentId, Security.CurrentUser.Id); - - var response = Request.CreateResponse(HttpStatusCode.OK); - response.Content = new StringContent(toMove.Path, Encoding.UTF8, "application/json"); - return response; - } - - /// - /// Copies a content item and places the copy as a child of a given parent Id - /// - /// - /// - [EnsureUserPermissionForContent("copy.ParentId", 'C')] - public HttpResponseMessage PostCopy(MoveOrCopy copy) - { - var toCopy = ValidateMoveOrCopy(copy); - - var c = Services.ContentService.Copy(toCopy, copy.ParentId, copy.RelateToOriginal, copy.Recursive, Security.CurrentUser.Id); - - var response = Request.CreateResponse(HttpStatusCode.OK); - response.Content = new StringContent(c.Path, Encoding.UTF8, "application/json"); - return response; - } - - /// - /// Unpublishes a node with a given Id and returns the unpublished entity - /// - /// - /// - [EnsureUserPermissionForContent("id", 'U')] - [OutgoingEditorModelEvent] - public ContentItemDisplay PostUnPublish(int id) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - - if (foundContent == null) - HandleContentNotFound(id); - - var unpublishResult = Services.ContentService.WithResult().UnPublish(foundContent, Security.CurrentUser.Id); - - var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); - - if (unpublishResult == false) - { - AddCancelMessage(content); - throw new HttpResponseException(Request.CreateValidationErrorResponse(content)); - } - else - { - content.AddSuccessNotification(Services.TextService.Localize("content/unPublish"), Services.TextService.Localize("speechBubbles/contentUnpublished")); - return content; - } - } - - /// - /// Maps the dto property values to the persisted model - /// - /// - private void MapPropertyValues(ContentItemSave contentItem) - { - UpdateName(contentItem); - - //TODO: We need to support 'send to publish' - - contentItem.PersistedContent.ExpireDate = contentItem.ExpireDate; - contentItem.PersistedContent.ReleaseDate = contentItem.ReleaseDate; - //only set the template if it didn't change - var templateChanged = (contentItem.PersistedContent.Template == null && contentItem.TemplateAlias.IsNullOrWhiteSpace() == false) - || (contentItem.PersistedContent.Template != null && contentItem.PersistedContent.Template.Alias != contentItem.TemplateAlias) - || (contentItem.PersistedContent.Template != null && contentItem.TemplateAlias.IsNullOrWhiteSpace()); - if (templateChanged) - { - var template = Services.FileService.GetTemplate(contentItem.TemplateAlias); - if (template == null && contentItem.TemplateAlias.IsNullOrWhiteSpace() == false) - { - //ModelState.AddModelError("Template", "No template exists with the specified alias: " + contentItem.TemplateAlias); - LogHelper.Warn("No template exists with the specified alias: " + contentItem.TemplateAlias); - } - else - { - //NOTE: this could be null if there was a template and the posted template is null, this should remove the assigned template - contentItem.PersistedContent.Template = template; - } - } - - base.MapPropertyValues(contentItem); - } - - /// - /// Ensures the item can be moved/copied to the new location - /// - /// - /// - private IContent ValidateMoveOrCopy(MoveOrCopy model) - { - if (model == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - var contentService = Services.ContentService; - var toMove = contentService.GetById(model.Id); - if (toMove == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - if (model.ParentId < 0) - { - //cannot move if the content item is not allowed at the root - if (toMove.ContentType.AllowedAsRoot == false) - { - throw new HttpResponseException( - Request.CreateNotificationValidationErrorResponse( - Services.TextService.Localize("moveOrCopy/notAllowedAtRoot"))); - } - } - else - { - var parent = contentService.GetById(model.ParentId); - if (parent == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - //check if the item is allowed under this one - if (parent.ContentType.AllowedContentTypes.Select(x => x.Id).ToArray() - .Any(x => x.Value == toMove.ContentType.Id) == false) - { - throw new HttpResponseException( - Request.CreateNotificationValidationErrorResponse( - Services.TextService.Localize("moveOrCopy/notAllowedByContentType"))); - } - - // Check on paths - if ((string.Format(",{0},", parent.Path)).IndexOf(string.Format(",{0},", toMove.Id), StringComparison.Ordinal) > -1) - { - throw new HttpResponseException( - Request.CreateNotificationValidationErrorResponse( - Services.TextService.Localize("moveOrCopy/notAllowedByPath"))); - } - } - - return toMove; - } - - private void ShowMessageForPublishStatus(PublishStatus status, INotificationModel display, DateTime? expireDate) - { - switch (status.StatusType) - { - case PublishStatusType.Success: - case PublishStatusType.SuccessAlreadyPublished: - display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentPublishedHeader"), - expireDate.HasValue - ? Services.TextService.Localize("speechBubbles/editContentPublishedWithExpireDateText", new [] { expireDate.Value.ToLongDateString(), expireDate.Value.ToShortTimeString() }) - : Services.TextService.Localize("speechBubbles/editContentPublishedText") - ); - break; - case PublishStatusType.FailedPathNotPublished: - display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedByParent", - new[] { string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id) }).Trim()); - break; - case PublishStatusType.FailedCancelledByEvent: - AddCancelMessage(display, "publish", "speechBubbles/contentPublishedFailedByEvent"); - break; - case PublishStatusType.FailedAwaitingRelease: - display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedAwaitingRelease", - new[] { string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id) }).Trim()); - break; - case PublishStatusType.FailedHasExpired: - display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedExpired", - new[] - { - string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id), - }).Trim()); - break; - case PublishStatusType.FailedIsTrashed: - //TODO: We should add proper error messaging for this! - break; - case PublishStatusType.FailedContentInvalid: - display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedInvalid", - new[] - { - string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id), - string.Join(",", status.InvalidProperties.Select(x => x.Alias)) - }).Trim()); - break; - default: - throw new IndexOutOfRangeException(); - } - } - - - /// - /// Performs a permissions check for the user to check if it has access to the node based on - /// start node and/or permissions for the node - /// - /// The storage to add the content item to so it can be reused - /// - /// - /// - /// - /// The content to lookup, if the contentItem is not specified - /// - /// Specifies the already resolved content item to check against - /// - internal static bool CheckPermissions( - IDictionary storage, - IUser user, - IUserService userService, - IContentService contentService, - IEntityService entityService, - int nodeId, - char[] permissionsToCheck = null, - IContent contentItem = null) - { - if (storage == null) throw new ArgumentNullException("storage"); - if (user == null) throw new ArgumentNullException("user"); - if (userService == null) throw new ArgumentNullException("userService"); - if (contentService == null) throw new ArgumentNullException("contentService"); - if (entityService == null) throw new ArgumentNullException("entityService"); - - if (contentItem == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinContent) - { - contentItem = contentService.GetById(nodeId); - //put the content item into storage so it can be retreived - // in the controller (saves a lookup) - storage[typeof(IContent).ToString()] = contentItem; - } - - if (contentItem == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinContent) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - var hasPathAccess = (nodeId == Constants.System.Root) - ? user.HasContentRootAccess(entityService) - : (nodeId == Constants.System.RecycleBinContent) - ? user.HasContentBinAccess(entityService) - : user.HasPathAccess(contentItem, entityService); - - if (hasPathAccess == false) - { - return false; - } - - if (permissionsToCheck == null || permissionsToCheck.Length == 0) - { - return true; - } - - //get the implicit/inherited permissions for the user for this path, - //if there is no content item for this id, than just use the id as the path (i.e. -1 or -20) - var path = contentItem != null ? contentItem.Path : nodeId.ToString(); - var permission = userService.GetPermissionsForPath(user, path); - - var allowed = true; - foreach (var p in permissionsToCheck) - { - if (permission == null - || permission.GetAllPermissions().Contains(p.ToString(CultureInfo.InvariantCulture)) == false) - { - allowed = false; - } - } - return allowed; - } - - [EnsureUserPermissionForContent("contentId", 'F')] - public IEnumerable GetNotificationOptions(int contentId) - { - var notifications = new List(); - if (contentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - - var content = Services.ContentService.GetById(contentId); - if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - - var actionList = ActionsResolver.Current.Actions; - foreach (var a in actionList) - { - if (a.ShowInNotifier) - { - NotifySetting n = new NotifySetting - { - Name = ui.Text("actions", a.Alias), - Checked = (UmbracoUser.GetNotifications(content.Path).IndexOf(a.Letter) > -1), - NotifyCode = a.Letter.ToString() - }; - notifications.Add(n); - } - } - return notifications; - } - - public void PostNotificationOptions(int contentId, string notifyOptions = "") - { - if (contentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - var content = Services.ContentService.GetById(contentId); - - if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - var node = new CMSNode(contentId); - - global::umbraco.cms.businesslogic.workflow.Notification.UpdateNotifications(UmbracoUser, node, notifyOptions ?? ""); - } - } -} +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Web.Http; +using System.Web.Http.Controllers; +using System.Web.Http.ModelBinding; +using AutoMapper; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Publishing; +using Umbraco.Core.Services; +using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Models.Mapping; +using Umbraco.Web.Mvc; +using Umbraco.Web.WebApi; +using Umbraco.Web.WebApi.Binders; +using Umbraco.Web.WebApi.Filters; +using Umbraco.Core.Events; +using Constants = Umbraco.Core.Constants; +using umbraco.cms.businesslogic; +using System.Collections; +using umbraco; + +namespace Umbraco.Web.Editors +{ + /// + /// The API controller used for editing content + /// + /// + /// This controller is decorated with the UmbracoApplicationAuthorizeAttribute which means that any user requesting + /// access to ALL of the methods on this controller will need access to the content application. + /// + [PluginController("UmbracoApi")] + [UmbracoApplicationAuthorize(Constants.Applications.Content)] + [ContentControllerConfiguration] + public class ContentController : ContentControllerBase + { + /// + /// Constructor + /// + public ContentController() + : this(UmbracoContext.Current) + { + } + + /// + /// Constructor + /// + /// + public ContentController(UmbracoContext umbracoContext) + : base(umbracoContext) + { + } + + /// + /// Configures this controller with a custom action selector + /// + private class ContentControllerConfigurationAttribute : Attribute, IControllerConfiguration + { + public void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor) + { + controllerSettings.Services.Replace(typeof(IHttpActionSelector), new ParameterSwapControllerActionSelector( + new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetNiceUrl", "id", typeof(int), typeof(Guid), typeof(Udi)), + new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetById", "id", typeof(int), typeof(Guid), typeof(Udi)) + )); + } + } + + /// + /// Return content for the specified ids + /// + /// + /// + [FilterAllowedOutgoingContent(typeof(IEnumerable))] + public IEnumerable GetByIds([FromUri]int[] ids) + { + var foundContent = Services.ContentService.GetByIds(ids); + return foundContent.Select(content => AutoMapperExtensions.MapWithUmbracoContext(content, UmbracoContext)); + } + + /// + /// Updates the permissions for a content item for a particular user group + /// + /// + /// + /// + /// Permission check is done for letter 'R' which is for which the user must have access to to update + /// + [EnsureUserPermissionForContent("saveModel.ContentId", 'R')] + public IEnumerable PostSaveUserGroupPermissions(UserGroupPermissionsSave saveModel) + { + if (saveModel.ContentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + + //TODO: Should non-admins be alowed to set granular permissions? + + var content = Services.ContentService.GetById(saveModel.ContentId); + if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + + //current permissions explicitly assigned to this content item + var contentPermissions = Services.ContentService.GetPermissionsForEntity(content) + .ToDictionary(x => x.UserGroupId, x => x); + + var allUserGroups = Services.UserService.GetAllUserGroups().ToArray(); + + //loop through each user group + foreach (var userGroup in allUserGroups) + { + //check if there's a permission set posted up for this user group + IEnumerable groupPermissions; + if (saveModel.AssignedPermissions.TryGetValue(userGroup.Id, out groupPermissions)) + { + //create a string collection of the assigned letters + var groupPermissionCodes = groupPermissions.ToArray(); + + //check if there are no permissions assigned for this group save model, if that is the case we want to reset the permissions + //for this group/node which will go back to the defaults + if (groupPermissionCodes.Length == 0) + { + Services.UserService.RemoveUserGroupPermissions(userGroup.Id, content.Id); + } + //check if they are the defaults, if so we should just remove them if they exist since it's more overhead having them stored + else if (userGroup.Permissions.UnsortedSequenceEqual(groupPermissionCodes)) + { + //only remove them if they are actually currently assigned + if (contentPermissions.ContainsKey(userGroup.Id)) + { + //remove these permissions from this node for this group since the ones being assigned are the same as the defaults + Services.UserService.RemoveUserGroupPermissions(userGroup.Id, content.Id); + } + } + //if they are different we need to update, otherwise there's nothing to update + else if (contentPermissions.ContainsKey(userGroup.Id) == false || contentPermissions[userGroup.Id].AssignedPermissions.UnsortedSequenceEqual(groupPermissionCodes) == false) + { + + Services.UserService.ReplaceUserGroupPermissions(userGroup.Id, groupPermissionCodes.Select(x => x[0]), content.Id); + } + } + } + + return GetDetailedPermissions(content, allUserGroups); + } + + /// + /// Returns the user group permissions for user groups assigned to this node + /// + /// + /// + /// + /// Permission check is done for letter 'R' which is for which the user must have access to to view + /// + [EnsureUserPermissionForContent("contentId", 'R')] + public IEnumerable GetDetailedPermissions(int contentId) + { + if (contentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + var content = Services.ContentService.GetById(contentId); + if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + + //TODO: Should non-admins be able to see detailed permissions? + + var allUserGroups = Services.UserService.GetAllUserGroups(); + + return GetDetailedPermissions(content, allUserGroups); + } + + private IEnumerable GetDetailedPermissions(IContent content, IEnumerable allUserGroups) + { + //get all user groups and map their default permissions to the AssignedUserGroupPermissions model. + //we do this because not all groups will have true assigned permissions for this node so if they don't have assigned permissions, we need to show the defaults. + + var defaultPermissionsByGroup = Mapper.Map>(allUserGroups).ToArray(); + + var defaultPermissionsAsDictionary = defaultPermissionsByGroup + .ToDictionary(x => Convert.ToInt32(x.Id), x => x); + + //get the actual assigned permissions + var assignedPermissionsByGroup = Services.ContentService.GetPermissionsForEntity(content).ToArray(); + + //iterate over assigned and update the defaults with the real values + foreach (var assignedGroupPermission in assignedPermissionsByGroup) + { + var defaultUserGroupPermissions = defaultPermissionsAsDictionary[assignedGroupPermission.UserGroupId]; + + //clone the default permissions model to the assigned ones + defaultUserGroupPermissions.AssignedPermissions = AssignedUserGroupPermissions.ClonePermissions(defaultUserGroupPermissions.DefaultPermissions); + + //since there is custom permissions assigned to this node for this group, we need to clear all of the default permissions + //and we'll re-check it if it's one of the explicitly assigned ones + foreach (var permission in defaultUserGroupPermissions.AssignedPermissions.SelectMany(x => x.Value)) + { + permission.Checked = false; + permission.Checked = assignedGroupPermission.AssignedPermissions.Contains(permission.PermissionCode, StringComparer.InvariantCulture); + } + + } + + return defaultPermissionsByGroup; + } + + /// + /// Returns an item to be used to display the recycle bin for content + /// + /// + public ContentItemDisplay GetRecycleBin() + { + var display = new ContentItemDisplay + { + Id = Constants.System.RecycleBinContent, + Alias = "recycleBin", + ParentId = -1, + Name = Services.TextService.Localize("general/recycleBin"), + ContentTypeAlias = "recycleBin", + CreateDate = DateTime.Now, + IsContainer = true, + Path = "-1," + Constants.System.RecycleBinContent + }; + + TabsAndPropertiesResolver.AddListView(display, "content", Services.DataTypeService, Services.TextService); + + return display; + } + + public ContentItemDisplay GetBlueprintById(int id) + { + var foundContent = Services.ContentService.GetBlueprintById(id); + if (foundContent == null) + { + HandleContentNotFound(id); + } + + var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); + + SetupBlueprint(content, foundContent); + + return content; + } + + private static void SetupBlueprint(ContentItemDisplay content, IContent persistedContent) + { + content.AllowPreview = false; + + //set a custom path since the tree that renders this has the content type id as the parent + content.Path = string.Format("-1,{0},{1}", persistedContent.ContentTypeId, content.Id); + + content.AllowedActions = new[] { "A" }; + content.IsBlueprint = true; + + var excludeProps = new[] { "_umb_urls", "_umb_releasedate", "_umb_expiredate", "_umb_template" }; + var propsTab = content.Tabs.Last(); + propsTab.Properties = propsTab.Properties + .Where(p => excludeProps.Contains(p.Alias) == false); + } + + /// + /// Gets the content json for the content id + /// + /// + /// + [OutgoingEditorModelEvent] + [EnsureUserPermissionForContent("id")] + public ContentItemDisplay GetById(int id) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + if (foundContent == null) + { + HandleContentNotFound(id); + } + + var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); + return content; + } + + /// + /// Gets the content json for the content id + /// + /// + /// + [OutgoingEditorModelEvent] + [EnsureUserPermissionForContent("id")] + public ContentItemDisplay GetById(Guid id) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + if (foundContent == null) + { + HandleContentNotFound(id); + } + + var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); + return content; + } + + /// + /// Gets the content json for the content id + /// + /// + /// + [OutgoingEditorModelEvent] + [EnsureUserPermissionForContent("id")] + public ContentItemDisplay GetById(Udi id) + { + var guidUdi = id as GuidUdi; + if (guidUdi != null) + { + return GetById(guidUdi.Guid); + } + + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + [EnsureUserPermissionForContent("id")] + public ContentItemDisplay GetWithTreeDefinition(int id) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + if (foundContent == null) + { + HandleContentNotFound(id); + } + + var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); + return content; + } + + /// + /// Gets an empty content item for the + /// + /// + /// + /// + /// If this is a container type, we'll remove the umbContainerView tab for a new item since + /// it cannot actually list children if it doesn't exist yet. + /// + [OutgoingEditorModelEvent] + public ContentItemDisplay GetEmpty(string contentTypeAlias, int parentId) + { + var contentType = Services.ContentTypeService.GetContentType(contentTypeAlias); + if (contentType == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + var emptyContent = Services.ContentService.CreateContent("", parentId, contentType.Alias, UmbracoUser.Id); + var mapped = AutoMapperExtensions.MapWithUmbracoContext(emptyContent, UmbracoContext); + // translate the content type name if applicable + mapped.ContentTypeName = Services.TextService.UmbracoDictionaryTranslate(mapped.ContentTypeName); + // if your user type doesn't have access to the Settings section it would not get this property mapped + if(mapped.DocumentType != null) + mapped.DocumentType.Name = Services.TextService.UmbracoDictionaryTranslate(mapped.DocumentType.Name); + + //remove this tab if it exists: umbContainerView + var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == Constants.Conventions.PropertyGroups.ListViewGroupName); + mapped.Tabs = mapped.Tabs.Except(new[] { containerTab }); + return mapped; + } + + [OutgoingEditorModelEvent] + public ContentItemDisplay GetEmpty(int blueprintId, int parentId) + { + var blueprint = Services.ContentService.GetBlueprintById(blueprintId); + if (blueprint == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + blueprint.Id = 0; + blueprint.Name = string.Empty; + blueprint.ParentId = parentId; + + var mapped = Mapper.Map(blueprint); + + //remove this tab if it exists: umbContainerView + var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == Constants.Conventions.PropertyGroups.ListViewGroupName); + mapped.Tabs = mapped.Tabs.Except(new[] { containerTab }); + return mapped; + } + + /// + /// Gets the Url for a given node ID + /// + /// + /// + public HttpResponseMessage GetNiceUrl(int id) + { + var url = Umbraco.NiceUrl(id); + var response = Request.CreateResponse(HttpStatusCode.OK); + response.Content = new StringContent(url, Encoding.UTF8, "application/json"); + return response; + } + + /// + /// Gets the Url for a given node ID + /// + /// + /// + public HttpResponseMessage GetNiceUrl(Guid id) + { + var url = Umbraco.UrlProvider.GetUrl(id); + var response = Request.CreateResponse(HttpStatusCode.OK); + response.Content = new StringContent(url, Encoding.UTF8, "application/json"); + return response; + } + + /// + /// Gets the Url for a given node ID + /// + /// + /// + public HttpResponseMessage GetNiceUrl(Udi id) + { + var guidUdi = id as GuidUdi; + if (guidUdi != null) + { + return GetNiceUrl(guidUdi.Guid); + } + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + /// + /// Gets the children for the content id passed in + /// + /// + [FilterAllowedOutgoingContent(typeof(IEnumerable>), "Items")] + public PagedResult> GetChildren( + int id, + int pageNumber = 0, //TODO: This should be '1' as it's not the index + int pageSize = 0, + string orderBy = "SortOrder", + Direction orderDirection = Direction.Ascending, + bool orderBySystemField = true, + string filter = "") + { + return GetChildren(id, null, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter); + } + + /// + /// Gets the children for the content id passed in + /// + /// + [FilterAllowedOutgoingContent(typeof(IEnumerable>), "Items")] + public PagedResult> GetChildren( + int id, + string includeProperties, + int pageNumber = 0, //TODO: This should be '1' as it's not the index + int pageSize = 0, + string orderBy = "SortOrder", + Direction orderDirection = Direction.Ascending, + bool orderBySystemField = true, + string filter = "") + { + long totalChildren; + IContent[] children; + if (pageNumber > 0 && pageSize > 0) + { + children = Services.ContentService + .GetPagedChildren(id, (pageNumber - 1), pageSize, out totalChildren + , orderBy, orderDirection, orderBySystemField, filter).ToArray(); + } + else + { + children = Services.ContentService.GetChildren(id).ToArray(); + totalChildren = children.Length; + } + + if (totalChildren == 0) + { + return new PagedResult>(0, 0, 0); + } + + var pagedResult = new PagedResult>(totalChildren, pageNumber, pageSize); + pagedResult.Items = children.Select(content => + Mapper.Map>(content, + opts => + { + // if there's a list of property aliases to map - we will make sure to store this in the mapping context. + if (String.IsNullOrWhiteSpace(includeProperties) == false) + { + opts.Items["IncludeProperties"] = includeProperties.Split(new[] { ", ", "," }, StringSplitOptions.RemoveEmptyEntries); + } + })); + + return pagedResult; + } + + [Obsolete("Dont use this, it is incorrectly named, use HasPermission instead")] + public bool GetHasPermission(string permissionToCheck, int nodeId) + { + return HasPermission(permissionToCheck, nodeId); + } + + /// + /// Returns permissions for all nodes passed in for the current user + /// TODO: This should be moved to the CurrentUserController? + /// + /// + /// + [HttpPost] + public Dictionary GetPermissions(int[] nodeIds) + { + var permissions = Services.UserService + .GetPermissions(Security.CurrentUser, nodeIds); + + var permissionsDictionary = new Dictionary(); + foreach (var nodeId in nodeIds) + { + var aggregatePerms = permissions.GetAllPermissions(nodeId).ToArray(); + permissionsDictionary.Add(nodeId, aggregatePerms); + } + + return permissionsDictionary; + } + + /// + /// Checks a nodes permission for the current user + /// TODO: This should be moved to the CurrentUserController? + /// + /// + /// + /// + [HttpGet] + public bool HasPermission(string permissionToCheck, int nodeId) + { + var p = Services.UserService.GetPermissions(Security.CurrentUser, nodeId).GetAllPermissions(); + if (p.Contains(permissionToCheck.ToString(CultureInfo.InvariantCulture))) + { + return true; + } + + return false; + } + + /// + /// Creates a blueprint from a content item + /// + /// The content id to copy + /// The name of the blueprint + /// + [HttpPost] + public SimpleNotificationModel CreateBlueprintFromContent([FromUri]int contentId, [FromUri]string name) + { + if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", "name"); + + var content = Services.ContentService.GetById(contentId); + if (content == null) + throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + + EnsureUniqueName(name, content, "name"); + + var blueprint = Services.ContentService.CreateContentFromBlueprint(content, name, Security.CurrentUser.Id); + + Services.ContentService.SaveBlueprint(blueprint, Security.CurrentUser.Id); + + var notificationModel = new SimpleNotificationModel(); + notificationModel.AddSuccessNotification( + Services.TextService.Localize("blueprints/createdBlueprintHeading"), + Services.TextService.Localize("blueprints/createdBlueprintMessage", new[] { content.Name }) + ); + + return notificationModel; + } + + private void EnsureUniqueName(string name, IContent content, string modelName) + { + var existing = Services.ContentService.GetBlueprintsForContentTypes(content.ContentTypeId); + if (existing.Any(x => x.Name == name && x.Id != content.Id)) + { + ModelState.AddModelError(modelName, Services.TextService.Localize("blueprints/duplicateBlueprintMessage")); + throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); + } + } + + /// + /// Saves content + /// + /// + [FileUploadCleanupFilter] + [ContentPostValidate] + public ContentItemDisplay PostSaveBlueprint( + [ModelBinder(typeof(BlueprintItemBinder))] ContentItemSave contentItem) + { + var contentItemDisplay = PostSaveInternal(contentItem, + content => + { + EnsureUniqueName(content.Name, content, "Name"); + + Services.ContentService.SaveBlueprint(contentItem.PersistedContent, Security.CurrentUser.Id); + //we need to reuse the underlying logic so return the result that it wants + return Attempt.Succeed(new OperationStatus(OperationStatusType.Success, new EventMessages())); + }); + SetupBlueprint(contentItemDisplay, contentItemDisplay.PersistedContent); + + return contentItemDisplay; + } + + /// + /// Saves content + /// + /// + [FileUploadCleanupFilter] + [ContentPostValidate] + [OutgoingEditorModelEvent] + public ContentItemDisplay PostSave( + [ModelBinder(typeof(ContentItemBinder))] + ContentItemSave contentItem) + { + return PostSaveInternal(contentItem, + content => Services.ContentService.WithResult().Save(contentItem.PersistedContent, Security.CurrentUser.Id)); + } + + private ContentItemDisplay PostSaveInternal(ContentItemSave contentItem, Func> saveMethod) + { + //Recent versions of IE/Edge may send in the full clientside file path instead of just the file name. + //To ensure similar behavior across all browsers no matter what they do - we strip the FileName property of all + //uploaded files to being *only* the actual file name (as it should be). + if (contentItem.UploadedFiles != null && contentItem.UploadedFiles.Any()) + { + foreach (var file in contentItem.UploadedFiles) + { + file.FileName = Path.GetFileName(file.FileName); + } + } + + //If we've reached here it means: + // * Our model has been bound + // * and validated + // * any file attachments have been saved to their temporary location for us to use + // * we have a reference to the DTO object and the persisted object + // * Permissions are valid + MapPropertyValues(contentItem); + + //We need to manually check the validation results here because: + // * We still need to save the entity even if there are validation value errors + // * Depending on if the entity is new, and if there are non property validation errors (i.e. the name is null) + // then we cannot continue saving, we can only display errors + // * If there are validation errors and they were attempting to publish, we can only save, NOT publish and display + // a message indicating this + if (ModelState.IsValid == false) + { + if (ValidationHelper.ModelHasRequiredForPersistenceErrors(contentItem) && IsCreatingAction(contentItem.Action)) + { + //ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue! + // add the modelstate to the outgoing object and throw a validation message + var forDisplay = AutoMapperExtensions.MapWithUmbracoContext(contentItem.PersistedContent, UmbracoContext); + forDisplay.Errors = ModelState.ToErrorDictionary(); + throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); + + } + + //if the model state is not valid we cannot publish so change it to save + switch (contentItem.Action) + { + case ContentSaveAction.Publish: + contentItem.Action = ContentSaveAction.Save; + break; + case ContentSaveAction.PublishNew: + contentItem.Action = ContentSaveAction.SaveNew; + break; + } + } + + //initialize this to successful + var publishStatus = Attempt.Succeed(); + var wasCancelled = false; //tracks if the operation was cancelled + var noop = false; //tracks if the operation performed no operation (nothing to save) + + if (contentItem.Action == ContentSaveAction.Save || contentItem.Action == ContentSaveAction.SaveNew) + { + //save the item + var saveResult = saveMethod(contentItem.PersistedContent); + + wasCancelled = saveResult.Success == false && saveResult.Result.StatusType == OperationStatusType.FailedCancelledByEvent; + noop = saveResult.Result.StatusType == OperationStatusType.NoOperation; + } + else if (contentItem.Action == ContentSaveAction.SendPublish || contentItem.Action == ContentSaveAction.SendPublishNew) + { + var sendResult = Services.ContentService.SendToPublication(contentItem.PersistedContent, Security.CurrentUser.Id); + wasCancelled = sendResult == false; + } + else + { + //publish the item and check if it worked, if not we will show a diff msg below + publishStatus = Services.ContentService.SaveAndPublishWithStatus(contentItem.PersistedContent, Security.CurrentUser.Id); + wasCancelled = publishStatus.Result.StatusType == PublishStatusType.FailedCancelledByEvent; + } + + //return the updated model + var display = AutoMapperExtensions.MapWithUmbracoContext(contentItem.PersistedContent, UmbracoContext); + + //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403 + HandleInvalidModelState(display); + + //put the correct msgs in + switch (contentItem.Action) + { + case ContentSaveAction.Save: + case ContentSaveAction.SaveNew: + if (wasCancelled) + { + AddCancelMessage(display); + + } + else if (noop == false) + { + display.AddSuccessNotification( + Services.TextService.Localize("speechBubbles/editContentSavedHeader"), + contentItem.ReleaseDate.HasValue + ? Services.TextService.Localize("speechBubbles/editContentSavedWithReleaseDateText", new[] { contentItem.ReleaseDate.Value.ToLongDateString(), contentItem.ReleaseDate.Value.ToShortTimeString() }) + : Services.TextService.Localize("speechBubbles/editContentSavedText") + ); + } + break; + case ContentSaveAction.SendPublish: + case ContentSaveAction.SendPublishNew: + if (wasCancelled) + { + AddCancelMessage(display); + } + else if (noop == false) + { + display.AddSuccessNotification( + Services.TextService.Localize("speechBubbles/editContentSendToPublish"), + Services.TextService.Localize("speechBubbles/editContentSendToPublishText")); + } + + break; + case ContentSaveAction.Publish: + case ContentSaveAction.PublishNew: + ShowMessageForPublishStatus(publishStatus.Result, display, contentItem.ExpireDate); + break; + } + + //If the item is new and the operation was cancelled, we need to return a different + // status code so the UI can handle it since it won't be able to redirect since there + // is no Id to redirect to! + if (wasCancelled && IsCreatingAction(contentItem.Action)) + { + throw new HttpResponseException(Request.CreateValidationErrorResponse(display)); + } + + display.PersistedContent = contentItem.PersistedContent; + + return display; + } + + /// + /// Publishes a document with a given ID + /// + /// + /// + /// + /// The CanAccessContentAuthorize attribute will deny access to this method if the current user + /// does not have Publish access to this node. + /// + /// + [EnsureUserPermissionForContent("id", 'U')] + public HttpResponseMessage PostPublishById(int id) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + + if (foundContent == null) + { + return HandleContentNotFound(id, false); + } + + var publishResult = Services.ContentService.PublishWithStatus(foundContent, Security.CurrentUser.Id); + if (publishResult.Success == false) + { + var notificationModel = new SimpleNotificationModel(); + ShowMessageForPublishStatus(publishResult.Result, notificationModel, foundContent.ExpireDate); + return Request.CreateValidationErrorResponse(notificationModel); + } + + //return ok + return Request.CreateResponse(HttpStatusCode.OK); + + } + + [HttpDelete] + [HttpPost] + public HttpResponseMessage DeleteBlueprint(int id) + { + var found = Services.ContentService.GetBlueprintById(id); + + if (found == null) + { + return HandleContentNotFound(id, false); + } + + Services.ContentService.DeleteBlueprint(found); + + return Request.CreateResponse(HttpStatusCode.OK); + } + + /// + /// Moves an item to the recycle bin, if it is already there then it will permanently delete it + /// + /// + /// + /// + /// The CanAccessContentAuthorize attribute will deny access to this method if the current user + /// does not have Delete access to this node. + /// + [EnsureUserPermissionForContent("id", 'D')] + [HttpDelete] + [HttpPost] + public HttpResponseMessage DeleteById(int id) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + + if (foundContent == null) + { + return HandleContentNotFound(id, false); + } + + //if the current item is in the recycle bin + if (foundContent.IsInRecycleBin() == false) + { + var moveResult = Services.ContentService.WithResult().MoveToRecycleBin(foundContent, Security.CurrentUser.Id); + if (moveResult == false) + { + //returning an object of INotificationModel will ensure that any pending + // notification messages are added to the response. + return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); + } + } + else + { + var deleteResult = Services.ContentService.WithResult().Delete(foundContent, Security.CurrentUser.Id); + if (deleteResult == false) + { + //returning an object of INotificationModel will ensure that any pending + // notification messages are added to the response. + return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); + } + } + + return Request.CreateResponse(HttpStatusCode.OK); + } + + /// + /// Empties the recycle bin + /// + /// + /// + /// attributed with EnsureUserPermissionForContent to verify the user has access to the recycle bin + /// + [HttpDelete] + [HttpPost] + [EnsureUserPermissionForContent(Constants.System.RecycleBinContent, 'D')] + public HttpResponseMessage EmptyRecycleBin() + { + Services.ContentService.EmptyRecycleBin(Security.CurrentUser.Id); + + return Request.CreateNotificationSuccessResponse(Services.TextService.Localize("defaultdialogs/recycleBinIsEmpty")); + } + + /// + /// Change the sort order for content + /// + /// + /// + [EnsureUserPermissionForContent("sorted.ParentId", 'S')] + public HttpResponseMessage PostSort(ContentSortOrder sorted) + { + if (sorted == null) + { + return Request.CreateResponse(HttpStatusCode.NotFound); + } + + //if there's nothing to sort just return ok + if (sorted.IdSortOrder.Length == 0) + { + return Request.CreateResponse(HttpStatusCode.OK); + } + + try + { + var contentService = Services.ContentService; + + // Save content with new sort order and update content xml in db accordingly + if (contentService.Sort(sorted.IdSortOrder, Security.CurrentUser.Id) == false) + { + LogHelper.Warn("Content sorting failed, this was probably caused by an event being cancelled"); + return Request.CreateValidationErrorResponse("Content sorting failed, this was probably caused by an event being cancelled"); + } + return Request.CreateResponse(HttpStatusCode.OK); + } + catch (Exception ex) + { + LogHelper.Error("Could not update content sort order", ex); + throw; + } + } + + /// + /// Change the sort order for media + /// + /// + /// + [EnsureUserPermissionForContent("move.ParentId", 'M')] + public HttpResponseMessage PostMove(MoveOrCopy move) + { + var toMove = ValidateMoveOrCopy(move); + + Services.ContentService.Move(toMove, move.ParentId, Security.CurrentUser.Id); + + var response = Request.CreateResponse(HttpStatusCode.OK); + response.Content = new StringContent(toMove.Path, Encoding.UTF8, "application/json"); + return response; + } + + /// + /// Copies a content item and places the copy as a child of a given parent Id + /// + /// + /// + [EnsureUserPermissionForContent("copy.ParentId", 'C')] + public HttpResponseMessage PostCopy(MoveOrCopy copy) + { + var toCopy = ValidateMoveOrCopy(copy); + + var c = Services.ContentService.Copy(toCopy, copy.ParentId, copy.RelateToOriginal, copy.Recursive, Security.CurrentUser.Id); + + var response = Request.CreateResponse(HttpStatusCode.OK); + response.Content = new StringContent(c.Path, Encoding.UTF8, "application/json"); + return response; + } + + /// + /// Unpublishes a node with a given Id and returns the unpublished entity + /// + /// + /// + [EnsureUserPermissionForContent("id", 'U')] + [OutgoingEditorModelEvent] + public ContentItemDisplay PostUnPublish(int id) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + + if (foundContent == null) + HandleContentNotFound(id); + + var unpublishResult = Services.ContentService.WithResult().UnPublish(foundContent, Security.CurrentUser.Id); + + var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); + + if (unpublishResult == false) + { + AddCancelMessage(content); + throw new HttpResponseException(Request.CreateValidationErrorResponse(content)); + } + else + { + content.AddSuccessNotification(Services.TextService.Localize("content/unPublish"), Services.TextService.Localize("speechBubbles/contentUnpublished")); + return content; + } + } + + /// + /// Maps the dto property values to the persisted model + /// + /// + private void MapPropertyValues(ContentItemSave contentItem) + { + UpdateName(contentItem); + + //TODO: We need to support 'send to publish' + + contentItem.PersistedContent.ExpireDate = contentItem.ExpireDate; + contentItem.PersistedContent.ReleaseDate = contentItem.ReleaseDate; + //only set the template if it didn't change + var templateChanged = (contentItem.PersistedContent.Template == null && contentItem.TemplateAlias.IsNullOrWhiteSpace() == false) + || (contentItem.PersistedContent.Template != null && contentItem.PersistedContent.Template.Alias != contentItem.TemplateAlias) + || (contentItem.PersistedContent.Template != null && contentItem.TemplateAlias.IsNullOrWhiteSpace()); + if (templateChanged) + { + var template = Services.FileService.GetTemplate(contentItem.TemplateAlias); + if (template == null && contentItem.TemplateAlias.IsNullOrWhiteSpace() == false) + { + //ModelState.AddModelError("Template", "No template exists with the specified alias: " + contentItem.TemplateAlias); + LogHelper.Warn("No template exists with the specified alias: " + contentItem.TemplateAlias); + } + else + { + //NOTE: this could be null if there was a template and the posted template is null, this should remove the assigned template + contentItem.PersistedContent.Template = template; + } + } + + base.MapPropertyValues(contentItem); + } + + /// + /// Ensures the item can be moved/copied to the new location + /// + /// + /// + private IContent ValidateMoveOrCopy(MoveOrCopy model) + { + if (model == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + var contentService = Services.ContentService; + var toMove = contentService.GetById(model.Id); + if (toMove == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + if (model.ParentId < 0) + { + //cannot move if the content item is not allowed at the root + if (toMove.ContentType.AllowedAsRoot == false) + { + throw new HttpResponseException( + Request.CreateNotificationValidationErrorResponse( + Services.TextService.Localize("moveOrCopy/notAllowedAtRoot"))); + } + } + else + { + var parent = contentService.GetById(model.ParentId); + if (parent == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + //check if the item is allowed under this one + if (parent.ContentType.AllowedContentTypes.Select(x => x.Id).ToArray() + .Any(x => x.Value == toMove.ContentType.Id) == false) + { + throw new HttpResponseException( + Request.CreateNotificationValidationErrorResponse( + Services.TextService.Localize("moveOrCopy/notAllowedByContentType"))); + } + + // Check on paths + if ((string.Format(",{0},", parent.Path)).IndexOf(string.Format(",{0},", toMove.Id), StringComparison.Ordinal) > -1) + { + throw new HttpResponseException( + Request.CreateNotificationValidationErrorResponse( + Services.TextService.Localize("moveOrCopy/notAllowedByPath"))); + } + } + + return toMove; + } + + private void ShowMessageForPublishStatus(PublishStatus status, INotificationModel display, DateTime? expireDate) + { + switch (status.StatusType) + { + case PublishStatusType.Success: + case PublishStatusType.SuccessAlreadyPublished: + display.AddSuccessNotification( + Services.TextService.Localize("speechBubbles/editContentPublishedHeader"), + expireDate.HasValue + ? Services.TextService.Localize("speechBubbles/editContentPublishedWithExpireDateText", new [] { expireDate.Value.ToLongDateString(), expireDate.Value.ToShortTimeString() }) + : Services.TextService.Localize("speechBubbles/editContentPublishedText") + ); + break; + case PublishStatusType.FailedPathNotPublished: + display.AddWarningNotification( + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedByParent", + new[] { string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id) }).Trim()); + break; + case PublishStatusType.FailedCancelledByEvent: + AddCancelMessage(display, "publish", "speechBubbles/contentPublishedFailedByEvent"); + break; + case PublishStatusType.FailedAwaitingRelease: + display.AddWarningNotification( + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedAwaitingRelease", + new[] { string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id) }).Trim()); + break; + case PublishStatusType.FailedHasExpired: + display.AddWarningNotification( + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedExpired", + new[] + { + string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id), + }).Trim()); + break; + case PublishStatusType.FailedIsTrashed: + //TODO: We should add proper error messaging for this! + break; + case PublishStatusType.FailedContentInvalid: + display.AddWarningNotification( + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedInvalid", + new[] + { + string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id), + string.Join(",", status.InvalidProperties.Select(x => x.Alias)) + }).Trim()); + break; + default: + throw new IndexOutOfRangeException(); + } + } + + + /// + /// Performs a permissions check for the user to check if it has access to the node based on + /// start node and/or permissions for the node + /// + /// The storage to add the content item to so it can be reused + /// + /// + /// + /// + /// The content to lookup, if the contentItem is not specified + /// + /// Specifies the already resolved content item to check against + /// + internal static bool CheckPermissions( + IDictionary storage, + IUser user, + IUserService userService, + IContentService contentService, + IEntityService entityService, + int nodeId, + char[] permissionsToCheck = null, + IContent contentItem = null) + { + if (storage == null) throw new ArgumentNullException("storage"); + if (user == null) throw new ArgumentNullException("user"); + if (userService == null) throw new ArgumentNullException("userService"); + if (contentService == null) throw new ArgumentNullException("contentService"); + if (entityService == null) throw new ArgumentNullException("entityService"); + + if (contentItem == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinContent) + { + contentItem = contentService.GetById(nodeId); + //put the content item into storage so it can be retreived + // in the controller (saves a lookup) + storage[typeof(IContent).ToString()] = contentItem; + } + + if (contentItem == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinContent) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + var hasPathAccess = (nodeId == Constants.System.Root) + ? user.HasContentRootAccess(entityService) + : (nodeId == Constants.System.RecycleBinContent) + ? user.HasContentBinAccess(entityService) + : user.HasPathAccess(contentItem, entityService); + + if (hasPathAccess == false) + { + return false; + } + + if (permissionsToCheck == null || permissionsToCheck.Length == 0) + { + return true; + } + + //get the implicit/inherited permissions for the user for this path, + //if there is no content item for this id, than just use the id as the path (i.e. -1 or -20) + var path = contentItem != null ? contentItem.Path : nodeId.ToString(); + var permission = userService.GetPermissionsForPath(user, path); + + var allowed = true; + foreach (var p in permissionsToCheck) + { + if (permission == null + || permission.GetAllPermissions().Contains(p.ToString(CultureInfo.InvariantCulture)) == false) + { + allowed = false; + } + } + return allowed; + } + + [EnsureUserPermissionForContent("contentId", 'F')] + public IEnumerable GetNotificationOptions(int contentId) + { + var notifications = new List(); + if (contentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + + var content = Services.ContentService.GetById(contentId); + if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + + var actionList = ActionsResolver.Current.Actions; + foreach (var a in actionList) + { + if (a.ShowInNotifier) + { + NotifySetting n = new NotifySetting + { + Name = ui.Text("actions", a.Alias), + Checked = (UmbracoUser.GetNotifications(content.Path).IndexOf(a.Letter) > -1), + NotifyCode = a.Letter.ToString() + }; + notifications.Add(n); + } + } + return notifications; + } + + public void PostNotificationOptions(int contentId, string notifyOptions = "") + { + if (contentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + var content = Services.ContentService.GetById(contentId); + + if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + var node = new CMSNode(contentId); + + global::umbraco.cms.businesslogic.workflow.Notification.UpdateNotifications(UmbracoUser, node, notifyOptions ?? ""); + } + } +} From b4495d0ae5142d0e6cb02c09b1bcabe3b8091dc3 Mon Sep 17 00:00:00 2001 From: martin j griffiths Date: Thu, 19 Sep 2019 10:30:51 +0100 Subject: [PATCH 02/17] Update rte.controller.js Fix issue where an embedded macro prevents editor from returning to pristine/valid after a publish --- .../src/views/propertyeditors/rte/rte.controller.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js index 109aa37fbb..eb049ea1b3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js @@ -257,7 +257,10 @@ angular.module("umbraco") // Update model on change, i.e. copy/pasted text, plugins altering content editor.on('SetContent', function (e) { if (!e.initial) { - syncContent(editor); + // sync content if editor is dirty + if (!editor.isNotDirty) { + syncContent(editor); + } } }); From 98e39ed3ef3277216df9c423494ecbc1d259476c Mon Sep 17 00:00:00 2001 From: stevemegson Date: Fri, 20 Sep 2019 10:38:44 +0100 Subject: [PATCH 03/17] V7: Error upgrading due to SecurityStamp being null (#6343) * Allow login with a null SecurityStamp when upgrading * Added migration to fill in missing SecurityStamp * Make sure we initialize UmbracoContext as it is needed in some tests that use UmbracoBackOfficeIdentity.AddUserDataClaims * Moved check for null SecurityStamp * Generate new SecurityStamp in C# * Bump version to 7.15.4 --- src/SolutionInfo.cs | 4 ++-- .../Configuration/UmbracoVersion.cs | 2 +- .../PopulateMissingSecurityStamps.cs | 24 +++++++++++++++++++ .../Security/UmbracoBackOfficeIdentity.cs | 8 +++++++ src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../UmbracoBackOfficeIdentityTests.cs | 22 ++++++++++++++++- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 4 ++-- 7 files changed, 59 insertions(+), 6 deletions(-) create mode 100644 src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFifteenFour/PopulateMissingSecurityStamps.cs diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 97ebd98e8c..b5a8fbc738 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -11,5 +11,5 @@ using System.Resources; [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyFileVersion("7.15.3")] -[assembly: AssemblyInformationalVersion("7.15.3")] +[assembly: AssemblyFileVersion("7.15.4")] +[assembly: AssemblyInformationalVersion("7.15.4")] diff --git a/src/Umbraco.Core/Configuration/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs index 8c8755577a..65bd4f8694 100644 --- a/src/Umbraco.Core/Configuration/UmbracoVersion.cs +++ b/src/Umbraco.Core/Configuration/UmbracoVersion.cs @@ -6,7 +6,7 @@ namespace Umbraco.Core.Configuration { public class UmbracoVersion { - private static readonly Version Version = new Version("7.15.3"); + private static readonly Version Version = new Version("7.15.4"); /// /// Gets the current version of Umbraco. diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFifteenFour/PopulateMissingSecurityStamps.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFifteenFour/PopulateMissingSecurityStamps.cs new file mode 100644 index 0000000000..042a003c75 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFifteenFour/PopulateMissingSecurityStamps.cs @@ -0,0 +1,24 @@ +using System; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenFifteenFour +{ + [Migration("7.15.4", 1, Constants.System.UmbracoMigrationName)] + public class PopulateMissingSecurityStamps : MigrationBase + { + public PopulateMissingSecurityStamps(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) + { + } + + public override void Up() + { + // A user with a NULL securityStampToken can't log in after v7.8.0 + Execute.Sql($@"UPDATE umbracoUser SET securityStampToken = '{Guid.NewGuid().ToString()}' WHERE securityStampToken IS NULL"); + } + + public override void Down() + { + } + } +} diff --git a/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs index fcb5547027..7772b198c6 100644 --- a/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs +++ b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs @@ -115,6 +115,12 @@ namespace Umbraco.Core.Security AddClaim(new Claim(ClaimTypes.CookiePath, "/", ClaimValueTypes.String, Issuer, Issuer, this)); } + // if upgrading from a pre-7.3.0 version, SecurityStamp will be null + if (userdata.SecurityStamp == null && ApplicationContext.Current.IsUpgrading) + { + userdata.SecurityStamp = Guid.NewGuid().ToString(); + } + _currentIssuer = claimsIdentity.AuthenticationType; UserData = userdata; AddExistingClaims(claimsIdentity); @@ -227,7 +233,9 @@ namespace Umbraco.Core.Security //The security stamp claim is also required... this is because this claim type is hard coded // by the SecurityStampValidator, see: https://katanaproject.codeplex.com/workitem/444 if (HasClaim(x => x.Type == Microsoft.AspNet.Identity.Constants.DefaultSecurityStampClaimType) == false) + { AddClaim(new Claim(Microsoft.AspNet.Identity.Constants.DefaultSecurityStampClaimType, SecurityStamp, ClaimValueTypes.String, Issuer, Issuer, this)); + } //Add each app as a separate claim if (HasClaim(x => x.Type == Constants.Security.AllowedApplicationsClaimType) == false) diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 3b5d0aaff6..fe822e10bc 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -576,6 +576,7 @@ + diff --git a/src/Umbraco.Tests/Security/UmbracoBackOfficeIdentityTests.cs b/src/Umbraco.Tests/Security/UmbracoBackOfficeIdentityTests.cs index d9ab9c9a3d..950c2b898f 100644 --- a/src/Umbraco.Tests/Security/UmbracoBackOfficeIdentityTests.cs +++ b/src/Umbraco.Tests/Security/UmbracoBackOfficeIdentityTests.cs @@ -4,9 +4,14 @@ using System.Security.Claims; using System.Text; using System.Threading.Tasks; using System.Web.Security; +using Moq; using Newtonsoft.Json; using NUnit.Framework; using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Core.Profiling; +using Umbraco.Core.Scoping; using Umbraco.Core.Security; using Umbraco.Core.Services; @@ -15,9 +20,24 @@ namespace Umbraco.Tests.Security [TestFixture] public class UmbracoBackOfficeIdentityTests { - public const string TestIssuer = "TestIssuer"; + [SetUp] + public void Initialize() + { + var sqlSyntax = new SqlCeSyntaxProvider(); + + //This is needed because the Migration resolver is creating migration instances with their full ctors + ApplicationContext.EnsureContext( + new ApplicationContext( + new DatabaseContext(Mock.Of(), Mock.Of(), sqlSyntax, "test"), + new ServiceContext(), + CacheHelper.CreateDisabledCacheHelper(), + new ProfilingLogger(Mock.Of(), Mock.Of())), + true); + } + + [Test] public void Create_From_Claims_Identity() { diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 76d2765672..3e0ce457e1 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -1028,9 +1028,9 @@ xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.1\x86\*.* "$(TargetDir)x86\" True True - 7153 + 7154 / - http://localhost:7153 + http://localhost:7154 False False From d3c4aace160b78739a8eeb673cb561e96cc04101 Mon Sep 17 00:00:00 2001 From: Benjamin Howarth Date: Tue, 24 Sep 2019 17:25:10 +0100 Subject: [PATCH 04/17] #2996 added GlobalSettings.DebugMode switch for JSON formatting indentation. --- src/Umbraco.Core/Serialization/JsonNetSerializer.cs | 6 ++++-- src/Umbraco.Web/Editors/BackOfficeController.cs | 12 ++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Core/Serialization/JsonNetSerializer.cs b/src/Umbraco.Core/Serialization/JsonNetSerializer.cs index 800278abf0..52f39c6109 100644 --- a/src/Umbraco.Core/Serialization/JsonNetSerializer.cs +++ b/src/Umbraco.Core/Serialization/JsonNetSerializer.cs @@ -4,6 +4,7 @@ using System.Reflection; using System.Text; using Newtonsoft.Json; using Newtonsoft.Json.Converters; +using Umbraco.Core.Configuration; namespace Umbraco.Core.Serialization { @@ -60,7 +61,8 @@ namespace Umbraco.Core.Serialization /// public IStreamedResult ToStream(object input) { - string s = JsonConvert.SerializeObject(input, Formatting.Indented, _settings); + var formatting = GlobalSettings.DebugMode ? Formatting.Indented : Formatting.None; + string s = JsonConvert.SerializeObject(input, formatting, _settings); byte[] bytes = Encoding.UTF8.GetBytes(s); MemoryStream ms = new MemoryStream(bytes); @@ -69,4 +71,4 @@ namespace Umbraco.Core.Serialization #endregion } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 093c095b5d..4a79038fc0 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -177,8 +177,8 @@ namespace Umbraco.Web.Editors //the dictionary returned is fine but the delimiter between an 'area' and a 'value' is a '/' but the javascript // in the back office requres the delimiter to be a '_' so we'll just replace it .ToDictionary(key => key.Key.Replace("/", "_"), val => val.Value); - - return new JsonNetResult { Data = textForCulture, Formatting = Formatting.Indented }; + var formatting = GlobalSettings.DebugMode ? Formatting.Indented : Formatting.None; + return new JsonNetResult { Data = textForCulture, Formatting = formatting }; } /// @@ -230,8 +230,8 @@ namespace Umbraco.Web.Editors typeof(BackOfficeController) + "GetManifestAssetList", () => getResult(), new TimeSpan(0, 10, 0)); - - return new JsonNetResult { Data = result, Formatting = Formatting.Indented }; + var formatting = GlobalSettings.DebugMode ? Formatting.Indented : Formatting.None; + return new JsonNetResult { Data = result, Formatting = formatting }; } [UmbracoAuthorize(Order = 0)] @@ -244,8 +244,8 @@ namespace Umbraco.Web.Editors new DirectoryInfo(Server.MapPath(SystemDirectories.AppPlugins)), new DirectoryInfo(Server.MapPath(SystemDirectories.Config)), HttpContext.IsDebuggingEnabled); - - return new JsonNetResult { Data = gridConfig.EditorsConfig.Editors, Formatting = Formatting.Indented }; + var formatting = GlobalSettings.DebugMode ? Formatting.Indented : Formatting.None; + return new JsonNetResult { Data = gridConfig.EditorsConfig.Editors, Formatting = formatting }; } From 16837d018a44c324e620d7c72b63be015a87895c Mon Sep 17 00:00:00 2001 From: Benjamin Howarth Date: Tue, 24 Sep 2019 21:34:40 +0100 Subject: [PATCH 05/17] #2996 changed formatting to none for both data transfer & persistence --- src/Umbraco.Core/Serialization/JsonNetSerializer.cs | 3 +-- src/Umbraco.Web/Editors/BackOfficeController.cs | 9 +++------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Core/Serialization/JsonNetSerializer.cs b/src/Umbraco.Core/Serialization/JsonNetSerializer.cs index 52f39c6109..80e123d9ea 100644 --- a/src/Umbraco.Core/Serialization/JsonNetSerializer.cs +++ b/src/Umbraco.Core/Serialization/JsonNetSerializer.cs @@ -61,8 +61,7 @@ namespace Umbraco.Core.Serialization /// public IStreamedResult ToStream(object input) { - var formatting = GlobalSettings.DebugMode ? Formatting.Indented : Formatting.None; - string s = JsonConvert.SerializeObject(input, formatting, _settings); + string s = JsonConvert.SerializeObject(input, Formatting.None, _settings); byte[] bytes = Encoding.UTF8.GetBytes(s); MemoryStream ms = new MemoryStream(bytes); diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 4a79038fc0..414fb9a1df 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -177,8 +177,7 @@ namespace Umbraco.Web.Editors //the dictionary returned is fine but the delimiter between an 'area' and a 'value' is a '/' but the javascript // in the back office requres the delimiter to be a '_' so we'll just replace it .ToDictionary(key => key.Key.Replace("/", "_"), val => val.Value); - var formatting = GlobalSettings.DebugMode ? Formatting.Indented : Formatting.None; - return new JsonNetResult { Data = textForCulture, Formatting = formatting }; + return new JsonNetResult { Data = textForCulture, Formatting = Formatting.None }; } /// @@ -230,8 +229,7 @@ namespace Umbraco.Web.Editors typeof(BackOfficeController) + "GetManifestAssetList", () => getResult(), new TimeSpan(0, 10, 0)); - var formatting = GlobalSettings.DebugMode ? Formatting.Indented : Formatting.None; - return new JsonNetResult { Data = result, Formatting = formatting }; + return new JsonNetResult { Data = result, Formatting = Formatting.None }; } [UmbracoAuthorize(Order = 0)] @@ -244,8 +242,7 @@ namespace Umbraco.Web.Editors new DirectoryInfo(Server.MapPath(SystemDirectories.AppPlugins)), new DirectoryInfo(Server.MapPath(SystemDirectories.Config)), HttpContext.IsDebuggingEnabled); - var formatting = GlobalSettings.DebugMode ? Formatting.Indented : Formatting.None; - return new JsonNetResult { Data = gridConfig.EditorsConfig.Editors, Formatting = formatting }; + return new JsonNetResult { Data = gridConfig.EditorsConfig.Editors, Formatting = Formatting.None }; } From 79bf9b753caf97a55d474c5c6db8821a33ca398f Mon Sep 17 00:00:00 2001 From: Benjamin Howarth <322383+benjaminhowarth1@users.noreply.github.com> Date: Mon, 30 Sep 2019 16:54:28 +0100 Subject: [PATCH 06/17] #2996 resubmitting ContentExtensions and ObjectExtensions fixes (#6473) --- src/Umbraco.Core/ObjectExtensions.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Umbraco.Core/ObjectExtensions.cs b/src/Umbraco.Core/ObjectExtensions.cs index 479e425c99..5c1c5fdeac 100644 --- a/src/Umbraco.Core/ObjectExtensions.cs +++ b/src/Umbraco.Core/ObjectExtensions.cs @@ -8,7 +8,10 @@ using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; using System.Xml; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Umbraco.Core.Collections; +using Formatting = Newtonsoft.Json.Formatting; namespace Umbraco.Core { @@ -125,6 +128,11 @@ namespace Umbraco.Core return Attempt.Succeed(input); } + if (target == typeof(string) && inputType == typeof(JObject)) + { + return Attempt.Succeed(JsonConvert.SerializeObject(input, Formatting.None)); + } + // Check for string so that overloaders of ToString() can take advantage of the conversion. if (target == typeof(string)) { From f967f2111938d8954b5a61b7db54f23c43c45b49 Mon Sep 17 00:00:00 2001 From: Dirk De Grave Date: Tue, 1 Oct 2019 13:39:32 +0200 Subject: [PATCH 07/17] Fixes #6499 by reloading the content in list view after bulk copy/move --- .../listview/listview.controller.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js index 9b4c01f4f9..0812bcff3e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js @@ -472,7 +472,7 @@ function listViewController($rootScope, $scope, $routeParams, $injector, $cookie // a specific value from one of the methods, so we'll have to try this way. Even though the first method // will fire once per every node moved, the destination path will be the same and we need to use that to sync. var newPath = null; - applySelected( + var attempt = applySelected( function(selected, index) { return contentResource.move({ parentId: target.id, id: getIdCallback(selected[index]) }) .then(function(path) { @@ -509,6 +509,11 @@ function listViewController($rootScope, $scope, $routeParams, $injector, $cookie }); } }); + if (attempt) { + attempt.then(function () { + $scope.getContent(); + }); + } } $scope.copy = function () { @@ -536,7 +541,7 @@ function listViewController($rootScope, $scope, $routeParams, $injector, $cookie }; function performCopy(target, relateToOriginal) { - applySelected( + var attempt = applySelected( function (selected, index) { return contentResource.copy({ parentId: target.id, id: getIdCallback(selected[index]), relateToOriginal: relateToOriginal }); }, function (count, total) { var key = (total === 1 ? "bulk_copiedItemOfItem" : "bulk_copiedItemOfItems"); @@ -546,6 +551,11 @@ function listViewController($rootScope, $scope, $routeParams, $injector, $cookie var key = (total === 1 ? "bulk_copiedItem" : "bulk_copiedItems"); return localizationService.localize(key, [total]); }); + if (attempt) { + attempt.then(function () { + $scope.getContent(); + }); + } } function getCustomPropertyValue(alias, properties) { From 9c28a1ff808d4ca0059002cd78d90eab7676bf69 Mon Sep 17 00:00:00 2001 From: jmayntzhusen Date: Tue, 1 Oct 2019 20:40:46 +0200 Subject: [PATCH 08/17] Update styles of API docs to match new identity --- apidocs/umbracotemplate/styles/main.css | 274 ++++++++++++++++++------ 1 file changed, 208 insertions(+), 66 deletions(-) diff --git a/apidocs/umbracotemplate/styles/main.css b/apidocs/umbracotemplate/styles/main.css index d74d51b150..69dde09875 100644 --- a/apidocs/umbracotemplate/styles/main.css +++ b/apidocs/umbracotemplate/styles/main.css @@ -1,73 +1,215 @@ body { - color: rgba(0,0,0,.8); -} -.navbar-inverse { - background: #a3db78; -} -.navbar-inverse .navbar-nav>li>a, .navbar-inverse .navbar-text { - color: rgba(0,0,0,.8); -} - -.navbar-inverse { - border-color: transparent; -} - -.sidetoc { - background-color: #f5fbf1; -} -body .toc { - background-color: #f5fbf1; -} -.sidefilter { - background-color: #daf0c9; -} -.subnav { - background-color: #f5fbf1; -} - -.navbar-inverse .navbar-nav>.active>a { - color: rgba(0,0,0,.8); - background-color: #daf0c9; -} - -.navbar-inverse .navbar-nav>.active>a:focus, .navbar-inverse .navbar-nav>.active>a:hover { - color: rgba(0,0,0,.8); - background-color: #daf0c9; -} - -.btn-primary { - color: rgba(0,0,0,.8); - background-color: #fff; - border-color: rgba(0,0,0,.8); -} -.btn-primary:hover { - background-color: #daf0c9; - color: rgba(0,0,0,.8); - border-color: rgba(0,0,0,.8); -} - -.toc .nav > li > a { - color: rgba(0,0,0,.8); -} - -button, a { - color: #f36f21; -} - -button:hover, -button:focus, -a:hover, -a:focus { - color: #143653; - text-decoration: none; + color: #34393e; + font-family: 'Roboto', sans-serif; + line-height: 1.5; + font-size: 16px; + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + word-wrap: break-word } .navbar-header .navbar-brand { - background: url(https://our.umbraco.com/assets/images/logo.svg) left center no-repeat; - background-size: 40px auto; - width:50px; + background: url(https://our.umbraco.com/assets/images/logo.svg) left center no-repeat; + background-size: 40px auto; + width:50px; } -.toc .nav > li.active > a { - color: #f36f21; +#_content>a { + margin-top: 5px; } +/* HEADINGS */ + +h1 { + font-weight: 600; + font-size: 32px; +} + +h2 { + font-weight: 600; + font-size: 24px; + line-height: 1.8; +} + +h3 { + font-weight: 600; + font-size: 20px; + line-height: 1.8; +} + +h5 { + font-size: 14px; + padding: 10px 0px; +} + +article h1, +article h2, +article h3, +article h4 { + margin-top: 35px; + margin-bottom: 15px; +} + +article h4 { + padding-bottom: 8px; + border-bottom: 2px solid #ddd; +} + +/* NAVBAR */ + +.navbar-brand>img { + color: #fff; +} + +.navbar { + border: none; + /* Both navbars use box-shadow */ + -webkit-box-shadow: 0px 1px 3px 0px rgba(100, 100, 100, 0.5); + -moz-box-shadow: 0px 1px 3px 0px rgba(100, 100, 100, 0.5); + box-shadow: 0px 1px 3px 0px rgba(100, 100, 100, 0.5); +} + +.subnav { + border-top: 1px solid #ddd; + background-color: #fff; +} + +.navbar-inverse { + background-color: #303ea1; + z-index: 100; +} + +.navbar-inverse .navbar-nav>li>a, +.navbar-inverse .navbar-text { + color: #fff; + background-color: #303ea1; + border-bottom: 3px solid transparent; + padding-bottom: 12px; +} + +.navbar-inverse .navbar-nav>li>a:focus, +.navbar-inverse .navbar-nav>li>a:hover { + color: #fff; + background-color: #303ea1; + border-bottom: 3px solid white; +} + +.navbar-inverse .navbar-nav>.active>a, +.navbar-inverse .navbar-nav>.active>a:focus, +.navbar-inverse .navbar-nav>.active>a:hover { + color: #fff; + background-color: #303ea1; + border-bottom: 3px solid white; +} + +.navbar-form .form-control { + border: none; + border-radius: 20px; +} + +/* SIDEBAR */ + +.toc .level1>li { + font-weight: 400; +} + +.toc .nav>li>a { + color: #34393e; +} + +.sidefilter { + background-color: #fff; + border-left: none; + border-right: none; +} + +.sidefilter { + background-color: #fff; + border-left: none; + border-right: none; +} + +.toc-filter { + padding: 10px; + margin: 0; +} + +.toc-filter>input { + border: 2px solid #ddd; + border-radius: 20px; +} + +.toc-filter>.filter-icon { + display: none; +} + +.sidetoc>.toc { + background-color: #fff; + overflow-x: hidden; +} + +.sidetoc { + background-color: #fff; + border: none; +} + +/* ALERTS */ + +.alert { + padding: 0px 0px 5px 0px; + color: inherit; + background-color: inherit; + border: none; + box-shadow: 0px 2px 2px 0px rgba(100, 100, 100, 0.4); +} + +.alert>p { + margin-bottom: 0; + padding: 5px 10px; +} + +.alert>ul { + margin-bottom: 0; + padding: 5px 40px; +} + +.alert>h5 { + padding: 10px 15px; + margin-top: 0; + text-transform: uppercase; + font-weight: bold; + border-radius: 4px 4px 0 0; +} + +.alert-info>h5 { + color: #1976d2; + border-bottom: 4px solid #1976d2; + background-color: #e3f2fd; +} + +.alert-warning>h5 { + color: #f57f17; + border-bottom: 4px solid #f57f17; + background-color: #fff3e0; +} + +.alert-danger>h5 { + color: #d32f2f; + border-bottom: 4px solid #d32f2f; + background-color: #ffebee; +} + +/* CODE HIGHLIGHT */ +pre { +padding: 9.5px; +margin: 0 0 10px; +font-size: 13px; +word-break: break-all; +word-wrap: break-word; +background-color: #fffaef; +border-radius: 4px; +box-shadow: 0px 1px 4px 1px rgba(100, 100, 100, 0.4); +} + +.sideaffix { + overflow: visible; +} \ No newline at end of file From e0ea4ef36ffb6ff5289c34541f006cdbba9394e2 Mon Sep 17 00:00:00 2001 From: Benjamin Carleski Date: Wed, 2 Oct 2019 14:16:03 -0700 Subject: [PATCH 09/17] Add null check in allowed template check --- src/Umbraco.Web/PublishedContentExtensions.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index c80f41d9fc..d1bc7dffff 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -135,6 +135,9 @@ namespace Umbraco.Web public static bool IsAllowedTemplate(this IPublishedContent content, int templateId) { + if (content == null) + return false; + if (UmbracoConfig.For.UmbracoSettings().WebRouting.DisableAlternativeTemplates == true) return content.TemplateId == templateId; From 96a2af2653da49ebdc793c9aa53acd54cc173110 Mon Sep 17 00:00:00 2001 From: Rasmus Olofsson Date: Thu, 24 Oct 2019 18:43:37 +0200 Subject: [PATCH 10/17] Add Swedish translations for prompt --- src/Umbraco.Web.UI/umbraco/config/lang/sv.xml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml b/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml index 982f31e383..0a2f37ce02 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml @@ -182,10 +182,11 @@ Besök - Stay - Discard changes - You have unsaved changes - Are you sure you want to navigate away from this page? - you have unsaved changes + Stanna + Avfärda ändringar + Du har inte sparat dina ändringar + Är du säker på att du vill navigera bort från denna sida? - du har inte sparat dina ändringar + Avpublicering kommer att ta bort denna sida och alla dess ättlingar från hemsidan. Done From 8a02b4f58723d2c9746e7a59d417449ff5e2e933 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Wed, 30 Oct 2019 11:24:08 +0100 Subject: [PATCH 11/17] Editors should always be allowed to delete their own content (#6799) * Users should always be allowed to delete their own content * Changed comment to be more precise --- src/Umbraco.Web/Editors/ContentController.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 8bb7af0077..bfc9ae071f 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -28,6 +28,7 @@ using Constants = Umbraco.Core.Constants; using umbraco.cms.businesslogic; using System.Collections; using umbraco; +using umbraco.BusinessLogic.Actions; namespace Umbraco.Web.Editors { @@ -1171,6 +1172,12 @@ namespace Umbraco.Web.Editors var path = contentItem != null ? contentItem.Path : nodeId.ToString(); var permission = userService.GetPermissionsForPath(user, path); + // users are allowed to delete their own content - see ContentTreeControllerBase.GetAllowedUserMenuItemsForNode() + if(contentItem != null && contentItem.CreatorId == user.Id) + { + permission.PermissionsSet.Add(new EntityPermission(0, contentItem.Id, new [] { ActionDelete.Instance.Letter.ToString() } )); + } + var allowed = true; foreach (var p in permissionsToCheck) { From d38f275467a84a031e89276557b1e81769d362c4 Mon Sep 17 00:00:00 2001 From: Steve Megson Date: Wed, 30 Oct 2019 12:15:32 +0000 Subject: [PATCH 12/17] Media uploader in RTE doesn't select uploaded image --- .../common/overlays/mediaPicker/mediapicker.controller.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js index fc62c0ce67..136e035b85 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js @@ -251,7 +251,12 @@ angular.module("umbraco") if (files.length === 1 && $scope.model.selectedImages.length === 0) { var image = $scope.images[$scope.images.length - 1]; $scope.target = image; - $scope.target.url = mediaHelper.resolveFile(image); + // handle both entity and full media object + if (image.image) { + $scope.target.url = image.image; + } else { + $scope.target.url = mediaHelper.resolveFile(image); + } selectImage(image); } }); From b0e2f2d5113d36049a32c9a9be2523f9964b88a4 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 8 Nov 2019 11:51:02 +1100 Subject: [PATCH 13/17] bumps cdf requirements --- build/NuSpecs/UmbracoCms.Core.nuspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/NuSpecs/UmbracoCms.Core.nuspec b/build/NuSpecs/UmbracoCms.Core.nuspec index 9f507b4915..7f180b4ca0 100644 --- a/build/NuSpecs/UmbracoCms.Core.nuspec +++ b/build/NuSpecs/UmbracoCms.Core.nuspec @@ -29,8 +29,8 @@ - - + + From 6f67105645e616a8b36dcddb1db7cabd8c9a3c2d Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Wed, 11 Dec 2019 09:25:30 +0100 Subject: [PATCH 14/17] Undo removing the MiniProfiler routes if solution is not in debug mode --- .../Profiling/WebProfilerProvider.cs | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/src/Umbraco.Web/Profiling/WebProfilerProvider.cs b/src/Umbraco.Web/Profiling/WebProfilerProvider.cs index e0dcfcf9b1..ffd1871ecc 100644 --- a/src/Umbraco.Web/Profiling/WebProfilerProvider.cs +++ b/src/Umbraco.Web/Profiling/WebProfilerProvider.cs @@ -1,10 +1,7 @@ using System; -using System.Linq; using System.Threading; using System.Web; -using System.Web.Routing; using StackExchange.Profiling; -using Umbraco.Core.Configuration; namespace Umbraco.Web.Profiling { @@ -27,20 +24,6 @@ namespace Umbraco.Web.Profiling { // booting... _bootPhase = BootPhase.Boot; - - // Remove Mini Profiler routes when not in debug mode - if (GlobalSettings.DebugMode == false) - { - //NOTE: Keep the global fully qualified name, for some reason without it I was getting null refs - var prefix = global::StackExchange.Profiling.MiniProfiler.Settings.RouteBasePath.Replace("~/", string.Empty); - - using (RouteTable.Routes.GetWriteLock()) - { - var routes = RouteTable.Routes.Where(x => x is Route r && r.Url.StartsWith(prefix)).ToList(); - foreach(var r in routes) - RouteTable.Routes.Remove(r); - } - } } /// @@ -135,4 +118,4 @@ namespace Umbraco.Web.Profiling } } } -} +} \ No newline at end of file From c845f5d8c578e6bdf12bc20000b485dd9933bf10 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Wed, 11 Dec 2019 09:26:31 +0100 Subject: [PATCH 15/17] Discard MiniProfiler results when not in debug mode --- src/Umbraco.Web/Profiling/WebProfiler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Profiling/WebProfiler.cs b/src/Umbraco.Web/Profiling/WebProfiler.cs index fd980db2d1..f44d4876e2 100644 --- a/src/Umbraco.Web/Profiling/WebProfiler.cs +++ b/src/Umbraco.Web/Profiling/WebProfiler.cs @@ -82,7 +82,7 @@ namespace Umbraco.Web.Profiling if (isBootRequest) _provider.EndBootRequest(); if (isBootRequest || ShouldProfile(sender)) - Stop(); + Stop(!GlobalSettings.DebugMode); } private bool ShouldProfile(object sender) From 262a4cba36fee93cff531ab84ad26b4f88d2ea9c Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Wed, 11 Dec 2019 09:30:25 +0100 Subject: [PATCH 16/17] Explicitly set MiniProfiler storage to HttpRuntimeCacheStorage --- src/Umbraco.Web/Profiling/WebProfiler.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Umbraco.Web/Profiling/WebProfiler.cs b/src/Umbraco.Web/Profiling/WebProfiler.cs index f44d4876e2..2fa5639fa7 100644 --- a/src/Umbraco.Web/Profiling/WebProfiler.cs +++ b/src/Umbraco.Web/Profiling/WebProfiler.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Web; using StackExchange.Profiling; using StackExchange.Profiling.SqlFormatters; +using StackExchange.Profiling.Storage; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; @@ -32,6 +33,7 @@ namespace Umbraco.Web.Profiling MiniProfiler.Settings.SqlFormatter = new SqlServerFormatter(); MiniProfiler.Settings.StackMaxLength = 5000; MiniProfiler.Settings.ProfilerProvider = _provider; + MiniProfiler.Settings.Storage = new HttpRuntimeCacheStorage(TimeSpan.FromMinutes(30)); //Binds to application events to enable the MiniProfiler with a real HttpRequest UmbracoApplicationBase.ApplicationInit += UmbracoApplicationApplicationInit; From 889f0fc08553c5e6ef47c70b81181bfb3517bf88 Mon Sep 17 00:00:00 2001 From: elitsa Date: Mon, 3 Feb 2020 11:39:11 +0100 Subject: [PATCH 17/17] Html encoding document name when it's rendered in the relation types html. --- .../umbraco/developer/RelationTypes/EditRelationType.aspx.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/EditRelationType.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/EditRelationType.aspx.cs index 33366681f5..c718183988 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/EditRelationType.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/EditRelationType.aspx.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text; +using System.Web; using System.Web.UI; using System.Web.UI.WebControls; using umbraco.BasePages; @@ -105,9 +106,9 @@ namespace umbraco.cms.presentation.developer.RelationTypes readOnlyRelation.Id = reader.GetInt("id"); readOnlyRelation.ParentId = reader.GetInt("parentId"); - readOnlyRelation.ParentText = reader.GetString("parentText"); + readOnlyRelation.ParentText = HttpUtility.HtmlEncode(reader.GetString("parentText")); readOnlyRelation.ChildId = reader.GetInt("childId"); - readOnlyRelation.ChildText = reader.GetString("childText"); + readOnlyRelation.ChildText = HttpUtility.HtmlEncode(reader.GetString("childText")); readOnlyRelation.RelType = reader.GetInt("relType"); readOnlyRelation.DateTime = reader.GetDateTime("datetime"); readOnlyRelation.Comment = reader.GetString("comment");