From d4311fe4ab50b3a5bd7716765739f5deb7826d74 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 24 Jun 2019 10:52:35 +0200 Subject: [PATCH] https://umbraco.visualstudio.com/D-Team%20Tracker/_workitems/edit/1479 - Replaced client side method to get Auchors with a server side version, and eliminated the use of contentResource in linkPikers --- src/Umbraco.Core/Services/ContentService.cs | 5843 +++++++++-------- src/Umbraco.Core/Services/IContentService.cs | 1437 ++-- .../src/common/resources/entity.resource.js | 35 + .../src/common/services/tinymce.service.js | 1729 +++-- .../common/dialogs/linkpicker.controller.js | 18 +- .../linkpicker/linkpicker.controller.js | 16 +- .../grid/editors/rte.controller.js | 29 +- .../propertyeditors/rte/rte.controller.js | 34 +- src/Umbraco.Web/Editors/EntityController.cs | 20 + .../Models/ContentEditing/UrlAndAnchors.cs | 21 + src/Umbraco.Web/Umbraco.Web.csproj | 4053 ++++++------ 11 files changed, 6665 insertions(+), 6570 deletions(-) create mode 100644 src/Umbraco.Web/Models/ContentEditing/UrlAndAnchors.cs diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index a1e7c9955e..cf49e892dd 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -1,2904 +1,2939 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Globalization; -using System.IO; -using System.Linq; -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); - - 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; - } - - /// - /// 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); + + 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 IList GetAnchorValuesFromRTEs(int id) + { + var result = new List(); + + var content = GetById(id); + + foreach (var contentProperty in content.Properties) + { + if (string.Equals(contentProperty.PropertyType.PropertyEditorAlias, Constants.PropertyEditors.TinyMCEAlias)) + { + var value = contentProperty.Value.ToString(); + + result.AddRange(GetAnchorValuesFromRTEContent(value)); + } + } + + + return result; + } + + public IList GetAnchorValuesFromRTEContent(string rteContent) + { + var result = new List(); + var regex = new Regex(""); + var matches = regex.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 + } +} diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 5a626c5d36..8c063890fd 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -1,717 +1,720 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Xml; -using System.Xml.Linq; -using Umbraco.Core.Models; -using Umbraco.Core.Models.Membership; -using Umbraco.Core.Persistence.DatabaseModelDefinitions; -using Umbraco.Core.Persistence.Querying; -using Umbraco.Core.Publishing; - -namespace Umbraco.Core.Services -{ - /// - /// A temporary interface until we are in v8, this is used to return a different result for the same method and this interface gets implemented - /// explicitly. These methods will replace the normal ones in IContentService in v8 and this will be removed. - /// - public interface IContentServiceOperations - { - //TODO: Remove this class in v8 - - //TODO: There's probably more that needs to be added like the EmptyRecycleBin, etc... - - /// - /// 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 Save(IContent content, int userId = 0, bool raiseEvents = true); - - /// - /// 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 Save(IEnumerable contents, int userId = 0, bool raiseEvents = true); - - /// - /// 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 Delete(IContent content, int userId = 0); - - /// - /// Publishes a single object - /// - /// The to publish - /// Optional Id of the User issueing the publishing - /// The published status attempt - Attempt Publish(IContent content, int userId = 0); - - /// - /// 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> PublishWithChildren(IContent content, int userId = 0, bool includeUnpublished = false); - - /// - /// 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 SaveAndPublish(IContent content, int userId = 0, bool raiseEvents = true); - - /// - /// 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 MoveToRecycleBin(IContent content, int userId = 0); - - /// - /// UnPublishes a single object - /// - /// The to publish - /// Optional Id of the User issueing the publishing - /// True if unpublishing succeeded, otherwise False - Attempt UnPublish(IContent content, int userId = 0); - } - - /// - /// Defines the ContentService, which is an easy access to operations involving - /// - public interface IContentService : IContentServiceBase - { - IEnumerable GetBlueprintsForContentTypes(params int[] documentTypeIds); - IContent GetBlueprintById(int id); - IContent GetBlueprintById(Guid id); - void SaveBlueprint(IContent content, int userId = 0); - void DeleteBlueprint(IContent content, int userId = 0); - IContent CreateContentFromBlueprint(IContent blueprint, string name, int userId = 0); - void DeleteBlueprintsOfType(int contentTypeId, int userId = 0); - void DeleteBlueprintsOfTypes(IEnumerable contentTypeIds, int userId = 0); - - /// - /// Gets all XML entries found in the cmsContentXml table based on the given 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 - /// - /// If -1 is passed, then this will return all content xml entries, otherwise will return all descendents from the path - /// - IEnumerable GetPagedXmlEntries(string path, long pageIndex, int pageSize, out long totalRecords); - - /// - /// This builds the Xml document used for the XML cache - /// - /// - XmlDocument BuildXmlCache(); - - XmlDocument BuildPreviewXmlCache(); - - /// - /// 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 - /// - void RebuildXmlStructures(params int[] contentTypeIds); - - int CountPublished(string contentTypeAlias = null); - int Count(string contentTypeAlias = null); - int CountChildren(int parentId, string contentTypeAlias = null); - int CountDescendants(int parentId, string contentTypeAlias = null); - - /// - /// 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 group id & permission pairs. - /// - /// - void ReplaceContentPermissions(EntityPermissionSet permissionSet); - - /// - /// Assigns a single permission to the current content item for the specified user group ids - /// - /// - /// - /// - void AssignContentPermission(IContent entity, char permission, IEnumerable groupIds); - - /// - /// Returns implicit/inherited permissions assigned to the content item for all user groups - /// - /// - /// - EntityPermissionCollection GetPermissionsForEntity(IContent content); - - bool SendToPublication(IContent content, int userId = 0); - - IEnumerable GetByIds(IEnumerable ids); - IEnumerable GetByIds(IEnumerable ids); - - /// - /// 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 - /// - IContent CreateContent(string name, Guid parentId, string contentTypeAlias, int userId = 0); - - /// - /// 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 - /// - IContent CreateContent(string name, int parentId, string contentTypeAlias, int userId = 0); - - /// - /// 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 - /// - IContent CreateContent(string name, IContent parent, string contentTypeAlias, int userId = 0); - - /// - /// Gets an object by Id - /// - /// Id of the Content to retrieve - /// - IContent GetById(int id); - - /// - /// Gets an object by its 'UniqueId' - /// - /// Guid key of the Content to retrieve - /// - IContent GetById(Guid key); - - /// - /// Gets a collection of objects by the Id of the - /// - /// Id of the - /// An Enumerable list of objects - IEnumerable GetContentOfContentType(int id); - - /// - /// Gets a collection of objects by Level - /// - /// The level to retrieve Content from - /// An Enumerable list of objects - IEnumerable GetByLevel(int level); - - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Children from - /// An Enumerable list of objects - IEnumerable GetChildren(int id); - - [Obsolete("Use the overload with 'long' parameter types instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalRecords, - string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, string filter = ""); - - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Children 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 - IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalRecords, - string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, string filter = ""); - - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Children 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 - IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, bool orderBySystemField, string filter); - - [Obsolete("Use the overload with 'long' parameter types instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalRecords, - string orderBy = "path", Direction orderDirection = Direction.Ascending, string 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 - /// Search text filter - /// An Enumerable list of objects - IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalRecords, - string orderBy = "path", Direction orderDirection = Direction.Ascending, string 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 - IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, bool orderBySystemField, string 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 - /// - /// An Enumerable list of objects - IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, bool orderBySystemField, IQuery filter); - - /// - /// Gets a collection of an objects versions by its Id - /// - /// - /// An Enumerable list of objects - IEnumerable GetVersions(int 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 - /// - IEnumerable GetVersionIds(int id, int maxRows); - - /// - /// Gets a collection of objects, which reside at the first level / root - /// - /// An Enumerable list of objects - IEnumerable GetRootContent(); - - /// - /// Gets a collection of objects, which has an expiration date greater then today - /// - /// An Enumerable list of objects - IEnumerable GetContentForExpiration(); - - /// - /// Gets a collection of objects, which has a release date greater then today - /// - /// An Enumerable list of objects - IEnumerable GetContentForRelease(); - - /// - /// Gets a collection of an objects, which resides in the Recycle Bin - /// - /// An Enumerable list of objects - IEnumerable GetContentInRecycleBin(); - - /// - /// Saves a single object - /// - /// The to save - /// Optional Id of the User saving the Content - /// Optional boolean indicating whether or not to raise events. - void Save(IContent content, int userId = 0, bool raiseEvents = true); - - /// - /// 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. - void Save(IEnumerable contents, int userId = 0, bool raiseEvents = true); - - /// - /// 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 - void DeleteContentOfType(int contentTypeId, int userId = 0); - - /// - /// Deletes all content of the specified types. All Descendants of deleted content that is not of these types is moved to Recycle Bin. - /// - /// This needs extra care and attention as its potentially a dangerous and extensive operation - /// Ids of the s - /// Optional Id of the user issueing the delete operation - void DeleteContentOfTypes(IEnumerable contentTypeIds, int userId = 0); - - /// - /// Permanently deletes versions from an object prior to a specific date. - /// - /// Id of the object to delete versions from - /// Latest version date - /// Optional Id of the User deleting versions of a Content object - void DeleteVersions(int id, DateTime versionDate, int userId = 0); - - /// - /// Permanently deletes a specific version from an object. - /// - /// Id of the object to delete a version from - /// Id of the version to delete - /// Boolean indicating whether to delete versions prior to the versionId - /// Optional Id of the User deleting versions of a Content object - void DeleteVersion(int id, Guid versionId, bool deletePriorVersions, int userId = 0); - - /// - /// 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 - void MoveToRecycleBin(IContent content, int userId = 0); - - /// - /// Moves an object to a new location - /// - /// The to move - /// Id of the Content's new Parent - /// Optional Id of the User moving the Content - void Move(IContent content, int parentId, int userId = 0); - - /// - /// 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")] - void EmptyRecycleBin(); - - /// - /// Empties the Recycle Bin by deleting all that resides in the bin - /// - /// Optional Id of the User emptying the Recycle Bin - void EmptyRecycleBin(int userId = 0); - - /// - /// Rollback an object to a previous version. - /// This will create a new version, which is a copy of all the old data. - /// - /// 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 - IContent Rollback(int id, Guid versionId, int userId = 0); - - /// - /// 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 - IEnumerable GetChildrenByName(int parentId, string name); - - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Descendants from - /// An Enumerable list of objects - IEnumerable GetDescendants(int id); - - /// - /// Gets a collection of objects by Parent Id - /// - /// item to retrieve Descendants from - /// An Enumerable list of objects - IEnumerable GetDescendants(IContent content); - - /// - /// Gets a specific version of an item. - /// - /// Id of the version to retrieve - /// An item - IContent GetByVersion(Guid versionId); - - /// - /// Gets the published version of an item - /// - /// Id of the to retrieve version from - /// An item - IContent GetPublishedVersion(int id); - - /// - /// Gets the published version of a item. - /// - /// The content item. - /// The published version, if any; otherwise, null. - IContent GetPublishedVersion(IContent content); - - /// - /// Checks whether an item has any children - /// - /// Id of the - /// True if the content has any children otherwise False - bool HasChildren(int id); - - /// - /// Checks whether an item has any published versions - /// - /// Id of the - /// True if the content has any published version otherwise False - bool HasPublishedVersion(int id); - - /// - /// Re-Publishes all Content - /// - /// Optional Id of the User issueing the publishing - /// True if publishing succeeded, otherwise False - bool RePublishAll(int userId = 0); - - /// - /// Publishes a single object - /// - /// The to publish - /// Optional Id of the User issueing the publishing - /// True if publishing succeeded, otherwise False - bool Publish(IContent content, int userId = 0); - - /// - /// Publishes a single object - /// - /// The to publish - /// Optional Id of the User issueing the publishing - /// The published status attempt - Attempt PublishWithStatus(IContent content, int userId = 0); - - /// - /// 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 - [EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("Use PublishWithChildrenWithStatus instead, that method will provide more detailed information on the outcome and also allows the includeUnpublished flag")] - bool PublishWithChildren(IContent content, int userId = 0); - - /// - /// 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> PublishWithChildrenWithStatus(IContent content, int userId = 0, bool includeUnpublished = false); - - /// - /// UnPublishes a single object - /// - /// The to publish - /// Optional Id of the User issueing the publishing - /// True if unpublishing succeeded, otherwise False - bool UnPublish(IContent content, int userId = 0); - - /// - /// 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")] - [EditorBrowsable(EditorBrowsableState.Never)] - bool SaveAndPublish(IContent content, int userId = 0, bool raiseEvents = true); - - /// - /// 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 SaveAndPublishWithStatus(IContent content, int userId = 0, bool raiseEvents = true); - - /// - /// 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 - void Delete(IContent content, int userId = 0); - - /// - /// 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 - IContent Copy(IContent content, int parentId, bool relateToOriginal, int userId = 0); - - /// - /// 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 - IContent Copy(IContent content, int parentId, bool relateToOriginal, bool recursive, int userId = 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 - bool IsPublishable(IContent content); - - /// - /// Gets a collection of objects, which are ancestors of the current content. - /// - /// Id of the to retrieve ancestors for - /// An Enumerable list of objects - IEnumerable GetAncestors(int id); - - /// - /// Gets a collection of objects, which are ancestors of the current content. - /// - /// to retrieve ancestors for - /// An Enumerable list of objects - IEnumerable GetAncestors(IContent 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 - bool Sort(IEnumerable items, int userId = 0, bool raiseEvents = 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 - bool Sort(int[] ids, int userId = 0, bool raiseEvents = true); - - /// - /// Gets the parent of the current content as an item. - /// - /// Id of the to retrieve the parent from - /// Parent object - IContent GetParent(int id); - - /// - /// Gets the parent of the current content as an item. - /// - /// to retrieve the parent from - /// Parent object - IContent GetParent(IContent content); - - /// - /// Creates and saves an object using the alias of the - /// that this Content should based on. - /// - /// - /// This method returns an object that has been persisted to the database - /// and therefor has an identity. - /// - /// Name of the Content object - /// Parent object for the new Content - /// Alias of the - /// Optional id of the user creating the content - /// - IContent CreateContentWithIdentity(string name, IContent parent, string contentTypeAlias, int userId = 0); - - /// - /// 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 - /// - IContent CreateContentWithIdentity(string name, int parentId, string contentTypeAlias, int userId = 0); - } -} +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Xml; +using System.Xml.Linq; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Publishing; + +namespace Umbraco.Core.Services +{ + /// + /// A temporary interface until we are in v8, this is used to return a different result for the same method and this interface gets implemented + /// explicitly. These methods will replace the normal ones in IContentService in v8 and this will be removed. + /// + public interface IContentServiceOperations + { + //TODO: Remove this class in v8 + + //TODO: There's probably more that needs to be added like the EmptyRecycleBin, etc... + + /// + /// 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 Save(IContent content, int userId = 0, bool raiseEvents = true); + + /// + /// 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 Save(IEnumerable contents, int userId = 0, bool raiseEvents = true); + + /// + /// 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 Delete(IContent content, int userId = 0); + + /// + /// Publishes a single object + /// + /// The to publish + /// Optional Id of the User issueing the publishing + /// The published status attempt + Attempt Publish(IContent content, int userId = 0); + + /// + /// 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> PublishWithChildren(IContent content, int userId = 0, bool includeUnpublished = false); + + /// + /// 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 SaveAndPublish(IContent content, int userId = 0, bool raiseEvents = true); + + /// + /// 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 MoveToRecycleBin(IContent content, int userId = 0); + + /// + /// UnPublishes a single object + /// + /// The to publish + /// Optional Id of the User issueing the publishing + /// True if unpublishing succeeded, otherwise False + Attempt UnPublish(IContent content, int userId = 0); + } + + /// + /// Defines the ContentService, which is an easy access to operations involving + /// + public interface IContentService : IContentServiceBase + { + IEnumerable GetBlueprintsForContentTypes(params int[] documentTypeIds); + IContent GetBlueprintById(int id); + IContent GetBlueprintById(Guid id); + void SaveBlueprint(IContent content, int userId = 0); + void DeleteBlueprint(IContent content, int userId = 0); + IContent CreateContentFromBlueprint(IContent blueprint, string name, int userId = 0); + void DeleteBlueprintsOfType(int contentTypeId, int userId = 0); + void DeleteBlueprintsOfTypes(IEnumerable contentTypeIds, int userId = 0); + + /// + /// Gets all XML entries found in the cmsContentXml table based on the given 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 + /// + /// If -1 is passed, then this will return all content xml entries, otherwise will return all descendents from the path + /// + IEnumerable GetPagedXmlEntries(string path, long pageIndex, int pageSize, out long totalRecords); + + /// + /// This builds the Xml document used for the XML cache + /// + /// + XmlDocument BuildXmlCache(); + + XmlDocument BuildPreviewXmlCache(); + + /// + /// 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 + /// + void RebuildXmlStructures(params int[] contentTypeIds); + + int CountPublished(string contentTypeAlias = null); + int Count(string contentTypeAlias = null); + int CountChildren(int parentId, string contentTypeAlias = null); + int CountDescendants(int parentId, string contentTypeAlias = null); + + /// + /// 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 group id & permission pairs. + /// + /// + void ReplaceContentPermissions(EntityPermissionSet permissionSet); + + /// + /// Assigns a single permission to the current content item for the specified user group ids + /// + /// + /// + /// + void AssignContentPermission(IContent entity, char permission, IEnumerable groupIds); + + /// + /// Returns implicit/inherited permissions assigned to the content item for all user groups + /// + /// + /// + EntityPermissionCollection GetPermissionsForEntity(IContent content); + + bool SendToPublication(IContent content, int userId = 0); + + IEnumerable GetByIds(IEnumerable ids); + IEnumerable GetByIds(IEnumerable ids); + + /// + /// 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 + /// + IContent CreateContent(string name, Guid parentId, string contentTypeAlias, int userId = 0); + + /// + /// 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 + /// + IContent CreateContent(string name, int parentId, string contentTypeAlias, int userId = 0); + + /// + /// 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 + /// + IContent CreateContent(string name, IContent parent, string contentTypeAlias, int userId = 0); + + /// + /// Gets an object by Id + /// + /// Id of the Content to retrieve + /// + IContent GetById(int id); + + /// + /// Gets an object by its 'UniqueId' + /// + /// Guid key of the Content to retrieve + /// + IContent GetById(Guid key); + + /// + /// Gets a collection of objects by the Id of the + /// + /// Id of the + /// An Enumerable list of objects + IEnumerable GetContentOfContentType(int id); + + /// + /// Gets a collection of objects by Level + /// + /// The level to retrieve Content from + /// An Enumerable list of objects + IEnumerable GetByLevel(int level); + + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Children from + /// An Enumerable list of objects + IEnumerable GetChildren(int id); + + [Obsolete("Use the overload with 'long' parameter types instead")] + [EditorBrowsable(EditorBrowsableState.Never)] + IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalRecords, + string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, string filter = ""); + + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Children 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 + IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalRecords, + string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, string filter = ""); + + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Children 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 + IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalRecords, + string orderBy, Direction orderDirection, bool orderBySystemField, string filter); + + [Obsolete("Use the overload with 'long' parameter types instead")] + [EditorBrowsable(EditorBrowsableState.Never)] + IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalRecords, + string orderBy = "path", Direction orderDirection = Direction.Ascending, string 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 + /// Search text filter + /// An Enumerable list of objects + IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalRecords, + string orderBy = "path", Direction orderDirection = Direction.Ascending, string 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 + IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalRecords, + string orderBy, Direction orderDirection, bool orderBySystemField, string 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 + /// + /// An Enumerable list of objects + IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalRecords, + string orderBy, Direction orderDirection, bool orderBySystemField, IQuery filter); + + /// + /// Gets a collection of an objects versions by its Id + /// + /// + /// An Enumerable list of objects + IEnumerable GetVersions(int 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 + /// + IEnumerable GetVersionIds(int id, int maxRows); + + /// + /// Gets a collection of objects, which reside at the first level / root + /// + /// An Enumerable list of objects + IEnumerable GetRootContent(); + + /// + /// Gets a collection of objects, which has an expiration date greater then today + /// + /// An Enumerable list of objects + IEnumerable GetContentForExpiration(); + + /// + /// Gets a collection of objects, which has a release date greater then today + /// + /// An Enumerable list of objects + IEnumerable GetContentForRelease(); + + /// + /// Gets a collection of an objects, which resides in the Recycle Bin + /// + /// An Enumerable list of objects + IEnumerable GetContentInRecycleBin(); + + /// + /// Saves a single object + /// + /// The to save + /// Optional Id of the User saving the Content + /// Optional boolean indicating whether or not to raise events. + void Save(IContent content, int userId = 0, bool raiseEvents = true); + + /// + /// 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. + void Save(IEnumerable contents, int userId = 0, bool raiseEvents = true); + + /// + /// 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 + void DeleteContentOfType(int contentTypeId, int userId = 0); + + /// + /// Deletes all content of the specified types. All Descendants of deleted content that is not of these types is moved to Recycle Bin. + /// + /// This needs extra care and attention as its potentially a dangerous and extensive operation + /// Ids of the s + /// Optional Id of the user issueing the delete operation + void DeleteContentOfTypes(IEnumerable contentTypeIds, int userId = 0); + + /// + /// Permanently deletes versions from an object prior to a specific date. + /// + /// Id of the object to delete versions from + /// Latest version date + /// Optional Id of the User deleting versions of a Content object + void DeleteVersions(int id, DateTime versionDate, int userId = 0); + + /// + /// Permanently deletes a specific version from an object. + /// + /// Id of the object to delete a version from + /// Id of the version to delete + /// Boolean indicating whether to delete versions prior to the versionId + /// Optional Id of the User deleting versions of a Content object + void DeleteVersion(int id, Guid versionId, bool deletePriorVersions, int userId = 0); + + /// + /// 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 + void MoveToRecycleBin(IContent content, int userId = 0); + + /// + /// Moves an object to a new location + /// + /// The to move + /// Id of the Content's new Parent + /// Optional Id of the User moving the Content + void Move(IContent content, int parentId, int userId = 0); + + /// + /// 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")] + void EmptyRecycleBin(); + + /// + /// Empties the Recycle Bin by deleting all that resides in the bin + /// + /// Optional Id of the User emptying the Recycle Bin + void EmptyRecycleBin(int userId = 0); + + /// + /// Rollback an object to a previous version. + /// This will create a new version, which is a copy of all the old data. + /// + /// 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 + IContent Rollback(int id, Guid versionId, int userId = 0); + + /// + /// 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 + IEnumerable GetChildrenByName(int parentId, string name); + + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Descendants from + /// An Enumerable list of objects + IEnumerable GetDescendants(int id); + + /// + /// Gets a collection of objects by Parent Id + /// + /// item to retrieve Descendants from + /// An Enumerable list of objects + IEnumerable GetDescendants(IContent content); + + /// + /// Gets a specific version of an item. + /// + /// Id of the version to retrieve + /// An item + IContent GetByVersion(Guid versionId); + + /// + /// Gets the published version of an item + /// + /// Id of the to retrieve version from + /// An item + IContent GetPublishedVersion(int id); + + /// + /// Gets the published version of a item. + /// + /// The content item. + /// The published version, if any; otherwise, null. + IContent GetPublishedVersion(IContent content); + + /// + /// Checks whether an item has any children + /// + /// Id of the + /// True if the content has any children otherwise False + bool HasChildren(int id); + + /// + /// Checks whether an item has any published versions + /// + /// Id of the + /// True if the content has any published version otherwise False + bool HasPublishedVersion(int id); + + /// + /// Re-Publishes all Content + /// + /// Optional Id of the User issueing the publishing + /// True if publishing succeeded, otherwise False + bool RePublishAll(int userId = 0); + + /// + /// Publishes a single object + /// + /// The to publish + /// Optional Id of the User issueing the publishing + /// True if publishing succeeded, otherwise False + bool Publish(IContent content, int userId = 0); + + /// + /// Publishes a single object + /// + /// The to publish + /// Optional Id of the User issueing the publishing + /// The published status attempt + Attempt PublishWithStatus(IContent content, int userId = 0); + + /// + /// 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 + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use PublishWithChildrenWithStatus instead, that method will provide more detailed information on the outcome and also allows the includeUnpublished flag")] + bool PublishWithChildren(IContent content, int userId = 0); + + /// + /// 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> PublishWithChildrenWithStatus(IContent content, int userId = 0, bool includeUnpublished = false); + + /// + /// UnPublishes a single object + /// + /// The to publish + /// Optional Id of the User issueing the publishing + /// True if unpublishing succeeded, otherwise False + bool UnPublish(IContent content, int userId = 0); + + /// + /// 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")] + [EditorBrowsable(EditorBrowsableState.Never)] + bool SaveAndPublish(IContent content, int userId = 0, bool raiseEvents = true); + + /// + /// 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 SaveAndPublishWithStatus(IContent content, int userId = 0, bool raiseEvents = true); + + /// + /// 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 + void Delete(IContent content, int userId = 0); + + /// + /// 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 + IContent Copy(IContent content, int parentId, bool relateToOriginal, int userId = 0); + + /// + /// 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 + IContent Copy(IContent content, int parentId, bool relateToOriginal, bool recursive, int userId = 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 + bool IsPublishable(IContent content); + + /// + /// Gets a collection of objects, which are ancestors of the current content. + /// + /// Id of the to retrieve ancestors for + /// An Enumerable list of objects + IEnumerable GetAncestors(int id); + + /// + /// Gets a collection of objects, which are ancestors of the current content. + /// + /// to retrieve ancestors for + /// An Enumerable list of objects + IEnumerable GetAncestors(IContent 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 + bool Sort(IEnumerable items, int userId = 0, bool raiseEvents = 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 + bool Sort(int[] ids, int userId = 0, bool raiseEvents = true); + + /// + /// Gets the parent of the current content as an item. + /// + /// Id of the to retrieve the parent from + /// Parent object + IContent GetParent(int id); + + /// + /// Gets the parent of the current content as an item. + /// + /// to retrieve the parent from + /// Parent object + IContent GetParent(IContent content); + + /// + /// Creates and saves an object using the alias of the + /// that this Content should based on. + /// + /// + /// This method returns an object that has been persisted to the database + /// and therefor has an identity. + /// + /// Name of the Content object + /// Parent object for the new Content + /// Alias of the + /// Optional id of the user creating the content + /// + IContent CreateContentWithIdentity(string name, IContent parent, string contentTypeAlias, int userId = 0); + + /// + /// 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 + /// + IContent CreateContentWithIdentity(string name, int parentId, string contentTypeAlias, int userId = 0); + + IList GetAnchorValuesFromRTEs(int id); + IList GetAnchorValuesFromRTEContent(string rteContent); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js index 31ff7424ac..db5e9dd404 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js @@ -160,6 +160,38 @@ function entityResource($q, $http, umbRequestHelper) { 'Failed to retrieve entity data for id ' + id); }, + + getUrlAndAnchors: function (id) { + + if (id === -1 || id === "-1") { + return null; + } + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "entityApiBaseUrl", + "GetUrlAndAnchors", + [{ id: id }])), + 'Failed to retrieve url and anchors data for id ' + id); + }, + + + getAnchors: function (rteContent) { + + if (rteContent == null || rteContent.length === 0) { + return []; + } + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "entityApiBaseUrl", + "GetAnchors", + [{ rteContent: rteContent }])), + 'Failed to anchors data for rte content ' + rteContent); + }, + /** * @ngdoc method * @name umbraco.resources.entityResource#getByIds @@ -488,6 +520,7 @@ function entityResource($q, $http, umbRequestHelper) { 'Failed to retrieve child data for id ' + parentId); }, + /** * @ngdoc method * @name umbraco.resources.entityResource#search @@ -583,6 +616,8 @@ function entityResource($q, $http, umbRequestHelper) { 'Failed to retrieve entity data for query ' + query); } + + }; } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js index 7edd462a57..f721c31337 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js @@ -1,879 +1,850 @@ -/** - * @ngdoc service - * @name umbraco.services.tinyMceService - * - * - * @description - * A service containing all logic for all of the Umbraco TinyMCE plugins - */ -function tinyMceService($log, imageHelper, $http, $timeout, macroResource, macroService, $routeParams, umbRequestHelper, angularHelper, userService) { - return { - - /** - * @ngdoc method - * @name umbraco.services.tinyMceService#configuration - * @methodOf umbraco.services.tinyMceService - * - * @description - * Returns a collection of plugins available to the tinyMCE editor - * - */ - configuration: function () { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "rteApiBaseUrl", - "GetConfiguration"), { - cache: true - }), - 'Failed to retrieve tinymce configuration'); - }, - - /** - * @ngdoc method - * @name umbraco.services.tinyMceService#defaultPrevalues - * @methodOf umbraco.services.tinyMceService - * - * @description - * Returns a default configration to fallback on in case none is provided - * - */ - defaultPrevalues: function () { - var cfg = {}; - cfg.toolbar = ["code", "bold", "italic", "styleselect", "alignleft", "aligncenter", "alignright", "bullist", "numlist", "outdent", "indent", "link", "image", "umbmediapicker", "umbembeddialog", "umbmacro"]; - cfg.stylesheets = []; - cfg.dimensions = { - height: 500 - }; - cfg.maxImageSize = 500; - return cfg; - }, - - /** - * @ngdoc method - * @name umbraco.services.tinyMceService#createInsertEmbeddedMedia - * @methodOf umbraco.services.tinyMceService - * - * @description - * Creates the umbrco insert embedded media tinymce plugin - * - * @param {Object} editor the TinyMCE editor instance - * @param {Object} $scope the current controller scope - */ - createInsertEmbeddedMedia: function (editor, scope, callback) { - editor.addButton('umbembeddialog', { - icon: 'custom icon-tv', - tooltip: 'Embed', - onclick: function () { - if (callback) { - callback(); - } - } - }); - }, - - insertEmbeddedMediaInEditor: function (editor, preview) { - editor.insertContent(preview); - }, - - /** - * @ngdoc method - * @name umbraco.services.tinyMceService#createMediaPicker - * @methodOf umbraco.services.tinyMceService - * - * @description - * Creates the umbrco insert media tinymce plugin - * - * @param {Object} editor the TinyMCE editor instance - * @param {Object} $scope the current controller scope - */ - createMediaPicker: function (editor, scope, callback) { - editor.addButton('umbmediapicker', { - icon: 'custom icon-picture', - tooltip: 'Media Picker', - stateSelector: 'img', - onclick: function () { - - var selectedElm = editor.selection.getNode(), - currentTarget; - - - if (selectedElm.nodeName === 'IMG') { - var img = $(selectedElm); - - var hasUdi = img.attr("data-udi") ? true : false; - - currentTarget = { - altText: img.attr("alt"), - url: img.attr("src") - }; - - if (hasUdi) { - currentTarget["udi"] = img.attr("data-udi"); - } else { - currentTarget["id"] = img.attr("rel"); - } - } - - userService.getCurrentUser().then(function (userData) { - if (callback) { - callback(currentTarget, userData); - } - }); - - } - }); - }, - - insertMediaInEditor: function (editor, img) { - if (img) { - - var hasUdi = img.udi ? true : false; - - var data = { - alt: img.altText || "", - src: (img.url) ? img.url : "nothing.jpg", - id: '__mcenew' - }; - - if (hasUdi) { - data["data-udi"] = img.udi; - } else { - //Considering these fixed because UDI will now be used and thus - // we have no need for rel http://issues.umbraco.org/issue/U4-6228, http://issues.umbraco.org/issue/U4-6595 - data["rel"] = img.id; - data["data-id"] = img.id; - } - - editor.insertContent(editor.dom.createHTML('img', data)); - - $timeout(function () { - var imgElm = editor.dom.get('__mcenew'); - var size = editor.dom.getSize(imgElm); - - if (editor.settings.maxImageSize && editor.settings.maxImageSize !== 0) { - var newSize = imageHelper.scaleToMaxSize(editor.settings.maxImageSize, size.w, size.h); - - var s = "width: " + newSize.width + "px; height:" + newSize.height + "px;"; - editor.dom.setAttrib(imgElm, 'style', s); - - if (img.url) { - var src = img.url + "?width=" + newSize.width + "&height=" + newSize.height; - editor.dom.setAttrib(imgElm, 'data-mce-src', src); - } - } - editor.dom.setAttrib(imgElm, 'id', null); - }, 500); - } - }, - - /** - * @ngdoc method - * @name umbraco.services.tinyMceService#createUmbracoMacro - * @methodOf umbraco.services.tinyMceService - * - * @description - * Creates the insert umbrco macro tinymce plugin - * - * @param {Object} editor the TinyMCE editor instance - * @param {Object} $scope the current controller scope - */ - createInsertMacro: function (editor, $scope, callback) { - - var createInsertMacroScope = this; - - /** Adds custom rules for the macro plugin and custom serialization */ - editor.on('preInit', function (args) { - //this is requires so that we tell the serializer that a 'div' is actually allowed in the root, otherwise the cleanup will strip it out - editor.serializer.addRules('div'); - - /** This checks if the div is a macro container, if so, checks if its wrapped in a p tag and then unwraps it (removes p tag) */ - editor.serializer.addNodeFilter('div', function (nodes, name) { - for (var i = 0; i < nodes.length; i++) { - if (nodes[i].attr("class") === "umb-macro-holder" && nodes[i].parent && nodes[i].parent.name.toUpperCase() === "P") { - nodes[i].parent.unwrap(); - } - } - }); - - }); - - /** - * Because the macro gets wrapped in a P tag because of the way 'enter' works, this - * method will return the macro element if not wrapped in a p, or the p if the macro - * element is the only one inside of it even if we are deep inside an element inside the macro - */ - function getRealMacroElem(element) { - var e = $(element).closest(".umb-macro-holder"); - if (e.length > 0) { - if (e.get(0).parentNode.nodeName === "P") { - //now check if we're the only element - if (element.parentNode.childNodes.length === 1) { - return e.get(0).parentNode; - } - } - return e.get(0); - } - return null; - } - - /** Adds the button instance */ - editor.addButton('umbmacro', { - icon: 'custom icon-settings-alt', - tooltip: 'Insert macro', - onPostRender: function () { - - var ctrl = this; - var isOnMacroElement = false; - - /** - if the selection comes from a different element that is not the macro's - we need to check if the selection includes part of the macro, if so we'll force the selection - to clear to the next element since if people can select part of the macro markup they can then modify it. - */ - function handleSelectionChange() { - - if (!editor.selection.isCollapsed()) { - var endSelection = tinymce.activeEditor.selection.getEnd(); - var startSelection = tinymce.activeEditor.selection.getStart(); - //don't proceed if it's an entire element selected - if (endSelection !== startSelection) { - - //if the end selection is a macro then move the cursor - //NOTE: we don't have to handle when the selection comes from a previous parent because - // that is automatically taken care of with the normal onNodeChanged logic since the - // evt.element will be the macro once it becomes part of the selection. - var $testForMacro = $(endSelection).closest(".umb-macro-holder"); - if ($testForMacro.length > 0) { - - //it came from before so move after, if there is no after then select ourselves - var next = $testForMacro.next(); - if (next.length > 0) { - editor.selection.setCursorLocation($testForMacro.next().get(0)); - } else { - selectMacroElement($testForMacro.get(0)); - } - - } - } - } - } - - /** helper method to select the macro element */ - function selectMacroElement(macroElement) { - - // move selection to top element to ensure we can't edit this - editor.selection.select(macroElement); - - // check if the current selection *is* the element (ie bug) - var currentSelection = editor.selection.getStart(); - if (tinymce.isIE) { - if (!editor.dom.hasClass(currentSelection, 'umb-macro-holder')) { - while (!editor.dom.hasClass(currentSelection, 'umb-macro-holder') && currentSelection.parentNode) { - currentSelection = currentSelection.parentNode; - } - editor.selection.select(currentSelection); - } - } - } - - /** - * Add a node change handler, test if we're editing a macro and select the whole thing, then set our isOnMacroElement flag. - * If we change the selection inside this method, then we end up in an infinite loop, so we have to remove ourselves - * from the event listener before changing selection, however, it seems that putting a break point in this method - * will always cause an 'infinite' loop as the caret keeps changing. - */ - function onNodeChanged(evt) { - - //set our macro button active when on a node of class umb-macro-holder - var $macroElement = $(evt.element).closest(".umb-macro-holder"); - - handleSelectionChange(); - - //set the button active - ctrl.active($macroElement.length !== 0); - - if ($macroElement.length > 0) { - var macroElement = $macroElement.get(0); - - //remove the event listener before re-selecting - editor.off('NodeChange', onNodeChanged); - - selectMacroElement(macroElement); - - //set the flag - isOnMacroElement = true; - - //re-add the event listener - editor.on('NodeChange', onNodeChanged); - } else { - isOnMacroElement = false; - } - - } - - /** when the contents load we need to find any macros declared and load in their content */ - editor.on("LoadContent", function (o) { - - //get all macro divs and load their content - $(editor.dom.select(".umb-macro-holder.mceNonEditable")).each(function () { - createInsertMacroScope.loadMacroContent($(this), null, $scope); - }); - - }); - - /** This prevents any other commands from executing when the current element is the macro so the content cannot be edited */ - editor.on('BeforeExecCommand', function (o) { - if (isOnMacroElement) { - if (o.preventDefault) { - o.preventDefault(); - } - if (o.stopImmediatePropagation) { - o.stopImmediatePropagation(); - } - return; - } - }); - - /** This double checks and ensures you can't paste content into the rendered macro */ - editor.on("Paste", function (o) { - if (isOnMacroElement) { - if (o.preventDefault) { - o.preventDefault(); - } - if (o.stopImmediatePropagation) { - o.stopImmediatePropagation(); - } - return; - } - }); - - //set onNodeChanged event listener - editor.on('NodeChange', onNodeChanged); - - /** - * Listen for the keydown in the editor, we'll check if we are currently on a macro element, if so - * we'll check if the key down is a supported key which requires an action, otherwise we ignore the request - * so the macro cannot be edited. - */ - editor.on('KeyDown', function (e) { - if (isOnMacroElement) { - var macroElement = editor.selection.getNode(); - - //get the 'real' element (either p or the real one) - macroElement = getRealMacroElem(macroElement); - - //prevent editing - e.preventDefault(); - e.stopPropagation(); - - var moveSibling = function (element, isNext) { - var $e = $(element); - var $sibling = isNext ? $e.next() : $e.prev(); - if ($sibling.length > 0) { - editor.selection.select($sibling.get(0)); - editor.selection.collapse(true); - } else { - //if we're moving previous and there is no sibling, then lets recurse and just select the next one - if (!isNext) { - moveSibling(element, true); - return; - } - - //if there is no sibling we'll generate a new p at the end and select it - editor.setContent(editor.getContent() + "

 

