From edf7df043ff37b756fb1ba1b0d9738533e839608 Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 28 Dec 2017 09:18:09 +0100 Subject: [PATCH] Reorg code, move services --- src/Umbraco.Core/Models/ContentExtensions.cs | 1 + .../Implement/ContentRepositoryBase.cs | 1 + .../Publishing/ScheduledPublisher.cs | 1 + .../Services/{ => Implement}/AuditService.cs | 2 +- .../{ => Implement}/ContentService.cs | 4710 ++++++++--------- .../{ => Implement}/ContentTypeService.cs | 175 +- .../{ => Implement}/ContentTypeServiceBase.cs | 2 +- .../ContentTypeServiceBaseOfTItemTService.cs | 2 +- ...peServiceBaseOfTRepositoryTItemTService.cs | 2 +- .../{ => Implement}/DataTypeService.cs | 1294 ++--- .../Services/{ => Implement}/DomainService.cs | 2 +- .../Services/{ => Implement}/EntityService.cs | 1459 +++-- .../{ => Implement}/ExternalLoginService.cs | 2 +- .../Services/{ => Implement}/FileService.cs | 2368 ++++----- .../{ => Implement}/LocalizationService.cs | 912 ++-- .../{ => Implement}/LocalizedTextService.cs | 3 +- .../LocalizedTextServiceFileSources.cs | 2 +- ...lizedTextServiceSupplementaryFileSource.cs | 2 +- .../Services/{ => Implement}/MacroService.cs | 396 +- .../Services/{ => Implement}/MediaService.cs | 3094 +++++------ .../{ => Implement}/MediaTypeService.cs | 3 +- .../{ => Implement}/MemberGroupService.cs | 2 +- .../Services/{ => Implement}/MemberService.cs | 2594 ++++----- .../{ => Implement}/MemberTypeService.cs | 116 +- .../{ => Implement}/NotificationService.cs | 2 +- .../{ => Implement}/PackagingService.cs | 3862 +++++++------- .../{ => Implement}/PublicAccessService.cs | 2 +- .../{ => Implement}/RedirectUrlService.cs | 2 +- .../{ => Implement}/RelationService.cs | 1422 ++--- .../{ => Implement}/RepositoryService.cs | 2 +- .../{ => Implement}/ScopeRepositoryService.cs | 2 +- .../ServerRegistrationService.cs | 324 +- .../Services/{ => Implement}/TagExtractor.cs | 2 +- .../Services/{ => Implement}/TagService.cs | 510 +- .../Services/{ => Implement}/TaskService.cs | 2 +- .../Services/{ => Implement}/UserService.cs | 2412 ++++----- .../Strategies/RelateOnCopyComponent.cs | 1 + .../Strategies/RelateOnTrashComponent.cs | 1 + src/Umbraco.Core/Umbraco.Core.csproj | 66 +- .../NPocoTests/PetaPocoCachesTest.cs | 1 + .../Scoping/ScopedNuCacheTests.cs | 1 + src/Umbraco.Tests/Scoping/ScopedXmlTests.cs | 1 + .../Services/ContentServiceTests.cs | 1 + .../Services/ContentTypeServiceTests.cs | 1 + .../Services/LocalizedTextServiceTests.cs | 1 + .../Services/MemberServiceTests.cs | 1 + .../Services/PackagingServiceTests.cs | 1 + .../Services/PerformanceTests.cs | 1 + .../Services/ThreadSafetyServiceTest.cs | 1 + .../Services/UserServiceTests.cs | 1 + src/Umbraco.Tests/TestHelpers/TestObjects.cs | 1 + .../Cache/CacheRefresherComponent.cs | 1 + .../Editors/ContentControllerBase.cs | 1 + src/Umbraco.Web/Editors/MemberController.cs | 1 + .../Editors/PackageInstallController.cs | 1 + src/Umbraco.Web/Macros/MacroModel.cs | 3 +- .../Models/Mapping/MemberMapperProfile.cs | 1 + .../PropertyEditorsComponent.cs | 1 + .../NuCache/PublishedSnapshotService.cs | 1 + .../XmlPublishedCache/XmlStore.cs | 1 + .../Routing/RedirectTrackingComponent.cs | 1 + src/Umbraco.Web/Search/ExamineComponent.cs | 1 + .../Strategies/NotificationsComponent.cs | 1 + .../Strategies/PublicAccessComponent.cs | 1 + .../WebApi/Binders/MemberBinder.cs | 1 + .../WebServices/SaveFileController.cs | 1 + src/Umbraco.Web/_Legacy/Packager/Installer.cs | 1 + 67 files changed, 12908 insertions(+), 12879 deletions(-) rename src/Umbraco.Core/Services/{ => Implement}/AuditService.cs (98%) rename src/Umbraco.Core/Services/{ => Implement}/ContentService.cs (97%) rename src/Umbraco.Core/Services/{ => Implement}/ContentTypeService.cs (96%) rename src/Umbraco.Core/Services/{ => Implement}/ContentTypeServiceBase.cs (89%) rename src/Umbraco.Core/Services/{ => Implement}/ContentTypeServiceBaseOfTItemTService.cs (99%) rename src/Umbraco.Core/Services/{ => Implement}/ContentTypeServiceBaseOfTRepositoryTItemTService.cs (99%) rename src/Umbraco.Core/Services/{ => Implement}/DataTypeService.cs (97%) rename src/Umbraco.Core/Services/{ => Implement}/DomainService.cs (99%) rename src/Umbraco.Core/Services/{ => Implement}/EntityService.cs (97%) rename src/Umbraco.Core/Services/{ => Implement}/ExternalLoginService.cs (98%) rename src/Umbraco.Core/Services/{ => Implement}/FileService.cs (97%) rename src/Umbraco.Core/Services/{ => Implement}/LocalizationService.cs (97%) rename src/Umbraco.Core/Services/{ => Implement}/LocalizedTextService.cs (99%) rename src/Umbraco.Core/Services/{ => Implement}/LocalizedTextServiceFileSources.cs (99%) rename src/Umbraco.Core/Services/{ => Implement}/LocalizedTextServiceSupplementaryFileSource.cs (92%) rename src/Umbraco.Core/Services/{ => Implement}/MacroService.cs (96%) rename src/Umbraco.Core/Services/{ => Implement}/MediaService.cs (97%) rename src/Umbraco.Core/Services/{ => Implement}/MediaTypeService.cs (96%) rename src/Umbraco.Core/Services/{ => Implement}/MemberGroupService.cs (99%) rename src/Umbraco.Core/Services/{ => Implement}/MemberService.cs (97%) rename src/Umbraco.Core/Services/{ => Implement}/MemberTypeService.cs (96%) rename src/Umbraco.Core/Services/{ => Implement}/NotificationService.cs (99%) rename src/Umbraco.Core/Services/{ => Implement}/PackagingService.cs (97%) rename src/Umbraco.Core/Services/{ => Implement}/PublicAccessService.cs (99%) rename src/Umbraco.Core/Services/{ => Implement}/RedirectUrlService.cs (98%) rename src/Umbraco.Core/Services/{ => Implement}/RelationService.cs (97%) rename src/Umbraco.Core/Services/{ => Implement}/RepositoryService.cs (95%) rename src/Umbraco.Core/Services/{ => Implement}/ScopeRepositoryService.cs (90%) rename src/Umbraco.Core/Services/{ => Implement}/ServerRegistrationService.cs (97%) rename src/Umbraco.Core/Services/{ => Implement}/TagExtractor.cs (99%) rename src/Umbraco.Core/Services/{ => Implement}/TagService.cs (97%) rename src/Umbraco.Core/Services/{ => Implement}/TaskService.cs (98%) rename src/Umbraco.Core/Services/{ => Implement}/UserService.cs (97%) diff --git a/src/Umbraco.Core/Models/ContentExtensions.cs b/src/Umbraco.Core/Models/ContentExtensions.cs index e34d7026da..31532c5d64 100644 --- a/src/Umbraco.Core/Models/ContentExtensions.cs +++ b/src/Umbraco.Core/Models/ContentExtensions.cs @@ -12,6 +12,7 @@ using Umbraco.Core.IO; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; +using Umbraco.Core.Services.Implement; namespace Umbraco.Core.Models { diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index b81bc9e2ac..b085951159 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -17,6 +17,7 @@ using Umbraco.Core.Persistence.Querying; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Core.Services; +using Umbraco.Core.Services.Implement; namespace Umbraco.Core.Persistence.Repositories.Implement { diff --git a/src/Umbraco.Core/Publishing/ScheduledPublisher.cs b/src/Umbraco.Core/Publishing/ScheduledPublisher.cs index 86530f54d7..bb09195691 100644 --- a/src/Umbraco.Core/Publishing/ScheduledPublisher.cs +++ b/src/Umbraco.Core/Publishing/ScheduledPublisher.cs @@ -3,6 +3,7 @@ using System.Linq; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Services; +using Umbraco.Core.Services.Implement; namespace Umbraco.Core.Publishing { diff --git a/src/Umbraco.Core/Services/AuditService.cs b/src/Umbraco.Core/Services/Implement/AuditService.cs similarity index 98% rename from src/Umbraco.Core/Services/AuditService.cs rename to src/Umbraco.Core/Services/Implement/AuditService.cs index 02b165c988..0e04121bd6 100644 --- a/src/Umbraco.Core/Services/AuditService.cs +++ b/src/Umbraco.Core/Services/Implement/AuditService.cs @@ -6,7 +6,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Scoping; -namespace Umbraco.Core.Services +namespace Umbraco.Core.Services.Implement { public sealed class AuditService : ScopeRepositoryService, IAuditService { diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs similarity index 97% rename from src/Umbraco.Core/Services/ContentService.cs rename to src/Umbraco.Core/Services/Implement/ContentService.cs index 448a91238b..5e0efc4223 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -1,2355 +1,2355 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using Umbraco.Core.Events; -using Umbraco.Core.Exceptions; -using Umbraco.Core.IO; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Core.Models.Membership; -using Umbraco.Core.Persistence.DatabaseModelDefinitions; -using Umbraco.Core.Persistence.Querying; -using Umbraco.Core.Persistence.Repositories; -using Umbraco.Core.Scoping; -using Umbraco.Core.Services.Changes; - -namespace Umbraco.Core.Services -{ - /// - /// Implements the content service. - /// - internal class ContentService : RepositoryService, IContentService - { - private readonly IDocumentRepository _documentRepository; - private readonly IEntityRepository _entityRepository; - private readonly IAuditRepository _auditRepository; - private readonly IContentTypeRepository _contentTypeRepository; - private readonly IDocumentBlueprintRepository _documentBlueprintRepository; - - private readonly MediaFileSystem _mediaFileSystem; - private IQuery _queryNotTrashed; - - #region Constructors - - public ContentService(IScopeProvider provider, ILogger logger, - IEventMessagesFactory eventMessagesFactory, MediaFileSystem mediaFileSystem, - IDocumentRepository documentRepository, IEntityRepository entityRepository, IAuditRepository auditRepository, - IContentTypeRepository contentTypeRepository, IDocumentBlueprintRepository documentBlueprintRepository) - : base(provider, logger, eventMessagesFactory) - { - _mediaFileSystem = mediaFileSystem; - _documentRepository = documentRepository; - _entityRepository = entityRepository; - _auditRepository = auditRepository; - _contentTypeRepository = contentTypeRepository; - _documentBlueprintRepository = documentBlueprintRepository; - } - - #endregion - - #region Static queries - - // lazy-constructed because when the ctor runs, the query factory may not be ready - - private IQuery QueryNotTrashed => _queryNotTrashed ?? (_queryNotTrashed = Query().Where(x => x.Trashed == false)); - - #endregion - - #region Count - - public int CountPublished(string contentTypeAlias = null) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.ContentTree); - return _documentRepository.CountPublished(); - } - } - - public int Count(string contentTypeAlias = null) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.ContentTree); - return _documentRepository.Count(contentTypeAlias); - } - } - - public int CountChildren(int parentId, string contentTypeAlias = null) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.ContentTree); - return _documentRepository.CountChildren(parentId, contentTypeAlias); - } - } - - public int CountDescendants(int parentId, string contentTypeAlias = null) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.ContentTree); - return _documentRepository.CountDescendants(parentId, contentTypeAlias); - } - } - - #endregion - - #region Permissions - - /// - /// 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 SetPermissions(EntityPermissionSet permissionSet) - { - using (var scope = ScopeProvider.CreateScope()) - { - scope.WriteLock(Constants.Locks.ContentTree); - _documentRepository.ReplaceContentPermissions(permissionSet); - scope.Complete(); - } - } - - /// - /// Assigns a single permission to the current content item for the specified group ids - /// - /// - /// - /// - public void SetPermission(IContent entity, char permission, IEnumerable groupIds) - { - using (var scope = ScopeProvider.CreateScope()) - { - scope.WriteLock(Constants.Locks.ContentTree); - _documentRepository.AssignEntityPermission(entity, permission, groupIds); - scope.Complete(); - } - } - - /// - /// Returns implicit/inherited permissions assigned to the content item for all user groups - /// - /// - /// - public EntityPermissionCollection GetPermissions(IContent content) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.ContentTree); - return _documentRepository.GetPermissionsForEntity(content.Id); - } - } - - #endregion - - #region Create - - /// - /// 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 Create(string name, Guid parentId, string contentTypeAlias, int userId = 0) - { - var parent = GetById(parentId); - return Create(name, parent, contentTypeAlias, userId); - } - - /// - /// Creates an object of a specified content type. - /// - /// This method simply returns a new, non-persisted, IContent without any identity. It - /// is intended as a shortcut to creating new content objects that does not invoke a save - /// operation against the database. - /// - /// The name of the content object. - /// The identifier of the parent, or -1. - /// The alias of the content type. - /// The optional id of the user creating the content. - /// The content object. - public IContent Create(string name, int parentId, string contentTypeAlias, int userId = 0) - { - var contentType = GetContentType(contentTypeAlias); - if (contentType == null) - throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias)); - var parent = parentId > 0 ? GetById(parentId) : null; - if (parentId > 0 && parent == null) - throw new ArgumentException("No content with that id.", nameof(parentId)); - - var content = new Content(name, parentId, contentType); - using (var scope = ScopeProvider.CreateScope()) - { - CreateContent(scope, content, parent, userId, false); - scope.Complete(); - } - - return content; - } - - /// - /// Creates an object of a specified content type, at root. - /// - /// This method simply returns a new, non-persisted, IContent without any identity. It - /// is intended as a shortcut to creating new content objects that does not invoke a save - /// operation against the database. - /// - /// The name of the content object. - /// The alias of the content type. - /// The optional id of the user creating the content. - /// The content object. - public IContent CreateContent(string name, string contentTypeAlias, int userId = 0) - { - // not locking since not saving anything - - var contentType = GetContentType(contentTypeAlias); - if (contentType == null) - throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias)); - - var content = new Content(name, -1, contentType); - using (var scope = ScopeProvider.CreateScope()) - { - CreateContent(scope, content, null, userId, false); - scope.Complete(); - } - - return content; - } - - /// - /// Creates an object of a specified content type, under a parent. - /// - /// This method simply returns a new, non-persisted, IContent without any identity. It - /// is intended as a shortcut to creating new content objects that does not invoke a save - /// operation against the database. - /// - /// The name of the content object. - /// The parent content object. - /// The alias of the content type. - /// The optional id of the user creating the content. - /// The content object. - public IContent Create(string name, IContent parent, string contentTypeAlias, int userId = 0) - { - if (parent == null) throw new ArgumentNullException(nameof(parent)); - - using (var scope = ScopeProvider.CreateScope()) - { - // not locking since not saving anything - - var contentType = GetContentType(contentTypeAlias); - if (contentType == null) - throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias)); // causes rollback - - var content = new Content(name, parent, contentType); - CreateContent(scope, content, parent, userId, false); - - scope.Complete(); - return content; - } - } - - /// - /// Creates an object of a specified content type. - /// - /// This method returns a new, persisted, IContent with an identity. - /// The name of the content object. - /// The identifier of the parent, or -1. - /// The alias of the content type. - /// The optional id of the user creating the content. - /// The content object. - public IContent CreateAndSave(string name, int parentId, string contentTypeAlias, int userId = 0) - { - using (var scope = ScopeProvider.CreateScope()) - { - // locking the content tree secures content types too - scope.WriteLock(Constants.Locks.ContentTree); - - var contentType = GetContentType(contentTypeAlias); // + locks - if (contentType == null) - throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias)); // causes rollback - - var parent = parentId > 0 ? GetById(parentId) : null; // + locks - if (parentId > 0 && parent == null) - throw new ArgumentException("No content with that id.", nameof(parentId)); // causes rollback - - var content = parentId > 0 ? new Content(name, parent, contentType) : new Content(name, parentId, contentType); - CreateContent(scope, content, parent, userId, true); - - scope.Complete(); - return content; - } - } - - /// - /// Creates an object of a specified content type, under a parent. - /// - /// This method returns a new, persisted, IContent with an identity. - /// The name of the content object. - /// The parent content object. - /// The alias of the content type. - /// The optional id of the user creating the content. - /// The content object. - public IContent CreateAndSave(string name, IContent parent, string contentTypeAlias, int userId = 0) - { - if (parent == null) throw new ArgumentNullException(nameof(parent)); - - using (var scope = ScopeProvider.CreateScope()) - { - // locking the content tree secures content types too - scope.WriteLock(Constants.Locks.ContentTree); - - var contentType = GetContentType(contentTypeAlias); // + locks - if (contentType == null) - throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias)); // causes rollback - - var content = new Content(name, parent, contentType); - CreateContent(scope, content, parent, userId, true); - - scope.Complete(); - return content; - } - } - - private void CreateContent(IScope scope, Content content, IContent parent, int userId, bool withIdentity) - { - // 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 newArgs = parent != null - ? new NewEventArgs(content, content.ContentType.Alias, parent) - : new NewEventArgs(content, content.ContentType.Alias, -1); - - if (scope.Events.DispatchCancelable(Creating, this, newArgs)) - { - content.WasCancelled = true; - return; - } - - content.CreatorId = userId; - content.WriterId = userId; - - if (withIdentity) - { - var saveEventArgs = new SaveEventArgs(content); - if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) - { - content.WasCancelled = true; - return; - } - - _documentRepository.Save(content); - - saveEventArgs.CanCancel = false; - scope.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); - scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, TreeChangeTypes.RefreshNode).ToEventArgs()); - } - - scope.Events.Dispatch(Created, this, new NewEventArgs(content, false, content.ContentType.Alias, parent)); - - if (withIdentity == false) - return; - - Audit(AuditType.New, $"Content '{content.Name}' was created with Id {content.Id}", content.CreatorId, content.Id); - } - - #endregion - - #region Get, Has, Is - - /// - /// Gets an object by Id - /// - /// Id of the Content to retrieve - /// - public IContent GetById(int id) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.ContentTree); - return _documentRepository.Get(id); - } - } - - /// - /// Gets an object by Id - /// - /// Ids of the Content to retrieve - /// - public IEnumerable GetByIds(IEnumerable ids) - { - var idsA = ids.ToArray(); - if (idsA.Length == 0) return Enumerable.Empty(); - - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.ContentTree); - var items = _documentRepository.GetMany(idsA); - - var index = items.ToDictionary(x => x.Id, x => x); - - return idsA.Select(x => index.TryGetValue(x, out var c) ? c : null).WhereNotNull(); - } - } - - /// - /// Gets an object by its 'UniqueId' - /// - /// Guid key of the Content to retrieve - /// - public IContent GetById(Guid key) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.ContentTree); - return _documentRepository.Get(key); - } - } - - /// - /// Gets objects by Ids - /// - /// Ids of the Content to retrieve - /// - public IEnumerable GetByIds(IEnumerable ids) - { - var idsA = ids.ToArray(); - if (idsA.Length == 0) return Enumerable.Empty(); - - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.ContentTree); - var items = _documentRepository.GetMany(idsA); - - var index = items.ToDictionary(x => x.Key, x => x); - - return idsA.Select(x => index.TryGetValue(x, out var c) ? c : null).WhereNotNull(); - } - } - - /// - /// Gets a collection of objects by the Id of the - /// - /// Id of the - /// An Enumerable list of objects - public IEnumerable GetByType(int id) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.ContentTree); - var query = Query().Where(x => x.ContentTypeId == id); - return _documentRepository.Get(query); - } - } - - internal IEnumerable GetPublishedContentOfContentType(int id) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.ContentTree); - var query = Query().Where(x => x.ContentTypeId == id); - return _documentRepository.Get(query); - } - } - - /// - /// Gets a collection of objects by Level - /// - /// The level to retrieve Content from - /// An Enumerable list of objects - /// Contrary to most methods, this method filters out trashed content items. - public IEnumerable GetByLevel(int level) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.ContentTree); - var query = Query().Where(x => x.Level == level && x.Trashed == false); - return _documentRepository.Get(query); - } - } - - /// - /// Gets a specific version of an item. - /// - /// Id of the version to retrieve - /// An item - public IContent GetVersion(int versionId) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.ContentTree); - return _documentRepository.GetVersion(versionId); - } - } - - /// - /// Gets a collection of an objects versions by Id - /// - /// - /// An Enumerable list of objects - public IEnumerable GetVersions(int id) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.ContentTree); - return _documentRepository.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 scope = ScopeProvider.CreateScope(autoComplete: true)) - { - return _documentRepository.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) - { - // intentionnaly not locking - 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 rootId = Constants.System.Root.ToInvariantString(); - var ids = content.Path.Split(',') - .Where(x => x != rootId && x != content.Id.ToString(CultureInfo.InvariantCulture)).Select(int.Parse).ToArray(); - if (ids.Any() == false) - return new List(); - - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.ContentTree); - return _documentRepository.GetMany(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 scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.ContentTree); - var query = Query().Where(x => x.ParentId == id); - return _documentRepository.Get(query).OrderBy(x => x.SortOrder); - } - } - - /// - /// Gets a collection of published objects by Parent Id - /// - /// Id of the Parent to retrieve Children from - /// An Enumerable list of published objects - public IEnumerable GetPublishedChildren(int id) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.ContentTree); - var query = Query().Where(x => x.ParentId == id && x.Published); - return _documentRepository.Get(query).OrderBy(x => x.SortOrder); - } - } - - /// - /// 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 = "") - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.ContentTree); - var filterQuery = filter.IsNullOrWhiteSpace() - ? null - : Query().Where(x => x.Name.Contains(filter)); - - return GetPagedChildren(id, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, true, filterQuery); - } - } - - /// - /// 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 - /// - /// An Enumerable list of objects - public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, - string orderBy, Direction orderDirection, bool orderBySystemField, IQuery filter) - { - if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex)); - if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize)); - - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.ContentTree); - - var query = Query(); - //if the id is System Root, then just get all - NO! does not make sense! - //if (id != Constants.System.Root) - query.Where(x => x.ParentId == id); - return _documentRepository.GetPage(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, 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 - public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = "") - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.ContentTree); - var filterQuery = filter.IsNullOrWhiteSpace() - ? null - : Query().Where(x => x.Name.Contains(filter)); - - return GetPagedDescendants(id, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, true, 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) - { - if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex)); - if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize)); - - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.ContentTree); - - var query = Query(); - //if the id is System Root, then just get all - if (id != Constants.System.Root) - { - var contentPath = _entityRepository.GetAllPaths(Constants.ObjectTypes.Document, id).ToArray(); - if (contentPath.Length == 0) - { - totalChildren = 0; - return Enumerable.Empty(); - } - query.Where(x => x.Path.SqlStartsWith($"{contentPath[0]},", TextColumnType.NVarchar)); - } - return _documentRepository.GetPage(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 GetChildren(int parentId, string name) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.ContentTree); - var query = Query().Where(x => x.ParentId == parentId && x.Name.Contains(name)); - return _documentRepository.Get(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) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.ContentTree); - var content = GetById(id); - if (content == null) - { - scope.Complete(); // else causes rollback - return Enumerable.Empty(); - } - var pathMatch = content.Path + ","; - var query = Query().Where(x => x.Id != content.Id && x.Path.StartsWith(pathMatch)); - return _documentRepository.Get(query); - } - } - - /// - /// Gets a collection of objects by Parent Id - /// - /// item to retrieve Descendants from - /// An Enumerable list of objects - public IEnumerable GetDescendants(IContent content) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.ContentTree); - var pathMatch = content.Path + ","; - var query = Query().Where(x => x.Id != content.Id && x.Path.StartsWith(pathMatch)); - return _documentRepository.Get(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) - { - // intentionnaly not locking - 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 a collection of objects, which reside at the first level / root - /// - /// An Enumerable list of objects - public IEnumerable GetRootContent() - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.ContentTree); - var query = Query().Where(x => x.ParentId == Constants.System.Root); - return _documentRepository.Get(query); - } - } - - /// - /// Gets all published content items - /// - /// - internal IEnumerable GetAllPublished() - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.ContentTree); - return _documentRepository.Get(QueryNotTrashed); - } - } - - /// - /// 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 scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.ContentTree); - var query = Query().Where(x => x.Published && x.ExpireDate <= DateTime.Now); - return _documentRepository.Get(query); - } - } - - /// - /// 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 scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.ContentTree); - var query = Query().Where(x => x.Published == false && x.ReleaseDate <= DateTime.Now); - return _documentRepository.Get(query); - } - } - - /// - /// Gets a collection of an objects, which resides in the Recycle Bin - /// - /// An Enumerable list of objects - public IEnumerable GetContentInRecycleBin() - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.ContentTree); - var bin = $"{Constants.System.Root},{Constants.System.RecycleBinContent},"; - var query = Query().Where(x => x.Path.StartsWith(bin)); - return _documentRepository.Get(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; - } - - /// - /// 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 IsPathPublishable(IContent content) - { - // fast - if (content.ParentId == Constants.System.Root) return true; // root content is always publishable - if (content.Trashed) return false; // trashed content is never publishable - - // not trashed and has a parent: publishable if the parent is path-published - var parent = GetById(content.ParentId); - return parent == null || IsPathPublished(parent); - } - - public bool IsPathPublished(IContent content) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.ContentTree); - return _documentRepository.IsPathPublished(content); - } - } - - #endregion - - #region Save, Publish, Unpublish - - // fixme - kill all those raiseEvents - - /// - public OperationResult Save(IContent content, int userId = 0, bool raiseEvents = true) - { - var publishedState = ((Content) content).PublishedState; - if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished) - throw new InvalidOperationException("Cannot save a (un)publishing, use the dedicated (un)publish method."); - - var evtMsgs = EventMessagesFactory.Get(); - - using (var scope = ScopeProvider.CreateScope()) - { - var saveEventArgs = new SaveEventArgs(content, evtMsgs); - if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) - { - scope.Complete(); - return OperationResult.Cancel(evtMsgs); - } - - if (string.IsNullOrWhiteSpace(content.Name)) - { - throw new ArgumentException("Cannot save content with empty name."); - } - - var isNew = content.IsNewEntity(); - - scope.WriteLock(Constants.Locks.ContentTree); - - if (content.HasIdentity == false) - content.CreatorId = userId; - content.WriterId = userId; - - _documentRepository.Save(content); - - if (raiseEvents) - { - saveEventArgs.CanCancel = false; - scope.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); - } - var changeType = isNew ? TreeChangeTypes.RefreshBranch : TreeChangeTypes.RefreshNode; - scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); - Audit(AuditType.Save, "Save Content performed by user", userId, content.Id); - scope.Complete(); - } - - return OperationResult.Succeed(evtMsgs); - } - - /// - public OperationResult Save(IEnumerable contents, int userId = 0, bool raiseEvents = true) - { - var evtMsgs = EventMessagesFactory.Get(); - var contentsA = contents.ToArray(); - - using (var scope = ScopeProvider.CreateScope()) - { - var saveEventArgs = new SaveEventArgs(contentsA, evtMsgs); - if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) - { - scope.Complete(); - return OperationResult.Cancel(evtMsgs); - } - - var treeChanges = contentsA.Select(x => new TreeChange(x, - x.IsNewEntity() ? TreeChangeTypes.RefreshBranch : TreeChangeTypes.RefreshNode)); - - scope.WriteLock(Constants.Locks.ContentTree); - foreach (var content in contentsA) - { - if (content.HasIdentity == false) - content.CreatorId = userId; - content.WriterId = userId; - - _documentRepository.Save(content); - } - - if (raiseEvents) - { - saveEventArgs.CanCancel = false; - scope.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); - } - scope.Events.Dispatch(TreeChanged, this, treeChanges.ToEventArgs()); - Audit(AuditType.Save, "Bulk Save content performed by user", userId == -1 ? 0 : userId, Constants.System.Root); - - scope.Complete(); - } - - return OperationResult.Succeed(evtMsgs); - } - - /// - public PublishResult SaveAndPublish(IContent content, int userId = 0, bool raiseEvents = true) - { - var evtMsgs = EventMessagesFactory.Get(); - PublishResult result; - - if (((Content) content).PublishedState != PublishedState.Publishing && content.Published) - { - // already published, and values haven't changed - i.e. not changing anything - // nothing to do - // fixme - unless we *want* to bump dates? - return new PublishResult(PublishResultType.SuccessAlready, evtMsgs, content); - } - - using (var scope = ScopeProvider.CreateScope()) - { - var saveEventArgs = new SaveEventArgs(content, evtMsgs); - if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) - { - scope.Complete(); - return new PublishResult(PublishResultType.FailedCancelledByEvent, evtMsgs, content); - } - - var isNew = content.IsNewEntity(); - var changeType = isNew ? TreeChangeTypes.RefreshBranch : TreeChangeTypes.RefreshNode; - var previouslyPublished = content.HasIdentity && content.Published; - - scope.WriteLock(Constants.Locks.ContentTree); - - // ensure that the document can be published, and publish - // handling events, business rules, etc - result = StrategyCanPublish(scope, content, userId, /*checkPath:*/ true, evtMsgs); - if (result.Success) - result = StrategyPublish(scope, content, /*canPublish:*/ true, userId, evtMsgs); - - // save - always, even if not publishing (this is SaveAndPublish) - if (content.HasIdentity == false) - content.CreatorId = userId; - content.WriterId = userId; - - // if not going to publish, must reset the published state - if (!result.Success) - ((Content) content).Published = content.Published; - - _documentRepository.Save(content); - - if (raiseEvents) // always - { - saveEventArgs.CanCancel = false; - scope.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); - } - - if (result.Success == false) - { - scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); - return result; - } - - if (isNew == false && previouslyPublished == false) - changeType = TreeChangeTypes.RefreshBranch; // whole branch - - // invalidate the node/branch - scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); - - scope.Events.Dispatch(Published, this, new PublishEventArgs(content, false, false), "Published"); - - // if was not published and now is... descendants that were 'published' (but - // had an unpublished ancestor) are 're-published' ie not explicitely published - // but back as 'published' nevertheless - if (isNew == false && previouslyPublished == false && HasChildren(content.Id)) - { - var descendants = GetPublishedDescendantsLocked(content).ToArray(); - scope.Events.Dispatch(Published, this, new PublishEventArgs(descendants, false, false), "Published"); - } - - Audit(AuditType.Publish, "Save and Publish performed by user", userId, content.Id); - - scope.Complete(); - } - - return result; - } - - /// - public PublishResult Unpublish(IContent content, int userId = 0) - { - var evtMsgs = EventMessagesFactory.Get(); - - using (var scope = ScopeProvider.CreateScope()) - { - scope.WriteLock(Constants.Locks.ContentTree); - - var newest = GetById(content.Id); // ensure we have the newest version - if (content.VersionId != newest.VersionId) // but use the original object if it's already the newest version - content = newest; - if (content.Published == false) - { - scope.Complete(); - return new PublishResult(PublishResultType.SuccessAlready, evtMsgs, content); // already unpublished - } - - // strategy - // fixme should we still complete the uow? don't want to rollback here! - var attempt = StrategyCanUnpublish(scope, content, userId, evtMsgs); - if (attempt.Success == false) return attempt; // causes rollback - attempt = StrategyUnpublish(scope, content, true, userId, evtMsgs); - if (attempt.Success == false) return attempt; // causes rollback - - content.WriterId = userId; - _documentRepository.Save(content); - - scope.Events.Dispatch(UnPublished, this, new PublishEventArgs(content, false, false), "UnPublished"); - scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, TreeChangeTypes.RefreshBranch).ToEventArgs()); - Audit(AuditType.UnPublish, "UnPublish performed by user", userId, content.Id); - - scope.Complete(); - } - - return new PublishResult(PublishResultType.Success, evtMsgs, content); - } - - /// - public IEnumerable PerformScheduledPublish() - { - using (var scope = ScopeProvider.CreateScope()) - { - scope.WriteLock(Constants.Locks.ContentTree); - - foreach (var d in GetContentForRelease()) - { - PublishResult result; - try - { - d.ReleaseDate = null; - d.PublishValues(); // fixme variants? - result = SaveAndPublish(d, d.WriterId); - if (result.Success == false) - Logger.Error($"Failed to publish document id={d.Id}, reason={result.Result}."); - } - catch (Exception e) - { - Logger.Error($"Failed to publish document id={d.Id}, an exception was thrown.", e); - throw; - } - yield return result; - } - foreach (var d in GetContentForExpiration()) - { - try - { - d.ExpireDate = null; - var result = Unpublish(d, d.WriterId); - if (result.Success == false) - Logger.Error($"Failed to unpublish document id={d.Id}, reason={result.Result}."); - } - catch (Exception e) - { - Logger.Error($"Failed to unpublish document id={d.Id}, an exception was thrown.", e); - throw; - } - } - - scope.Complete(); - } - } - - /// - public IEnumerable SaveAndPublishBranch(IContent content, bool force, int? languageId = null, string segment = null, int userId = 0) - { - segment = segment?.ToLowerInvariant(); - - bool IsEditing(IContent c, int? l, string s) - => c.Properties.Any(x => x.Values.Where(y => y.LanguageId == l && y.Segment == s).Any(y => y.EditedValue != y.PublishedValue)); - - return SaveAndPublishBranch(content, force, document => IsEditing(document, languageId, segment), document => document.PublishValues(languageId, segment), userId); - } - - /// - public IEnumerable SaveAndPublishBranch(IContent document, bool force, - Func editing, Func publishValues, int userId = 0) - { - var evtMsgs = EventMessagesFactory.Get(); - var results = new List(); - var publishedDocuments = new List(); - - using (var scope = ScopeProvider.CreateScope()) - { - scope.WriteLock(Constants.Locks.ContentTree); - - // fixme events?! - - if (!document.HasIdentity) - throw new InvalidOperationException("Do not branch-publish a new document."); - - var publishedState = ((Content) document).PublishedState; - if (publishedState == PublishedState.Publishing) - throw new InvalidOperationException("Do not publish values when publishing branches."); - - // deal with the branch root - if it fails, abort - var result = SaveAndPublishBranchOne(scope, document, editing, publishValues, true, publishedDocuments, evtMsgs, userId); - results.Add(result); - if (!result.Success) return results; - - // deal with descendants - // if one fails, abort its branch - var exclude = new HashSet(); - foreach (var d in GetDescendants(document)) - { - // if parent is excluded, exclude document and ignore - // if not forcing, and not publishing, exclude document and ignore - if (exclude.Contains(d.ParentId) || !force && !d.Published) - { - exclude.Add(d.Id); - continue; - } - - // no need to check path here, - // 1. because we know the parent is path-published (we just published it) - // 2. because it would not work as nothing's been written out to the db until the uow completes - result = SaveAndPublishBranchOne(scope, d, editing, publishValues, false, publishedDocuments, evtMsgs, userId); - results.Add(result); - if (result.Success) continue; - - // abort branch - exclude.Add(d.Id); - } - - scope.Events.Dispatch(TreeChanged, this, new TreeChange(document, TreeChangeTypes.RefreshBranch).ToEventArgs()); - scope.Events.Dispatch(Published, this, new PublishEventArgs(publishedDocuments, false, false), "Published"); - Audit(AuditType.Publish, "SaveAndPublishBranch performed by user", userId, document.Id); - - scope.Complete(); - } - - return results; - } - - private PublishResult SaveAndPublishBranchOne(IScope scope, IContent document, - Func editing, Func publishValues, - bool checkPath, - List publishedDocuments, - EventMessages evtMsgs, int userId) - { - // if already published, and values haven't changed - i.e. not changing anything - // nothing to do - fixme - unless we *want* to bump dates? - if (document.Published && (editing == null || !editing(document))) - return new PublishResult(PublishResultType.SuccessAlready, evtMsgs, document); - - // publish & check if values are valid - if (publishValues != null && !publishValues(document)) - return new PublishResult(PublishResultType.FailedContentInvalid, evtMsgs, document); - - // check if we can publish - var result = StrategyCanPublish(scope, document, userId, checkPath, evtMsgs); - if (!result.Success) - return result; - - // publish - should be successful - var publishResult = StrategyPublish(scope, document, /*canPublish:*/ true, userId, evtMsgs); - if (!publishResult.Success) - throw new Exception("oops: failed to publish."); - - // save - document.WriterId = userId; - _documentRepository.Save(document); - publishedDocuments.Add(document); - return publishResult; - } - - #endregion - - #region Delete - - /// - public OperationResult Delete(IContent content, int userId) - { - var evtMsgs = EventMessagesFactory.Get(); - - using (var scope = ScopeProvider.CreateScope()) - { - var deleteEventArgs = new DeleteEventArgs(content, evtMsgs); - if (scope.Events.DispatchCancelable(Deleting, this, deleteEventArgs)) - { - scope.Complete(); - return OperationResult.Cancel(evtMsgs); - } - - scope.WriteLock(Constants.Locks.ContentTree); - - // if it's not trashed yet, and published, we should unpublish - // but... UnPublishing event makes no sense (not going to cancel?) and no need to save - // just raise the event - if (content.Trashed == false && content.Published) - scope.Events.Dispatch(UnPublished, this, new PublishEventArgs(content, false, false), "UnPublished"); - - DeleteLocked(scope, content); - - scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, TreeChangeTypes.Remove).ToEventArgs()); - Audit(AuditType.Delete, "Delete Content performed by user", userId, content.Id); - - scope.Complete(); - } - - return OperationResult.Succeed(evtMsgs); - } - - private void DeleteLocked(IScope scope, IContent content) - { - // then recursively delete descendants, bottom-up - // just repository.Delete + an event - var stack = new Stack(); - stack.Push(content); - var level = 1; - while (stack.Count > 0) - { - var c = stack.Peek(); - IContent[] cc; - if (c.Level == level) - while ((cc = c.Children(this).ToArray()).Length > 0) - { - foreach (var ci in cc) - stack.Push(ci); - c = cc[cc.Length - 1]; - } - c = stack.Pop(); - level = c.Level; - - _documentRepository.Delete(c); - var args = new DeleteEventArgs(c, false); // raise event & get flagged files - scope.Events.Dispatch(Deleted, this, args); - - // fixme not going to work, do it differently - _mediaFileSystem.DeleteFiles(args.MediaFilesToDelete, // remove flagged files - (file, e) => Logger.Error("An error occurred while deleting file attached to nodes: " + file, e)); - } - } - - //TODO: - // both DeleteVersions methods below have an issue. Sort of. They do NOT take care of files the way - // Delete does - for a good reason: the file may be referenced by other, non-deleted, versions. BUT, - // if that's not the case, then the file will never be deleted, because when we delete the content, - // the version referencing the file will not be there anymore. SO, we can leak files. - - /// - /// 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 scope = ScopeProvider.CreateScope()) - { - var deleteRevisionsEventArgs = new DeleteRevisionsEventArgs(id, dateToRetain: versionDate); - if (scope.Events.DispatchCancelable(DeletingVersions, this, deleteRevisionsEventArgs)) - { - scope.Complete(); - return; - } - - scope.WriteLock(Constants.Locks.ContentTree); - _documentRepository.DeleteVersions(id, versionDate); - - deleteRevisionsEventArgs.CanCancel = false; - scope.Events.Dispatch(DeletedVersions, this, deleteRevisionsEventArgs); - Audit(AuditType.Delete, "Delete Content by version date performed by user", userId, Constants.System.Root); - - scope.Complete(); - } - } - - /// - /// 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, int versionId, bool deletePriorVersions, int userId = 0) - { - using (var scope = ScopeProvider.CreateScope()) - { - if (scope.Events.DispatchCancelable(DeletingVersions, this, new DeleteRevisionsEventArgs(id, /*specificVersion:*/ versionId))) - { - scope.Complete(); - return; - } - - if (deletePriorVersions) - { - var content = GetVersion(versionId); - // fixme nesting uow? - DeleteVersions(id, content.UpdateDate, userId); - } - - scope.WriteLock(Constants.Locks.ContentTree); - var c = _documentRepository.Get(id); - if (c.VersionId != versionId) // don't delete the current version - _documentRepository.DeleteVersion(versionId); - - scope.Events.Dispatch(DeletedVersions, this, new DeleteRevisionsEventArgs(id, false,/* specificVersion:*/ versionId)); - Audit(AuditType.Delete, "Delete Content by version performed by user", userId, Constants.System.Root); - - scope.Complete(); - } - } - - #endregion - - #region Move, RecycleBin - - /// - public OperationResult MoveToRecycleBin(IContent content, int userId) - { - var evtMsgs = EventMessagesFactory.Get(); - var moves = new List>(); - - using (var scope = ScopeProvider.CreateScope()) - { - scope.WriteLock(Constants.Locks.ContentTree); - - var originalPath = content.Path; - var moveEventInfo = new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent); - var moveEventArgs = new MoveEventArgs(evtMsgs, moveEventInfo); - if (scope.Events.DispatchCancelable(Trashing, this, moveEventArgs)) - { - scope.Complete(); - return OperationResult.Cancel(evtMsgs); // causes rollback - } - - // if it's published we may want to force-unpublish it - that would be backward-compatible... but... - // making a radical decision here: trashing is equivalent to moving under an unpublished node so - // it's NOT unpublishing, only the content is now masked - allowing us to restore it if wanted - //if (content.HasPublishedVersion) - //{ } - - PerformMoveLocked(content, Constants.System.RecycleBinContent, null, userId, moves, true); - scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, TreeChangeTypes.RefreshBranch).ToEventArgs()); - - var moveInfo = moves - .Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId)) - .ToArray(); - - moveEventArgs.CanCancel = false; - moveEventArgs.MoveInfoCollection = moveInfo; - scope.Events.Dispatch(Trashed, this, moveEventArgs); - Audit(AuditType.Move, "Move Content to Recycle Bin performed by user", userId, content.Id); - - scope.Complete(); - } - - return OperationResult.Succeed(evtMsgs); - } - - /// - /// 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) - { - // if moving to the recycle bin then use the proper method - if (parentId == Constants.System.RecycleBinContent) - { - MoveToRecycleBin(content, userId); - return; - } - - var moves = new List>(); - - using (var scope = ScopeProvider.CreateScope()) - { - scope.WriteLock(Constants.Locks.ContentTree); - - var parent = parentId == Constants.System.Root ? null : GetById(parentId); - if (parentId != Constants.System.Root && (parent == null || parent.Trashed)) - throw new InvalidOperationException("Parent does not exist or is trashed."); // causes rollback - - var moveEventInfo = new MoveEventInfo(content, content.Path, parentId); - var moveEventArgs = new MoveEventArgs(moveEventInfo); - if (scope.Events.DispatchCancelable(Moving, this, moveEventArgs)) - { - scope.Complete(); - return; // causes rollback - } - - // if content was trashed, and since we're not moving to the recycle bin, - // indicate that the trashed status should be changed to false, else just - // leave it unchanged - var trashed = content.Trashed ? false : (bool?)null; - - // if the content was trashed under another content, and so has a published version, - // it cannot move back as published but has to be unpublished first - that's for the - // root content, everything underneath will retain its published status - if (content.Trashed && content.Published) - { - // however, it had been masked when being trashed, so there's no need for - // any special event here - just change its state - ((Content) content).PublishedState = PublishedState.Unpublishing; - } - - PerformMoveLocked(content, parentId, parent, userId, moves, trashed); - - scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, TreeChangeTypes.RefreshBranch).ToEventArgs()); - - var moveInfo = moves //changes - .Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId)) - .ToArray(); - - moveEventArgs.MoveInfoCollection = moveInfo; - moveEventArgs.CanCancel = false; - scope.Events.Dispatch(Moved, this, moveEventArgs); - Audit(AuditType.Move, "Move Content performed by user", userId, content.Id); - - scope.Complete(); - } - } - - // MUST be called from within WriteLock - // trash indicates whether we are trashing, un-trashing, or not changing anything - private void PerformMoveLocked(IContent content, int parentId, IContent parent, int userId, - ICollection> moves, - bool? trash) - { - content.WriterId = userId; - content.ParentId = parentId; - - // get the level delta (old pos to new pos) - var levelDelta = parent == null - ? 1 - content.Level + (parentId == Constants.System.RecycleBinContent ? 1 : 0) - : parent.Level + 1 - content.Level; - - var paths = new Dictionary(); - - moves.Add(Tuple.Create(content, content.Path)); // capture original path - - // get before moving, in case uow is immediate - var descendants = GetDescendants(content); - - // these will be updated by the repo because we changed parentId - //content.Path = (parent == null ? "-1" : parent.Path) + "," + content.Id; - //content.SortOrder = ((ContentRepository) repository).NextChildSortOrder(parentId); - //content.Level += levelDelta; - PerformMoveContentLocked(content, userId, trash); - - // if uow is not immediate, content.Path will be updated only when the UOW commits, - // and because we want it now, we have to calculate it by ourselves - //paths[content.Id] = content.Path; - paths[content.Id] = (parent == null ? (parentId == Constants.System.RecycleBinContent ? "-1,-20" : "-1") : parent.Path) + "," + content.Id; - - foreach (var descendant in descendants) - { - moves.Add(Tuple.Create(descendant, descendant.Path)); // capture original path - - // update path and level since we do not update parentId - if (paths.ContainsKey(descendant.ParentId) == false) - Console.WriteLine("oops on " + descendant.ParentId + " for " + content.Path + " " + parent?.Path); - descendant.Path = paths[descendant.Id] = paths[descendant.ParentId] + "," + descendant.Id; - Console.WriteLine("path " + descendant.Id + " = " + paths[descendant.Id]); - descendant.Level += levelDelta; - PerformMoveContentLocked(descendant, userId, trash); - } - } - - private void PerformMoveContentLocked(IContent content, int userId, bool? trash) - { - if (trash.HasValue) ((ContentBase) content).Trashed = trash.Value; - content.WriterId = userId; - _documentRepository.Save(content); - } - - /// - /// Empties the Recycle Bin by deleting all that resides in the bin - /// - public void EmptyRecycleBin() - { - var nodeObjectType = Constants.ObjectTypes.Document; - var deleted = new List(); - var evtMsgs = EventMessagesFactory.Get(); // todo - and then? - - using (var scope = ScopeProvider.CreateScope()) - { - scope.WriteLock(Constants.Locks.ContentTree); - - // v7 EmptyingRecycleBin and EmptiedRecycleBin events are greatly simplified since - // each deleted items will have its own deleting/deleted events. so, files and such - // are managed by Delete, and not here. - - // no idea what those events are for, keep a simplified version - var recycleBinEventArgs = new RecycleBinEventArgs(nodeObjectType); - if (scope.Events.DispatchCancelable(EmptyingRecycleBin, this, recycleBinEventArgs)) - { - scope.Complete(); - return; // causes rollback - } - - // emptying the recycle bin means deleting whetever is in there - do it properly! - var query = Query().Where(x => x.ParentId == Constants.System.RecycleBinContent); - var contents = _documentRepository.Get(query).ToArray(); - foreach (var content in contents) - { - DeleteLocked(scope, content); - deleted.Add(content); - } - - recycleBinEventArgs.CanCancel = false; - recycleBinEventArgs.RecycleBinEmptiedSuccessfully = true; // oh my?! - scope.Events.Dispatch(EmptiedRecycleBin, this, recycleBinEventArgs); - scope.Events.Dispatch(TreeChanged, this, deleted.Select(x => new TreeChange(x, TreeChangeTypes.Remove)).ToEventArgs()); - Audit(AuditType.Delete, "Empty Content Recycle Bin performed by user", 0, Constants.System.RecycleBinContent); - - scope.Complete(); - } - } - - #endregion - - #region Others - - /// - /// 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) - { - var copy = content.DeepCloneWithResetIdentities(); - copy.ParentId = parentId; - - using (var scope = ScopeProvider.CreateScope()) - { - var copyEventArgs = new CopyEventArgs(content, copy, true, parentId, relateToOriginal); - if (scope.Events.DispatchCancelable(Copying, this, copyEventArgs)) - { - scope.Complete(); - return null; - } - - // note - relateToOriginal is not managed here, - // it's just part of the Copied event args so the RelateOnCopyHandler knows what to do - // meaning that the event has to trigger for every copied content including descendants - - var copies = new List>(); - - scope.WriteLock(Constants.Locks.ContentTree); - - // a copy is not published (but not really unpublishing either) - // update the create author and last edit author - if (copy.Published) - ((Content) copy).Published = false; - copy.CreatorId = userId; - copy.WriterId = userId; - - //get the current permissions, if there are any explicit ones they need to be copied - var currentPermissions = GetPermissions(content); - currentPermissions.RemoveWhere(p => p.IsDefaultPermissions); - - // save and flush because we need the ID for the recursive Copying events - _documentRepository.Save(copy); - - //add permissions - if (currentPermissions.Count > 0) - { - var permissionSet = new ContentPermissionSet(copy, currentPermissions); - _documentRepository.AddOrUpdatePermissions(permissionSet); - } - - // keep track of copies - copies.Add(Tuple.Create(content, copy)); - var idmap = new Dictionary { [content.Id] = copy.Id }; - - if (recursive) // process descendants - { - foreach (var descendant in GetDescendants(content)) - { - // if parent has not been copied, skip, else gets its copy id - if (idmap.TryGetValue(descendant.ParentId, out parentId) == false) continue; - - var descendantCopy = descendant.DeepCloneWithResetIdentities(); - descendantCopy.ParentId = parentId; - - if (scope.Events.DispatchCancelable(Copying, this, new CopyEventArgs(descendant, descendantCopy, parentId))) - continue; - - // a copy is not published (but not really unpublishing either) - // update the create author and last edit author - if (descendantCopy.Published) - ((Content) descendantCopy).Published = false; - descendantCopy.CreatorId = userId; - descendantCopy.WriterId = userId; - - // save and flush (see above) - _documentRepository.Save(descendantCopy); - - copies.Add(Tuple.Create(descendant, descendantCopy)); - idmap[descendant.Id] = descendantCopy.Id; - } - } - - // not handling tags here, because - // - tags should be handled by the content repository - // - a copy is unpublished and therefore has no impact on tags in DB - - scope.Events.Dispatch(TreeChanged, this, new TreeChange(copy, TreeChangeTypes.RefreshBranch).ToEventArgs()); - foreach (var x in copies) - scope.Events.Dispatch(Copied, this, new CopyEventArgs(x.Item1, x.Item2, false, x.Item2.ParentId, relateToOriginal)); - Audit(AuditType.Copy, "Copy Content performed by user", content.WriterId, content.Id); - - scope.Complete(); - } - - 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 scope = ScopeProvider.CreateScope()) - { - var sendToPublishEventArgs = new SendToPublishEventArgs(content); - if (scope.Events.DispatchCancelable(SendingToPublish, this, sendToPublishEventArgs)) - { - scope.Complete(); - return false; - } - - //Save before raising event - // fixme - nesting uow? - Save(content, userId); - - sendToPublishEventArgs.CanCancel = false; - scope.Events.Dispatch(SentToPublish, this, sendToPublishEventArgs); - Audit(AuditType.SendToPublish, "Send to Publish performed by user", content.WriterId, content.Id); - } - - return true; - } - - /// - /// 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 itemsA = items.ToArray(); - if (itemsA.Length == 0) return true; - - using (var scope = ScopeProvider.CreateScope()) - { - var saveEventArgs = new SaveEventArgs(itemsA); - if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) - return false; - - var published = new List(); - var saved = new List(); - - scope.WriteLock(Constants.Locks.ContentTree); - var sortOrder = 0; - - foreach (var content in itemsA) - { - // 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 == sortOrder) - { - sortOrder++; - continue; - } - - // else update - content.SortOrder = sortOrder++; - content.WriterId = userId; - - // if it's published, register it, no point running StrategyPublish - // since we're not really publishing it and it cannot be cancelled etc - if (content.Published) - published.Add(content); - - // save - saved.Add(content); - _documentRepository.Save(content); - } - - if (raiseEvents) - { - saveEventArgs.CanCancel = false; - scope.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); - } - - scope.Events.Dispatch(TreeChanged, this, saved.Select(x => new TreeChange(x, TreeChangeTypes.RefreshNode)).ToEventArgs()); - - if (raiseEvents && published.Any()) - scope.Events.Dispatch(Published, this, new PublishEventArgs(published, false, false), "Published"); - - Audit(AuditType.Sort, "Sorting content performed by user", userId, 0); - - scope.Complete(); - } - - return true; - } - - #endregion - - #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 scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.ContentTree); - return GetPublishedDescendantsLocked(content).ToArray(); // ToArray important in uow! - } - } - - internal IEnumerable GetPublishedDescendantsLocked(IContent content) - { - var pathMatch = content.Path + ","; - var query = Query().Where(x => x.Id != content.Id && x.Path.StartsWith(pathMatch) /*&& x.Trashed == false*/); - var contents = _documentRepository.Get(query); - - // beware! contents contains all published version below content - // including those that are not directly published because below an unpublished content - // these must be filtered out here - - var parents = new List { content.Id }; - foreach (var c in contents) - { - if (parents.Contains(c.ParentId)) - { - yield return c; - parents.Add(c.Id); - } - } - } - - #endregion - - #region Private Methods - - private void Audit(AuditType type, string message, int userId, int objectId) - { - _auditRepository.Save(new AuditItem(objectId, message, type, userId)); - } - - #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 before publish - /// - public static event TypedEventHandler> Publishing; - - /// - /// Occurs after publish - /// - public static event TypedEventHandler> Published; - - /// - /// Occurs before unpublish - /// - public static event TypedEventHandler> UnPublishing; - - /// - /// Occurs after unpublish - /// - public static event TypedEventHandler> UnPublished; - - /// - /// Occurs after change. - /// - internal static event TypedEventHandler.EventArgs> TreeChanged; - - /// - /// 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 - - #region Publishing Strategies - - // ensures that a document can be published - internal PublishResult StrategyCanPublish(IScope scope, IContent content, int userId, bool checkPath, EventMessages evtMsgs) - { - // raise Publishing event - if (scope.Events.DispatchCancelable(Publishing, this, new PublishEventArgs(content, evtMsgs))) - { - Logger.Info($"Document \"'{content.Name}\" (id={content.Id}) cannot be published: publishing was cancelled."); - return new PublishResult(PublishResultType.FailedCancelledByEvent, evtMsgs, content); - } - - // ensure that the document has published values - // either because it is 'publishing' or because it already has a published version - if (((Content) content).PublishedState != PublishedState.Publishing && content.PublishedVersionId == 0) - { - Logger.Info($"Document \"{content.Name}\" (id={content.Id}) cannot be published: document does not have published values."); - return new PublishResult(PublishResultType.FailedNoPublishedValues, evtMsgs, content); - } - - // ensure that the document status is correct - switch (content.Status) - { - case ContentStatus.Expired: - Logger.Info($"Document \"{content.Name}\" (id={content.Id}) cannot be published: document has expired."); - return new PublishResult(PublishResultType.FailedHasExpired, evtMsgs, content); - - case ContentStatus.AwaitingRelease: - Logger.Info($"Document \"{content.Name}\" (id={content.Id}) cannot be published: document is awaiting release."); - return new PublishResult(PublishResultType.FailedAwaitingRelease, evtMsgs, content); - - case ContentStatus.Trashed: - Logger.Info($"Document \"{content.Name}\" (id={content.Id}) cannot be published: document is trashed."); - return new PublishResult(PublishResultType.FailedIsTrashed, evtMsgs, content); - } - - if (!checkPath) return new PublishResult(evtMsgs, content); - - // check if the content can be path-published - // root content can be published - // else check ancestors - we know we are not trashed - var pathIsOk = content.ParentId == Constants.System.Root || IsPathPublished(GetParent(content)); - if (pathIsOk == false) - { - Logger.Info($"Document \"{content.Name}\" (id={content.Id}) cannot be published: parent is not published."); - return new PublishResult(PublishResultType.FailedPathNotPublished, evtMsgs, content); - } - - return new PublishResult(evtMsgs, content); - } - - // publishes a document - internal PublishResult StrategyPublish(IScope scope, IContent content, bool canPublish, int userId, EventMessages evtMsgs) - { - // note: when used at top-level, StrategyCanPublish with checkPath=true should have run already - // and alreadyCheckedCanPublish should be true, so not checking again. when used at nested level, - // there is no need to check the path again. so, checkPath=false in StrategyCanPublish below - - var result = canPublish - ? new PublishResult(evtMsgs, content) // already know we can - : StrategyCanPublish(scope, content, userId, /*checkPath:*/ false, evtMsgs); // else check - - if (result.Success == false) - return result; - - // change state to publishing - ((Content) content).PublishedState = PublishedState.Publishing; - - Logger.Info($"Content \"{content.Name}\" (id={content.Id}) has been published."); - return result; - } - - // ensures that a document can be unpublished - internal PublishResult StrategyCanUnpublish(IScope scope, IContent content, int userId, EventMessages evtMsgs) - { - // raise UnPublishing event - if (scope.Events.DispatchCancelable(UnPublishing, this, new PublishEventArgs(content, evtMsgs))) - { - Logger.Info($"Document \"{content.Name}\" (id={content.Id}) cannot be unpublished: unpublishing was cancelled."); - return new PublishResult(PublishResultType.FailedCancelledByEvent, evtMsgs, content); - } - - return new PublishResult(evtMsgs, content); - } - - // unpublishes a document - internal PublishResult StrategyUnpublish(IScope scope, IContent content, bool canUnpublish, int userId, EventMessages evtMsgs) - { - var attempt = canUnpublish - ? new PublishResult(evtMsgs, content) // already know we can - : StrategyCanUnpublish(scope, content, userId, evtMsgs); // else check - - if (attempt.Success == false) - return attempt; - - // if the document has a release date set to before now, - // it should be removed so it doesn't interrupt an unpublish - // otherwise it would remain released == published - if (content.ReleaseDate.HasValue && content.ReleaseDate.Value <= DateTime.Now) - { - content.ReleaseDate = null; - Logger.Info($"Document \"{content.Name}\" (id={content.Id}) had its release date removed, because it was unpublished."); - } - - // change state to unpublishing - ((Content) content).PublishedState = PublishedState.Unpublishing; - - Logger.Info($"Document \"{content.Name}\" (id={content.Id}) has been unpublished."); - return attempt; - } - - #endregion - - #region Content Types - - /// - /// 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. - /// Deletes content items of the specified type, and only that type. Does *not* handle content types - /// inheritance and compositions, which need to be managed outside of this method. - /// - /// Id of the - /// Optional Id of the user issueing the delete operation - public void DeleteOfTypes(IEnumerable contentTypeIds, int userId = 0) - { - //TODO: This currently this is called from the ContentTypeService but that needs to change, - // if we are deleting a content type, we should just delete the data and do this operation slightly differently. - // This method will recursively go lookup every content item, check if any of it's descendants are - // of a different type, move them to the recycle bin, then permanently delete the content items. - // The main problem with this is that for every content item being deleted, events are raised... - // which we need for many things like keeping caches in sync, but we can surely do this MUCH better. - - var changes = new List>(); - var moves = new List>(); - var contentTypeIdsA = contentTypeIds.ToArray(); - - // using an immediate uow here because we keep making changes with - // PerformMoveLocked and DeleteLocked that must be applied immediately, - // no point queuing operations - // - using (var scope = ScopeProvider.CreateScope()) - { - scope.WriteLock(Constants.Locks.ContentTree); - - var query = Query().WhereIn(x => x.ContentTypeId, contentTypeIdsA); - var contents = _documentRepository.Get(query).ToArray(); - - if (scope.Events.DispatchCancelable(Deleting, this, new DeleteEventArgs(contents))) - { - scope.Complete(); - return; - } - - // order by level, descending, so deepest first - that way, we cannot move - // a content of the deleted type, to the recycle bin (and then delete it...) - foreach (var content in contents.OrderByDescending(x => x.ParentId)) - { - // if it's not trashed yet, and published, we should unpublish - // but... UnPublishing event makes no sense (not going to cancel?) and no need to save - // just raise the event - if (content.Trashed == false && content.Published) - scope.Events.Dispatch(UnPublished, this, new PublishEventArgs(content, false, false), "UnPublished"); - - // if current content has children, move them to trash - var c = content; - var childQuery = Query().Where(x => x.ParentId == c.Id); - var children = _documentRepository.Get(childQuery); - foreach (var child in children) - { - // see MoveToRecycleBin - PerformMoveLocked(child, Constants.System.RecycleBinContent, null, userId, moves, true); - changes.Add(new TreeChange(content, TreeChangeTypes.RefreshBranch)); - } - - // delete content - // triggers the deleted event (and handles the files) - DeleteLocked(scope, content); - changes.Add(new TreeChange(content, TreeChangeTypes.Remove)); - } - - var moveInfos = moves - .Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId)) - .ToArray(); - if (moveInfos.Length > 0) - scope.Events.Dispatch(Trashed, this, new MoveEventArgs(false, moveInfos), "Trashed"); - scope.Events.Dispatch(TreeChanged, this, changes.ToEventArgs()); - - Audit(AuditType.Delete, $"Delete Content of Type {string.Join(",", contentTypeIdsA)} performed by user", userId, Constants.System.Root); - - scope.Complete(); - } - } - - /// - /// Deletes all content items of specified type. All children of deleted content item 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 deleting the media - public void DeleteOfType(int contentTypeId, int userId = 0) - { - DeleteOfTypes(new[] { contentTypeId }, userId); - } - - private IContentType GetContentType(IScope scope, string contentTypeAlias) - { - if (string.IsNullOrWhiteSpace(contentTypeAlias)) throw new ArgumentNullOrEmptyException(nameof(contentTypeAlias)); - - scope.ReadLock(Constants.Locks.ContentTypes); - - var query = Query().Where(x => x.Alias == contentTypeAlias); - var contentType = _contentTypeRepository.Get(query).FirstOrDefault(); - - if (contentType == null) - throw new Exception($"No ContentType matching the passed in Alias: '{contentTypeAlias}' was found"); // causes rollback - - return contentType; - } - - private IContentType GetContentType(string contentTypeAlias) - { - if (string.IsNullOrWhiteSpace(contentTypeAlias)) throw new ArgumentNullOrEmptyException(nameof(contentTypeAlias)); - - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - return GetContentType(scope, contentTypeAlias); - } - } - - #endregion - - #region Blueprints - - public IContent GetBlueprintById(int id) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.ContentTree); - var blueprint = _documentBlueprintRepository.Get(id); - if (blueprint != null) - ((Content) blueprint).Blueprint = true; - return blueprint; - } - } - - public IContent GetBlueprintById(Guid id) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - scope.ReadLock(Constants.Locks.ContentTree); - var blueprint = _documentBlueprintRepository.Get(id); - if (blueprint != null) - ((Content) blueprint).Blueprint = 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).Blueprint = true; - - using (var scope = ScopeProvider.CreateScope()) - { - scope.WriteLock(Constants.Locks.ContentTree); - - if (string.IsNullOrWhiteSpace(content.Name)) - { - throw new ArgumentException("Cannot save content blueprint with empty name."); - } - - if (content.HasIdentity == false) - { - content.CreatorId = userId; - } - content.WriterId = userId; - - _documentBlueprintRepository.Save(content); - - scope.Events.Dispatch(SavedBlueprint, this, new SaveEventArgs(content), "SavedBlueprint"); - - scope.Complete(); - } - } - - public void DeleteBlueprint(IContent content, int userId = 0) - { - using (var scope = ScopeProvider.CreateScope()) - { - scope.WriteLock(Constants.Locks.ContentTree); - _documentBlueprintRepository.Delete(content); - scope.Events.Dispatch(DeletedBlueprint, this, new DeleteEventArgs(content), "DeletedBlueprint"); - scope.Complete(); - } - } - - public IContent CreateContentFromBlueprint(IContent blueprint, string name, int userId = 0) - { - if (blueprint == null) throw new ArgumentNullException(nameof(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.GetValue()); - - return content; - } - - public IEnumerable GetBlueprintsForContentTypes(params int[] contentTypeId) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - var query = Query(); - if (contentTypeId.Length > 0) - { - query.Where(x => contentTypeId.Contains(x.ContentTypeId)); - } - return _documentBlueprintRepository.Get(query).Select(x => - { - ((Content) x).Blueprint = true; - return x; - }); - } - } - - #endregion - } -} +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Umbraco.Core.Events; +using Umbraco.Core.Exceptions; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Persistence.Repositories; +using Umbraco.Core.Scoping; +using Umbraco.Core.Services.Changes; + +namespace Umbraco.Core.Services.Implement +{ + /// + /// Implements the content service. + /// + internal class ContentService : RepositoryService, IContentService + { + private readonly IDocumentRepository _documentRepository; + private readonly IEntityRepository _entityRepository; + private readonly IAuditRepository _auditRepository; + private readonly IContentTypeRepository _contentTypeRepository; + private readonly IDocumentBlueprintRepository _documentBlueprintRepository; + + private readonly MediaFileSystem _mediaFileSystem; + private IQuery _queryNotTrashed; + + #region Constructors + + public ContentService(IScopeProvider provider, ILogger logger, + IEventMessagesFactory eventMessagesFactory, MediaFileSystem mediaFileSystem, + IDocumentRepository documentRepository, IEntityRepository entityRepository, IAuditRepository auditRepository, + IContentTypeRepository contentTypeRepository, IDocumentBlueprintRepository documentBlueprintRepository) + : base(provider, logger, eventMessagesFactory) + { + _mediaFileSystem = mediaFileSystem; + _documentRepository = documentRepository; + _entityRepository = entityRepository; + _auditRepository = auditRepository; + _contentTypeRepository = contentTypeRepository; + _documentBlueprintRepository = documentBlueprintRepository; + } + + #endregion + + #region Static queries + + // lazy-constructed because when the ctor runs, the query factory may not be ready + + private IQuery QueryNotTrashed => _queryNotTrashed ?? (_queryNotTrashed = Query().Where(x => x.Trashed == false)); + + #endregion + + #region Count + + public int CountPublished(string contentTypeAlias = null) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.ContentTree); + return _documentRepository.CountPublished(); + } + } + + public int Count(string contentTypeAlias = null) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.ContentTree); + return _documentRepository.Count(contentTypeAlias); + } + } + + public int CountChildren(int parentId, string contentTypeAlias = null) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.ContentTree); + return _documentRepository.CountChildren(parentId, contentTypeAlias); + } + } + + public int CountDescendants(int parentId, string contentTypeAlias = null) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.ContentTree); + return _documentRepository.CountDescendants(parentId, contentTypeAlias); + } + } + + #endregion + + #region Permissions + + /// + /// 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 SetPermissions(EntityPermissionSet permissionSet) + { + using (var scope = ScopeProvider.CreateScope()) + { + scope.WriteLock(Constants.Locks.ContentTree); + _documentRepository.ReplaceContentPermissions(permissionSet); + scope.Complete(); + } + } + + /// + /// Assigns a single permission to the current content item for the specified group ids + /// + /// + /// + /// + public void SetPermission(IContent entity, char permission, IEnumerable groupIds) + { + using (var scope = ScopeProvider.CreateScope()) + { + scope.WriteLock(Constants.Locks.ContentTree); + _documentRepository.AssignEntityPermission(entity, permission, groupIds); + scope.Complete(); + } + } + + /// + /// Returns implicit/inherited permissions assigned to the content item for all user groups + /// + /// + /// + public EntityPermissionCollection GetPermissions(IContent content) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.ContentTree); + return _documentRepository.GetPermissionsForEntity(content.Id); + } + } + + #endregion + + #region Create + + /// + /// 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 Create(string name, Guid parentId, string contentTypeAlias, int userId = 0) + { + var parent = GetById(parentId); + return Create(name, parent, contentTypeAlias, userId); + } + + /// + /// Creates an object of a specified content type. + /// + /// This method simply returns a new, non-persisted, IContent without any identity. It + /// is intended as a shortcut to creating new content objects that does not invoke a save + /// operation against the database. + /// + /// The name of the content object. + /// The identifier of the parent, or -1. + /// The alias of the content type. + /// The optional id of the user creating the content. + /// The content object. + public IContent Create(string name, int parentId, string contentTypeAlias, int userId = 0) + { + var contentType = GetContentType(contentTypeAlias); + if (contentType == null) + throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias)); + var parent = parentId > 0 ? GetById(parentId) : null; + if (parentId > 0 && parent == null) + throw new ArgumentException("No content with that id.", nameof(parentId)); + + var content = new Content(name, parentId, contentType); + using (var scope = ScopeProvider.CreateScope()) + { + CreateContent(scope, content, parent, userId, false); + scope.Complete(); + } + + return content; + } + + /// + /// Creates an object of a specified content type, at root. + /// + /// This method simply returns a new, non-persisted, IContent without any identity. It + /// is intended as a shortcut to creating new content objects that does not invoke a save + /// operation against the database. + /// + /// The name of the content object. + /// The alias of the content type. + /// The optional id of the user creating the content. + /// The content object. + public IContent CreateContent(string name, string contentTypeAlias, int userId = 0) + { + // not locking since not saving anything + + var contentType = GetContentType(contentTypeAlias); + if (contentType == null) + throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias)); + + var content = new Content(name, -1, contentType); + using (var scope = ScopeProvider.CreateScope()) + { + CreateContent(scope, content, null, userId, false); + scope.Complete(); + } + + return content; + } + + /// + /// Creates an object of a specified content type, under a parent. + /// + /// This method simply returns a new, non-persisted, IContent without any identity. It + /// is intended as a shortcut to creating new content objects that does not invoke a save + /// operation against the database. + /// + /// The name of the content object. + /// The parent content object. + /// The alias of the content type. + /// The optional id of the user creating the content. + /// The content object. + public IContent Create(string name, IContent parent, string contentTypeAlias, int userId = 0) + { + if (parent == null) throw new ArgumentNullException(nameof(parent)); + + using (var scope = ScopeProvider.CreateScope()) + { + // not locking since not saving anything + + var contentType = GetContentType(contentTypeAlias); + if (contentType == null) + throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias)); // causes rollback + + var content = new Content(name, parent, contentType); + CreateContent(scope, content, parent, userId, false); + + scope.Complete(); + return content; + } + } + + /// + /// Creates an object of a specified content type. + /// + /// This method returns a new, persisted, IContent with an identity. + /// The name of the content object. + /// The identifier of the parent, or -1. + /// The alias of the content type. + /// The optional id of the user creating the content. + /// The content object. + public IContent CreateAndSave(string name, int parentId, string contentTypeAlias, int userId = 0) + { + using (var scope = ScopeProvider.CreateScope()) + { + // locking the content tree secures content types too + scope.WriteLock(Constants.Locks.ContentTree); + + var contentType = GetContentType(contentTypeAlias); // + locks + if (contentType == null) + throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias)); // causes rollback + + var parent = parentId > 0 ? GetById(parentId) : null; // + locks + if (parentId > 0 && parent == null) + throw new ArgumentException("No content with that id.", nameof(parentId)); // causes rollback + + var content = parentId > 0 ? new Content(name, parent, contentType) : new Content(name, parentId, contentType); + CreateContent(scope, content, parent, userId, true); + + scope.Complete(); + return content; + } + } + + /// + /// Creates an object of a specified content type, under a parent. + /// + /// This method returns a new, persisted, IContent with an identity. + /// The name of the content object. + /// The parent content object. + /// The alias of the content type. + /// The optional id of the user creating the content. + /// The content object. + public IContent CreateAndSave(string name, IContent parent, string contentTypeAlias, int userId = 0) + { + if (parent == null) throw new ArgumentNullException(nameof(parent)); + + using (var scope = ScopeProvider.CreateScope()) + { + // locking the content tree secures content types too + scope.WriteLock(Constants.Locks.ContentTree); + + var contentType = GetContentType(contentTypeAlias); // + locks + if (contentType == null) + throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias)); // causes rollback + + var content = new Content(name, parent, contentType); + CreateContent(scope, content, parent, userId, true); + + scope.Complete(); + return content; + } + } + + private void CreateContent(IScope scope, Content content, IContent parent, int userId, bool withIdentity) + { + // 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 newArgs = parent != null + ? new NewEventArgs(content, content.ContentType.Alias, parent) + : new NewEventArgs(content, content.ContentType.Alias, -1); + + if (scope.Events.DispatchCancelable(Creating, this, newArgs)) + { + content.WasCancelled = true; + return; + } + + content.CreatorId = userId; + content.WriterId = userId; + + if (withIdentity) + { + var saveEventArgs = new SaveEventArgs(content); + if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) + { + content.WasCancelled = true; + return; + } + + _documentRepository.Save(content); + + saveEventArgs.CanCancel = false; + scope.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); + scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, TreeChangeTypes.RefreshNode).ToEventArgs()); + } + + scope.Events.Dispatch(Created, this, new NewEventArgs(content, false, content.ContentType.Alias, parent)); + + if (withIdentity == false) + return; + + Audit(AuditType.New, $"Content '{content.Name}' was created with Id {content.Id}", content.CreatorId, content.Id); + } + + #endregion + + #region Get, Has, Is + + /// + /// Gets an object by Id + /// + /// Id of the Content to retrieve + /// + public IContent GetById(int id) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.ContentTree); + return _documentRepository.Get(id); + } + } + + /// + /// Gets an object by Id + /// + /// Ids of the Content to retrieve + /// + public IEnumerable GetByIds(IEnumerable ids) + { + var idsA = ids.ToArray(); + if (idsA.Length == 0) return Enumerable.Empty(); + + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.ContentTree); + var items = _documentRepository.GetMany(idsA); + + var index = items.ToDictionary(x => x.Id, x => x); + + return idsA.Select(x => index.TryGetValue(x, out var c) ? c : null).WhereNotNull(); + } + } + + /// + /// Gets an object by its 'UniqueId' + /// + /// Guid key of the Content to retrieve + /// + public IContent GetById(Guid key) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.ContentTree); + return _documentRepository.Get(key); + } + } + + /// + /// Gets objects by Ids + /// + /// Ids of the Content to retrieve + /// + public IEnumerable GetByIds(IEnumerable ids) + { + var idsA = ids.ToArray(); + if (idsA.Length == 0) return Enumerable.Empty(); + + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.ContentTree); + var items = _documentRepository.GetMany(idsA); + + var index = items.ToDictionary(x => x.Key, x => x); + + return idsA.Select(x => index.TryGetValue(x, out var c) ? c : null).WhereNotNull(); + } + } + + /// + /// Gets a collection of objects by the Id of the + /// + /// Id of the + /// An Enumerable list of objects + public IEnumerable GetByType(int id) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.ContentTree); + var query = Query().Where(x => x.ContentTypeId == id); + return _documentRepository.Get(query); + } + } + + internal IEnumerable GetPublishedContentOfContentType(int id) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.ContentTree); + var query = Query().Where(x => x.ContentTypeId == id); + return _documentRepository.Get(query); + } + } + + /// + /// Gets a collection of objects by Level + /// + /// The level to retrieve Content from + /// An Enumerable list of objects + /// Contrary to most methods, this method filters out trashed content items. + public IEnumerable GetByLevel(int level) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.ContentTree); + var query = Query().Where(x => x.Level == level && x.Trashed == false); + return _documentRepository.Get(query); + } + } + + /// + /// Gets a specific version of an item. + /// + /// Id of the version to retrieve + /// An item + public IContent GetVersion(int versionId) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.ContentTree); + return _documentRepository.GetVersion(versionId); + } + } + + /// + /// Gets a collection of an objects versions by Id + /// + /// + /// An Enumerable list of objects + public IEnumerable GetVersions(int id) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.ContentTree); + return _documentRepository.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 scope = ScopeProvider.CreateScope(autoComplete: true)) + { + return _documentRepository.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) + { + // intentionnaly not locking + 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 rootId = Constants.System.Root.ToInvariantString(); + var ids = content.Path.Split(',') + .Where(x => x != rootId && x != content.Id.ToString(CultureInfo.InvariantCulture)).Select(int.Parse).ToArray(); + if (ids.Any() == false) + return new List(); + + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.ContentTree); + return _documentRepository.GetMany(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 scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.ContentTree); + var query = Query().Where(x => x.ParentId == id); + return _documentRepository.Get(query).OrderBy(x => x.SortOrder); + } + } + + /// + /// Gets a collection of published objects by Parent Id + /// + /// Id of the Parent to retrieve Children from + /// An Enumerable list of published objects + public IEnumerable GetPublishedChildren(int id) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.ContentTree); + var query = Query().Where(x => x.ParentId == id && x.Published); + return _documentRepository.Get(query).OrderBy(x => x.SortOrder); + } + } + + /// + /// 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 = "") + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.ContentTree); + var filterQuery = filter.IsNullOrWhiteSpace() + ? null + : Query().Where(x => x.Name.Contains(filter)); + + return GetPagedChildren(id, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, true, filterQuery); + } + } + + /// + /// 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 + /// + /// An Enumerable list of objects + public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, + string orderBy, Direction orderDirection, bool orderBySystemField, IQuery filter) + { + if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex)); + if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize)); + + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.ContentTree); + + var query = Query(); + //if the id is System Root, then just get all - NO! does not make sense! + //if (id != Constants.System.Root) + query.Where(x => x.ParentId == id); + return _documentRepository.GetPage(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, 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 + public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = "") + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.ContentTree); + var filterQuery = filter.IsNullOrWhiteSpace() + ? null + : Query().Where(x => x.Name.Contains(filter)); + + return GetPagedDescendants(id, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, true, 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) + { + if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex)); + if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize)); + + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.ContentTree); + + var query = Query(); + //if the id is System Root, then just get all + if (id != Constants.System.Root) + { + var contentPath = _entityRepository.GetAllPaths(Constants.ObjectTypes.Document, id).ToArray(); + if (contentPath.Length == 0) + { + totalChildren = 0; + return Enumerable.Empty(); + } + query.Where(x => x.Path.SqlStartsWith($"{contentPath[0]},", TextColumnType.NVarchar)); + } + return _documentRepository.GetPage(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 GetChildren(int parentId, string name) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.ContentTree); + var query = Query().Where(x => x.ParentId == parentId && x.Name.Contains(name)); + return _documentRepository.Get(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) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.ContentTree); + var content = GetById(id); + if (content == null) + { + scope.Complete(); // else causes rollback + return Enumerable.Empty(); + } + var pathMatch = content.Path + ","; + var query = Query().Where(x => x.Id != content.Id && x.Path.StartsWith(pathMatch)); + return _documentRepository.Get(query); + } + } + + /// + /// Gets a collection of objects by Parent Id + /// + /// item to retrieve Descendants from + /// An Enumerable list of objects + public IEnumerable GetDescendants(IContent content) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.ContentTree); + var pathMatch = content.Path + ","; + var query = Query().Where(x => x.Id != content.Id && x.Path.StartsWith(pathMatch)); + return _documentRepository.Get(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) + { + // intentionnaly not locking + 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 a collection of objects, which reside at the first level / root + /// + /// An Enumerable list of objects + public IEnumerable GetRootContent() + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.ContentTree); + var query = Query().Where(x => x.ParentId == Constants.System.Root); + return _documentRepository.Get(query); + } + } + + /// + /// Gets all published content items + /// + /// + internal IEnumerable GetAllPublished() + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.ContentTree); + return _documentRepository.Get(QueryNotTrashed); + } + } + + /// + /// 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 scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.ContentTree); + var query = Query().Where(x => x.Published && x.ExpireDate <= DateTime.Now); + return _documentRepository.Get(query); + } + } + + /// + /// 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 scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.ContentTree); + var query = Query().Where(x => x.Published == false && x.ReleaseDate <= DateTime.Now); + return _documentRepository.Get(query); + } + } + + /// + /// Gets a collection of an objects, which resides in the Recycle Bin + /// + /// An Enumerable list of objects + public IEnumerable GetContentInRecycleBin() + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.ContentTree); + var bin = $"{Constants.System.Root},{Constants.System.RecycleBinContent},"; + var query = Query().Where(x => x.Path.StartsWith(bin)); + return _documentRepository.Get(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; + } + + /// + /// 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 IsPathPublishable(IContent content) + { + // fast + if (content.ParentId == Constants.System.Root) return true; // root content is always publishable + if (content.Trashed) return false; // trashed content is never publishable + + // not trashed and has a parent: publishable if the parent is path-published + var parent = GetById(content.ParentId); + return parent == null || IsPathPublished(parent); + } + + public bool IsPathPublished(IContent content) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.ContentTree); + return _documentRepository.IsPathPublished(content); + } + } + + #endregion + + #region Save, Publish, Unpublish + + // fixme - kill all those raiseEvents + + /// + public OperationResult Save(IContent content, int userId = 0, bool raiseEvents = true) + { + var publishedState = ((Content) content).PublishedState; + if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished) + throw new InvalidOperationException("Cannot save a (un)publishing, use the dedicated (un)publish method."); + + var evtMsgs = EventMessagesFactory.Get(); + + using (var scope = ScopeProvider.CreateScope()) + { + var saveEventArgs = new SaveEventArgs(content, evtMsgs); + if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) + { + scope.Complete(); + return OperationResult.Cancel(evtMsgs); + } + + if (string.IsNullOrWhiteSpace(content.Name)) + { + throw new ArgumentException("Cannot save content with empty name."); + } + + var isNew = content.IsNewEntity(); + + scope.WriteLock(Constants.Locks.ContentTree); + + if (content.HasIdentity == false) + content.CreatorId = userId; + content.WriterId = userId; + + _documentRepository.Save(content); + + if (raiseEvents) + { + saveEventArgs.CanCancel = false; + scope.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); + } + var changeType = isNew ? TreeChangeTypes.RefreshBranch : TreeChangeTypes.RefreshNode; + scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); + Audit(AuditType.Save, "Save Content performed by user", userId, content.Id); + scope.Complete(); + } + + return OperationResult.Succeed(evtMsgs); + } + + /// + public OperationResult Save(IEnumerable contents, int userId = 0, bool raiseEvents = true) + { + var evtMsgs = EventMessagesFactory.Get(); + var contentsA = contents.ToArray(); + + using (var scope = ScopeProvider.CreateScope()) + { + var saveEventArgs = new SaveEventArgs(contentsA, evtMsgs); + if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) + { + scope.Complete(); + return OperationResult.Cancel(evtMsgs); + } + + var treeChanges = contentsA.Select(x => new TreeChange(x, + x.IsNewEntity() ? TreeChangeTypes.RefreshBranch : TreeChangeTypes.RefreshNode)); + + scope.WriteLock(Constants.Locks.ContentTree); + foreach (var content in contentsA) + { + if (content.HasIdentity == false) + content.CreatorId = userId; + content.WriterId = userId; + + _documentRepository.Save(content); + } + + if (raiseEvents) + { + saveEventArgs.CanCancel = false; + scope.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); + } + scope.Events.Dispatch(TreeChanged, this, treeChanges.ToEventArgs()); + Audit(AuditType.Save, "Bulk Save content performed by user", userId == -1 ? 0 : userId, Constants.System.Root); + + scope.Complete(); + } + + return OperationResult.Succeed(evtMsgs); + } + + /// + public PublishResult SaveAndPublish(IContent content, int userId = 0, bool raiseEvents = true) + { + var evtMsgs = EventMessagesFactory.Get(); + PublishResult result; + + if (((Content) content).PublishedState != PublishedState.Publishing && content.Published) + { + // already published, and values haven't changed - i.e. not changing anything + // nothing to do + // fixme - unless we *want* to bump dates? + return new PublishResult(PublishResultType.SuccessAlready, evtMsgs, content); + } + + using (var scope = ScopeProvider.CreateScope()) + { + var saveEventArgs = new SaveEventArgs(content, evtMsgs); + if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) + { + scope.Complete(); + return new PublishResult(PublishResultType.FailedCancelledByEvent, evtMsgs, content); + } + + var isNew = content.IsNewEntity(); + var changeType = isNew ? TreeChangeTypes.RefreshBranch : TreeChangeTypes.RefreshNode; + var previouslyPublished = content.HasIdentity && content.Published; + + scope.WriteLock(Constants.Locks.ContentTree); + + // ensure that the document can be published, and publish + // handling events, business rules, etc + result = StrategyCanPublish(scope, content, userId, /*checkPath:*/ true, evtMsgs); + if (result.Success) + result = StrategyPublish(scope, content, /*canPublish:*/ true, userId, evtMsgs); + + // save - always, even if not publishing (this is SaveAndPublish) + if (content.HasIdentity == false) + content.CreatorId = userId; + content.WriterId = userId; + + // if not going to publish, must reset the published state + if (!result.Success) + ((Content) content).Published = content.Published; + + _documentRepository.Save(content); + + if (raiseEvents) // always + { + saveEventArgs.CanCancel = false; + scope.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); + } + + if (result.Success == false) + { + scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); + return result; + } + + if (isNew == false && previouslyPublished == false) + changeType = TreeChangeTypes.RefreshBranch; // whole branch + + // invalidate the node/branch + scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); + + scope.Events.Dispatch(Published, this, new PublishEventArgs(content, false, false), "Published"); + + // if was not published and now is... descendants that were 'published' (but + // had an unpublished ancestor) are 're-published' ie not explicitely published + // but back as 'published' nevertheless + if (isNew == false && previouslyPublished == false && HasChildren(content.Id)) + { + var descendants = GetPublishedDescendantsLocked(content).ToArray(); + scope.Events.Dispatch(Published, this, new PublishEventArgs(descendants, false, false), "Published"); + } + + Audit(AuditType.Publish, "Save and Publish performed by user", userId, content.Id); + + scope.Complete(); + } + + return result; + } + + /// + public PublishResult Unpublish(IContent content, int userId = 0) + { + var evtMsgs = EventMessagesFactory.Get(); + + using (var scope = ScopeProvider.CreateScope()) + { + scope.WriteLock(Constants.Locks.ContentTree); + + var newest = GetById(content.Id); // ensure we have the newest version + if (content.VersionId != newest.VersionId) // but use the original object if it's already the newest version + content = newest; + if (content.Published == false) + { + scope.Complete(); + return new PublishResult(PublishResultType.SuccessAlready, evtMsgs, content); // already unpublished + } + + // strategy + // fixme should we still complete the uow? don't want to rollback here! + var attempt = StrategyCanUnpublish(scope, content, userId, evtMsgs); + if (attempt.Success == false) return attempt; // causes rollback + attempt = StrategyUnpublish(scope, content, true, userId, evtMsgs); + if (attempt.Success == false) return attempt; // causes rollback + + content.WriterId = userId; + _documentRepository.Save(content); + + scope.Events.Dispatch(UnPublished, this, new PublishEventArgs(content, false, false), "UnPublished"); + scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, TreeChangeTypes.RefreshBranch).ToEventArgs()); + Audit(AuditType.UnPublish, "UnPublish performed by user", userId, content.Id); + + scope.Complete(); + } + + return new PublishResult(PublishResultType.Success, evtMsgs, content); + } + + /// + public IEnumerable PerformScheduledPublish() + { + using (var scope = ScopeProvider.CreateScope()) + { + scope.WriteLock(Constants.Locks.ContentTree); + + foreach (var d in GetContentForRelease()) + { + PublishResult result; + try + { + d.ReleaseDate = null; + d.PublishValues(); // fixme variants? + result = SaveAndPublish(d, d.WriterId); + if (result.Success == false) + Logger.Error($"Failed to publish document id={d.Id}, reason={result.Result}."); + } + catch (Exception e) + { + Logger.Error($"Failed to publish document id={d.Id}, an exception was thrown.", e); + throw; + } + yield return result; + } + foreach (var d in GetContentForExpiration()) + { + try + { + d.ExpireDate = null; + var result = Unpublish(d, d.WriterId); + if (result.Success == false) + Logger.Error($"Failed to unpublish document id={d.Id}, reason={result.Result}."); + } + catch (Exception e) + { + Logger.Error($"Failed to unpublish document id={d.Id}, an exception was thrown.", e); + throw; + } + } + + scope.Complete(); + } + } + + /// + public IEnumerable SaveAndPublishBranch(IContent content, bool force, int? languageId = null, string segment = null, int userId = 0) + { + segment = segment?.ToLowerInvariant(); + + bool IsEditing(IContent c, int? l, string s) + => c.Properties.Any(x => x.Values.Where(y => y.LanguageId == l && y.Segment == s).Any(y => y.EditedValue != y.PublishedValue)); + + return SaveAndPublishBranch(content, force, document => IsEditing(document, languageId, segment), document => document.PublishValues(languageId, segment), userId); + } + + /// + public IEnumerable SaveAndPublishBranch(IContent document, bool force, + Func editing, Func publishValues, int userId = 0) + { + var evtMsgs = EventMessagesFactory.Get(); + var results = new List(); + var publishedDocuments = new List(); + + using (var scope = ScopeProvider.CreateScope()) + { + scope.WriteLock(Constants.Locks.ContentTree); + + // fixme events?! + + if (!document.HasIdentity) + throw new InvalidOperationException("Do not branch-publish a new document."); + + var publishedState = ((Content) document).PublishedState; + if (publishedState == PublishedState.Publishing) + throw new InvalidOperationException("Do not publish values when publishing branches."); + + // deal with the branch root - if it fails, abort + var result = SaveAndPublishBranchOne(scope, document, editing, publishValues, true, publishedDocuments, evtMsgs, userId); + results.Add(result); + if (!result.Success) return results; + + // deal with descendants + // if one fails, abort its branch + var exclude = new HashSet(); + foreach (var d in GetDescendants(document)) + { + // if parent is excluded, exclude document and ignore + // if not forcing, and not publishing, exclude document and ignore + if (exclude.Contains(d.ParentId) || !force && !d.Published) + { + exclude.Add(d.Id); + continue; + } + + // no need to check path here, + // 1. because we know the parent is path-published (we just published it) + // 2. because it would not work as nothing's been written out to the db until the uow completes + result = SaveAndPublishBranchOne(scope, d, editing, publishValues, false, publishedDocuments, evtMsgs, userId); + results.Add(result); + if (result.Success) continue; + + // abort branch + exclude.Add(d.Id); + } + + scope.Events.Dispatch(TreeChanged, this, new TreeChange(document, TreeChangeTypes.RefreshBranch).ToEventArgs()); + scope.Events.Dispatch(Published, this, new PublishEventArgs(publishedDocuments, false, false), "Published"); + Audit(AuditType.Publish, "SaveAndPublishBranch performed by user", userId, document.Id); + + scope.Complete(); + } + + return results; + } + + private PublishResult SaveAndPublishBranchOne(IScope scope, IContent document, + Func editing, Func publishValues, + bool checkPath, + List publishedDocuments, + EventMessages evtMsgs, int userId) + { + // if already published, and values haven't changed - i.e. not changing anything + // nothing to do - fixme - unless we *want* to bump dates? + if (document.Published && (editing == null || !editing(document))) + return new PublishResult(PublishResultType.SuccessAlready, evtMsgs, document); + + // publish & check if values are valid + if (publishValues != null && !publishValues(document)) + return new PublishResult(PublishResultType.FailedContentInvalid, evtMsgs, document); + + // check if we can publish + var result = StrategyCanPublish(scope, document, userId, checkPath, evtMsgs); + if (!result.Success) + return result; + + // publish - should be successful + var publishResult = StrategyPublish(scope, document, /*canPublish:*/ true, userId, evtMsgs); + if (!publishResult.Success) + throw new Exception("oops: failed to publish."); + + // save + document.WriterId = userId; + _documentRepository.Save(document); + publishedDocuments.Add(document); + return publishResult; + } + + #endregion + + #region Delete + + /// + public OperationResult Delete(IContent content, int userId) + { + var evtMsgs = EventMessagesFactory.Get(); + + using (var scope = ScopeProvider.CreateScope()) + { + var deleteEventArgs = new DeleteEventArgs(content, evtMsgs); + if (scope.Events.DispatchCancelable(Deleting, this, deleteEventArgs)) + { + scope.Complete(); + return OperationResult.Cancel(evtMsgs); + } + + scope.WriteLock(Constants.Locks.ContentTree); + + // if it's not trashed yet, and published, we should unpublish + // but... UnPublishing event makes no sense (not going to cancel?) and no need to save + // just raise the event + if (content.Trashed == false && content.Published) + scope.Events.Dispatch(UnPublished, this, new PublishEventArgs(content, false, false), "UnPublished"); + + DeleteLocked(scope, content); + + scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, TreeChangeTypes.Remove).ToEventArgs()); + Audit(AuditType.Delete, "Delete Content performed by user", userId, content.Id); + + scope.Complete(); + } + + return OperationResult.Succeed(evtMsgs); + } + + private void DeleteLocked(IScope scope, IContent content) + { + // then recursively delete descendants, bottom-up + // just repository.Delete + an event + var stack = new Stack(); + stack.Push(content); + var level = 1; + while (stack.Count > 0) + { + var c = stack.Peek(); + IContent[] cc; + if (c.Level == level) + while ((cc = c.Children(this).ToArray()).Length > 0) + { + foreach (var ci in cc) + stack.Push(ci); + c = cc[cc.Length - 1]; + } + c = stack.Pop(); + level = c.Level; + + _documentRepository.Delete(c); + var args = new DeleteEventArgs(c, false); // raise event & get flagged files + scope.Events.Dispatch(Deleted, this, args); + + // fixme not going to work, do it differently + _mediaFileSystem.DeleteFiles(args.MediaFilesToDelete, // remove flagged files + (file, e) => Logger.Error("An error occurred while deleting file attached to nodes: " + file, e)); + } + } + + //TODO: + // both DeleteVersions methods below have an issue. Sort of. They do NOT take care of files the way + // Delete does - for a good reason: the file may be referenced by other, non-deleted, versions. BUT, + // if that's not the case, then the file will never be deleted, because when we delete the content, + // the version referencing the file will not be there anymore. SO, we can leak files. + + /// + /// 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 scope = ScopeProvider.CreateScope()) + { + var deleteRevisionsEventArgs = new DeleteRevisionsEventArgs(id, dateToRetain: versionDate); + if (scope.Events.DispatchCancelable(DeletingVersions, this, deleteRevisionsEventArgs)) + { + scope.Complete(); + return; + } + + scope.WriteLock(Constants.Locks.ContentTree); + _documentRepository.DeleteVersions(id, versionDate); + + deleteRevisionsEventArgs.CanCancel = false; + scope.Events.Dispatch(DeletedVersions, this, deleteRevisionsEventArgs); + Audit(AuditType.Delete, "Delete Content by version date performed by user", userId, Constants.System.Root); + + scope.Complete(); + } + } + + /// + /// 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, int versionId, bool deletePriorVersions, int userId = 0) + { + using (var scope = ScopeProvider.CreateScope()) + { + if (scope.Events.DispatchCancelable(DeletingVersions, this, new DeleteRevisionsEventArgs(id, /*specificVersion:*/ versionId))) + { + scope.Complete(); + return; + } + + if (deletePriorVersions) + { + var content = GetVersion(versionId); + // fixme nesting uow? + DeleteVersions(id, content.UpdateDate, userId); + } + + scope.WriteLock(Constants.Locks.ContentTree); + var c = _documentRepository.Get(id); + if (c.VersionId != versionId) // don't delete the current version + _documentRepository.DeleteVersion(versionId); + + scope.Events.Dispatch(DeletedVersions, this, new DeleteRevisionsEventArgs(id, false,/* specificVersion:*/ versionId)); + Audit(AuditType.Delete, "Delete Content by version performed by user", userId, Constants.System.Root); + + scope.Complete(); + } + } + + #endregion + + #region Move, RecycleBin + + /// + public OperationResult MoveToRecycleBin(IContent content, int userId) + { + var evtMsgs = EventMessagesFactory.Get(); + var moves = new List>(); + + using (var scope = ScopeProvider.CreateScope()) + { + scope.WriteLock(Constants.Locks.ContentTree); + + var originalPath = content.Path; + var moveEventInfo = new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent); + var moveEventArgs = new MoveEventArgs(evtMsgs, moveEventInfo); + if (scope.Events.DispatchCancelable(Trashing, this, moveEventArgs)) + { + scope.Complete(); + return OperationResult.Cancel(evtMsgs); // causes rollback + } + + // if it's published we may want to force-unpublish it - that would be backward-compatible... but... + // making a radical decision here: trashing is equivalent to moving under an unpublished node so + // it's NOT unpublishing, only the content is now masked - allowing us to restore it if wanted + //if (content.HasPublishedVersion) + //{ } + + PerformMoveLocked(content, Constants.System.RecycleBinContent, null, userId, moves, true); + scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, TreeChangeTypes.RefreshBranch).ToEventArgs()); + + var moveInfo = moves + .Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId)) + .ToArray(); + + moveEventArgs.CanCancel = false; + moveEventArgs.MoveInfoCollection = moveInfo; + scope.Events.Dispatch(Trashed, this, moveEventArgs); + Audit(AuditType.Move, "Move Content to Recycle Bin performed by user", userId, content.Id); + + scope.Complete(); + } + + return OperationResult.Succeed(evtMsgs); + } + + /// + /// 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) + { + // if moving to the recycle bin then use the proper method + if (parentId == Constants.System.RecycleBinContent) + { + MoveToRecycleBin(content, userId); + return; + } + + var moves = new List>(); + + using (var scope = ScopeProvider.CreateScope()) + { + scope.WriteLock(Constants.Locks.ContentTree); + + var parent = parentId == Constants.System.Root ? null : GetById(parentId); + if (parentId != Constants.System.Root && (parent == null || parent.Trashed)) + throw new InvalidOperationException("Parent does not exist or is trashed."); // causes rollback + + var moveEventInfo = new MoveEventInfo(content, content.Path, parentId); + var moveEventArgs = new MoveEventArgs(moveEventInfo); + if (scope.Events.DispatchCancelable(Moving, this, moveEventArgs)) + { + scope.Complete(); + return; // causes rollback + } + + // if content was trashed, and since we're not moving to the recycle bin, + // indicate that the trashed status should be changed to false, else just + // leave it unchanged + var trashed = content.Trashed ? false : (bool?)null; + + // if the content was trashed under another content, and so has a published version, + // it cannot move back as published but has to be unpublished first - that's for the + // root content, everything underneath will retain its published status + if (content.Trashed && content.Published) + { + // however, it had been masked when being trashed, so there's no need for + // any special event here - just change its state + ((Content) content).PublishedState = PublishedState.Unpublishing; + } + + PerformMoveLocked(content, parentId, parent, userId, moves, trashed); + + scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, TreeChangeTypes.RefreshBranch).ToEventArgs()); + + var moveInfo = moves //changes + .Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId)) + .ToArray(); + + moveEventArgs.MoveInfoCollection = moveInfo; + moveEventArgs.CanCancel = false; + scope.Events.Dispatch(Moved, this, moveEventArgs); + Audit(AuditType.Move, "Move Content performed by user", userId, content.Id); + + scope.Complete(); + } + } + + // MUST be called from within WriteLock + // trash indicates whether we are trashing, un-trashing, or not changing anything + private void PerformMoveLocked(IContent content, int parentId, IContent parent, int userId, + ICollection> moves, + bool? trash) + { + content.WriterId = userId; + content.ParentId = parentId; + + // get the level delta (old pos to new pos) + var levelDelta = parent == null + ? 1 - content.Level + (parentId == Constants.System.RecycleBinContent ? 1 : 0) + : parent.Level + 1 - content.Level; + + var paths = new Dictionary(); + + moves.Add(Tuple.Create(content, content.Path)); // capture original path + + // get before moving, in case uow is immediate + var descendants = GetDescendants(content); + + // these will be updated by the repo because we changed parentId + //content.Path = (parent == null ? "-1" : parent.Path) + "," + content.Id; + //content.SortOrder = ((ContentRepository) repository).NextChildSortOrder(parentId); + //content.Level += levelDelta; + PerformMoveContentLocked(content, userId, trash); + + // if uow is not immediate, content.Path will be updated only when the UOW commits, + // and because we want it now, we have to calculate it by ourselves + //paths[content.Id] = content.Path; + paths[content.Id] = (parent == null ? (parentId == Constants.System.RecycleBinContent ? "-1,-20" : "-1") : parent.Path) + "," + content.Id; + + foreach (var descendant in descendants) + { + moves.Add(Tuple.Create(descendant, descendant.Path)); // capture original path + + // update path and level since we do not update parentId + if (paths.ContainsKey(descendant.ParentId) == false) + Console.WriteLine("oops on " + descendant.ParentId + " for " + content.Path + " " + parent?.Path); + descendant.Path = paths[descendant.Id] = paths[descendant.ParentId] + "," + descendant.Id; + Console.WriteLine("path " + descendant.Id + " = " + paths[descendant.Id]); + descendant.Level += levelDelta; + PerformMoveContentLocked(descendant, userId, trash); + } + } + + private void PerformMoveContentLocked(IContent content, int userId, bool? trash) + { + if (trash.HasValue) ((ContentBase) content).Trashed = trash.Value; + content.WriterId = userId; + _documentRepository.Save(content); + } + + /// + /// Empties the Recycle Bin by deleting all that resides in the bin + /// + public void EmptyRecycleBin() + { + var nodeObjectType = Constants.ObjectTypes.Document; + var deleted = new List(); + var evtMsgs = EventMessagesFactory.Get(); // todo - and then? + + using (var scope = ScopeProvider.CreateScope()) + { + scope.WriteLock(Constants.Locks.ContentTree); + + // v7 EmptyingRecycleBin and EmptiedRecycleBin events are greatly simplified since + // each deleted items will have its own deleting/deleted events. so, files and such + // are managed by Delete, and not here. + + // no idea what those events are for, keep a simplified version + var recycleBinEventArgs = new RecycleBinEventArgs(nodeObjectType); + if (scope.Events.DispatchCancelable(EmptyingRecycleBin, this, recycleBinEventArgs)) + { + scope.Complete(); + return; // causes rollback + } + + // emptying the recycle bin means deleting whetever is in there - do it properly! + var query = Query().Where(x => x.ParentId == Constants.System.RecycleBinContent); + var contents = _documentRepository.Get(query).ToArray(); + foreach (var content in contents) + { + DeleteLocked(scope, content); + deleted.Add(content); + } + + recycleBinEventArgs.CanCancel = false; + recycleBinEventArgs.RecycleBinEmptiedSuccessfully = true; // oh my?! + scope.Events.Dispatch(EmptiedRecycleBin, this, recycleBinEventArgs); + scope.Events.Dispatch(TreeChanged, this, deleted.Select(x => new TreeChange(x, TreeChangeTypes.Remove)).ToEventArgs()); + Audit(AuditType.Delete, "Empty Content Recycle Bin performed by user", 0, Constants.System.RecycleBinContent); + + scope.Complete(); + } + } + + #endregion + + #region Others + + /// + /// 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) + { + var copy = content.DeepCloneWithResetIdentities(); + copy.ParentId = parentId; + + using (var scope = ScopeProvider.CreateScope()) + { + var copyEventArgs = new CopyEventArgs(content, copy, true, parentId, relateToOriginal); + if (scope.Events.DispatchCancelable(Copying, this, copyEventArgs)) + { + scope.Complete(); + return null; + } + + // note - relateToOriginal is not managed here, + // it's just part of the Copied event args so the RelateOnCopyHandler knows what to do + // meaning that the event has to trigger for every copied content including descendants + + var copies = new List>(); + + scope.WriteLock(Constants.Locks.ContentTree); + + // a copy is not published (but not really unpublishing either) + // update the create author and last edit author + if (copy.Published) + ((Content) copy).Published = false; + copy.CreatorId = userId; + copy.WriterId = userId; + + //get the current permissions, if there are any explicit ones they need to be copied + var currentPermissions = GetPermissions(content); + currentPermissions.RemoveWhere(p => p.IsDefaultPermissions); + + // save and flush because we need the ID for the recursive Copying events + _documentRepository.Save(copy); + + //add permissions + if (currentPermissions.Count > 0) + { + var permissionSet = new ContentPermissionSet(copy, currentPermissions); + _documentRepository.AddOrUpdatePermissions(permissionSet); + } + + // keep track of copies + copies.Add(Tuple.Create(content, copy)); + var idmap = new Dictionary { [content.Id] = copy.Id }; + + if (recursive) // process descendants + { + foreach (var descendant in GetDescendants(content)) + { + // if parent has not been copied, skip, else gets its copy id + if (idmap.TryGetValue(descendant.ParentId, out parentId) == false) continue; + + var descendantCopy = descendant.DeepCloneWithResetIdentities(); + descendantCopy.ParentId = parentId; + + if (scope.Events.DispatchCancelable(Copying, this, new CopyEventArgs(descendant, descendantCopy, parentId))) + continue; + + // a copy is not published (but not really unpublishing either) + // update the create author and last edit author + if (descendantCopy.Published) + ((Content) descendantCopy).Published = false; + descendantCopy.CreatorId = userId; + descendantCopy.WriterId = userId; + + // save and flush (see above) + _documentRepository.Save(descendantCopy); + + copies.Add(Tuple.Create(descendant, descendantCopy)); + idmap[descendant.Id] = descendantCopy.Id; + } + } + + // not handling tags here, because + // - tags should be handled by the content repository + // - a copy is unpublished and therefore has no impact on tags in DB + + scope.Events.Dispatch(TreeChanged, this, new TreeChange(copy, TreeChangeTypes.RefreshBranch).ToEventArgs()); + foreach (var x in copies) + scope.Events.Dispatch(Copied, this, new CopyEventArgs(x.Item1, x.Item2, false, x.Item2.ParentId, relateToOriginal)); + Audit(AuditType.Copy, "Copy Content performed by user", content.WriterId, content.Id); + + scope.Complete(); + } + + 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 scope = ScopeProvider.CreateScope()) + { + var sendToPublishEventArgs = new SendToPublishEventArgs(content); + if (scope.Events.DispatchCancelable(SendingToPublish, this, sendToPublishEventArgs)) + { + scope.Complete(); + return false; + } + + //Save before raising event + // fixme - nesting uow? + Save(content, userId); + + sendToPublishEventArgs.CanCancel = false; + scope.Events.Dispatch(SentToPublish, this, sendToPublishEventArgs); + Audit(AuditType.SendToPublish, "Send to Publish performed by user", content.WriterId, content.Id); + } + + return true; + } + + /// + /// 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 itemsA = items.ToArray(); + if (itemsA.Length == 0) return true; + + using (var scope = ScopeProvider.CreateScope()) + { + var saveEventArgs = new SaveEventArgs(itemsA); + if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) + return false; + + var published = new List(); + var saved = new List(); + + scope.WriteLock(Constants.Locks.ContentTree); + var sortOrder = 0; + + foreach (var content in itemsA) + { + // 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 == sortOrder) + { + sortOrder++; + continue; + } + + // else update + content.SortOrder = sortOrder++; + content.WriterId = userId; + + // if it's published, register it, no point running StrategyPublish + // since we're not really publishing it and it cannot be cancelled etc + if (content.Published) + published.Add(content); + + // save + saved.Add(content); + _documentRepository.Save(content); + } + + if (raiseEvents) + { + saveEventArgs.CanCancel = false; + scope.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); + } + + scope.Events.Dispatch(TreeChanged, this, saved.Select(x => new TreeChange(x, TreeChangeTypes.RefreshNode)).ToEventArgs()); + + if (raiseEvents && published.Any()) + scope.Events.Dispatch(Published, this, new PublishEventArgs(published, false, false), "Published"); + + Audit(AuditType.Sort, "Sorting content performed by user", userId, 0); + + scope.Complete(); + } + + return true; + } + + #endregion + + #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 scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.ContentTree); + return GetPublishedDescendantsLocked(content).ToArray(); // ToArray important in uow! + } + } + + internal IEnumerable GetPublishedDescendantsLocked(IContent content) + { + var pathMatch = content.Path + ","; + var query = Query().Where(x => x.Id != content.Id && x.Path.StartsWith(pathMatch) /*&& x.Trashed == false*/); + var contents = _documentRepository.Get(query); + + // beware! contents contains all published version below content + // including those that are not directly published because below an unpublished content + // these must be filtered out here + + var parents = new List { content.Id }; + foreach (var c in contents) + { + if (parents.Contains(c.ParentId)) + { + yield return c; + parents.Add(c.Id); + } + } + } + + #endregion + + #region Private Methods + + private void Audit(AuditType type, string message, int userId, int objectId) + { + _auditRepository.Save(new AuditItem(objectId, message, type, userId)); + } + + #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 before publish + /// + public static event TypedEventHandler> Publishing; + + /// + /// Occurs after publish + /// + public static event TypedEventHandler> Published; + + /// + /// Occurs before unpublish + /// + public static event TypedEventHandler> UnPublishing; + + /// + /// Occurs after unpublish + /// + public static event TypedEventHandler> UnPublished; + + /// + /// Occurs after change. + /// + internal static event TypedEventHandler.EventArgs> TreeChanged; + + /// + /// 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 + + #region Publishing Strategies + + // ensures that a document can be published + internal PublishResult StrategyCanPublish(IScope scope, IContent content, int userId, bool checkPath, EventMessages evtMsgs) + { + // raise Publishing event + if (scope.Events.DispatchCancelable(Publishing, this, new PublishEventArgs(content, evtMsgs))) + { + Logger.Info($"Document \"'{content.Name}\" (id={content.Id}) cannot be published: publishing was cancelled."); + return new PublishResult(PublishResultType.FailedCancelledByEvent, evtMsgs, content); + } + + // ensure that the document has published values + // either because it is 'publishing' or because it already has a published version + if (((Content) content).PublishedState != PublishedState.Publishing && content.PublishedVersionId == 0) + { + Logger.Info($"Document \"{content.Name}\" (id={content.Id}) cannot be published: document does not have published values."); + return new PublishResult(PublishResultType.FailedNoPublishedValues, evtMsgs, content); + } + + // ensure that the document status is correct + switch (content.Status) + { + case ContentStatus.Expired: + Logger.Info($"Document \"{content.Name}\" (id={content.Id}) cannot be published: document has expired."); + return new PublishResult(PublishResultType.FailedHasExpired, evtMsgs, content); + + case ContentStatus.AwaitingRelease: + Logger.Info($"Document \"{content.Name}\" (id={content.Id}) cannot be published: document is awaiting release."); + return new PublishResult(PublishResultType.FailedAwaitingRelease, evtMsgs, content); + + case ContentStatus.Trashed: + Logger.Info($"Document \"{content.Name}\" (id={content.Id}) cannot be published: document is trashed."); + return new PublishResult(PublishResultType.FailedIsTrashed, evtMsgs, content); + } + + if (!checkPath) return new PublishResult(evtMsgs, content); + + // check if the content can be path-published + // root content can be published + // else check ancestors - we know we are not trashed + var pathIsOk = content.ParentId == Constants.System.Root || IsPathPublished(GetParent(content)); + if (pathIsOk == false) + { + Logger.Info($"Document \"{content.Name}\" (id={content.Id}) cannot be published: parent is not published."); + return new PublishResult(PublishResultType.FailedPathNotPublished, evtMsgs, content); + } + + return new PublishResult(evtMsgs, content); + } + + // publishes a document + internal PublishResult StrategyPublish(IScope scope, IContent content, bool canPublish, int userId, EventMessages evtMsgs) + { + // note: when used at top-level, StrategyCanPublish with checkPath=true should have run already + // and alreadyCheckedCanPublish should be true, so not checking again. when used at nested level, + // there is no need to check the path again. so, checkPath=false in StrategyCanPublish below + + var result = canPublish + ? new PublishResult(evtMsgs, content) // already know we can + : StrategyCanPublish(scope, content, userId, /*checkPath:*/ false, evtMsgs); // else check + + if (result.Success == false) + return result; + + // change state to publishing + ((Content) content).PublishedState = PublishedState.Publishing; + + Logger.Info($"Content \"{content.Name}\" (id={content.Id}) has been published."); + return result; + } + + // ensures that a document can be unpublished + internal PublishResult StrategyCanUnpublish(IScope scope, IContent content, int userId, EventMessages evtMsgs) + { + // raise UnPublishing event + if (scope.Events.DispatchCancelable(UnPublishing, this, new PublishEventArgs(content, evtMsgs))) + { + Logger.Info($"Document \"{content.Name}\" (id={content.Id}) cannot be unpublished: unpublishing was cancelled."); + return new PublishResult(PublishResultType.FailedCancelledByEvent, evtMsgs, content); + } + + return new PublishResult(evtMsgs, content); + } + + // unpublishes a document + internal PublishResult StrategyUnpublish(IScope scope, IContent content, bool canUnpublish, int userId, EventMessages evtMsgs) + { + var attempt = canUnpublish + ? new PublishResult(evtMsgs, content) // already know we can + : StrategyCanUnpublish(scope, content, userId, evtMsgs); // else check + + if (attempt.Success == false) + return attempt; + + // if the document has a release date set to before now, + // it should be removed so it doesn't interrupt an unpublish + // otherwise it would remain released == published + if (content.ReleaseDate.HasValue && content.ReleaseDate.Value <= DateTime.Now) + { + content.ReleaseDate = null; + Logger.Info($"Document \"{content.Name}\" (id={content.Id}) had its release date removed, because it was unpublished."); + } + + // change state to unpublishing + ((Content) content).PublishedState = PublishedState.Unpublishing; + + Logger.Info($"Document \"{content.Name}\" (id={content.Id}) has been unpublished."); + return attempt; + } + + #endregion + + #region Content Types + + /// + /// 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. + /// Deletes content items of the specified type, and only that type. Does *not* handle content types + /// inheritance and compositions, which need to be managed outside of this method. + /// + /// Id of the + /// Optional Id of the user issueing the delete operation + public void DeleteOfTypes(IEnumerable contentTypeIds, int userId = 0) + { + //TODO: This currently this is called from the ContentTypeService but that needs to change, + // if we are deleting a content type, we should just delete the data and do this operation slightly differently. + // This method will recursively go lookup every content item, check if any of it's descendants are + // of a different type, move them to the recycle bin, then permanently delete the content items. + // The main problem with this is that for every content item being deleted, events are raised... + // which we need for many things like keeping caches in sync, but we can surely do this MUCH better. + + var changes = new List>(); + var moves = new List>(); + var contentTypeIdsA = contentTypeIds.ToArray(); + + // using an immediate uow here because we keep making changes with + // PerformMoveLocked and DeleteLocked that must be applied immediately, + // no point queuing operations + // + using (var scope = ScopeProvider.CreateScope()) + { + scope.WriteLock(Constants.Locks.ContentTree); + + var query = Query().WhereIn(x => x.ContentTypeId, contentTypeIdsA); + var contents = _documentRepository.Get(query).ToArray(); + + if (scope.Events.DispatchCancelable(Deleting, this, new DeleteEventArgs(contents))) + { + scope.Complete(); + return; + } + + // order by level, descending, so deepest first - that way, we cannot move + // a content of the deleted type, to the recycle bin (and then delete it...) + foreach (var content in contents.OrderByDescending(x => x.ParentId)) + { + // if it's not trashed yet, and published, we should unpublish + // but... UnPublishing event makes no sense (not going to cancel?) and no need to save + // just raise the event + if (content.Trashed == false && content.Published) + scope.Events.Dispatch(UnPublished, this, new PublishEventArgs(content, false, false), "UnPublished"); + + // if current content has children, move them to trash + var c = content; + var childQuery = Query().Where(x => x.ParentId == c.Id); + var children = _documentRepository.Get(childQuery); + foreach (var child in children) + { + // see MoveToRecycleBin + PerformMoveLocked(child, Constants.System.RecycleBinContent, null, userId, moves, true); + changes.Add(new TreeChange(content, TreeChangeTypes.RefreshBranch)); + } + + // delete content + // triggers the deleted event (and handles the files) + DeleteLocked(scope, content); + changes.Add(new TreeChange(content, TreeChangeTypes.Remove)); + } + + var moveInfos = moves + .Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId)) + .ToArray(); + if (moveInfos.Length > 0) + scope.Events.Dispatch(Trashed, this, new MoveEventArgs(false, moveInfos), "Trashed"); + scope.Events.Dispatch(TreeChanged, this, changes.ToEventArgs()); + + Audit(AuditType.Delete, $"Delete Content of Type {string.Join(",", contentTypeIdsA)} performed by user", userId, Constants.System.Root); + + scope.Complete(); + } + } + + /// + /// Deletes all content items of specified type. All children of deleted content item 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 deleting the media + public void DeleteOfType(int contentTypeId, int userId = 0) + { + DeleteOfTypes(new[] { contentTypeId }, userId); + } + + private IContentType GetContentType(IScope scope, string contentTypeAlias) + { + if (string.IsNullOrWhiteSpace(contentTypeAlias)) throw new ArgumentNullOrEmptyException(nameof(contentTypeAlias)); + + scope.ReadLock(Constants.Locks.ContentTypes); + + var query = Query().Where(x => x.Alias == contentTypeAlias); + var contentType = _contentTypeRepository.Get(query).FirstOrDefault(); + + if (contentType == null) + throw new Exception($"No ContentType matching the passed in Alias: '{contentTypeAlias}' was found"); // causes rollback + + return contentType; + } + + private IContentType GetContentType(string contentTypeAlias) + { + if (string.IsNullOrWhiteSpace(contentTypeAlias)) throw new ArgumentNullOrEmptyException(nameof(contentTypeAlias)); + + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + return GetContentType(scope, contentTypeAlias); + } + } + + #endregion + + #region Blueprints + + public IContent GetBlueprintById(int id) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.ContentTree); + var blueprint = _documentBlueprintRepository.Get(id); + if (blueprint != null) + ((Content) blueprint).Blueprint = true; + return blueprint; + } + } + + public IContent GetBlueprintById(Guid id) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.ContentTree); + var blueprint = _documentBlueprintRepository.Get(id); + if (blueprint != null) + ((Content) blueprint).Blueprint = 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).Blueprint = true; + + using (var scope = ScopeProvider.CreateScope()) + { + scope.WriteLock(Constants.Locks.ContentTree); + + if (string.IsNullOrWhiteSpace(content.Name)) + { + throw new ArgumentException("Cannot save content blueprint with empty name."); + } + + if (content.HasIdentity == false) + { + content.CreatorId = userId; + } + content.WriterId = userId; + + _documentBlueprintRepository.Save(content); + + scope.Events.Dispatch(SavedBlueprint, this, new SaveEventArgs(content), "SavedBlueprint"); + + scope.Complete(); + } + } + + public void DeleteBlueprint(IContent content, int userId = 0) + { + using (var scope = ScopeProvider.CreateScope()) + { + scope.WriteLock(Constants.Locks.ContentTree); + _documentBlueprintRepository.Delete(content); + scope.Events.Dispatch(DeletedBlueprint, this, new DeleteEventArgs(content), "DeletedBlueprint"); + scope.Complete(); + } + } + + public IContent CreateContentFromBlueprint(IContent blueprint, string name, int userId = 0) + { + if (blueprint == null) throw new ArgumentNullException(nameof(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.GetValue()); + + return content; + } + + public IEnumerable GetBlueprintsForContentTypes(params int[] contentTypeId) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + var query = Query(); + if (contentTypeId.Length > 0) + { + query.Where(x => contentTypeId.Contains(x.ContentTypeId)); + } + return _documentBlueprintRepository.Get(query).Select(x => + { + ((Content) x).Blueprint = true; + return x; + }); + } + } + + #endregion + } +} diff --git a/src/Umbraco.Core/Services/ContentTypeService.cs b/src/Umbraco.Core/Services/Implement/ContentTypeService.cs similarity index 96% rename from src/Umbraco.Core/Services/ContentTypeService.cs rename to src/Umbraco.Core/Services/Implement/ContentTypeService.cs index 077b522064..cbbe9e6f63 100644 --- a/src/Umbraco.Core/Services/ContentTypeService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentTypeService.cs @@ -1,88 +1,87 @@ -using System; -using System.Collections.Generic; -using LightInject; -using Umbraco.Core.Events; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Core.Persistence.Repositories; -using Umbraco.Core.Scoping; - -namespace Umbraco.Core.Services -{ - /// - /// Represents the ContentType Service, which is an easy access to operations involving - /// - internal class ContentTypeService : ContentTypeServiceBase, IContentTypeService - { - public ContentTypeService(IScopeProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory, IContentService contentService, - IContentTypeRepository repository, IAuditRepository auditRepository, IDocumentTypeContainerRepository entityContainerRepository, IEntityRepository entityRepository) - : base(provider, logger, eventMessagesFactory, repository, auditRepository, entityContainerRepository, entityRepository) - { - ContentService = contentService; - } - - protected override IContentTypeService This => this; - - // beware! order is important to avoid deadlocks - protected override int[] ReadLockIds { get; } = { Constants.Locks.ContentTypes }; - protected override int[] WriteLockIds { get; } = { Constants.Locks.ContentTree, Constants.Locks.ContentTypes }; - - private IContentService ContentService { get; } - - protected override Guid ContainedObjectType => Constants.ObjectTypes.DocumentType; - - protected override void DeleteItemsOfTypes(IEnumerable typeIds) - { - foreach (var typeId in typeIds) - ContentService.DeleteOfType(typeId); - } - - /// - /// Gets all property type aliases accross content, media and member types. - /// - /// All property type aliases. - /// Beware! Works accross content, media and member types. - public IEnumerable GetAllPropertyTypeAliases() - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - // that one is special because it works accross content, media and member types - scope.ReadLock(Constants.Locks.ContentTypes, Constants.Locks.MediaTypes, Constants.Locks.MemberTypes); - return Repository.GetAllPropertyTypeAliases(); - } - } - - /// - /// Gets all content type aliases accross content, media and member types. - /// - /// Optional object types guid to restrict to content, and/or media, and/or member types. - /// All content type aliases. - /// Beware! Works accross content, media and member types. - public IEnumerable GetAllContentTypeAliases(params Guid[] guids) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - // that one is special because it works accross content, media and member types - scope.ReadLock(Constants.Locks.ContentTypes, Constants.Locks.MediaTypes, Constants.Locks.MemberTypes); - return Repository.GetAllContentTypeAliases(guids); - } - } - - /// - /// Gets all content type id for aliases accross content, media and member types. - /// - /// Aliases to look for. - /// All content type ids. - /// Beware! Works accross content, media and member types. - public IEnumerable GetAllContentTypeIds(string[] aliases) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - // that one is special because it works accross content, media and member types - scope.ReadLock(Constants.Locks.ContentTypes, Constants.Locks.MediaTypes, Constants.Locks.MemberTypes); - return Repository.GetAllContentTypeIds(aliases); - } - } - - } -} +using System; +using System.Collections.Generic; +using Umbraco.Core.Events; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence.Repositories; +using Umbraco.Core.Scoping; + +namespace Umbraco.Core.Services.Implement +{ + /// + /// Represents the ContentType Service, which is an easy access to operations involving + /// + internal class ContentTypeService : ContentTypeServiceBase, IContentTypeService + { + public ContentTypeService(IScopeProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory, IContentService contentService, + IContentTypeRepository repository, IAuditRepository auditRepository, IDocumentTypeContainerRepository entityContainerRepository, IEntityRepository entityRepository) + : base(provider, logger, eventMessagesFactory, repository, auditRepository, entityContainerRepository, entityRepository) + { + ContentService = contentService; + } + + protected override IContentTypeService This => this; + + // beware! order is important to avoid deadlocks + protected override int[] ReadLockIds { get; } = { Constants.Locks.ContentTypes }; + protected override int[] WriteLockIds { get; } = { Constants.Locks.ContentTree, Constants.Locks.ContentTypes }; + + private IContentService ContentService { get; } + + protected override Guid ContainedObjectType => Constants.ObjectTypes.DocumentType; + + protected override void DeleteItemsOfTypes(IEnumerable typeIds) + { + foreach (var typeId in typeIds) + ContentService.DeleteOfType(typeId); + } + + /// + /// Gets all property type aliases accross content, media and member types. + /// + /// All property type aliases. + /// Beware! Works accross content, media and member types. + public IEnumerable GetAllPropertyTypeAliases() + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + // that one is special because it works accross content, media and member types + scope.ReadLock(Constants.Locks.ContentTypes, Constants.Locks.MediaTypes, Constants.Locks.MemberTypes); + return Repository.GetAllPropertyTypeAliases(); + } + } + + /// + /// Gets all content type aliases accross content, media and member types. + /// + /// Optional object types guid to restrict to content, and/or media, and/or member types. + /// All content type aliases. + /// Beware! Works accross content, media and member types. + public IEnumerable GetAllContentTypeAliases(params Guid[] guids) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + // that one is special because it works accross content, media and member types + scope.ReadLock(Constants.Locks.ContentTypes, Constants.Locks.MediaTypes, Constants.Locks.MemberTypes); + return Repository.GetAllContentTypeAliases(guids); + } + } + + /// + /// Gets all content type id for aliases accross content, media and member types. + /// + /// Aliases to look for. + /// All content type ids. + /// Beware! Works accross content, media and member types. + public IEnumerable GetAllContentTypeIds(string[] aliases) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + // that one is special because it works accross content, media and member types + scope.ReadLock(Constants.Locks.ContentTypes, Constants.Locks.MediaTypes, Constants.Locks.MemberTypes); + return Repository.GetAllContentTypeIds(aliases); + } + } + + } +} diff --git a/src/Umbraco.Core/Services/ContentTypeServiceBase.cs b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBase.cs similarity index 89% rename from src/Umbraco.Core/Services/ContentTypeServiceBase.cs rename to src/Umbraco.Core/Services/Implement/ContentTypeServiceBase.cs index 56a3325880..d5cdd36318 100644 --- a/src/Umbraco.Core/Services/ContentTypeServiceBase.cs +++ b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBase.cs @@ -2,7 +2,7 @@ using Umbraco.Core.Logging; using Umbraco.Core.Scoping; -namespace Umbraco.Core.Services +namespace Umbraco.Core.Services.Implement { internal abstract class ContentTypeServiceBase : ScopeRepositoryService { diff --git a/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTItemTService.cs b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTItemTService.cs similarity index 99% rename from src/Umbraco.Core/Services/ContentTypeServiceBaseOfTItemTService.cs rename to src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTItemTService.cs index fc420ffecf..63c5340a6c 100644 --- a/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTItemTService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTItemTService.cs @@ -4,7 +4,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Scoping; using Umbraco.Core.Services.Changes; -namespace Umbraco.Core.Services +namespace Umbraco.Core.Services.Implement { internal abstract class ContentTypeServiceBase : ContentTypeServiceBase where TItem : class, IContentTypeComposition diff --git a/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs similarity index 99% rename from src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs rename to src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs index 33f1b8a1df..ce3ec2d343 100644 --- a/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs @@ -11,7 +11,7 @@ using Umbraco.Core.Persistence.Repositories.Implement; using Umbraco.Core.Scoping; using Umbraco.Core.Services.Changes; -namespace Umbraco.Core.Services +namespace Umbraco.Core.Services.Implement { internal abstract class ContentTypeServiceBase : ContentTypeServiceBase, IContentTypeServiceBase where TRepository : IContentTypeRepositoryBase diff --git a/src/Umbraco.Core/Services/DataTypeService.cs b/src/Umbraco.Core/Services/Implement/DataTypeService.cs similarity index 97% rename from src/Umbraco.Core/Services/DataTypeService.cs rename to src/Umbraco.Core/Services/Implement/DataTypeService.cs index ca614334b1..853c35f409 100644 --- a/src/Umbraco.Core/Services/DataTypeService.cs +++ b/src/Umbraco.Core/Services/Implement/DataTypeService.cs @@ -1,647 +1,647 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Umbraco.Core.Events; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Core.PropertyEditors; -using Umbraco.Core.Exceptions; -using Umbraco.Core.Persistence.Dtos; -using Umbraco.Core.Persistence.Repositories; -using Umbraco.Core.Persistence.Repositories.Implement; -using Umbraco.Core.Scoping; - -namespace Umbraco.Core.Services -{ - /// - /// Represents the DataType Service, which is an easy access to operations involving - /// - internal class DataTypeService : ScopeRepositoryService, IDataTypeService - { - private readonly IDataTypeDefinitionRepository _dataTypeDefinitionRepository; - private readonly IDataTypeContainerRepository _dataTypeContainerRepository; - private readonly IContentTypeRepository _contentTypeRepository; - private readonly IAuditRepository _auditRepository; - private readonly IEntityRepository _entityRepository; - - public DataTypeService(IScopeProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory, - IDataTypeDefinitionRepository dataTypeDefinitionRepository, IDataTypeContainerRepository dataTypeContainerRepository, - IAuditRepository auditRepository, IEntityRepository entityRepository, IContentTypeRepository contentTypeRepository) - : base(provider, logger, eventMessagesFactory) - { - _dataTypeDefinitionRepository = dataTypeDefinitionRepository; - _dataTypeContainerRepository = dataTypeContainerRepository; - _auditRepository = auditRepository; - _entityRepository = entityRepository; - _contentTypeRepository = contentTypeRepository; - } - - #region Containers - - public Attempt> CreateContainer(int parentId, string name, int userId = 0) - { - var evtMsgs = EventMessagesFactory.Get(); - using (var scope = ScopeProvider.CreateScope()) - { - try - { - var container = new EntityContainer(Constants.ObjectTypes.DataType) - { - Name = name, - ParentId = parentId, - CreatorId = userId - }; - - if (scope.Events.DispatchCancelable(SavingContainer, this, new SaveEventArgs(container, evtMsgs))) - { - scope.Complete(); - return OperationResult.Attempt.Cancel(evtMsgs, container); - } - - _dataTypeContainerRepository.Save(container); - scope.Complete(); - - scope.Events.Dispatch(SavedContainer, this, new SaveEventArgs(container, evtMsgs)); - //TODO: Audit trail ? - - return OperationResult.Attempt.Succeed(evtMsgs, container); - } - catch (Exception ex) - { - return OperationResult.Attempt.Fail(evtMsgs, ex); - } - } - } - - public EntityContainer GetContainer(int containerId) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - return _dataTypeContainerRepository.Get(containerId); - } - } - - public EntityContainer GetContainer(Guid containerId) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - return ((EntityContainerRepository) _dataTypeContainerRepository).Get(containerId); - } - } - - public IEnumerable GetContainers(string name, int level) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - return ((EntityContainerRepository) _dataTypeContainerRepository).Get(name, level); - } - } - - public IEnumerable GetContainers(IDataTypeDefinition dataTypeDefinition) - { - var ancestorIds = dataTypeDefinition.Path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(x => - { - var asInt = x.TryConvertTo(); - return asInt ? asInt.Result : int.MinValue; - }) - .Where(x => x != int.MinValue && x != dataTypeDefinition.Id) - .ToArray(); - - return GetContainers(ancestorIds); - } - - public IEnumerable GetContainers(int[] containerIds) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - return _dataTypeContainerRepository.GetMany(containerIds); - } - } - - public Attempt SaveContainer(EntityContainer container, int userId = 0) - { - var evtMsgs = EventMessagesFactory.Get(); - - if (container.ContainedObjectType != Constants.ObjectTypes.DataType) - { - var ex = new InvalidOperationException("Not a " + Constants.ObjectTypes.DataType + " container."); - return OperationResult.Attempt.Fail(evtMsgs, ex); - } - - if (container.HasIdentity && container.IsPropertyDirty("ParentId")) - { - var ex = new InvalidOperationException("Cannot save a container with a modified parent, move the container instead."); - return OperationResult.Attempt.Fail(evtMsgs, ex); - } - - using (var scope = ScopeProvider.CreateScope()) - { - if (scope.Events.DispatchCancelable(SavingContainer, this, new SaveEventArgs(container, evtMsgs))) - { - scope.Complete(); - return OperationResult.Attempt.Cancel(evtMsgs); - } - - _dataTypeContainerRepository.Save(container); - - scope.Events.Dispatch(SavedContainer, this, new SaveEventArgs(container, evtMsgs)); - scope.Complete(); - } - - //TODO: Audit trail ? - return OperationResult.Attempt.Succeed(evtMsgs); - } - - public Attempt DeleteContainer(int containerId, int userId = 0) - { - var evtMsgs = EventMessagesFactory.Get(); - using (var scope = ScopeProvider.CreateScope()) - { - var container = _dataTypeContainerRepository.Get(containerId); - if (container == null) return OperationResult.Attempt.NoOperation(evtMsgs); - - var entity = _entityRepository.Get(container.Id); - if (entity.HasChildren()) // because container.HasChildren() does not work? - return Attempt.Fail(new OperationResult(OperationResultType.FailedCannot, evtMsgs)); // causes rollback - - if (scope.Events.DispatchCancelable(DeletingContainer, this, new DeleteEventArgs(container, evtMsgs))) - { - scope.Complete(); - return Attempt.Fail(new OperationResult(OperationResultType.FailedCancelledByEvent, evtMsgs)); - } - - _dataTypeContainerRepository.Delete(container); - - scope.Events.Dispatch(DeletedContainer, this, new DeleteEventArgs(container, evtMsgs)); - scope.Complete(); - } - - //TODO: Audit trail ? - return OperationResult.Attempt.Succeed(evtMsgs); - } - - public Attempt> RenameContainer(int id, string name, int userId = 0) - { - var evtMsgs = EventMessagesFactory.Get(); - using (var scope = ScopeProvider.CreateScope()) - { - try - { - var container = _dataTypeContainerRepository.Get(id); - - //throw if null, this will be caught by the catch and a failed returned - if (container == null) - throw new InvalidOperationException("No container found with id " + id); - - container.Name = name; - - _dataTypeContainerRepository.Save(container); - scope.Complete(); - - // fixme - triggering SavedContainer with a different name?! - scope.Events.Dispatch(SavedContainer, this, new SaveEventArgs(container, evtMsgs), "RenamedContainer"); - - return OperationResult.Attempt.Succeed(OperationResultType.Success, evtMsgs, container); - } - catch (Exception ex) - { - return OperationResult.Attempt.Fail(evtMsgs, ex); - } - } - } - - #endregion - - /// - /// Gets a by its Name - /// - /// Name of the - /// - public IDataTypeDefinition GetDataTypeDefinitionByName(string name) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - return _dataTypeDefinitionRepository.Get(Query().Where(x => x.Name == name)).FirstOrDefault(); - } - } - - /// - /// Gets a by its Id - /// - /// Id of the - /// - public IDataTypeDefinition GetDataTypeDefinitionById(int id) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - return _dataTypeDefinitionRepository.Get(id); - } - } - - /// - /// Gets a by its unique guid Id - /// - /// Unique guid Id of the DataType - /// - public IDataTypeDefinition GetDataTypeDefinitionById(Guid id) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - var query = Query().Where(x => x.Key == id); - return _dataTypeDefinitionRepository.Get(query).FirstOrDefault(); - } - } - - /// - /// Gets a by its control Id - /// - /// Alias of the property editor - /// Collection of objects with a matching contorl id - public IEnumerable GetDataTypeDefinitionByPropertyEditorAlias(string propertyEditorAlias) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - var query = Query().Where(x => x.PropertyEditorAlias == propertyEditorAlias); - return _dataTypeDefinitionRepository.Get(query); - } - } - - /// - /// Gets all objects or those with the ids passed in - /// - /// Optional array of Ids - /// An enumerable list of objects - public IEnumerable GetAllDataTypeDefinitions(params int[] ids) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - return _dataTypeDefinitionRepository.GetMany(ids); - } - } - - /// - /// Gets all prevalues for an - /// - /// Id of the to retrieve prevalues from - /// An enumerable list of string values - public IEnumerable GetPreValuesByDataTypeId(int id) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - var collection = _dataTypeDefinitionRepository.GetPreValuesCollectionByDataTypeId(id); - //now convert the collection to a string list - return collection.FormatAsDictionary() - .Select(x => x.Value.Value) - .ToList(); - } - } - - /// - /// Returns the PreValueCollection for the specified data type - /// - /// - /// - public PreValueCollection GetPreValuesCollectionByDataTypeId(int id) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - return _dataTypeDefinitionRepository.GetPreValuesCollectionByDataTypeId(id); - } - } - - /// - /// Gets a specific PreValue by its Id - /// - /// Id of the PreValue to retrieve the value from - /// PreValue as a string - public string GetPreValueAsString(int id) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - return _dataTypeDefinitionRepository.GetPreValueAsString(id); - } - } - - public Attempt> Move(IDataTypeDefinition toMove, int parentId) - { - var evtMsgs = EventMessagesFactory.Get(); - var moveInfo = new List>(); - - using (var scope = ScopeProvider.CreateScope()) - { - var moveEventInfo = new MoveEventInfo(toMove, toMove.Path, parentId); - var moveEventArgs = new MoveEventArgs(evtMsgs, moveEventInfo); - if (scope.Events.DispatchCancelable(Moving, this, moveEventArgs)) - { - scope.Complete(); - return OperationResult.Attempt.Fail(MoveOperationStatusType.FailedCancelledByEvent, evtMsgs); - } - - try - { - EntityContainer container = null; - if (parentId > 0) - { - container = _dataTypeContainerRepository.Get(parentId); - if (container == null) - throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); // causes rollback - } - moveInfo.AddRange(_dataTypeDefinitionRepository.Move(toMove, container)); - - moveEventArgs.MoveInfoCollection = moveInfo; - moveEventArgs.CanCancel = false; - scope.Events.Dispatch(Moved, this, moveEventArgs); - scope.Complete(); - } - catch (DataOperationException ex) - { - scope.Complete(); // fixme what are we doing here exactly? - return OperationResult.Attempt.Fail(ex.Operation, evtMsgs); - } - } - - return OperationResult.Attempt.Succeed(MoveOperationStatusType.Success, evtMsgs); - } - - /// - /// Saves an - /// - /// to save - /// Id of the user issueing the save - public void Save(IDataTypeDefinition dataTypeDefinition, int userId = 0) - { - dataTypeDefinition.CreatorId = userId; - - using (var scope = ScopeProvider.CreateScope()) - { - var saveEventArgs = new SaveEventArgs(dataTypeDefinition); - if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs)) - { - scope.Complete(); - return; - } - - if (string.IsNullOrWhiteSpace(dataTypeDefinition.Name)) - { - throw new ArgumentException("Cannot save datatype with empty name."); - } - - _dataTypeDefinitionRepository.Save(dataTypeDefinition); - - saveEventArgs.CanCancel = false; - scope.Events.Dispatch(Saved, this, saveEventArgs); - Audit(AuditType.Save, "Save DataTypeDefinition performed by user", userId, dataTypeDefinition.Id); - scope.Complete(); - } - } - - /// - /// Saves a collection of - /// - /// to save - /// Id of the user issueing the save - public void Save(IEnumerable dataTypeDefinitions, int userId = 0) - { - Save(dataTypeDefinitions, userId, true); - } - - /// - /// Saves a collection of - /// - /// to save - /// Id of the user issueing the save - /// Boolean indicating whether or not to raise events - public void Save(IEnumerable dataTypeDefinitions, int userId, bool raiseEvents) - { - var dataTypeDefinitionsA = dataTypeDefinitions.ToArray(); - var saveEventArgs = new SaveEventArgs(dataTypeDefinitionsA); - - using (var scope = ScopeProvider.CreateScope()) - { - if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs)) - { - scope.Complete(); - return; - } - - foreach (var dataTypeDefinition in dataTypeDefinitionsA) - { - dataTypeDefinition.CreatorId = userId; - _dataTypeDefinitionRepository.Save(dataTypeDefinition); - } - - if (raiseEvents) - { - saveEventArgs.CanCancel = false; - scope.Events.Dispatch(Saved, this, saveEventArgs); - } - Audit(AuditType.Save, "Save DataTypeDefinition performed by user", userId, -1); - - scope.Complete(); - } - } - - /// - /// Saves a list of PreValues for a given DataTypeDefinition - /// - /// Id of the DataTypeDefinition to save PreValues for - /// List of string values to save - [Obsolete("This should no longer be used, use the alternative SavePreValues or SaveDataTypeAndPreValues methods instead. This will only insert pre-values without keys")] - public void SavePreValues(int dataTypeId, IEnumerable values) - { - //TODO: Should we raise an event here since we are really saving values for the data type? - - using (var scope = ScopeProvider.CreateScope()) - { - var sortOrderObj = scope.Database.ExecuteScalar( - "SELECT max(sortorder) FROM cmsDataTypePreValues WHERE datatypeNodeId = @DataTypeId", new { DataTypeId = dataTypeId }); - - if (sortOrderObj == null || int.TryParse(sortOrderObj.ToString(), out int sortOrder) == false) - sortOrder = 1; - - foreach (var value in values) - { - var dto = new DataTypePreValueDto { DataTypeNodeId = dataTypeId, Value = value, SortOrder = sortOrder }; - scope.Database.Insert(dto); - sortOrder++; - } - - scope.Complete(); - } - } - - /// - /// Saves/updates the pre-values - /// - /// - /// - /// - /// We need to actually look up each pre-value and maintain it's id if possible - this is because of silly property editors - /// like 'dropdown list publishing keys' - /// - public void SavePreValues(int dataTypeId, IDictionary values) - { - var dtd = GetDataTypeDefinitionById(dataTypeId); - if (dtd == null) - throw new InvalidOperationException("No data type found for id " + dataTypeId); - - SavePreValues(dtd, values); - } - - /// - /// Saves/updates the pre-values - /// - /// - /// - /// - /// We need to actually look up each pre-value and maintain it's id if possible - this is because of silly property editors - /// like 'dropdown list publishing keys' - /// - public void SavePreValues(IDataTypeDefinition dataTypeDefinition, IDictionary values) - { - //TODO: Should we raise an event here since we are really saving values for the data type? - - using (var scope = ScopeProvider.CreateScope()) - { - _dataTypeDefinitionRepository.AddOrUpdatePreValues(dataTypeDefinition, values); - scope.Complete(); - } - } - - /// - /// This will save a data type and it's pre-values in one transaction - /// - /// - /// - /// - public void SaveDataTypeAndPreValues(IDataTypeDefinition dataTypeDefinition, IDictionary values, int userId = 0) - { - using (var scope = ScopeProvider.CreateScope()) - { - var saveEventArgs = new SaveEventArgs(dataTypeDefinition); - if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs)) - { - scope.Complete(); - return; - } - - // if preValues contain the data type, override the data type definition accordingly - if (values != null && values.ContainsKey(Constants.PropertyEditors.PreValueKeys.DataValueType)) - dataTypeDefinition.DatabaseType = PropertyValueEditor.GetDatabaseType(values[Constants.PropertyEditors.PreValueKeys.DataValueType].Value); - - dataTypeDefinition.CreatorId = userId; - - _dataTypeDefinitionRepository.Save(dataTypeDefinition); // definition - _dataTypeDefinitionRepository.AddOrUpdatePreValues(dataTypeDefinition, values); //prevalues - - saveEventArgs.CanCancel = false; - scope.Events.Dispatch(Saved, this, saveEventArgs); - Audit(AuditType.Save, "Save DataTypeDefinition performed by user", userId, dataTypeDefinition.Id); - - scope.Complete(); - } - } - - /// - /// Deletes an - /// - /// - /// Please note that deleting a will remove - /// all the data that references this . - /// - /// to delete - /// Optional Id of the user issueing the deletion - public void Delete(IDataTypeDefinition dataTypeDefinition, int userId = 0) - { - using (var scope = ScopeProvider.CreateScope()) - { - var deleteEventArgs = new DeleteEventArgs(dataTypeDefinition); - if (scope.Events.DispatchCancelable(Deleting, this, deleteEventArgs)) - { - scope.Complete(); - return; - } - - - // find ContentTypes using this IDataTypeDefinition on a PropertyType, and delete - // fixme - media and members?! - // fixme - non-group properties?! - var query = Query().Where(x => x.DataTypeDefinitionId == dataTypeDefinition.Id); - var contentTypes = _contentTypeRepository.GetByQuery(query); - foreach (var contentType in contentTypes) - { - foreach (var propertyGroup in contentType.PropertyGroups) - { - var types = propertyGroup.PropertyTypes.Where(x => x.DataTypeDefinitionId == dataTypeDefinition.Id).ToList(); - foreach (var propertyType in types) - { - propertyGroup.PropertyTypes.Remove(propertyType); - } - } - - // so... we are modifying content types here. the service will trigger Deleted event, - // which will propagate to DataTypeCacheRefresher which will clear almost every cache - // there is to clear... and in addition published snapshot caches will clear themselves too, so - // this is probably safe alghough it looks... weird. - // - // what IS weird is that a content type is losing a property and we do NOT raise any - // content type event... so ppl better listen on the data type events too. - - _contentTypeRepository.Save(contentType); - } - - _dataTypeDefinitionRepository.Delete(dataTypeDefinition); - - deleteEventArgs.CanCancel = false; - scope.Events.Dispatch(Deleted, this, deleteEventArgs); - Audit(AuditType.Delete, "Delete DataTypeDefinition performed by user", userId, dataTypeDefinition.Id); - - scope.Complete(); - } - } - - private void Audit(AuditType type, string message, int userId, int objectId) - { - _auditRepository.Save(new AuditItem(objectId, message, type, userId)); - } - - #region Event Handlers - - public static event TypedEventHandler> SavingContainer; - public static event TypedEventHandler> SavedContainer; - public static event TypedEventHandler> DeletingContainer; - public static event TypedEventHandler> DeletedContainer; - - /// - /// Occurs before Delete - /// - public static event TypedEventHandler> Deleting; - - /// - /// Occurs after Delete - /// - public static event TypedEventHandler> Deleted; - - /// - /// Occurs before Save - /// - public static event TypedEventHandler> Saving; - - /// - /// Occurs after Save - /// - public static event TypedEventHandler> Saved; - - /// - /// Occurs before Move - /// - public static event TypedEventHandler> Moving; - - /// - /// Occurs after Move - /// - public static event TypedEventHandler> Moved; - #endregion - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Events; +using Umbraco.Core.Exceptions; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence.Dtos; +using Umbraco.Core.Persistence.Repositories; +using Umbraco.Core.Persistence.Repositories.Implement; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Scoping; + +namespace Umbraco.Core.Services.Implement +{ + /// + /// Represents the DataType Service, which is an easy access to operations involving + /// + internal class DataTypeService : ScopeRepositoryService, IDataTypeService + { + private readonly IDataTypeDefinitionRepository _dataTypeDefinitionRepository; + private readonly IDataTypeContainerRepository _dataTypeContainerRepository; + private readonly IContentTypeRepository _contentTypeRepository; + private readonly IAuditRepository _auditRepository; + private readonly IEntityRepository _entityRepository; + + public DataTypeService(IScopeProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory, + IDataTypeDefinitionRepository dataTypeDefinitionRepository, IDataTypeContainerRepository dataTypeContainerRepository, + IAuditRepository auditRepository, IEntityRepository entityRepository, IContentTypeRepository contentTypeRepository) + : base(provider, logger, eventMessagesFactory) + { + _dataTypeDefinitionRepository = dataTypeDefinitionRepository; + _dataTypeContainerRepository = dataTypeContainerRepository; + _auditRepository = auditRepository; + _entityRepository = entityRepository; + _contentTypeRepository = contentTypeRepository; + } + + #region Containers + + public Attempt> CreateContainer(int parentId, string name, int userId = 0) + { + var evtMsgs = EventMessagesFactory.Get(); + using (var scope = ScopeProvider.CreateScope()) + { + try + { + var container = new EntityContainer(Constants.ObjectTypes.DataType) + { + Name = name, + ParentId = parentId, + CreatorId = userId + }; + + if (scope.Events.DispatchCancelable(SavingContainer, this, new SaveEventArgs(container, evtMsgs))) + { + scope.Complete(); + return OperationResult.Attempt.Cancel(evtMsgs, container); + } + + _dataTypeContainerRepository.Save(container); + scope.Complete(); + + scope.Events.Dispatch(SavedContainer, this, new SaveEventArgs(container, evtMsgs)); + //TODO: Audit trail ? + + return OperationResult.Attempt.Succeed(evtMsgs, container); + } + catch (Exception ex) + { + return OperationResult.Attempt.Fail(evtMsgs, ex); + } + } + } + + public EntityContainer GetContainer(int containerId) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + return _dataTypeContainerRepository.Get(containerId); + } + } + + public EntityContainer GetContainer(Guid containerId) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + return ((EntityContainerRepository) _dataTypeContainerRepository).Get(containerId); + } + } + + public IEnumerable GetContainers(string name, int level) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + return ((EntityContainerRepository) _dataTypeContainerRepository).Get(name, level); + } + } + + public IEnumerable GetContainers(IDataTypeDefinition dataTypeDefinition) + { + var ancestorIds = dataTypeDefinition.Path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(x => + { + var asInt = x.TryConvertTo(); + return asInt ? asInt.Result : int.MinValue; + }) + .Where(x => x != int.MinValue && x != dataTypeDefinition.Id) + .ToArray(); + + return GetContainers(ancestorIds); + } + + public IEnumerable GetContainers(int[] containerIds) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + return _dataTypeContainerRepository.GetMany(containerIds); + } + } + + public Attempt SaveContainer(EntityContainer container, int userId = 0) + { + var evtMsgs = EventMessagesFactory.Get(); + + if (container.ContainedObjectType != Constants.ObjectTypes.DataType) + { + var ex = new InvalidOperationException("Not a " + Constants.ObjectTypes.DataType + " container."); + return OperationResult.Attempt.Fail(evtMsgs, ex); + } + + if (container.HasIdentity && container.IsPropertyDirty("ParentId")) + { + var ex = new InvalidOperationException("Cannot save a container with a modified parent, move the container instead."); + return OperationResult.Attempt.Fail(evtMsgs, ex); + } + + using (var scope = ScopeProvider.CreateScope()) + { + if (scope.Events.DispatchCancelable(SavingContainer, this, new SaveEventArgs(container, evtMsgs))) + { + scope.Complete(); + return OperationResult.Attempt.Cancel(evtMsgs); + } + + _dataTypeContainerRepository.Save(container); + + scope.Events.Dispatch(SavedContainer, this, new SaveEventArgs(container, evtMsgs)); + scope.Complete(); + } + + //TODO: Audit trail ? + return OperationResult.Attempt.Succeed(evtMsgs); + } + + public Attempt DeleteContainer(int containerId, int userId = 0) + { + var evtMsgs = EventMessagesFactory.Get(); + using (var scope = ScopeProvider.CreateScope()) + { + var container = _dataTypeContainerRepository.Get(containerId); + if (container == null) return OperationResult.Attempt.NoOperation(evtMsgs); + + var entity = _entityRepository.Get(container.Id); + if (entity.HasChildren()) // because container.HasChildren() does not work? + return Attempt.Fail(new OperationResult(OperationResultType.FailedCannot, evtMsgs)); // causes rollback + + if (scope.Events.DispatchCancelable(DeletingContainer, this, new DeleteEventArgs(container, evtMsgs))) + { + scope.Complete(); + return Attempt.Fail(new OperationResult(OperationResultType.FailedCancelledByEvent, evtMsgs)); + } + + _dataTypeContainerRepository.Delete(container); + + scope.Events.Dispatch(DeletedContainer, this, new DeleteEventArgs(container, evtMsgs)); + scope.Complete(); + } + + //TODO: Audit trail ? + return OperationResult.Attempt.Succeed(evtMsgs); + } + + public Attempt> RenameContainer(int id, string name, int userId = 0) + { + var evtMsgs = EventMessagesFactory.Get(); + using (var scope = ScopeProvider.CreateScope()) + { + try + { + var container = _dataTypeContainerRepository.Get(id); + + //throw if null, this will be caught by the catch and a failed returned + if (container == null) + throw new InvalidOperationException("No container found with id " + id); + + container.Name = name; + + _dataTypeContainerRepository.Save(container); + scope.Complete(); + + // fixme - triggering SavedContainer with a different name?! + scope.Events.Dispatch(SavedContainer, this, new SaveEventArgs(container, evtMsgs), "RenamedContainer"); + + return OperationResult.Attempt.Succeed(OperationResultType.Success, evtMsgs, container); + } + catch (Exception ex) + { + return OperationResult.Attempt.Fail(evtMsgs, ex); + } + } + } + + #endregion + + /// + /// Gets a by its Name + /// + /// Name of the + /// + public IDataTypeDefinition GetDataTypeDefinitionByName(string name) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + return _dataTypeDefinitionRepository.Get(Query().Where(x => x.Name == name)).FirstOrDefault(); + } + } + + /// + /// Gets a by its Id + /// + /// Id of the + /// + public IDataTypeDefinition GetDataTypeDefinitionById(int id) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + return _dataTypeDefinitionRepository.Get(id); + } + } + + /// + /// Gets a by its unique guid Id + /// + /// Unique guid Id of the DataType + /// + public IDataTypeDefinition GetDataTypeDefinitionById(Guid id) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + var query = Query().Where(x => x.Key == id); + return _dataTypeDefinitionRepository.Get(query).FirstOrDefault(); + } + } + + /// + /// Gets a by its control Id + /// + /// Alias of the property editor + /// Collection of objects with a matching contorl id + public IEnumerable GetDataTypeDefinitionByPropertyEditorAlias(string propertyEditorAlias) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + var query = Query().Where(x => x.PropertyEditorAlias == propertyEditorAlias); + return _dataTypeDefinitionRepository.Get(query); + } + } + + /// + /// Gets all objects or those with the ids passed in + /// + /// Optional array of Ids + /// An enumerable list of objects + public IEnumerable GetAllDataTypeDefinitions(params int[] ids) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + return _dataTypeDefinitionRepository.GetMany(ids); + } + } + + /// + /// Gets all prevalues for an + /// + /// Id of the to retrieve prevalues from + /// An enumerable list of string values + public IEnumerable GetPreValuesByDataTypeId(int id) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + var collection = _dataTypeDefinitionRepository.GetPreValuesCollectionByDataTypeId(id); + //now convert the collection to a string list + return collection.FormatAsDictionary() + .Select(x => x.Value.Value) + .ToList(); + } + } + + /// + /// Returns the PreValueCollection for the specified data type + /// + /// + /// + public PreValueCollection GetPreValuesCollectionByDataTypeId(int id) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + return _dataTypeDefinitionRepository.GetPreValuesCollectionByDataTypeId(id); + } + } + + /// + /// Gets a specific PreValue by its Id + /// + /// Id of the PreValue to retrieve the value from + /// PreValue as a string + public string GetPreValueAsString(int id) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + return _dataTypeDefinitionRepository.GetPreValueAsString(id); + } + } + + public Attempt> Move(IDataTypeDefinition toMove, int parentId) + { + var evtMsgs = EventMessagesFactory.Get(); + var moveInfo = new List>(); + + using (var scope = ScopeProvider.CreateScope()) + { + var moveEventInfo = new MoveEventInfo(toMove, toMove.Path, parentId); + var moveEventArgs = new MoveEventArgs(evtMsgs, moveEventInfo); + if (scope.Events.DispatchCancelable(Moving, this, moveEventArgs)) + { + scope.Complete(); + return OperationResult.Attempt.Fail(MoveOperationStatusType.FailedCancelledByEvent, evtMsgs); + } + + try + { + EntityContainer container = null; + if (parentId > 0) + { + container = _dataTypeContainerRepository.Get(parentId); + if (container == null) + throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); // causes rollback + } + moveInfo.AddRange(_dataTypeDefinitionRepository.Move(toMove, container)); + + moveEventArgs.MoveInfoCollection = moveInfo; + moveEventArgs.CanCancel = false; + scope.Events.Dispatch(Moved, this, moveEventArgs); + scope.Complete(); + } + catch (DataOperationException ex) + { + scope.Complete(); // fixme what are we doing here exactly? + return OperationResult.Attempt.Fail(ex.Operation, evtMsgs); + } + } + + return OperationResult.Attempt.Succeed(MoveOperationStatusType.Success, evtMsgs); + } + + /// + /// Saves an + /// + /// to save + /// Id of the user issueing the save + public void Save(IDataTypeDefinition dataTypeDefinition, int userId = 0) + { + dataTypeDefinition.CreatorId = userId; + + using (var scope = ScopeProvider.CreateScope()) + { + var saveEventArgs = new SaveEventArgs(dataTypeDefinition); + if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs)) + { + scope.Complete(); + return; + } + + if (string.IsNullOrWhiteSpace(dataTypeDefinition.Name)) + { + throw new ArgumentException("Cannot save datatype with empty name."); + } + + _dataTypeDefinitionRepository.Save(dataTypeDefinition); + + saveEventArgs.CanCancel = false; + scope.Events.Dispatch(Saved, this, saveEventArgs); + Audit(AuditType.Save, "Save DataTypeDefinition performed by user", userId, dataTypeDefinition.Id); + scope.Complete(); + } + } + + /// + /// Saves a collection of + /// + /// to save + /// Id of the user issueing the save + public void Save(IEnumerable dataTypeDefinitions, int userId = 0) + { + Save(dataTypeDefinitions, userId, true); + } + + /// + /// Saves a collection of + /// + /// to save + /// Id of the user issueing the save + /// Boolean indicating whether or not to raise events + public void Save(IEnumerable dataTypeDefinitions, int userId, bool raiseEvents) + { + var dataTypeDefinitionsA = dataTypeDefinitions.ToArray(); + var saveEventArgs = new SaveEventArgs(dataTypeDefinitionsA); + + using (var scope = ScopeProvider.CreateScope()) + { + if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs)) + { + scope.Complete(); + return; + } + + foreach (var dataTypeDefinition in dataTypeDefinitionsA) + { + dataTypeDefinition.CreatorId = userId; + _dataTypeDefinitionRepository.Save(dataTypeDefinition); + } + + if (raiseEvents) + { + saveEventArgs.CanCancel = false; + scope.Events.Dispatch(Saved, this, saveEventArgs); + } + Audit(AuditType.Save, "Save DataTypeDefinition performed by user", userId, -1); + + scope.Complete(); + } + } + + /// + /// Saves a list of PreValues for a given DataTypeDefinition + /// + /// Id of the DataTypeDefinition to save PreValues for + /// List of string values to save + [Obsolete("This should no longer be used, use the alternative SavePreValues or SaveDataTypeAndPreValues methods instead. This will only insert pre-values without keys")] + public void SavePreValues(int dataTypeId, IEnumerable values) + { + //TODO: Should we raise an event here since we are really saving values for the data type? + + using (var scope = ScopeProvider.CreateScope()) + { + var sortOrderObj = scope.Database.ExecuteScalar( + "SELECT max(sortorder) FROM cmsDataTypePreValues WHERE datatypeNodeId = @DataTypeId", new { DataTypeId = dataTypeId }); + + if (sortOrderObj == null || int.TryParse(sortOrderObj.ToString(), out int sortOrder) == false) + sortOrder = 1; + + foreach (var value in values) + { + var dto = new DataTypePreValueDto { DataTypeNodeId = dataTypeId, Value = value, SortOrder = sortOrder }; + scope.Database.Insert(dto); + sortOrder++; + } + + scope.Complete(); + } + } + + /// + /// Saves/updates the pre-values + /// + /// + /// + /// + /// We need to actually look up each pre-value and maintain it's id if possible - this is because of silly property editors + /// like 'dropdown list publishing keys' + /// + public void SavePreValues(int dataTypeId, IDictionary values) + { + var dtd = GetDataTypeDefinitionById(dataTypeId); + if (dtd == null) + throw new InvalidOperationException("No data type found for id " + dataTypeId); + + SavePreValues(dtd, values); + } + + /// + /// Saves/updates the pre-values + /// + /// + /// + /// + /// We need to actually look up each pre-value and maintain it's id if possible - this is because of silly property editors + /// like 'dropdown list publishing keys' + /// + public void SavePreValues(IDataTypeDefinition dataTypeDefinition, IDictionary values) + { + //TODO: Should we raise an event here since we are really saving values for the data type? + + using (var scope = ScopeProvider.CreateScope()) + { + _dataTypeDefinitionRepository.AddOrUpdatePreValues(dataTypeDefinition, values); + scope.Complete(); + } + } + + /// + /// This will save a data type and it's pre-values in one transaction + /// + /// + /// + /// + public void SaveDataTypeAndPreValues(IDataTypeDefinition dataTypeDefinition, IDictionary values, int userId = 0) + { + using (var scope = ScopeProvider.CreateScope()) + { + var saveEventArgs = new SaveEventArgs(dataTypeDefinition); + if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs)) + { + scope.Complete(); + return; + } + + // if preValues contain the data type, override the data type definition accordingly + if (values != null && values.ContainsKey(Constants.PropertyEditors.PreValueKeys.DataValueType)) + dataTypeDefinition.DatabaseType = PropertyValueEditor.GetDatabaseType(values[Constants.PropertyEditors.PreValueKeys.DataValueType].Value); + + dataTypeDefinition.CreatorId = userId; + + _dataTypeDefinitionRepository.Save(dataTypeDefinition); // definition + _dataTypeDefinitionRepository.AddOrUpdatePreValues(dataTypeDefinition, values); //prevalues + + saveEventArgs.CanCancel = false; + scope.Events.Dispatch(Saved, this, saveEventArgs); + Audit(AuditType.Save, "Save DataTypeDefinition performed by user", userId, dataTypeDefinition.Id); + + scope.Complete(); + } + } + + /// + /// Deletes an + /// + /// + /// Please note that deleting a will remove + /// all the data that references this . + /// + /// to delete + /// Optional Id of the user issueing the deletion + public void Delete(IDataTypeDefinition dataTypeDefinition, int userId = 0) + { + using (var scope = ScopeProvider.CreateScope()) + { + var deleteEventArgs = new DeleteEventArgs(dataTypeDefinition); + if (scope.Events.DispatchCancelable(Deleting, this, deleteEventArgs)) + { + scope.Complete(); + return; + } + + + // find ContentTypes using this IDataTypeDefinition on a PropertyType, and delete + // fixme - media and members?! + // fixme - non-group properties?! + var query = Query().Where(x => x.DataTypeDefinitionId == dataTypeDefinition.Id); + var contentTypes = _contentTypeRepository.GetByQuery(query); + foreach (var contentType in contentTypes) + { + foreach (var propertyGroup in contentType.PropertyGroups) + { + var types = propertyGroup.PropertyTypes.Where(x => x.DataTypeDefinitionId == dataTypeDefinition.Id).ToList(); + foreach (var propertyType in types) + { + propertyGroup.PropertyTypes.Remove(propertyType); + } + } + + // so... we are modifying content types here. the service will trigger Deleted event, + // which will propagate to DataTypeCacheRefresher which will clear almost every cache + // there is to clear... and in addition published snapshot caches will clear themselves too, so + // this is probably safe alghough it looks... weird. + // + // what IS weird is that a content type is losing a property and we do NOT raise any + // content type event... so ppl better listen on the data type events too. + + _contentTypeRepository.Save(contentType); + } + + _dataTypeDefinitionRepository.Delete(dataTypeDefinition); + + deleteEventArgs.CanCancel = false; + scope.Events.Dispatch(Deleted, this, deleteEventArgs); + Audit(AuditType.Delete, "Delete DataTypeDefinition performed by user", userId, dataTypeDefinition.Id); + + scope.Complete(); + } + } + + private void Audit(AuditType type, string message, int userId, int objectId) + { + _auditRepository.Save(new AuditItem(objectId, message, type, userId)); + } + + #region Event Handlers + + public static event TypedEventHandler> SavingContainer; + public static event TypedEventHandler> SavedContainer; + public static event TypedEventHandler> DeletingContainer; + public static event TypedEventHandler> DeletedContainer; + + /// + /// Occurs before Delete + /// + public static event TypedEventHandler> Deleting; + + /// + /// Occurs after Delete + /// + public static event TypedEventHandler> Deleted; + + /// + /// Occurs before Save + /// + public static event TypedEventHandler> Saving; + + /// + /// Occurs after Save + /// + public static event TypedEventHandler> Saved; + + /// + /// Occurs before Move + /// + public static event TypedEventHandler> Moving; + + /// + /// Occurs after Move + /// + public static event TypedEventHandler> Moved; + #endregion + } +} diff --git a/src/Umbraco.Core/Services/DomainService.cs b/src/Umbraco.Core/Services/Implement/DomainService.cs similarity index 99% rename from src/Umbraco.Core/Services/DomainService.cs rename to src/Umbraco.Core/Services/Implement/DomainService.cs index 2688b6b452..32decb56d5 100644 --- a/src/Umbraco.Core/Services/DomainService.cs +++ b/src/Umbraco.Core/Services/Implement/DomainService.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Scoping; -namespace Umbraco.Core.Services +namespace Umbraco.Core.Services.Implement { public class DomainService : ScopeRepositoryService, IDomainService { diff --git a/src/Umbraco.Core/Services/EntityService.cs b/src/Umbraco.Core/Services/Implement/EntityService.cs similarity index 97% rename from src/Umbraco.Core/Services/EntityService.cs rename to src/Umbraco.Core/Services/Implement/EntityService.cs index 141c2b6395..e1190701d5 100644 --- a/src/Umbraco.Core/Services/EntityService.cs +++ b/src/Umbraco.Core/Services/Implement/EntityService.cs @@ -1,730 +1,729 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NPoco; -using System.Linq.Expressions; -using System.Text; -using Umbraco.Core.Cache; -using Umbraco.Core.CodeAnnotations; -using Umbraco.Core.Events; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Core.Models.EntityBase; -using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.DatabaseModelDefinitions; -using Umbraco.Core.Persistence.Dtos; -using Umbraco.Core.Persistence.Querying; -using Umbraco.Core.Persistence.Repositories; -using Umbraco.Core.Scoping; - -namespace Umbraco.Core.Services -{ - public class EntityService : ScopeRepositoryService, IEntityService - { - private readonly IEntityRepository _entityRepository; - private readonly Dictionary>> _supportedObjectTypes; - private IQuery _queryRootEntity; - private readonly IdkMap _idkMap; - - public EntityService(IScopeProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory, - IContentService contentService, IContentTypeService contentTypeService, - IMediaService mediaService, IMediaTypeService mediaTypeService, - IDataTypeService dataTypeService, - IMemberService memberService, IMemberTypeService memberTypeService, IdkMap idkMap, - IRuntimeCacheProvider runtimeCache, - IEntityRepository entityRepository) - : base(provider, logger, eventMessagesFactory) - { - _idkMap = idkMap; - _entityRepository = entityRepository; - - _supportedObjectTypes = new Dictionary>> - { - {typeof (IDataTypeDefinition).FullName, new Tuple>(UmbracoObjectTypes.DataType, dataTypeService.GetDataTypeDefinitionById)}, - {typeof (IContent).FullName, new Tuple>(UmbracoObjectTypes.Document, contentService.GetById)}, - {typeof (IContentType).FullName, new Tuple>(UmbracoObjectTypes.DocumentType, contentTypeService.Get)}, - {typeof (IMedia).FullName, new Tuple>(UmbracoObjectTypes.Media, mediaService.GetById)}, - {typeof (IMediaType).FullName, new Tuple>(UmbracoObjectTypes.MediaType, mediaTypeService.Get)}, - {typeof (IMember).FullName, new Tuple>(UmbracoObjectTypes.Member, memberService.GetById)}, - {typeof (IMemberType).FullName, new Tuple>(UmbracoObjectTypes.MemberType, memberTypeService.Get)}, - }; - } - - #region Static Queries - - // lazy-constructed because when the ctor runs, the query factory may not be ready - - private IQuery QueryRootEntity => _queryRootEntity - ?? (_queryRootEntity = Query().Where(x => x.ParentId == -1)); - - #endregion - - /// - /// Returns the integer id for a given GUID - /// - /// - /// - /// - public Attempt GetIdForKey(Guid key, UmbracoObjectTypes umbracoObjectType) - { - return _idkMap.GetIdForKey(key, umbracoObjectType); - } - - public Attempt GetIdForUdi(Udi udi) - { - return _idkMap.GetIdForUdi(udi); - } - - /// - /// Returns the GUID for a given integer id - /// - /// - /// - /// - public Attempt GetKeyForId(int id, UmbracoObjectTypes umbracoObjectType) - { - return _idkMap.GetKeyForId(id, umbracoObjectType); - } - - public IUmbracoEntity GetByKey(Guid key, bool loadBaseType = true) - { - if (loadBaseType) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - return _entityRepository.GetByKey(key); - } - } - - //SD: TODO: Need to enable this at some stage ... just need to ask Morten what the deal is with what this does. - throw new NotSupportedException(); - - //var objectType = GetObjectType(key); - //var entityType = GetEntityType(objectType); - //var typeFullName = entityType.FullName; - //var entity = _supportedObjectTypes[typeFullName].Item2(id); - - //return entity; - } - - /// - /// Gets an UmbracoEntity by its Id, and optionally loads the complete object graph. - /// - /// - /// By default this will load the base type with a minimum set of properties. - /// - /// Id of the object to retrieve - /// Optional bool to load the complete object graph when set to False. - /// An - public virtual IUmbracoEntity Get(int id, bool loadBaseType = true) - { - if (loadBaseType) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - return _entityRepository.Get(id); - } - } - - var objectType = GetObjectType(id); - var entityType = GetEntityType(objectType); - var typeFullName = entityType.FullName; - var entity = _supportedObjectTypes[typeFullName].Item2(id); - - return entity; - } - - public IUmbracoEntity GetByKey(Guid key, UmbracoObjectTypes umbracoObjectType, bool loadBaseType = true) - { - if (loadBaseType) - { - var objectTypeId = umbracoObjectType.GetGuid(); - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - return _entityRepository.GetByKey(key, objectTypeId); - } - } - - //SD: TODO: Need to enable this at some stage ... just need to ask Morten what the deal is with what this does. - throw new NotSupportedException(); - - //var entityType = GetEntityType(umbracoObjectType); - //var typeFullName = entityType.FullName; - //var entity = _supportedObjectTypes[typeFullName].Item2(id); - - //return entity; - } - - /// - /// Gets an UmbracoEntity by its Id and UmbracoObjectType, and optionally loads the complete object graph. - /// - /// - /// By default this will load the base type with a minimum set of properties. - /// - /// Id of the object to retrieve - /// UmbracoObjectType of the entity to retrieve - /// Optional bool to load the complete object graph when set to False. - /// An - public virtual IUmbracoEntity Get(int id, UmbracoObjectTypes umbracoObjectType, bool loadBaseType = true) - { - if (loadBaseType) - { - var objectTypeId = umbracoObjectType.GetGuid(); - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - return _entityRepository.Get(id, objectTypeId); - } - } - - var entityType = GetEntityType(umbracoObjectType); - var typeFullName = entityType.FullName; - var entity = _supportedObjectTypes[typeFullName].Item2(id); - - return entity; - } - - public IUmbracoEntity GetByKey(Guid key, bool loadBaseType = true) where T : IUmbracoEntity - { - throw new NotImplementedException(); - } - - /// - /// Gets an UmbracoEntity by its Id and specified Type. Optionally loads the complete object graph. - /// - /// - /// By default this will load the base type with a minimum set of properties. - /// - /// Type of the model to retrieve. Must be based on an - /// Id of the object to retrieve - /// Optional bool to load the complete object graph when set to False. - /// An - public virtual IUmbracoEntity Get(int id, bool loadBaseType = true) where T : IUmbracoEntity - { - if (loadBaseType) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - return _entityRepository.Get(id); - } - } - - var typeFullName = typeof(T).FullName; - if (_supportedObjectTypes.ContainsKey(typeFullName) == false) - throw new NotSupportedException("The passed in type is not supported"); - var entity = _supportedObjectTypes[typeFullName].Item2(id); - - return entity; - } - - /// - /// Gets the parent of entity by its id - /// - /// Id of the entity to retrieve the Parent for - /// An - public virtual IUmbracoEntity GetParent(int id) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - var entity = _entityRepository.Get(id); - if (entity.ParentId == -1 || entity.ParentId == -20 || entity.ParentId == -21) - return null; - - return _entityRepository.Get(entity.ParentId); - } - } - - /// - /// Gets the parent of entity by its id and UmbracoObjectType - /// - /// Id of the entity to retrieve the Parent for - /// UmbracoObjectType of the parent to retrieve - /// An - public virtual IUmbracoEntity GetParent(int id, UmbracoObjectTypes umbracoObjectType) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - var entity = _entityRepository.Get(id); - if (entity.ParentId == -1 || entity.ParentId == -20 || entity.ParentId == -21) - return null; - - var objectTypeId = umbracoObjectType.GetGuid(); - return _entityRepository.Get(entity.ParentId, objectTypeId); - } - } - - /// - /// Gets a collection of children by the parents Id - /// - /// Id of the parent to retrieve children for - /// An enumerable list of objects - public virtual IEnumerable GetChildren(int parentId) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - var query = Query().Where(x => x.ParentId == parentId); - return _entityRepository.GetByQuery(query); - } - } - - /// - /// Gets a collection of children by the parents Id and UmbracoObjectType - /// - /// Id of the parent to retrieve children for - /// UmbracoObjectType of the children to retrieve - /// An enumerable list of objects - public virtual IEnumerable GetChildren(int parentId, UmbracoObjectTypes umbracoObjectType) - { - var objectTypeId = umbracoObjectType.GetGuid(); - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - var query = Query().Where(x => x.ParentId == parentId); - return _entityRepository.GetByQuery(query, objectTypeId).ToList(); // run within using! // run within using! - } - } - - /// - /// Gets a collection of descendents by the parents Id - /// - /// Id of entity to retrieve descendents for - /// An enumerable list of objects - public virtual IEnumerable GetDescendents(int id) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - var entity = _entityRepository.Get(id); - var pathMatch = entity.Path + ","; - var query = Query().Where(x => x.Path.StartsWith(pathMatch) && x.Id != id); - return _entityRepository.GetByQuery(query); - } - } - - /// - /// Gets a collection of descendents by the parents Id - /// - /// Id of entity to retrieve descendents for - /// UmbracoObjectType of the descendents to retrieve - /// An enumerable list of objects - public virtual IEnumerable GetDescendents(int id, UmbracoObjectTypes umbracoObjectType) - { - var objectTypeId = umbracoObjectType.GetGuid(); - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - var entity = _entityRepository.Get(id); - var query = Query().Where(x => x.Path.StartsWith(entity.Path) && x.Id != id); - return _entityRepository.GetByQuery(query, objectTypeId); - } - } - - /// - /// Returns a paged collection of children - /// - /// The parent id to return children for - /// - /// - /// - /// - /// - /// - /// - /// - public IEnumerable GetPagedChildren(int parentId, UmbracoObjectTypes umbracoObjectType, long pageIndex, int pageSize, out long totalRecords, - string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, string filter = "") - { - var objectTypeId = umbracoObjectType.GetGuid(); - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - var query = Query().Where(x => x.ParentId == parentId && x.Trashed == false); - - IQuery filterQuery = null; - if (filter.IsNullOrWhiteSpace() == false) - { - filterQuery = Query().Where(x => x.Name.Contains(filter)); - } - - var contents = _entityRepository.GetPagedResultsByQuery(query, objectTypeId, pageIndex, pageSize, out totalRecords, orderBy, orderDirection, filterQuery); - return contents; - } - } - - /// - /// Returns a paged collection of descendants - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public IEnumerable GetPagedDescendants(int id, UmbracoObjectTypes umbracoObjectType, long pageIndex, int pageSize, out long totalRecords, - string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = "") - { - var objectTypeId = umbracoObjectType.GetGuid(); - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - var query = Query(); - //if the id is System Root, then just get all - //if the id is System Root, then just get all - - if (id != Constants.System.Root) - { - //lookup the path so we can use it in the prefix query below - var itemPaths = _entityRepository.GetAllPaths(objectTypeId, id).ToArray(); - if (itemPaths.Length == 0) - { - totalRecords = 0; - return Enumerable.Empty(); - } - var itemPath = itemPaths[0].Path; - - query.Where(x => x.Path.SqlStartsWith(itemPath + ",", TextColumnType.NVarchar)); - } - IQuery filterQuery = null; - - if (filter.IsNullOrWhiteSpace() == false) - { - filterQuery = Query().Where(x => x.Name.Contains(filter)); - } - var contents = _entityRepository.GetPagedResultsByQuery(query, objectTypeId, pageIndex, pageSize, out totalRecords, orderBy, orderDirection, filterQuery); - return contents; - } - } - /// - /// Returns a paged collection of descendants. - /// - public IEnumerable GetPagedDescendants(IEnumerable ids, UmbracoObjectTypes umbracoObjectType, long pageIndex, int pageSize, out long totalRecords, - string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = "") - { - totalRecords = 0; - - var idsA = ids.ToArray(); - if (idsA.Length == 0) - return Enumerable.Empty(); - - var objectTypeId = umbracoObjectType.GetGuid(); - - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - var query = Query(); - if (idsA.All(x => x != Constants.System.Root)) - { - //lookup the paths so we can use it in the prefix query below - //lookup the paths so we can use it in the prefix query below - var itemPaths = _entityRepository.GetAllPaths(objectTypeId, idsA).ToArray(); - - if (itemPaths.Length == 0) - { - totalRecords = 0; - return Enumerable.Empty(); - } - var clauses = new List>>(); - foreach (var id in idsA) - { - //if the id is root then don't add any clauses - if (id != Constants.System.Root) - { - var itemPath = itemPaths.FirstOrDefault(x => x.Id == id); - if (itemPath == null) continue; - var path = itemPath.Path; - var qid = id; - clauses.Add(x => x.Path.SqlStartsWith(path + ",", TextColumnType.NVarchar) || x.Path.SqlEndsWith("," + qid, TextColumnType.NVarchar)); - } - } - query.WhereAny(clauses); - } - - IQuery filterQuery = null; - if (filter.IsNullOrWhiteSpace() == false) - { - filterQuery = Query().Where(x => x.Name.Contains(filter)); - } - - var contents = _entityRepository.GetPagedResultsByQuery(query, objectTypeId, pageIndex, pageSize, out totalRecords, orderBy, orderDirection, filterQuery); - return contents; - } - } - - /// - /// Returns a paged collection of descendants from the root - /// - /// - /// - /// - /// - /// - /// - /// - /// true/false to include trashed objects - /// - public IEnumerable GetPagedDescendantsFromRoot(UmbracoObjectTypes umbracoObjectType, long pageIndex, int pageSize, out long totalRecords, - string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = "", bool includeTrashed = true) - { - var objectTypeId = umbracoObjectType.GetGuid(); - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - var query = Query(); - //don't include trashed if specfied - //don't include trashed if specfied - - if (includeTrashed == false) - { - query.Where(x => x.Trashed == false); - } - IQuery filterQuery = null; - - if (filter.IsNullOrWhiteSpace() == false) - { - filterQuery = Query().Where(x => x.Name.Contains(filter)); - } - var contents = _entityRepository.GetPagedResultsByQuery(query, objectTypeId, pageIndex, pageSize, out totalRecords, orderBy, orderDirection, filterQuery); - return contents; - } - } - - /// - /// Gets a collection of the entities at the root, which corresponds to the entities with a Parent Id of -1. - /// - /// UmbracoObjectType of the root entities to retrieve - /// An enumerable list of objects - public virtual IEnumerable GetRootEntities(UmbracoObjectTypes umbracoObjectType) - { - var objectTypeId = umbracoObjectType.GetGuid(); - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - return _entityRepository.GetByQuery(QueryRootEntity, objectTypeId); - } - } - - /// - /// Gets a collection of all of a given type. - /// - /// Type of the entities to retrieve - /// An enumerable list of objects - public virtual IEnumerable GetAll(params int[] ids) where T : IUmbracoEntity - { - var typeFullName = typeof(T).FullName; - if (typeFullName == null || _supportedObjectTypes.ContainsKey(typeFullName) == false) - throw new NotSupportedException("The passed in type is not supported"); - - var objectType = _supportedObjectTypes[typeFullName].Item1; - return GetAll(objectType, ids); - } - - /// - /// Gets a collection of all of a given type. - /// - /// UmbracoObjectType of the entities to return - /// - /// An enumerable list of objects - public virtual IEnumerable GetAll(UmbracoObjectTypes umbracoObjectType, params int[] ids) - { - var entityType = GetEntityType(umbracoObjectType); - - var typeFullName = entityType.FullName; - if (typeFullName == null || _supportedObjectTypes.ContainsKey(typeFullName) == false) - throw new NotSupportedException("The passed in type is not supported"); - - var objectTypeId = umbracoObjectType.GetGuid(); - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - return _entityRepository.GetAll(objectTypeId, ids); - } - } - - public IEnumerable GetAll(UmbracoObjectTypes umbracoObjectType, Guid[] keys) - { - var entityType = GetEntityType(umbracoObjectType); - - var typeFullName = entityType.FullName; - if (typeFullName == null || _supportedObjectTypes.ContainsKey(typeFullName) == false) - throw new NotSupportedException("The passed in type is not supported"); - - var objectTypeId = umbracoObjectType.GetGuid(); - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - return _entityRepository.GetAll(objectTypeId, keys); - } - } - - public virtual IEnumerable GetAllPaths(UmbracoObjectTypes umbracoObjectType, params int[] ids) - { - var entityType = GetEntityType(umbracoObjectType); - var typeFullName = entityType.FullName; - if (typeFullName == null || _supportedObjectTypes.ContainsKey(typeFullName) == false) - throw new NotSupportedException("The passed in type is not supported."); - - var objectTypeId = umbracoObjectType.GetGuid(); - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - return _entityRepository.GetAllPaths(objectTypeId, ids); - } - } - - public virtual IEnumerable GetAllPaths(UmbracoObjectTypes umbracoObjectType, params Guid[] keys) - { - var entityType = GetEntityType(umbracoObjectType); - var typeFullName = entityType.FullName; - if (typeFullName == null || _supportedObjectTypes.ContainsKey(typeFullName) == false) - throw new NotSupportedException("The passed in type is not supported."); - - var objectTypeId = umbracoObjectType.GetGuid(); - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - return _entityRepository.GetAllPaths(objectTypeId, keys); - } - } - - /// - /// Gets a collection of - /// - /// Guid id of the UmbracoObjectType - /// - /// An enumerable list of objects - public virtual IEnumerable GetAll(Guid objectTypeId, params int[] ids) - { - var umbracoObjectType = UmbracoObjectTypesExtensions.GetUmbracoObjectType(objectTypeId); - var entityType = GetEntityType(umbracoObjectType); - - var typeFullName = entityType.FullName; - if (typeFullName == null || _supportedObjectTypes.ContainsKey(typeFullName) == false) - throw new NotSupportedException("The passed in type is not supported"); - - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - return _entityRepository.GetAll(objectTypeId, ids); - } - } - - /// - /// Gets the UmbracoObjectType from the integer id of an IUmbracoEntity. - /// - /// Id of the entity - /// - public virtual UmbracoObjectTypes GetObjectType(int id) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - var sql = scope.SqlContext.Sql() - .Select("nodeObjectType") - .From() - .Where(x => x.NodeId == id); - var nodeObjectTypeId = scope.Database.ExecuteScalar(sql); - var objectTypeId = nodeObjectTypeId; - return UmbracoObjectTypesExtensions.GetUmbracoObjectType(objectTypeId); - } - } - - /// - /// Gets the UmbracoObjectType from the integer id of an IUmbracoEntity. - /// - /// Unique Id of the entity - /// - public virtual UmbracoObjectTypes GetObjectType(Guid key) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - var sql = scope.SqlContext.Sql() - .Select("nodeObjectType") - .From() - .Where(x => x.UniqueId == key); - var nodeObjectTypeId = scope.Database.ExecuteScalar(sql); - var objectTypeId = nodeObjectTypeId; - return UmbracoObjectTypesExtensions.GetUmbracoObjectType(objectTypeId); - } - } - - /// - /// Gets the UmbracoObjectType from an IUmbracoEntity. - /// - /// - /// - public virtual UmbracoObjectTypes GetObjectType(IUmbracoEntity entity) - { - return entity is UmbracoEntity entityImpl - ? UmbracoObjectTypesExtensions.GetUmbracoObjectType(entityImpl.NodeObjectTypeId) - : GetObjectType(entity.Id); - } - - /// - /// Gets the Type of an entity by its Id - /// - /// Id of the entity - /// Type of the entity - public virtual Type GetEntityType(int id) - { - var objectType = GetObjectType(id); - return GetEntityType(objectType); - } - - /// - /// Gets the Type of an entity by its - /// - /// - /// Type of the entity - public virtual Type GetEntityType(UmbracoObjectTypes umbracoObjectType) - { - var type = typeof(UmbracoObjectTypes); - var memInfo = type.GetMember(umbracoObjectType.ToString()); - var attributes = memInfo[0].GetCustomAttributes(typeof(UmbracoObjectTypeAttribute), - false); - - var attribute = ((UmbracoObjectTypeAttribute)attributes[0]); - if (attribute == null) - throw new NullReferenceException("The passed in UmbracoObjectType does not contain an UmbracoObjectTypeAttribute, which is used to retrieve the Type."); - - if (attribute.ModelType == null) - throw new NullReferenceException("The passed in UmbracoObjectType does not contain a Type definition"); - - return attribute.ModelType; - } - - public bool Exists(Guid key) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - var exists = _entityRepository.Exists(key); - return exists; - } - } - - public bool Exists(int id) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - var exists = _entityRepository.Exists(id); - return exists; - } - } - - /// - public int ReserveId(Guid key) - { - NodeDto node; - using (var scope = ScopeProvider.CreateScope()) - { - var sql = new Sql("SELECT * FROM umbracoNode WHERE uniqueID=@0 AND nodeObjectType=@1", key, Constants.ObjectTypes.IdReservation); - node = scope.Database.SingleOrDefault(sql); - if (node != null) throw new InvalidOperationException("An identifier has already been reserved for this Udi."); - node = new NodeDto - { - UniqueId = key, - Text = "RESERVED.ID", - NodeObjectType = Constants.ObjectTypes.IdReservation, - - CreateDate = DateTime.Now, - UserId = 0, - ParentId = -1, - Level = 1, - Path = "-1", - SortOrder = 0, - Trashed = false - }; - scope.Database.Insert(node); - scope.Complete(); - } - return node.NodeId; - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using NPoco; +using Umbraco.Core.Cache; +using Umbraco.Core.CodeAnnotations; +using Umbraco.Core.Events; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Persistence.Dtos; +using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Persistence.Repositories; +using Umbraco.Core.Scoping; + +namespace Umbraco.Core.Services.Implement +{ + public class EntityService : ScopeRepositoryService, IEntityService + { + private readonly IEntityRepository _entityRepository; + private readonly Dictionary>> _supportedObjectTypes; + private IQuery _queryRootEntity; + private readonly IdkMap _idkMap; + + public EntityService(IScopeProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory, + IContentService contentService, IContentTypeService contentTypeService, + IMediaService mediaService, IMediaTypeService mediaTypeService, + IDataTypeService dataTypeService, + IMemberService memberService, IMemberTypeService memberTypeService, IdkMap idkMap, + IRuntimeCacheProvider runtimeCache, + IEntityRepository entityRepository) + : base(provider, logger, eventMessagesFactory) + { + _idkMap = idkMap; + _entityRepository = entityRepository; + + _supportedObjectTypes = new Dictionary>> + { + {typeof (IDataTypeDefinition).FullName, new Tuple>(UmbracoObjectTypes.DataType, dataTypeService.GetDataTypeDefinitionById)}, + {typeof (IContent).FullName, new Tuple>(UmbracoObjectTypes.Document, contentService.GetById)}, + {typeof (IContentType).FullName, new Tuple>(UmbracoObjectTypes.DocumentType, contentTypeService.Get)}, + {typeof (IMedia).FullName, new Tuple>(UmbracoObjectTypes.Media, mediaService.GetById)}, + {typeof (IMediaType).FullName, new Tuple>(UmbracoObjectTypes.MediaType, mediaTypeService.Get)}, + {typeof (IMember).FullName, new Tuple>(UmbracoObjectTypes.Member, memberService.GetById)}, + {typeof (IMemberType).FullName, new Tuple>(UmbracoObjectTypes.MemberType, memberTypeService.Get)}, + }; + } + + #region Static Queries + + // lazy-constructed because when the ctor runs, the query factory may not be ready + + private IQuery QueryRootEntity => _queryRootEntity + ?? (_queryRootEntity = Query().Where(x => x.ParentId == -1)); + + #endregion + + /// + /// Returns the integer id for a given GUID + /// + /// + /// + /// + public Attempt GetIdForKey(Guid key, UmbracoObjectTypes umbracoObjectType) + { + return _idkMap.GetIdForKey(key, umbracoObjectType); + } + + public Attempt GetIdForUdi(Udi udi) + { + return _idkMap.GetIdForUdi(udi); + } + + /// + /// Returns the GUID for a given integer id + /// + /// + /// + /// + public Attempt GetKeyForId(int id, UmbracoObjectTypes umbracoObjectType) + { + return _idkMap.GetKeyForId(id, umbracoObjectType); + } + + public IUmbracoEntity GetByKey(Guid key, bool loadBaseType = true) + { + if (loadBaseType) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + return _entityRepository.GetByKey(key); + } + } + + //SD: TODO: Need to enable this at some stage ... just need to ask Morten what the deal is with what this does. + throw new NotSupportedException(); + + //var objectType = GetObjectType(key); + //var entityType = GetEntityType(objectType); + //var typeFullName = entityType.FullName; + //var entity = _supportedObjectTypes[typeFullName].Item2(id); + + //return entity; + } + + /// + /// Gets an UmbracoEntity by its Id, and optionally loads the complete object graph. + /// + /// + /// By default this will load the base type with a minimum set of properties. + /// + /// Id of the object to retrieve + /// Optional bool to load the complete object graph when set to False. + /// An + public virtual IUmbracoEntity Get(int id, bool loadBaseType = true) + { + if (loadBaseType) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + return _entityRepository.Get(id); + } + } + + var objectType = GetObjectType(id); + var entityType = GetEntityType(objectType); + var typeFullName = entityType.FullName; + var entity = _supportedObjectTypes[typeFullName].Item2(id); + + return entity; + } + + public IUmbracoEntity GetByKey(Guid key, UmbracoObjectTypes umbracoObjectType, bool loadBaseType = true) + { + if (loadBaseType) + { + var objectTypeId = umbracoObjectType.GetGuid(); + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + return _entityRepository.GetByKey(key, objectTypeId); + } + } + + //SD: TODO: Need to enable this at some stage ... just need to ask Morten what the deal is with what this does. + throw new NotSupportedException(); + + //var entityType = GetEntityType(umbracoObjectType); + //var typeFullName = entityType.FullName; + //var entity = _supportedObjectTypes[typeFullName].Item2(id); + + //return entity; + } + + /// + /// Gets an UmbracoEntity by its Id and UmbracoObjectType, and optionally loads the complete object graph. + /// + /// + /// By default this will load the base type with a minimum set of properties. + /// + /// Id of the object to retrieve + /// UmbracoObjectType of the entity to retrieve + /// Optional bool to load the complete object graph when set to False. + /// An + public virtual IUmbracoEntity Get(int id, UmbracoObjectTypes umbracoObjectType, bool loadBaseType = true) + { + if (loadBaseType) + { + var objectTypeId = umbracoObjectType.GetGuid(); + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + return _entityRepository.Get(id, objectTypeId); + } + } + + var entityType = GetEntityType(umbracoObjectType); + var typeFullName = entityType.FullName; + var entity = _supportedObjectTypes[typeFullName].Item2(id); + + return entity; + } + + public IUmbracoEntity GetByKey(Guid key, bool loadBaseType = true) where T : IUmbracoEntity + { + throw new NotImplementedException(); + } + + /// + /// Gets an UmbracoEntity by its Id and specified Type. Optionally loads the complete object graph. + /// + /// + /// By default this will load the base type with a minimum set of properties. + /// + /// Type of the model to retrieve. Must be based on an + /// Id of the object to retrieve + /// Optional bool to load the complete object graph when set to False. + /// An + public virtual IUmbracoEntity Get(int id, bool loadBaseType = true) where T : IUmbracoEntity + { + if (loadBaseType) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + return _entityRepository.Get(id); + } + } + + var typeFullName = typeof(T).FullName; + if (_supportedObjectTypes.ContainsKey(typeFullName) == false) + throw new NotSupportedException("The passed in type is not supported"); + var entity = _supportedObjectTypes[typeFullName].Item2(id); + + return entity; + } + + /// + /// Gets the parent of entity by its id + /// + /// Id of the entity to retrieve the Parent for + /// An + public virtual IUmbracoEntity GetParent(int id) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + var entity = _entityRepository.Get(id); + if (entity.ParentId == -1 || entity.ParentId == -20 || entity.ParentId == -21) + return null; + + return _entityRepository.Get(entity.ParentId); + } + } + + /// + /// Gets the parent of entity by its id and UmbracoObjectType + /// + /// Id of the entity to retrieve the Parent for + /// UmbracoObjectType of the parent to retrieve + /// An + public virtual IUmbracoEntity GetParent(int id, UmbracoObjectTypes umbracoObjectType) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + var entity = _entityRepository.Get(id); + if (entity.ParentId == -1 || entity.ParentId == -20 || entity.ParentId == -21) + return null; + + var objectTypeId = umbracoObjectType.GetGuid(); + return _entityRepository.Get(entity.ParentId, objectTypeId); + } + } + + /// + /// Gets a collection of children by the parents Id + /// + /// Id of the parent to retrieve children for + /// An enumerable list of objects + public virtual IEnumerable GetChildren(int parentId) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + var query = Query().Where(x => x.ParentId == parentId); + return _entityRepository.GetByQuery(query); + } + } + + /// + /// Gets a collection of children by the parents Id and UmbracoObjectType + /// + /// Id of the parent to retrieve children for + /// UmbracoObjectType of the children to retrieve + /// An enumerable list of objects + public virtual IEnumerable GetChildren(int parentId, UmbracoObjectTypes umbracoObjectType) + { + var objectTypeId = umbracoObjectType.GetGuid(); + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + var query = Query().Where(x => x.ParentId == parentId); + return _entityRepository.GetByQuery(query, objectTypeId).ToList(); // run within using! // run within using! + } + } + + /// + /// Gets a collection of descendents by the parents Id + /// + /// Id of entity to retrieve descendents for + /// An enumerable list of objects + public virtual IEnumerable GetDescendents(int id) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + var entity = _entityRepository.Get(id); + var pathMatch = entity.Path + ","; + var query = Query().Where(x => x.Path.StartsWith(pathMatch) && x.Id != id); + return _entityRepository.GetByQuery(query); + } + } + + /// + /// Gets a collection of descendents by the parents Id + /// + /// Id of entity to retrieve descendents for + /// UmbracoObjectType of the descendents to retrieve + /// An enumerable list of objects + public virtual IEnumerable GetDescendents(int id, UmbracoObjectTypes umbracoObjectType) + { + var objectTypeId = umbracoObjectType.GetGuid(); + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + var entity = _entityRepository.Get(id); + var query = Query().Where(x => x.Path.StartsWith(entity.Path) && x.Id != id); + return _entityRepository.GetByQuery(query, objectTypeId); + } + } + + /// + /// Returns a paged collection of children + /// + /// The parent id to return children for + /// + /// + /// + /// + /// + /// + /// + /// + public IEnumerable GetPagedChildren(int parentId, UmbracoObjectTypes umbracoObjectType, long pageIndex, int pageSize, out long totalRecords, + string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, string filter = "") + { + var objectTypeId = umbracoObjectType.GetGuid(); + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + var query = Query().Where(x => x.ParentId == parentId && x.Trashed == false); + + IQuery filterQuery = null; + if (filter.IsNullOrWhiteSpace() == false) + { + filterQuery = Query().Where(x => x.Name.Contains(filter)); + } + + var contents = _entityRepository.GetPagedResultsByQuery(query, objectTypeId, pageIndex, pageSize, out totalRecords, orderBy, orderDirection, filterQuery); + return contents; + } + } + + /// + /// Returns a paged collection of descendants + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public IEnumerable GetPagedDescendants(int id, UmbracoObjectTypes umbracoObjectType, long pageIndex, int pageSize, out long totalRecords, + string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = "") + { + var objectTypeId = umbracoObjectType.GetGuid(); + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + var query = Query(); + //if the id is System Root, then just get all + //if the id is System Root, then just get all + + if (id != Constants.System.Root) + { + //lookup the path so we can use it in the prefix query below + var itemPaths = _entityRepository.GetAllPaths(objectTypeId, id).ToArray(); + if (itemPaths.Length == 0) + { + totalRecords = 0; + return Enumerable.Empty(); + } + var itemPath = itemPaths[0].Path; + + query.Where(x => x.Path.SqlStartsWith(itemPath + ",", TextColumnType.NVarchar)); + } + IQuery filterQuery = null; + + if (filter.IsNullOrWhiteSpace() == false) + { + filterQuery = Query().Where(x => x.Name.Contains(filter)); + } + var contents = _entityRepository.GetPagedResultsByQuery(query, objectTypeId, pageIndex, pageSize, out totalRecords, orderBy, orderDirection, filterQuery); + return contents; + } + } + /// + /// Returns a paged collection of descendants. + /// + public IEnumerable GetPagedDescendants(IEnumerable ids, UmbracoObjectTypes umbracoObjectType, long pageIndex, int pageSize, out long totalRecords, + string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = "") + { + totalRecords = 0; + + var idsA = ids.ToArray(); + if (idsA.Length == 0) + return Enumerable.Empty(); + + var objectTypeId = umbracoObjectType.GetGuid(); + + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + var query = Query(); + if (idsA.All(x => x != Constants.System.Root)) + { + //lookup the paths so we can use it in the prefix query below + //lookup the paths so we can use it in the prefix query below + var itemPaths = _entityRepository.GetAllPaths(objectTypeId, idsA).ToArray(); + + if (itemPaths.Length == 0) + { + totalRecords = 0; + return Enumerable.Empty(); + } + var clauses = new List>>(); + foreach (var id in idsA) + { + //if the id is root then don't add any clauses + if (id != Constants.System.Root) + { + var itemPath = itemPaths.FirstOrDefault(x => x.Id == id); + if (itemPath == null) continue; + var path = itemPath.Path; + var qid = id; + clauses.Add(x => x.Path.SqlStartsWith(path + ",", TextColumnType.NVarchar) || x.Path.SqlEndsWith("," + qid, TextColumnType.NVarchar)); + } + } + query.WhereAny(clauses); + } + + IQuery filterQuery = null; + if (filter.IsNullOrWhiteSpace() == false) + { + filterQuery = Query().Where(x => x.Name.Contains(filter)); + } + + var contents = _entityRepository.GetPagedResultsByQuery(query, objectTypeId, pageIndex, pageSize, out totalRecords, orderBy, orderDirection, filterQuery); + return contents; + } + } + + /// + /// Returns a paged collection of descendants from the root + /// + /// + /// + /// + /// + /// + /// + /// + /// true/false to include trashed objects + /// + public IEnumerable GetPagedDescendantsFromRoot(UmbracoObjectTypes umbracoObjectType, long pageIndex, int pageSize, out long totalRecords, + string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = "", bool includeTrashed = true) + { + var objectTypeId = umbracoObjectType.GetGuid(); + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + var query = Query(); + //don't include trashed if specfied + //don't include trashed if specfied + + if (includeTrashed == false) + { + query.Where(x => x.Trashed == false); + } + IQuery filterQuery = null; + + if (filter.IsNullOrWhiteSpace() == false) + { + filterQuery = Query().Where(x => x.Name.Contains(filter)); + } + var contents = _entityRepository.GetPagedResultsByQuery(query, objectTypeId, pageIndex, pageSize, out totalRecords, orderBy, orderDirection, filterQuery); + return contents; + } + } + + /// + /// Gets a collection of the entities at the root, which corresponds to the entities with a Parent Id of -1. + /// + /// UmbracoObjectType of the root entities to retrieve + /// An enumerable list of objects + public virtual IEnumerable GetRootEntities(UmbracoObjectTypes umbracoObjectType) + { + var objectTypeId = umbracoObjectType.GetGuid(); + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + return _entityRepository.GetByQuery(QueryRootEntity, objectTypeId); + } + } + + /// + /// Gets a collection of all of a given type. + /// + /// Type of the entities to retrieve + /// An enumerable list of objects + public virtual IEnumerable GetAll(params int[] ids) where T : IUmbracoEntity + { + var typeFullName = typeof(T).FullName; + if (typeFullName == null || _supportedObjectTypes.ContainsKey(typeFullName) == false) + throw new NotSupportedException("The passed in type is not supported"); + + var objectType = _supportedObjectTypes[typeFullName].Item1; + return GetAll(objectType, ids); + } + + /// + /// Gets a collection of all of a given type. + /// + /// UmbracoObjectType of the entities to return + /// + /// An enumerable list of objects + public virtual IEnumerable GetAll(UmbracoObjectTypes umbracoObjectType, params int[] ids) + { + var entityType = GetEntityType(umbracoObjectType); + + var typeFullName = entityType.FullName; + if (typeFullName == null || _supportedObjectTypes.ContainsKey(typeFullName) == false) + throw new NotSupportedException("The passed in type is not supported"); + + var objectTypeId = umbracoObjectType.GetGuid(); + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + return _entityRepository.GetAll(objectTypeId, ids); + } + } + + public IEnumerable GetAll(UmbracoObjectTypes umbracoObjectType, Guid[] keys) + { + var entityType = GetEntityType(umbracoObjectType); + + var typeFullName = entityType.FullName; + if (typeFullName == null || _supportedObjectTypes.ContainsKey(typeFullName) == false) + throw new NotSupportedException("The passed in type is not supported"); + + var objectTypeId = umbracoObjectType.GetGuid(); + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + return _entityRepository.GetAll(objectTypeId, keys); + } + } + + public virtual IEnumerable GetAllPaths(UmbracoObjectTypes umbracoObjectType, params int[] ids) + { + var entityType = GetEntityType(umbracoObjectType); + var typeFullName = entityType.FullName; + if (typeFullName == null || _supportedObjectTypes.ContainsKey(typeFullName) == false) + throw new NotSupportedException("The passed in type is not supported."); + + var objectTypeId = umbracoObjectType.GetGuid(); + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + return _entityRepository.GetAllPaths(objectTypeId, ids); + } + } + + public virtual IEnumerable GetAllPaths(UmbracoObjectTypes umbracoObjectType, params Guid[] keys) + { + var entityType = GetEntityType(umbracoObjectType); + var typeFullName = entityType.FullName; + if (typeFullName == null || _supportedObjectTypes.ContainsKey(typeFullName) == false) + throw new NotSupportedException("The passed in type is not supported."); + + var objectTypeId = umbracoObjectType.GetGuid(); + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + return _entityRepository.GetAllPaths(objectTypeId, keys); + } + } + + /// + /// Gets a collection of + /// + /// Guid id of the UmbracoObjectType + /// + /// An enumerable list of objects + public virtual IEnumerable GetAll(Guid objectTypeId, params int[] ids) + { + var umbracoObjectType = UmbracoObjectTypesExtensions.GetUmbracoObjectType(objectTypeId); + var entityType = GetEntityType(umbracoObjectType); + + var typeFullName = entityType.FullName; + if (typeFullName == null || _supportedObjectTypes.ContainsKey(typeFullName) == false) + throw new NotSupportedException("The passed in type is not supported"); + + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + return _entityRepository.GetAll(objectTypeId, ids); + } + } + + /// + /// Gets the UmbracoObjectType from the integer id of an IUmbracoEntity. + /// + /// Id of the entity + /// + public virtual UmbracoObjectTypes GetObjectType(int id) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + var sql = scope.SqlContext.Sql() + .Select("nodeObjectType") + .From() + .Where(x => x.NodeId == id); + var nodeObjectTypeId = scope.Database.ExecuteScalar(sql); + var objectTypeId = nodeObjectTypeId; + return UmbracoObjectTypesExtensions.GetUmbracoObjectType(objectTypeId); + } + } + + /// + /// Gets the UmbracoObjectType from the integer id of an IUmbracoEntity. + /// + /// Unique Id of the entity + /// + public virtual UmbracoObjectTypes GetObjectType(Guid key) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + var sql = scope.SqlContext.Sql() + .Select("nodeObjectType") + .From() + .Where(x => x.UniqueId == key); + var nodeObjectTypeId = scope.Database.ExecuteScalar(sql); + var objectTypeId = nodeObjectTypeId; + return UmbracoObjectTypesExtensions.GetUmbracoObjectType(objectTypeId); + } + } + + /// + /// Gets the UmbracoObjectType from an IUmbracoEntity. + /// + /// + /// + public virtual UmbracoObjectTypes GetObjectType(IUmbracoEntity entity) + { + return entity is UmbracoEntity entityImpl + ? UmbracoObjectTypesExtensions.GetUmbracoObjectType(entityImpl.NodeObjectTypeId) + : GetObjectType(entity.Id); + } + + /// + /// Gets the Type of an entity by its Id + /// + /// Id of the entity + /// Type of the entity + public virtual Type GetEntityType(int id) + { + var objectType = GetObjectType(id); + return GetEntityType(objectType); + } + + /// + /// Gets the Type of an entity by its + /// + /// + /// Type of the entity + public virtual Type GetEntityType(UmbracoObjectTypes umbracoObjectType) + { + var type = typeof(UmbracoObjectTypes); + var memInfo = type.GetMember(umbracoObjectType.ToString()); + var attributes = memInfo[0].GetCustomAttributes(typeof(UmbracoObjectTypeAttribute), + false); + + var attribute = ((UmbracoObjectTypeAttribute)attributes[0]); + if (attribute == null) + throw new NullReferenceException("The passed in UmbracoObjectType does not contain an UmbracoObjectTypeAttribute, which is used to retrieve the Type."); + + if (attribute.ModelType == null) + throw new NullReferenceException("The passed in UmbracoObjectType does not contain a Type definition"); + + return attribute.ModelType; + } + + public bool Exists(Guid key) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + var exists = _entityRepository.Exists(key); + return exists; + } + } + + public bool Exists(int id) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + var exists = _entityRepository.Exists(id); + return exists; + } + } + + /// + public int ReserveId(Guid key) + { + NodeDto node; + using (var scope = ScopeProvider.CreateScope()) + { + var sql = new Sql("SELECT * FROM umbracoNode WHERE uniqueID=@0 AND nodeObjectType=@1", key, Constants.ObjectTypes.IdReservation); + node = scope.Database.SingleOrDefault(sql); + if (node != null) throw new InvalidOperationException("An identifier has already been reserved for this Udi."); + node = new NodeDto + { + UniqueId = key, + Text = "RESERVED.ID", + NodeObjectType = Constants.ObjectTypes.IdReservation, + + CreateDate = DateTime.Now, + UserId = 0, + ParentId = -1, + Level = 1, + Path = "-1", + SortOrder = 0, + Trashed = false + }; + scope.Database.Insert(node); + scope.Complete(); + } + return node.NodeId; + } + } +} diff --git a/src/Umbraco.Core/Services/ExternalLoginService.cs b/src/Umbraco.Core/Services/Implement/ExternalLoginService.cs similarity index 98% rename from src/Umbraco.Core/Services/ExternalLoginService.cs rename to src/Umbraco.Core/Services/Implement/ExternalLoginService.cs index 09460a9f04..59f031ad6e 100644 --- a/src/Umbraco.Core/Services/ExternalLoginService.cs +++ b/src/Umbraco.Core/Services/Implement/ExternalLoginService.cs @@ -7,7 +7,7 @@ using Umbraco.Core.Models.Identity; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Scoping; -namespace Umbraco.Core.Services +namespace Umbraco.Core.Services.Implement { public class ExternalLoginService : ScopeRepositoryService, IExternalLoginService { diff --git a/src/Umbraco.Core/Services/FileService.cs b/src/Umbraco.Core/Services/Implement/FileService.cs similarity index 97% rename from src/Umbraco.Core/Services/FileService.cs rename to src/Umbraco.Core/Services/Implement/FileService.cs index 1cb5de6938..23a4cddc55 100644 --- a/src/Umbraco.Core/Services/FileService.cs +++ b/src/Umbraco.Core/Services/Implement/FileService.cs @@ -1,1185 +1,1185 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.IO; -using System.Linq; -using System.Text.RegularExpressions; -using Umbraco.Core.Events; -using Umbraco.Core.IO; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Core.Persistence.Repositories; -using Umbraco.Core.Persistence.Repositories.Implement; -using Umbraco.Core.Scoping; - -namespace Umbraco.Core.Services -{ - /// - /// Represents the File Service, which is an easy access to operations involving objects like Scripts, Stylesheets and Templates - /// - public class FileService : ScopeRepositoryService, IFileService - { - private readonly IStylesheetRepository _stylesheetRepository; - private readonly IScriptRepository _scriptRepository; - private readonly ITemplateRepository _templateRepository; - private readonly IPartialViewRepository _partialViewRepository; - private readonly IPartialViewMacroRepository _partialViewMacroRepository; - private readonly IXsltFileRepository _xsltRepository; - private readonly IAuditRepository _auditRepository; - - private const string PartialViewHeader = "@inherits Umbraco.Web.Mvc.UmbracoTemplatePage"; - private const string PartialViewMacroHeader = "@inherits Umbraco.Web.Macros.PartialViewMacroPage"; - - public FileService(IScopeProvider uowProvider, ILogger logger, IEventMessagesFactory eventMessagesFactory, - IStylesheetRepository stylesheetRepository, IScriptRepository scriptRepository, ITemplateRepository templateRepository, - IPartialViewRepository partialViewRepository, IPartialViewMacroRepository partialViewMacroRepository, - IXsltFileRepository xsltRepository, IAuditRepository auditRepository) - : base(uowProvider, logger, eventMessagesFactory) - { - _stylesheetRepository = stylesheetRepository; - _scriptRepository = scriptRepository; - _templateRepository = templateRepository; - _partialViewRepository = partialViewRepository; - _partialViewMacroRepository = partialViewMacroRepository; - _xsltRepository = xsltRepository; - _auditRepository = auditRepository; - } - - #region Stylesheets - - /// - /// Gets a list of all objects - /// - /// An enumerable list of objects - public IEnumerable GetStylesheets(params string[] names) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - return _stylesheetRepository.GetMany(names); - } - } - - /// - /// Gets a object by its name - /// - /// Name of the stylesheet incl. extension - /// A object - public Stylesheet GetStylesheetByName(string name) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - return _stylesheetRepository.Get(name); - } - } - - /// - /// Saves a - /// - /// to save - /// - public void SaveStylesheet(Stylesheet stylesheet, int userId = 0) - { - using (var scope = ScopeProvider.CreateScope()) - { - var saveEventArgs = new SaveEventArgs(stylesheet); - if (scope.Events.DispatchCancelable(SavingStylesheet, this, saveEventArgs)) - { - scope.Complete(); - return; - } - - - _stylesheetRepository.Save(stylesheet); - saveEventArgs.CanCancel = false; - scope.Events.Dispatch(SavedStylesheet, this, saveEventArgs); - - Audit(AuditType.Save, "Save Stylesheet performed by user", userId, -1); - scope.Complete(); - } - } - - /// - /// Deletes a stylesheet by its name - /// - /// Name incl. extension of the Stylesheet to delete - /// - public void DeleteStylesheet(string path, int userId = 0) - { - using (var scope = ScopeProvider.CreateScope()) - { - var stylesheet = _stylesheetRepository.Get(path); - if (stylesheet == null) - { - scope.Complete(); - return; - } - - var deleteEventArgs = new DeleteEventArgs(stylesheet); - if (scope.Events.DispatchCancelable(DeletingStylesheet, this, deleteEventArgs)) - { - scope.Complete(); - return; // causes rollback // causes rollback - } - - _stylesheetRepository.Delete(stylesheet); - deleteEventArgs.CanCancel = false; - scope.Events.Dispatch(DeletedStylesheet, this, deleteEventArgs); - - Audit(AuditType.Delete, "Delete Stylesheet performed by user", userId, -1); - scope.Complete(); - } - } - - /// - /// Validates a - /// - /// to validate - /// True if Stylesheet is valid, otherwise false - public bool ValidateStylesheet(Stylesheet stylesheet) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - return _stylesheetRepository.ValidateStylesheet(stylesheet); - } - } - - public Stream GetStylesheetFileContentStream(string filepath) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - return _stylesheetRepository.GetFileContentStream(filepath); - } - } - - public void SetStylesheetFileContent(string filepath, Stream content) - { - using (var scope = ScopeProvider.CreateScope()) - { - _stylesheetRepository.SetFileContent(filepath, content); - scope.Complete(); - } - } - - public long GetStylesheetFileSize(string filepath) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - return _stylesheetRepository.GetFileSize(filepath); - } - } - - #endregion - - #region Scripts - - /// - /// Gets a list of all objects - /// - /// An enumerable list of objects - public IEnumerable