"); - editor.selection.select($(editor.dom.getRoot()).children().last().get(0)); - editor.selection.collapse(true); - - } - }; - - //supported keys to move to the next or prev element (13-enter, 27-esc, 38-up, 40-down, 39-right, 37-left) - //supported keys to remove the macro (8-backspace, 46-delete) - //TODO: Should we make the enter key insert a line break before or leave it as moving to the next element? - if ($.inArray(e.keyCode, [13, 40, 39]) !== -1) { - //move to next element - moveSibling(macroElement, true); - } else if ($.inArray(e.keyCode, [27, 38, 37]) !== -1) { - //move to prev element - moveSibling(macroElement, false); - } else if ($.inArray(e.keyCode, [8, 46]) !== -1) { - //delete macro element - - //move first, then delete - moveSibling(macroElement, false); - editor.dom.remove(macroElement); - } - return; - } - }); - - }, - - /** The insert macro button click event handler */ - onclick: function () { - - var dialogData = { - //flag for use in rte so we only show macros flagged for the editor - richTextEditor: true - }; - - //when we click we could have a macro already selected and in that case we'll want to edit the current parameters - //so we'll need to extract them and submit them to the dialog. - var macroElement = editor.selection.getNode(); - macroElement = getRealMacroElem(macroElement); - if (macroElement) { - //we have a macro selected so we'll need to parse it's alias and parameters - var contents = $(macroElement).contents(); - var comment = _.find(contents, function (item) { - return item.nodeType === 8; - }); - if (!comment) { - throw "Cannot parse the current macro, the syntax in the editor is invalid"; - } - var syntax = comment.textContent.trim(); - var parsed = macroService.parseMacroSyntax(syntax); - dialogData = { - macroData: parsed - }; - } - - if (callback) { - callback(dialogData); - } - - } - }); - }, - - insertMacroInEditor: function (editor, macroObject, $scope) { - - //put the macro syntax in comments, we will parse this out on the server side to be used - //for persisting. - var macroSyntaxComment = ""; - //create an id class for this element so we can re-select it after inserting - var uniqueId = "umb-macro-" + editor.dom.uniqueId(); - var macroDiv = editor.dom.create('div', { - 'class': 'umb-macro-holder ' + macroObject.macroAlias + ' mceNonEditable ' + uniqueId - }, - macroSyntaxComment + 'Macro alias: ' + macroObject.macroAlias + ''); - - editor.selection.setNode(macroDiv); - - var $macroDiv = $(editor.dom.select("div.umb-macro-holder." + uniqueId)); - - //async load the macro content - this.loadMacroContent($macroDiv, macroObject, $scope); - - }, - - /** loads in the macro content async from the server */ - loadMacroContent: function ($macroDiv, macroData, $scope) { - - //if we don't have the macroData, then we'll need to parse it from the macro div - if (!macroData) { - var contents = $macroDiv.contents(); - var comment = _.find(contents, function (item) { - return item.nodeType === 8; - }); - if (!comment) { - throw "Cannot parse the current macro, the syntax in the editor is invalid"; - } - var syntax = comment.textContent.trim(); - var parsed = macroService.parseMacroSyntax(syntax); - macroData = parsed; - } - - var $ins = $macroDiv.find("ins"); - - //show the throbber - $macroDiv.addClass("loading"); - - var contentId = $routeParams.id; - - //need to wrap in safe apply since this might be occuring outside of angular - angularHelper.safeApply($scope, function () { - macroResource.getMacroResultAsHtmlForEditor(macroData.macroAlias, contentId, macroData.macroParamsDictionary) - .then(function (htmlResult) { - - $macroDiv.removeClass("loading"); - htmlResult = htmlResult.trim(); - if (htmlResult !== "") { - $ins.html(htmlResult); - } - }); - }); - - }, - - createLinkPicker: function (editor, $scope, onClick) { - - function createLinkList(callback) { - return function () { - var linkList = editor.settings.link_list; - - if (typeof (linkList) === "string") { - tinymce.util.XHR.send({ - url: linkList, - success: function (text) { - callback(tinymce.util.JSON.parse(text)); - } - }); - } else { - callback(linkList); - } - }; - } - - function showDialog(linkList) { - var data = {}, - selection = editor.selection, - dom = editor.dom, - selectedElm, anchorElm, initialText; - var win, linkListCtrl, relListCtrl, targetListCtrl; - - function linkListChangeHandler(e) { - var textCtrl = win.find('#text'); - - if (!textCtrl.value() || (e.lastControl && textCtrl.value() === e.lastControl.text())) { - textCtrl.value(e.control.text()); - } - - win.find('#href').value(e.control.value()); - } - - function buildLinkList() { - var linkListItems = [{ - text: 'None', - value: '' - }]; - - tinymce.each(linkList, function (link) { - linkListItems.push({ - text: link.text || link.title, - value: link.value || link.url, - menu: link.menu - }); - }); - - return linkListItems; - } - - function buildRelList(relValue) { - var relListItems = [{ - text: 'None', - value: '' - }]; - - tinymce.each(editor.settings.rel_list, function (rel) { - relListItems.push({ - text: rel.text || rel.title, - value: rel.value, - selected: relValue === rel.value - }); - }); - - return relListItems; - } - - function buildTargetList(targetValue) { - var targetListItems = [{ - text: 'None', - value: '' - }]; - - if (!editor.settings.target_list) { - targetListItems.push({ - text: 'New window', - value: '_blank' - }); - } - - tinymce.each(editor.settings.target_list, function (target) { - targetListItems.push({ - text: target.text || target.title, - value: target.value, - selected: targetValue === target.value - }); - }); - - return targetListItems; - } - - function buildAnchorListControl(url) { - var anchorList = []; - - tinymce.each(editor.dom.select('a:not([href])'), function (anchor) { - var id = anchor.name || anchor.id; - - if (id) { - anchorList.push({ - text: id, - value: '#' + id, - selected: url.indexOf('#' + id) !== -1 - }); - } - }); - - if (anchorList.length) { - anchorList.unshift({ - text: 'None', - value: '' - }); - - return { - name: 'anchor', - type: 'listbox', - label: 'Anchors', - values: anchorList, - onselect: linkListChangeHandler - }; - } - } - - function updateText() { - if (!initialText && data.text.length === 0) { - this.parent().parent().find('#text')[0].value(this.value()); - } - } - - selectedElm = selection.getNode(); - anchorElm = dom.getParent(selectedElm, 'a[href]'); - - data.text = initialText = anchorElm ? (anchorElm.innerText || anchorElm.textContent) : selection.getContent({ - format: 'text' - }); - data.href = anchorElm ? dom.getAttrib(anchorElm, 'href') : ''; - data.target = anchorElm ? dom.getAttrib(anchorElm, 'target') : ''; - data.rel = anchorElm ? dom.getAttrib(anchorElm, 'rel') : ''; - - if (selectedElm.nodeName === "IMG") { - data.text = initialText = " "; - } - - if (linkList) { - linkListCtrl = { - type: 'listbox', - label: 'Link list', - values: buildLinkList(), - onselect: linkListChangeHandler - }; - } - - if (editor.settings.target_list !== false) { - targetListCtrl = { - name: 'target', - type: 'listbox', - label: 'Target', - values: buildTargetList(data.target) - }; - } - - if (editor.settings.rel_list) { - relListCtrl = { - name: 'rel', - type: 'listbox', - label: 'Rel', - values: buildRelList(data.rel) - }; - } - - var currentTarget = null; - - //if we already have a link selected, we want to pass that data over to the dialog - if (anchorElm) { - var anchor = $(anchorElm); - currentTarget = { - name: anchor.attr("title"), - url: anchor.attr("href"), - target: anchor.attr("target") - }; - - // drop the lead char from the anchor text, if it has a value - var anchorVal = anchor[0].dataset.anchor; - if (anchorVal) { - currentTarget.anchor = anchorVal.substring(1); - } - - //locallink detection, we do this here, to avoid poluting the dialogservice - //so the dialog service can just expect to get a node-like structure - if (currentTarget.url.indexOf("localLink:") > 0) { - // if the current link has an anchor, it needs to be considered when getting the udi/id - // if an anchor exists, reduce the substring max by its length plus two to offset the removed prefix and trailing curly brace - var linkId = currentTarget.url.substring(currentTarget.url.indexOf(":") + 1, currentTarget.url.lastIndexOf("}")); - - //we need to check if this is an INT or a UDI - var parsedIntId = parseInt(linkId, 10); - if (isNaN(parsedIntId)) { - //it's a UDI - currentTarget.udi = linkId; - } else { - currentTarget.id = linkId; - } - } - } - - if (onClick) { - onClick(currentTarget, anchorElm); - } - - } - - editor.addButton('link', { - icon: 'link', - tooltip: 'Insert/edit link', - shortcut: 'Ctrl+K', - onclick: createLinkList(showDialog), - stateSelector: 'a[href]' - }); - - editor.addButton('unlink', { - icon: 'unlink', - tooltip: 'Remove link', - cmd: 'unlink', - stateSelector: 'a[href]' - }); - - editor.addShortcut('Ctrl+K', '', createLinkList(showDialog)); - this.showDialog = showDialog; - - editor.addMenuItem('link', { - icon: 'link', - text: 'Insert link', - shortcut: 'Ctrl+K', - onclick: createLinkList(showDialog), - stateSelector: 'a[href]', - context: 'insert', - prependToContext: true - }); - - }, - - /** - * @ngdoc method - * @name umbraco.services.tinyMceService#getAnchorNames - * @methodOf umbraco.services.tinyMceService - * - * @description - * From the given string, generates a string array where each item is the id attribute value from a named anchor - * 'some string
with a named anchor' returns ['anchor'] - * - * @param {string} input the string to parse - */ - getAnchorNames: function (input) { - if (!input) return []; - - var anchorPattern = //gi; - var matches = input.match(anchorPattern); - var anchors = []; - - if (matches) { - anchors = matches.map(function (v) { - return v.substring(v.indexOf('"') + 1, v.lastIndexOf('\\')); - }); - } - - return anchors.filter(function(val, i, self) { - return self.indexOf(val) === i; - }); - }, - - insertLinkInEditor: function (editor, target, anchorElm) { - - var href = target.url; - // We want to use the Udi. If it is set, we use it, else fallback to id, and finally to null - var hasUdi = target.udi ? true : false; - var id = hasUdi ? target.udi : (target.id ? target.id : null); - - // if an anchor exists, check that it is appropriately prefixed - if (target.anchor && target.anchor[0] !== '?' && target.anchor[0] !== '#') { - target.anchor = (target.anchor.indexOf('=') === -1 ? '#' : '?') + target.anchor; - } - - // the href might be an external url, so check the value for an anchor/qs - // href has the anchor re-appended later, hence the reset here to avoid duplicating the anchor - if (!target.anchor) { - var urlParts = href.split(/(#|\?)/); - if (urlParts.length === 3) { - href = urlParts[0]; - target.anchor = urlParts[1] + urlParts[2]; - } - } - - //Create a json obj used to create the attributes for the tag - function createElemAttributes() { - var a = { - href: href, - title: target.name, - target: target.target ? target.target : null, - rel: target.rel ? target.rel : null - }; - - if (hasUdi) { - a["data-udi"] = target.udi; - } else if (target.id) { - a["data-id"] = target.id; - } - - if (target.anchor) { - a["data-anchor"] = target.anchor; - a.href = a.href + target.anchor; - } else { - a["data-anchor"] = null; - } - - return a; - } - - function insertLink() { - if (anchorElm) { - editor.dom.setAttribs(anchorElm, createElemAttributes()); - - editor.selection.select(anchorElm); - editor.execCommand('mceEndTyping'); - } else { - editor.execCommand('mceInsertLink', false, createElemAttributes()); - } - } - - if (!href && !target.anchor) { - editor.execCommand('unlink'); - return; - } - - //if we have an id, it must be a locallink:id, aslong as the isMedia flag is not set - if (id && (angular.isUndefined(target.isMedia) || !target.isMedia)) { - - href = "/{localLink:" + id + "}"; - - insertLink(); - return; - } - - if (!href) { - href = ""; - } - - // Is email and not //user@domain.com and protocol (e.g. mailto:, sip:) is not specified - if (href.indexOf('@') > 0 && href.indexOf('//') === -1 && href.indexOf(':') === -1) { - // assume it's a mailto link - href = 'mailto:' + href; - insertLink(); - return; - } - - // Is www. prefixed - if (/^\s*www\./i.test(href)) { - href = 'http://' + href; - insertLink(); - return; - } - - insertLink(); - - } - - }; -} - -angular.module('umbraco.services').factory('tinyMceService', tinyMceService); +/** + * @ngdoc service + * @name umbraco.services.tinyMceService + * + * + * @description + * A service containing all logic for all of the Umbraco TinyMCE plugins + */ +function tinyMceService($log, imageHelper, $http, $timeout, macroResource, macroService, $routeParams, umbRequestHelper, angularHelper, userService) { + return { + + /** + * @ngdoc method + * @name umbraco.services.tinyMceService#configuration + * @methodOf umbraco.services.tinyMceService + * + * @description + * Returns a collection of plugins available to the tinyMCE editor + * + */ + configuration: function () { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "rteApiBaseUrl", + "GetConfiguration"), { + cache: true + }), + 'Failed to retrieve tinymce configuration'); + }, + + /** + * @ngdoc method + * @name umbraco.services.tinyMceService#defaultPrevalues + * @methodOf umbraco.services.tinyMceService + * + * @description + * Returns a default configration to fallback on in case none is provided + * + */ + defaultPrevalues: function () { + var cfg = {}; + cfg.toolbar = ["code", "bold", "italic", "styleselect", "alignleft", "aligncenter", "alignright", "bullist", "numlist", "outdent", "indent", "link", "image", "umbmediapicker", "umbembeddialog", "umbmacro"]; + cfg.stylesheets = []; + cfg.dimensions = { + height: 500 + }; + cfg.maxImageSize = 500; + return cfg; + }, + + /** + * @ngdoc method + * @name umbraco.services.tinyMceService#createInsertEmbeddedMedia + * @methodOf umbraco.services.tinyMceService + * + * @description + * Creates the umbrco insert embedded media tinymce plugin + * + * @param {Object} editor the TinyMCE editor instance + * @param {Object} $scope the current controller scope + */ + createInsertEmbeddedMedia: function (editor, scope, callback) { + editor.addButton('umbembeddialog', { + icon: 'custom icon-tv', + tooltip: 'Embed', + onclick: function () { + if (callback) { + callback(); + } + } + }); + }, + + insertEmbeddedMediaInEditor: function (editor, preview) { + editor.insertContent(preview); + }, + + /** + * @ngdoc method + * @name umbraco.services.tinyMceService#createMediaPicker + * @methodOf umbraco.services.tinyMceService + * + * @description + * Creates the umbrco insert media tinymce plugin + * + * @param {Object} editor the TinyMCE editor instance + * @param {Object} $scope the current controller scope + */ + createMediaPicker: function (editor, scope, callback) { + editor.addButton('umbmediapicker', { + icon: 'custom icon-picture', + tooltip: 'Media Picker', + stateSelector: 'img', + onclick: function () { + + var selectedElm = editor.selection.getNode(), + currentTarget; + + + if (selectedElm.nodeName === 'IMG') { + var img = $(selectedElm); + + var hasUdi = img.attr("data-udi") ? true : false; + + currentTarget = { + altText: img.attr("alt"), + url: img.attr("src") + }; + + if (hasUdi) { + currentTarget["udi"] = img.attr("data-udi"); + } else { + currentTarget["id"] = img.attr("rel"); + } + } + + userService.getCurrentUser().then(function (userData) { + if (callback) { + callback(currentTarget, userData); + } + }); + + } + }); + }, + + insertMediaInEditor: function (editor, img) { + if (img) { + + var hasUdi = img.udi ? true : false; + + var data = { + alt: img.altText || "", + src: (img.url) ? img.url : "nothing.jpg", + id: '__mcenew' + }; + + if (hasUdi) { + data["data-udi"] = img.udi; + } else { + //Considering these fixed because UDI will now be used and thus + // we have no need for rel http://issues.umbraco.org/issue/U4-6228, http://issues.umbraco.org/issue/U4-6595 + data["rel"] = img.id; + data["data-id"] = img.id; + } + + editor.insertContent(editor.dom.createHTML('img', data)); + + $timeout(function () { + var imgElm = editor.dom.get('__mcenew'); + var size = editor.dom.getSize(imgElm); + + if (editor.settings.maxImageSize && editor.settings.maxImageSize !== 0) { + var newSize = imageHelper.scaleToMaxSize(editor.settings.maxImageSize, size.w, size.h); + + var s = "width: " + newSize.width + "px; height:" + newSize.height + "px;"; + editor.dom.setAttrib(imgElm, 'style', s); + + if (img.url) { + var src = img.url + "?width=" + newSize.width + "&height=" + newSize.height; + editor.dom.setAttrib(imgElm, 'data-mce-src', src); + } + } + editor.dom.setAttrib(imgElm, 'id', null); + }, 500); + } + }, + + /** + * @ngdoc method + * @name umbraco.services.tinyMceService#createUmbracoMacro + * @methodOf umbraco.services.tinyMceService + * + * @description + * Creates the insert umbrco macro tinymce plugin + * + * @param {Object} editor the TinyMCE editor instance + * @param {Object} $scope the current controller scope + */ + createInsertMacro: function (editor, $scope, callback) { + + var createInsertMacroScope = this; + + /** Adds custom rules for the macro plugin and custom serialization */ + editor.on('preInit', function (args) { + //this is requires so that we tell the serializer that a 'div' is actually allowed in the root, otherwise the cleanup will strip it out + editor.serializer.addRules('div'); + + /** This checks if the div is a macro container, if so, checks if its wrapped in a p tag and then unwraps it (removes p tag) */ + editor.serializer.addNodeFilter('div', function (nodes, name) { + for (var i = 0; i < nodes.length; i++) { + if (nodes[i].attr("class") === "umb-macro-holder" && nodes[i].parent && nodes[i].parent.name.toUpperCase() === "P") { + nodes[i].parent.unwrap(); + } + } + }); + + }); + + /** + * Because the macro gets wrapped in a P tag because of the way 'enter' works, this + * method will return the macro element if not wrapped in a p, or the p if the macro + * element is the only one inside of it even if we are deep inside an element inside the macro + */ + function getRealMacroElem(element) { + var e = $(element).closest(".umb-macro-holder"); + if (e.length > 0) { + if (e.get(0).parentNode.nodeName === "P") { + //now check if we're the only element + if (element.parentNode.childNodes.length === 1) { + return e.get(0).parentNode; + } + } + return e.get(0); + } + return null; + } + + /** Adds the button instance */ + editor.addButton('umbmacro', { + icon: 'custom icon-settings-alt', + tooltip: 'Insert macro', + onPostRender: function () { + + var ctrl = this; + var isOnMacroElement = false; + + /** + if the selection comes from a different element that is not the macro's + we need to check if the selection includes part of the macro, if so we'll force the selection + to clear to the next element since if people can select part of the macro markup they can then modify it. + */ + function handleSelectionChange() { + + if (!editor.selection.isCollapsed()) { + var endSelection = tinymce.activeEditor.selection.getEnd(); + var startSelection = tinymce.activeEditor.selection.getStart(); + //don't proceed if it's an entire element selected + if (endSelection !== startSelection) { + + //if the end selection is a macro then move the cursor + //NOTE: we don't have to handle when the selection comes from a previous parent because + // that is automatically taken care of with the normal onNodeChanged logic since the + // evt.element will be the macro once it becomes part of the selection. + var $testForMacro = $(endSelection).closest(".umb-macro-holder"); + if ($testForMacro.length > 0) { + + //it came from before so move after, if there is no after then select ourselves + var next = $testForMacro.next(); + if (next.length > 0) { + editor.selection.setCursorLocation($testForMacro.next().get(0)); + } else { + selectMacroElement($testForMacro.get(0)); + } + + } + } + } + } + + /** helper method to select the macro element */ + function selectMacroElement(macroElement) { + + // move selection to top element to ensure we can't edit this + editor.selection.select(macroElement); + + // check if the current selection *is* the element (ie bug) + var currentSelection = editor.selection.getStart(); + if (tinymce.isIE) { + if (!editor.dom.hasClass(currentSelection, 'umb-macro-holder')) { + while (!editor.dom.hasClass(currentSelection, 'umb-macro-holder') && currentSelection.parentNode) { + currentSelection = currentSelection.parentNode; + } + editor.selection.select(currentSelection); + } + } + } + + /** + * Add a node change handler, test if we're editing a macro and select the whole thing, then set our isOnMacroElement flag. + * If we change the selection inside this method, then we end up in an infinite loop, so we have to remove ourselves + * from the event listener before changing selection, however, it seems that putting a break point in this method + * will always cause an 'infinite' loop as the caret keeps changing. + */ + function onNodeChanged(evt) { + + //set our macro button active when on a node of class umb-macro-holder + var $macroElement = $(evt.element).closest(".umb-macro-holder"); + + handleSelectionChange(); + + //set the button active + ctrl.active($macroElement.length !== 0); + + if ($macroElement.length > 0) { + var macroElement = $macroElement.get(0); + + //remove the event listener before re-selecting + editor.off('NodeChange', onNodeChanged); + + selectMacroElement(macroElement); + + //set the flag + isOnMacroElement = true; + + //re-add the event listener + editor.on('NodeChange', onNodeChanged); + } else { + isOnMacroElement = false; + } + + } + + /** when the contents load we need to find any macros declared and load in their content */ + editor.on("LoadContent", function (o) { + + //get all macro divs and load their content + $(editor.dom.select(".umb-macro-holder.mceNonEditable")).each(function () { + createInsertMacroScope.loadMacroContent($(this), null, $scope); + }); + + }); + + /** This prevents any other commands from executing when the current element is the macro so the content cannot be edited */ + editor.on('BeforeExecCommand', function (o) { + if (isOnMacroElement) { + if (o.preventDefault) { + o.preventDefault(); + } + if (o.stopImmediatePropagation) { + o.stopImmediatePropagation(); + } + return; + } + }); + + /** This double checks and ensures you can't paste content into the rendered macro */ + editor.on("Paste", function (o) { + if (isOnMacroElement) { + if (o.preventDefault) { + o.preventDefault(); + } + if (o.stopImmediatePropagation) { + o.stopImmediatePropagation(); + } + return; + } + }); + + //set onNodeChanged event listener + editor.on('NodeChange', onNodeChanged); + + /** + * Listen for the keydown in the editor, we'll check if we are currently on a macro element, if so + * we'll check if the key down is a supported key which requires an action, otherwise we ignore the request + * so the macro cannot be edited. + */ + editor.on('KeyDown', function (e) { + if (isOnMacroElement) { + var macroElement = editor.selection.getNode(); + + //get the 'real' element (either p or the real one) + macroElement = getRealMacroElem(macroElement); + + //prevent editing + e.preventDefault(); + e.stopPropagation(); + + var moveSibling = function (element, isNext) { + var $e = $(element); + var $sibling = isNext ? $e.next() : $e.prev(); + if ($sibling.length > 0) { + editor.selection.select($sibling.get(0)); + editor.selection.collapse(true); + } else { + //if we're moving previous and there is no sibling, then lets recurse and just select the next one + if (!isNext) { + moveSibling(element, true); + return; + } + + //if there is no sibling we'll generate a new p at the end and select it + editor.setContent(editor.getContent() + "

 

"); + editor.selection.select($(editor.dom.getRoot()).children().last().get(0)); + editor.selection.collapse(true); + + } + }; + + //supported keys to move to the next or prev element (13-enter, 27-esc, 38-up, 40-down, 39-right, 37-left) + //supported keys to remove the macro (8-backspace, 46-delete) + //TODO: Should we make the enter key insert a line break before or leave it as moving to the next element? + if ($.inArray(e.keyCode, [13, 40, 39]) !== -1) { + //move to next element + moveSibling(macroElement, true); + } else if ($.inArray(e.keyCode, [27, 38, 37]) !== -1) { + //move to prev element + moveSibling(macroElement, false); + } else if ($.inArray(e.keyCode, [8, 46]) !== -1) { + //delete macro element + + //move first, then delete + moveSibling(macroElement, false); + editor.dom.remove(macroElement); + } + return; + } + }); + + }, + + /** The insert macro button click event handler */ + onclick: function () { + + var dialogData = { + //flag for use in rte so we only show macros flagged for the editor + richTextEditor: true + }; + + //when we click we could have a macro already selected and in that case we'll want to edit the current parameters + //so we'll need to extract them and submit them to the dialog. + var macroElement = editor.selection.getNode(); + macroElement = getRealMacroElem(macroElement); + if (macroElement) { + //we have a macro selected so we'll need to parse it's alias and parameters + var contents = $(macroElement).contents(); + var comment = _.find(contents, function (item) { + return item.nodeType === 8; + }); + if (!comment) { + throw "Cannot parse the current macro, the syntax in the editor is invalid"; + } + var syntax = comment.textContent.trim(); + var parsed = macroService.parseMacroSyntax(syntax); + dialogData = { + macroData: parsed + }; + } + + if (callback) { + callback(dialogData); + } + + } + }); + }, + + insertMacroInEditor: function (editor, macroObject, $scope) { + + //put the macro syntax in comments, we will parse this out on the server side to be used + //for persisting. + var macroSyntaxComment = ""; + //create an id class for this element so we can re-select it after inserting + var uniqueId = "umb-macro-" + editor.dom.uniqueId(); + var macroDiv = editor.dom.create('div', { + 'class': 'umb-macro-holder ' + macroObject.macroAlias + ' mceNonEditable ' + uniqueId + }, + macroSyntaxComment + 'Macro alias: ' + macroObject.macroAlias + ''); + + editor.selection.setNode(macroDiv); + + var $macroDiv = $(editor.dom.select("div.umb-macro-holder." + uniqueId)); + + //async load the macro content + this.loadMacroContent($macroDiv, macroObject, $scope); + + }, + + /** loads in the macro content async from the server */ + loadMacroContent: function ($macroDiv, macroData, $scope) { + + //if we don't have the macroData, then we'll need to parse it from the macro div + if (!macroData) { + var contents = $macroDiv.contents(); + var comment = _.find(contents, function (item) { + return item.nodeType === 8; + }); + if (!comment) { + throw "Cannot parse the current macro, the syntax in the editor is invalid"; + } + var syntax = comment.textContent.trim(); + var parsed = macroService.parseMacroSyntax(syntax); + macroData = parsed; + } + + var $ins = $macroDiv.find("ins"); + + //show the throbber + $macroDiv.addClass("loading"); + + var contentId = $routeParams.id; + + //need to wrap in safe apply since this might be occuring outside of angular + angularHelper.safeApply($scope, function () { + macroResource.getMacroResultAsHtmlForEditor(macroData.macroAlias, contentId, macroData.macroParamsDictionary) + .then(function (htmlResult) { + + $macroDiv.removeClass("loading"); + htmlResult = htmlResult.trim(); + if (htmlResult !== "") { + $ins.html(htmlResult); + } + }); + }); + + }, + + createLinkPicker: function (editor, $scope, onClick) { + + function createLinkList(callback) { + return function () { + var linkList = editor.settings.link_list; + + if (typeof (linkList) === "string") { + tinymce.util.XHR.send({ + url: linkList, + success: function (text) { + callback(tinymce.util.JSON.parse(text)); + } + }); + } else { + callback(linkList); + } + }; + } + + function showDialog(linkList) { + var data = {}, + selection = editor.selection, + dom = editor.dom, + selectedElm, anchorElm, initialText; + var win, linkListCtrl, relListCtrl, targetListCtrl; + + function linkListChangeHandler(e) { + var textCtrl = win.find('#text'); + + if (!textCtrl.value() || (e.lastControl && textCtrl.value() === e.lastControl.text())) { + textCtrl.value(e.control.text()); + } + + win.find('#href').value(e.control.value()); + } + + function buildLinkList() { + var linkListItems = [{ + text: 'None', + value: '' + }]; + + tinymce.each(linkList, function (link) { + linkListItems.push({ + text: link.text || link.title, + value: link.value || link.url, + menu: link.menu + }); + }); + + return linkListItems; + } + + function buildRelList(relValue) { + var relListItems = [{ + text: 'None', + value: '' + }]; + + tinymce.each(editor.settings.rel_list, function (rel) { + relListItems.push({ + text: rel.text || rel.title, + value: rel.value, + selected: relValue === rel.value + }); + }); + + return relListItems; + } + + function buildTargetList(targetValue) { + var targetListItems = [{ + text: 'None', + value: '' + }]; + + if (!editor.settings.target_list) { + targetListItems.push({ + text: 'New window', + value: '_blank' + }); + } + + tinymce.each(editor.settings.target_list, function (target) { + targetListItems.push({ + text: target.text || target.title, + value: target.value, + selected: targetValue === target.value + }); + }); + + return targetListItems; + } + + function buildAnchorListControl(url) { + var anchorList = []; + + tinymce.each(editor.dom.select('a:not([href])'), function (anchor) { + var id = anchor.name || anchor.id; + + if (id) { + anchorList.push({ + text: id, + value: '#' + id, + selected: url.indexOf('#' + id) !== -1 + }); + } + }); + + if (anchorList.length) { + anchorList.unshift({ + text: 'None', + value: '' + }); + + return { + name: 'anchor', + type: 'listbox', + label: 'Anchors', + values: anchorList, + onselect: linkListChangeHandler + }; + } + } + + function updateText() { + if (!initialText && data.text.length === 0) { + this.parent().parent().find('#text')[0].value(this.value()); + } + } + + selectedElm = selection.getNode(); + anchorElm = dom.getParent(selectedElm, 'a[href]'); + + data.text = initialText = anchorElm ? (anchorElm.innerText || anchorElm.textContent) : selection.getContent({ + format: 'text' + }); + data.href = anchorElm ? dom.getAttrib(anchorElm, 'href') : ''; + data.target = anchorElm ? dom.getAttrib(anchorElm, 'target') : ''; + data.rel = anchorElm ? dom.getAttrib(anchorElm, 'rel') : ''; + + if (selectedElm.nodeName === "IMG") { + data.text = initialText = " "; + } + + if (linkList) { + linkListCtrl = { + type: 'listbox', + label: 'Link list', + values: buildLinkList(), + onselect: linkListChangeHandler + }; + } + + if (editor.settings.target_list !== false) { + targetListCtrl = { + name: 'target', + type: 'listbox', + label: 'Target', + values: buildTargetList(data.target) + }; + } + + if (editor.settings.rel_list) { + relListCtrl = { + name: 'rel', + type: 'listbox', + label: 'Rel', + values: buildRelList(data.rel) + }; + } + + var currentTarget = null; + + //if we already have a link selected, we want to pass that data over to the dialog + if (anchorElm) { + var anchor = $(anchorElm); + currentTarget = { + name: anchor.attr("title"), + url: anchor.attr("href"), + target: anchor.attr("target") + }; + + // drop the lead char from the anchor text, if it has a value + var anchorVal = anchor[0].dataset.anchor; + if (anchorVal) { + currentTarget.anchor = anchorVal.substring(1); + } + + //locallink detection, we do this here, to avoid poluting the dialogservice + //so the dialog service can just expect to get a node-like structure + if (currentTarget.url.indexOf("localLink:") > 0) { + // if the current link has an anchor, it needs to be considered when getting the udi/id + // if an anchor exists, reduce the substring max by its length plus two to offset the removed prefix and trailing curly brace + var linkId = currentTarget.url.substring(currentTarget.url.indexOf(":") + 1, currentTarget.url.lastIndexOf("}")); + + //we need to check if this is an INT or a UDI + var parsedIntId = parseInt(linkId, 10); + if (isNaN(parsedIntId)) { + //it's a UDI + currentTarget.udi = linkId; + } else { + currentTarget.id = linkId; + } + } + } + + if (onClick) { + onClick(currentTarget, anchorElm); + } + + } + + editor.addButton('link', { + icon: 'link', + tooltip: 'Insert/edit link', + shortcut: 'Ctrl+K', + onclick: createLinkList(showDialog), + stateSelector: 'a[href]' + }); + + editor.addButton('unlink', { + icon: 'unlink', + tooltip: 'Remove link', + cmd: 'unlink', + stateSelector: 'a[href]' + }); + + editor.addShortcut('Ctrl+K', '', createLinkList(showDialog)); + this.showDialog = showDialog; + + editor.addMenuItem('link', { + icon: 'link', + text: 'Insert link', + shortcut: 'Ctrl+K', + onclick: createLinkList(showDialog), + stateSelector: 'a[href]', + context: 'insert', + prependToContext: true + }); + + }, + + insertLinkInEditor: function (editor, target, anchorElm) { + + var href = target.url; + // We want to use the Udi. If it is set, we use it, else fallback to id, and finally to null + var hasUdi = target.udi ? true : false; + var id = hasUdi ? target.udi : (target.id ? target.id : null); + + // if an anchor exists, check that it is appropriately prefixed + if (target.anchor && target.anchor[0] !== '?' && target.anchor[0] !== '#') { + target.anchor = (target.anchor.indexOf('=') === -1 ? '#' : '?') + target.anchor; + } + + // the href might be an external url, so check the value for an anchor/qs + // href has the anchor re-appended later, hence the reset here to avoid duplicating the anchor + if (!target.anchor) { + var urlParts = href.split(/(#|\?)/); + if (urlParts.length === 3) { + href = urlParts[0]; + target.anchor = urlParts[1] + urlParts[2]; + } + } + + //Create a json obj used to create the attributes for the tag + function createElemAttributes() { + var a = { + href: href, + title: target.name, + target: target.target ? target.target : null, + rel: target.rel ? target.rel : null + }; + + if (hasUdi) { + a["data-udi"] = target.udi; + } else if (target.id) { + a["data-id"] = target.id; + } + + if (target.anchor) { + a["data-anchor"] = target.anchor; + a.href = a.href + target.anchor; + } else { + a["data-anchor"] = null; + } + + return a; + } + + function insertLink() { + if (anchorElm) { + editor.dom.setAttribs(anchorElm, createElemAttributes()); + + editor.selection.select(anchorElm); + editor.execCommand('mceEndTyping'); + } else { + editor.execCommand('mceInsertLink', false, createElemAttributes()); + } + } + + if (!href && !target.anchor) { + editor.execCommand('unlink'); + return; + } + + //if we have an id, it must be a locallink:id, aslong as the isMedia flag is not set + if (id && (angular.isUndefined(target.isMedia) || !target.isMedia)) { + + href = "/{localLink:" + id + "}"; + + insertLink(); + return; + } + + if (!href) { + href = ""; + } + + // Is email and not //user@domain.com and protocol (e.g. mailto:, sip:) is not specified + if (href.indexOf('@') > 0 && href.indexOf('//') === -1 && href.indexOf(':') === -1) { + // assume it's a mailto link + href = 'mailto:' + href; + insertLink(); + return; + } + + // Is www. prefixed + if (/^\s*www\./i.test(href)) { + href = 'http://' + href; + insertLink(); + return; + } + + insertLink(); + + } + + }; +} + +angular.module('umbraco.services').factory('tinyMceService', tinyMceService); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/linkpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/linkpicker.controller.js index 67d62de223..b1fa038c44 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/linkpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/linkpicker.controller.js @@ -1,6 +1,6 @@ //used for the media picker dialog angular.module("umbraco").controller("Umbraco.Dialogs.LinkPickerController", - function ($scope, eventsService, dialogService, entityResource, contentResource, mediaHelper, userService, localizationService, tinyMceService) { + function ($scope, eventsService, dialogService, entityResource, mediaHelper, userService, localizationService, tinyMceService) { var dialogOptions = $scope.dialogOptions; var searchText = "Search..."; @@ -40,10 +40,10 @@ angular.module("umbraco").controller("Umbraco.Dialogs.LinkPickerController", } // if a link exists, get the properties to build the anchor name list - contentResource.getById(id).then(function (resp) { - $scope.anchorValues = tinyMceService.getAnchorNames(JSON.stringify(resp.properties)); - $scope.target.url = resp.urls[0]; - }); + entityResource.getUrlAndAnchors(id).then(function(resp){ + $scope.anchorValues = resp.anchorValues; + $scope.target.url = resp.url; + }); } else if ($scope.target.url.length) { // a url but no id/udi indicates an external link - trim the url to remove the anchor/qs // only do the substring if there's a # or a ? @@ -88,10 +88,10 @@ angular.module("umbraco").controller("Umbraco.Dialogs.LinkPickerController", if (args.node.id < 0) { $scope.target.url = "/"; } else { - contentResource.getById(args.node.id).then(function (resp) { - $scope.anchorValues = tinyMceService.getAnchorNames(JSON.stringify(resp.properties)); - $scope.target.url = resp.urls[0]; - }); + entityResource.getUrlAndAnchors(args.node.id).then(function(resp){ + $scope.anchorValues = resp.anchorValues; + $scope.target.url = resp.url; + }); } if (!angular.isUndefined($scope.target.isMedia)) { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js index 8a1c3eb9a9..787fe4186e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js @@ -1,6 +1,6 @@ //used for the media picker dialog angular.module("umbraco").controller("Umbraco.Overlays.LinkPickerController", - function ($scope, eventsService, dialogService, entityResource, contentResource, mediaHelper, userService, localizationService, tinyMceService) { + function ($scope, eventsService, dialogService, entityResource, mediaHelper, userService, localizationService, tinyMceService) { var dialogOptions = $scope.model; var searchText = "Search..."; @@ -45,10 +45,10 @@ angular.module("umbraco").controller("Umbraco.Overlays.LinkPickerController", }); }); - // if a link exists, get the properties to build the anchor name list - contentResource.getById(id, { dataTypeId: dialogOptions.dataTypeId }).then(function (resp) { - $scope.model.target.url = resp.urls[0]; - $scope.anchorValues = tinyMceService.getAnchorNames(JSON.stringify(resp.properties)); + + entityResource.getUrlAndAnchors(id).then(function(resp){ + $scope.anchorValues = resp.anchorValues; + $scope.model.target.url = resp.url; }); } } else if ($scope.model.target.url.length) { @@ -88,9 +88,9 @@ angular.module("umbraco").controller("Umbraco.Overlays.LinkPickerController", if (args.node.id < 0) { $scope.model.target.url = "/"; } else { - contentResource.getById(args.node.id, { dataTypeId: dialogOptions.dataTypeId }).then(function (resp) { - $scope.model.target.url = resp.urls[0]; - $scope.anchorValues = tinyMceService.getAnchorNames(JSON.stringify(resp.properties)); + entityResource.getUrlAndAnchors(args.node.id).then(function(resp){ + $scope.anchorValues = resp.anchorValues; + $scope.model.target.url = resp.url; }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js index e3e5d5e072..30ea1eaef6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js @@ -12,19 +12,22 @@ function openLinkPicker(editor, currentTarget, anchorElement) { - vm.linkPickerOverlay = { - view: "linkpicker", - currentTarget: currentTarget, - anchors: tinyMceService.getAnchorNames(JSON.stringify(editorState.current.properties)), - dataTypeId: $scope.model.dataTypeId, - ignoreUserStartNodes : $scope.model.config.ignoreUserStartNodes, - show: true, - submit: function(model) { - tinyMceService.insertLinkInEditor(editor, model.target, anchorElement); - vm.linkPickerOverlay.show = false; - vm.linkPickerOverlay = null; - } - }; + entityResource.getAnchors($scope.model.value).then(function(anchorValues) { + vm.linkPickerOverlay = { + view: "linkpicker", + currentTarget: currentTarget, + anchors: anchorValues, + dataTypeId: $scope.model.dataTypeId, + ignoreUserStartNodes : $scope.model.config.ignoreUserStartNodes, + show: true, + submit: function(model) { + tinyMceService.insertLinkInEditor(editor, model.target, anchorElement); + vm.linkPickerOverlay.show = false; + vm.linkPickerOverlay = null; + } + }; + }); + } function openMediaPicker(editor, currentTarget, userData) { 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 cadbc5f57a..dd9fb404c1 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 @@ -1,6 +1,6 @@ angular.module("umbraco") .controller("Umbraco.PropertyEditors.RTEController", - function ($rootScope, $scope, $q, $locale, dialogService, $log, imageHelper, assetsService, $timeout, tinyMceService, angularHelper, stylesheetResource, macroService, editorState) { + function ($rootScope, $scope, $q, $locale, dialogService, $log, imageHelper, assetsService, $timeout, tinyMceService, angularHelper, stylesheetResource, macroService, editorState, entityResource) { $scope.isLoading = true; @@ -270,19 +270,25 @@ angular.module("umbraco") }); tinyMceService.createLinkPicker(editor, $scope, function(currentTarget, anchorElement) { - $scope.linkPickerOverlay = { - view: "linkpicker", - currentTarget: currentTarget, - anchors: editorState.current ? tinyMceService.getAnchorNames(JSON.stringify(editorState.current.properties)) : [], - dataTypeId: $scope.model.dataTypeId, - ignoreUserStartNodes: $scope.model.config.ignoreUserStartNodes, - show: true, - submit: function(model) { - tinyMceService.insertLinkInEditor(editor, model.target, anchorElement); - $scope.linkPickerOverlay.show = false; - $scope.linkPickerOverlay = null; - } - }; + + entityResource.getAnchors($scope.model.value).then(function(anchorValues){ + $scope.linkPickerOverlay = { + view: "linkpicker", + currentTarget: currentTarget, + anchors: anchorValues, + dataTypeId: $scope.model.dataTypeId, + ignoreUserStartNodes: $scope.model.config.ignoreUserStartNodes, + show: true, + submit: function(model) { + tinyMceService.insertLinkInEditor(editor, model.target, anchorElement); + $scope.linkPickerOverlay.show = false; + $scope.linkPickerOverlay = null; + } + }; + }); + + + }); //Create the insert media plugin diff --git a/src/Umbraco.Web/Editors/EntityController.cs b/src/Umbraco.Web/Editors/EntityController.cs index 36f4465e96..d6e997b57e 100644 --- a/src/Umbraco.Web/Editors/EntityController.cs +++ b/src/Umbraco.Web/Editors/EntityController.cs @@ -289,6 +289,26 @@ namespace Umbraco.Web.Editors publishedContentExists: i => Umbraco.TypedContent(i) != null); } + + [HttpGet] + public UrlAndAnchors GetUrlAndAnchors(int id) + { + var x = GetResultForId(id, UmbracoEntityTypes.Document); + + var url = Umbraco.Url(id); + var anchorValues = Services.ContentService.GetAnchorValuesFromRTEs(id); + return new UrlAndAnchors(url, anchorValues); + } + + [HttpPost] + public IList GetAnchors(string rteContent) + { + + var anchorValues = Services.ContentService.GetAnchorValuesFromRTEContent(rteContent); + return anchorValues; + } + + #region GetById /// diff --git a/src/Umbraco.Web/Models/ContentEditing/UrlAndAnchors.cs b/src/Umbraco.Web/Models/ContentEditing/UrlAndAnchors.cs new file mode 100644 index 0000000000..a8cf2e26ee --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/UrlAndAnchors.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + [DataContract(Name = "urlAndAnchors", Namespace = "")] + public class UrlAndAnchors + { + public UrlAndAnchors(string url, IList anchorValues) + { + Url = url; + AnchorValues = anchorValues; + } + + [DataMember(Name = "url")] + public string Url { get; } + + [DataMember(Name = "anchorValues")] + public IList AnchorValues { get; } + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 90624df5f2..d4fd27c323 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -1,2027 +1,2028 @@ - - - - - 9.0.30729 - 2.0 - {651E1350-91B6-44B7-BD60-7207006D7003} - Debug - AnyCPU - - - - - umbraco - - - JScript - Grid - IE50 - false - Library - Umbraco.Web - OnBuildSuccess - - - - - - - - - - - - - - - 4.0 - v4.5.2 - - ..\ - true - latest - - - bin\Debug\ - false - 285212672 - false - - - DEBUG;TRACE - - - true - 4096 - false - - - false - false - false - false - 4 - full - prompt - AllRules.ruleset - false - Off - latest - - - bin\Release\ - false - 285212672 - false - - - TRACE - bin\Release\umbraco.xml - true - 4096 - false - - - true - false - false - false - 4 - pdbonly - prompt - AllRules.ruleset - false - Off - - - - {07fbc26b-2927-4a22-8d96-d644c667fecc} - UmbracoExamine - - - ..\packages\AutoMapper.3.3.1\lib\net40\AutoMapper.dll - True - - - ..\packages\AutoMapper.3.3.1\lib\net40\AutoMapper.Net4.dll - True - - - ..\packages\ClientDependency.1.9.7\lib\net45\ClientDependency.Core.dll - - - ..\packages\dotless.1.5.2\lib\dotless.Core.dll - - - ..\packages\Examine.0.1.90\lib\net45\Examine.dll - - - ..\packages\HtmlAgilityPack.1.8.8\lib\Net45\HtmlAgilityPack.dll - - - ..\packages\SharpZipLib.0.86.0\lib\20\ICSharpCode.SharpZipLib.dll - - - ..\packages\Lucene.Net.2.9.4.1\lib\net40\Lucene.Net.dll - - - ..\packages\Markdown.1.14.7\lib\net45\MarkdownSharp.dll - True - - - ..\packages\Microsoft.AspNet.Identity.Core.2.2.2\lib\net45\Microsoft.AspNet.Identity.Core.dll - - - ..\packages\Microsoft.AspNet.Identity.Owin.2.2.2\lib\net45\Microsoft.AspNet.Identity.Owin.dll - - - ..\packages\Microsoft.AspNet.SignalR.Core.2.4.1\lib\net45\Microsoft.AspNet.SignalR.Core.dll - - - - ..\packages\Microsoft.Owin.4.0.1\lib\net45\Microsoft.Owin.dll - - - ..\packages\Microsoft.Owin.Host.SystemWeb.4.0.1\lib\net45\Microsoft.Owin.Host.SystemWeb.dll - - - ..\packages\Microsoft.Owin.Security.4.0.1\lib\net45\Microsoft.Owin.Security.dll - - - ..\packages\Microsoft.Owin.Security.Cookies.4.0.1\lib\net45\Microsoft.Owin.Security.Cookies.dll - - - ..\packages\Microsoft.Owin.Security.OAuth.4.0.1\lib\net45\Microsoft.Owin.Security.OAuth.dll - - - ..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll - True - - - ..\packages\MiniProfiler.2.1.0\lib\net40\MiniProfiler.dll - True - - - ..\packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll - - - ..\packages\Owin.1.0\lib\net40\Owin.dll - True - - - ..\packages\semver.1.1.2\lib\net451\Semver.dll - - - System - - - - - - System.Data - - - - - System.Drawing - - - - - - ..\packages\Microsoft.AspNet.WebApi.Client.5.2.7\lib\net45\System.Net.Http.Formatting.dll - - - - - - ..\packages\System.Threading.Tasks.Dataflow.4.9.0\lib\portable-net45+win8+wpa81\System.Threading.Tasks.Dataflow.dll - - - ..\packages\System.ValueTuple.4.5.0\lib\netstandard1.0\System.ValueTuple.dll - - - - 3.5 - - - - - - - - ..\packages\Microsoft.AspNet.WebPages.3.2.7\lib\net45\System.Web.Helpers.dll - - - ..\packages\Microsoft.AspNet.WebApi.Core.5.2.7\lib\net45\System.Web.Http.dll - - - ..\packages\Microsoft.AspNet.WebApi.WebHost.5.2.7\lib\net45\System.Web.Http.WebHost.dll - - - ..\packages\Microsoft.AspNet.Mvc.5.2.7\lib\net45\System.Web.Mvc.dll - - - ..\packages\Microsoft.AspNet.Razor.3.2.7\lib\net45\System.Web.Razor.dll - - - System.Web.Services - - - ..\packages\Microsoft.AspNet.WebPages.3.2.7\lib\net45\System.Web.WebPages.dll - - - ..\packages\Microsoft.AspNet.WebPages.3.2.7\lib\net45\System.Web.WebPages.Deployment.dll - - - ..\packages\Microsoft.AspNet.WebPages.3.2.7\lib\net45\System.Web.WebPages.Razor.dll - - - - System.XML - - - - {5BA5425F-27A7-4677-865E-82246498AA2E} - SqlCE4Umbraco - - - {31785BC3-256C-4613-B2F5-A1B0BDDED8C1} - Umbraco.Core - - - {6EDD2061-82F2-461B-BB6E-879245A832DE} - umbraco.controls - - - umbraco.businesslogic - {E469A9CE-1BEC-423F-AC44-713CD72457EA} - {FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - - - {CCD75EC3-63DB-4184-B49D-51C1DD337230} - umbraco.cms - - - {C7CB79F0-1C97-4B33-BFA7-00731B579AE2} - umbraco.datalayer - - - umbraco.interfaces - {511F6D8D-7717-440A-9A57-A507E9A8B27F} - {FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - - - {D7636876-0756-43CB-A192-138C6F0D5E42} - umbraco.providers - - - - - Properties\SolutionInfo.cs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ASPXCodeBehind - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ASPXCodeBehind - - - ASPXCodeBehind - - - ASPXCodeBehind - - - - - - True - True - Reference.map - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - True - True - Resources.resx - - - - - - - - - - - - - - ASPXCodeBehind - - - ASPXCodeBehind - - - ASPXCodeBehind - - - ASPXCodeBehind - - - ASPXCodeBehind - - - AssignDomain2.aspx - ASPXCodeBehind - - - AssignDomain2.aspx - - - - ASPXCodeBehind - - - ASPXCodeBehind - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ASPXCodeBehind - - - - - - - - - - - - - - - - - - - - - True - True - Strings.resx - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ASPXCodeBehind - - - ASPXCodeBehind - - - - ASPXCodeBehind - - - - - ASPXCodeBehind - - - - - ASPXCodeBehind - - - ASPXCodeBehind - - - ASPXCodeBehind - - - ASPXCodeBehind - - - ASPXCodeBehind - - - ASPXCodeBehind - - - ASPXCodeBehind - - - ASPXCodeBehind - - - ASPXCodeBehind - - - ASPXCodeBehind - - - ASPXCodeBehind - - - ASPXCodeBehind - - - ASPXCodeBehind - - - ASPXCodeBehind - - - ASPXCodeBehind - - - - ASPXCodeBehind - - - ASPXCodeBehind - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ASPXCodeBehind - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Code - - - - Code - - - Code - - - Code - - - Code - - - Code - - - - Code - - - True - True - Settings.settings - - - Code - - - - - Code - - - Code - - - Code - - - Code - - - - ASPXCodeBehind - - - Code - - - Code - - - Code - - - - delete.aspx - ASPXCodeBehind - - - delete.aspx - - - editContent.aspx - ASPXCodeBehind - - - editContent.aspx - - - preview.aspx - ASPXCodeBehind - - - preview.aspx - - - publish.aspx - ASPXCodeBehind - - - publish.aspx - - - - - - ASPXCodeBehind - - - - - - - ProgressBar.ascx - ASPXCodeBehind - - - ProgressBar.ascx - - - - ASPXCodeBehind - - - Component - - - - - Code - - - - - - - - - - FeedProxy.aspx - ASPXCodeBehind - - - FeedProxy.aspx - - - EditRelationType.aspx - ASPXCodeBehind - - - EditRelationType.aspx - - - NewRelationType.aspx - ASPXCodeBehind - - - NewRelationType.aspx - - - - RelationTypesWebService.asmx - Component - - - - - Preview.aspx - ASPXCodeBehind - - - Preview.aspx - - - MemberSearch.ascx - ASPXCodeBehind - - - MemberSearch.ascx - - - - - - xsltVisualize.aspx - ASPXCodeBehind - - - xsltVisualize.aspx - - - insertMasterpageContent.aspx - ASPXCodeBehind - - - insertMasterpageContent.aspx - - - insertMasterpagePlaceholder.aspx - ASPXCodeBehind - - - insertMasterpagePlaceholder.aspx - - - republish.aspx - ASPXCodeBehind - - - republish.aspx - - - search.aspx - ASPXCodeBehind - - - search.aspx - - - SendPublish.aspx - ASPXCodeBehind - - - SendPublish.aspx - - - Code - - - Code - - - assemblyBrowser.aspx - ASPXCodeBehind - - - assemblyBrowser.aspx - - - editPackage.aspx - ASPXCodeBehind - - - editPackage.aspx - - - getXsltStatus.asmx - Component - - - xsltChooseExtension.aspx - ASPXCodeBehind - - - xsltChooseExtension.aspx - - - xsltInsertValueOf.aspx - ASPXCodeBehind - - - xsltInsertValueOf.aspx - - - exportDocumenttype.aspx - ASPXCodeBehind - - - importDocumenttype.aspx - ASPXCodeBehind - - - rollBack.aspx - ASPXCodeBehind - - - rollBack.aspx - - - sendToTranslation.aspx - ASPXCodeBehind - - - sendToTranslation.aspx - - - viewAuditTrail.aspx - ASPXCodeBehind - - - viewAuditTrail.aspx - - - EditMemberGroup.aspx - ASPXCodeBehind - - - EditMemberGroup.aspx - - - search.aspx - ASPXCodeBehind - - - search.aspx - - - ViewMembers.aspx - ASPXCodeBehind - - - ViewMembers.aspx - - - Code - - - - - - - - - - tinymce3tinymceCompress.aspx - ASPXCodeBehind - - - tinymce3tinymceCompress.aspx - - - QuickSearchHandler.ashx - - - DictionaryItemList.aspx - ASPXCodeBehind - - - DictionaryItemList.aspx - - - editLanguage.aspx - ASPXCodeBehind - - - editLanguage.aspx - - - - - - Code - - - - - - - - - - - - - - - - - MacroContainerService.asmx - Component - - - TagsAutoCompleteHandler.ashx - - - TreeClientService.asmx - Component - - - - - - - - True - True - Resources.resx - - - default.aspx - ASPXCodeBehind - - - default.aspx - - - details.aspx - ASPXCodeBehind - - - details.aspx - - - preview.aspx - ASPXCodeBehind - - - preview.aspx - - - xml.aspx - ASPXCodeBehind - - - xml.aspx - - - ASPXCodeBehind - - - - - - - - - - - - - - - - - - - - - - - - - - - - TreeDataService.ashx - - - - - - - XmlTree.xsd - - - - - CacheRefresher.asmx - Component - - - CheckForUpgrade.asmx - Component - - - CMSNode.asmx - Component - - - codeEditorSave.asmx - Component - - - legacyAjaxCalls.asmx - Component - - - nodeSorter.asmx - Component - - - progressStatus.asmx - Component - - - publication.asmx - Component - - - - ASPXCodeBehind - - - - uQuery.cs - - - uQuery.cs - - - uQuery.cs - - - uQuery.cs - - - uQuery.cs - - - uQuery.cs - - - uQuery.cs - - - uQuery.cs - - - uQuery.cs - - - - - - - - True - True - Reference.map - - - - - - - - - - - - Component - - - - Component - - - - - Mvc\web.config - - - - MSDiscoCodeGenerator - Reference.cs - - - - - - - - - - - - ASPXCodeBehind - - - ASPXCodeBehind - - - - ASPXCodeBehind - - - - - ASPXCodeBehind - - - ASPXCodeBehind - - - - - - ASPXCodeBehind - - - - - - - - ASPXCodeBehind - - - - - ASPXCodeBehind - - - - - - - - - Reference.map - - - Reference.map - - - SettingsSingleFileGenerator - Settings1.Designer.cs - - - - - - ASPXCodeBehind - - - - - - - - - - Form - - - Designer - - - - - Form - - - - - - - Form - - - - - - - - - - XmlTree.xsd - - - - - - MSDiscoCodeGenerator - Reference.cs - - - ResXFileCodeGenerator - Resources.Designer.cs - Designer - - - ResXFileCodeGenerator - Strings.Designer.cs - - - ResXFileCodeGenerator - Resources.Designer.cs - Designer - - - - - - - package.xsd - - - umbraco.xsd - - - umbraco.xsd - - - umbraco.xsd - - - - - - - - Dynamic - Web References\org.umbraco.our\ - https://our.umbraco.com/umbraco/webservices/api/repository.asmx - - - - - Settings - umbraco_org_umbraco_our_Repository - - - Dynamic - Web References\org.umbraco.update\ - http://update.umbraco.org/checkforupgrade.asmx - - - - - Settings - umbraco_org_umbraco_update_CheckForUpgrade - - - - - - - 11.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v11.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v12.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v15.0 - - - - - - - - - - - - - - - + + + + + 9.0.30729 + 2.0 + {651E1350-91B6-44B7-BD60-7207006D7003} + Debug + AnyCPU + + + + + umbraco + + + JScript + Grid + IE50 + false + Library + Umbraco.Web + OnBuildSuccess + + + + + + + + + + + + + + + 4.0 + v4.5.2 + + ..\ + true + latest + + + bin\Debug\ + false + 285212672 + false + + + DEBUG;TRACE + + + true + 4096 + false + + + false + false + false + false + 4 + full + prompt + AllRules.ruleset + false + Off + latest + + + bin\Release\ + false + 285212672 + false + + + TRACE + bin\Release\umbraco.xml + true + 4096 + false + + + true + false + false + false + 4 + pdbonly + prompt + AllRules.ruleset + false + Off + + + + {07fbc26b-2927-4a22-8d96-d644c667fecc} + UmbracoExamine + + + ..\packages\AutoMapper.3.3.1\lib\net40\AutoMapper.dll + True + + + ..\packages\AutoMapper.3.3.1\lib\net40\AutoMapper.Net4.dll + True + + + ..\packages\ClientDependency.1.9.7\lib\net45\ClientDependency.Core.dll + + + ..\packages\dotless.1.5.2\lib\dotless.Core.dll + + + ..\packages\Examine.0.1.90\lib\net45\Examine.dll + + + ..\packages\HtmlAgilityPack.1.8.8\lib\Net45\HtmlAgilityPack.dll + + + ..\packages\SharpZipLib.0.86.0\lib\20\ICSharpCode.SharpZipLib.dll + + + ..\packages\Lucene.Net.2.9.4.1\lib\net40\Lucene.Net.dll + + + ..\packages\Markdown.1.14.7\lib\net45\MarkdownSharp.dll + True + + + ..\packages\Microsoft.AspNet.Identity.Core.2.2.2\lib\net45\Microsoft.AspNet.Identity.Core.dll + + + ..\packages\Microsoft.AspNet.Identity.Owin.2.2.2\lib\net45\Microsoft.AspNet.Identity.Owin.dll + + + ..\packages\Microsoft.AspNet.SignalR.Core.2.4.1\lib\net45\Microsoft.AspNet.SignalR.Core.dll + + + + ..\packages\Microsoft.Owin.4.0.1\lib\net45\Microsoft.Owin.dll + + + ..\packages\Microsoft.Owin.Host.SystemWeb.4.0.1\lib\net45\Microsoft.Owin.Host.SystemWeb.dll + + + ..\packages\Microsoft.Owin.Security.4.0.1\lib\net45\Microsoft.Owin.Security.dll + + + ..\packages\Microsoft.Owin.Security.Cookies.4.0.1\lib\net45\Microsoft.Owin.Security.Cookies.dll + + + ..\packages\Microsoft.Owin.Security.OAuth.4.0.1\lib\net45\Microsoft.Owin.Security.OAuth.dll + + + ..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll + True + + + ..\packages\MiniProfiler.2.1.0\lib\net40\MiniProfiler.dll + True + + + ..\packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll + + + ..\packages\Owin.1.0\lib\net40\Owin.dll + True + + + ..\packages\semver.1.1.2\lib\net451\Semver.dll + + + System + + + + + + System.Data + + + + + System.Drawing + + + + + + ..\packages\Microsoft.AspNet.WebApi.Client.5.2.7\lib\net45\System.Net.Http.Formatting.dll + + + + + + ..\packages\System.Threading.Tasks.Dataflow.4.9.0\lib\portable-net45+win8+wpa81\System.Threading.Tasks.Dataflow.dll + + + ..\packages\System.ValueTuple.4.5.0\lib\netstandard1.0\System.ValueTuple.dll + + + + 3.5 + + + + + + + + ..\packages\Microsoft.AspNet.WebPages.3.2.7\lib\net45\System.Web.Helpers.dll + + + ..\packages\Microsoft.AspNet.WebApi.Core.5.2.7\lib\net45\System.Web.Http.dll + + + ..\packages\Microsoft.AspNet.WebApi.WebHost.5.2.7\lib\net45\System.Web.Http.WebHost.dll + + + ..\packages\Microsoft.AspNet.Mvc.5.2.7\lib\net45\System.Web.Mvc.dll + + + ..\packages\Microsoft.AspNet.Razor.3.2.7\lib\net45\System.Web.Razor.dll + + + System.Web.Services + + + ..\packages\Microsoft.AspNet.WebPages.3.2.7\lib\net45\System.Web.WebPages.dll + + + ..\packages\Microsoft.AspNet.WebPages.3.2.7\lib\net45\System.Web.WebPages.Deployment.dll + + + ..\packages\Microsoft.AspNet.WebPages.3.2.7\lib\net45\System.Web.WebPages.Razor.dll + + + + System.XML + + + + {5BA5425F-27A7-4677-865E-82246498AA2E} + SqlCE4Umbraco + + + {31785BC3-256C-4613-B2F5-A1B0BDDED8C1} + Umbraco.Core + + + {6EDD2061-82F2-461B-BB6E-879245A832DE} + umbraco.controls + + + umbraco.businesslogic + {E469A9CE-1BEC-423F-AC44-713CD72457EA} + {FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + + + {CCD75EC3-63DB-4184-B49D-51C1DD337230} + umbraco.cms + + + {C7CB79F0-1C97-4B33-BFA7-00731B579AE2} + umbraco.datalayer + + + umbraco.interfaces + {511F6D8D-7717-440A-9A57-A507E9A8B27F} + {FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + + + {D7636876-0756-43CB-A192-138C6F0D5E42} + umbraco.providers + + + + + Properties\SolutionInfo.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ASPXCodeBehind + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ASPXCodeBehind + + + ASPXCodeBehind + + + ASPXCodeBehind + + + + + + True + True + Reference.map + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + True + Resources.resx + + + + + + + + + + + + + + ASPXCodeBehind + + + ASPXCodeBehind + + + ASPXCodeBehind + + + ASPXCodeBehind + + + ASPXCodeBehind + + + AssignDomain2.aspx + ASPXCodeBehind + + + AssignDomain2.aspx + + + + ASPXCodeBehind + + + ASPXCodeBehind + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ASPXCodeBehind + + + + + + + + + + + + + + + + + + + + + True + True + Strings.resx + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ASPXCodeBehind + + + ASPXCodeBehind + + + + ASPXCodeBehind + + + + + ASPXCodeBehind + + + + + ASPXCodeBehind + + + ASPXCodeBehind + + + ASPXCodeBehind + + + ASPXCodeBehind + + + ASPXCodeBehind + + + ASPXCodeBehind + + + ASPXCodeBehind + + + ASPXCodeBehind + + + ASPXCodeBehind + + + ASPXCodeBehind + + + ASPXCodeBehind + + + ASPXCodeBehind + + + ASPXCodeBehind + + + ASPXCodeBehind + + + ASPXCodeBehind + + + + ASPXCodeBehind + + + ASPXCodeBehind + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ASPXCodeBehind + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Code + + + + Code + + + Code + + + Code + + + Code + + + Code + + + + Code + + + True + True + Settings.settings + + + Code + + + + + Code + + + Code + + + Code + + + Code + + + + ASPXCodeBehind + + + Code + + + Code + + + Code + + + + delete.aspx + ASPXCodeBehind + + + delete.aspx + + + editContent.aspx + ASPXCodeBehind + + + editContent.aspx + + + preview.aspx + ASPXCodeBehind + + + preview.aspx + + + publish.aspx + ASPXCodeBehind + + + publish.aspx + + + + + + ASPXCodeBehind + + + + + + + ProgressBar.ascx + ASPXCodeBehind + + + ProgressBar.ascx + + + + ASPXCodeBehind + + + Component + + + + + Code + + + + + + + + + + FeedProxy.aspx + ASPXCodeBehind + + + FeedProxy.aspx + + + EditRelationType.aspx + ASPXCodeBehind + + + EditRelationType.aspx + + + NewRelationType.aspx + ASPXCodeBehind + + + NewRelationType.aspx + + + + RelationTypesWebService.asmx + Component + + + + + Preview.aspx + ASPXCodeBehind + + + Preview.aspx + + + MemberSearch.ascx + ASPXCodeBehind + + + MemberSearch.ascx + + + + + + xsltVisualize.aspx + ASPXCodeBehind + + + xsltVisualize.aspx + + + insertMasterpageContent.aspx + ASPXCodeBehind + + + insertMasterpageContent.aspx + + + insertMasterpagePlaceholder.aspx + ASPXCodeBehind + + + insertMasterpagePlaceholder.aspx + + + republish.aspx + ASPXCodeBehind + + + republish.aspx + + + search.aspx + ASPXCodeBehind + + + search.aspx + + + SendPublish.aspx + ASPXCodeBehind + + + SendPublish.aspx + + + Code + + + Code + + + assemblyBrowser.aspx + ASPXCodeBehind + + + assemblyBrowser.aspx + + + editPackage.aspx + ASPXCodeBehind + + + editPackage.aspx + + + getXsltStatus.asmx + Component + + + xsltChooseExtension.aspx + ASPXCodeBehind + + + xsltChooseExtension.aspx + + + xsltInsertValueOf.aspx + ASPXCodeBehind + + + xsltInsertValueOf.aspx + + + exportDocumenttype.aspx + ASPXCodeBehind + + + importDocumenttype.aspx + ASPXCodeBehind + + + rollBack.aspx + ASPXCodeBehind + + + rollBack.aspx + + + sendToTranslation.aspx + ASPXCodeBehind + + + sendToTranslation.aspx + + + viewAuditTrail.aspx + ASPXCodeBehind + + + viewAuditTrail.aspx + + + EditMemberGroup.aspx + ASPXCodeBehind + + + EditMemberGroup.aspx + + + search.aspx + ASPXCodeBehind + + + search.aspx + + + ViewMembers.aspx + ASPXCodeBehind + + + ViewMembers.aspx + + + Code + + + + + + + + + + tinymce3tinymceCompress.aspx + ASPXCodeBehind + + + tinymce3tinymceCompress.aspx + + + QuickSearchHandler.ashx + + + DictionaryItemList.aspx + ASPXCodeBehind + + + DictionaryItemList.aspx + + + editLanguage.aspx + ASPXCodeBehind + + + editLanguage.aspx + + + + + + Code + + + + + + + + + + + + + + + + + MacroContainerService.asmx + Component + + + TagsAutoCompleteHandler.ashx + + + TreeClientService.asmx + Component + + + + + + + + True + True + Resources.resx + + + default.aspx + ASPXCodeBehind + + + default.aspx + + + details.aspx + ASPXCodeBehind + + + details.aspx + + + preview.aspx + ASPXCodeBehind + + + preview.aspx + + + xml.aspx + ASPXCodeBehind + + + xml.aspx + + + ASPXCodeBehind + + + + + + + + + + + + + + + + + + + + + + + + + + + + TreeDataService.ashx + + + + + + + XmlTree.xsd + + + + + CacheRefresher.asmx + Component + + + CheckForUpgrade.asmx + Component + + + CMSNode.asmx + Component + + + codeEditorSave.asmx + Component + + + legacyAjaxCalls.asmx + Component + + + nodeSorter.asmx + Component + + + progressStatus.asmx + Component + + + publication.asmx + Component + + + + ASPXCodeBehind + + + + uQuery.cs + + + uQuery.cs + + + uQuery.cs + + + uQuery.cs + + + uQuery.cs + + + uQuery.cs + + + uQuery.cs + + + uQuery.cs + + + uQuery.cs + + + + + + + + True + True + Reference.map + + + + + + + + + + + + Component + + + + Component + + + + + Mvc\web.config + + + + MSDiscoCodeGenerator + Reference.cs + + + + + + + + + + + + ASPXCodeBehind + + + ASPXCodeBehind + + + + ASPXCodeBehind + + + + + ASPXCodeBehind + + + ASPXCodeBehind + + + + + + ASPXCodeBehind + + + + + + + + ASPXCodeBehind + + + + + ASPXCodeBehind + + + + + + + + + Reference.map + + + Reference.map + + + SettingsSingleFileGenerator + Settings1.Designer.cs + + + + + + ASPXCodeBehind + + + + + + + + + + Form + + + Designer + + + + + Form + + + + + + + Form + + + + + + + + + + XmlTree.xsd + + + + + + MSDiscoCodeGenerator + Reference.cs + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + ResXFileCodeGenerator + Strings.Designer.cs + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + + + + + package.xsd + + + umbraco.xsd + + + umbraco.xsd + + + umbraco.xsd + + + + + + + + Dynamic + Web References\org.umbraco.our\ + https://our.umbraco.com/umbraco/webservices/api/repository.asmx + + + + + Settings + umbraco_org_umbraco_our_Repository + + + Dynamic + Web References\org.umbraco.update\ + http://update.umbraco.org/checkforupgrade.asmx + + + + + Settings + umbraco_org_umbraco_update_CheckForUpgrade + + + + + + + 11.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v11.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v12.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v15.0 + + + + + + + + + + + + + + + \ No newline at end of file