From ddf38407d89db077eccd58a187095f94fd096b12 Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 18 May 2016 10:55:19 +0200 Subject: [PATCH] U4-4847 Refactor ContentService (#1266) * U4-4748 - refactor Content-, Media- and MemberTypeRepository * Cleanup Attempt * Cleanup OperationStatus * U4-4748 - refactor Content-, Media- and MemberTypeService * U4-4748 - cleanup locking * U4-4748 - refactor Content-, Media- and MemberRepository * U4-4748 - refactor ContentService (in progress) * U4-4748 - all unit of work must be completed * U4-4748 - refactor locks, fix tests * U4-4748 - deal with fixmes * U4-4748 - lock table migration * Update UmbracoVersion * Fix AuthorizeUpgrade * U4-4748 - cleanup+bugfix lock objects * U4-4748 - bugfix * updates a string interpolation --- .../{Attempt{T}.cs => AttemptOfT.cs} | 300 +- src/Umbraco.Core/Constants-ObjectTypes.cs | 15 +- src/Umbraco.Core/Constants-System.cs | 7 +- .../ServicesCompositionRoot.cs | 13 +- .../Events/RecycleBinEventArgs.cs | 19 + src/Umbraco.Core/IO/IOHelper.cs | 62 +- src/Umbraco.Core/Models/Content.cs | 70 +- src/Umbraco.Core/Models/ContentBase.cs | 12 +- src/Umbraco.Core/Models/ContentExtensions.cs | 332 +- src/Umbraco.Core/Models/ContentType.cs | 26 - src/Umbraco.Core/Models/ContentTypeBase.cs | 21 + .../Models/ContentTypeExtensions.cs | 4 +- src/Umbraco.Core/Models/IContentBase.cs | 7 - src/Umbraco.Core/Models/Media.cs | 2 +- src/Umbraco.Core/Models/Member.cs | 5 - .../PublishedContent/PublishedContentType.cs | 4 +- src/Umbraco.Core/Models/PublishedState.cs | 49 +- src/Umbraco.Core/Models/Rdbms/LockDto.cs | 24 + .../Persistence/Constants-Locks.cs | 18 + .../Persistence/Factories/ContentFactory.cs | 1 - .../Migrations/Initial/BaseDataCreation.cs | 30 +- .../Initial/DatabaseSchemaCreation.cs | 3 +- .../TargetVersionEight/AddLockObjects.cs | 48 + .../TargetVersionEight/AddLockTable.cs | 31 + .../AddServerRegistrationColumnsAndLock.cs | 4 +- .../Repositories/ContentRepository.cs | 536 ++-- .../Repositories/ContentTypeRepository.cs | 87 +- ...sitory.cs => ContentTypeRepositoryBase.cs} | 2504 +++++++-------- .../DataTypeDefinitionRepository.cs | 12 +- .../Interfaces/IContentRepository.cs | 9 +- .../IContentTypeCompositionRepository.cs | 11 - .../Interfaces/IContentTypeRepository.cs | 4 +- .../Interfaces/IContentTypeRepositoryBase.cs | 23 + .../Interfaces/IMediaTypeRepository.cs | 12 +- .../Interfaces/IMemberTypeRepository.cs | 9 +- .../IServerRegistrationRepository.cs | 3 - .../Repositories/MediaRepository.cs | 248 +- .../Repositories/MediaTypeRepository.cs | 103 +- .../Repositories/MemberRepository.cs | 247 +- .../Repositories/MemberTypeRepository.cs | 97 +- .../ServerRegistrationRepository.cs | 11 - .../Repositories/VersionableRepositoryBase.cs | 12 +- .../SqlSyntax/ISqlSyntaxProvider.cs | 1 + .../SqlSyntax/MySqlSyntaxProvider.cs | 2 +- .../SqlSyntax/SqlCeSyntaxProvider.cs | 12 +- .../SqlSyntax/SqlSyntaxProviderBase.cs | 7 +- .../UnitOfWork/IDatabaseUnitOfWork.cs | 4 +- .../Persistence/UnitOfWork/IUnitOfWork.cs | 3 + .../Persistence/UnitOfWork/NPocoUnitOfWork.cs | 16 +- src/Umbraco.Core/Publishing/PublishStatus.cs | 42 +- .../Publishing/PublishStatusType.cs | 59 +- .../Publishing/PublishingStrategy.cs | 469 --- .../Publishing/UnPublishStatus.cs | 34 +- src/Umbraco.Core/ServiceContextExtensions.cs | 21 + src/Umbraco.Core/Services/AuditService.cs | 4 + src/Umbraco.Core/Services/ContentService.cs | 2693 +++++++++-------- .../Services/ContentTypeService.cs | 1407 +-------- .../Services/ContentTypeServiceBase.cs | 903 +++++- src/Umbraco.Core/Services/DataTypeService.cs | 116 +- src/Umbraco.Core/Services/DomainService.cs | 28 +- src/Umbraco.Core/Services/EntityService.cs | 82 +- .../Services/EntityXmlSerializer.cs | 2 +- .../Services/ExternalLoginService.cs | 8 +- src/Umbraco.Core/Services/FileService.cs | 127 +- .../Services/IContentTypeService.cs | 261 +- .../Services/IContentTypeServiceBase.cs | 52 + src/Umbraco.Core/Services/IDataTypeService.cs | 2 +- src/Umbraco.Core/Services/IFileService.cs | 2 +- .../Services/IMediaTypeService.cs | 10 + .../Services/IMemberTypeService.cs | 5 +- .../Services/IPublicAccessService.cs | 2 +- .../Services/LocalizationService.cs | 33 +- src/Umbraco.Core/Services/MacroService.cs | 14 +- src/Umbraco.Core/Services/MediaService.cs | 97 +- src/Umbraco.Core/Services/MediaTypeService.cs | 57 + .../Services/MemberGroupService.cs | 12 +- src/Umbraco.Core/Services/MemberService.cs | 133 +- .../Services/MemberTypeService.cs | 218 +- .../Services/MigrationEntryService.cs | 8 +- .../Services/NotificationService.cs | 19 +- src/Umbraco.Core/Services/OperationStatus.cs | 234 +- .../Services/OperationStatusType.cs | 55 +- src/Umbraco.Core/Services/PackagingService.cs | 31 +- .../Services/PublicAccessService.cs | 43 +- src/Umbraco.Core/Services/RelationService.cs | 69 +- .../Services/ServerRegistrationService.cs | 15 +- src/Umbraco.Core/Services/ServiceContext.cs | 11 +- src/Umbraco.Core/Services/TagService.cs | 64 +- src/Umbraco.Core/Services/TaskService.cs | 20 +- src/Umbraco.Core/Services/UserService.cs | 86 +- src/Umbraco.Core/Umbraco.Core.csproj | 15 +- .../Models/ContentExtensionsTests.cs | 683 ++++- src/Umbraco.Tests/Models/ContentTests.cs | 57 +- src/Umbraco.Tests/Models/MediaXmlTest.cs | 2 +- .../Repositories/ContentRepositoryTest.cs | 20 +- .../Repositories/MediaRepositoryTest.cs | 6 +- .../Persistence/UnitOfWorkTests.cs | 67 + .../PublishedContent/PublishedMediaTests.cs | 2 +- .../Publishing/PublishingStrategyTests.cs | 59 +- src/Umbraco.Tests/Services/BaseServiceTest.cs | 2 +- .../Services/ContentServicePerformanceTest.cs | 12 +- .../Services/ContentServiceTests.cs | 569 ++-- .../Services/ContentTypeServiceTests.cs | 84 +- .../Services/DataTypeServiceTests.cs | 2 +- .../Services/EntityServiceTests.cs | 6 +- .../Services/MediaServiceTests.cs | 6 +- .../Services/ThreadSafetyServiceTest.cs | 72 +- .../TestHelpers/BaseDatabaseFactoryTest.cs | 1 - .../TestHelpers/BaseUmbracoApplicationTest.cs | 1 - .../TestHelpers/TestObjects-Mocks.cs | 1 + src/Umbraco.Tests/TestHelpers/TestObjects.cs | 10 +- src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + .../Umbraco/Views/AuthorizeUpgrade.cshtml | 3 +- .../umbraco/dialogs/ChangeDocType.aspx.cs | 4 +- .../Cache/CacheRefresherEventHandler.cs | 20 +- .../Cache/ContentTypeCacheRefresher.cs | 2 +- src/Umbraco.Web/Editors/ContentController.cs | 2 +- .../Editors/ContentTypeController.cs | 34 +- .../Editors/ContentTypeControllerBase.cs | 31 +- src/Umbraco.Web/Editors/EntityController.cs | 8 +- src/Umbraco.Web/Editors/MediaController.cs | 4 +- .../Editors/MediaTypeController.cs | 34 +- .../Editors/MemberTypeController.cs | 4 +- .../Editors/TemplateQueryController.cs | 2 +- .../Models/Mapping/ContentModelMapper.cs | 2 +- .../ContentTypeModelMapperExtensions.cs | 4 +- .../Mapping/LockedCompositionsResolver.cs | 4 +- src/Umbraco.Web/PublishedContentExtensions.cs | 4 +- src/Umbraco.Web/Services/SectionService.cs | 9 +- .../Trees/ContentTypeTreeController.cs | 4 +- .../WebApi/Binders/ContentItemBinder.cs | 2 +- .../WebApi/Binders/MediaItemBinder.cs | 2 +- .../WebServices/BulkPublishController.cs | 2 +- .../Packages/installedPackage.aspx.cs | 2 +- .../dialogs/exportDocumenttype.aspx.cs | 2 +- .../umbraco/dialogs/protectPage.aspx.cs | 4 +- src/umbraco.cms/businesslogic/ContentType.cs | 7 +- .../businesslogic/media/MediaType.cs | 12 +- .../businesslogic/web/DocumentType.cs | 6 +- 139 files changed, 7539 insertions(+), 6981 deletions(-) rename src/Umbraco.Core/{Attempt{T}.cs => AttemptOfT.cs} (63%) create mode 100644 src/Umbraco.Core/Models/Rdbms/LockDto.cs create mode 100644 src/Umbraco.Core/Persistence/Constants-Locks.cs create mode 100644 src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionEight/AddLockObjects.cs create mode 100644 src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionEight/AddLockTable.cs rename src/Umbraco.Core/Persistence/Repositories/{ContentTypeBaseRepository.cs => ContentTypeRepositoryBase.cs} (94%) delete mode 100644 src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeCompositionRepository.cs create mode 100644 src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepositoryBase.cs delete mode 100644 src/Umbraco.Core/Publishing/PublishingStrategy.cs create mode 100644 src/Umbraco.Core/ServiceContextExtensions.cs create mode 100644 src/Umbraco.Core/Services/IContentTypeServiceBase.cs create mode 100644 src/Umbraco.Core/Services/IMediaTypeService.cs create mode 100644 src/Umbraco.Core/Services/MediaTypeService.cs create mode 100644 src/Umbraco.Tests/Persistence/UnitOfWorkTests.cs diff --git a/src/Umbraco.Core/Attempt{T}.cs b/src/Umbraco.Core/AttemptOfT.cs similarity index 63% rename from src/Umbraco.Core/Attempt{T}.cs rename to src/Umbraco.Core/AttemptOfT.cs index fc8fa3dcc9..d9e2abb186 100644 --- a/src/Umbraco.Core/Attempt{T}.cs +++ b/src/Umbraco.Core/AttemptOfT.cs @@ -1,174 +1,128 @@ -using System; -using Umbraco.Core.Dynamics; - -namespace Umbraco.Core -{ - /// - /// Represents the result of an operation attempt. - /// - /// The type of the attempted operation result. - [Serializable] - public struct Attempt - { - private readonly bool _success; - private readonly T _result; - private readonly Exception _exception; - - /// - /// Gets a value indicating whether this was successful. - /// - public bool Success - { - get { return _success; } - } - - /// - /// Gets the exception associated with an unsuccessful attempt. - /// - public Exception Exception { get { return _exception; } } - - /// - /// Gets the exception associated with an unsuccessful attempt. - /// - /// Keep it for backward compatibility sake. - [Obsolete(".Error is obsolete, you should use .Exception instead.", false)] - public Exception Error { get { return _exception; } } - - /// - /// Gets the attempt result. - /// - public T Result - { - get { return _result; } - } - - // optimize, use a singleton failed attempt - private static readonly Attempt Failed = new Attempt(false, default(T), null); - - /// - /// Represents an unsuccessful attempt. - /// - /// Keep it for backward compatibility sake. - [Obsolete(".Failed is obsolete, you should use Attempt.Fail() instead.", false)] - public static readonly Attempt False = Failed; - - // private - use Succeed() or Fail() methods to create attempts - private Attempt(bool success, T result, Exception exception) - { - _success = success; - _result = result; - _exception = exception; - } - - /// - /// Initialize a new instance of the struct with a result. - /// - /// A value indicating whether the attempt is successful. - /// The result of the attempt. - /// Keep it for backward compatibility sake. - [Obsolete("Attempt ctors are obsolete, you should use Attempt.Succeed(), Attempt.Fail() or Attempt.If() instead.", false)] - public Attempt(bool success, T result) - : this(success, result, null) - { } - - /// - /// Initialize a new instance of the struct representing a failed attempt, with an exception. - /// - /// The exception causing the failure of the attempt. - /// Keep it for backward compatibility sake. - [Obsolete("Attempt ctors are obsolete, you should use Attempt.Succeed(), Attempt.Fail() or Attempt.If() instead.", false)] - public Attempt(Exception exception) - : this(false, default(T), exception) - { } - - /// - /// Creates a successful attempt. - /// - /// The successful attempt. - public static Attempt Succeed() - { - return new Attempt(true, default(T), null); - } - - /// - /// Creates a successful attempt with a result. - /// - /// The result of the attempt. - /// The successful attempt. - public static Attempt Succeed(T result) - { - return new Attempt(true, result, null); - } - - /// - /// Creates a failed attempt. - /// - /// The failed attempt. - public static Attempt Fail() - { - return Failed; - } - - /// - /// Creates a failed attempt with an exception. - /// - /// The exception causing the failure of the attempt. - /// The failed attempt. - public static Attempt Fail(Exception exception) - { - return new Attempt(false, default(T), exception); - } - - /// - /// Creates a failed attempt with a result. - /// - /// The result of the attempt. - /// The failed attempt. - public static Attempt Fail(T result) - { - return new Attempt(false, result, null); - } - - /// - /// Creates a failed attempt with a result and an exception. - /// - /// The result of the attempt. - /// The exception causing the failure of the attempt. - /// The failed attempt. - public static Attempt Fail(T result, Exception exception) - { - return new Attempt(false, result, exception); - } - - /// - /// Creates a successful or a failed attempt. - /// - /// A value indicating whether the attempt is successful. - /// The attempt. - public static Attempt SucceedIf(bool condition) - { - return condition ? new Attempt(true, default(T), null) : Failed; - } - - /// - /// Creates a successful or a failed attempt, with a result. - /// - /// A value indicating whether the attempt is successful. - /// The result of the attempt. - /// The attempt. - public static Attempt SucceedIf(bool condition, T result) - { - return new Attempt(condition, result, null); - } - - /// - /// Implicity operator to check if the attempt was successful without having to access the 'success' property - /// - /// - /// - public static implicit operator bool(Attempt a) - { - return a.Success; - } - } +using System; + +namespace Umbraco.Core +{ + /// + /// Represents the result of an operation attempt. + /// + /// The type of the attempted operation result. + [Serializable] + public struct Attempt + { + /// + /// Gets a value indicating whether this was successful. + /// + public bool Success { get; } + + /// + /// Gets the exception associated with an unsuccessful attempt. + /// + public Exception Exception { get; } + + /// + /// Gets the attempt result. + /// + public T Result { get; } + + // optimize, use a singleton failed attempt + private static readonly Attempt Failed = new Attempt(false, default(T), null); + + // private - use Succeed() or Fail() methods to create attempts + private Attempt(bool success, T result, Exception exception) + { + Success = success; + Result = result; + Exception = exception; + } + + /// + /// Creates a successful attempt. + /// + /// The successful attempt. + public static Attempt Succeed() + { + return new Attempt(true, default(T), null); + } + + /// + /// Creates a successful attempt with a result. + /// + /// The result of the attempt. + /// The successful attempt. + public static Attempt Succeed(T result) + { + return new Attempt(true, result, null); + } + + /// + /// Creates a failed attempt. + /// + /// The failed attempt. + public static Attempt Fail() + { + return Failed; + } + + /// + /// Creates a failed attempt with an exception. + /// + /// The exception causing the failure of the attempt. + /// The failed attempt. + public static Attempt Fail(Exception exception) + { + return new Attempt(false, default(T), exception); + } + + /// + /// Creates a failed attempt with a result. + /// + /// The result of the attempt. + /// The failed attempt. + public static Attempt Fail(T result) + { + return new Attempt(false, result, null); + } + + /// + /// Creates a failed attempt with a result and an exception. + /// + /// The result of the attempt. + /// The exception causing the failure of the attempt. + /// The failed attempt. + public static Attempt Fail(T result, Exception exception) + { + return new Attempt(false, result, exception); + } + + /// + /// Creates a successful or a failed attempt. + /// + /// A value indicating whether the attempt is successful. + /// The attempt. + public static Attempt SucceedIf(bool condition) + { + return condition ? new Attempt(true, default(T), null) : Failed; + } + + /// + /// Creates a successful or a failed attempt, with a result. + /// + /// A value indicating whether the attempt is successful. + /// The result of the attempt. + /// The attempt. + public static Attempt SucceedIf(bool condition, T result) + { + return new Attempt(condition, result, null); + } + + /// + /// Implicity operator to check if the attempt was successful without having to access the 'success' property + /// + /// + /// + public static implicit operator bool(Attempt a) + { + return a.Success; + } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Constants-ObjectTypes.cs b/src/Umbraco.Core/Constants-ObjectTypes.cs index 560cd4b306..6fd3f01fe2 100644 --- a/src/Umbraco.Core/Constants-ObjectTypes.cs +++ b/src/Umbraco.Core/Constants-ObjectTypes.cs @@ -69,6 +69,11 @@ namespace Umbraco.Core /// public const string Document = "C66BA18E-EAF3-4CFF-8A22-41B16D66A972"; + /// + /// Guid for a Document object. + /// + public static readonly Guid DocumentGuid = new Guid(Document); + /// /// Guid for a Document Type object. /// @@ -84,6 +89,11 @@ namespace Umbraco.Core /// public const string Media = "B796F64C-1F99-4FFB-B886-4BF4BC011A9C"; + /// + /// Guid for a Media object. + /// + public static readonly Guid MediaGuid = new Guid(Media); + /// /// Guid for the Media Recycle Bin. /// @@ -143,7 +153,10 @@ namespace Umbraco.Core /// public const string LockObject = "87A9F1FF-B1E4-4A25-BABB-465A4A47EC41"; - + /// + /// Guid for a Lock object. + /// + public static readonly Guid LockObjectGuid = new Guid(LockObject); } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Constants-System.cs b/src/Umbraco.Core/Constants-System.cs index 82e3a1ff3f..4ac1ff9a70 100644 --- a/src/Umbraco.Core/Constants-System.cs +++ b/src/Umbraco.Core/Constants-System.cs @@ -25,9 +25,6 @@ public const int DefaultContentListViewDataTypeId = -95; public const int DefaultMediaListViewDataTypeId = -96; public const int DefaultMembersListViewDataTypeId = -97; - - // identifiers for lock objects - public const int ServersLock = -331; - } - } + } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/DependencyInjection/ServicesCompositionRoot.cs b/src/Umbraco.Core/DependencyInjection/ServicesCompositionRoot.cs index 4f74797f45..e6a100b3e2 100644 --- a/src/Umbraco.Core/DependencyInjection/ServicesCompositionRoot.cs +++ b/src/Umbraco.Core/DependencyInjection/ServicesCompositionRoot.cs @@ -19,34 +19,33 @@ namespace Umbraco.Core.DependencyInjection { public void Compose(IServiceRegistry container) { - container.RegisterSingleton(); - // register a transient messages factory, which will be replaced byt the web // boot manager when running in a web context container.Register(); - + //the context container.RegisterSingleton(); - + //now the services... container.RegisterSingleton(); container.RegisterSingleton(); container.RegisterSingleton(); container.RegisterSingleton(); - container.RegisterSingleton(); + container.RegisterSingleton(); container.RegisterSingleton(); container.RegisterSingleton(); container.RegisterSingleton(); container.RegisterSingleton(); container.RegisterSingleton(); container.RegisterSingleton(); + container.RegisterSingleton(); container.RegisterSingleton(); container.RegisterSingleton(); container.RegisterSingleton(); container.RegisterSingleton(); container.RegisterSingleton(); container.RegisterSingleton(); - container.RegisterSingleton(); + container.RegisterSingleton(); container.RegisterSingleton(); container.RegisterSingleton(); container.RegisterSingleton(); @@ -84,7 +83,7 @@ namespace Umbraco.Core.DependencyInjection factory.GetInstance>(), factory.GetInstance())); - //TODO: These are replaced in the web project - we need to declare them so that + //TODO: These are replaced in the web project - we need to declare them so that // something is wired up, just not sure this is very nice but will work for now. container.RegisterSingleton(); container.RegisterSingleton(); diff --git a/src/Umbraco.Core/Events/RecycleBinEventArgs.cs b/src/Umbraco.Core/Events/RecycleBinEventArgs.cs index ca4bbd2719..7818156207 100644 --- a/src/Umbraco.Core/Events/RecycleBinEventArgs.cs +++ b/src/Umbraco.Core/Events/RecycleBinEventArgs.cs @@ -17,6 +17,16 @@ namespace Umbraco.Core.Events Files = new List(); } + public RecycleBinEventArgs(Guid nodeObjectType, bool emptiedSuccessfully) + : base(false) + { + AllPropertyData = new Dictionary>(); + NodeObjectType = nodeObjectType; + Ids = new int[0]; + RecycleBinEmptiedSuccessfully = emptiedSuccessfully; + Files = new List(); + } + public RecycleBinEventArgs(Guid nodeObjectType, Dictionary> allPropertyData) : base(true) { @@ -26,6 +36,15 @@ namespace Umbraco.Core.Events Files = new List(); } + public RecycleBinEventArgs(Guid nodeObjectType) + : base(true) + { + AllPropertyData = new Dictionary>(); + NodeObjectType = nodeObjectType; + Ids = new int[0]; + Files = new List(); + } + /// /// Backwards compatibility constructor /// diff --git a/src/Umbraco.Core/IO/IOHelper.cs b/src/Umbraco.Core/IO/IOHelper.cs index 4da43d35bb..60f72ed19c 100644 --- a/src/Umbraco.Core/IO/IOHelper.cs +++ b/src/Umbraco.Core/IO/IOHelper.cs @@ -4,8 +4,10 @@ using System.Globalization; using System.Reflection; using System.IO; using System.Configuration; +using System.Linq; using System.Web; using System.Text.RegularExpressions; +using System.Threading.Tasks; using System.Web.Hosting; using ICSharpCode.SharpZipLib.Zip; using Umbraco.Core.Configuration; @@ -306,7 +308,7 @@ namespace Umbraco.Core.IO var debugFolder = Path.Combine(binFolder, "debug"); if (Directory.Exists(debugFolder)) return debugFolder; -#endif +#endif var releaseFolder = Path.Combine(binFolder, "release"); if (Directory.Exists(releaseFolder)) return releaseFolder; @@ -341,7 +343,7 @@ namespace Umbraco.Core.IO public static void EnsurePathExists(string path) { - var absolutePath = IOHelper.MapPath(path); + var absolutePath = MapPath(path); if (Directory.Exists(absolutePath) == false) Directory.CreateDirectory(absolutePath); } @@ -349,14 +351,58 @@ namespace Umbraco.Core.IO public static void EnsureFileExists(string path, string contents) { var absolutePath = IOHelper.MapPath(path); - if (File.Exists(absolutePath) == false) + if (File.Exists(absolutePath)) return; + + using (var writer = File.CreateText(absolutePath)) { - using (var writer = File.CreateText(absolutePath)) - { - writer.Write(contents); - } + writer.Write(contents); } - } + + /// + /// Deletes all files passed in. + /// + /// + /// + /// + internal static bool DeleteFiles(IEnumerable files, Action onError = null) + { + //ensure duplicates are removed + files = files.Distinct(); + + var allsuccess = true; + + var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); + Parallel.ForEach(files, file => + { + try + { + if (file.IsNullOrWhiteSpace()) return; + + var relativeFilePath = fs.GetRelativePath(file); + if (fs.FileExists(relativeFilePath) == false) return; + + var parentDirectory = Path.GetDirectoryName(relativeFilePath); + + // don't want to delete the media folder if not using directories. + if (UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories && parentDirectory != fs.GetRelativePath("/")) + { + //issue U4-771: if there is a parent directory the recursive parameter should be true + fs.DeleteDirectory(parentDirectory, String.IsNullOrEmpty(parentDirectory) == false); + } + else + { + fs.DeleteFile(file, true); + } + } + catch (Exception e) + { + onError?.Invoke(file, e); + allsuccess = false; + } + }); + + return allsuccess; + } } } diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs index b8e96c2793..be24049364 100644 --- a/src/Umbraco.Core/Models/Content.cs +++ b/src/Umbraco.Core/Models/Content.cs @@ -17,12 +17,14 @@ namespace Umbraco.Core.Models private IContentType _contentType; private ITemplate _template; private bool _published; + private bool? _publishedOriginal; private string _language; private DateTime? _releaseDate; private DateTime? _expireDate; private int _writer; private string _nodeName;//NOTE Once localization is introduced this will be the non-localized Node Name. private bool _permissionsChanged; + /// /// Constructor for creating a Content object /// @@ -31,8 +33,7 @@ namespace Umbraco.Core.Models /// ContentType for the current Content object public Content(string name, IContent parent, IContentType contentType) : this(name, parent, contentType, new PropertyCollection()) - { - } + { } /// /// Constructor for creating a Content object @@ -47,6 +48,7 @@ namespace Umbraco.Core.Models Mandate.ParameterNotNull(contentType, "contentType"); _contentType = contentType; + PublishedState = PublishedState.Unpublished; } /// @@ -57,8 +59,7 @@ namespace Umbraco.Core.Models /// ContentType for the current Content object public Content(string name, int parentId, IContentType contentType) : this(name, parentId, contentType, new PropertyCollection()) - { - } + { } /// /// Constructor for creating a Content object @@ -73,6 +74,7 @@ namespace Umbraco.Core.Models Mandate.ParameterNotNull(contentType, "contentType"); _contentType = contentType; + PublishedState = PublishedState.Unpublished; } private static readonly PropertyInfo TemplateSelector = ExpressionHelper.GetPropertyInfo(x => x.Template); @@ -95,7 +97,13 @@ namespace Umbraco.Core.Models [DataMember] public virtual ITemplate Template { - get { return _template; } + get + { + if (_template == null) + return _contentType.DefaultTemplate; + + return _template; + } set { SetPropertyValueAndDetectChanges(o => @@ -146,11 +154,19 @@ namespace Umbraco.Core.Models SetPropertyValueAndDetectChanges(o => { _published = value; + _publishedOriginal = _publishedOriginal ?? _published; + PublishedState = _published ? PublishedState.Published : PublishedState.Unpublished; return _published; }, _published, PublishedSelector); } } + [IgnoreDataMember] + public bool PublishedOriginal + { + get { return _publishedOriginal ?? false; } + } + /// /// Language of the data contained within this Content object. /// @@ -305,12 +321,14 @@ namespace Umbraco.Core.Models /// public void ChangePublishedState(PublishedState state) { - Published = state == PublishedState.Published; + if (state == PublishedState.Published || state == PublishedState.Unpublished) + throw new ArgumentException("Invalid state."); + Published = state == PublishedState.Publishing; PublishedState = state; } [DataMember] - internal PublishedState PublishedState { get; set; } + internal PublishedState PublishedState { get; private set; } /// /// Gets or sets the unique identifier of the published version, if any. @@ -322,24 +340,26 @@ namespace Umbraco.Core.Models /// Gets a value indicating whether the content has a published version. /// public bool HasPublishedVersion { get { return PublishedVersionGuid != default(Guid); } } - - /// - /// Changes the Trashed state of the content object - /// - /// Boolean indicating whether content is trashed (true) or not trashed (false) - /// - public override void ChangeTrashedState(bool isTrashed, int parentId = -20) - { - Trashed = isTrashed; - ParentId = parentId; - - //If the content is trashed and is published it should be marked as unpublished - if (isTrashed && Published) - { - ChangePublishedState(PublishedState.Unpublished); - } - } + public override void ResetDirtyProperties(bool rememberPreviouslyChangedProperties) + { + base.ResetDirtyProperties(rememberPreviouslyChangedProperties); + + // take care of the published state + switch (PublishedState) + { + case PublishedState.Saving: + case PublishedState.Unpublishing: + PublishedState = PublishedState.Unpublished; + break; + case PublishedState.Publishing: + PublishedState = PublishedState.Published; + break; + } + + _publishedOriginal = _published; + } + /// /// Method to call when Entity is being updated /// @@ -377,6 +397,8 @@ namespace Umbraco.Core.Models property.Version = clone.Version; } + clone.PublishedVersionGuid = Guid.Empty; + return clone; } diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs index b3d0f693d9..aff5dc35a3 100644 --- a/src/Umbraco.Core/Models/ContentBase.cs +++ b/src/Umbraco.Core/Models/ContentBase.cs @@ -118,6 +118,16 @@ namespace Umbraco.Core.Models } } + /// + /// Sets the ParentId from the lazy integer id + /// + /// Id of the Parent + internal protected void SetLazyParentId(Lazy parentId) + { + _parentId = parentId; + OnPropertyChanged(ParentIdSelector); + } + /// /// Gets or sets the name of the entity /// @@ -483,8 +493,6 @@ namespace Umbraco.Core.Models get { return _lastInvalidProperties; } } - public abstract void ChangeTrashedState(bool isTrashed, int parentId = -20); - #region Dirty property handling /// diff --git a/src/Umbraco.Core/Models/ContentExtensions.cs b/src/Umbraco.Core/Models/ContentExtensions.cs index e91996e32a..ceed239d53 100644 --- a/src/Umbraco.Core/Models/ContentExtensions.cs +++ b/src/Umbraco.Core/Models/ContentExtensions.cs @@ -1,13 +1,10 @@ using System; using System.Collections.Generic; using System.Drawing; -using System.Drawing.Drawing2D; -using System.Drawing.Imaging; using System.Globalization; using System.IO; using System.Linq; using System.Web; -using System.Xml; using System.Xml.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -17,9 +14,6 @@ using Umbraco.Core.IO; using Umbraco.Core.Media; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Membership; -using Umbraco.Core.Strings; -using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.UnitOfWork; using Umbraco.Core.Services; namespace Umbraco.Core.Models @@ -43,175 +37,184 @@ namespace Umbraco.Core.Models } /// - /// Determines if the item should be persisted at all + /// Determines whether the content should be persisted. /// - /// - /// - /// - /// In one particular case, a content item shouldn't be persisted: - /// * The item exists and is published - /// * A call to ContentService.Save is made - /// * The item has not been modified whatsoever apart from changing it's published status from published to saved - /// - /// In this case, there is no reason to make any database changes at all - /// + /// The content. + /// True is the content should be persisted, otherwise false. + /// See remarks in overload. internal static bool RequiresSaving(this IContent entity) { - var publishedState = ((Content)entity).PublishedState; - return RequiresSaving(entity, publishedState); + return RequiresSaving(entity, ((Content) entity).PublishedState); } /// - /// Determines if the item should be persisted at all + /// Determines whether the content should be persisted. /// - /// - /// - /// + /// The content. + /// The published state of the content. + /// True is the content should be persisted, otherwise false. /// - /// In one particular case, a content item shouldn't be persisted: - /// * The item exists and is published - /// * A call to ContentService.Save is made - /// * The item has not been modified whatsoever apart from changing it's published status from published to saved - /// - /// In this case, there is no reason to make any database changes at all + /// This is called by the repository when persisting an existing content, to + /// figure out whether it needs to persist the content at all. /// internal static bool RequiresSaving(this IContent entity, PublishedState publishedState) { - var publishedChanged = entity.IsPropertyDirty("Published") && publishedState != PublishedState.Unpublished; - //check if any user prop has changed - var propertyValueChanged = entity.IsAnyUserPropertyDirty(); - - //We need to know if any other property apart from Published was changed here - //don't create a new version if the published state has changed to 'Save' but no data has actually been changed - if (publishedChanged && entity.Published == false && propertyValueChanged == false) + // note: publishedState is always the entity's PublishedState except for tests + + var content = (Content) entity; + var userPropertyChanged = content.IsAnyUserPropertyDirty(); + var dirtyProps = content.GetDirtyProperties(); + //var contentPropertyChanged = content.IsEntityDirty(); + var contentPropertyChangedExceptPublished = dirtyProps.Any(x => x != "Published"); + + // we don't want to save (write to DB) if we are "saving" either a published content + // (.Saving) or an unpublished content (.Unpublished) and strictly nothing has changed + + var noSave = (publishedState == PublishedState.Saving || publishedState == PublishedState.Unpublished) + && userPropertyChanged == false + && contentPropertyChangedExceptPublished == false; + + return noSave == false; + } + + /// + /// Determines whether a new version of the content should be created. + /// + /// The content. + /// True if a new version should be created, otherwise false. + /// See remarks in overload. + internal static bool RequiresNewVersion(this IContent entity) + { + return RequiresNewVersion(entity, ((Content) entity).PublishedState); + } + + /// + /// Determines whether a new version of the content should be created. + /// + /// The content. + /// The published state of the content. + /// True if a new version should be created, otherwise false. + /// + /// This is called by the repository when persisting an existing content, to + /// figure out whether it needs to create a new version for that content. + /// A new version needs to be created when: + /// * The publish status is changed + /// * The language is changed + /// * A content property is changed (? why ?) + /// * The item is already published and is being published again and any property value is changed (to enable a rollback) + /// + internal static bool RequiresNewVersion(this IContent entity, PublishedState publishedState) + { + // note: publishedState is always the entity's PublishedState except for tests + + // read + // http://issues.umbraco.org/issue/U4-2589 (save & publish & creating new versions) + // http://issues.umbraco.org/issue/U4-3404 (pressing preview does save then preview) + // http://issues.umbraco.org/issue/U4-5510 (previewing & creating new versions) + // + // slightly modifying the rules to make more sense (marked with CHANGE) + // but should respect the result of the discussions in those issues + + // figure out whether .Language has changed + // this language stuff was an old POC and should be removed + var hasLanguageChanged = entity.IsPropertyDirty("Language"); + if (hasLanguageChanged) + return true; // language change => new version + + var content = (Content) entity; + //var contentPropertyChanged = content2.IsEntityDirty(); + var userPropertyChanged = content.IsAnyUserPropertyDirty(); + var dirtyProps = content.GetDirtyProperties(); + var contentPropertyChangedExceptPublished = dirtyProps.Any(x => x != "Published"); + var wasPublished = content.PublishedOriginal; + + switch (publishedState) { - //at this point we need to check if any non property value has changed that wasn't the published state - var changedProps = ((TracksChangesEntityBase)entity).GetDirtyProperties(); - if (changedProps.Any(x => x != "Published") == false) - { + case PublishedState.Publishing: + // changed state, publishing either a published or an unpublished version: + // DO create a new (published) version IF it was published already AND + // anything has changed, else can reuse the current version + return (contentPropertyChangedExceptPublished || userPropertyChanged) && wasPublished; + + case PublishedState.Unpublishing: + // changed state, unpublishing a published version: + // DO create a new (draft) version and preserve the (formerly) published + // version for rollback purposes IF the version that's being saved is the + // published version, else it's a draft that we can reuse + return wasPublished; + + case PublishedState.Saving: + // changed state, saving a published version: + // DO create a new (draft) version and preserve the published version IF + // anything has changed, else do NOT create a new version (pointless) + return contentPropertyChangedExceptPublished || userPropertyChanged; + + case PublishedState.Published: + // unchanged state, saving a published version: + // (can happen eg when moving content, never otherwise) + // do NOT create a new version as we're just saving after operations (eg + // move) that cannot be rolled back anyway - ensure that's really it + if (userPropertyChanged) + throw new InvalidOperationException("Invalid PublishedState \"Published\" with user property changes."); return false; - } + + case PublishedState.Unpublished: + // unchanged state, saving an unpublished version: + // do NOT create a new version for user property changes, + // BUT create a new version in case of content property changes, for + // rollback purposes + return contentPropertyChangedExceptPublished; + + default: + throw new NotSupportedException(); } - - return true; } /// - /// Determines if a new version should be created + /// Determines whether the database published flag should be cleared for versions + /// other than this content version. /// - /// - /// + /// The content. + /// True if the published flag should be cleared, otherwise false. + /// See remarks in overload. + internal static bool RequiresClearPublishedFlag(this IContent entity) + { + var publishedState = ((Content) entity).PublishedState; + var requiresNewVersion = entity.RequiresNewVersion(publishedState); + return entity.RequiresClearPublishedFlag(publishedState, requiresNewVersion); + } + + /// + /// Determines whether the database published flag should be cleared for versions + /// other than this content version. + /// + /// The content. + /// The published state of the content. + /// Indicates whether the content is a new version. + /// True if the published flag should be cleared, otherwise false. /// - /// A new version needs to be created when: - /// * The publish status is changed - /// * The language is changed - /// * The item is already published and is being published again and any property value is changed (to enable a rollback) + /// This is called by the repository when persisting an existing content, to + /// figure out whether it needs to clear the published flag for other versions. /// - internal static bool ShouldCreateNewVersion(this IContent entity) + internal static bool RequiresClearPublishedFlag(this IContent entity, PublishedState publishedState, bool isNewVersion) { - var publishedState = ((Content)entity).PublishedState; - return ShouldCreateNewVersion(entity, publishedState); - } + // note: publishedState is always the entity's PublishedState except for tests - /// - /// Returns a list of all dirty user defined properties - /// - /// - public static IEnumerable GetDirtyUserProperties(this IContentBase entity) - { - return entity.Properties.Where(x => x.IsDirty()).Select(x => x.Alias); - } - - public static bool IsAnyUserPropertyDirty(this IContentBase entity) - { - return entity.Properties.Any(x => x.IsDirty()); - } - - public static bool WasAnyUserPropertyDirty(this IContentBase entity) - { - return entity.Properties.Any(x => x.WasDirty()); - } - - /// - /// Determines if a new version should be created - /// - /// - /// - /// - /// - /// A new version needs to be created when: - /// * The publish status is changed - /// * The language is changed - /// * The item is already published and is being published again and any property value is changed (to enable a rollback) - /// - internal static bool ShouldCreateNewVersion(this IContent entity, PublishedState publishedState) - { - //check if the published state has changed or the language - var publishedChanged = entity.IsPropertyDirty("Published") && publishedState != PublishedState.Unpublished; - var langChanged = entity.IsPropertyDirty("Language"); - var contentChanged = publishedChanged || langChanged; - - //check if any user prop has changed - var propertyValueChanged = entity.IsAnyUserPropertyDirty(); - - //return true if published or language has changed - if (contentChanged) - { + // new, published version => everything else must be cleared + if (isNewVersion && entity.Published) return true; - } - //check if any content prop has changed - var contentDataChanged = ((Content)entity).IsEntityDirty(); + // if that entity was published then that entity has the flag and + // it does not need to be cleared for other versions + // NOT TRUE when unpublishing we create a NEW version + //var wasPublished = ((Content)entity).PublishedOriginal; + //if (wasPublished) + // return false; - //return true if the item is published and a property has changed or if any content property has changed - return (propertyValueChanged && publishedState == PublishedState.Published) || contentDataChanged; - } - - /// - /// Determines if the published db flag should be set to true for the current entity version and all other db - /// versions should have their flag set to false. - /// - /// - /// - /// - /// This is determined by: - /// * If a new version is being created and the entity is published - /// * If the published state has changed and the entity is published OR the entity has been un-published. - /// - internal static bool ShouldClearPublishedFlagForPreviousVersions(this IContent entity) - { - var publishedState = ((Content)entity).PublishedState; - return entity.ShouldClearPublishedFlagForPreviousVersions(publishedState, entity.ShouldCreateNewVersion(publishedState)); - } - - /// - /// Determines if the published db flag should be set to true for the current entity version and all other db - /// versions should have their flag set to false. - /// - /// - /// - /// - /// - /// - /// This is determined by: - /// * If a new version is being created and the entity is published - /// * If the published state has changed and the entity is published OR the entity has been un-published. - /// - internal static bool ShouldClearPublishedFlagForPreviousVersions(this IContent entity, PublishedState publishedState, bool isCreatingNewVersion) - { - if (isCreatingNewVersion && entity.Published) - { - return true; - } - - //If Published state has changed then previous versions should have their publish state reset. - //If state has been changed to unpublished the previous versions publish state should also be reset. - if (entity.IsPropertyDirty("Published") && (entity.Published || publishedState == PublishedState.Unpublished)) - { - return true; - } - - return false; + // clear whenever we are publishing or unpublishing + // publishing: because there might be a previously published version, which needs to be cleared + // unpublishing: same - we might be a saved version, not the published one, which needs to be cleared + return publishedState == PublishedState.Publishing || publishedState == PublishedState.Unpublishing; } /// @@ -312,9 +315,9 @@ namespace Umbraco.Core.Models return content.Path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) .Contains(recycleBinId.ToInvariantString()); } - + /// - /// Removes characters that are not valide XML characters from all entity properties + /// Removes characters that are not valide XML characters from all entity properties /// of type string. See: http://stackoverflow.com/a/961504/5018 /// /// @@ -454,7 +457,7 @@ namespace Umbraco.Core.Models /// The containing the file that will be uploaded public static void SetValue(this IContentBase content, string propertyTypeAlias, HttpPostedFileBase value) { - // Ensure we get the filename without the path in IE in intranet mode + // Ensure we get the filename without the path in IE in intranet mode // http://stackoverflow.com/questions/382464/httppostedfile-filename-different-from-ie var fileName = value.FileName; if (fileName.LastIndexOf(@"\") > 0) @@ -597,7 +600,7 @@ namespace Umbraco.Core.Models #endregion #region User/Profile methods - + /// /// Gets the for the Creator of this media item. /// @@ -672,7 +675,7 @@ namespace Umbraco.Core.Models ///// ///// ///// - ///// The tags returned are only relavent for published content & saved media or members + ///// The tags returned are only relavent for published content & saved media or members ///// //public static IEnumerable GetTags(this IContentBase content, string propertyTypeAlias, string tagGroup = "default") //{ @@ -909,5 +912,24 @@ namespace Umbraco.Core.Models return ((PackagingService)(packagingService)).Export(member); } #endregion + + #region Dirty + + public static IEnumerable GetDirtyUserProperties(this IContentBase entity) + { + return entity.Properties.Where(x => x.IsDirty()).Select(x => x.Alias); + } + + public static bool IsAnyUserPropertyDirty(this IContentBase entity) + { + return entity.Properties.Any(x => x.IsDirty()); + } + + public static bool WasAnyUserPropertyDirty(this IContentBase entity) + { + return entity.Properties.Any(x => x.WasDirty()); + } + + #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/ContentType.cs b/src/Umbraco.Core/Models/ContentType.cs index 0926e48e31..d711cf5a1a 100644 --- a/src/Umbraco.Core/Models/ContentType.cs +++ b/src/Umbraco.Core/Models/ContentType.cs @@ -152,31 +152,5 @@ namespace Umbraco.Core.Models { return DeepCloneWithResetIdentities(alias); } - - /// - /// Creates a deep clone of the current entity with its identity/alias and it's property identities reset - /// - /// - public IContentType DeepCloneWithResetIdentities(string alias) - { - var clone = (ContentType)DeepClone(); - clone.Alias = alias; - clone.Key = Guid.Empty; - foreach (var propertyGroup in clone.PropertyGroups) - { - propertyGroup.ResetIdentity(); - propertyGroup.ResetDirtyProperties(false); - } - foreach (var propertyType in clone.PropertyTypes) - { - propertyType.ResetIdentity(); - propertyType.ResetDirtyProperties(false); - } - - clone.ResetIdentity(); - clone.ResetDirtyProperties(false); - return clone; - } - } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs index a0305d2cfb..21ff40ce05 100644 --- a/src/Umbraco.Core/Models/ContentTypeBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeBase.cs @@ -642,5 +642,26 @@ namespace Umbraco.Core.Models return clone; } + + public IContentType DeepCloneWithResetIdentities(string alias) + { + var clone = (ContentType)DeepClone(); + clone.Alias = alias; + clone.Key = Guid.Empty; + foreach (var propertyGroup in clone.PropertyGroups) + { + propertyGroup.ResetIdentity(); + propertyGroup.ResetDirtyProperties(false); + } + foreach (var propertyType in clone.PropertyTypes) + { + propertyType.ResetIdentity(); + propertyType.ResetDirtyProperties(false); + } + + clone.ResetIdentity(); + clone.ResetDirtyProperties(false); + return clone; + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/ContentTypeExtensions.cs b/src/Umbraco.Core/Models/ContentTypeExtensions.cs index a47b430979..6301b46950 100644 --- a/src/Umbraco.Core/Models/ContentTypeExtensions.cs +++ b/src/Umbraco.Core/Models/ContentTypeExtensions.cs @@ -14,8 +14,8 @@ namespace Umbraco.Core.Models public static IEnumerable Descendants(this IContentTypeBase contentType) { var contentTypeService = ApplicationContext.Current.Services.ContentTypeService; - var descendants = contentTypeService.GetContentTypeChildren(contentType.Id) - .SelectRecursive(type => contentTypeService.GetContentTypeChildren(type.Id)); + var descendants = contentTypeService.GetChildren(contentType.Id) + .SelectRecursive(type => contentTypeService.GetChildren(type.Id)); return descendants; } diff --git a/src/Umbraco.Core/Models/IContentBase.cs b/src/Umbraco.Core/Models/IContentBase.cs index 7d2075cb65..d7e26808c8 100644 --- a/src/Umbraco.Core/Models/IContentBase.cs +++ b/src/Umbraco.Core/Models/IContentBase.cs @@ -72,12 +72,5 @@ namespace Umbraco.Core.Models /// /// True if content is valid otherwise false bool IsValid(); - - /// - /// Changes the Trashed state of the content object - /// - /// Boolean indicating whether content is trashed (true) or not trashed (false) - /// - void ChangeTrashedState(bool isTrashed, int parentId = -20); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Media.cs b/src/Umbraco.Core/Models/Media.cs index a7e794a400..d91fe9fdf8 100644 --- a/src/Umbraco.Core/Models/Media.cs +++ b/src/Umbraco.Core/Models/Media.cs @@ -112,7 +112,7 @@ namespace Umbraco.Core.Models /// /// Boolean indicating whether content is trashed (true) or not trashed (false) /// - public override void ChangeTrashedState(bool isTrashed, int parentId = -20) + public void ChangeTrashedState(bool isTrashed, int parentId = -20) { Trashed = isTrashed; //The Media Recycle Bin Id is -21 so we correct that here diff --git a/src/Umbraco.Core/Models/Member.cs b/src/Umbraco.Core/Models/Member.cs index 70c21e4307..ace6c2bf67 100644 --- a/src/Umbraco.Core/Models/Member.cs +++ b/src/Umbraco.Core/Models/Member.cs @@ -522,11 +522,6 @@ namespace Umbraco.Core.Models get { return _contentType; } } - public override void ChangeTrashedState(bool isTrashed, int parentId = -20) - { - throw new NotSupportedException("Members can't be trashed as no Recycle Bin exists, so use of this method is invalid"); - } - /* Internal experiment - only used for mapping queries. * Adding these to have first level properties instead of the Properties collection. */ diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs index d05960b08f..9baf0c1024 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs @@ -166,10 +166,10 @@ namespace Umbraco.Core.Models.PublishedContent switch (itemType) { case PublishedItemType.Content: - contentType = ApplicationContext.Current.Services.ContentTypeService.GetContentType(alias); + contentType = ApplicationContext.Current.Services.ContentTypeService.Get(alias); break; case PublishedItemType.Media: - contentType = ApplicationContext.Current.Services.ContentTypeService.GetMediaType(alias); + contentType = ApplicationContext.Current.Services.MediaTypeService.Get(alias); break; case PublishedItemType.Member: contentType = ApplicationContext.Current.Services.MemberTypeService.Get(alias); diff --git a/src/Umbraco.Core/Models/PublishedState.cs b/src/Umbraco.Core/Models/PublishedState.cs index 4469eba9cc..d169262802 100644 --- a/src/Umbraco.Core/Models/PublishedState.cs +++ b/src/Umbraco.Core/Models/PublishedState.cs @@ -1,9 +1,54 @@ -namespace Umbraco.Core.Models +using System; + +namespace Umbraco.Core.Models { + /// + /// The IContent states of a content version. + /// public enum PublishedState { + // when a content version is loaded, its state is one of those two: + + /// + /// The version is published. + /// Published, + + /// + /// The version is not published. + /// + /// Also: the version is being saved, in order to register changes + /// made to an unpublished version of the content. Unpublished, - Saved + + // legacy - remove + [Obsolete("kill!", true)] + Saved, + + // when it is saved, its state can also be one of those: + + /// + /// The version is being saved, in order to register changes made to a published content. + /// + /// The Saving state is transitional. Once the version + /// is saved, its state changes to Unpublished. + Saving, + + /// + /// The version is being saved, in order to publish the content. + /// + /// The Publishing state is transitional. Once the version + /// is saved, its state changes to Published. The content is published, + /// and all other versions are unpublished. + Publishing, + + /// + /// The version is being saved, in order to unpublish the content. + /// + /// The Unpublishing state is transitional. Once the version + /// is saved, its state changes to Unpublished. The content and all + /// other versions are unpublished. + Unpublishing + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Rdbms/LockDto.cs b/src/Umbraco.Core/Models/Rdbms/LockDto.cs new file mode 100644 index 0000000000..5227c15c66 --- /dev/null +++ b/src/Umbraco.Core/Models/Rdbms/LockDto.cs @@ -0,0 +1,24 @@ +using NPoco; +using Umbraco.Core.Persistence.DatabaseAnnotations; + +namespace Umbraco.Core.Models.Rdbms +{ + [TableName("umbracoLock")] + [PrimaryKey("id")] + [ExplicitColumns] + internal class LockDto + { + [Column("id")] + [PrimaryKeyColumn(Name = "PK_umbracoLock")] + public int Id { get; set; } + + [Column("value")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public int Value { get; set; } = 1; + + [Column("name")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Length(64)] + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Constants-Locks.cs b/src/Umbraco.Core/Persistence/Constants-Locks.cs new file mode 100644 index 0000000000..89d7cf7391 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Constants-Locks.cs @@ -0,0 +1,18 @@ +// ReSharper disable once CheckNamespace +namespace Umbraco.Core +{ + static partial class Constants + { + public static class Locks + { + public const int Servers = -331; + public const int ContentTypes = -332; + public const int ContentTree = -333; + public const int MediaTree = -334; + public const int MemberTree = -335; + public const int MediaTypes = -336; + public const int MemberTypes = -337; + public const int Domains = -338; + } + } +} diff --git a/src/Umbraco.Core/Persistence/Factories/ContentFactory.cs b/src/Umbraco.Core/Persistence/Factories/ContentFactory.cs index d21aea33e6..49445ffc9d 100644 --- a/src/Umbraco.Core/Persistence/Factories/ContentFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/ContentFactory.cs @@ -48,7 +48,6 @@ namespace Umbraco.Core.Persistence.Factories ExpireDate = dto.ExpiresDate.HasValue ? dto.ExpiresDate.Value : (DateTime?)null, ReleaseDate = dto.ReleaseDate.HasValue ? dto.ReleaseDate.Value : (DateTime?)null, Version = dto.ContentVersionDto.VersionId, - PublishedState = dto.Published ? PublishedState.Published : PublishedState.Unpublished, PublishedVersionGuid = dto.DocumentPublishedReadOnlyDto == null ? default(Guid) : dto.DocumentPublishedReadOnlyDto.VersionId }; //on initial construction we don't want to have dirty properties tracked diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs index 66d9c8be0c..76f29f3350 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs @@ -27,14 +27,19 @@ namespace Umbraco.Core.Persistence.Migrations.Initial /// Name of the table to create base data for public void InitializeBaseData(string tableName) { - _logger.Info(string.Format("Creating data in table {0}", tableName)); + _logger.Info($"Creating data in table {tableName}"); if(tableName.Equals("umbracoNode")) { - CreateUmbracNodeData(); + CreateUmbracoNodeData(); } - if(tableName.Equals("cmsContentType")) + if (tableName.Equals("umbracoLock")) + { + CreateUmbracoLockData(); + } + + if (tableName.Equals("cmsContentType")) { CreateCmsContentTypeData(); } @@ -102,7 +107,7 @@ namespace Umbraco.Core.Persistence.Migrations.Initial _logger.Info(string.Format("Done creating data in table {0}", tableName)); } - private void CreateUmbracNodeData() + private void CreateUmbracoNodeData() { _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = -1, Trashed = false, ParentId = -1, UserId = 0, Level = 0, Path = "-1", SortOrder = 0, UniqueId = new Guid("916724a5-173d-4619-b97e-b9de133dd6f5"), Text = "SYSTEM DATA: umbraco master root", NodeObjectType = new Guid(Constants.ObjectTypes.SystemRoot), CreateDate = DateTime.Now }); _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = -20, Trashed = false, ParentId = -1, UserId = 0, Level = 0, Path = "-1,-20", SortOrder = 0, UniqueId = new Guid("0F582A79-1E41-4CF0-BFA0-76340651891A"), Text = "Recycle Bin", NodeObjectType = new Guid(Constants.ObjectTypes.ContentRecycleBin), CreateDate = DateTime.Now }); @@ -135,15 +140,28 @@ namespace Umbraco.Core.Persistence.Migrations.Initial _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1043, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1043", SortOrder = 2, UniqueId = new Guid("1df9f033-e6d4-451f-b8d2-e0cbc50a836f"), Text = "Image Cropper", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1044, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1044", SortOrder = 0, UniqueId = new Guid("d59be02f-1df9-4228-aa1e-01917d806cda"), Text = Constants.Conventions.MemberTypes.DefaultAlias, NodeObjectType = new Guid(Constants.ObjectTypes.MemberType), CreateDate = DateTime.Now }); _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1045, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1045", SortOrder = 2, UniqueId = new Guid("7E3962CC-CE20-4FFC-B661-5897A894BA7E"), Text = "Multiple Media Picker", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); - + //TODO: We're not creating these for 7.0 //_database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1039, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1039", SortOrder = 2, UniqueId = new Guid("06f349a9-c949-4b6a-8660-59c10451af42"), Text = "Ultimate Picker", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); //_database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1038, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1038", SortOrder = 2, UniqueId = new Guid("1251c96c-185c-4e9b-93f4-b48205573cbd"), Text = "Simple Editor", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); - + //_database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1042, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1042", SortOrder = 2, UniqueId = new Guid("0a452bd5-83f9-4bc3-8403-1286e13fb77e"), Text = "Macro Container", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); } + private void CreateUmbracoLockData() + { + // all lock objects + _database.Insert("umbracoLock", "id", false, new LockDto { Id = Constants.Locks.Servers, Name = "Servers" }); + _database.Insert("umbracoLock", "id", false, new LockDto { Id = Constants.Locks.ContentTypes, Name = "ContentTypes" }); + _database.Insert("umbracoLock", "id", false, new LockDto { Id = Constants.Locks.ContentTree, Name = "ContentTree" }); + _database.Insert("umbracoLock", "id", false, new LockDto { Id = Constants.Locks.MediaTypes, Name = "MediaTypes" }); + _database.Insert("umbracoLock", "id", false, new LockDto { Id = Constants.Locks.MediaTree, Name = "MediaTree" }); + _database.Insert("umbracoLock", "id", false, new LockDto { Id = Constants.Locks.MemberTypes, Name = "MemberTypes" }); + _database.Insert("umbracoLock", "id", false, new LockDto { Id = Constants.Locks.MemberTree, Name = "MemberTree" }); + _database.Insert("umbracoLock", "id", false, new LockDto { Id = Constants.Locks.Domains, Name = "Domains" }); + } + private void CreateCmsContentTypeData() { _database.Insert("cmsContentType", "pk", false, new ContentTypeDto { PrimaryKey = 532, NodeId = 1031, Alias = Constants.Conventions.MediaTypes.Folder, Icon = "icon-folder", Thumbnail = "icon-folder", IsContainer = false, AllowAtRoot = true }); diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs index 9891476b40..31e3152f04 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs @@ -84,7 +84,8 @@ namespace Umbraco.Core.Persistence.Migrations.Initial {44, typeof (ExternalLoginDto)}, {45, typeof (MigrationDto)}, {46, typeof (UmbracoDeployChecksumDto)}, - {47, typeof (UmbracoDeployDependencyDto)} + {47, typeof (UmbracoDeployDependencyDto)}, + {48, typeof (LockDto) } }; #endregion diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionEight/AddLockObjects.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionEight/AddLockObjects.cs new file mode 100644 index 0000000000..238196cc40 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionEight/AddLockObjects.cs @@ -0,0 +1,48 @@ +using System; +using System.Linq; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.Rdbms; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionEight +{ + [Migration("8.0.0", 101, GlobalSettings.UmbracoMigrationName)] + public class AddLockObjects : MigrationBase + { + public AddLockObjects(ILogger logger) + : base(logger) + { } + + public override void Up() + { + // some may already exist, just ensure everything we need is here + EnsureLockObject(Constants.Locks.Servers, "Servers"); + EnsureLockObject(Constants.Locks.ContentTypes, "ContentTypes"); + EnsureLockObject(Constants.Locks.ContentTree, "ContentTree"); + EnsureLockObject(Constants.Locks.MediaTree, "MediaTree"); + EnsureLockObject(Constants.Locks.MemberTree, "MemberTree"); + EnsureLockObject(Constants.Locks.MediaTypes, "MediaTypes"); + EnsureLockObject(Constants.Locks.MemberTypes, "MemberTypes"); + EnsureLockObject(Constants.Locks.Domains, "Domains"); + } + + public override void Down() + { + // not implemented + } + + private void EnsureLockObject(int id, string name) + { + Execute.Code(db => + { + var exists = db.Exists(id); + if (exists) return string.Empty; + // be safe: delete old umbracoNode lock objects if any + db.Execute($"DELETE FROM umbracoNode WHERE id={id};"); + // then create umbracoLock object + db.Execute($"INSERT umbracoLock (id, name, value) VALUES ({id}, '{name}', 1);"); + return string.Empty; + }); + } + } +} diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionEight/AddLockTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionEight/AddLockTable.cs new file mode 100644 index 0000000000..ceff5e3537 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionEight/AddLockTable.cs @@ -0,0 +1,31 @@ +using System.Linq; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionEight +{ + [Migration("8.0.0", 100, GlobalSettings.UmbracoMigrationName)] + public class AddLockTable : MigrationBase + { + public AddLockTable(ILogger logger) + : base(logger) + { } + + public override void Up() + { + var tables = SqlSyntax.GetTablesInSchema(Context.Database).ToArray(); + if (tables.InvariantContains("umbracoLock") == false) + { + Create.Table("umbracoLock") + .WithColumn("id").AsInt32().PrimaryKey("PK_umbracoLock") + .WithColumn("value").AsInt32().NotNullable() + .WithColumn("name").AsString(64).NotNullable(); + } + } + + public override void Down() + { + // not implemented + } + } +} diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddServerRegistrationColumnsAndLock.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddServerRegistrationColumnsAndLock.cs index d605151249..c73201850c 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddServerRegistrationColumnsAndLock.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddServerRegistrationColumnsAndLock.cs @@ -23,7 +23,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZe Create.Column("isMaster").OnTable("umbracoServer").AsBoolean().NotNullable().WithDefaultValue(0); } - EnsureLockObject(Constants.System.ServersLock, "0AF5E610-A310-4B6F-925F-E928D5416AF7", "LOCK: Servers"); + EnsureLockObject(Constants.Locks.Servers, "0AF5E610-A310-4B6F-925F-E928D5416AF7", "LOCK: Servers"); } public override void Down() @@ -50,7 +50,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZe sortOrder = 0, uniqueId = new Guid(uniqueId), text = text, - nodeObjectType = new Guid(Constants.ObjectTypes.LockObject), + nodeObjectType = Constants.ObjectTypes.LockObjectGuid, createDate = DateTime.Now }); } diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index b3808ef804..7d4579611e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -1,17 +1,9 @@ using System; using System.Collections.Generic; -using System.Data; using System.Globalization; using System.Linq; -using System.Linq.Expressions; -using System.Net.Http.Headers; -using System.Text; using System.Xml.Linq; using NPoco; -using StackExchange.Profiling.Helpers.Dapper; -using Umbraco.Core.Configuration; -using Umbraco.Core.Dynamics; -using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; @@ -30,7 +22,7 @@ using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Persistence.Repositories { /// - /// Represents a repository for doing CRUD operations for + /// Represents a repository for doing CRUD operations for . /// internal class ContentRepository : RecycleBinRepository, IContentRepository { @@ -44,9 +36,9 @@ namespace Umbraco.Core.Persistence.Repositories public ContentRepository(IDatabaseUnitOfWork work, CacheHelper cacheHelper, ILogger logger, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, IContentSection contentSection, IMappingResolver mappingResolver) : base(work, cacheHelper, logger, contentSection, mappingResolver) { - if (contentTypeRepository == null) throw new ArgumentNullException("contentTypeRepository"); - if (templateRepository == null) throw new ArgumentNullException("templateRepository"); - if (tagRepository == null) throw new ArgumentNullException("tagRepository"); + if (contentTypeRepository == null) throw new ArgumentNullException(nameof(contentTypeRepository)); + if (templateRepository == null) throw new ArgumentNullException(nameof(templateRepository)); + if (tagRepository == null) throw new ArgumentNullException(nameof(tagRepository)); _contentTypeRepository = contentTypeRepository; _templateRepository = templateRepository; _tagRepository = tagRepository; @@ -73,7 +65,7 @@ namespace Umbraco.Core.Persistence.Repositories if (dto == null) return null; - var content = CreateContentFromDto(dto, dto.ContentVersionDto.VersionId, sql); + var content = CreateContentFromDto(dto, dto.ContentVersionDto.VersionId); return content; } @@ -83,7 +75,7 @@ namespace Umbraco.Core.Persistence.Repositories var sql = GetBaseQuery(false); if (ids.Any()) { - sql.Where("umbracoNode.id in (@ids)", new { ids = ids }); + sql.Where("umbracoNode.id in (@ids)", new { /*ids =*/ ids }); } //we only want the newest ones with this method @@ -98,7 +90,8 @@ namespace Umbraco.Core.Persistence.Repositories var translator = new SqlTranslator(sqlClause, query); var sql = translator.Translate() .Where(x => x.Newest) - .OrderByDescending(x => x.VersionDate) + //.OrderByDescending(x => x.VersionDate) + .OrderBy(x => x.Level) .OrderBy(x => x.SortOrder); return MapQueryDtos(Database.Fetch(sql)); @@ -173,100 +166,12 @@ namespace Umbraco.Core.Persistence.Repositories return list; } - protected override Guid NodeObjectTypeId - { - get { return new Guid(Constants.ObjectTypes.Document); } - } + protected override Guid NodeObjectTypeId => new Guid(Constants.ObjectTypes.Document); #endregion #region Overrides of VersionableRepositoryBase - public void RebuildXmlStructures(Func serializer, int groupSize = 5000, IEnumerable contentTypeIds = null) - { - - //Ok, now we need to remove the data and re-insert it, we'll do this all in one transaction too. - using (var tr = Database.GetTransaction()) - { - //Remove all the data first, if anything fails after this it's no problem the transaction will be reverted - if (contentTypeIds == null) - { - var subQuery = Sql() - .Select("DISTINCT cmsContentXml.nodeId") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId); - - var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); - Database.Execute(deleteSql); - } - else - { - foreach (var id in contentTypeIds) - { - var id1 = id; - var subQuery = Sql() - .Select("cmsDocument.nodeId") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(dto => dto.Published) - .Where( dto => dto.ContentTypeId == id1); - - var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); - Database.Execute(deleteSql); - } - } - - //now insert the data, again if something fails here, the whole transaction is reversed - if (contentTypeIds == null) - { - var query = Query.Where(x => x.Published == true); - RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); - } - else - { - foreach (var contentTypeId in contentTypeIds) - { - //copy local - var id = contentTypeId; - var query = Query.Where(x => x.Published == true && x.ContentTypeId == id && x.Trashed == false); - RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); - } - } - - tr.Complete(); - } - } - - private void RebuildXmlStructuresProcessQuery(Func serializer, IQuery query, ITransaction tr, int pageSize) - { - var pageIndex = 0; - var total = long.MinValue; - var processed = 0; - do - { - //NOTE: This is an important call, we cannot simply make a call to: - // GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending); - // because that method is used to query 'latest' content items where in this case we don't necessarily - // want latest content items because a pulished content item might not actually be the latest. - // see: http://issues.umbraco.org/issue/U4-6322 & http://issues.umbraco.org/issue/U4-5982 - var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, - MapQueryDtos, "Path", Direction.Ascending, true); - - var xmlItems = (from descendant in descendants - let xml = serializer(descendant) - select new ContentXmlDto { NodeId = descendant.Id, Xml = xml.ToDataString() }).ToArray(); - - //bulk insert it into the database - Database.BulkInsertRecords(SqlSyntax, xmlItems, tr); - - processed += xmlItems.Length; - - pageIndex++; - } while (processed < total); - } - public override IContent GetByVersion(Guid versionId) { var sql = GetBaseQuery(false); @@ -278,7 +183,7 @@ namespace Umbraco.Core.Persistence.Repositories if (dto == null) return null; - var content = CreateContentFromDto(dto, versionId, sql); + var content = CreateContentFromDto(dto, versionId); return content; } @@ -360,9 +265,7 @@ namespace Umbraco.Core.Persistence.Repositories //ensure the default template is assigned if (entity.Template == null) - { entity.Template = entity.ContentType.DefaultTemplate; - } //Ensure unique name on the same level entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name); @@ -375,10 +278,10 @@ namespace Umbraco.Core.Persistence.Repositories //NOTE Should the logic below have some kind of fallback for empty parent ids ? //Logic for setting Path, Level and SortOrder - var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); + var parent = Database.First("WHERE id = @ParentId", new { /*ParentId =*/ entity.ParentId }); var level = parent.Level + 1; var maxSortOrder = Database.ExecuteScalar( - "SELECT coalesce(max(sortOrder),-1) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", + "SELECT coalesce(max(sortOrder),-1) FROM umbracoNode WHERE parentId = @ParentId AND nodeObjectType = @NodeObjectType", new { /*ParentId =*/ entity.ParentId, NodeObjectType = NodeObjectTypeId }); var sortOrder = maxSortOrder + 1; @@ -387,7 +290,7 @@ namespace Umbraco.Core.Persistence.Repositories nodeDto.Path = parent.Path; nodeDto.Level = short.Parse(level.ToString(CultureInfo.InvariantCulture)); nodeDto.SortOrder = sortOrder; - var o = Database.IsNew(nodeDto) ? Convert.ToInt32(Database.Insert(nodeDto)) : Database.Update(nodeDto); + var o = Database.IsNew(nodeDto) ? Convert.ToInt32(Database.Insert(nodeDto)) : Database.Update(nodeDto); //Update with new correct path nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); @@ -401,6 +304,8 @@ namespace Umbraco.Core.Persistence.Repositories //Assign the same permissions to it as the parent node // http://issues.umbraco.org/issue/U4-2161 + // fixme STOP new-ing repos everywhere! + // var prepo = UnitOfWork.CreateRepository>(); var permissionsRepo = new PermissionRepository(UnitOfWork, _cacheHelper); var parentPermissions = permissionsRepo.GetPermissionsForEntity(entity.ParentId).ToArray(); //if there are parent permissions then assign them, otherwise leave null and permissions will become the @@ -448,15 +353,11 @@ namespace Umbraco.Core.Persistence.Repositories //Update Properties with its newly set Id foreach (var property in entity.Properties) - { property.Id = keyDictionary[property.PropertyTypeId]; - } //lastly, check if we are a creating a published version , then update the tags table if (entity.Published) - { - UpdatePropertyTags(entity, _tagRepository); - } + UpdateEntityTags(entity, _tagRepository); // published => update published version infos, else leave it blank if (entity.Published) @@ -476,7 +377,9 @@ namespace Umbraco.Core.Persistence.Repositories protected override void PersistUpdatedItem(IContent entity) { - var publishedState = ((Content)entity).PublishedState; + var content = (Content) entity; + var publishedState = content.PublishedState; + var publishedStateChanged = publishedState == PublishedState.Publishing || publishedState == PublishedState.Unpublishing; //check if we need to make any database changes at all if (entity.RequiresSaving(publishedState) == false) @@ -486,11 +389,11 @@ namespace Umbraco.Core.Persistence.Repositories } //check if we need to create a new version - bool shouldCreateNewVersion = entity.ShouldCreateNewVersion(publishedState); - if (shouldCreateNewVersion) + var requiresNewVersion = entity.RequiresNewVersion(publishedState); + if (requiresNewVersion) { //Updates Modified date and Version Guid - ((Content)entity).UpdatingEntity(); + content.UpdatingEntity(); } else { @@ -506,14 +409,10 @@ namespace Umbraco.Core.Persistence.Repositories //Look up parent to get and set the correct Path and update SortOrder if ParentId has changed if (entity.IsPropertyDirty("ParentId")) { - var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); + var parent = Database.First("WHERE id = @ParentId", new { /*ParentId =*/ entity.ParentId }); entity.Path = string.Concat(parent.Path, ",", entity.Id); entity.Level = parent.Level + 1; - var maxSortOrder = - Database.ExecuteScalar( - "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", - new { ParentId = entity.ParentId, NodeObjectType = NodeObjectTypeId }); - entity.SortOrder = maxSortOrder + 1; + entity.SortOrder = NextChildSortOrder(entity.ParentId); //Question: If we move a node, should we update permissions to inherit from the new parent if the parent has permissions assigned? // if we do that, then we'd need to propogate permissions all the way downward which might not be ideal for many people. @@ -522,7 +421,7 @@ namespace Umbraco.Core.Persistence.Repositories var factory = new ContentFactory(NodeObjectTypeId, entity.Id); //Look up Content entry to get Primary for updating the DTO - var contentDto = Database.SingleOrDefault("WHERE nodeId = @Id", new { Id = entity.Id }); + var contentDto = Database.SingleOrDefault("WHERE nodeId = @Id", new { /*Id =*/ entity.Id }); factory.SetPrimaryKey(contentDto.PrimaryKey); var dto = factory.BuildDto(entity); @@ -538,37 +437,17 @@ namespace Umbraco.Core.Persistence.Repositories Database.Update(newContentDto); } - //a flag that we'll use later to create the tags in the tag db table - var publishedStateChanged = false; - //If Published state has changed then previous versions should have their publish state reset. //If state has been changed to unpublished the previous versions publish state should also be reset. //if (((ICanBeDirty)entity).IsPropertyDirty("Published") && (entity.Published || publishedState == PublishedState.Unpublished)) - if (entity.ShouldClearPublishedFlagForPreviousVersions(publishedState, shouldCreateNewVersion)) - { - var publishedDocs = Database.Fetch("WHERE nodeId = @Id AND published = @IsPublished", new { Id = entity.Id, IsPublished = true }); - foreach (var doc in publishedDocs) - { - var docDto = doc; - docDto.Published = false; - Database.Update(docDto); - } - - //this is a newly published version so we'll update the tags table too (end of this method) - publishedStateChanged = true; - } + if (entity.RequiresClearPublishedFlag(publishedState, requiresNewVersion)) + ClearPublishedFlag(entity); //Look up (newest) entries by id in cmsDocument table to set newest = false - var documentDtos = Database.Fetch("WHERE nodeId = @Id AND newest = @IsNewest", new { Id = entity.Id, IsNewest = true }); - foreach (var documentDto in documentDtos) - { - var docDto = documentDto; - docDto.Newest = false; - Database.Update(docDto); - } + ClearNewestFlag(entity); var contentVersionDto = dto.ContentVersionDto; - if (shouldCreateNewVersion) + if (requiresNewVersion) { //Create a new version - cmsContentVersion //Assumes a new Version guid and Version date (modified date) has been set @@ -580,7 +459,7 @@ namespace Umbraco.Core.Persistence.Repositories else { //In order to update the ContentVersion we need to retrieve its primary key id - var contentVerDto = Database.SingleOrDefault("WHERE VersionId = @Version", new { Version = entity.Version }); + var contentVerDto = Database.SingleOrDefault("WHERE VersionId = @Version", new { /*Version =*/ entity.Version }); contentVersionDto.Id = contentVerDto.Id; Database.Update(contentVersionDto); @@ -595,7 +474,7 @@ namespace Umbraco.Core.Persistence.Repositories //Add Properties foreach (var propertyDataDto in propertyDataDtos) { - if (shouldCreateNewVersion == false && propertyDataDto.Id > 0) + if (requiresNewVersion == false && propertyDataDto.Id > 0) { Database.Update(propertyDataDto); } @@ -617,19 +496,38 @@ namespace Umbraco.Core.Persistence.Repositories } } - //lastly, check if we are a newly published version and then update the tags table - if (publishedStateChanged && entity.Published) + // tags: + if (HasTagProperty(entity)) { - UpdatePropertyTags(entity, _tagRepository); - } - else if (publishedStateChanged && (entity.Trashed || entity.Published == false)) - { - //it's in the trash or not published remove all entity tags - ClearEntityTags(entity, _tagRepository); + // if path-published, update tags, else clear tags + switch (content.PublishedState) + { + case PublishedState.Publishing: + // explicitely publishing, must update tags + UpdateEntityTags(entity, _tagRepository); + break; + case PublishedState.Unpublishing: + // explicitely unpublishing, must clear tags + ClearEntityTags(entity, _tagRepository); + break; + case PublishedState.Saving: + // saving, nothing to do + break; + case PublishedState.Published: + case PublishedState.Unpublished: + // no change, depends on path-published + // that should take care of trashing and un-trashing + if (IsPathPublished(entity)) // slightly expensive ;-( + UpdateEntityTags(entity, _tagRepository); + else + ClearEntityTags(entity, _tagRepository); + break; + } } // published => update published version infos, // else if unpublished then clear published version infos + // else leave unchanged if (entity.Published) { dto.DocumentPublishedReadOnlyDto = new DocumentPublishedReadOnlyDto @@ -639,7 +537,7 @@ namespace Umbraco.Core.Persistence.Repositories NodeId = dto.NodeId, Published = true }; - ((Content)entity).PublishedVersionGuid = dto.VersionId; + content.PublishedVersionGuid = dto.VersionId; } else if (publishedStateChanged) { @@ -650,12 +548,20 @@ namespace Umbraco.Core.Persistence.Repositories NodeId = dto.NodeId, Published = false }; - ((Content)entity).PublishedVersionGuid = default(Guid); + content.PublishedVersionGuid = default(Guid); } entity.ResetDirtyProperties(); } + private int NextChildSortOrder(int parentId) + { + var maxSortOrder = + Database.ExecuteScalar( + "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", + new { ParentId = parentId, NodeObjectType = NodeObjectTypeId }); + return maxSortOrder + 1; + } #endregion @@ -664,9 +570,7 @@ namespace Umbraco.Core.Persistence.Repositories public IEnumerable GetByPublishedVersion(IQuery query) { // we WANT to return contents in top-down order, ie parents should come before children - // ideal would be pure xml "document order" which can be achieved with: - // ORDER BY substring(path, 1, len(path) - charindex(',', reverse(path))), sortOrder - // but that's probably an overkill - sorting by level,sortOrder should be enough + // ideal would be pure xml "document order" - which we cannot achieve at database level var sqlClause = GetBaseQuery(false); var translator = new SqlTranslator(sqlClause, query); @@ -680,27 +584,42 @@ namespace Umbraco.Core.Persistence.Repositories foreach (var dto in dtos) { - //Check in the cache first. If it exists there AND it is published - // then we can use that entity. Otherwise if it is not published (which can be the case - // because we only store the 'latest' entries in the cache which might not be the published - // version) + // check cache first, if it exists and is published, use it + // it may exist and not be published as the cache has 'latest version used' var fromCache = RuntimeCache.GetCacheItem(GetCacheIdKey(dto.NodeId)); - //var fromCache = TryGetFromCache(dto.NodeId); - if (fromCache != null && fromCache.Published) - { - yield return fromCache; - } - else - { - yield return CreateContentFromDto(dto, dto.VersionId, sql); - } + yield return fromCache != null && fromCache.Published + ? fromCache + : CreateContentFromDto(dto, dto.VersionId); } } - public int CountPublished() + public int CountPublished(string contentTypeAlias = null) { - var sql = GetBaseQuery(true).Where(x => x.Trashed == false) - .Where(x => x.Published == true); + var sql = Sql(); + if (contentTypeAlias.IsNullOrWhiteSpace()) + { + sql.SelectCount() + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId && x.Trashed == false) + .Where(x => x.Published); + } + else + { + sql.SelectCount() + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.ContentTypeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId && x.Trashed == false) + .Where(x => x.Alias == contentTypeAlias) + .Where(x => x.Published); + } + return Database.ExecuteScalar(sql); } @@ -710,10 +629,10 @@ namespace Umbraco.Core.Persistence.Repositories repo.ReplaceEntityPermissions(permissionSet); } - public void ClearPublished(IContent content) + public void ClearPublishedFlag(IContent content) { - // race cond! - var documentDtos = Database.Fetch("WHERE nodeId=@id AND published=@published", new { id = content.Id, published = true }); + // no race cond if locked + var documentDtos = Database.Fetch("WHERE nodeId=@Id AND published=@IsPublished", new { /*Id =*/ content.Id, IsPublished = true }); foreach (var documentDto in documentDtos) { documentDto.Published = false; @@ -721,6 +640,17 @@ namespace Umbraco.Core.Persistence.Repositories } } + public void ClearNewestFlag(IContent content) + { + // no race cond if locked + var documentDtos = Database.Fetch("WHERE nodeId=@Id AND newest=@IsNewest", new { /*Id =*/ content.Id, IsNewest = true }); + foreach (var documentDto in documentDtos) + { + documentDto.Newest = false; + Database.Update(documentDto); + } + } + /// /// Assigns a single permission to the current content item for the specified user ids /// @@ -739,35 +669,6 @@ namespace Umbraco.Core.Persistence.Repositories return repo.GetPermissionsForEntity(entityId); } - /// - /// Adds/updates content/published xml - /// - /// - /// - public void AddOrUpdateContentXml(IContent content, Func xml) - { - _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(content, xml)); - } - - /// - /// Used to remove the content xml for a content item - /// - /// - public void DeleteContentXml(IContent content) - { - _contentXmlRepository.Delete(new ContentXmlEntity(content)); - } - - /// - /// Adds/updates preview xml - /// - /// - /// - public void AddOrUpdatePreviewXml(IContent content, Func xml) - { - _contentPreviewRepository.AddOrUpdate(new ContentPreviewEntity(content, xml)); - } - /// /// Gets paged content results /// @@ -793,42 +694,40 @@ namespace Umbraco.Core.Persistence.Repositories filterSql); } - /// - /// Returns the persisted content's preview XML structure - /// - /// - /// - public XElement GetContentXml(int contentId) + public bool IsPathPublished(IContent content) { - var sql = Sql().SelectAll().From().Where(d => d.NodeId == contentId); - var dto = Database.SingleOrDefault(sql); - if (dto == null) return null; - return XElement.Parse(dto.Xml); - } + // fail fast + if (content.Path.StartsWith("-1,-20,")) + return false; + // succeed fast + if (content.ParentId == -1) + return content.HasPublishedVersion; - /// - /// Returns the persisted content's preview XML structure - /// - /// - /// - /// - public XElement GetContentPreviewXml(int contentId, Guid version) - { - var sql = Sql().SelectAll().From() - .Where(d => d.NodeId == contentId && d.VersionId == version); - var dto = Database.SingleOrDefault(sql); - if (dto == null) return null; - return XElement.Parse(dto.Xml); + var syntaxUmbracoNode = SqlSyntax.GetQuotedTableName("umbracoNode"); + var syntaxPath = SqlSyntax.GetQuotedColumnName("path"); + var syntaxConcat = SqlSyntax.GetConcat(syntaxUmbracoNode + "." + syntaxPath, "',%'"); + + var sql = string.Format(@"SELECT COUNT({0}.{1}) +FROM {0} +JOIN {2} ON ({0}.{1}={2}.{3} AND {2}.{4}=@published) +WHERE (@path LIKE {5})", + syntaxUmbracoNode, + SqlSyntax.GetQuotedColumnName("id"), + SqlSyntax.GetQuotedTableName("cmsDocument"), + SqlSyntax.GetQuotedColumnName("nodeId"), + SqlSyntax.GetQuotedColumnName("published"), + syntaxConcat); + + var count = Database.ExecuteScalar(sql, new { @published=true, @path=content.Path }); + count += 1; // because content does not count + return count == content.Level; } #endregion #region IRecycleBinRepository members - protected override int RecycleBinId - { - get { return Constants.System.RecycleBinContent; } - } + protected override int RecycleBinId => Constants.System.RecycleBinContent; #endregion @@ -885,7 +784,7 @@ namespace Umbraco.Core.Persistence.Repositories return dtosWithContentTypes.Select(d => CreateContentFromDto( d.dto, contentTypes.First(ct => ct.Id == d.dto.ContentVersionDto.ContentDto.ContentTypeId), - templates.FirstOrDefault(tem => tem.Id == (d.dto.TemplateId.HasValue ? d.dto.TemplateId.Value : -1)), + templates.FirstOrDefault(tem => tem.Id == (d.dto.TemplateId ?? -1)), propertyData[d.dto.NodeId])); } @@ -900,7 +799,7 @@ namespace Umbraco.Core.Persistence.Repositories private IContent CreateContentFromDto(DocumentDto dto, IContentType contentType, ITemplate template, - Models.PropertyCollection propCollection) + PropertyCollection propCollection) { var factory = new ContentFactory(contentType, NodeObjectTypeId, dto.NodeId); var content = factory.BuildEntity(dto); @@ -929,9 +828,8 @@ namespace Umbraco.Core.Persistence.Repositories /// /// /// - /// /// - private IContent CreateContentFromDto(DocumentDto dto, Guid versionId, Sql docSql) + private IContent CreateContentFromDto(DocumentDto dto, Guid versionId) { var contentType = _contentTypeRepository.Get(dto.ContentVersionDto.ContentDto.ContentTypeId); @@ -979,7 +877,7 @@ namespace Umbraco.Core.Persistence.Repositories if (dto.Text.ToLowerInvariant().Equals(currentName.ToLowerInvariant())) { - currentName = nodeName + string.Format(" ({0})", uniqueNumber); + currentName = nodeName + $" ({uniqueNumber})"; uniqueNumber++; } } @@ -987,5 +885,151 @@ namespace Umbraco.Core.Persistence.Repositories return currentName; } + + #region Xml - Should Move! + + public void RebuildXmlStructures(Func serializer, int groupSize = 5000, IEnumerable contentTypeIds = null) + { + + //Ok, now we need to remove the data and re-insert it, we'll do this all in one transaction too. + using (var tr = Database.GetTransaction()) + { + //Remove all the data first, if anything fails after this it's no problem the transaction will be reverted + if (contentTypeIds == null) + { + var subQuery = Sql() + .Select("DISTINCT cmsContentXml.nodeId") + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId); + + var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); + Database.Execute(deleteSql); + } + else + { + foreach (var id in contentTypeIds) + { + var id1 = id; + var subQuery = Sql() + .Select("cmsDocument.nodeId") + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .Where(dto => dto.Published) + .Where(dto => dto.ContentTypeId == id1); + + var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); + Database.Execute(deleteSql); + } + } + + //now insert the data, again if something fails here, the whole transaction is reversed + if (contentTypeIds == null) + { + var query = Query.Where(x => x.Published); + RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); + } + else + { + foreach (var contentTypeId in contentTypeIds) + { + //copy local + var id = contentTypeId; + var query = Query.Where(x => x.Published && x.ContentTypeId == id && x.Trashed == false); + RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); + } + } + + tr.Complete(); + } + } + + private void RebuildXmlStructuresProcessQuery(Func serializer, IQuery query, ITransaction tr, int pageSize) + { + var pageIndex = 0; + long total; + var processed = 0; + do + { + //NOTE: This is an important call, we cannot simply make a call to: + // GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending); + // because that method is used to query 'latest' content items where in this case we don't necessarily + // want latest content items because a pulished content item might not actually be the latest. + // see: http://issues.umbraco.org/issue/U4-6322 & http://issues.umbraco.org/issue/U4-5982 + var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, + MapQueryDtos, "Path", Direction.Ascending, true); + + var xmlItems = (from descendant in descendants + let xml = serializer(descendant) + select new ContentXmlDto { NodeId = descendant.Id, Xml = xml.ToDataString() }).ToArray(); + + //bulk insert it into the database + Database.BulkInsertRecords(SqlSyntax, xmlItems, tr); + + processed += xmlItems.Length; + + pageIndex++; + } while (processed < total); + } + + /// + /// Adds/updates content/published xml + /// + /// + /// + public void AddOrUpdateContentXml(IContent content, Func xml) + { + _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(content, xml)); + } + + /// + /// Used to remove the content xml for a content item + /// + /// + public void DeleteContentXml(IContent content) + { + _contentXmlRepository.Delete(new ContentXmlEntity(content)); + } + + /// + /// Adds/updates preview xml + /// + /// + /// + public void AddOrUpdatePreviewXml(IContent content, Func xml) + { + _contentPreviewRepository.AddOrUpdate(new ContentPreviewEntity(content, xml)); + } + + /// + /// Returns the persisted content's preview XML structure + /// + /// + /// + public XElement GetContentXml(int contentId) + { + var sql = Sql().SelectAll().From().Where(d => d.NodeId == contentId); + var dto = Database.SingleOrDefault(sql); + if (dto == null) return null; + return XElement.Parse(dto.Xml); + } + + /// + /// Returns the persisted content's preview XML structure + /// + /// + /// + /// + public XElement GetContentPreviewXml(int contentId, Guid version) + { + var sql = Sql().SelectAll().From() + .Where(d => d.NodeId == contentId && d.VersionId == version); + var dto = Database.SingleOrDefault(sql); + if (dto == null) return null; + return XElement.Parse(dto.Xml); + } + + #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs index 40cf02af38..aacd51c685 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs @@ -3,26 +3,21 @@ using System.Collections.Generic; using System.Linq; using NPoco; using Umbraco.Core.Cache; -using Umbraco.Core.Events; -using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Rdbms; - -using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Mappers; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Persistence.UnitOfWork; -using Umbraco.Core.Services; namespace Umbraco.Core.Persistence.Repositories { /// /// Represents a repository for doing CRUD operations for /// - internal class ContentTypeRepository : ContentTypeBaseRepository, IContentTypeRepository + internal class ContentTypeRepository : ContentTypeRepositoryBase, IContentTypeRepository { private readonly ITemplateRepository _templateRepository; @@ -51,6 +46,23 @@ namespace Umbraco.Core.Persistence.Repositories return GetAll().FirstOrDefault(x => x.Id == id); } + protected override IContentType PerformGet(Guid id) + { + //use the underlying GetAll which will force cache all content types + return GetAll().FirstOrDefault(x => x.Key == id); + } + + protected override IContentType PerformGet(string alias) + { + //use the underlying GetAll which will force cache all content types + return GetAll().FirstOrDefault(x => x.Alias.InvariantEquals(alias)); + } + + protected override bool PerformExists(Guid id) + { + return GetAll().FirstOrDefault(x => x.Key == id) != null; + } + protected override IEnumerable PerformGetAll(params int[] ids) { if (ids.Any()) @@ -63,6 +75,12 @@ namespace Umbraco.Core.Persistence.Repositories return ContentTypeQueryMapper.GetContentTypes(Database, SqlSyntax, this, _templateRepository); } + + protected override IEnumerable PerformGetAll(params Guid[] ids) + { + // use the underlying GetAll which will force cache all content types + return ids.Any() ? GetAll().Where(x => ids.Contains(x.Key)) : GetAll(); + } protected override IEnumerable PerformGetByQuery(IQuery query) { var sqlClause = GetBaseQuery(false); @@ -154,28 +172,14 @@ namespace Umbraco.Core.Persistence.Repositories protected override IEnumerable GetDeleteClauses() { - var list = new List - { - "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @Id", - "DELETE FROM umbracoUser2NodePermission WHERE nodeId = @Id", - "DELETE FROM cmsTagRelationship WHERE nodeId = @Id", - "DELETE FROM cmsContentTypeAllowedContentType WHERE Id = @Id", - "DELETE FROM cmsContentTypeAllowedContentType WHERE AllowedId = @Id", - "DELETE FROM cmsContentType2ContentType WHERE parentContentTypeId = @Id", - "DELETE FROM cmsContentType2ContentType WHERE childContentTypeId = @Id", - "DELETE FROM cmsPropertyType WHERE contentTypeId = @Id", - "DELETE FROM cmsPropertyTypeGroup WHERE contenttypeNodeId = @Id", - "DELETE FROM cmsDocumentType WHERE contentTypeNodeId = @Id", - "DELETE FROM cmsContentType WHERE nodeId = @Id", - "DELETE FROM umbracoNode WHERE id = @Id" - }; - return list; + var l = (List) base.GetDeleteClauses(); // we know it's a list + l.Add("DELETE FROM cmsDocumentType WHERE contentTypeNodeId = @Id"); + l.Add("DELETE FROM cmsContentType WHERE nodeId = @Id"); + l.Add("DELETE FROM umbracoNode WHERE id = @Id"); + return l; } - protected override Guid NodeObjectTypeId - { - get { return new Guid(Constants.ObjectTypes.DocumentType); } - } + protected override Guid NodeObjectTypeId => Constants.ObjectTypes.DocumentTypeGuid; /// /// Deletes a content type @@ -290,36 +294,5 @@ namespace Umbraco.Core.Persistence.Repositories entity.ResetDirtyProperties(); } - - protected override IContentType PerformGet(Guid id) - { - //use the underlying GetAll which will force cache all content types - return GetAll().FirstOrDefault(x => x.Key == id); - } - - protected override IContentType PerformGet(string alias) - { - //use the underlying GetAll which will force cache all content types - return GetAll().FirstOrDefault(x => x.Alias.InvariantEquals(alias)); - } - - protected override IEnumerable PerformGetAll(params Guid[] ids) - { - //use the underlying GetAll which will force cache all content types - - if (ids.Any()) - { - return GetAll().Where(x => ids.Contains(x.Key)); - } - else - { - return GetAll(); - } - } - - protected override bool PerformExists(Guid id) - { - return GetAll().FirstOrDefault(x => x.Key == id) != null; - } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepositoryBase.cs similarity index 94% rename from src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs rename to src/Umbraco.Core/Persistence/Repositories/ContentTypeRepositoryBase.cs index a446a0a390..cd1ef66168 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepositoryBase.cs @@ -1,1230 +1,1274 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Diagnostics; -using System.Globalization; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using NPoco; -using Umbraco.Core.Events; -using Umbraco.Core.Exceptions; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Core.Models.EntityBase; -using Umbraco.Core.Models.Rdbms; - -using Umbraco.Core.Persistence.Factories; -using Umbraco.Core.Persistence.Mappers; -using Umbraco.Core.Persistence.Querying; -using Umbraco.Core.Persistence.SqlSyntax; -using Umbraco.Core.Persistence.UnitOfWork; -using Umbraco.Core.Services; - -namespace Umbraco.Core.Persistence.Repositories -{ - /// - /// Represent an abstract Repository for ContentType based repositories - /// - /// Exposes shared functionality - /// - internal abstract class ContentTypeBaseRepository : NPocoRepositoryBase, IReadRepository - where TEntity : class, IContentTypeComposition - { - protected ContentTypeBaseRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, IMappingResolver mappingResolver) - : base(work, cache, logger, mappingResolver) - { - } - - public IEnumerable> Move(TEntity toMove, EntityContainer container) - { - var parentId = Constants.System.Root; - if (container != null) - { - // Check on paths - if ((string.Format(",{0},", container.Path)).IndexOf(string.Format(",{0},", toMove.Id), StringComparison.Ordinal) > -1) - { - throw new DataOperationException(MoveOperationStatusType.FailedNotAllowedByPath); - } - parentId = container.Id; - } - - //used to track all the moved entities to be given to the event - var moveInfo = new List> - { - new MoveEventInfo(toMove, toMove.Path, parentId) - }; - - - // get the level delta (old pos to new pos) - var levelDelta = container == null - ? 1 - toMove.Level - : container.Level + 1 - toMove.Level; - - // move to parent (or -1), update path, save - toMove.ParentId = parentId; - var toMovePath = toMove.Path + ","; // save before changing - toMove.Path = (container == null ? Constants.System.Root.ToString() : container.Path) + "," + toMove.Id; - toMove.Level = container == null ? 1 : container.Level + 1; - AddOrUpdate(toMove); - - //update all descendants, update in order of level - var descendants = GetByQuery(Query.Where(type => type.Path.StartsWith(toMovePath))); - var paths = new Dictionary(); - paths[toMove.Id] = toMove.Path; - - foreach (var descendant in descendants.OrderBy(x => x.Level)) - { - moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); - - descendant.Path = paths[descendant.Id] = paths[descendant.ParentId] + "," + descendant.Id; - descendant.Level += levelDelta; - - AddOrUpdate(descendant); - } - - return moveInfo; - } - /// - /// Returns the content type ids that match the query - /// - /// - /// - protected IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = Sql() - .SelectAll() - .From() - .RightJoin() - .On(left => left.Id, right => right.PropertyTypeGroupId) - .InnerJoin() - .On(left => left.DataTypeId, right => right.DataTypeId); - - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate() - .OrderBy(x => x.PropertyTypeGroupId); - - return Database - .FetchOneToMany(x => x.PropertyTypeDtos, sql) - .Select(x => x.ContentTypeNodeId).Distinct(); - } - - protected virtual PropertyType CreatePropertyType(string propertyEditorAlias, DataTypeDatabaseType dbType, string propertyTypeAlias) - { - return new PropertyType(propertyEditorAlias, dbType, propertyTypeAlias); - } - - protected void PersistNewBaseContentType(IContentTypeComposition entity) - { - var factory = new ContentTypeFactory(); - var dto = factory.BuildContentTypeDto(entity); - - //Cannot add a duplicate content type type - var exists = Database.ExecuteScalar(@"SELECT COUNT(*) FROM cmsContentType -INNER JOIN umbracoNode ON cmsContentType.nodeId = umbracoNode.id -WHERE cmsContentType." + SqlSyntax.GetQuotedColumnName("alias") + @"= @alias -AND umbracoNode.nodeObjectType = @objectType", - new { alias = entity.Alias, objectType = NodeObjectTypeId }); - if (exists > 0) - { - throw new DuplicateNameException("An item with the alias " + entity.Alias + " already exists"); - } - - //Logic for setting Path, Level and SortOrder - var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); - int level = parent.Level + 1; - int sortOrder = - Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoNode WHERE parentID = @ParentId AND nodeObjectType = @NodeObjectType", - new { ParentId = entity.ParentId, NodeObjectType = NodeObjectTypeId }); - - //Create the (base) node data - umbracoNode - var nodeDto = dto.NodeDto; - nodeDto.Path = parent.Path; - nodeDto.Level = short.Parse(level.ToString(CultureInfo.InvariantCulture)); - nodeDto.SortOrder = sortOrder; - var o = Database.IsNew(nodeDto) ? Convert.ToInt32(Database.Insert(nodeDto)) : Database.Update(nodeDto); - - //Update with new correct path - nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); - Database.Update(nodeDto); - - //Update entity with correct values - entity.Id = nodeDto.NodeId; //Set Id on entity to ensure an Id is set - entity.Path = nodeDto.Path; - entity.SortOrder = sortOrder; - entity.Level = level; - - //Insert new ContentType entry - dto.NodeId = nodeDto.NodeId; - Database.Insert(dto); - - //Insert ContentType composition in new table - foreach (var composition in entity.ContentTypeComposition) - { - if (composition.Id == entity.Id) continue;//Just to ensure that we aren't creating a reference to ourself. - - if (composition.HasIdentity) - { - Database.Insert(new ContentType2ContentTypeDto { ParentId = composition.Id, ChildId = entity.Id }); - } - else - { - //Fallback for ContentTypes with no identity - var contentTypeDto = Database.FirstOrDefault("WHERE alias = @Alias", new { Alias = composition.Alias }); - if (contentTypeDto != null) - { - Database.Insert(new ContentType2ContentTypeDto { ParentId = contentTypeDto.NodeId, ChildId = entity.Id }); - } - } - } - - //Insert collection of allowed content types - foreach (var allowedContentType in entity.AllowedContentTypes) - { - Database.Insert(new ContentTypeAllowedContentTypeDto - { - Id = entity.Id, - AllowedId = allowedContentType.Id.Value, - SortOrder = allowedContentType.SortOrder - }); - } - - var propertyFactory = new PropertyGroupFactory(nodeDto.NodeId); - - //Insert Tabs - foreach (var propertyGroup in entity.PropertyGroups) - { - var tabDto = propertyFactory.BuildGroupDto(propertyGroup); - var primaryKey = Convert.ToInt32(Database.Insert(tabDto)); - propertyGroup.Id = primaryKey;//Set Id on PropertyGroup - - //Ensure that the PropertyGroup's Id is set on the PropertyTypes within a group - //unless the PropertyGroupId has already been changed. - foreach (var propertyType in propertyGroup.PropertyTypes) - { - if (propertyType.IsPropertyDirty("PropertyGroupId") == false) - { - var tempGroup = propertyGroup; - propertyType.PropertyGroupId = new Lazy(() => tempGroup.Id); - } - } - } - - //Insert PropertyTypes - foreach (var propertyType in entity.PropertyTypes) - { - var tabId = propertyType.PropertyGroupId != null ? propertyType.PropertyGroupId.Value : default(int); - //If the Id of the DataType is not set, we resolve it from the db by its PropertyEditorAlias - if (propertyType.DataTypeDefinitionId == 0 || propertyType.DataTypeDefinitionId == default(int)) - { - AssignDataTypeFromPropertyEditor(propertyType); - } - var propertyTypeDto = propertyFactory.BuildPropertyTypeDto(tabId, propertyType); - int typePrimaryKey = Convert.ToInt32(Database.Insert(propertyTypeDto)); - propertyType.Id = typePrimaryKey; //Set Id on new PropertyType - - //Update the current PropertyType with correct PropertyEditorAlias and DatabaseType - var dataTypeDto = Database.FirstOrDefault("WHERE nodeId = @Id", new { Id = propertyTypeDto.DataTypeId }); - propertyType.PropertyEditorAlias = dataTypeDto.PropertyEditorAlias; - propertyType.DataTypeDatabaseType = dataTypeDto.DbType.EnumParse(true); - } - } - - protected void PersistUpdatedBaseContentType(IContentTypeComposition entity) - { - var factory = new ContentTypeFactory(); - var dto = factory.BuildContentTypeDto(entity); - - // ensure the alias is not used already - var exists = Database.ExecuteScalar(@"SELECT COUNT(*) FROM cmsContentType -INNER JOIN umbracoNode ON cmsContentType.nodeId = umbracoNode.id -WHERE cmsContentType." + SqlSyntax.GetQuotedColumnName("alias") + @"= @alias -AND umbracoNode.nodeObjectType = @objectType -AND umbracoNode.id <> @id", - new { id = dto.NodeId, alias = dto.Alias, objectType = NodeObjectTypeId }); - if (exists > 0) - throw new DuplicateNameException("An item with the alias " + dto.Alias + " already exists"); - - // handle (update) the node - var nodeDto = dto.NodeDto; - Database.Update(nodeDto); - - // fixme - why? we are UPDATING so we should ALREADY have a PK! - //Look up ContentType entry to get PrimaryKey for updating the DTO - var dtoPk = Database.First("WHERE nodeId = @Id", new { Id = entity.Id }); - dto.PrimaryKey = dtoPk.PrimaryKey; - Database.Update(dto); - - // handle (delete then recreate) compositions - Database.Delete("WHERE childContentTypeId = @Id", new { Id = entity.Id }); - foreach (var composition in entity.ContentTypeComposition) - Database.Insert(new ContentType2ContentTypeDto { ParentId = composition.Id, ChildId = entity.Id }); - - //Removing a ContentType from a composition (U4-1690) - //1. Find content based on the current ContentType: entity.Id - //2. Find all PropertyTypes on the ContentType that was removed - tracked id (key) - //3. Remove properties based on property types from the removed content type where the content ids correspond to those found in step one - var compositionBase = entity as ContentTypeCompositionBase; - if (compositionBase != null && compositionBase.RemovedContentTypeKeyTracker != null && - compositionBase.RemovedContentTypeKeyTracker.Any()) - { - //Find Content based on the current ContentType - var sql = Sql() - .SelectAll() - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(x => x.NodeObjectType == new Guid(Constants.ObjectTypes.Document)) - .Where(x => x.ContentTypeId == entity.Id); - - var contentDtos = Database.Fetch(sql); - //Loop through all tracked keys, which corresponds to the ContentTypes that has been removed from the composition - foreach (var key in compositionBase.RemovedContentTypeKeyTracker) - { - //Find PropertyTypes for the removed ContentType - var propertyTypes = Database.Fetch("WHERE contentTypeId = @Id", new { Id = key }); - //Loop through the Content that is based on the current ContentType in order to remove the Properties that are - //based on the PropertyTypes that belong to the removed ContentType. - foreach (var contentDto in contentDtos) - { - foreach (var propertyType in propertyTypes) - { - var nodeId = contentDto.NodeId; - var propertyTypeId = propertyType.Id; - var propertySql = Sql() - .Select("cmsPropertyData.id") - .From() - .InnerJoin() - .On(left => left.PropertyTypeId, right => right.Id) - .Where(x => x.NodeId == nodeId) - .Where(x => x.Id == propertyTypeId); - - //Finally delete the properties that match our criteria for removing a ContentType from the composition - Database.Delete(new Sql("WHERE id IN (" + propertySql.SQL + ")", propertySql.Arguments)); - } - } - } - } - - //Delete the allowed content type entries before adding the updated collection - Database.Delete("WHERE Id = @Id", new { Id = entity.Id }); - //Insert collection of allowed content types - foreach (var allowedContentType in entity.AllowedContentTypes) - { - Database.Insert(new ContentTypeAllowedContentTypeDto - { - Id = entity.Id, - AllowedId = allowedContentType.Id.Value, - SortOrder = allowedContentType.SortOrder - }); - } - - - if (((ICanBeDirty)entity).IsPropertyDirty("PropertyTypes") || entity.PropertyTypes.Any(x => x.IsDirty())) - { - //Delete PropertyTypes by excepting entries from db with entries from collections - var dbPropertyTypes = Database.Fetch("WHERE contentTypeId = @Id", new { Id = entity.Id }); - var dbPropertyTypeAlias = dbPropertyTypes.Select(x => x.Id); - var entityPropertyTypes = entity.PropertyTypes.Where(x => x.HasIdentity).Select(x => x.Id); - var items = dbPropertyTypeAlias.Except(entityPropertyTypes); - foreach (var item in items) - { - //Before a PropertyType can be deleted, all Properties based on that PropertyType should be deleted. - Database.Delete("WHERE propertyTypeId = @Id", new { Id = item }); - Database.Delete("WHERE propertytypeid = @Id", new { Id = item }); - Database.Delete("WHERE contentTypeId = @Id AND id = @PropertyTypeId", - new { Id = entity.Id, PropertyTypeId = item }); - } - } - - if (entity.IsPropertyDirty("PropertyGroups") || entity.PropertyGroups.Any(x => x.IsDirty())) - { - // todo - // we used to try to propagate tabs renaming downstream, relying on ParentId, but - // 1) ParentId makes no sense (if a tab can be inherited from multiple composition - // types) so we would need to figure things out differently, visiting downstream - // content types and looking for tabs with the same name... - // 2) It was not deployable as changing a content type changes other content types - // that was not deterministic, because it would depend on the order of the changes. - // That last point could be fixed if (1) is fixed, but then it still is an issue with - // deploy because changing a content type changes other content types that are not - // dependencies but dependents, and then what? - // - // So... for the time being, all renaming propagation is disabled. We just don't do it. - - // (all gone) - - // delete tabs that do not exist anymore - // get the tabs that are currently existing (in the db) - // get the tabs that we want, now - // and derive the tabs that we want to delete - var existingPropertyGroups = Database.Fetch("WHERE contentTypeNodeId = @id", new { id = entity.Id }) - .Select(x => x.Id) - .ToList(); - var newPropertyGroups = entity.PropertyGroups.Select(x => x.Id).ToList(); - var tabsToDelete = existingPropertyGroups - .Except(newPropertyGroups) - .ToArray(); - - // move properties to generic properties, and delete the tabs - if (tabsToDelete.Length > 0) - { - Database.Update("SET propertyTypeGroupId=NULL WHERE propertyTypeGroupId IN (@ids)", new { ids = tabsToDelete }); - Database.Delete("WHERE id IN (@ids)", new { ids = tabsToDelete }); - } - } - var propertyGroupFactory = new PropertyGroupFactory(entity.Id); - - //Run through all groups to insert or update entries - foreach (var propertyGroup in entity.PropertyGroups) - { - var tabDto = propertyGroupFactory.BuildGroupDto(propertyGroup); - int groupPrimaryKey = propertyGroup.HasIdentity - ? Database.Update(tabDto) - : Convert.ToInt32(Database.Insert(tabDto)); - if (propertyGroup.HasIdentity == false) - propertyGroup.Id = groupPrimaryKey; //Set Id on new PropertyGroup - - //Ensure that the PropertyGroup's Id is set on the PropertyTypes within a group - //unless the PropertyGroupId has already been changed. - foreach (var propertyType in propertyGroup.PropertyTypes) - { - if (propertyType.IsPropertyDirty("PropertyGroupId") == false) - { - var tempGroup = propertyGroup; - propertyType.PropertyGroupId = new Lazy(() => tempGroup.Id); - } - } - } - - //Run through all PropertyTypes to insert or update entries - foreach (var propertyType in entity.PropertyTypes) - { - var tabId = propertyType.PropertyGroupId != null ? propertyType.PropertyGroupId.Value : default(int); - //If the Id of the DataType is not set, we resolve it from the db by its PropertyEditorAlias - if (propertyType.DataTypeDefinitionId == 0 || propertyType.DataTypeDefinitionId == default(int)) - { - AssignDataTypeFromPropertyEditor(propertyType); - } - - //validate the alias! - ValidateAlias(propertyType); - - var propertyTypeDto = propertyGroupFactory.BuildPropertyTypeDto(tabId, propertyType); - int typePrimaryKey = propertyType.HasIdentity - ? Database.Update(propertyTypeDto) - : Convert.ToInt32(Database.Insert(propertyTypeDto)); - if (propertyType.HasIdentity == false) - propertyType.Id = typePrimaryKey; //Set Id on new PropertyType - } - } - - protected IEnumerable GetAllowedContentTypeIds(int id) - { - var sql = Sql() - .SelectAll() - .From() - .LeftJoin() - .On(left => left.AllowedId, right => right.NodeId) - .Where(x => x.Id == id); - - var allowedContentTypeDtos = Database.Fetch(sql); - return allowedContentTypeDtos.Select(x => new ContentTypeSort(new Lazy(() => x.AllowedId), x.SortOrder, x.ContentTypeDto.Alias)).ToList(); - } - - protected PropertyGroupCollection GetPropertyGroupCollection(int id, DateTime createDate, DateTime updateDate) - { - var sql = Sql() - .SelectAll() - .From() - .LeftJoin() - .On(left => left.Id, right => right.PropertyTypeGroupId) - .LeftJoin() - .On(left => left.DataTypeId, right => right.DataTypeId) - .Where(x => x.ContentTypeNodeId == id) - .OrderBy(x => x.Id); - - - var dtos = Database - .Fetch(sql); - - var propertyGroupFactory = new PropertyGroupFactory(id, createDate, updateDate, CreatePropertyType); - var propertyGroups = propertyGroupFactory.BuildEntity(dtos); - return new PropertyGroupCollection(propertyGroups); - } - - protected PropertyTypeCollection GetPropertyTypeCollection(int id, DateTime createDate, DateTime updateDate) - { - var sql = Sql() - .SelectAll() - .From() - .InnerJoin() - .On(left => left.DataTypeId, right => right.DataTypeId) - .Where(x => x.ContentTypeId == id); - - var dtos = Database.Fetch(sql); - - //TODO Move this to a PropertyTypeFactory - var list = new List(); - foreach (var dto in dtos.Where(x => (x.PropertyTypeGroupId > 0) == false)) - { - var propType = CreatePropertyType(dto.DataTypeDto.PropertyEditorAlias, dto.DataTypeDto.DbType.EnumParse(true), dto.Alias); - propType.DataTypeDefinitionId = dto.DataTypeId; - propType.Description = dto.Description; - propType.Id = dto.Id; - propType.Key = dto.UniqueId; - propType.Name = dto.Name; - propType.Mandatory = dto.Mandatory; - propType.SortOrder = dto.SortOrder; - propType.ValidationRegExp = dto.ValidationRegExp; - propType.CreateDate = createDate; - propType.UpdateDate = updateDate; - list.Add(propType); - } - //Reset dirty properties - Parallel.ForEach(list, currentFile => currentFile.ResetDirtyProperties(false)); - - return new PropertyTypeCollection(list); - } - - protected void ValidateAlias(PropertyType pt) - { - Mandate.That(string.IsNullOrEmpty(pt.Alias) == false, - () => - { - var message = - string.Format( - "{0} '{1}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias.", - "Property Type", - pt.Name); - var exception = new InvalidOperationException(message); - - Logger.Error>(message, exception); - throw exception; - }); - } - - protected void ValidateAlias(TEntity entity) - { - Mandate.That(string.IsNullOrEmpty(entity.Alias) == false, - () => - { - var message = - string.Format( - "{0} '{1}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias.", - typeof(TEntity).Name, - entity.Name); - var exception = new InvalidOperationException(message); - - Logger.Error>(message, exception); - throw exception; - }); - } - - /// - /// Try to set the data type id based on its ControlId - /// - /// - private void AssignDataTypeFromPropertyEditor(PropertyType propertyType) - { - //we cannot try to assign a data type of it's empty - if (propertyType.PropertyEditorAlias.IsNullOrWhiteSpace() == false) - { - var sql = Sql() - .SelectAll() - .From() - .Where("propertyEditorAlias = @propertyEditorAlias", new { propertyEditorAlias = propertyType.PropertyEditorAlias }) - .OrderBy(typeDto => typeDto.DataTypeId); - var datatype = Database.FirstOrDefault(sql); - //we cannot assign a data type if one was not found - if (datatype != null) - { - propertyType.DataTypeDefinitionId = datatype.DataTypeId; - } - else - { - Logger.Warn>("Could not assign a data type for the property type " + propertyType.Alias + " since no data type was found with a property editor " + propertyType.PropertyEditorAlias); - } - } - } - - internal static class ContentTypeQueryMapper - { - - public class AssociatedTemplate - { - public AssociatedTemplate(int templateId, string @alias, string templateName) - { - TemplateId = templateId; - Alias = alias; - TemplateName = templateName; - } - - public int TemplateId { get; set; } - public string Alias { get; set; } - public string TemplateName { get; set; } - - protected bool Equals(AssociatedTemplate other) - { - return TemplateId == other.TemplateId; - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((AssociatedTemplate)obj); - } - - public override int GetHashCode() - { - return TemplateId; - } - } - - public static IEnumerable GetMediaTypes( - Database db, ISqlSyntaxProvider sqlSyntax, - TRepo contentTypeRepository) - where TRepo : IReadRepository - { - IDictionary> allParentMediaTypeIds; - var mediaTypes = MapMediaTypes(db, sqlSyntax, out allParentMediaTypeIds) - .ToArray(); - - MapContentTypeChildren(mediaTypes, db, sqlSyntax, contentTypeRepository, allParentMediaTypeIds); - - return mediaTypes; - } - - public static IEnumerable GetContentTypes( - Database db, ISqlSyntaxProvider sqlSyntax, - TRepo contentTypeRepository, - ITemplateRepository templateRepository) - where TRepo : IReadRepository - { - IDictionary> allAssociatedTemplates; - IDictionary> allParentContentTypeIds; - var contentTypes = MapContentTypes(db, sqlSyntax, out allAssociatedTemplates, out allParentContentTypeIds) - .ToArray(); - - if (contentTypes.Any()) - { - MapContentTypeTemplates( - contentTypes, db, contentTypeRepository, templateRepository, allAssociatedTemplates); - - MapContentTypeChildren( - contentTypes, db, sqlSyntax, contentTypeRepository, allParentContentTypeIds); - } - - return contentTypes; - } - - internal static void MapContentTypeChildren(IContentTypeComposition[] contentTypes, - Database db, ISqlSyntaxProvider sqlSyntax, - TRepo contentTypeRepository, - IDictionary> allParentContentTypeIds) - where TRepo : IReadRepository - { - //NOTE: SQL call #2 - - var ids = contentTypes.Select(x => x.Id).ToArray(); - IDictionary allPropGroups; - IDictionary allPropTypes; - MapGroupsAndProperties(ids, db, sqlSyntax, out allPropTypes, out allPropGroups); - - foreach (var contentType in contentTypes) - { - contentType.PropertyGroups = allPropGroups[contentType.Id]; - contentType.NoGroupPropertyTypes = allPropTypes[contentType.Id]; - } - - //NOTE: SQL call #3++ - - if (allParentContentTypeIds != null) - { - var allParentIdsAsArray = allParentContentTypeIds.SelectMany(x => x.Value).Distinct().ToArray(); - if (allParentIdsAsArray.Any()) - { - var allParentContentTypes = contentTypes.Where(x => allParentIdsAsArray.Contains(x.Id)).ToArray(); - - foreach (var contentType in contentTypes) - { - var entityId = contentType.Id; - - var parentContentTypes = allParentContentTypes.Where(x => - { - var parentEntityId = x.Id; - - return allParentContentTypeIds[entityId].Contains(parentEntityId); - }); - foreach (var parentContentType in parentContentTypes) - { - var result = contentType.AddContentType(parentContentType); - //Do something if adding fails? (Should hopefully not be possible unless someone created a circular reference) - } - - //on initial construction we don't want to have dirty properties tracked - // http://issues.umbraco.org/issue/U4-1946 - ((Entity)contentType).ResetDirtyProperties(false); - } - } - } - - - } - - internal static void MapContentTypeTemplates(IContentType[] contentTypes, - Database db, - TRepo contentTypeRepository, - ITemplateRepository templateRepository, - IDictionary> associatedTemplates) - where TRepo : IReadRepository - { - if (associatedTemplates == null || associatedTemplates.Any() == false) return; - - //NOTE: SQL call #3++ - //SEE: http://issues.umbraco.org/issue/U4-5174 to fix this - - var templateIds = associatedTemplates.SelectMany(x => x.Value).Select(x => x.TemplateId) - .Distinct() - .ToArray(); - - var templates = (templateIds.Any() - ? templateRepository.GetAll(templateIds) - : Enumerable.Empty()).ToArray(); - - foreach (var contentType in contentTypes) - { - var entityId = contentType.Id; - - var associatedTemplateIds = associatedTemplates[entityId].Select(x => x.TemplateId) - .Distinct() - .ToArray(); - - contentType.AllowedTemplates = (associatedTemplateIds.Any() - ? templates.Where(x => associatedTemplateIds.Contains(x.Id)) - : Enumerable.Empty()).ToArray(); - } - - - } - - internal static IEnumerable MapMediaTypes(Database db, ISqlSyntaxProvider sqlSyntax, - out IDictionary> parentMediaTypeIds) - { - Mandate.ParameterNotNull(db, "db"); - - var sql = @"SELECT cmsContentType.pk as ctPk, cmsContentType.alias as ctAlias, cmsContentType.allowAtRoot as ctAllowAtRoot, cmsContentType.description as ctDesc, - cmsContentType.icon as ctIcon, cmsContentType.isContainer as ctIsContainer, cmsContentType.nodeId as ctId, cmsContentType.thumbnail as ctThumb, - AllowedTypes.AllowedId as ctaAllowedId, AllowedTypes.SortOrder as ctaSortOrder, AllowedTypes.alias as ctaAlias, - ParentTypes.parentContentTypeId as chtParentId, ParentTypes.parentContentTypeKey as chtParentKey, - umbracoNode.createDate as nCreateDate, umbracoNode." + sqlSyntax.GetQuotedColumnName("level") + @" as nLevel, umbracoNode.nodeObjectType as nObjectType, umbracoNode.nodeUser as nUser, - umbracoNode.parentID as nParentId, umbracoNode." + sqlSyntax.GetQuotedColumnName("path") + @" as nPath, umbracoNode.sortOrder as nSortOrder, umbracoNode." + sqlSyntax.GetQuotedColumnName("text") + @" as nName, umbracoNode.trashed as nTrashed, - umbracoNode.uniqueID as nUniqueId - FROM cmsContentType - INNER JOIN umbracoNode - ON cmsContentType.nodeId = umbracoNode.id - LEFT JOIN ( - SELECT cmsContentTypeAllowedContentType.Id, cmsContentTypeAllowedContentType.AllowedId, cmsContentType.alias, cmsContentTypeAllowedContentType.SortOrder - FROM cmsContentTypeAllowedContentType - INNER JOIN cmsContentType - ON cmsContentTypeAllowedContentType.AllowedId = cmsContentType.nodeId - ) AllowedTypes - ON AllowedTypes.Id = cmsContentType.nodeId - LEFT JOIN ( - SELECT cmsContentType2ContentType.parentContentTypeId, umbracoNode.uniqueID AS parentContentTypeKey, cmsContentType2ContentType.childContentTypeId - FROM cmsContentType2ContentType - INNER JOIN umbracoNode - ON cmsContentType2ContentType.parentContentTypeId = umbracoNode." + sqlSyntax.GetQuotedColumnName("id") + @" - ) ParentTypes - ON ParentTypes.childContentTypeId = cmsContentType.nodeId - WHERE (umbracoNode.nodeObjectType = @nodeObjectType) - ORDER BY ctId"; - - var result = db.Fetch(sql, new { nodeObjectType = new Guid(Constants.ObjectTypes.MediaType) }); - - if (result.Any() == false) - { - parentMediaTypeIds = null; - return Enumerable.Empty(); - } - - parentMediaTypeIds = new Dictionary>(); - var mappedMediaTypes = new List(); - - //loop through each result and fill in our required values, each row will contain different requried data than the rest. - // it is much quicker to iterate each result and populate instead of looking up the values over and over in the result like - // we used to do. - var queue = new Queue(result); - var currAllowedContentTypes = new List(); - - while (queue.Count > 0) - { - var ct = queue.Dequeue(); - - //check for allowed content types - int? allowedCtId = ct.ctaAllowedId; - int? allowedCtSort = ct.ctaSortOrder; - string allowedCtAlias = ct.ctaAlias; - if (allowedCtId.HasValue && allowedCtSort.HasValue && allowedCtAlias != null) - { - var ctSort = new ContentTypeSort(new Lazy(() => allowedCtId.Value), allowedCtSort.Value, allowedCtAlias); - if (currAllowedContentTypes.Contains(ctSort) == false) - { - currAllowedContentTypes.Add(ctSort); - } - } - - //always ensure there's a list for this content type - if (parentMediaTypeIds.ContainsKey(ct.ctId) == false) - parentMediaTypeIds[ct.ctId] = new List(); - - //check for parent ids and assign to the outgoing collection - int? parentId = ct.chtParentId; - if (parentId.HasValue) - { - var associatedParentIds = parentMediaTypeIds[ct.ctId]; - if (associatedParentIds.Contains(parentId.Value) == false) - associatedParentIds.Add(parentId.Value); - } - - if (queue.Count == 0 || queue.Peek().ctId != ct.ctId) - { - //it's the last in the queue or the content type is changing (moving to the next one) - var mediaType = CreateForMapping(ct, currAllowedContentTypes); - mappedMediaTypes.Add(mediaType); - - //Here we need to reset the current variables, we're now collecting data for a different content type - currAllowedContentTypes = new List(); - } - } - - return mappedMediaTypes; - } - - private static IMediaType CreateForMapping(dynamic currCt, List currAllowedContentTypes) - { - // * create the DTO object - // * create the content type object - // * map the allowed content types - // * add to the outgoing list - - var contentTypeDto = new ContentTypeDto - { - Alias = currCt.ctAlias, - AllowAtRoot = currCt.ctAllowAtRoot, - Description = currCt.ctDesc, - Icon = currCt.ctIcon, - IsContainer = currCt.ctIsContainer, - NodeId = currCt.ctId, - PrimaryKey = currCt.ctPk, - Thumbnail = currCt.ctThumb, - //map the underlying node dto - NodeDto = new NodeDto - { - CreateDate = currCt.nCreateDate, - Level = (short)currCt.nLevel, - NodeId = currCt.ctId, - NodeObjectType = currCt.nObjectType, - ParentId = currCt.nParentId, - Path = currCt.nPath, - SortOrder = currCt.nSortOrder, - Text = currCt.nName, - Trashed = currCt.nTrashed, - UniqueId = currCt.nUniqueId, - UserId = currCt.nUser - } - }; - - //now create the content type object - - var factory = new ContentTypeFactory(); - var mediaType = factory.BuildMediaTypeEntity(contentTypeDto); - - //map the allowed content types - mediaType.AllowedContentTypes = currAllowedContentTypes; - - return mediaType; - } - - internal static IEnumerable MapContentTypes(Database db, ISqlSyntaxProvider sqlSyntax, - out IDictionary> associatedTemplates, - out IDictionary> parentContentTypeIds) - { - Mandate.ParameterNotNull(db, "db"); - - var sql = @"SELECT cmsDocumentType.IsDefault as dtIsDefault, cmsDocumentType.templateNodeId as dtTemplateId, - cmsContentType.pk as ctPk, cmsContentType.alias as ctAlias, cmsContentType.allowAtRoot as ctAllowAtRoot, cmsContentType.description as ctDesc, - cmsContentType.icon as ctIcon, cmsContentType.isContainer as ctIsContainer, cmsContentType.nodeId as ctId, cmsContentType.thumbnail as ctThumb, - AllowedTypes.AllowedId as ctaAllowedId, AllowedTypes.SortOrder as ctaSortOrder, AllowedTypes.alias as ctaAlias, - ParentTypes.parentContentTypeId as chtParentId,ParentTypes.parentContentTypeKey as chtParentKey, - umbracoNode.createDate as nCreateDate, umbracoNode." + sqlSyntax.GetQuotedColumnName("level") + @" as nLevel, umbracoNode.nodeObjectType as nObjectType, umbracoNode.nodeUser as nUser, - umbracoNode.parentID as nParentId, umbracoNode." + sqlSyntax.GetQuotedColumnName("path") + @" as nPath, umbracoNode.sortOrder as nSortOrder, umbracoNode." + sqlSyntax.GetQuotedColumnName("text") + @" as nName, umbracoNode.trashed as nTrashed, - umbracoNode.uniqueID as nUniqueId, - Template.alias as tAlias, Template.nodeId as tId,Template.text as tText - FROM cmsContentType - INNER JOIN umbracoNode - ON cmsContentType.nodeId = umbracoNode.id - LEFT JOIN cmsDocumentType - ON cmsDocumentType.contentTypeNodeId = cmsContentType.nodeId - LEFT JOIN ( - SELECT cmsContentTypeAllowedContentType.Id, cmsContentTypeAllowedContentType.AllowedId, cmsContentType.alias, cmsContentTypeAllowedContentType.SortOrder - FROM cmsContentTypeAllowedContentType - INNER JOIN cmsContentType - ON cmsContentTypeAllowedContentType.AllowedId = cmsContentType.nodeId - ) AllowedTypes - ON AllowedTypes.Id = cmsContentType.nodeId - LEFT JOIN ( - SELECT * FROM cmsTemplate - INNER JOIN umbracoNode - ON cmsTemplate.nodeId = umbracoNode.id - ) as Template - ON Template.nodeId = cmsDocumentType.templateNodeId - LEFT JOIN ( - SELECT cmsContentType2ContentType.parentContentTypeId, umbracoNode.uniqueID AS parentContentTypeKey, cmsContentType2ContentType.childContentTypeId - FROM cmsContentType2ContentType - INNER JOIN umbracoNode - ON cmsContentType2ContentType.parentContentTypeId = umbracoNode." + sqlSyntax.GetQuotedColumnName("id") + @" - ) ParentTypes - ON ParentTypes.childContentTypeId = cmsContentType.nodeId - WHERE (umbracoNode.nodeObjectType = @nodeObjectType) - ORDER BY ctId"; - - var result = db.Fetch(sql, new { nodeObjectType = new Guid(Constants.ObjectTypes.DocumentType)}); - - if (result.Any() == false) - { - parentContentTypeIds = null; - associatedTemplates = null; - return Enumerable.Empty(); - } - - parentContentTypeIds = new Dictionary>(); - associatedTemplates = new Dictionary>(); - var mappedContentTypes = new List(); - - var queue = new Queue(result); - var currDefaultTemplate = -1; - var currAllowedContentTypes = new List(); - while (queue.Count > 0) - { - var ct = queue.Dequeue(); - - //check for default templates - bool? isDefaultTemplate = Convert.ToBoolean(ct.dtIsDefault); - int? templateId = ct.dtTemplateId; - if (currDefaultTemplate == -1 && isDefaultTemplate.HasValue && isDefaultTemplate.Value && templateId.HasValue) - { - currDefaultTemplate = templateId.Value; - } - - //always ensure there's a list for this content type - if (associatedTemplates.ContainsKey(ct.ctId) == false) - associatedTemplates[ct.ctId] = new List(); - - //check for associated templates and assign to the outgoing collection - if (ct.tId != null) - { - var associatedTemplate = new AssociatedTemplate(ct.tId, ct.tAlias, ct.tText); - var associatedList = associatedTemplates[ct.ctId]; - - if (associatedList.Contains(associatedTemplate) == false) - associatedList.Add(associatedTemplate); - } - - //check for allowed content types - int? allowedCtId = ct.ctaAllowedId; - int? allowedCtSort = ct.ctaSortOrder; - string allowedCtAlias = ct.ctaAlias; - if (allowedCtId.HasValue && allowedCtSort.HasValue && allowedCtAlias != null) - { - var ctSort = new ContentTypeSort(new Lazy(() => allowedCtId.Value), allowedCtSort.Value, allowedCtAlias); - if (currAllowedContentTypes.Contains(ctSort) == false) - { - currAllowedContentTypes.Add(ctSort); - } - } - - //always ensure there's a list for this content type - if (parentContentTypeIds.ContainsKey(ct.ctId) == false) - parentContentTypeIds[ct.ctId] = new List(); - - //check for parent ids and assign to the outgoing collection - int? parentId = ct.chtParentId; - if (parentId.HasValue) - { - var associatedParentIds = parentContentTypeIds[ct.ctId]; - - if (associatedParentIds.Contains(parentId.Value) == false) - associatedParentIds.Add(parentId.Value); - } - - if (queue.Count == 0 || queue.Peek().ctId != ct.ctId) - { - //it's the last in the queue or the content type is changing (moving to the next one) - var contentType = CreateForMapping(ct, currAllowedContentTypes, currDefaultTemplate); - mappedContentTypes.Add(contentType); - - //Here we need to reset the current variables, we're now collecting data for a different content type - currDefaultTemplate = -1; - currAllowedContentTypes = new List(); - } - } - - return mappedContentTypes; - } - - private static IContentType CreateForMapping(dynamic currCt, List currAllowedContentTypes, int currDefaultTemplate) - { - // * set the default template to the first one if a default isn't found - // * create the DTO object - // * create the content type object - // * map the allowed content types - // * add to the outgoing list - - var dtDto = new ContentTypeTemplateDto - { - //create the content type dto - ContentTypeDto = new ContentTypeDto - { - Alias = currCt.ctAlias, - AllowAtRoot = currCt.ctAllowAtRoot, - Description = currCt.ctDesc, - Icon = currCt.ctIcon, - IsContainer = currCt.ctIsContainer, - NodeId = currCt.ctId, - PrimaryKey = currCt.ctPk, - Thumbnail = currCt.ctThumb, - //map the underlying node dto - NodeDto = new NodeDto - { - CreateDate = currCt.nCreateDate, - Level = (short)currCt.nLevel, - NodeId = currCt.ctId, - NodeObjectType = currCt.nObjectType, - ParentId = currCt.nParentId, - Path = currCt.nPath, - SortOrder = currCt.nSortOrder, - Text = currCt.nName, - Trashed = currCt.nTrashed, - UniqueId = currCt.nUniqueId, - UserId = currCt.nUser - } - }, - ContentTypeNodeId = currCt.ctId, - IsDefault = currDefaultTemplate != -1, - TemplateNodeId = currDefaultTemplate != -1 ? currDefaultTemplate : 0, - }; - - //now create the content type object - - var factory = new ContentTypeFactory(); - var contentType = factory.BuildContentTypeEntity(dtDto.ContentTypeDto); - - // NOTE - // that was done by the factory but makes little sense, moved here, so - // now we have to reset dirty props again (as the factory does it) and yet, - // we are not managing allowed templates... the whole thing is weird. - ((ContentType)contentType).DefaultTemplateId = dtDto.TemplateNodeId; - contentType.ResetDirtyProperties(false); - - //map the allowed content types - contentType.AllowedContentTypes = currAllowedContentTypes; - - return contentType; - } - - internal static void MapGroupsAndProperties(int[] contentTypeIds, Database db, ISqlSyntaxProvider sqlSyntax, - out IDictionary allPropertyTypeCollection, - out IDictionary allPropertyGroupCollection) - { - allPropertyGroupCollection = new Dictionary(); - allPropertyTypeCollection = new Dictionary(); - - // query below is not safe + pointless if array is empty - if (contentTypeIds.Length == 0) return; - - // first part Gets all property groups including property type data even when no property type exists on the group - // second part Gets all property types including ones that are not on a group - // therefore the union of the two contains all of the property type and property group information we need - // NOTE: MySQL requires a SELECT * FROM the inner union in order to be able to sort . lame. - - var sqlBuilder = new StringBuilder(@"SELECT PG.contenttypeNodeId as contentTypeId, - PT.ptUniqueId as ptUniqueID, PT.ptId, PT.ptAlias, PT.ptDesc,PT.ptMandatory,PT.ptName,PT.ptSortOrder,PT.ptRegExp, - PT.dtId,PT.dtDbType,PT.dtPropEdAlias, - PG.id as pgId, PG.uniqueID as pgKey, PG.sortorder as pgSortOrder, PG." + sqlSyntax.GetQuotedColumnName("text") + @" as pgText - FROM cmsPropertyTypeGroup as PG - LEFT JOIN - ( - SELECT PT.uniqueID as ptUniqueId, PT.id as ptId, PT.Alias as ptAlias, PT." + sqlSyntax.GetQuotedColumnName("Description") + @" as ptDesc, - PT.mandatory as ptMandatory, PT.Name as ptName, PT.sortOrder as ptSortOrder, PT.validationRegExp as ptRegExp, - PT.propertyTypeGroupId as ptGroupId, - DT.dbType as dtDbType, DT.nodeId as dtId, DT.propertyEditorAlias as dtPropEdAlias - FROM cmsPropertyType as PT - INNER JOIN cmsDataType as DT - ON PT.dataTypeId = DT.nodeId - ) as PT - ON PT.ptGroupId = PG.id - WHERE (PG.contenttypeNodeId in (@contentTypeIds)) - - UNION - - SELECT PT.contentTypeId as contentTypeId, - PT.uniqueID as ptUniqueID, PT.id as ptId, PT.Alias as ptAlias, PT." + sqlSyntax.GetQuotedColumnName("Description") + @" as ptDesc, - PT.mandatory as ptMandatory, PT.Name as ptName, PT.sortOrder as ptSortOrder, PT.validationRegExp as ptRegExp, - DT.nodeId as dtId, DT.dbType as dtDbType, DT.propertyEditorAlias as dtPropEdAlias, - PG.id as pgId, PG.uniqueID as pgKey, PG.sortorder as pgSortOrder, PG." + sqlSyntax.GetQuotedColumnName("text") + @" as pgText - FROM cmsPropertyType as PT - INNER JOIN cmsDataType as DT - ON PT.dataTypeId = DT.nodeId - LEFT JOIN cmsPropertyTypeGroup as PG - ON PG.id = PT.propertyTypeGroupId - WHERE (PT.contentTypeId in (@contentTypeIds))"); - - sqlBuilder.AppendLine(" ORDER BY (pgId)"); - - //NOTE: we are going to assume there's not going to be more than 2100 content type ids since that is the max SQL param count! - // Since there are 2 groups of params, it will be half! - if (((contentTypeIds.Length / 2) - 1) > 2000) - throw new InvalidOperationException("Cannot perform this lookup, too many sql parameters"); - - var result = db.Fetch(sqlBuilder.ToString(), new { contentTypeIds = contentTypeIds }); - - foreach (var contentTypeId in contentTypeIds) - { - //from this we need to make : - // * PropertyGroupCollection - Contains all property groups along with all property types associated with a group - // * PropertyTypeCollection - Contains all property types that do not belong to a group - - //create the property group collection first, this means all groups (even empty ones) and all groups with properties - - int currId = contentTypeId; - - var propertyGroupCollection = new PropertyGroupCollection(result - //get all rows that have a group id - .Where(x => x.pgId != null) - //filter based on the current content type - .Where(x => x.contentTypeId == currId) - //turn that into a custom object containing only the group info - .Select(x => new { GroupId = x.pgId, SortOrder = x.pgSortOrder, Text = x.pgText, Key = x.pgKey }) - //get distinct data by id - .DistinctBy(x => (int)x.GroupId) - //for each of these groups, create a group object with it's associated properties - .Select(group => new PropertyGroup(new PropertyTypeCollection( - result - .Where(row => row.pgId == group.GroupId && row.ptId != null) - .Select(row => new PropertyType(row.dtPropEdAlias, Enum.Parse(row.dtDbType), row.ptAlias) - { - //fill in the rest of the property type properties - Description = row.ptDesc, - DataTypeDefinitionId = row.dtId, - Id = row.ptId, - Key = row.ptUniqueID, - Mandatory = Convert.ToBoolean(row.ptMandatory), - Name = row.ptName, - PropertyGroupId = new Lazy(() => group.GroupId, false), - SortOrder = row.ptSortOrder, - ValidationRegExp = row.ptRegExp - }))) - { - //fill in the rest of the group properties - Id = group.GroupId, - Name = group.Text, - SortOrder = group.SortOrder, - Key = group.Key - }).ToArray()); - - allPropertyGroupCollection[currId] = propertyGroupCollection; - - //Create the property type collection now (that don't have groups) - - var propertyTypeCollection = new PropertyTypeCollection(result - .Where(x => x.pgId == null) - //filter based on the current content type - .Where(x => x.contentTypeId == currId) - .Select(row => new PropertyType(row.dtPropEdAlias, Enum.Parse(row.dtDbType), row.ptAlias) - { - //fill in the rest of the property type properties - Description = row.ptDesc, - DataTypeDefinitionId = row.dtId, - Id = row.ptId, - Key = row.ptUniqueID, - Mandatory = Convert.ToBoolean(row.ptMandatory), - Name = row.ptName, - PropertyGroupId = null, - SortOrder = row.ptSortOrder, - ValidationRegExp = row.ptRegExp - }).ToArray()); - - allPropertyTypeCollection[currId] = propertyTypeCollection; - } - - - } - - } - - protected abstract TEntity PerformGet(Guid id); - protected abstract TEntity PerformGet(string alias); - protected abstract IEnumerable PerformGetAll(params Guid[] ids); - protected abstract bool PerformExists(Guid id); - - /// - /// Gets an Entity by alias - /// - /// - /// - public TEntity Get(string alias) - { - return PerformGet(alias); - } - - /// - /// Gets an Entity by Id - /// - /// - /// - public TEntity Get(Guid id) - { - return PerformGet(id); - } - - /// - /// Gets all entities of the spefified type - /// - /// - /// - /// - /// Ensure explicit implementation, we don't want to have any accidental calls to this since it is essentially the same signature as the main GetAll when there are no parameters - /// - IEnumerable IReadRepository.GetAll(params Guid[] ids) - { - return PerformGetAll(ids); - } - - /// - /// Boolean indicating whether an Entity with the specified Id exists - /// - /// - /// - public bool Exists(Guid id) - { - return PerformExists(id); - } - - public string GetUniqueAlias(string alias) - { - // alias is unique accross ALL content types! - var aliasColumn = SqlSyntax.GetQuotedColumnName("alias"); - var aliases = Database.Fetch(@"SELECT cmsContentType." + aliasColumn + @" FROM cmsContentType -INNER JOIN umbracoNode ON cmsContentType.nodeId = umbracoNode.id -WHERE cmsContentType." + aliasColumn + @" LIKE @pattern", - new { pattern = alias + "%", objectType = NodeObjectTypeId }); - var i = 1; - string test; - while (aliases.Contains(test = alias + i)) i++; - return test; - } - } -} +using System; +using System.Collections.Generic; +using System.Data; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NPoco; +using Umbraco.Core.Events; +using Umbraco.Core.Exceptions; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Models.Rdbms; + +using Umbraco.Core.Persistence.Factories; +using Umbraco.Core.Persistence.Mappers; +using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Core.Persistence.UnitOfWork; +using Umbraco.Core.Services; + +namespace Umbraco.Core.Persistence.Repositories +{ + /// + /// Represent an abstract Repository for ContentType based repositories + /// + /// Exposes shared functionality + /// + internal abstract class ContentTypeRepositoryBase : NPocoRepositoryBase, IReadRepository + where TEntity : class, IContentTypeComposition + { + protected ContentTypeRepositoryBase(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, IMappingResolver mappingResolver) + : base(work, cache, logger, mappingResolver) + { + } + + public IEnumerable> Move(TEntity moving, EntityContainer container) + { + var parentId = Constants.System.Root; + if (container != null) + { + // check path + if ((string.Format(",{0},", container.Path)).IndexOf(string.Format(",{0},", moving.Id), StringComparison.Ordinal) > -1) + throw new DataOperationException(MoveOperationStatusType.FailedNotAllowedByPath); + + parentId = container.Id; + } + + // track moved entities + var moveInfo = new List> + { + new MoveEventInfo(moving, moving.Path, parentId) + }; + + + // get the level delta (old pos to new pos) + var levelDelta = container == null + ? 1 - moving.Level + : container.Level + 1 - moving.Level; + + // move to parent (or -1), update path, save + moving.ParentId = parentId; + var movingPath = moving.Path + ","; // save before changing + moving.Path = (container == null ? Constants.System.Root.ToString() : container.Path) + "," + moving.Id; + moving.Level = container == null ? 1 : container.Level + 1; + AddOrUpdate(moving); + + //update all descendants, update in order of level + var descendants = GetByQuery(Query.Where(type => type.Path.StartsWith(movingPath))); + var paths = new Dictionary(); + paths[moving.Id] = moving.Path; + + foreach (var descendant in descendants.OrderBy(x => x.Level)) + { + moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); + + descendant.Path = paths[descendant.Id] = paths[descendant.ParentId] + "," + descendant.Id; + descendant.Level += levelDelta; + + AddOrUpdate(descendant); + } + + return moveInfo; + } + /// + /// Returns the content type ids that match the query + /// + /// + /// + protected IEnumerable PerformGetByQuery(IQuery query) + { + // used by DataTypeDefinitionRepository to remove properties + // from content types if they have a deleted data type - see + // notes in DataTypeDefinitionRepository.Delete as it's a bit + // weird + + var sqlClause = Sql() + .SelectAll() + .From() + .RightJoin() + .On(left => left.Id, right => right.PropertyTypeGroupId) + .InnerJoin() + .On(left => left.DataTypeId, right => right.DataTypeId); + + var translator = new SqlTranslator(sqlClause, query); + var sql = translator.Translate() + .OrderBy(x => x.PropertyTypeGroupId); + + return Database + .FetchOneToMany(x => x.PropertyTypeDtos, sql) + .Select(x => x.ContentTypeNodeId).Distinct(); + } + + protected virtual PropertyType CreatePropertyType(string propertyEditorAlias, DataTypeDatabaseType dbType, string propertyTypeAlias) + { + return new PropertyType(propertyEditorAlias, dbType, propertyTypeAlias); + } + + protected void PersistNewBaseContentType(IContentTypeComposition entity) + { + var factory = new ContentTypeFactory(); + var dto = factory.BuildContentTypeDto(entity); + + //Cannot add a duplicate content type type + var exists = Database.ExecuteScalar(@"SELECT COUNT(*) FROM cmsContentType +INNER JOIN umbracoNode ON cmsContentType.nodeId = umbracoNode.id +WHERE cmsContentType." + SqlSyntax.GetQuotedColumnName("alias") + @"= @alias +AND umbracoNode.nodeObjectType = @objectType", + new { alias = entity.Alias, objectType = NodeObjectTypeId }); + if (exists > 0) + { + throw new DuplicateNameException("An item with the alias " + entity.Alias + " already exists"); + } + + //Logic for setting Path, Level and SortOrder + var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); + int level = parent.Level + 1; + int sortOrder = + Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoNode WHERE parentID = @ParentId AND nodeObjectType = @NodeObjectType", + new { ParentId = entity.ParentId, NodeObjectType = NodeObjectTypeId }); + + //Create the (base) node data - umbracoNode + var nodeDto = dto.NodeDto; + nodeDto.Path = parent.Path; + nodeDto.Level = short.Parse(level.ToString(CultureInfo.InvariantCulture)); + nodeDto.SortOrder = sortOrder; + var o = Database.IsNew(nodeDto) ? Convert.ToInt32(Database.Insert(nodeDto)) : Database.Update(nodeDto); + + //Update with new correct path + nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); + Database.Update(nodeDto); + + //Update entity with correct values + entity.Id = nodeDto.NodeId; //Set Id on entity to ensure an Id is set + entity.Path = nodeDto.Path; + entity.SortOrder = sortOrder; + entity.Level = level; + + //Insert new ContentType entry + dto.NodeId = nodeDto.NodeId; + Database.Insert(dto); + + //Insert ContentType composition in new table + foreach (var composition in entity.ContentTypeComposition) + { + if (composition.Id == entity.Id) continue;//Just to ensure that we aren't creating a reference to ourself. + + if (composition.HasIdentity) + { + Database.Insert(new ContentType2ContentTypeDto { ParentId = composition.Id, ChildId = entity.Id }); + } + else + { + //Fallback for ContentTypes with no identity + var contentTypeDto = Database.FirstOrDefault("WHERE alias = @Alias", new { Alias = composition.Alias }); + if (contentTypeDto != null) + { + Database.Insert(new ContentType2ContentTypeDto { ParentId = contentTypeDto.NodeId, ChildId = entity.Id }); + } + } + } + + //Insert collection of allowed content types + foreach (var allowedContentType in entity.AllowedContentTypes) + { + Database.Insert(new ContentTypeAllowedContentTypeDto + { + Id = entity.Id, + AllowedId = allowedContentType.Id.Value, + SortOrder = allowedContentType.SortOrder + }); + } + + var propertyFactory = new PropertyGroupFactory(nodeDto.NodeId); + + //Insert Tabs + foreach (var propertyGroup in entity.PropertyGroups) + { + var tabDto = propertyFactory.BuildGroupDto(propertyGroup); + var primaryKey = Convert.ToInt32(Database.Insert(tabDto)); + propertyGroup.Id = primaryKey;//Set Id on PropertyGroup + + //Ensure that the PropertyGroup's Id is set on the PropertyTypes within a group + //unless the PropertyGroupId has already been changed. + foreach (var propertyType in propertyGroup.PropertyTypes) + { + if (propertyType.IsPropertyDirty("PropertyGroupId") == false) + { + var tempGroup = propertyGroup; + propertyType.PropertyGroupId = new Lazy(() => tempGroup.Id); + } + } + } + + //Insert PropertyTypes + foreach (var propertyType in entity.PropertyTypes) + { + var tabId = propertyType.PropertyGroupId != null ? propertyType.PropertyGroupId.Value : default(int); + //If the Id of the DataType is not set, we resolve it from the db by its PropertyEditorAlias + if (propertyType.DataTypeDefinitionId == 0 || propertyType.DataTypeDefinitionId == default(int)) + { + AssignDataTypeFromPropertyEditor(propertyType); + } + var propertyTypeDto = propertyFactory.BuildPropertyTypeDto(tabId, propertyType); + int typePrimaryKey = Convert.ToInt32(Database.Insert(propertyTypeDto)); + propertyType.Id = typePrimaryKey; //Set Id on new PropertyType + + //Update the current PropertyType with correct PropertyEditorAlias and DatabaseType + var dataTypeDto = Database.FirstOrDefault("WHERE nodeId = @Id", new { Id = propertyTypeDto.DataTypeId }); + propertyType.PropertyEditorAlias = dataTypeDto.PropertyEditorAlias; + propertyType.DataTypeDatabaseType = dataTypeDto.DbType.EnumParse(true); + } + } + + protected void PersistUpdatedBaseContentType(IContentTypeComposition entity) + { + var factory = new ContentTypeFactory(); + var dto = factory.BuildContentTypeDto(entity); + + // ensure the alias is not used already + var exists = Database.ExecuteScalar(@"SELECT COUNT(*) FROM cmsContentType +INNER JOIN umbracoNode ON cmsContentType.nodeId = umbracoNode.id +WHERE cmsContentType." + SqlSyntax.GetQuotedColumnName("alias") + @"= @alias +AND umbracoNode.nodeObjectType = @objectType +AND umbracoNode.id <> @id", + new { id = dto.NodeId, alias = dto.Alias, objectType = NodeObjectTypeId }); + if (exists > 0) + { + throw new DuplicateNameException("An item with the alias " + dto.Alias + " already exists"); + } + + // repository should be write-locked when doing this, so we are safe from race-conds + // handle (update) the node + var nodeDto = dto.NodeDto; + Database.Update(nodeDto); + + // we NEED this: updating, so the .PrimaryKey already exists, but the entity does + // not carry it and therefore the dto does not have it yet - must get it from db + //Look up ContentType entry to get PrimaryKey for updating the DTO + var dtoPk = Database.First("WHERE nodeId = @Id", new { Id = entity.Id }); + dto.PrimaryKey = dtoPk.PrimaryKey; + Database.Update(dto); + + // handle (delete then recreate) compositions + Database.Delete("WHERE childContentTypeId = @Id", new { Id = entity.Id }); + foreach (var composition in entity.ContentTypeComposition) + Database.Insert(new ContentType2ContentTypeDto { ParentId = composition.Id, ChildId = entity.Id }); + + //Removing a ContentType from a composition (U4-1690) + //1. Find content based on the current ContentType: entity.Id + //2. Find all PropertyTypes on the ContentType that was removed - tracked id (key) + //3. Remove properties based on property types from the removed content type where the content ids correspond to those found in step one + var compositionBase = entity as ContentTypeCompositionBase; + if (compositionBase != null && compositionBase.RemovedContentTypeKeyTracker != null && + compositionBase.RemovedContentTypeKeyTracker.Any()) + { + //Find Content based on the current ContentType + var sql = Sql() + .SelectAll() + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .Where(x => x.NodeObjectType == new Guid(Constants.ObjectTypes.Document)) + .Where(x => x.ContentTypeId == entity.Id); + + var contentDtos = Database.Fetch(sql); + //Loop through all tracked keys, which corresponds to the ContentTypes that has been removed from the composition + foreach (var key in compositionBase.RemovedContentTypeKeyTracker) + { + //Find PropertyTypes for the removed ContentType + var propertyTypes = Database.Fetch("WHERE contentTypeId = @Id", new { Id = key }); + //Loop through the Content that is based on the current ContentType in order to remove the Properties that are + //based on the PropertyTypes that belong to the removed ContentType. + foreach (var contentDto in contentDtos) + { + foreach (var propertyType in propertyTypes) + { + var nodeId = contentDto.NodeId; + var propertyTypeId = propertyType.Id; + var propertySql = Sql() + .Select("cmsPropertyData.id") + .From() + .InnerJoin() + .On(left => left.PropertyTypeId, right => right.Id) + .Where(x => x.NodeId == nodeId) + .Where(x => x.Id == propertyTypeId); + + //Finally delete the properties that match our criteria for removing a ContentType from the composition + Database.Delete(new Sql("WHERE id IN (" + propertySql.SQL + ")", propertySql.Arguments)); + } + } + } + } + + //Delete the allowed content type entries before adding the updated collection + Database.Delete("WHERE Id = @Id", new { Id = entity.Id }); + //Insert collection of allowed content types + foreach (var allowedContentType in entity.AllowedContentTypes) + { + Database.Insert(new ContentTypeAllowedContentTypeDto + { + Id = entity.Id, + AllowedId = allowedContentType.Id.Value, + SortOrder = allowedContentType.SortOrder + }); + } + + + if (((ICanBeDirty)entity).IsPropertyDirty("PropertyTypes") || entity.PropertyTypes.Any(x => x.IsDirty())) + { + //Delete PropertyTypes by excepting entries from db with entries from collections + var dbPropertyTypes = Database.Fetch("WHERE contentTypeId = @Id", new { Id = entity.Id }); + var dbPropertyTypeAlias = dbPropertyTypes.Select(x => x.Id); + var entityPropertyTypes = entity.PropertyTypes.Where(x => x.HasIdentity).Select(x => x.Id); + var items = dbPropertyTypeAlias.Except(entityPropertyTypes); + foreach (var item in items) + { + //Before a PropertyType can be deleted, all Properties based on that PropertyType should be deleted. + Database.Delete("WHERE propertyTypeId = @Id", new { Id = item }); + Database.Delete("WHERE propertytypeid = @Id", new { Id = item }); + Database.Delete("WHERE contentTypeId = @Id AND id = @PropertyTypeId", + new { Id = entity.Id, PropertyTypeId = item }); + } + } + + if (entity.IsPropertyDirty("PropertyGroups") || entity.PropertyGroups.Any(x => x.IsDirty())) + { + // todo + // we used to try to propagate tabs renaming downstream, relying on ParentId, but + // 1) ParentId makes no sense (if a tab can be inherited from multiple composition + // types) so we would need to figure things out differently, visiting downstream + // content types and looking for tabs with the same name... + // 2) It was not deployable as changing a content type changes other content types + // that was not deterministic, because it would depend on the order of the changes. + // That last point could be fixed if (1) is fixed, but then it still is an issue with + // deploy because changing a content type changes other content types that are not + // dependencies but dependents, and then what? + // + // So... for the time being, all renaming propagation is disabled. We just don't do it. + + // (all gone) + + // delete tabs that do not exist anymore + // get the tabs that are currently existing (in the db) + // get the tabs that we want, now + // and derive the tabs that we want to delete + var existingPropertyGroups = Database.Fetch("WHERE contentTypeNodeId = @id", new { id = entity.Id }) + .Select(x => x.Id) + .ToList(); + var newPropertyGroups = entity.PropertyGroups.Select(x => x.Id).ToList(); + var tabsToDelete = existingPropertyGroups + .Except(newPropertyGroups) + .ToArray(); + + // move properties to generic properties, and delete the tabs + if (tabsToDelete.Length > 0) + { + Database.Update("SET propertyTypeGroupId=NULL WHERE propertyTypeGroupId IN (@ids)", new { ids = tabsToDelete }); + Database.Delete("WHERE id IN (@ids)", new { ids = tabsToDelete }); + } + } + var propertyGroupFactory = new PropertyGroupFactory(entity.Id); + + //Run through all groups to insert or update entries + foreach (var propertyGroup in entity.PropertyGroups) + { + var tabDto = propertyGroupFactory.BuildGroupDto(propertyGroup); + int groupPrimaryKey = propertyGroup.HasIdentity + ? Database.Update(tabDto) + : Convert.ToInt32(Database.Insert(tabDto)); + if (propertyGroup.HasIdentity == false) + propertyGroup.Id = groupPrimaryKey; //Set Id on new PropertyGroup + + //Ensure that the PropertyGroup's Id is set on the PropertyTypes within a group + //unless the PropertyGroupId has already been changed. + foreach (var propertyType in propertyGroup.PropertyTypes) + { + if (propertyType.IsPropertyDirty("PropertyGroupId") == false) + { + var tempGroup = propertyGroup; + propertyType.PropertyGroupId = new Lazy(() => tempGroup.Id); + } + } + } + + //Run through all PropertyTypes to insert or update entries + foreach (var propertyType in entity.PropertyTypes) + { + var tabId = propertyType.PropertyGroupId != null ? propertyType.PropertyGroupId.Value : default(int); + //If the Id of the DataType is not set, we resolve it from the db by its PropertyEditorAlias + if (propertyType.DataTypeDefinitionId == 0 || propertyType.DataTypeDefinitionId == default(int)) + { + AssignDataTypeFromPropertyEditor(propertyType); + } + + //validate the alias! + ValidateAlias(propertyType); + + var propertyTypeDto = propertyGroupFactory.BuildPropertyTypeDto(tabId, propertyType); + int typePrimaryKey = propertyType.HasIdentity + ? Database.Update(propertyTypeDto) + : Convert.ToInt32(Database.Insert(propertyTypeDto)); + if (propertyType.HasIdentity == false) + propertyType.Id = typePrimaryKey; //Set Id on new PropertyType + } + } + + protected IEnumerable GetAllowedContentTypeIds(int id) + { + var sql = Sql() + .SelectAll() + .From() + .LeftJoin() + .On(left => left.AllowedId, right => right.NodeId) + .Where(x => x.Id == id); + + var allowedContentTypeDtos = Database.Fetch(sql); + return allowedContentTypeDtos.Select(x => new ContentTypeSort(new Lazy(() => x.AllowedId), x.SortOrder, x.ContentTypeDto.Alias)).ToList(); + } + + protected PropertyGroupCollection GetPropertyGroupCollection(int id, DateTime createDate, DateTime updateDate) + { + var sql = Sql() + .SelectAll() + .From() + .LeftJoin() + .On(left => left.Id, right => right.PropertyTypeGroupId) + .LeftJoin() + .On(left => left.DataTypeId, right => right.DataTypeId) + .Where(x => x.ContentTypeNodeId == id) + .OrderBy(x => x.Id); + + + var dtos = Database + .Fetch(sql); + + var propertyGroupFactory = new PropertyGroupFactory(id, createDate, updateDate, CreatePropertyType); + var propertyGroups = propertyGroupFactory.BuildEntity(dtos); + return new PropertyGroupCollection(propertyGroups); + } + + protected PropertyTypeCollection GetPropertyTypeCollection(int id, DateTime createDate, DateTime updateDate) + { + var sql = Sql() + .SelectAll() + .From() + .InnerJoin() + .On(left => left.DataTypeId, right => right.DataTypeId) + .Where(x => x.ContentTypeId == id); + + var dtos = Database.Fetch(sql); + + //TODO Move this to a PropertyTypeFactory + var list = new List(); + foreach (var dto in dtos.Where(x => (x.PropertyTypeGroupId > 0) == false)) + { + var propType = CreatePropertyType(dto.DataTypeDto.PropertyEditorAlias, dto.DataTypeDto.DbType.EnumParse(true), dto.Alias); + propType.DataTypeDefinitionId = dto.DataTypeId; + propType.Description = dto.Description; + propType.Id = dto.Id; + propType.Key = dto.UniqueId; + propType.Name = dto.Name; + propType.Mandatory = dto.Mandatory; + propType.SortOrder = dto.SortOrder; + propType.ValidationRegExp = dto.ValidationRegExp; + propType.CreateDate = createDate; + propType.UpdateDate = updateDate; + list.Add(propType); + } + //Reset dirty properties + Parallel.ForEach(list, currentFile => currentFile.ResetDirtyProperties(false)); + + return new PropertyTypeCollection(list); + } + + protected void ValidateAlias(PropertyType pt) + { + Mandate.That(string.IsNullOrEmpty(pt.Alias) == false, + () => + { + var message = + string.Format( + "{0} '{1}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias.", + "Property Type", + pt.Name); + var exception = new InvalidOperationException(message); + + Logger.Error>(message, exception); + throw exception; + }); + } + + protected void ValidateAlias(TEntity entity) + { + Mandate.That(string.IsNullOrEmpty(entity.Alias) == false, + () => + { + var message = + string.Format( + "{0} '{1}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias.", + typeof(TEntity).Name, + entity.Name); + var exception = new InvalidOperationException(message); + + Logger.Error>(message, exception); + throw exception; + }); + } + + /// + /// Try to set the data type id based on its ControlId + /// + /// + private void AssignDataTypeFromPropertyEditor(PropertyType propertyType) + { + //we cannot try to assign a data type of it's empty + if (propertyType.PropertyEditorAlias.IsNullOrWhiteSpace() == false) + { + var sql = Sql() + .SelectAll() + .From() + .Where("propertyEditorAlias = @propertyEditorAlias", new { propertyEditorAlias = propertyType.PropertyEditorAlias }) + .OrderBy(typeDto => typeDto.DataTypeId); + var datatype = Database.FirstOrDefault(sql); + //we cannot assign a data type if one was not found + if (datatype != null) + { + propertyType.DataTypeDefinitionId = datatype.DataTypeId; + } + else + { + Logger.Warn>("Could not assign a data type for the property type " + propertyType.Alias + " since no data type was found with a property editor " + propertyType.PropertyEditorAlias); + } + } + } + + public IEnumerable GetTypesDirectlyComposedOf(int id) + { + var sql = Sql() + .SelectAll() + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.ChildId) + .Where(x => x.NodeObjectType == NodeObjectTypeId) + .Where(x => x.ParentId == id); + var dtos = Database.Fetch(sql); + return dtos.Any() + ? GetAll(dtos.DistinctBy(x => x.NodeId).Select(x => x.NodeId).ToArray()) + : Enumerable.Empty(); + } + + internal static class ContentTypeQueryMapper + { + + public class AssociatedTemplate + { + public AssociatedTemplate(int templateId, string @alias, string templateName) + { + TemplateId = templateId; + Alias = alias; + TemplateName = templateName; + } + + public int TemplateId { get; set; } + public string Alias { get; set; } + public string TemplateName { get; set; } + + protected bool Equals(AssociatedTemplate other) + { + return TemplateId == other.TemplateId; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((AssociatedTemplate)obj); + } + + public override int GetHashCode() + { + return TemplateId; + } + } + + public static IEnumerable GetMediaTypes( + Database db, ISqlSyntaxProvider sqlSyntax, + TRepo contentTypeRepository) + where TRepo : IReadRepository + { + IDictionary> allParentMediaTypeIds; + var mediaTypes = MapMediaTypes(db, sqlSyntax, out allParentMediaTypeIds) + .ToArray(); + + MapContentTypeChildren(mediaTypes, db, sqlSyntax, contentTypeRepository, allParentMediaTypeIds); + + return mediaTypes; + } + + public static IEnumerable GetContentTypes( + Database db, ISqlSyntaxProvider sqlSyntax, + TRepo contentTypeRepository, + ITemplateRepository templateRepository) + where TRepo : IReadRepository + { + IDictionary> allAssociatedTemplates; + IDictionary> allParentContentTypeIds; + var contentTypes = MapContentTypes(db, sqlSyntax, out allAssociatedTemplates, out allParentContentTypeIds) + .ToArray(); + + if (contentTypes.Any()) + { + MapContentTypeTemplates( + contentTypes, db, contentTypeRepository, templateRepository, allAssociatedTemplates); + + MapContentTypeChildren( + contentTypes, db, sqlSyntax, contentTypeRepository, allParentContentTypeIds); + } + + return contentTypes; + } + + internal static void MapContentTypeChildren(IContentTypeComposition[] contentTypes, + Database db, ISqlSyntaxProvider sqlSyntax, + TRepo contentTypeRepository, + IDictionary> allParentContentTypeIds) + where TRepo : IReadRepository + { + //NOTE: SQL call #2 + + var ids = contentTypes.Select(x => x.Id).ToArray(); + IDictionary allPropGroups; + IDictionary allPropTypes; + MapGroupsAndProperties(ids, db, sqlSyntax, out allPropTypes, out allPropGroups); + + foreach (var contentType in contentTypes) + { + contentType.PropertyGroups = allPropGroups[contentType.Id]; + contentType.NoGroupPropertyTypes = allPropTypes[contentType.Id]; + } + + //NOTE: SQL call #3++ + + if (allParentContentTypeIds != null) + { + var allParentIdsAsArray = allParentContentTypeIds.SelectMany(x => x.Value).Distinct().ToArray(); + if (allParentIdsAsArray.Any()) + { + var allParentContentTypes = contentTypes.Where(x => allParentIdsAsArray.Contains(x.Id)).ToArray(); + + foreach (var contentType in contentTypes) + { + var entityId = contentType.Id; + + var parentContentTypes = allParentContentTypes.Where(x => + { + var parentEntityId = x.Id; + + return allParentContentTypeIds[entityId].Contains(parentEntityId); + }); + foreach (var parentContentType in parentContentTypes) + { + var result = contentType.AddContentType(parentContentType); + //Do something if adding fails? (Should hopefully not be possible unless someone created a circular reference) + } + + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + ((Entity)contentType).ResetDirtyProperties(false); + } + } + } + + + } + + internal static void MapContentTypeTemplates(IContentType[] contentTypes, + Database db, + TRepo contentTypeRepository, + ITemplateRepository templateRepository, + IDictionary> associatedTemplates) + where TRepo : IReadRepository + { + if (associatedTemplates == null || associatedTemplates.Any() == false) return; + + //NOTE: SQL call #3++ + //SEE: http://issues.umbraco.org/issue/U4-5174 to fix this + + var templateIds = associatedTemplates.SelectMany(x => x.Value).Select(x => x.TemplateId) + .Distinct() + .ToArray(); + + var templates = (templateIds.Any() + ? templateRepository.GetAll(templateIds) + : Enumerable.Empty()).ToArray(); + + foreach (var contentType in contentTypes) + { + var entityId = contentType.Id; + + var associatedTemplateIds = associatedTemplates[entityId].Select(x => x.TemplateId) + .Distinct() + .ToArray(); + + contentType.AllowedTemplates = (associatedTemplateIds.Any() + ? templates.Where(x => associatedTemplateIds.Contains(x.Id)) + : Enumerable.Empty()).ToArray(); + } + + + } + + internal static IEnumerable MapMediaTypes(Database db, ISqlSyntaxProvider sqlSyntax, + out IDictionary> parentMediaTypeIds) + { + Mandate.ParameterNotNull(db, "db"); + + var sql = @"SELECT cmsContentType.pk as ctPk, cmsContentType.alias as ctAlias, cmsContentType.allowAtRoot as ctAllowAtRoot, cmsContentType.description as ctDesc, + cmsContentType.icon as ctIcon, cmsContentType.isContainer as ctIsContainer, cmsContentType.nodeId as ctId, cmsContentType.thumbnail as ctThumb, + AllowedTypes.AllowedId as ctaAllowedId, AllowedTypes.SortOrder as ctaSortOrder, AllowedTypes.alias as ctaAlias, + ParentTypes.parentContentTypeId as chtParentId, ParentTypes.parentContentTypeKey as chtParentKey, + umbracoNode.createDate as nCreateDate, umbracoNode." + sqlSyntax.GetQuotedColumnName("level") + @" as nLevel, umbracoNode.nodeObjectType as nObjectType, umbracoNode.nodeUser as nUser, + umbracoNode.parentID as nParentId, umbracoNode." + sqlSyntax.GetQuotedColumnName("path") + @" as nPath, umbracoNode.sortOrder as nSortOrder, umbracoNode." + sqlSyntax.GetQuotedColumnName("text") + @" as nName, umbracoNode.trashed as nTrashed, + umbracoNode.uniqueID as nUniqueId + FROM cmsContentType + INNER JOIN umbracoNode + ON cmsContentType.nodeId = umbracoNode.id + LEFT JOIN ( + SELECT cmsContentTypeAllowedContentType.Id, cmsContentTypeAllowedContentType.AllowedId, cmsContentType.alias, cmsContentTypeAllowedContentType.SortOrder + FROM cmsContentTypeAllowedContentType + INNER JOIN cmsContentType + ON cmsContentTypeAllowedContentType.AllowedId = cmsContentType.nodeId + ) AllowedTypes + ON AllowedTypes.Id = cmsContentType.nodeId + LEFT JOIN ( + SELECT cmsContentType2ContentType.parentContentTypeId, umbracoNode.uniqueID AS parentContentTypeKey, cmsContentType2ContentType.childContentTypeId + FROM cmsContentType2ContentType + INNER JOIN umbracoNode + ON cmsContentType2ContentType.parentContentTypeId = umbracoNode." + sqlSyntax.GetQuotedColumnName("id") + @" + ) ParentTypes + ON ParentTypes.childContentTypeId = cmsContentType.nodeId + WHERE (umbracoNode.nodeObjectType = @nodeObjectType) + ORDER BY ctId"; + + var result = db.Fetch(sql, new { nodeObjectType = new Guid(Constants.ObjectTypes.MediaType) }); + + if (result.Any() == false) + { + parentMediaTypeIds = null; + return Enumerable.Empty(); + } + + parentMediaTypeIds = new Dictionary>(); + var mappedMediaTypes = new List(); + + //loop through each result and fill in our required values, each row will contain different requried data than the rest. + // it is much quicker to iterate each result and populate instead of looking up the values over and over in the result like + // we used to do. + var queue = new Queue(result); + var currAllowedContentTypes = new List(); + + while (queue.Count > 0) + { + var ct = queue.Dequeue(); + + //check for allowed content types + int? allowedCtId = ct.ctaAllowedId; + int? allowedCtSort = ct.ctaSortOrder; + string allowedCtAlias = ct.ctaAlias; + if (allowedCtId.HasValue && allowedCtSort.HasValue && allowedCtAlias != null) + { + var ctSort = new ContentTypeSort(new Lazy(() => allowedCtId.Value), allowedCtSort.Value, allowedCtAlias); + if (currAllowedContentTypes.Contains(ctSort) == false) + { + currAllowedContentTypes.Add(ctSort); + } + } + + //always ensure there's a list for this content type + if (parentMediaTypeIds.ContainsKey(ct.ctId) == false) + parentMediaTypeIds[ct.ctId] = new List(); + + //check for parent ids and assign to the outgoing collection + int? parentId = ct.chtParentId; + if (parentId.HasValue) + { + var associatedParentIds = parentMediaTypeIds[ct.ctId]; + if (associatedParentIds.Contains(parentId.Value) == false) + associatedParentIds.Add(parentId.Value); + } + + if (queue.Count == 0 || queue.Peek().ctId != ct.ctId) + { + //it's the last in the queue or the content type is changing (moving to the next one) + var mediaType = CreateForMapping(ct, currAllowedContentTypes); + mappedMediaTypes.Add(mediaType); + + //Here we need to reset the current variables, we're now collecting data for a different content type + currAllowedContentTypes = new List(); + } + } + + return mappedMediaTypes; + } + + private static IMediaType CreateForMapping(dynamic currCt, List currAllowedContentTypes) + { + // * create the DTO object + // * create the content type object + // * map the allowed content types + // * add to the outgoing list + + var contentTypeDto = new ContentTypeDto + { + Alias = currCt.ctAlias, + AllowAtRoot = currCt.ctAllowAtRoot, + Description = currCt.ctDesc, + Icon = currCt.ctIcon, + IsContainer = currCt.ctIsContainer, + NodeId = currCt.ctId, + PrimaryKey = currCt.ctPk, + Thumbnail = currCt.ctThumb, + //map the underlying node dto + NodeDto = new NodeDto + { + CreateDate = currCt.nCreateDate, + Level = (short)currCt.nLevel, + NodeId = currCt.ctId, + NodeObjectType = currCt.nObjectType, + ParentId = currCt.nParentId, + Path = currCt.nPath, + SortOrder = currCt.nSortOrder, + Text = currCt.nName, + Trashed = currCt.nTrashed, + UniqueId = currCt.nUniqueId, + UserId = currCt.nUser + } + }; + + //now create the content type object + + var factory = new ContentTypeFactory(); + var mediaType = factory.BuildMediaTypeEntity(contentTypeDto); + + //map the allowed content types + mediaType.AllowedContentTypes = currAllowedContentTypes; + + return mediaType; + } + + internal static IEnumerable MapContentTypes(Database db, ISqlSyntaxProvider sqlSyntax, + out IDictionary> associatedTemplates, + out IDictionary> parentContentTypeIds) + { + Mandate.ParameterNotNull(db, "db"); + + var sql = @"SELECT cmsDocumentType.IsDefault as dtIsDefault, cmsDocumentType.templateNodeId as dtTemplateId, + cmsContentType.pk as ctPk, cmsContentType.alias as ctAlias, cmsContentType.allowAtRoot as ctAllowAtRoot, cmsContentType.description as ctDesc, + cmsContentType.icon as ctIcon, cmsContentType.isContainer as ctIsContainer, cmsContentType.nodeId as ctId, cmsContentType.thumbnail as ctThumb, + AllowedTypes.AllowedId as ctaAllowedId, AllowedTypes.SortOrder as ctaSortOrder, AllowedTypes.alias as ctaAlias, + ParentTypes.parentContentTypeId as chtParentId,ParentTypes.parentContentTypeKey as chtParentKey, + umbracoNode.createDate as nCreateDate, umbracoNode." + sqlSyntax.GetQuotedColumnName("level") + @" as nLevel, umbracoNode.nodeObjectType as nObjectType, umbracoNode.nodeUser as nUser, + umbracoNode.parentID as nParentId, umbracoNode." + sqlSyntax.GetQuotedColumnName("path") + @" as nPath, umbracoNode.sortOrder as nSortOrder, umbracoNode." + sqlSyntax.GetQuotedColumnName("text") + @" as nName, umbracoNode.trashed as nTrashed, + umbracoNode.uniqueID as nUniqueId, + Template.alias as tAlias, Template.nodeId as tId,Template.text as tText + FROM cmsContentType + INNER JOIN umbracoNode + ON cmsContentType.nodeId = umbracoNode.id + LEFT JOIN cmsDocumentType + ON cmsDocumentType.contentTypeNodeId = cmsContentType.nodeId + LEFT JOIN ( + SELECT cmsContentTypeAllowedContentType.Id, cmsContentTypeAllowedContentType.AllowedId, cmsContentType.alias, cmsContentTypeAllowedContentType.SortOrder + FROM cmsContentTypeAllowedContentType + INNER JOIN cmsContentType + ON cmsContentTypeAllowedContentType.AllowedId = cmsContentType.nodeId + ) AllowedTypes + ON AllowedTypes.Id = cmsContentType.nodeId + LEFT JOIN ( + SELECT * FROM cmsTemplate + INNER JOIN umbracoNode + ON cmsTemplate.nodeId = umbracoNode.id + ) as Template + ON Template.nodeId = cmsDocumentType.templateNodeId + LEFT JOIN ( + SELECT cmsContentType2ContentType.parentContentTypeId, umbracoNode.uniqueID AS parentContentTypeKey, cmsContentType2ContentType.childContentTypeId + FROM cmsContentType2ContentType + INNER JOIN umbracoNode + ON cmsContentType2ContentType.parentContentTypeId = umbracoNode." + sqlSyntax.GetQuotedColumnName("id") + @" + ) ParentTypes + ON ParentTypes.childContentTypeId = cmsContentType.nodeId + WHERE (umbracoNode.nodeObjectType = @nodeObjectType) + ORDER BY ctId"; + + var result = db.Fetch(sql, new { nodeObjectType = new Guid(Constants.ObjectTypes.DocumentType)}); + + if (result.Any() == false) + { + parentContentTypeIds = null; + associatedTemplates = null; + return Enumerable.Empty(); + } + + parentContentTypeIds = new Dictionary>(); + associatedTemplates = new Dictionary>(); + var mappedContentTypes = new List(); + + var queue = new Queue(result); + var currDefaultTemplate = -1; + var currAllowedContentTypes = new List(); + while (queue.Count > 0) + { + var ct = queue.Dequeue(); + + //check for default templates + bool? isDefaultTemplate = Convert.ToBoolean(ct.dtIsDefault); + int? templateId = ct.dtTemplateId; + if (currDefaultTemplate == -1 && isDefaultTemplate.HasValue && isDefaultTemplate.Value && templateId.HasValue) + { + currDefaultTemplate = templateId.Value; + } + + //always ensure there's a list for this content type + if (associatedTemplates.ContainsKey(ct.ctId) == false) + associatedTemplates[ct.ctId] = new List(); + + //check for associated templates and assign to the outgoing collection + if (ct.tId != null) + { + var associatedTemplate = new AssociatedTemplate(ct.tId, ct.tAlias, ct.tText); + var associatedList = associatedTemplates[ct.ctId]; + + if (associatedList.Contains(associatedTemplate) == false) + associatedList.Add(associatedTemplate); + } + + //check for allowed content types + int? allowedCtId = ct.ctaAllowedId; + int? allowedCtSort = ct.ctaSortOrder; + string allowedCtAlias = ct.ctaAlias; + if (allowedCtId.HasValue && allowedCtSort.HasValue && allowedCtAlias != null) + { + var ctSort = new ContentTypeSort(new Lazy(() => allowedCtId.Value), allowedCtSort.Value, allowedCtAlias); + if (currAllowedContentTypes.Contains(ctSort) == false) + { + currAllowedContentTypes.Add(ctSort); + } + } + + //always ensure there's a list for this content type + if (parentContentTypeIds.ContainsKey(ct.ctId) == false) + parentContentTypeIds[ct.ctId] = new List(); + + //check for parent ids and assign to the outgoing collection + int? parentId = ct.chtParentId; + if (parentId.HasValue) + { + var associatedParentIds = parentContentTypeIds[ct.ctId]; + + if (associatedParentIds.Contains(parentId.Value) == false) + associatedParentIds.Add(parentId.Value); + } + + if (queue.Count == 0 || queue.Peek().ctId != ct.ctId) + { + //it's the last in the queue or the content type is changing (moving to the next one) + var contentType = CreateForMapping(ct, currAllowedContentTypes, currDefaultTemplate); + mappedContentTypes.Add(contentType); + + //Here we need to reset the current variables, we're now collecting data for a different content type + currDefaultTemplate = -1; + currAllowedContentTypes = new List(); + } + } + + return mappedContentTypes; + } + + private static IContentType CreateForMapping(dynamic currCt, List currAllowedContentTypes, int currDefaultTemplate) + { + // * set the default template to the first one if a default isn't found + // * create the DTO object + // * create the content type object + // * map the allowed content types + // * add to the outgoing list + + var dtDto = new ContentTypeTemplateDto + { + //create the content type dto + ContentTypeDto = new ContentTypeDto + { + Alias = currCt.ctAlias, + AllowAtRoot = currCt.ctAllowAtRoot, + Description = currCt.ctDesc, + Icon = currCt.ctIcon, + IsContainer = currCt.ctIsContainer, + NodeId = currCt.ctId, + PrimaryKey = currCt.ctPk, + Thumbnail = currCt.ctThumb, + //map the underlying node dto + NodeDto = new NodeDto + { + CreateDate = currCt.nCreateDate, + Level = (short)currCt.nLevel, + NodeId = currCt.ctId, + NodeObjectType = currCt.nObjectType, + ParentId = currCt.nParentId, + Path = currCt.nPath, + SortOrder = currCt.nSortOrder, + Text = currCt.nName, + Trashed = currCt.nTrashed, + UniqueId = currCt.nUniqueId, + UserId = currCt.nUser + } + }, + ContentTypeNodeId = currCt.ctId, + IsDefault = currDefaultTemplate != -1, + TemplateNodeId = currDefaultTemplate != -1 ? currDefaultTemplate : 0, + }; + + //now create the content type object + + var factory = new ContentTypeFactory(); + var contentType = factory.BuildContentTypeEntity(dtDto.ContentTypeDto); + + // NOTE + // that was done by the factory but makes little sense, moved here, so + // now we have to reset dirty props again (as the factory does it) and yet, + // we are not managing allowed templates... the whole thing is weird. + ((ContentType)contentType).DefaultTemplateId = dtDto.TemplateNodeId; + contentType.ResetDirtyProperties(false); + + //map the allowed content types + contentType.AllowedContentTypes = currAllowedContentTypes; + + return contentType; + } + + internal static void MapGroupsAndProperties(int[] contentTypeIds, Database db, ISqlSyntaxProvider sqlSyntax, + out IDictionary allPropertyTypeCollection, + out IDictionary allPropertyGroupCollection) + { + allPropertyGroupCollection = new Dictionary(); + allPropertyTypeCollection = new Dictionary(); + + // query below is not safe + pointless if array is empty + if (contentTypeIds.Length == 0) return; + + // first part Gets all property groups including property type data even when no property type exists on the group + // second part Gets all property types including ones that are not on a group + // therefore the union of the two contains all of the property type and property group information we need + // NOTE: MySQL requires a SELECT * FROM the inner union in order to be able to sort . lame. + + var sqlBuilder = new StringBuilder(@"SELECT PG.contenttypeNodeId as contentTypeId, + PT.ptUniqueId as ptUniqueID, PT.ptId, PT.ptAlias, PT.ptDesc,PT.ptMandatory,PT.ptName,PT.ptSortOrder,PT.ptRegExp, + PT.dtId,PT.dtDbType,PT.dtPropEdAlias, + PG.id as pgId, PG.uniqueID as pgKey, PG.sortorder as pgSortOrder, PG." + sqlSyntax.GetQuotedColumnName("text") + @" as pgText + FROM cmsPropertyTypeGroup as PG + LEFT JOIN + ( + SELECT PT.uniqueID as ptUniqueId, PT.id as ptId, PT.Alias as ptAlias, PT." + sqlSyntax.GetQuotedColumnName("Description") + @" as ptDesc, + PT.mandatory as ptMandatory, PT.Name as ptName, PT.sortOrder as ptSortOrder, PT.validationRegExp as ptRegExp, + PT.propertyTypeGroupId as ptGroupId, + DT.dbType as dtDbType, DT.nodeId as dtId, DT.propertyEditorAlias as dtPropEdAlias + FROM cmsPropertyType as PT + INNER JOIN cmsDataType as DT + ON PT.dataTypeId = DT.nodeId + ) as PT + ON PT.ptGroupId = PG.id + WHERE (PG.contenttypeNodeId in (@contentTypeIds)) + + UNION + + SELECT PT.contentTypeId as contentTypeId, + PT.uniqueID as ptUniqueID, PT.id as ptId, PT.Alias as ptAlias, PT." + sqlSyntax.GetQuotedColumnName("Description") + @" as ptDesc, + PT.mandatory as ptMandatory, PT.Name as ptName, PT.sortOrder as ptSortOrder, PT.validationRegExp as ptRegExp, + DT.nodeId as dtId, DT.dbType as dtDbType, DT.propertyEditorAlias as dtPropEdAlias, + PG.id as pgId, PG.uniqueID as pgKey, PG.sortorder as pgSortOrder, PG." + sqlSyntax.GetQuotedColumnName("text") + @" as pgText + FROM cmsPropertyType as PT + INNER JOIN cmsDataType as DT + ON PT.dataTypeId = DT.nodeId + LEFT JOIN cmsPropertyTypeGroup as PG + ON PG.id = PT.propertyTypeGroupId + WHERE (PT.contentTypeId in (@contentTypeIds))"); + + sqlBuilder.AppendLine(" ORDER BY (pgId)"); + + //NOTE: we are going to assume there's not going to be more than 2100 content type ids since that is the max SQL param count! + // Since there are 2 groups of params, it will be half! + if (((contentTypeIds.Length / 2) - 1) > 2000) + throw new InvalidOperationException("Cannot perform this lookup, too many sql parameters"); + + var result = db.Fetch(sqlBuilder.ToString(), new { contentTypeIds = contentTypeIds }); + + foreach (var contentTypeId in contentTypeIds) + { + //from this we need to make : + // * PropertyGroupCollection - Contains all property groups along with all property types associated with a group + // * PropertyTypeCollection - Contains all property types that do not belong to a group + + //create the property group collection first, this means all groups (even empty ones) and all groups with properties + + int currId = contentTypeId; + + var propertyGroupCollection = new PropertyGroupCollection(result + //get all rows that have a group id + .Where(x => x.pgId != null) + //filter based on the current content type + .Where(x => x.contentTypeId == currId) + //turn that into a custom object containing only the group info + .Select(x => new { GroupId = x.pgId, SortOrder = x.pgSortOrder, Text = x.pgText, Key = x.pgKey }) + //get distinct data by id + .DistinctBy(x => (int)x.GroupId) + //for each of these groups, create a group object with it's associated properties + .Select(group => new PropertyGroup(new PropertyTypeCollection( + result + .Where(row => row.pgId == group.GroupId && row.ptId != null) + .Select(row => new PropertyType(row.dtPropEdAlias, Enum.Parse(row.dtDbType), row.ptAlias) + { + //fill in the rest of the property type properties + Description = row.ptDesc, + DataTypeDefinitionId = row.dtId, + Id = row.ptId, + Key = row.ptUniqueID, + Mandatory = Convert.ToBoolean(row.ptMandatory), + Name = row.ptName, + PropertyGroupId = new Lazy(() => group.GroupId, false), + SortOrder = row.ptSortOrder, + ValidationRegExp = row.ptRegExp + }))) + { + //fill in the rest of the group properties + Id = group.GroupId, + Name = group.Text, + SortOrder = group.SortOrder, + Key = group.Key + }).ToArray()); + + allPropertyGroupCollection[currId] = propertyGroupCollection; + + //Create the property type collection now (that don't have groups) + + var propertyTypeCollection = new PropertyTypeCollection(result + .Where(x => x.pgId == null) + //filter based on the current content type + .Where(x => x.contentTypeId == currId) + .Select(row => new PropertyType(row.dtPropEdAlias, Enum.Parse(row.dtDbType), row.ptAlias) + { + //fill in the rest of the property type properties + Description = row.ptDesc, + DataTypeDefinitionId = row.dtId, + Id = row.ptId, + Key = row.ptUniqueID, + Mandatory = Convert.ToBoolean(row.ptMandatory), + Name = row.ptName, + PropertyGroupId = null, + SortOrder = row.ptSortOrder, + ValidationRegExp = row.ptRegExp + }).ToArray()); + + allPropertyTypeCollection[currId] = propertyTypeCollection; + } + + + } + + } + + protected abstract TEntity PerformGet(Guid id); + protected abstract TEntity PerformGet(string alias); + protected abstract IEnumerable PerformGetAll(params Guid[] ids); + protected abstract bool PerformExists(Guid id); + + /// + /// Gets an Entity by alias + /// + /// + /// + public TEntity Get(string alias) + { + return PerformGet(alias); + } + + /// + /// Gets an Entity by Id + /// + /// + /// + public TEntity Get(Guid id) + { + return PerformGet(id); + } + + /// + /// Gets all entities of the spefified type + /// + /// + /// + /// + /// Ensure explicit implementation, we don't want to have any accidental calls to this since it is essentially the same signature as the main GetAll when there are no parameters + /// + IEnumerable IReadRepository.GetAll(params Guid[] ids) + { + return PerformGetAll(ids); + } + + /// + /// Boolean indicating whether an Entity with the specified Id exists + /// + /// + /// + public bool Exists(Guid id) + { + return PerformExists(id); + } + + public string GetUniqueAlias(string alias) + { + // alias is unique accross ALL content types! + var aliasColumn = SqlSyntax.GetQuotedColumnName("alias"); + var aliases = Database.Fetch(@"SELECT cmsContentType." + aliasColumn + @" FROM cmsContentType +INNER JOIN umbracoNode ON cmsContentType.nodeId = umbracoNode.id +WHERE cmsContentType." + aliasColumn + @" LIKE @pattern", + new { pattern = alias + "%", objectType = NodeObjectTypeId }); + var i = 1; + string test; + while (aliases.Contains(test = alias + i)) i++; + return test; + } + + protected override IEnumerable GetDeleteClauses() + { + // in theory, services should have ensured that content items of the given content type + // have been deleted and therefore cmsPropertyData has been cleared, so cmsPropertyData + // is included here just to be 100% sure since it has a FK on cmsPropertyType. + + var list = new List + { + "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @Id", + "DELETE FROM umbracoUser2NodePermission WHERE nodeId = @Id", + "DELETE FROM cmsTagRelationship WHERE nodeId = @Id", + "DELETE FROM cmsContentTypeAllowedContentType WHERE Id = @Id", + "DELETE FROM cmsContentTypeAllowedContentType WHERE AllowedId = @Id", + "DELETE FROM cmsContentType2ContentType WHERE parentContentTypeId = @Id", + "DELETE FROM cmsContentType2ContentType WHERE childContentTypeId = @Id", + "DELETE FROM cmsPropertyData WHERE propertyTypeId IN (SELECT id FROM cmsPropertyType WHERE contentTypeId = @Id)", + "DELETE FROM cmsPropertyType WHERE contentTypeId = @Id", + "DELETE FROM cmsPropertyTypeGroup WHERE contenttypeNodeId = @Id", + }; + return list; + } + } +} diff --git a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs index 309f14c52e..691d579401 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs @@ -103,6 +103,14 @@ namespace Umbraco.Core.Persistence.Repositories } } + // 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 facade 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.AddOrUpdate(contentType); } @@ -282,8 +290,6 @@ AND umbracoNode.id <> @id", #endregion - - public PreValueCollection GetPreValuesCollectionByDataTypeId(int dataTypeId) { var cached = RuntimeCache.GetCacheItemsByKeySearch(GetPrefixedCacheKey(dataTypeId)); @@ -485,8 +491,6 @@ AND umbracoNode.id <> @id", private string EnsureUniqueNodeName(string nodeName, int id = 0) { - - var sql = Sql() .SelectAll() .From() diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs index d0bc046055..6807b80c90 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq.Expressions; using System.Xml.Linq; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; @@ -9,7 +8,7 @@ using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.Repositories { - public interface IContentRepository : IRepositoryVersionable, IRecycleBinRepository, IDeleteMediaFilesRepository + public interface IContentRepository : IRepositoryVersionable, IRecycleBinRepository { /// /// Get the count of published items @@ -18,7 +17,9 @@ namespace Umbraco.Core.Persistence.Repositories /// /// We require this on the repo because the IQuery{IContent} cannot supply the 'newest' parameter /// - int CountPublished(); + int CountPublished(string contentTypeAlias = null); + + bool IsPathPublished(IContent content); /// /// Used to bulk update the permissions set for a content item. This will replace all permissions @@ -31,7 +32,7 @@ namespace Umbraco.Core.Persistence.Repositories /// Clears the published flag for a content. /// /// - void ClearPublished(IContent content); + void ClearPublishedFlag(IContent content); /// /// Gets all published Content by the specified query diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeCompositionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeCompositionRepository.cs deleted file mode 100644 index 649726b100..0000000000 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeCompositionRepository.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using Umbraco.Core.Models; - -namespace Umbraco.Core.Persistence.Repositories -{ - public interface IContentTypeCompositionRepository : IRepositoryQueryable, IReadRepository - where TEntity : IContentTypeComposition - { - TEntity Get(string alias); - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepository.cs index 61d83645b3..c0c23ee0ed 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepository.cs @@ -6,7 +6,7 @@ using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.Repositories { - public interface IContentTypeRepository : IContentTypeCompositionRepository + public interface IContentTypeRepository : IContentTypeRepositoryBase { /// /// Gets all entities of the specified query @@ -21,8 +21,6 @@ namespace Umbraco.Core.Persistence.Repositories /// IEnumerable GetAllPropertyTypeAliases(); - IEnumerable> Move(IContentType toMove, EntityContainer container); - /// /// Gets all content type aliases /// diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepositoryBase.cs new file mode 100644 index 0000000000..8d49698e6b --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepositoryBase.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using Umbraco.Core.Events; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Persistence.Repositories +{ + public interface IContentTypeRepositoryBase : IRepositoryQueryable, IReadRepository + where TItem : IContentTypeComposition + { + TItem Get(string alias); + IEnumerable> Move(TItem moving, EntityContainer container); + IEnumerable GetTypesDirectlyComposedOf(int id); + + /// + /// Derives a unique alias from an existing alias. + /// + /// The original alias. + /// The original alias with a number appended to it, so that it is unique. + /// Unique accross all content, media and member types. + string GetUniqueAlias(string alias); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaTypeRepository.cs index 7f2f76e541..a601723494 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaTypeRepository.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.Repositories { - public interface IMediaTypeRepository : IContentTypeCompositionRepository + public interface IMediaTypeRepository : IContentTypeRepositoryBase { /// /// Gets all entities of the specified query @@ -13,15 +13,5 @@ namespace Umbraco.Core.Persistence.Repositories /// /// An enumerable list of objects IEnumerable GetByQuery(IQuery query); - - IEnumerable> Move(IMediaType toMove, EntityContainer container); - - /// - /// Derives a unique alias from an existing alias. - /// - /// The original alias. - /// The original alias with a number appended to it, so that it is unique. - /// Unique accross all content, media and member types. - string GetUniqueAlias(string alias); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberTypeRepository.cs index fc877d5227..9d55df0d02 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberTypeRepository.cs @@ -1,10 +1,7 @@ -using System; -using Umbraco.Core.Models; +using Umbraco.Core.Models; namespace Umbraco.Core.Persistence.Repositories { - public interface IMemberTypeRepository : IContentTypeCompositionRepository - { - - } + public interface IMemberTypeRepository : IContentTypeRepositoryBase + { } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IServerRegistrationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IServerRegistrationRepository.cs index 498082461e..5db9c6087e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IServerRegistrationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IServerRegistrationRepository.cs @@ -6,8 +6,5 @@ namespace Umbraco.Core.Persistence.Repositories public interface IServerRegistrationRepository : IRepositoryQueryable { void DeactiveStaleServers(TimeSpan staleTimeout); - - void ReadLockServers(); - void WriteLockServers(); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs index 8b3e1b52d6..ebbbe4db3a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs @@ -2,14 +2,10 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Text; using System.Text.RegularExpressions; using System.Xml.Linq; using NPoco; -using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; -using Umbraco.Core.Dynamics; -using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; @@ -21,7 +17,6 @@ using Umbraco.Core.Persistence.Mappers; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Persistence.UnitOfWork; -using Umbraco.Core.Services; namespace Umbraco.Core.Persistence.Repositories { @@ -38,8 +33,8 @@ namespace Umbraco.Core.Persistence.Repositories public MediaRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, IMediaTypeRepository mediaTypeRepository, ITagRepository tagRepository, IContentSection contentSection, IMappingResolver mappingResolver) : base(work, cache, logger, contentSection, mappingResolver) { - if (mediaTypeRepository == null) throw new ArgumentNullException("mediaTypeRepository"); - if (tagRepository == null) throw new ArgumentNullException("tagRepository"); + if (mediaTypeRepository == null) throw new ArgumentNullException(nameof(mediaTypeRepository)); + if (tagRepository == null) throw new ArgumentNullException(nameof(tagRepository)); _mediaTypeRepository = mediaTypeRepository; _tagRepository = tagRepository; _contentXmlRepository = new ContentXmlRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, mappingResolver); @@ -47,7 +42,7 @@ namespace Umbraco.Core.Persistence.Repositories EnsureUniqueNaming = contentSection.EnsureUniqueNaming; } - public bool EnsureUniqueNaming { get; private set; } + public bool EnsureUniqueNaming { get; } #region Overrides of RepositoryBase @@ -62,7 +57,7 @@ namespace Umbraco.Core.Persistence.Repositories if (dto == null) return null; - var content = CreateMediaFromDto(dto, dto.VersionId, sql); + var content = CreateMediaFromDto(dto, dto.VersionId); return content; } @@ -72,7 +67,7 @@ namespace Umbraco.Core.Persistence.Repositories var sql = GetBaseQuery(false); if (ids.Any()) { - sql.Where("umbracoNode.id in (@ids)", new { ids = ids }); + sql.Where("umbracoNode.id in (@ids)", new { /*ids =*/ ids }); } return ProcessQuery(sql); @@ -139,10 +134,7 @@ namespace Umbraco.Core.Persistence.Repositories return list; } - protected override Guid NodeObjectTypeId - { - get { return new Guid(Constants.ObjectTypes.Media); } - } + protected override Guid NodeObjectTypeId => new Guid(Constants.ObjectTypes.Media); #endregion @@ -174,94 +166,11 @@ namespace Umbraco.Core.Persistence.Repositories return media; } - public void RebuildXmlStructures(Func serializer, int groupSize = 5000, IEnumerable contentTypeIds = null) - { - - //Ok, now we need to remove the data and re-insert it, we'll do this all in one transaction too. - using (var tr = Database.GetTransaction()) - { - //Remove all the data first, if anything fails after this it's no problem the transaction will be reverted - if (contentTypeIds == null) - { - var mediaObjectType = Guid.Parse(Constants.ObjectTypes.Media); - var subQuery = Sql() - .Select("DISTINCT cmsContentXml.nodeId") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(dto => dto.NodeObjectType == mediaObjectType); - - var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); - Database.Execute(deleteSql); - } - else - { - foreach (var id in contentTypeIds) - { - var id1 = id; - var mediaObjectType = Guid.Parse(Constants.ObjectTypes.Media); - var subQuery = Sql() - .Select("DISTINCT cmsContentXml.nodeId") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(dto => dto.NodeObjectType == mediaObjectType) - .Where(dto => dto.ContentTypeId == id1); - - var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); - Database.Execute(deleteSql); - } - } - - //now insert the data, again if something fails here, the whole transaction is reversed - if (contentTypeIds == null) - { - RebuildXmlStructuresProcessQuery(serializer, Query, tr, groupSize); - } - else - { - foreach (var contentTypeId in contentTypeIds) - { - //copy local - var id = contentTypeId; - var query = Query.Where(x => x.ContentTypeId == id && x.Trashed == false); - RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); - } - } - - tr.Complete(); - } - } - - private void RebuildXmlStructuresProcessQuery(Func serializer, IQuery query, ITransaction tr, int pageSize) - { - var pageIndex = 0; - var total = long.MinValue; - var processed = 0; - do - { - var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending, true); - - var xmlItems = (from descendant in descendants - let xml = serializer(descendant) - select new ContentXmlDto { NodeId = descendant.Id, Xml = xml.ToDataString() }).ToArray(); - - //bulk insert it into the database - Database.BulkInsertRecords(SqlSyntax, xmlItems, tr); - - processed += xmlItems.Length; - - pageIndex++; - } while (processed < total); - } - public IMedia GetMediaByPath(string mediaPath) { var umbracoFileValue = mediaPath; - const string Pattern = ".*[_][0-9]+[x][0-9]+[.].*"; - var isResized = Regex.IsMatch(mediaPath, Pattern); + const string pattern = ".*[_][0-9]+[x][0-9]+[.].*"; + var isResized = Regex.IsMatch(mediaPath, pattern); // If the image has been resized we strip the "_403x328" of the original "/media/1024/koala_403x328.jpg" url. if (isResized) @@ -292,17 +201,6 @@ namespace Umbraco.Core.Persistence.Repositories return propertyDataDto == null ? null : Get(propertyDataDto.NodeId); } - - public void AddOrUpdateContentXml(IMedia content, Func xml) - { - _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(content, xml)); - } - - public void AddOrUpdatePreviewXml(IMedia content, Func xml) - { - _contentPreviewRepository.AddOrUpdate(new ContentPreviewEntity(content, xml)); - } - protected override void PerformDeleteVersion(int id, Guid versionId) { Database.Delete("WHERE nodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); @@ -329,10 +227,10 @@ namespace Umbraco.Core.Persistence.Repositories //NOTE Should the logic below have some kind of fallback for empty parent ids ? //Logic for setting Path, Level and SortOrder - var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); + var parent = Database.First("WHERE id = @ParentId", new { /*ParentId =*/ entity.ParentId }); var level = parent.Level + 1; var maxSortOrder = Database.ExecuteScalar( - "SELECT coalesce(max(sortOrder),-1) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", + "SELECT coalesce(max(sortOrder),-1) FROM umbracoNode WHERE parentId = @ParentId AND nodeObjectType = @NodeObjectType", new { /*ParentId =*/ entity.ParentId, NodeObjectType = NodeObjectTypeId }); var sortOrder = maxSortOrder + 1; @@ -341,7 +239,7 @@ namespace Umbraco.Core.Persistence.Repositories nodeDto.Path = parent.Path; nodeDto.Level = short.Parse(level.ToString(CultureInfo.InvariantCulture)); nodeDto.SortOrder = sortOrder; - var o = Database.IsNew(nodeDto) ? Convert.ToInt32(Database.Insert(nodeDto)) : Database.Update(nodeDto); + var o = Database.IsNew(nodeDto) ? Convert.ToInt32(Database.Insert(nodeDto)) : Database.Update(nodeDto); //Update with new correct path nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); @@ -381,7 +279,7 @@ namespace Umbraco.Core.Persistence.Repositories property.Id = keyDictionary[property.PropertyTypeId]; } - UpdatePropertyTags(entity, _tagRepository); + UpdateEntityTags(entity, _tagRepository); entity.ResetDirtyProperties(); } @@ -400,19 +298,19 @@ namespace Umbraco.Core.Persistence.Repositories //Look up parent to get and set the correct Path and update SortOrder if ParentId has changed if (entity.IsPropertyDirty("ParentId")) { - var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); + var parent = Database.First("WHERE id = @ParentId", new { /*ParentId =*/ entity.ParentId }); entity.Path = string.Concat(parent.Path, ",", entity.Id); entity.Level = parent.Level + 1; var maxSortOrder = Database.ExecuteScalar( "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", - new { ParentId = entity.ParentId, NodeObjectType = NodeObjectTypeId }); + new { /*ParentId =*/ entity.ParentId, NodeObjectType = NodeObjectTypeId }); entity.SortOrder = maxSortOrder + 1; } var factory = new MediaFactory(NodeObjectTypeId, entity.Id); //Look up Content entry to get Primary for updating the DTO - var contentDto = Database.SingleOrDefault("WHERE nodeId = @Id", new { Id = entity.Id }); + var contentDto = Database.SingleOrDefault("WHERE nodeId = @Id", new { /*Id =*/ entity.Id }); factory.SetPrimaryKey(contentDto.PrimaryKey); var dto = factory.BuildDto(entity); @@ -429,7 +327,7 @@ namespace Umbraco.Core.Persistence.Repositories } //In order to update the ContentVersion we need to retrieve its primary key id - var contentVerDto = Database.SingleOrDefault("WHERE VersionId = @Version", new { Version = entity.Version }); + var contentVerDto = Database.SingleOrDefault("WHERE VersionId = @Version", new { /*Version =*/ entity.Version }); dto.Id = contentVerDto.Id; //Updates the current version - cmsContentVersion //Assumes a Version guid exists and Version date (modified date) has been set/updated @@ -463,7 +361,7 @@ namespace Umbraco.Core.Persistence.Repositories } } - UpdatePropertyTags(entity, _tagRepository); + UpdateEntityTags(entity, _tagRepository); entity.ResetDirtyProperties(); } @@ -472,10 +370,7 @@ namespace Umbraco.Core.Persistence.Repositories #region IRecycleBinRepository members - protected override int RecycleBinId - { - get { return Constants.System.RecycleBinMedia; } - } + protected override int RecycleBinId => Constants.System.RecycleBinMedia; #endregion @@ -544,7 +439,7 @@ namespace Umbraco.Core.Persistence.Repositories /// /// Private method to create a media object from a ContentDto /// - /// + /// /// /// /// @@ -566,11 +461,10 @@ namespace Umbraco.Core.Persistence.Repositories /// /// Private method to create a media object from a ContentDto /// - /// + /// /// - /// /// - private IMedia CreateMediaFromDto(ContentVersionDto dto, Guid versionId, Sql docSql) + private IMedia CreateMediaFromDto(ContentVersionDto dto, Guid versionId) { var contentType = _mediaTypeRepository.Get(dto.ContentDto.ContentTypeId); @@ -612,7 +506,7 @@ namespace Umbraco.Core.Persistence.Repositories if (dto.Text.ToLowerInvariant().Equals(currentName.ToLowerInvariant())) { - currentName = nodeName + string.Format(" ({0})", uniqueNumber); + currentName = $"{nodeName} ({uniqueNumber})"; uniqueNumber++; } } @@ -620,5 +514,103 @@ namespace Umbraco.Core.Persistence.Repositories return currentName; } + + #region Xml - Should Move! + + public void RebuildXmlStructures(Func serializer, int groupSize = 5000, IEnumerable contentTypeIds = null) + { + + //Ok, now we need to remove the data and re-insert it, we'll do this all in one transaction too. + using (var tr = Database.GetTransaction()) + { + //Remove all the data first, if anything fails after this it's no problem the transaction will be reverted + if (contentTypeIds == null) + { + var mediaObjectType = Guid.Parse(Constants.ObjectTypes.Media); + var subQuery = Sql() + .Select("DISTINCT cmsContentXml.nodeId") + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .Where(dto => dto.NodeObjectType == mediaObjectType); + + var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); + Database.Execute(deleteSql); + } + else + { + foreach (var id in contentTypeIds) + { + var id1 = id; + var mediaObjectType = Guid.Parse(Constants.ObjectTypes.Media); + var subQuery = Sql() + .Select("DISTINCT cmsContentXml.nodeId") + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .Where(dto => dto.NodeObjectType == mediaObjectType) + .Where(dto => dto.ContentTypeId == id1); + + var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); + Database.Execute(deleteSql); + } + } + + //now insert the data, again if something fails here, the whole transaction is reversed + if (contentTypeIds == null) + { + RebuildXmlStructuresProcessQuery(serializer, Query, tr, groupSize); + } + else + { + foreach (var contentTypeId in contentTypeIds) + { + //copy local + var id = contentTypeId; + var query = Query.Where(x => x.ContentTypeId == id && x.Trashed == false); + RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); + } + } + + tr.Complete(); + } + } + + private void RebuildXmlStructuresProcessQuery(Func serializer, IQuery query, ITransaction tr, int pageSize) + { + var pageIndex = 0; + long total; + var processed = 0; + do + { + var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending, true); + + var xmlItems = (from descendant in descendants + let xml = serializer(descendant) + select new ContentXmlDto { NodeId = descendant.Id, Xml = xml.ToDataString() }).ToArray(); + + //bulk insert it into the database + Database.BulkInsertRecords(SqlSyntax, xmlItems, tr); + + processed += xmlItems.Length; + + pageIndex++; + } while (processed < total); + } + + + public void AddOrUpdateContentXml(IMedia content, Func xml) + { + _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(content, xml)); + } + + public void AddOrUpdatePreviewXml(IMedia content, Func xml) + { + _contentPreviewRepository.AddOrUpdate(new ContentPreviewEntity(content, xml)); + } + + #endregion } } diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs index 68f3dcfd05..546b90e107 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs @@ -3,32 +3,23 @@ using System.Collections.Generic; using System.Linq; using NPoco; using Umbraco.Core.Cache; -using Umbraco.Core.Events; -using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models; -using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Rdbms; - -using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Mappers; using Umbraco.Core.Persistence.Querying; -using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Persistence.UnitOfWork; -using Umbraco.Core.Services; namespace Umbraco.Core.Persistence.Repositories { /// /// Represents a repository for doing CRUD operations for /// - internal class MediaTypeRepository : ContentTypeBaseRepository, IMediaTypeRepository + internal class MediaTypeRepository : ContentTypeRepositoryBase, IMediaTypeRepository { - public MediaTypeRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, IMappingResolver mappingResolver) : base(work, cache, logger, mappingResolver) - { - } + { } private FullDataSetRepositoryCachePolicyFactory _cachePolicyFactory; protected override IRepositoryCachePolicyFactory CachePolicyFactory @@ -49,6 +40,23 @@ namespace Umbraco.Core.Persistence.Repositories return GetAll().FirstOrDefault(x => x.Id == id); } + protected override IMediaType PerformGet(Guid id) + { + //use the underlying GetAll which will force cache all content types + return GetAll().FirstOrDefault(x => x.Key == id); + } + + protected override bool PerformExists(Guid id) + { + return GetAll().FirstOrDefault(x => x.Key == id) != null; + } + + protected override IMediaType PerformGet(string alias) + { + //use the underlying GetAll which will force cache all content types + return GetAll().FirstOrDefault(x => x.Alias.InvariantEquals(alias)); + } + protected override IEnumerable PerformGetAll(params int[] ids) { if (ids.Any()) @@ -61,6 +69,20 @@ namespace Umbraco.Core.Persistence.Repositories return ContentTypeQueryMapper.GetMediaTypes(Database, SqlSyntax, this); } + protected override IEnumerable PerformGetAll(params Guid[] ids) + { + //use the underlying GetAll which will force cache all content types + + if (ids.Any()) + { + return GetAll().Where(x => ids.Contains(x.Key)); + } + else + { + return GetAll(); + } + } + protected override IEnumerable PerformGetByQuery(IQuery query) { var sqlClause = GetBaseQuery(false); @@ -82,7 +104,7 @@ namespace Umbraco.Core.Persistence.Repositories /// Gets all entities of the specified query /// /// - /// An enumerable list of objects + /// An enumerable list of objects public IEnumerable GetByQuery(IQuery query) { var ints = PerformGetByQuery(query).ToArray(); @@ -116,28 +138,14 @@ namespace Umbraco.Core.Persistence.Repositories protected override IEnumerable GetDeleteClauses() { - var list = new List - { - "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @Id", - "DELETE FROM umbracoUser2NodePermission WHERE nodeId = @Id", - "DELETE FROM cmsTagRelationship WHERE nodeId = @Id", - "DELETE FROM cmsContentTypeAllowedContentType WHERE Id = @Id", - "DELETE FROM cmsContentTypeAllowedContentType WHERE AllowedId = @Id", - "DELETE FROM cmsContentType2ContentType WHERE parentContentTypeId = @Id", - "DELETE FROM cmsContentType2ContentType WHERE childContentTypeId = @Id", - "DELETE FROM cmsPropertyType WHERE contentTypeId = @Id", - "DELETE FROM cmsPropertyTypeGroup WHERE contenttypeNodeId = @Id", - "DELETE FROM cmsContentType WHERE nodeId = @Id", - "DELETE FROM umbracoNode WHERE id = @Id" - }; - return list; + var l = (List) base.GetDeleteClauses(); // we know it's a list + l.Add("DELETE FROM cmsContentType WHERE nodeId = @Id"); + l.Add("DELETE FROM umbracoNode WHERE id = @Id"); + return l; } - protected override Guid NodeObjectTypeId - { - get { return new Guid(Constants.ObjectTypes.MediaType); } - } - + protected override Guid NodeObjectTypeId => Constants.ObjectTypes.MediaTypeGuid; + protected override void PersistNewItem(IMediaType entity) { ((MediaType)entity).AddingEntity(); @@ -171,36 +179,5 @@ namespace Umbraco.Core.Persistence.Repositories entity.ResetDirtyProperties(); } - - protected override IMediaType PerformGet(Guid id) - { - //use the underlying GetAll which will force cache all content types - return GetAll().FirstOrDefault(x => x.Key == id); - } - - protected override IEnumerable PerformGetAll(params Guid[] ids) - { - //use the underlying GetAll which will force cache all content types - - if (ids.Any()) - { - return GetAll().Where(x => ids.Contains(x.Key)); - } - else - { - return GetAll(); - } - } - - protected override bool PerformExists(Guid id) - { - return GetAll().FirstOrDefault(x => x.Key == id) != null; - } - - protected override IMediaType PerformGet(string alias) - { - //use the underlying GetAll which will force cache all content types - return GetAll().FirstOrDefault(x => x.Alias.InvariantEquals(alias)); - } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs index a266b71eea..74a82efa08 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs @@ -2,13 +2,9 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Linq.Expressions; -using System.Text; using System.Xml.Linq; using NPoco; -using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; -using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models; @@ -19,7 +15,6 @@ using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Persistence.UnitOfWork; -using Umbraco.Core.Dynamics; using Umbraco.Core.Persistence.Mappers; namespace Umbraco.Core.Persistence.Repositories @@ -38,8 +33,8 @@ namespace Umbraco.Core.Persistence.Repositories public MemberRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, IMemberTypeRepository memberTypeRepository, IMemberGroupRepository memberGroupRepository, ITagRepository tagRepository, IContentSection contentSection, IMappingResolver mappingResolver) : base(work, cache, logger, contentSection, mappingResolver) { - if (memberTypeRepository == null) throw new ArgumentNullException("memberTypeRepository"); - if (tagRepository == null) throw new ArgumentNullException("tagRepository"); + if (memberTypeRepository == null) throw new ArgumentNullException(nameof(memberTypeRepository)); + if (tagRepository == null) throw new ArgumentNullException(nameof(tagRepository)); _memberTypeRepository = memberTypeRepository; _tagRepository = tagRepository; _memberGroupRepository = memberGroupRepository; @@ -60,7 +55,7 @@ namespace Umbraco.Core.Persistence.Repositories if (dto == null) return null; - var content = CreateMemberFromDto(dto, dto.ContentVersionDto.VersionId, sql); + var content = CreateMemberFromDto(dto, dto.ContentVersionDto.VersionId); return content; @@ -71,7 +66,7 @@ namespace Umbraco.Core.Persistence.Repositories var sql = GetBaseQuery(false); if (ids.Any()) { - sql.Where("umbracoNode.id in (@ids)", new { ids = ids }); + sql.Where("umbracoNode.id in (@ids)", new { /*ids =*/ ids }); } return MapQueryDtos(Database.Fetch(sql)); @@ -117,8 +112,8 @@ namespace Umbraco.Core.Persistence.Repositories { var sql = Sql(); - sql = isCount - ? sql.SelectCount() + sql = isCount + ? sql.SelectCount() : sql.Select(r => r.Select(rr => rr.Select(rrr => @@ -184,10 +179,7 @@ namespace Umbraco.Core.Persistence.Repositories return list; } - protected override Guid NodeObjectTypeId - { - get { return new Guid(Constants.ObjectTypes.Member); } - } + protected override Guid NodeObjectTypeId => new Guid(Constants.ObjectTypes.Member); #endregion @@ -205,18 +197,18 @@ namespace Umbraco.Core.Persistence.Repositories //NOTE Should the logic below have some kind of fallback for empty parent ids ? //Logic for setting Path, Level and SortOrder - var parent = Database.First("WHERE id = @ParentId", new { ParentId = ((IUmbracoEntity)entity).ParentId }); - int level = parent.Level + 1; - int sortOrder = + var parent = Database.First("WHERE id = @ParentId", new { /*ParentId =*/ entity.ParentId }); + var level = parent.Level + 1; + var sortOrder = Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoNode WHERE parentID = @ParentId AND nodeObjectType = @NodeObjectType", - new { ParentId = ((IUmbracoEntity)entity).ParentId, NodeObjectType = NodeObjectTypeId }); + new { /*ParentId =*/ entity.ParentId, NodeObjectType = NodeObjectTypeId }); //Create the (base) node data - umbracoNode var nodeDto = dto.ContentVersionDto.ContentDto.NodeDto; nodeDto.Path = parent.Path; nodeDto.Level = short.Parse(level.ToString(CultureInfo.InvariantCulture)); nodeDto.SortOrder = sortOrder; - var o = Database.IsNew(nodeDto) ? Convert.ToInt32(Database.Insert(nodeDto)) : Database.Update(nodeDto); + var o = Database.IsNew(nodeDto) ? Convert.ToInt32(Database.Insert(nodeDto)) : Database.Update(nodeDto); //Update with new correct path nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); @@ -265,7 +257,7 @@ namespace Umbraco.Core.Persistence.Repositories property.Id = keyDictionary[property.PropertyTypeId]; } - UpdatePropertyTags(entity, _tagRepository); + UpdateEntityTags(entity, _tagRepository); ((Member)entity).ResetDirtyProperties(); } @@ -283,19 +275,19 @@ namespace Umbraco.Core.Persistence.Repositories //Look up parent to get and set the correct Path and update SortOrder if ParentId has changed if (dirtyEntity.IsPropertyDirty("ParentId")) { - var parent = Database.First("WHERE id = @ParentId", new { ParentId = ((IUmbracoEntity)entity).ParentId }); - ((IUmbracoEntity)entity).Path = string.Concat(parent.Path, ",", entity.Id); - ((IUmbracoEntity)entity).Level = parent.Level + 1; + var parent = Database.First("WHERE id = @ParentId", new { /*ParentId =*/ entity.ParentId }); + entity.Path = string.Concat(parent.Path, ",", entity.Id); + entity.Level = parent.Level + 1; var maxSortOrder = Database.ExecuteScalar( "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", - new { ParentId = ((IUmbracoEntity)entity).ParentId, NodeObjectType = NodeObjectTypeId }); - ((IUmbracoEntity)entity).SortOrder = maxSortOrder + 1; + new { /*ParentId =*/ entity.ParentId, NodeObjectType = NodeObjectTypeId }); + entity.SortOrder = maxSortOrder + 1; } var factory = new MemberFactory(NodeObjectTypeId, entity.Id); //Look up Content entry to get Primary for updating the DTO - var contentDto = Database.SingleOrDefault("WHERE nodeId = @Id", new { Id = entity.Id }); + var contentDto = Database.SingleOrDefault("WHERE nodeId = @Id", new { /*Id =*/ entity.Id }); factory.SetPrimaryKey(contentDto.PrimaryKey); var dto = factory.BuildDto(entity); @@ -312,7 +304,7 @@ namespace Umbraco.Core.Persistence.Repositories } //In order to update the ContentVersion we need to retrieve its primary key id - var contentVerDto = Database.SingleOrDefault("WHERE VersionId = @Version", new { Version = entity.Version }); + var contentVerDto = Database.SingleOrDefault("WHERE VersionId = @Version", new { /*Version =*/ entity.Version }); dto.ContentVersionDto.Id = contentVerDto.Id; //Updates the current version - cmsContentVersion //Assumes a Version guid exists and Version date (modified date) has been set/updated @@ -378,7 +370,7 @@ namespace Umbraco.Core.Persistence.Repositories } } - UpdatePropertyTags(entity, _tagRepository); + UpdateEntityTags(entity, _tagRepository); dirtyEntity.ResetDirtyProperties(); } @@ -387,90 +379,6 @@ namespace Umbraco.Core.Persistence.Repositories #region Overrides of VersionableRepositoryBase - public void RebuildXmlStructures(Func serializer, int groupSize = 5000, IEnumerable contentTypeIds = null) - { - - //Ok, now we need to remove the data and re-insert it, we'll do this all in one transaction too. - using (var tr = Database.GetTransaction()) - { - //Remove all the data first, if anything fails after this it's no problem the transaction will be reverted - if (contentTypeIds == null) - { - var memberObjectType = Guid.Parse(Constants.ObjectTypes.Member); - var subQuery = Sql() - .Select("DISTINCT cmsContentXml.nodeId") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(dto => dto.NodeObjectType == memberObjectType); - - var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); - Database.Execute(deleteSql); - } - else - { - foreach (var id in contentTypeIds) - { - var id1 = id; - var memberObjectType = Guid.Parse(Constants.ObjectTypes.Member); - var subQuery = Sql() - .Select("DISTINCT cmsContentXml.nodeId") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(dto => dto.NodeObjectType == memberObjectType) - .Where( dto => dto.ContentTypeId == id1); - - var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); - Database.Execute(deleteSql); - } - } - - //now insert the data, again if something fails here, the whole transaction is reversed - if (contentTypeIds == null) - { - var query = Query; - RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); - } - else - { - foreach (var contentTypeId in contentTypeIds) - { - //copy local - var id = contentTypeId; - var query = Query.Where(x => x.ContentTypeId == id && x.Trashed == false); - RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); - } - } - - tr.Complete(); - } - } - - private void RebuildXmlStructuresProcessQuery(Func serializer, IQuery query, ITransaction tr, int pageSize) - { - var pageIndex = 0; - var total = long.MinValue; - var processed = 0; - do - { - var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending, true); - - var xmlItems = (from descendant in descendants - let xml = serializer(descendant) - select new ContentXmlDto { NodeId = descendant.Id, Xml = xml.ToDataString() }).ToArray(); - - //bulk insert it into the database - Database.BulkInsertRecords(SqlSyntax, xmlItems, tr); - - processed += xmlItems.Length; - - pageIndex++; - } while (processed < total); - } - public override IMember GetByVersion(Guid versionId) { var sql = GetBaseQuery(false); @@ -534,7 +442,7 @@ namespace Umbraco.Core.Persistence.Repositories query.Where(member => member.Username.SqlWildcard(usernameToMatch, TextColumnType.NVarchar)); break; default: - throw new ArgumentOutOfRangeException("matchType"); + throw new ArgumentOutOfRangeException(nameof(matchType)); } var matchedMembers = GetByQuery(query).ToArray(); @@ -635,16 +543,6 @@ namespace Umbraco.Core.Persistence.Repositories filterSql); } - public void AddOrUpdateContentXml(IMember content, Func xml) - { - _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(content, xml)); - } - - public void AddOrUpdatePreviewXml(IMember content, Func xml) - { - _contentPreviewRepository.AddOrUpdate(new ContentPreviewEntity(content, xml)); - } - protected override string GetDatabaseFieldNameForOrderBy(string orderBy) { //Some custom ones @@ -716,9 +614,8 @@ namespace Umbraco.Core.Persistence.Repositories /// /// /// - /// /// - private IMember CreateMemberFromDto(MemberDto dto, Guid versionId, Sql docSql) + private IMember CreateMemberFromDto(MemberDto dto, Guid versionId) { var memberType = _memberTypeRepository.Get(dto.ContentVersionDto.ContentDto.ContentTypeId); @@ -736,5 +633,103 @@ namespace Umbraco.Core.Persistence.Repositories ((Entity)member).ResetDirtyProperties(false); return member; } + + #region Xml - Should Move! + + public void AddOrUpdateContentXml(IMember content, Func xml) + { + _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(content, xml)); + } + + public void AddOrUpdatePreviewXml(IMember content, Func xml) + { + _contentPreviewRepository.AddOrUpdate(new ContentPreviewEntity(content, xml)); + } + + public void RebuildXmlStructures(Func serializer, int groupSize = 5000, IEnumerable contentTypeIds = null) + { + + //Ok, now we need to remove the data and re-insert it, we'll do this all in one transaction too. + using (var tr = Database.GetTransaction()) + { + //Remove all the data first, if anything fails after this it's no problem the transaction will be reverted + if (contentTypeIds == null) + { + var memberObjectType = Guid.Parse(Constants.ObjectTypes.Member); + var subQuery = Sql() + .Select("DISTINCT cmsContentXml.nodeId") + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .Where(dto => dto.NodeObjectType == memberObjectType); + + var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); + Database.Execute(deleteSql); + } + else + { + foreach (var id in contentTypeIds) + { + var id1 = id; + var memberObjectType = Guid.Parse(Constants.ObjectTypes.Member); + var subQuery = Sql() + .Select("DISTINCT cmsContentXml.nodeId") + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .Where(dto => dto.NodeObjectType == memberObjectType) + .Where(dto => dto.ContentTypeId == id1); + + var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); + Database.Execute(deleteSql); + } + } + + //now insert the data, again if something fails here, the whole transaction is reversed + if (contentTypeIds == null) + { + var query = Query; + RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); + } + else + { + foreach (var contentTypeId in contentTypeIds) + { + //copy local + var id = contentTypeId; + var query = Query.Where(x => x.ContentTypeId == id && x.Trashed == false); + RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); + } + } + + tr.Complete(); + } + } + + private void RebuildXmlStructuresProcessQuery(Func serializer, IQuery query, ITransaction tr, int pageSize) + { + var pageIndex = 0; + long total; + var processed = 0; + do + { + var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending, true); + + var xmlItems = (from descendant in descendants + let xml = serializer(descendant) + select new ContentXmlDto { NodeId = descendant.Id, Xml = xml.ToDataString() }).ToArray(); + + //bulk insert it into the database + Database.BulkInsertRecords(SqlSyntax, xmlItems, tr); + + processed += xmlItems.Length; + + pageIndex++; + } while (processed < total); + } + + #endregion } } diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs index 95f26447c4..81cc28b05c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs @@ -1,18 +1,15 @@ using System; using System.Collections.Generic; using System.Linq; -using log4net; using NPoco; using Umbraco.Core.Cache; using Umbraco.Core.Logging; -using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models; using Umbraco.Core.Models.Rdbms; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Mappers; using Umbraco.Core.Persistence.Querying; -using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Persistence.Repositories @@ -20,13 +17,11 @@ namespace Umbraco.Core.Persistence.Repositories /// /// Represents a repository for doing CRUD operations for /// - internal class MemberTypeRepository : ContentTypeBaseRepository, IMemberTypeRepository + internal class MemberTypeRepository : ContentTypeRepositoryBase, IMemberTypeRepository { - public MemberTypeRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, IMappingResolver mappingResolver) : base(work, cache, logger, mappingResolver) - { - } + { } private FullDataSetRepositoryCachePolicyFactory _cachePolicyFactory; protected override IRepositoryCachePolicyFactory CachePolicyFactory @@ -47,6 +42,37 @@ namespace Umbraco.Core.Persistence.Repositories return GetAll().FirstOrDefault(x => x.Id == id); } + protected override IMemberType PerformGet(Guid id) + { + //use the underlying GetAll which will force cache all content types + return GetAll().FirstOrDefault(x => x.Key == id); + } + + protected override IEnumerable PerformGetAll(params Guid[] ids) + { + //use the underlying GetAll which will force cache all content types + + if (ids.Any()) + { + return GetAll().Where(x => ids.Contains(x.Key)); + } + else + { + return GetAll(); + } + } + + protected override bool PerformExists(Guid id) + { + return GetAll().FirstOrDefault(x => x.Key == id) != null; + } + + protected override IMemberType PerformGet(string alias) + { + //use the underlying GetAll which will force cache all content types + return GetAll().FirstOrDefault(x => x.Alias.InvariantEquals(alias)); + } + protected override IEnumerable PerformGetAll(params int[] ids) { var sql = GetBaseQuery(false); @@ -165,28 +191,14 @@ namespace Umbraco.Core.Persistence.Repositories protected override IEnumerable GetDeleteClauses() { - var list = new List - { - "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @Id", - "DELETE FROM umbracoUser2NodePermission WHERE nodeId = @Id", - "DELETE FROM cmsTagRelationship WHERE nodeId = @Id", - "DELETE FROM cmsContentTypeAllowedContentType WHERE Id = @Id", - "DELETE FROM cmsContentTypeAllowedContentType WHERE AllowedId = @Id", - "DELETE FROM cmsContentType2ContentType WHERE parentContentTypeId = @Id", - "DELETE FROM cmsContentType2ContentType WHERE childContentTypeId = @Id", - "DELETE FROM cmsPropertyType WHERE contentTypeId = @Id", - "DELETE FROM cmsPropertyTypeGroup WHERE contenttypeNodeId = @Id", - "DELETE FROM cmsMemberType WHERE NodeId = @Id", - "DELETE FROM cmsContentType WHERE nodeId = @Id", - "DELETE FROM umbracoNode WHERE id = @Id" - }; - return list; + var l = (List)base.GetDeleteClauses(); // we know it's a list + l.Add("DELETE FROM cmsMemberType WHERE NodeId = @Id"); + l.Add("DELETE FROM cmsContentType WHERE nodeId = @Id"); + l.Add("DELETE FROM umbracoNode WHERE id = @Id"); + return l; } - protected override Guid NodeObjectTypeId - { - get { return new Guid(Constants.ObjectTypes.MemberType); } - } + protected override Guid NodeObjectTypeId => Constants.ObjectTypes.MemberTypeGuid; protected override void PersistNewItem(IMemberType entity) { @@ -278,37 +290,6 @@ namespace Umbraco.Core.Persistence.Repositories propertyTypeAlias); } - protected override IMemberType PerformGet(Guid id) - { - //use the underlying GetAll which will force cache all content types - return GetAll().FirstOrDefault(x => x.Key == id); - } - - protected override IEnumerable PerformGetAll(params Guid[] ids) - { - //use the underlying GetAll which will force cache all content types - - if (ids.Any()) - { - return GetAll().Where(x => ids.Contains(x.Key)); - } - else - { - return GetAll(); - } - } - - protected override bool PerformExists(Guid id) - { - return GetAll().FirstOrDefault(x => x.Key == id) != null; - } - - protected override IMemberType PerformGet(string alias) - { - //use the underlying GetAll which will force cache all content types - return GetAll().FirstOrDefault(x => x.Alias.InvariantEquals(alias)); - } - /// /// Ensure that all the built-in membership provider properties have their correct data type /// and property editors assigned. This occurs prior to saving so that the correct values are persisted. diff --git a/src/Umbraco.Core/Persistence/Repositories/ServerRegistrationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ServerRegistrationRepository.cs index 265e7c2857..37f249df6e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ServerRegistrationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ServerRegistrationRepository.cs @@ -18,7 +18,6 @@ namespace Umbraco.Core.Persistence.Repositories internal class ServerRegistrationRepository : NPocoRepositoryBase, IServerRegistrationRepository { private readonly ICacheProvider _staticCache; - private readonly int[] _lockIds = { Constants.System.ServersLock }; public ServerRegistrationRepository(IDatabaseUnitOfWork work, CacheHelper cacheHelper, ILogger logger, IMappingResolver mappingResolver) : base(work, CacheHelper.CreateDisabledCacheHelper(), logger, mappingResolver) @@ -147,15 +146,5 @@ namespace Umbraco.Core.Persistence.Repositories Database.Update("SET isActive=0, isMaster=0 WHERE lastNotifiedDate < @timeoutDate", new { /*timeoutDate =*/ timeoutDate }); ReloadCache(); } - - public void ReadLockServers() - { - UnitOfWork.ReadLockNodes(_lockIds); - } - - public void WriteLockServers() - { - UnitOfWork.WriteLockNodes(_lockIds); - } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs index 0e6fb58dea..3b0550a8a6 100644 --- a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs @@ -230,7 +230,7 @@ namespace Umbraco.Core.Persistence.Repositories /// /// /// - protected void UpdatePropertyTags(IContentBase entity, ITagRepository tagRepo) + protected void UpdateEntityTags(IContentBase entity, ITagRepository tagRepo) { foreach (var tagProp in entity.Properties.Where(x => x.TagSupport.Enable)) { @@ -254,6 +254,10 @@ namespace Umbraco.Core.Persistence.Repositories } } + protected bool HasTagProperty(IContentBase entity) + { + return entity.Properties.Any(x => x.TagSupport.Enable); + } private Sql PrepareSqlForPagedResults(Sql sql, Sql filterSql, string orderBy, Direction orderDirection, bool orderBySystemField) { @@ -315,14 +319,14 @@ namespace Umbraco.Core.Persistence.Repositories END AS CustomPropVal, cd.nodeId AS CustomPropValContentId FROM cmsDocument cd - INNER JOIN cmsPropertyData cpd ON cpd.contentNodeId = cd.nodeId AND cpd.versionId = cd.versionId + INNER JOIN cmsPropertyData cpd ON cpd.contentNodeId = cd.nodeId AND cpd.versionId = cd.versionId INNER JOIN cmsPropertyType cpt ON cpt.Id = cpd.propertytypeId WHERE cpt.Alias = @2 AND cd.newest = 1) AS CustomPropData ON CustomPropData.CustomPropValContentId = umbracoNode.id ", sortedInt, sortedDecimal, sortedDate, sortedString); //insert this just above the LEFT OUTER JOIN - var newSql = psql.SQL.Insert(psql.SQL.IndexOf("LEFT OUTER JOIN"), innerJoinTempTable); + var newSql = psql.SQL.Insert(psql.SQL.IndexOf("LEFT OUTER JOIN"), innerJoinTempTable); var newArgs = psql.Arguments.ToList(); newArgs.Add(orderBy); @@ -522,7 +526,7 @@ namespace Umbraco.Core.Persistence.Repositories /// /// /// - public virtual bool DeleteMediaFiles(IEnumerable files) + public virtual bool DeleteMediaFiles(IEnumerable files) // fixme kill eventually { //ensure duplicates are removed files = files.Distinct(); diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs index fc26bcd7c9..90e01b88fb 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs @@ -17,6 +17,7 @@ namespace Umbraco.Core.Persistence.SqlSyntax string GetWildcardPlaceholder(); string GetStringColumnEqualComparison(string column, int paramIndex, TextColumnType columnType); string GetStringColumnWildcardComparison(string column, int paramIndex, TextColumnType columnType); + string GetConcat(params string[] args); [Obsolete("Use the overload with the parameter index instead")] string GetStringColumnEqualComparison(string column, string value, TextColumnType columnType); diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs index 479d575fc1..4d2200b44c 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs @@ -11,7 +11,7 @@ namespace Umbraco.Core.Persistence.SqlSyntax /// /// Represents an SqlSyntaxProvider for MySql /// - [SqlSyntaxProviderAttribute(Constants.DbProviderNames.MySql)] + [SqlSyntaxProvider(Constants.DbProviderNames.MySql)] public class MySqlSyntaxProvider : SqlSyntaxProviderBase { private readonly ILogger _logger; diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs index 81db82a890..b169943b80 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs @@ -11,14 +11,9 @@ namespace Umbraco.Core.Persistence.SqlSyntax /// /// Represents an SqlSyntaxProvider for Sql Ce /// - [SqlSyntaxProviderAttribute(Constants.DbProviderNames.SqlCe)] + [SqlSyntaxProvider(Constants.DbProviderNames.SqlCe)] public class SqlCeSyntaxProvider : MicrosoftSqlSyntaxProviderBase { - public SqlCeSyntaxProvider() - { - - } - public override bool SupportsClustered() { return false; @@ -64,7 +59,10 @@ namespace Umbraco.Core.Persistence.SqlSyntax } } - + public override string GetConcat(params string[] args) + { + return "(" + string.Join("+", args) + ")"; + } public override string FormatColumnRename(string tableName, string oldName, string newName) { diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs index a992cdd231..e194c7c79c 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs @@ -71,7 +71,7 @@ namespace Umbraco.Core.Persistence.SqlSyntax public string DateTimeColumnDefinition = "DATETIME"; public string TimeColumnDefinition = "DATETIME"; - protected IList> ClauseOrder { get; set; } + protected IList> ClauseOrder { get; } protected DbTypes DbTypeMap = new DbTypes(); protected void InitColumnTypeMap() @@ -172,6 +172,11 @@ namespace Umbraco.Core.Persistence.SqlSyntax return string.Format("upper({0}) LIKE '{1}'", column, value.ToUpper()); } + public virtual string GetConcat(params string[] args) + { + return "concat(" + string.Join(",", args) + ")"; + } + public virtual string GetQuotedTableName(string tableName) { return string.Format("\"{0}\"", tableName); diff --git a/src/Umbraco.Core/Persistence/UnitOfWork/IDatabaseUnitOfWork.cs b/src/Umbraco.Core/Persistence/UnitOfWork/IDatabaseUnitOfWork.cs index 9bced69f52..42aeeefdd5 100644 --- a/src/Umbraco.Core/Persistence/UnitOfWork/IDatabaseUnitOfWork.cs +++ b/src/Umbraco.Core/Persistence/UnitOfWork/IDatabaseUnitOfWork.cs @@ -7,7 +7,7 @@ namespace Umbraco.Core.Persistence.UnitOfWork { UmbracoDatabase Database { get; } - void ReadLockNodes(params int[] lockIds); - void WriteLockNodes(params int[] lockIds); + void ReadLock(params int[] lockIds); + void WriteLock(params int[] lockIds); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/UnitOfWork/IUnitOfWork.cs b/src/Umbraco.Core/Persistence/UnitOfWork/IUnitOfWork.cs index 0f8d775020..e59473436e 100644 --- a/src/Umbraco.Core/Persistence/UnitOfWork/IUnitOfWork.cs +++ b/src/Umbraco.Core/Persistence/UnitOfWork/IUnitOfWork.cs @@ -63,6 +63,9 @@ namespace Umbraco.Core.Persistence.UnitOfWork /// If any operation is added to the unit of work after it has been completed, then its completion /// status is resetted. So in a way it could be possible to always complete and never flush, but flush /// is preferred when appropriate to indicate that you understand what you are doing. + /// Every units of work should be completed, unless a rollback is required. That is, even if the unit of + /// work contains only read operations, that do not need to be "commited", the unit of work should be + /// properly completed, else it may force an unexpected rollback of a higher-level transaction. /// void Complete(); diff --git a/src/Umbraco.Core/Persistence/UnitOfWork/NPocoUnitOfWork.cs b/src/Umbraco.Core/Persistence/UnitOfWork/NPocoUnitOfWork.cs index 08550c2b76..cfa530c76e 100644 --- a/src/Umbraco.Core/Persistence/UnitOfWork/NPocoUnitOfWork.cs +++ b/src/Umbraco.Core/Persistence/UnitOfWork/NPocoUnitOfWork.cs @@ -70,7 +70,7 @@ namespace Umbraco.Core.Persistence.UnitOfWork _transaction = null; } - public void ReadLockNodes(params int[] lockIds) + public void ReadLock(params int[] lockIds) { Begin(); // we need a transaction @@ -78,11 +78,15 @@ namespace Umbraco.Core.Persistence.UnitOfWork throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required."); // *not* using a unique 'WHERE IN' query here because the *order* of lockIds is important to avoid deadlocks foreach (var lockId in lockIds) - Database.ExecuteScalar("SELECT sortOrder FROM umbracoNode WHERE id=@id", + { + var i = Database.ExecuteScalar("SELECT value FROM umbracoLock WHERE id=@id", new { @id = lockId }); + if (i == null) // ensure we are actually locking! + throw new Exception($"LockObject with id={lockId} does not exist."); + } } - public void WriteLockNodes(params int[] lockIds) + public void WriteLock(params int[] lockIds) { Begin(); // we need a transaction @@ -90,8 +94,12 @@ namespace Umbraco.Core.Persistence.UnitOfWork throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required."); // *not* using a unique 'WHERE IN' query here because the *order* of lockIds is important to avoid deadlocks foreach (var lockId in lockIds) - Database.Execute("UPDATE umbracoNode SET sortOrder = (CASE WHEN (sortOrder=1) THEN -1 ELSE 1 END) WHERE id=@id", + { + var i = Database.Execute("UPDATE umbracoLock SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id", new { @id = lockId }); + if (i == 0) // ensure we are actually locking! + throw new Exception($"LockObject with id={lockId} does not exist."); + } } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Publishing/PublishStatus.cs b/src/Umbraco.Core/Publishing/PublishStatus.cs index 3436e9070e..f348367f22 100644 --- a/src/Umbraco.Core/Publishing/PublishStatus.cs +++ b/src/Umbraco.Core/Publishing/PublishStatus.cs @@ -6,30 +6,36 @@ using Umbraco.Core.Services; namespace Umbraco.Core.Publishing { /// - /// The result of publishing a content item + /// Represents the result of publishing a content item. /// - public class PublishStatus : OperationStatus + public class PublishStatus : OperationStatus { - public PublishStatus(IContent content, PublishStatusType statusType, EventMessages eventMessages) - : base(content, statusType, eventMessages) - { - } - /// - /// Creates a successful publish status + /// Creates a new instance of the class with a status type, event messages, and a content item. /// - public PublishStatus(IContent content, EventMessages eventMessages) - : this(content, PublishStatusType.Success, eventMessages) - { - } - - public IContent ContentItem - { - get { return Entity; } - } + /// The status of the operation. + /// Event messages produced by the operation. + /// The content item. + public PublishStatus(PublishStatusType statusType, EventMessages eventMessages, IContent content) + : base(statusType, eventMessages, content) + { } /// - /// Gets sets the invalid properties if the status failed due to validation. + /// Creates a new successful instance of the class with a event messages, and a content item. + /// + /// Event messages produced by the operation. + /// The content item. + public PublishStatus(IContent content, EventMessages eventMessages) + : base(PublishStatusType.Success, eventMessages, content) + { } + + /// + /// Gets the content item. + /// + public IContent ContentItem => Value; + + /// + /// Gets or sets the invalid properties, if the status failed due to validation. /// public IEnumerable InvalidProperties { get; set; } } diff --git a/src/Umbraco.Core/Publishing/PublishStatusType.cs b/src/Umbraco.Core/Publishing/PublishStatusType.cs index 0d9ffcfa02..e68b31c887 100644 --- a/src/Umbraco.Core/Publishing/PublishStatusType.cs +++ b/src/Umbraco.Core/Publishing/PublishStatusType.cs @@ -1,10 +1,11 @@ namespace Umbraco.Core.Publishing { /// - /// A status type of the result of publishing a content item + /// A value indicating the result of publishing a content item. /// - /// - /// Anything less than 10 = Success! + /// Do NOT compare against a hard-coded numeric value to check for success or failure, + /// but instead use the IsSuccess() extension method defined below - which should be the unique + /// place where the numeric test should take place. /// public enum PublishStatusType { @@ -14,40 +15,70 @@ namespace Umbraco.Core.Publishing Success = 0, /// - /// The item was already published + /// The item was already published. /// SuccessAlreadyPublished = 1, + // Values below this value indicate a success, values above it indicate a failure. + // This value is considered a failure. + //Reserved = 10, + /// - /// The content could not be published because it's ancestor path isn't published + /// The content could not be published because it's ancestor path isn't published. /// - FailedPathNotPublished = 10, + FailedPathNotPublished = 11, /// /// The content item was scheduled to be un-published and it has expired so we cannot force it to be /// published again as part of a bulk publish operation. /// - FailedHasExpired = 11, + FailedHasExpired = 12, /// /// The content item is scheduled to be released in the future and therefore we cannot force it to /// be published during a bulk publish operation. /// - FailedAwaitingRelease = 12, + FailedAwaitingRelease = 13, /// - /// The content item is in the trash, it cannot be published + /// The content item could not be published because it is in the trash. /// - FailedIsTrashed = 13, + FailedIsTrashed = 14, /// - /// The publish action has been cancelled by an event handler + /// The publish action has been cancelled by an event handler. /// - FailedCancelledByEvent = 14, + FailedCancelledByEvent = 15, /// - /// The content item contains invalid data (has not passed validation requirements) + /// The content item could not be published because it contains invalid data (has not passed validation requirements). /// - FailedContentInvalid = 15 + FailedContentInvalid = 16 + } + + /// + /// Provides extension methods for the enum. + /// + public static class PublicStatusTypeExtensions + { + /// + /// Gets a value indicating whether the status indicates a success. + /// + /// The status. + /// A value indicating whether the status indicates a success. + public static bool IsSuccess(this PublishStatusType status) + { + return (int) status < 10; + } + + /// + /// Gets a value indicating whether the status indicates a failure. + /// + /// The status. + /// A value indicating whether the status indicates a failure. + public static bool IsFailure(this PublishStatusType status) + { + return (int) status >= 10; + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Publishing/PublishingStrategy.cs b/src/Umbraco.Core/Publishing/PublishingStrategy.cs deleted file mode 100644 index c6d4f19baf..0000000000 --- a/src/Umbraco.Core/Publishing/PublishingStrategy.cs +++ /dev/null @@ -1,469 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Umbraco.Core.Events; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Core; -using Umbraco.Core.Services; - -namespace Umbraco.Core.Publishing -{ - //TODO: Do we need this anymore?? - /// - /// Currently acts as an interconnection between the new public api and the legacy api for publishing - /// - public class PublishingStrategy : BasePublishingStrategy - { - private readonly IEventMessagesFactory _eventMessagesFactory; - private readonly ILogger _logger; - - public PublishingStrategy(IEventMessagesFactory eventMessagesFactory, ILogger logger) - { - if (eventMessagesFactory == null) throw new ArgumentNullException("eventMessagesFactory"); - if (logger == null) throw new ArgumentNullException("logger"); - _eventMessagesFactory = eventMessagesFactory; - _logger = logger; - } - - /// - /// Publishes a single piece of Content - /// - /// to publish - /// Id of the User issueing the publish operation - internal Attempt PublishInternal(IContent content, int userId) - { - var evtMsgs = _eventMessagesFactory.Get(); - - if (Publishing.IsRaisedEventCancelled( - new PublishEventArgs(content, evtMsgs), this)) - { - _logger.Info( - string.Format("Content '{0}' with Id '{1}' will not be published, the event was cancelled.", content.Name, content.Id)); - return Attempt.Fail(new PublishStatus(content, PublishStatusType.FailedCancelledByEvent, evtMsgs)); - } - - //Check if the Content is Expired to verify that it can in fact be published - if (content.Status == ContentStatus.Expired) - { - _logger.Info( - string.Format("Content '{0}' with Id '{1}' has expired and could not be published.", - content.Name, content.Id)); - return Attempt.Fail(new PublishStatus(content, PublishStatusType.FailedHasExpired, evtMsgs)); - } - - //Check if the Content is Awaiting Release to verify that it can in fact be published - if (content.Status == ContentStatus.AwaitingRelease) - { - _logger.Info( - string.Format("Content '{0}' with Id '{1}' is awaiting release and could not be published.", - content.Name, content.Id)); - return Attempt.Fail(new PublishStatus(content, PublishStatusType.FailedAwaitingRelease, evtMsgs)); - } - - //Check if the Content is Trashed to verify that it can in fact be published - if (content.Status == ContentStatus.Trashed) - { - _logger.Info( - string.Format("Content '{0}' with Id '{1}' is trashed and could not be published.", - content.Name, content.Id)); - return Attempt.Fail(new PublishStatus(content, PublishStatusType.FailedIsTrashed, evtMsgs)); - } - - content.ChangePublishedState(PublishedState.Published); - - _logger.Info( - string.Format("Content '{0}' with Id '{1}' has been published.", - content.Name, content.Id)); - - return Attempt.Succeed(new PublishStatus(content, evtMsgs)); - } - - /// - /// Publishes a single piece of Content - /// - /// to publish - /// Id of the User issueing the publish operation - /// True if the publish operation was successfull and not cancelled, otherwise false - public override bool Publish(IContent content, int userId) - { - return PublishInternal(content, userId).Success; - } - - /// - /// Publishes a list of content items - /// - /// - /// - /// - /// By default this is set to true which means that it will publish any content item in the list that is completely unpublished and - /// not visible on the front-end. If set to false, this will only publish content that is live on the front-end but has new versions - /// that have yet to be published. - /// - /// - /// - /// - /// This method becomes complex once we start to be able to cancel events or stop publishing a content item in any way because if a - /// content item is not published then it's children shouldn't be published either. This rule will apply for the following conditions: - /// * If a document fails to be published, do not proceed to publish it's children if: - /// ** The document does not have a publish version - /// ** The document does have a published version but the includeUnpublishedDocuments = false - /// - /// In order to do this, we will order the content by level and begin by publishing each item at that level, then proceed to the next - /// level and so on. If we detect that the above rule applies when the document publishing is cancelled we'll add it to the list of - /// parentsIdsCancelled so that it's children don't get published. - /// - /// Its important to note that all 'root' documents included in the list *will* be published regardless of the rules mentioned - /// above (unless it is invalid)!! By 'root' documents we are referring to documents in the list with the minimum value for their 'level'. - /// In most cases the 'root' documents will only be one document since under normal circumstance we only publish one document and - /// its children. The reason we have to do this is because if a user is publishing a document and it's children, it is implied that - /// the user definitely wants to publish it even if it has never been published before. - /// - /// - internal IEnumerable> PublishWithChildrenInternal( - IEnumerable content, int userId, bool includeUnpublishedDocuments = true) - { - var statuses = new List>(); - - //a list of all document ids that had their publishing cancelled during these iterations. - //this helps us apply the rule listed in the notes above by checking if a document's parent id - //matches one in this list. - var parentsIdsCancelled = new List(); - - //group by levels and iterate over the sorted ascending level. - //TODO: This will cause all queries to execute, they will not be lazy but I'm not really sure being lazy actually made - // much difference because we iterate over them all anyways?? Morten? - // Because we're grouping I think this will execute all the queries anyways so need to fetch it all first. - var fetchedContent = content.ToArray(); - - var evtMsgs = _eventMessagesFactory.Get(); - - //We're going to populate the statuses with all content that is already published because below we are only going to iterate over - // content that is not published. We'll set the status to "AlreadyPublished" - statuses.AddRange(fetchedContent.Where(x => x.Published) - .Select(x => Attempt.Succeed(new PublishStatus(x, PublishStatusType.SuccessAlreadyPublished, evtMsgs)))); - - int? firstLevel = null; - - //group by level and iterate over each level (sorted ascending) - var levelGroups = fetchedContent.GroupBy(x => x.Level); - foreach (var level in levelGroups.OrderBy(x => x.Key)) - { - //set the first level flag, used to ensure that all documents at the first level will - //be published regardless of the rules mentioned in the remarks. - if (!firstLevel.HasValue) - { - firstLevel = level.Key; - } - - /* Only update content thats not already been published - we want to loop through - * all unpublished content to write skipped content (expired and awaiting release) to log. - */ - foreach (var item in level.Where(x => x.Published == false)) - { - //Check if this item should be excluded because it's parent's publishing has failed/cancelled - if (parentsIdsCancelled.Contains(item.ParentId)) - { - _logger.Info( - string.Format("Content '{0}' with Id '{1}' will not be published because it's parent's publishing action failed or was cancelled.", item.Name, item.Id)); - //if this cannot be published, ensure that it's children can definitely not either! - parentsIdsCancelled.Add(item.Id); - continue; - } - - //Check if this item has never been published (and that it is not at the root level) - if (item.Level != firstLevel && !includeUnpublishedDocuments && !item.HasPublishedVersion()) - { - //this item does not have a published version and the flag is set to not include them - parentsIdsCancelled.Add(item.Id); - continue; - } - - //Fire Publishing event - if (Publishing.IsRaisedEventCancelled( - new PublishEventArgs(item, evtMsgs), this)) - { - //the publishing has been cancelled. - _logger.Info( - string.Format("Content '{0}' with Id '{1}' will not be published, the event was cancelled.", item.Name, item.Id)); - statuses.Add(Attempt.Fail(new PublishStatus(item, PublishStatusType.FailedCancelledByEvent, evtMsgs))); - - //Does this document apply to our rule to cancel it's children being published? - CheckCancellingOfChildPublishing(item, parentsIdsCancelled, includeUnpublishedDocuments); - - continue; - } - - //Check if the content is valid if the flag is set to check - if (item.IsValid() == false) - { - _logger.Info( - string.Format("Content '{0}' with Id '{1}' will not be published because some of it's content is not passing validation rules.", - item.Name, item.Id)); - statuses.Add(Attempt.Fail(new PublishStatus(item, PublishStatusType.FailedContentInvalid, evtMsgs))); - - //Does this document apply to our rule to cancel it's children being published? - CheckCancellingOfChildPublishing(item, parentsIdsCancelled, includeUnpublishedDocuments); - - continue; - } - - //Check if the Content is Expired to verify that it can in fact be published - if (item.Status == ContentStatus.Expired) - { - _logger.Info( - string.Format("Content '{0}' with Id '{1}' has expired and could not be published.", - item.Name, item.Id)); - statuses.Add(Attempt.Fail(new PublishStatus(item, PublishStatusType.FailedHasExpired, evtMsgs))); - - //Does this document apply to our rule to cancel it's children being published? - CheckCancellingOfChildPublishing(item, parentsIdsCancelled, includeUnpublishedDocuments); - - continue; - } - - //Check if the Content is Awaiting Release to verify that it can in fact be published - if (item.Status == ContentStatus.AwaitingRelease) - { - _logger.Info( - string.Format("Content '{0}' with Id '{1}' is awaiting release and could not be published.", - item.Name, item.Id)); - statuses.Add(Attempt.Fail(new PublishStatus(item, PublishStatusType.FailedAwaitingRelease, evtMsgs))); - - //Does this document apply to our rule to cancel it's children being published? - CheckCancellingOfChildPublishing(item, parentsIdsCancelled, includeUnpublishedDocuments); - - continue; - } - - //Check if the Content is Trashed to verify that it can in fact be published - if (item.Status == ContentStatus.Trashed) - { - _logger.Info( - string.Format("Content '{0}' with Id '{1}' is trashed and could not be published.", - item.Name, item.Id)); - statuses.Add(Attempt.Fail(new PublishStatus(item, PublishStatusType.FailedIsTrashed, evtMsgs))); - - //Does this document apply to our rule to cancel it's children being published? - CheckCancellingOfChildPublishing(item, parentsIdsCancelled, includeUnpublishedDocuments); - - continue; - } - - item.ChangePublishedState(PublishedState.Published); - - _logger.Info( - string.Format("Content '{0}' with Id '{1}' has been published.", - item.Name, item.Id)); - - statuses.Add(Attempt.Succeed(new PublishStatus(item, evtMsgs))); - } - - } - - return statuses; - } - - /// - /// Based on the information provider we'll check if we should cancel the publishing of this document's children - /// - /// - /// - /// - /// - /// See remarks on method: PublishWithChildrenInternal - /// - private void CheckCancellingOfChildPublishing(IContent content, List parentsIdsCancelled, bool includeUnpublishedDocuments) - { - //Does this document apply to our rule to cancel it's children being published? - //TODO: We're going back to the service layer here... not sure how to avoid this? And this will add extra overhead to - // any document that fails to publish... - var hasPublishedVersion = ApplicationContext.Current.Services.ContentService.HasPublishedVersion(content.Id); - - if (hasPublishedVersion && !includeUnpublishedDocuments) - { - //it has a published version but our flag tells us to not include un-published documents and therefore we should - // not be forcing decendant/child documents to be published if their parent fails. - parentsIdsCancelled.Add(content.Id); - } - else if (!hasPublishedVersion) - { - //it doesn't have a published version so we certainly cannot publish it's children. - parentsIdsCancelled.Add(content.Id); - } - } - - /// - /// Publishes a list of Content - /// - /// An enumerable list of - /// Id of the User issueing the publish operation - /// True if the publish operation was successfull and not cancelled, otherwise false - public override bool PublishWithChildren(IEnumerable content, int userId) - { - var result = PublishWithChildrenInternal(content, userId); - - //NOTE: This previously always returned true so I've left it that way. It returned true because (from Morten)... - // ... if one item couldn't be published it wouldn't be correct to return false. - // in retrospect it should have returned a list of with Ids and Publish Status - // come to think of it ... the cache would still be updated for a failed item or at least tried updated. - // It would call the Published event for the entire list, but if the Published property isn't set to True it - // wouldn't actually update the cache for that item. But not really ideal nevertheless... - return true; - } - - /// - /// Unpublishes a single piece of Content - /// - /// to unpublish - /// Id of the User issueing the unpublish operation - /// True if the unpublish operation was successfull and not cancelled, otherwise false - public override bool UnPublish(IContent content, int userId) - { - return UnPublishInternal(content, userId).Success; - } - - /// - /// Unpublishes a list of Content - /// - /// An enumerable list of - /// Id of the User issueing the unpublish operation - /// A list of publish statuses - private IEnumerable> UnPublishInternal(IEnumerable content, int userId) - { - return content.Select(x => UnPublishInternal(x, userId)); - } - - private Attempt UnPublishInternal(IContent content, int userId) - { - // content should (is assumed to ) be the newest version, which may not be published - // don't know how to test this, so it's not verified - // NOTE - // if published != newest, then the published flags need to be reseted by whoever is calling that method - // at the moment it's done by the content service - - var evtMsgs = _eventMessagesFactory.Get(); - - //Fire UnPublishing event - if (UnPublishing.IsRaisedEventCancelled( - new PublishEventArgs(content, evtMsgs), this)) - { - _logger.Info( - string.Format("Content '{0}' with Id '{1}' will not be unpublished, the event was cancelled.", content.Name, content.Id)); - return Attempt.Fail(new PublishStatus(content, PublishStatusType.FailedCancelledByEvent, evtMsgs)); - } - - //If Content 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( - string.Format("Content '{0}' with Id '{1}' had its release date removed, because it was unpublished.", - content.Name, content.Id)); - } - - // if newest is published, unpublish - if (content.Published) - content.ChangePublishedState(PublishedState.Unpublished); - - _logger.Info( - string.Format("Content '{0}' with Id '{1}' has been unpublished.", - content.Name, content.Id)); - - return Attempt.Succeed(new PublishStatus(content, evtMsgs)); - } - - /// - /// Unpublishes a list of Content - /// - /// An enumerable list of - /// Id of the User issueing the unpublish operation - /// True if the unpublish operation was successfull and not cancelled, otherwise false - public override bool UnPublish(IEnumerable content, int userId) - { - var result = UnPublishInternal(content, userId); - - //NOTE: This previously always returned true so I've left it that way. It returned true because (from Morten)... - // ... if one item couldn't be published it wouldn't be correct to return false. - // in retrospect it should have returned a list of with Ids and Publish Status - // come to think of it ... the cache would still be updated for a failed item or at least tried updated. - // It would call the Published event for the entire list, but if the Published property isn't set to True it - // wouldn't actually update the cache for that item. But not really ideal nevertheless... - return true; - } - - /// - /// Call to fire event that updating the published content has finalized. - /// - /// - /// This seperation of the OnPublished event is done to ensure that the Content - /// has been properly updated (committed unit of work) and xml saved in the db. - /// - /// thats being published - public override void PublishingFinalized(IContent content) - { - var evtMsgs = _eventMessagesFactory.Get(); - Published.RaiseEvent( - new PublishEventArgs(content, false, false, evtMsgs), this); - } - - /// - /// Call to fire event that updating the published content has finalized. - /// - /// An enumerable list of thats being published - /// Boolean indicating whether its all content that is republished - public override void PublishingFinalized(IEnumerable content, bool isAllRepublished) - { - var evtMsgs = _eventMessagesFactory.Get(); - Published.RaiseEvent( - new PublishEventArgs(content, false, isAllRepublished, evtMsgs), this); - - } - - /// - /// Call to fire event that updating the unpublished content has finalized. - /// - /// thats being unpublished - public override void UnPublishingFinalized(IContent content) - { - var evtMsgs = _eventMessagesFactory.Get(); - UnPublished.RaiseEvent( - new PublishEventArgs(content, false, false, evtMsgs), this); - } - - /// - /// Call to fire event that updating the unpublished content has finalized. - /// - /// An enumerable list of thats being unpublished - public override void UnPublishingFinalized(IEnumerable content) - { - var evtMsgs = _eventMessagesFactory.Get(); - UnPublished.RaiseEvent( - new PublishEventArgs(content, false, false, evtMsgs), this); - } - - /// - /// 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; - - - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Publishing/UnPublishStatus.cs b/src/Umbraco.Core/Publishing/UnPublishStatus.cs index 80ab786e1a..91f242cc6f 100644 --- a/src/Umbraco.Core/Publishing/UnPublishStatus.cs +++ b/src/Umbraco.Core/Publishing/UnPublishStatus.cs @@ -5,26 +5,32 @@ using Umbraco.Core.Services; namespace Umbraco.Core.Publishing { /// - /// The result of unpublishing a content item + /// Represents the result of unpublishing a content item. /// - public class UnPublishStatus : OperationStatus + public class UnPublishStatus : OperationStatus { - public UnPublishStatus(IContent content, UnPublishedStatusType statusType, EventMessages eventMessages) - : base(content, statusType, eventMessages) - { - } + /// + /// Creates a new instance of the class with a status type, event messages, and a content item. + /// + /// The status of the operation. + /// Event messages produced by the operation. + /// The content item. + public UnPublishStatus(UnPublishedStatusType statusType, EventMessages eventMessages, IContent content) + : base(statusType, eventMessages, content) + { } /// - /// Creates a successful unpublish status + /// Creates a new successful instance of the class with a event messages, and a content item. /// + /// Event messages produced by the operation. + /// The content item. public UnPublishStatus(IContent content, EventMessages eventMessages) - : this(content, UnPublishedStatusType.Success, eventMessages) - { - } + : base(UnPublishedStatusType.Success, eventMessages, content) + { } - public IContent ContentItem - { - get { return Entity; } - } + /// + /// Gets the content item. + /// + public IContent ContentItem => Value; } } \ No newline at end of file diff --git a/src/Umbraco.Core/ServiceContextExtensions.cs b/src/Umbraco.Core/ServiceContextExtensions.cs new file mode 100644 index 0000000000..b1ca021173 --- /dev/null +++ b/src/Umbraco.Core/ServiceContextExtensions.cs @@ -0,0 +1,21 @@ +using System; +using Umbraco.Core.Models; +using Umbraco.Core.Services; + +namespace Umbraco.Core +{ + public static class ServiceContextExtensions + { + public static IContentTypeServiceBase GetContentTypeService(this ServiceContext services) + where T : IContentTypeComposition + { + if (typeof(T).Implements()) + return services.ContentTypeService as IContentTypeServiceBase; + if (typeof(T).Implements()) + return services.MediaTypeService as IContentTypeServiceBase; + if (typeof(T).Implements()) + return services.MemberTypeService as IContentTypeServiceBase; + throw new ArgumentException("Type " + typeof(T).FullName + " does not have a service."); + } + } +} diff --git a/src/Umbraco.Core/Services/AuditService.cs b/src/Umbraco.Core/Services/AuditService.cs index 24a950a0b4..614d44ac98 100644 --- a/src/Umbraco.Core/Services/AuditService.cs +++ b/src/Umbraco.Core/Services/AuditService.cs @@ -33,6 +33,7 @@ namespace Umbraco.Core.Services { var repo = uow.CreateRepository(); var result = repo.GetByQuery(repo.Query.Where(x => x.Id == objectId)); + uow.Complete(); return result; } } @@ -45,6 +46,7 @@ namespace Umbraco.Core.Services var result = sinceDate.HasValue == false ? repo.GetByQuery(repo.Query.Where(x => x.UserId == userId && x.AuditType == type)) : repo.GetByQuery(repo.Query.Where(x => x.UserId == userId && x.AuditType == type && x.CreateDate >= sinceDate.Value)); + uow.Complete(); return result; } } @@ -57,6 +59,7 @@ namespace Umbraco.Core.Services var result = sinceDate.HasValue == false ? repo.GetByQuery(repo.Query.Where(x => x.AuditType == type)) : repo.GetByQuery(repo.Query.Where(x => x.AuditType == type && x.CreateDate >= sinceDate.Value)); + uow.Complete(); return result; } } @@ -67,6 +70,7 @@ namespace Umbraco.Core.Services { var repo = uow.CreateRepository(); repo.CleanLogs(maximumAgeOfLogsInMinutes); + uow.Complete(); } } } diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index eb50815957..ce9c9ad323 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -1,22 +1,16 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.Globalization; using System.Linq; -using System.Threading; using System.Xml.Linq; -using Umbraco.Core.Configuration; using Umbraco.Core.Events; +using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; -using Umbraco.Core.Models.Rdbms; -using Umbraco.Core.Persistence; - using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.Repositories; -using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Persistence.UnitOfWork; using Umbraco.Core.Publishing; using Umbraco.Core.Strings; @@ -28,42 +22,58 @@ namespace Umbraco.Core.Services /// public class ContentService : RepositoryService, IContentService, IContentServiceOperations { - private readonly IPublishingStrategy _publishingStrategy; private readonly EntityXmlSerializer _entitySerializer = new EntityXmlSerializer(); private readonly IDataTypeService _dataTypeService; private readonly IUserService _userService; private readonly IEnumerable _urlSegmentProviders; + private IContentTypeService _contentTypeService; - //Support recursive locks because some of the methods that require locking call other methods that require locking. - //for example, the Move method needs to be locked but this calls the Save method which also needs to be locked. - private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); + #region Constructors public ContentService( IDatabaseUnitOfWorkProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory, - IPublishingStrategy publishingStrategy, IDataTypeService dataTypeService, IUserService userService, IEnumerable urlSegmentProviders) : base(provider, logger, eventMessagesFactory) { - if (publishingStrategy == null) throw new ArgumentNullException("publishingStrategy"); - if (dataTypeService == null) throw new ArgumentNullException("dataTypeService"); - if (userService == null) throw new ArgumentNullException("userService"); - if (urlSegmentProviders == null) throw new ArgumentNullException("urlSegmentProviders"); - _publishingStrategy = publishingStrategy; + if (dataTypeService == null) throw new ArgumentNullException(nameof(dataTypeService)); + if (userService == null) throw new ArgumentNullException(nameof(userService)); + if (urlSegmentProviders == null) throw new ArgumentNullException(nameof(urlSegmentProviders)); _dataTypeService = dataTypeService; _userService = userService; _urlSegmentProviders = urlSegmentProviders; } + // don't change or remove this, will need it later + private IContentTypeService ContentTypeService => _contentTypeService; + //// handle circular dependencies + //internal IContentTypeService ContentTypeService + //{ + // get + // { + // if (_contentTypeService == null) + // throw new InvalidOperationException("ContentService.ContentTypeService has not been initialized."); + // return _contentTypeService; + // } + // set { _contentTypeService = value; } + //} + + #endregion + + #region Count + public int CountPublished(string contentTypeAlias = null) { using (var uow = UowProvider.CreateUnitOfWork()) { - var repository = uow.CreateRepository(); - return repository.CountPublished(); + uow.ReadLock(Constants.Locks.ContentTree); + var repo = uow.CreateRepository(); + var count = repo.CountPublished(); + uow.Complete(); + return count; } } @@ -71,8 +81,11 @@ namespace Umbraco.Core.Services { using (var uow = UowProvider.CreateUnitOfWork()) { - var repository = uow.CreateRepository(); - return repository.Count(contentTypeAlias); + uow.ReadLock(Constants.Locks.ContentTree); + var repo = uow.CreateRepository(); + var count = repo.Count(contentTypeAlias); + uow.Complete(); + return count; } } @@ -80,8 +93,11 @@ namespace Umbraco.Core.Services { using (var uow = UowProvider.CreateUnitOfWork()) { - var repository = uow.CreateRepository(); - return repository.CountChildren(parentId, contentTypeAlias); + uow.ReadLock(Constants.Locks.ContentTree); + var repo = uow.CreateRepository(); + var count = repo.CountChildren(parentId, contentTypeAlias); + uow.Complete(); + return count; } } @@ -89,11 +105,18 @@ namespace Umbraco.Core.Services { using (var uow = UowProvider.CreateUnitOfWork()) { - var repository = uow.CreateRepository(); - return repository.CountDescendants(parentId, contentTypeAlias); + uow.ReadLock(Constants.Locks.ContentTree); + var repo = uow.CreateRepository(); + var count = repo.CountDescendants(parentId, contentTypeAlias); + uow.Complete(); + return count; } } + #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. @@ -103,8 +126,10 @@ namespace Umbraco.Core.Services { using (var uow = UowProvider.CreateUnitOfWork()) { - var repository = uow.CreateRepository(); - repository.ReplaceContentPermissions(permissionSet); + uow.WriteLock(Constants.Locks.ContentTree); + var repo = uow.CreateRepository(); + repo.ReplaceContentPermissions(permissionSet); + uow.Complete(); } } @@ -118,8 +143,10 @@ namespace Umbraco.Core.Services { using (var uow = UowProvider.CreateUnitOfWork()) { - var repository = uow.CreateRepository(); - repository.AssignEntityPermission(entity, permission, userIds); + uow.WriteLock(Constants.Locks.ContentTree); + var repo = uow.CreateRepository(); + repo.AssignEntityPermission(entity, permission, userIds); + uow.Complete(); } } @@ -132,194 +159,208 @@ namespace Umbraco.Core.Services { using (var uow = UowProvider.CreateUnitOfWork()) { - var repository = uow.CreateRepository(); - return repository.GetPermissionsForEntity(content.Id); + uow.ReadLock(Constants.Locks.ContentTree); + var repo = uow.CreateRepository(); + var perms = repo.GetPermissionsForEntity(content.Id); + uow.Complete(); + return perms; } } + #endregion + + #region Create + /// - /// Creates an object using the alias of the - /// that this Content should based on. + /// Creates an object of a specified content type. /// - /// - /// 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. + /// 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. /// - /// Name of the Content object - /// Id of Parent for the new Content - /// Alias of the - /// Optional id of the user creating the content - /// + /// 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 CreateContent(string name, int parentId, string contentTypeAlias, int userId = 0) { - var contentType = FindContentTypeByAlias(contentTypeAlias); + 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); - var parent = GetById(content.ParentId); - content.Path = string.Concat(parent.IfNotNull(x => x.Path, content.ParentId.ToString()), ",", content.Id); - - - if (Creating.IsRaisedEventCancelled(new NewEventArgs(content, contentTypeAlias, parentId), this)) - { - content.WasCancelled = true; - return content; - } - - content.CreatorId = userId; - content.WriterId = userId; - - Created.RaiseEvent(new NewEventArgs(content, false, contentTypeAlias, parentId), this); - - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repo = uow.CreateRepository(); - repo.AddOrUpdate(new AuditItem(content.Id, $"Content '{name}' was created", AuditType.New, content.CreatorId)); - uow.Complete(); - } + CreateContent(null, content, parent, userId, false); return content; } /// - /// Creates an object using the alias of the - /// that this Content should based on. + /// Creates an object of a specified content type, at root. /// - /// - /// 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. + /// 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. /// - /// Name of the Content object - /// Parent object for the new Content - /// Alias of the - /// Optional id of the user creating the content - /// + /// 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); + CreateContent(null, content, null, userId, false); + + 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 CreateContent(string name, IContent parent, string contentTypeAlias, int userId = 0) { - if (parent == null) throw new ArgumentNullException("parent"); + if (parent == null) throw new ArgumentNullException(nameof(parent)); - var contentType = FindContentTypeByAlias(contentTypeAlias); - var content = new Content(name, parent, contentType); - content.Path = string.Concat(parent.Path, ",", content.Id); - - if (Creating.IsRaisedEventCancelled(new NewEventArgs(content, contentTypeAlias, parent), this)) + using (var uow = UowProvider.CreateUnitOfWork()) { - content.WasCancelled = true; + // 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(uow, content, parent, userId, false); + + uow.Complete(); return content; } - - content.CreatorId = userId; - content.WriterId = userId; - - Created.RaiseEvent(new NewEventArgs(content, false, contentTypeAlias, parent), this); - - Audit(AuditType.New, string.Format("Content '{0}' was created", name), content.CreatorId, content.Id); - - return content; } /// - /// Creates and saves an object using the alias of the - /// that this Content should based on. + /// Creates an object of a specified content type. /// - /// - /// This method returns an object that has been persisted to the database - /// and therefor has an identity. - /// - /// Name of the Content object - /// Id of Parent for the new Content - /// Alias of the - /// Optional id of the user creating the content - /// + /// 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 CreateContentWithIdentity(string name, int parentId, string contentTypeAlias, int userId = 0) { - var contentType = FindContentTypeByAlias(contentTypeAlias); - var content = new Content(name, parentId, contentType); - - //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. - if (Creating.IsRaisedEventCancelled(new NewEventArgs(content, contentTypeAlias, parentId), this)) - { - content.WasCancelled = true; - return content; - } - - if (Saving.IsRaisedEventCancelled(new SaveEventArgs(content), this)) - { - content.WasCancelled = true; - return content; - } - - content.CreatorId = userId; - content.WriterId = userId; - using (var uow = UowProvider.CreateUnitOfWork()) { - var repository = uow.CreateRepository(); - repository.AddOrUpdate(content); - repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); + // locking the content tree secures content types too + uow.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(uow, content, parent, userId, true); + uow.Complete(); + return content; } - - Saved.RaiseEvent(new SaveEventArgs(content, false), this); - Created.RaiseEvent(new NewEventArgs(content, false, contentTypeAlias, parentId), this); - Audit(AuditType.New, $"Content '{name}' was created with Id {content.Id}", content.CreatorId, content.Id); - - return content; } /// - /// Creates and saves an object using the alias of the - /// that this Content should based on. + /// Creates an object of a specified content type, under a parent. /// - /// - /// This method returns an object that has been persisted to the database - /// and therefor has an identity. - /// - /// Name of the Content object - /// Parent object for the new Content - /// Alias of the - /// Optional id of the user creating the content - /// + /// 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 CreateContentWithIdentity(string name, IContent parent, string contentTypeAlias, int userId = 0) { - if (parent == null) throw new ArgumentNullException("parent"); + if (parent == null) throw new ArgumentNullException(nameof(parent)); - var contentType = FindContentTypeByAlias(contentTypeAlias); - var content = new Content(name, parent, contentType); - - //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. - if (Creating.IsRaisedEventCancelled(new NewEventArgs(content, contentTypeAlias, parent), this)) + using (var uow = UowProvider.CreateUnitOfWork()) { - content.WasCancelled = true; + // locking the content tree secures content types too + uow.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(uow, content, parent, userId, true); + + uow.Complete(); return content; } + } - if (Saving.IsRaisedEventCancelled(new SaveEventArgs(content), this)) + private void CreateContent(IDatabaseUnitOfWork uow, 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 (Creating.IsRaisedEventCancelled(newArgs, this)) { content.WasCancelled = true; - return content; + return; } content.CreatorId = userId; content.WriterId = userId; - using (var uow = UowProvider.CreateUnitOfWork()) + if (withIdentity) { - var repository = uow.CreateRepository(); - repository.AddOrUpdate(content); - repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); - uow.Complete(); + if (Saving.IsRaisedEventCancelled(new SaveEventArgs(content), this)) + { + content.WasCancelled = true; + return; + } + + var repo = uow.CreateRepository(); + repo.AddOrUpdate(content); + repo.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); + + Saved.RaiseEvent(new SaveEventArgs(content, false), this); } - Saved.RaiseEvent(new SaveEventArgs(content, false), this); - Created.RaiseEvent(new NewEventArgs(content, false, contentTypeAlias, parent), this); - Audit(AuditType.New, $"Content '{name}' was created with Id {content.Id}", content.CreatorId, content.Id); + Created.RaiseEvent(new NewEventArgs(content, false, content.ContentType.Alias, parent), this); - return content; + var msg = withIdentity + ? "Content '{0}' was created with Id {1}" + : "Content '{0}' was created"; + Audit(AuditType.New, string.Format(msg, content.Name, content.Id), content.CreatorId, content.Id); } + #endregion + + #region Get, Has, Is + /// /// Gets an object by Id /// @@ -329,8 +370,11 @@ namespace Umbraco.Core.Services { using (var uow = UowProvider.CreateUnitOfWork()) { + uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); - return repository.Get(id); + var content = repository.Get(id); + uow.Complete(); + return content; } } @@ -341,12 +385,16 @@ namespace Umbraco.Core.Services /// public IEnumerable GetByIds(IEnumerable ids) { - if (ids.Any() == false) return Enumerable.Empty(); + var idsA = ids.ToArray(); + if (idsA.Length == 0) return Enumerable.Empty(); using (var uow = UowProvider.CreateUnitOfWork()) { + uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); - return repository.GetAll(ids.ToArray()); + var content = repository.GetAll(idsA); + uow.Complete(); + return content; } } @@ -359,10 +407,13 @@ namespace Umbraco.Core.Services { using (var uow = UowProvider.CreateUnitOfWork()) { + uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var query = repository.Query.Where(x => x.Key == key); var contents = repository.GetByQuery(query); - return contents.SingleOrDefault(); + var content = contents.SingleOrDefault(); + uow.Complete(); + return content; } } @@ -375,11 +426,12 @@ namespace Umbraco.Core.Services { using (var uow = UowProvider.CreateUnitOfWork()) { + uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var query = repository.Query.Where(x => x.ContentTypeId == id); - var contents = repository.GetByQuery(query); - - return contents; + var content = repository.GetByQuery(query); + uow.Complete(); + return content; } } @@ -387,11 +439,12 @@ namespace Umbraco.Core.Services { using (var uow = UowProvider.CreateUnitOfWork()) { + uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var query = repository.Query.Where(x => x.ContentTypeId == id); - var contents = repository.GetByPublishedVersion(query); - - return contents; + var content = repository.GetByPublishedVersion(query); + uow.Complete(); + return content; } } @@ -400,15 +453,17 @@ namespace Umbraco.Core.Services /// /// 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 uow = UowProvider.CreateUnitOfWork()) { + uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); - var query = repository.Query.Where(x => x.Level == level && !x.Path.StartsWith(Constants.System.RecycleBinContent.ToInvariantString())); - var contents = repository.GetByQuery(query); - - return contents; + var query = repository.Query.Where(x => x.Level == level && x.Trashed == false); + var content = repository.GetByQuery(query); + uow.Complete(); + return content; } } @@ -421,12 +476,14 @@ namespace Umbraco.Core.Services { using (var uow = UowProvider.CreateUnitOfWork()) { + uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); - return repository.GetByVersion(versionId); + var content = repository.GetByVersion(versionId); + uow.Complete(); + return content; } } - /// /// Gets a collection of an objects versions by Id /// @@ -436,9 +493,11 @@ namespace Umbraco.Core.Services { using (var uow = UowProvider.CreateUnitOfWork()) { + uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); - var versions = repository.GetAllVersions(id); - return versions; + var content = repository.GetAllVersions(id); + uow.Complete(); + return content; } } @@ -449,6 +508,7 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects public IEnumerable GetAncestors(int id) { + // intentionnaly not locking var content = GetById(id); return GetAncestors(content); } @@ -463,14 +523,19 @@ namespace Umbraco.Core.Services //null check otherwise we get exceptions if (content.Path.IsNullOrWhiteSpace()) return Enumerable.Empty(); - var ids = content.Path.Split(',').Where(x => x != Constants.System.Root.ToInvariantString() && x != content.Id.ToString(CultureInfo.InvariantCulture)).Select(int.Parse).ToArray(); + 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 uow = UowProvider.CreateUnitOfWork()) { + uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); - return repository.GetAll(ids); + var ancestors = repository.GetAll(ids); + uow.Complete(); + return ancestors; } } @@ -483,11 +548,30 @@ namespace Umbraco.Core.Services { using (var uow = UowProvider.CreateUnitOfWork()) { + uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var query = repository.Query.Where(x => x.ParentId == id); - var contents = repository.GetByQuery(query).OrderBy(x => x.SortOrder); + var children = repository.GetByQuery(query).OrderBy(x => x.SortOrder); + uow.Complete(); + return children; + } + } - return contents; + /// + /// 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 uow = UowProvider.CreateUnitOfWork()) + { + uow.ReadLock(Constants.Locks.ContentTree); + var repository = uow.CreateRepository(); + var query = repository.Query.Where(x => x.ParentId == id && x.Published); + var children = repository.GetByQuery(query).OrderBy(x => x.SortOrder); + uow.Complete(); + return children; } } @@ -525,19 +609,19 @@ namespace Umbraco.Core.Services { Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); Mandate.ParameterCondition(pageSize > 0, "pageSize"); + using (var uow = UowProvider.CreateUnitOfWork()) { + uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var query = repository.Query; //if the id is System Root, then just get all if (id != Constants.System.Root) - { query.Where(x => x.ParentId == id); - } - var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); - - return contents; + var children = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); + uow.Complete(); + return children; } } @@ -571,20 +655,20 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy, Direction orderDirection, bool orderBySystemField, string filter) { - Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); - Mandate.ParameterCondition(pageSize > 0, "pageSize"); + Mandate.ParameterCondition(pageIndex >= 0, nameof(pageIndex)); + Mandate.ParameterCondition(pageSize > 0, nameof(pageSize)); + using (var uow = UowProvider.CreateUnitOfWork()) { + uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var query = repository.Query; //if the id is System Root, then just get all if (id != Constants.System.Root) - { - query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); - } + query.Where(x => x.Path.SqlContains($",{id},", TextColumnType.NVarchar)); var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); - + uow.Complete(); return contents; } } @@ -599,11 +683,12 @@ namespace Umbraco.Core.Services { using (var uow = UowProvider.CreateUnitOfWork()) { + uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var query = repository.Query.Where(x => x.ParentId == parentId && x.Name.Contains(name)); - var contents = repository.GetByQuery(query); - - return contents; + var children = repository.GetByQuery(query); + uow.Complete(); + return children; } } @@ -614,12 +699,22 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects public IEnumerable GetDescendants(int id) { - var content = GetById(id); - if (content == null) + using (var uow = UowProvider.CreateUnitOfWork()) { - return Enumerable.Empty(); + uow.ReadLock(Constants.Locks.ContentTree); + var repository = uow.CreateRepository(); + var content = GetById(id); + if (content == null) + { + uow.Complete(); // else causes rollback + return Enumerable.Empty(); + } + var pathMatch = content.Path + ","; + var query = repository.Query.Where(x => x.Id != content.Id && x.Path.StartsWith(pathMatch)); + var descendants = repository.GetByQuery(query); + uow.Complete(); + return descendants; } - return GetDescendants(content); } /// @@ -631,12 +726,13 @@ namespace Umbraco.Core.Services { using (var uow = UowProvider.CreateUnitOfWork()) { + uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var pathMatch = content.Path + ","; - var query = repository.Query.Where(x => x.Path.StartsWith(pathMatch) && x.Id != content.Id); - var contents = repository.GetByQuery(query); - - return contents; + var query = repository.Query.Where(x => x.Id != content.Id && x.Path.StartsWith(pathMatch)); + var descendants = repository.GetByQuery(query); + uow.Complete(); + return descendants; } } @@ -647,6 +743,7 @@ namespace Umbraco.Core.Services /// Parent object public IContent GetParent(int id) { + // intentionnaly not locking var content = GetById(id); return GetParent(content); } @@ -672,7 +769,7 @@ namespace Umbraco.Core.Services public IContent GetPublishedVersion(int id) { var version = GetVersions(id); - return version.FirstOrDefault(x => x.Published == true); + return version.FirstOrDefault(x => x.Published); } /// @@ -696,11 +793,12 @@ namespace Umbraco.Core.Services { using (var uow = UowProvider.CreateUnitOfWork()) { + uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var query = repository.Query.Where(x => x.ParentId == Constants.System.Root); - var contents = repository.GetByQuery(query); - - return contents; + var content = repository.GetByQuery(query); + uow.Complete(); + return content; } } @@ -712,9 +810,12 @@ namespace Umbraco.Core.Services { using (var uow = UowProvider.CreateUnitOfWork()) { + uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var query = repository.Query.Where(x => x.Trashed == false); - return repository.GetByPublishedVersion(query); + var content = repository.GetByPublishedVersion(query); + uow.Complete(); + return content; } } @@ -726,11 +827,12 @@ namespace Umbraco.Core.Services { using (var uow = UowProvider.CreateUnitOfWork()) { + uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); - var query = repository.Query.Where(x => x.Published == true && x.ExpireDate <= DateTime.Now); - var contents = repository.GetByQuery(query); - - return contents; + var query = repository.Query.Where(x => x.Published && x.ExpireDate <= DateTime.Now); + var content = repository.GetByQuery(query); + uow.Complete(); + return content; } } @@ -742,11 +844,12 @@ namespace Umbraco.Core.Services { using (var uow = UowProvider.CreateUnitOfWork()) { + uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var query = repository.Query.Where(x => x.Published == false && x.ReleaseDate <= DateTime.Now); - var contents = repository.GetByQuery(query); - - return contents; + var content = repository.GetByQuery(query); + uow.Complete(); + return content; } } @@ -758,16 +861,15 @@ namespace Umbraco.Core.Services { using (var uow = UowProvider.CreateUnitOfWork()) { + uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var query = repository.Query.Where(x => x.Path.Contains(Constants.System.RecycleBinContent.ToInvariantString())); - var contents = repository.GetByQuery(query); - - return contents; + var content = repository.GetByQuery(query); + uow.Complete(); + return content; } } - - /// /// Checks whether an item has any children /// @@ -778,17 +880,6 @@ namespace Umbraco.Core.Services return CountChildren(id) > 0; } - internal int CountChildren(int id) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - var query = repository.Query.Where(x => x.ParentId == id); - var count = repository.Count(query); - return count; - } - } - /// /// Checks whether an item has any published versions /// @@ -798,9 +889,11 @@ namespace Umbraco.Core.Services { using (var uow = UowProvider.CreateUnitOfWork()) { + uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); - var query = repository.Query.Where(x => x.Published == true && x.Id == id && x.Trashed == false); - int count = repository.Count(query); + var query = repository.Query.Where(x => x.Published && x.Id == id && x.Trashed == false); + var count = repository.Count(query); + uow.Complete(); return count > 0; } } @@ -812,17 +905,40 @@ namespace Umbraco.Core.Services /// True if the Content can be published, otherwise False public bool IsPublishable(IContent content) { - //If the passed in content has yet to be saved we "fallback" to checking the Parent - //because if the Parent is publishable then the current content can be Saved and Published - if (content.HasIdentity == false) - { - IContent parent = GetById(content.ParentId); - return IsPublishable(parent, true); - } + // fast + if (content.ParentId == Constants.System.Root) return true; // root content is always publishable + if (content.Trashed) return false; // trashed content is never publishable - return IsPublishable(content, false); + // not trashed and has a parent: publishable if the parent is path-published + using (var uow = UowProvider.CreateUnitOfWork()) + { + uow.ReadLock(Constants.Locks.ContentTree); + var repo = uow.CreateRepository(); + var parent = repo.Get(content.ParentId); + if (parent == null) + throw new Exception("Out of sync."); // causes rollback + var isPublishable = repo.IsPathPublished(parent); + uow.Complete(); + return isPublishable; + } } + public bool IsPathPublished(IContent content) + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + uow.ReadLock(Constants.Locks.ContentTree); + var repo = uow.CreateRepository(); + var isPathPublished = repo.IsPathPublished(content); + uow.Complete(); + return isPathPublished; + } + } + + #endregion + + #region Save, Publish, Unpublish + /// /// This will rebuild the xml structures for content in the database. /// @@ -866,172 +982,113 @@ namespace Umbraco.Core.Services } /// - /// Publishes a single object + /// Saves a single object /// - /// The to publish - /// Optional Id of the User issueing the publishing - /// True if publishing succeeded, otherwise False - public bool Publish(IContent content, int userId = 0) + /// The to save + /// Optional Id of the User saving the Content + /// Optional boolean indicating whether or not to raise events. + public void Save(IContent content, int userId = 0, bool raiseEvents = true) { - var result = SaveAndPublishDo(content, userId); - Logger.Info("Call was made to ContentService.Publish, use PublishWithStatus instead since that method will provide more detailed information on the outcome"); - return result.Success; + ((IContentServiceOperations) this).Save(content, userId, raiseEvents); } /// - /// Publishes a object and all its children + /// Saves a single object /// - /// The to publish along with its children - /// Optional Id of the User issueing the publishing - /// - /// The list of statuses for all published items - IEnumerable> IContentServiceOperations.PublishWithChildren(IContent content, int userId, bool includeUnpublished) - { - return PublishWithChildrenDo(content, userId, includeUnpublished); - } - - /// - /// Saves and Publishes a single object - /// - /// The to save and publish - /// Optional Id of the User issueing the publishing - /// Optional boolean indicating whether or not to raise save events. - /// True if publishing succeeded, otherwise False - Attempt IContentServiceOperations.SaveAndPublish(IContent content, int userId, bool raiseEvents) - { - return SaveAndPublishDo(content, userId, raiseEvents); - } - - /// - /// Deletes an object by moving it to the Recycle Bin - /// - /// Move an item to the Recycle Bin will result in the item being unpublished - /// The to delete - /// Optional Id of the User deleting the Content - Attempt IContentServiceOperations.MoveToRecycleBin(IContent content, int userId) + /// The to save + /// Optional Id of the User saving the Content + /// Optional boolean indicating whether or not to raise events. + Attempt IContentServiceOperations.Save(IContent content, int userId, bool raiseEvents) { var evtMsgs = EventMessagesFactory.Get(); - using (new WriteLock(Locker)) + if (raiseEvents && Saving.IsRaisedEventCancelled(new SaveEventArgs(content, evtMsgs), this)) + return OperationStatus.Attempt.Cancel(evtMsgs); + + using (var uow = UowProvider.CreateUnitOfWork()) { - var originalPath = content.Path; - - if (Trashing.IsRaisedEventCancelled( - new MoveEventArgs(evtMsgs, new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent)), - this)) - { - return OperationStatus.Cancelled(evtMsgs); - } - - var moveInfo = new List> - { - new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent) - }; - - //Make sure that published content is unpublished before being moved to the Recycle Bin - if (HasPublishedVersion(content.Id)) - { - //TODO: this shouldn't be a 'sub operation', and if it needs to be it cannot raise events and cannot be cancelled! - UnPublish(content, userId); - } - - //Unpublish descendents of the content item that is being moved to trash - var descendants = GetDescendants(content).OrderBy(x => x.Level).ToList(); - foreach (var descendant in descendants) - { - //TODO: this shouldn't be a 'sub operation', and if it needs to be it cannot raise events and cannot be cancelled! - UnPublish(descendant, userId); - } + uow.WriteLock(Constants.Locks.ContentTree); + var repository = uow.CreateRepository(); + if (content.HasIdentity == false) + content.CreatorId = userId; content.WriterId = userId; - content.ChangeTrashedState(true); - using (var uow = UowProvider.CreateUnitOfWork()) + // saving the Published version => indicate we are .Saving + // saving the Unpublished version => remains .Unpublished + if (content.Published) + content.ChangePublishedState(PublishedState.Saving); + + repository.AddOrUpdate(content); + repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); + + uow.Complete(); + } + + if (raiseEvents) + Saved.RaiseEvent(new SaveEventArgs(content, false, evtMsgs), this); + + Audit(AuditType.Save, "Save Content performed by user", userId, content.Id); + + return OperationStatus.Attempt.Succeed(evtMsgs); + } + + /// + /// Saves a collection of objects. + /// + /// + /// If the collection of content contains new objects that references eachother by Id or ParentId, + /// then use the overload Save method with a collection of Lazy . + /// + /// Collection of to save + /// Optional Id of the User saving the Content + /// Optional boolean indicating whether or not to raise events. + public void Save(IEnumerable contents, int userId = 0, bool raiseEvents = true) + { + ((IContentServiceOperations) this).Save(contents, userId, raiseEvents); + } + + /// + /// Saves a collection of objects. + /// + /// Collection of to save + /// Optional Id of the User saving the Content + /// Optional boolean indicating whether or not to raise events. + Attempt IContentServiceOperations.Save(IEnumerable contents, int userId, bool raiseEvents) + { + var evtMsgs = EventMessagesFactory.Get(); + var contentsA = contents.ToArray(); + + if (raiseEvents && Saving.IsRaisedEventCancelled(new SaveEventArgs(contentsA, evtMsgs), this)) + return OperationStatus.Attempt.Cancel(evtMsgs); + + using (var uow = UowProvider.CreateUnitOfWork()) + { + uow.WriteLock(Constants.Locks.ContentTree); + var repository = uow.CreateRepository(); + foreach (var content in contentsA) { - var repository = uow.CreateRepository(); + if (content.HasIdentity == false) + content.CreatorId = userId; + content.WriterId = userId; + + // saving the Published version => indicate we are .Saving + // saving the Unpublished version => remains .Unpublished + if (content.Published) + content.ChangePublishedState(PublishedState.Saving); + repository.AddOrUpdate(content); - - //Loop through descendants to update their trash state, but ensuring structure by keeping the ParentId - foreach (var descendant in descendants) - { - moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); - descendant.WriterId = userId; - descendant.ChangeTrashedState(true, descendant.ParentId); - repository.AddOrUpdate(descendant); - } - - uow.Complete(); + repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); } - Trashed.RaiseEvent(new MoveEventArgs(false, evtMsgs, moveInfo.ToArray()), this); - Audit(AuditType.Move, "Move Content to Recycle Bin performed by user", userId, content.Id); - - return OperationStatus.Success(evtMsgs); + uow.Complete(); } - } - /// - /// UnPublishes a single object - /// - /// The to publish - /// Optional Id of the User issueing the publishing - /// True if unpublishing succeeded, otherwise False - Attempt IContentServiceOperations.UnPublish(IContent content, int userId) - { - return UnPublishDo(content, false, userId); - } + if (raiseEvents) + Saved.RaiseEvent(new SaveEventArgs(contentsA, false, evtMsgs), this); + Audit(AuditType.Save, "Bulk Save content performed by user", userId == -1 ? 0 : userId, Constants.System.Root); - /// - /// Publishes a single object - /// - /// The to publish - /// Optional Id of the User issueing the publishing - /// True if publishing succeeded, otherwise False - public Attempt PublishWithStatus(IContent content, int userId = 0) - { - return ((IContentServiceOperations)this).Publish(content, userId); - } - - /// - /// Publishes a object and all its children - /// - /// The to publish along with its children - /// Optional Id of the User issueing the publishing - /// True if publishing succeeded, otherwise False - [Obsolete("Use PublishWithChildrenWithStatus instead, that method will provide more detailed information on the outcome and also allows the includeUnpublished flag")] - public bool PublishWithChildren(IContent content, int userId = 0) - { - var result = PublishWithChildrenDo(content, userId, true); - - //This used to just return false only when the parent content failed, otherwise would always return true so we'll - // do the same thing for the moment - if (result.All(x => x.Result.ContentItem.Id != content.Id)) - return false; - - return result.Single(x => x.Result.ContentItem.Id == content.Id).Success; - } - - /// - /// Publishes a object and all its children - /// - /// The to publish along with its children - /// Optional Id of the User issueing the publishing - /// set to true if you want to also publish children that are currently unpublished - /// True if publishing succeeded, otherwise False - public IEnumerable> PublishWithChildrenWithStatus(IContent content, int userId = 0, bool includeUnpublished = false) - { - return ((IContentServiceOperations)this).PublishWithChildren(content, userId, includeUnpublished); - } - - /// - /// UnPublishes a single object - /// - /// The to publish - /// Optional Id of the User issueing the publishing - /// True if unpublishing succeeded, otherwise False - public bool UnPublish(IContent content, int userId = 0) - { - return ((IContentServiceOperations)this).UnPublish(content, userId).Success; + return OperationStatus.Attempt.Succeed(evtMsgs); } /// @@ -1055,139 +1112,22 @@ namespace Umbraco.Core.Services /// Optional Id of the User issueing the publishing /// Optional boolean indicating whether or not to raise save events. /// True if publishing succeeded, otherwise False - public Attempt SaveAndPublishWithStatus(IContent content, int userId = 0, bool raiseEvents = true) + Attempt IContentServiceOperations.SaveAndPublish(IContent content, int userId, bool raiseEvents) { - return ((IContentServiceOperations)this).SaveAndPublish(content, userId, raiseEvents); + return SaveAndPublishDo(content, userId, raiseEvents); } /// - /// Saves a single object + /// Publishes a single object /// - /// The to save - /// Optional Id of the User saving the Content - /// Optional boolean indicating whether or not to raise events. - public void Save(IContent content, int userId = 0, bool raiseEvents = true) + /// The to publish + /// Optional Id of the User issueing the publishing + /// True if publishing succeeded, otherwise False + public bool Publish(IContent content, int userId = 0) { - ((IContentServiceOperations)this).Save(content, userId, raiseEvents); - } - - /// - /// Saves a collection of objects. - /// - /// Collection of to save - /// Optional Id of the User saving the Content - /// Optional boolean indicating whether or not to raise events. - Attempt IContentServiceOperations.Save(IEnumerable contents, int userId, bool raiseEvents) - { - var asArray = contents.ToArray(); - - var evtMsgs = EventMessagesFactory.Get(); - - if (raiseEvents) - { - if (Saving.IsRaisedEventCancelled( - new SaveEventArgs(asArray, evtMsgs), - this)) - { - return OperationStatus.Cancelled(evtMsgs); - } - } - using (new WriteLock(Locker)) - { - var containsNew = asArray.Any(x => x.HasIdentity == false); - - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - if (containsNew) - { - foreach (var content in asArray) - { - content.WriterId = userId; - - //Only change the publish state if the "previous" version was actually published - if (content.Published) - content.ChangePublishedState(PublishedState.Saved); - - repository.AddOrUpdate(content); - //add or update preview - repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); - } - } - else - { - foreach (var content in asArray) - { - content.WriterId = userId; - repository.AddOrUpdate(content); - //add or update preview - repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); - } - } - - uow.Complete(); - } - - if (raiseEvents) - Saved.RaiseEvent(new SaveEventArgs(asArray, false, evtMsgs), this); - Audit(AuditType.Save, "Bulk Save content performed by user", userId == -1 ? 0 : userId, Constants.System.Root); - - return OperationStatus.Success(evtMsgs); - } - } - - /// - /// Permanently deletes an object. - /// - /// - /// This method will also delete associated media files, child content and possibly associated domains. - /// - /// Please note that this method will completely remove the Content from the database - /// The to delete - /// Optional Id of the User deleting the Content - Attempt IContentServiceOperations.Delete(IContent content, int userId) - { - var evtMsgs = EventMessagesFactory.Get(); - - using (new WriteLock(Locker)) - { - if (Deleting.IsRaisedEventCancelled( - new DeleteEventArgs(content, evtMsgs), - this)) - { - return OperationStatus.Cancelled(evtMsgs); - } - - //Make sure that published content is unpublished before being deleted - if (HasPublishedVersion(content.Id)) - { - UnPublish(content, userId); - } - - //Delete children before deleting the 'possible parent' - var children = GetChildren(content.Id); - foreach (var child in children) - { - Delete(child, userId); - } - - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - repository.Delete(content); - uow.Complete(); - - var args = new DeleteEventArgs(content, false, evtMsgs); - Deleted.RaiseEvent(args, this); - - //remove any flagged media files - repository.DeleteMediaFiles(args.MediaFilesToDelete); - } - - Audit(AuditType.Delete, "Delete Content performed by user", userId, content.Id); - - return OperationStatus.Success(evtMsgs); - } + var result = SaveAndPublishDo(content, userId); + Logger.Info("Call was made to ContentService.Publish, use PublishWithStatus instead since that method will provide more detailed information on the outcome"); + return result.Success; } /// @@ -1202,82 +1142,100 @@ namespace Umbraco.Core.Services } /// - /// Saves a single object + /// UnPublishes a single object /// - /// The to save - /// Optional Id of the User saving the Content - /// Optional boolean indicating whether or not to raise events. - Attempt IContentServiceOperations.Save(IContent content, int userId, bool raiseEvents) + /// The to publish + /// Optional Id of the User issueing the publishing + /// True if unpublishing succeeded, otherwise False + public bool UnPublish(IContent content, int userId = 0) { - return Save(content, true, userId, raiseEvents); + return ((IContentServiceOperations)this).UnPublish(content, userId).Success; } /// - /// Saves a collection of objects. + /// UnPublishes a single object /// - /// - /// If the collection of content contains new objects that references eachother by Id or ParentId, - /// then use the overload Save method with a collection of Lazy . - /// - /// Collection of to save - /// Optional Id of the User saving the Content - /// Optional boolean indicating whether or not to raise events. - public void Save(IEnumerable contents, int userId = 0, bool raiseEvents = true) + /// The to publish + /// Optional Id of the User issueing the publishing + /// True if unpublishing succeeded, otherwise False + Attempt IContentServiceOperations.UnPublish(IContent content, int userId) { - ((IContentServiceOperations)this).Save(contents, userId, raiseEvents); + return UnPublishDo(content, false, userId); } /// - /// Deletes all content of specified type. All children of deleted content is moved to Recycle Bin. + /// Saves and Publishes a single object /// - /// This needs extra care and attention as its potentially a dangerous and extensive operation - /// Id of the - /// Optional Id of the user issueing the delete operation - public void DeleteContentOfType(int contentTypeId, int userId = 0) + /// The to save and publish + /// Optional Id of the User issueing the publishing + /// Optional boolean indicating whether or not to raise save events. + /// True if publishing succeeded, otherwise False + public Attempt SaveAndPublishWithStatus(IContent content, int userId = 0, bool raiseEvents = true) { - //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. - - using (new WriteLock(Locker)) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - //NOTE What about content that has the contenttype as part of its composition? - var query = repository.Query.Where(x => x.ContentTypeId == contentTypeId); - var contents = repository.GetByQuery(query).ToArray(); - - if (Deleting.IsRaisedEventCancelled(new DeleteEventArgs(contents), this)) - return; - - foreach (var content in contents.OrderByDescending(x => x.ParentId)) - { - //Look for children of current content and move that to trash before the current content is deleted - var c = content; - var childQuery = repository.Query.Where(x => x.Path.StartsWith(c.Path)); - var children = repository.GetByQuery(childQuery); - - foreach (var child in children) - { - if (child.ContentType.Id != contentTypeId) - MoveToRecycleBin(child, userId); - } - - //Permantly delete the content - Delete(content, userId); - } - } - - Audit(AuditType.Delete, - string.Format("Delete Content of Type {0} performed by user", contentTypeId), - userId, Constants.System.Root); - } + return ((IContentServiceOperations)this).SaveAndPublish(content, userId, raiseEvents); } + /// + /// Publishes a single object + /// + /// The to publish + /// Optional Id of the User issueing the publishing + /// True if publishing succeeded, otherwise False + public Attempt PublishWithStatus(IContent content, int userId = 0) + { + return ((IContentServiceOperations) this).Publish(content, userId); + } + + /// + /// Publishes a object and all its children + /// + /// The to publish along with its children + /// Optional Id of the User issueing the publishing + /// True if publishing succeeded, otherwise False + [Obsolete("Use PublishWithChildrenWithStatus instead, that method will provide more detailed information on the outcome and also allows the includeUnpublished flag")] + public bool PublishWithChildren(IContent content, int userId = 0) + { + // this used to just return false only when the parent content failed, otherwise would + // always return true so we'll do the same thing for the moment + + var result = PublishWithChildrenDo(content, userId, true); + + // FirstOrDefault() is a pain to use with structs and result contain Attempt structs + // so use this code, which is fast and works - and please ReSharper do NOT suggest otherwise + // ReSharper disable once LoopCanBeConvertedToQuery + foreach (var r in result) + if (r.Result.ContentItem.Id == content.Id) return r.Success; + return false; + } + + /// + /// Publishes a object and all its children + /// + /// The to publish along with its children + /// Optional Id of the User issueing the publishing + /// set to true if you want to also publish children that are currently unpublished + /// True if publishing succeeded, otherwise False + public IEnumerable> PublishWithChildrenWithStatus(IContent content, int userId = 0, bool includeUnpublished = false) + { + return ((IContentServiceOperations)this).PublishWithChildren(content, userId, includeUnpublished); + } + + /// + /// Publishes a object and all its children + /// + /// The to publish along with its children + /// Optional Id of the User issueing the publishing + /// + /// The list of statuses for all published items + IEnumerable> IContentServiceOperations.PublishWithChildren(IContent content, int userId, bool includeUnpublished) + { + return PublishWithChildrenDo(content, userId, includeUnpublished); + } + + #endregion + + #region Delete + /// /// Permanently deletes an object as well as all of its Children. /// @@ -1292,6 +1250,78 @@ namespace Umbraco.Core.Services ((IContentServiceOperations)this).Delete(content, userId); } + /// + /// Permanently deletes an object. + /// + /// + /// This method will also delete associated media files, child content and possibly associated domains. + /// + /// Please note that this method will completely remove the Content from the database + /// The to delete + /// Optional Id of the User deleting the Content + Attempt IContentServiceOperations.Delete(IContent content, int userId) + { + var evtMsgs = EventMessagesFactory.Get(); + + if (Deleting.IsRaisedEventCancelled(new DeleteEventArgs(content, evtMsgs), this)) + return OperationStatus.Attempt.Cancel(evtMsgs); + + using (var uow = UowProvider.CreateUnitOfWork()) + { + uow.WriteLock(Constants.Locks.ContentTree); + var repository = uow.CreateRepository(); + + // 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.HasPublishedVersion) + UnPublished.RaiseEvent(new PublishEventArgs(content, false, false), this); + + DeleteLocked(repository, content); + uow.Complete(); + } + + Audit(AuditType.Delete, "Delete Content performed by user", userId, content.Id); + + return OperationStatus.Attempt.Succeed(evtMsgs); + } + + private void DeleteLocked(IContentRepository repository, 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().ToArray()).Length > 0) + { + foreach (var ci in cc) + stack.Push(ci); + c = cc[cc.Length - 1]; + } + c = stack.Pop(); + level = c.Level; + + repository.Delete(c); + var args = new DeleteEventArgs(c, false); // raise event & get flagged files + Deleted.RaiseEvent(args, this); + + IOHelper.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. @@ -1306,6 +1336,7 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork()) { + uow.WriteLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); repository.DeleteVersions(id, versionDate); uow.Complete(); @@ -1326,30 +1357,32 @@ namespace Umbraco.Core.Services /// Optional Id of the User deleting versions of a Content object public void DeleteVersion(int id, Guid versionId, bool deletePriorVersions, int userId = 0) { - using (new WriteLock(Locker)) + if (DeletingVersions.IsRaisedEventCancelled(new DeleteRevisionsEventArgs(id, /*specificVersion:*/ versionId), this)) + return; + + if (deletePriorVersions) { - if (DeletingVersions.IsRaisedEventCancelled(new DeleteRevisionsEventArgs(id, specificVersion: versionId), this)) - return; - - if (deletePriorVersions) - { - var content = GetByVersion(versionId); - DeleteVersions(id, content.UpdateDate, userId); - } - - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - repository.DeleteVersion(versionId); - uow.Complete(); - } - - DeletedVersions.RaiseEvent(new DeleteRevisionsEventArgs(id, false, specificVersion: versionId), this); - - Audit(AuditType.Delete, "Delete Content by version performed by user", userId, Constants.System.Root); + var content = GetByVersion(versionId); + DeleteVersions(id, content.UpdateDate, userId); } + + using (var uow = UowProvider.CreateUnitOfWork()) + { + uow.WriteLock(Constants.Locks.ContentTree); + var repository = uow.CreateRepository(); + repository.DeleteVersion(versionId); + uow.Complete(); + } + + DeletedVersions.RaiseEvent(new DeleteRevisionsEventArgs(id, false,/* specificVersion:*/ versionId), this); + + Audit(AuditType.Delete, "Delete Content by version performed by user", userId, Constants.System.Root); } + #endregion + + #region Move, RecycleBin + /// /// Deletes an object by moving it to the Recycle Bin /// @@ -1361,6 +1394,46 @@ namespace Umbraco.Core.Services ((IContentServiceOperations)this).MoveToRecycleBin(content, userId); } + /// + /// Deletes an object by moving it to the Recycle Bin + /// + /// Move an item to the Recycle Bin will result in the item being unpublished + /// The to delete + /// Optional Id of the User deleting the Content + Attempt IContentServiceOperations.MoveToRecycleBin(IContent content, int userId) + { + var evtMsgs = EventMessagesFactory.Get(); + var moves = new List>(); + + using (var uow = UowProvider.CreateUnitOfWork()) + { + uow.WriteLock(Constants.Locks.ContentTree); + var repository = uow.CreateRepository(); + + var originalPath = content.Path; + if (Trashing.IsRaisedEventCancelled(new MoveEventArgs(new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent)), this)) + return OperationStatus.Attempt.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(repository, content, Constants.System.RecycleBinContent, null, userId, moves, true); + uow.Complete(); + } + + var moveInfo = moves + .Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId)) + .ToArray(); + + Trashed.RaiseEvent(new MoveEventArgs(false, evtMsgs, moveInfo), this); + Audit(AuditType.Move, "Move Content to Recycle Bin performed by user", userId, content.Id); + + return OperationStatus.Attempt.Succeed(evtMsgs); + } + /// /// Moves an object to a new location by changing its parent id. /// @@ -1374,32 +1447,104 @@ namespace Umbraco.Core.Services /// Optional Id of the User moving the Content public void Move(IContent content, int parentId, int userId = 0) { - using (new WriteLock(Locker)) + // if moving to the recycle bin then use the proper method + if (parentId == Constants.System.RecycleBinContent) { - //This ensures that the correct method is called if this method is used to Move to recycle bin. - if (parentId == Constants.System.RecycleBinContent) - { - MoveToRecycleBin(content, userId); - return; - } - - if (Moving.IsRaisedEventCancelled( - new MoveEventArgs( - new MoveEventInfo(content, content.Path, parentId)), this)) - { - return; - } - - //used to track all the moved entities to be given to the event - var moveInfo = new List>(); - - //call private method that does the recursive moving - PerformMove(content, parentId, userId, moveInfo); - - Moved.RaiseEvent(new MoveEventArgs(false, moveInfo.ToArray()), this); - - Audit(AuditType.Move, "Move Content performed by user", userId, content.Id); + MoveToRecycleBin(content, userId); + return; } + + var moves = new List>(); + + using (var uow = UowProvider.CreateUnitOfWork()) + { + uow.WriteLock(Constants.Locks.ContentTree); + var repository = uow.CreateRepository(); + + 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 + + if (Moving.IsRaisedEventCancelled(new MoveEventArgs(new MoveEventInfo(content, content.Path, parentId)), this)) + 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.HasPublishedVersion) + { + // however, it had been masked when being trashed, so there's no need for + // any special event here - just change its state + content.ChangePublishedState(PublishedState.Unpublishing); + } + + PerformMoveLocked(repository, content, parentId, parent, userId, moves, trashed); + + uow.Complete(); + } + + var moveInfo = moves //changes + .Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId)) + .ToArray(); + + Moved.RaiseEvent(new MoveEventArgs(false, moveInfo), this); + + Audit(AuditType.Move, "Move Content performed by user", userId, content.Id); + } + + // MUST be called from within WriteLock + // trash indicates whether we are trashing, un-trashing, or not changing anything + private void PerformMoveLocked(IContentRepository repository, + 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 + + // 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(repository, content, userId, trash); + + // BUT 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; + + var descendants = GetDescendants(content); + 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 + descendant.Path = paths[descendant.Id] = paths[descendant.ParentId] + "," + descendant.Id; + descendant.Level += levelDelta; + PerformMoveContentLocked(repository, descendant, userId, trash); + } + } + + private static void PerformMoveContentLocked(IContentRepository repository, IContent content, int userId, + bool? trash) + { + if (trash.HasValue) ((ContentBase) content).Trashed = trash.Value; + content.WriterId = userId; + repository.AddOrUpdate(content); } /// @@ -1407,38 +1552,43 @@ namespace Umbraco.Core.Services /// public void EmptyRecycleBin() { - using (new WriteLock(Locker)) + var nodeObjectType = new Guid(Constants.ObjectTypes.Document); + var deleted = new List(); + var evtMsgs = EventMessagesFactory.Get(); // todo - and then? + + using (var uow = UowProvider.CreateUnitOfWork()) { - Dictionary> entities; - List files; - bool success; - var nodeObjectType = new Guid(Constants.ObjectTypes.Document); + uow.WriteLock(Constants.Locks.ContentTree); + var repository = uow.CreateRepository(); - using (var uow = UowProvider.CreateUnitOfWork()) + // 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 + if (EmptyingRecycleBin.IsRaisedEventCancelled(new RecycleBinEventArgs(nodeObjectType), this)) + return; // causes rollback + + // emptying the recycle bin means deleting whetever is in there - do it properly! + var query = repository.Query.Where(x => x.ParentId == Constants.System.RecycleBinContent); + var contents = repository.GetByQuery(query).ToArray(); + foreach (var content in contents) { - var repository = uow.CreateRepository(); - //Create a dictionary of ids -> dictionary of property aliases + values - entities = repository.GetEntitiesInRecycleBin() - .ToDictionary( - key => key.Id, - val => (IEnumerable)val.Properties); - - files = ((ContentRepository)repository).GetFilesInRecycleBinForUploadField(); - - if (EmptyingRecycleBin.IsRaisedEventCancelled(new RecycleBinEventArgs(nodeObjectType, entities, files), this)) - return; - - success = repository.EmptyRecycleBin(); - - EmptiedRecycleBin.RaiseEvent(new RecycleBinEventArgs(nodeObjectType, entities, files, success), this); - - if (success) - repository.DeleteMediaFiles(files); + DeleteLocked(repository, content); + deleted.Add(content); } + + EmptiedRecycleBin.RaiseEvent(new RecycleBinEventArgs(nodeObjectType, true), this); + uow.Complete(); } + Audit(AuditType.Delete, "Empty Content Recycle Bin performed by user", 0, Constants.System.RecycleBinContent); } + #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. @@ -1465,68 +1615,69 @@ namespace Umbraco.Core.Services /// The newly created object public IContent Copy(IContent content, int parentId, bool relateToOriginal, bool recursive, int userId = 0) { - //TODO: This all needs to be managed correctly so that the logic is submitted in one - // transaction, the CRUD needs to be moved to the repo + var copy = content.DeepCloneWithResetIdentities(); + copy.ParentId = parentId; - using (new WriteLock(Locker)) + if (Copying.IsRaisedEventCancelled(new CopyEventArgs(content, copy, parentId), this)) + return null; + + // fixme - relateToOriginal is ignored?! + + using (var uow = UowProvider.CreateUnitOfWork()) { - var copy = content.DeepCloneWithResetIdentities(); - copy.ParentId = parentId; + uow.WriteLock(Constants.Locks.ContentTree); + var repository = uow.CreateRepository(); - // A copy should never be set to published automatically even if the original was. - copy.ChangePublishedState(PublishedState.Unpublished); + // a copy is .Saving and will be .Unpublished + if (copy.Published) + copy.ChangePublishedState(PublishedState.Saving); - if (Copying.IsRaisedEventCancelled(new CopyEventArgs(content, copy, parentId), this)) - return null; + // update the create author and last edit author + copy.CreatorId = userId; + copy.WriterId = userId; - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - // Update the create author and last edit author - copy.CreatorId = userId; - copy.WriterId = userId; + // save + repository.AddOrUpdate(copy); + repository.AddOrUpdatePreviewXml(copy, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); - repository.AddOrUpdate(copy); - //add or update a preview - repository.AddOrUpdatePreviewXml(copy, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); - uow.Flush(); // ensure copy has an ID - - - //Special case for the associated tags - //TODO: Move this to the repository layer in a single transaction! - //don't copy tags data in tags table if the item is in the recycle bin - if (parentId != Constants.System.RecycleBinContent) - { - - var tags = uow.Database.Fetch("WHERE nodeId = @Id", new { Id = content.Id }); - foreach (var tag in tags) - { - uow.Database.Insert(new TagRelationshipDto { NodeId = copy.Id, TagId = tag.TagId, PropertyTypeId = tag.PropertyTypeId }); - } - } - - uow.Complete(); - } + uow.Flush(); // ensure copy has an ID - fixme why? if (recursive) { - //Look for children and copy those as well - var children = GetChildren(content.Id); - foreach (var child in children) + // process descendants + var copyIds = new Dictionary(); + copyIds[content.Id] = copy; + foreach (var descendant in GetDescendants(content)) { - //TODO: This shouldn't recurse back to this method, it should be done in a private method - // that doesn't have a nested lock and so we can perform the entire operation in one commit. - Copy(child, copy.Id, relateToOriginal, true, userId); + var dcopy = descendant.DeepCloneWithResetIdentities(); + //dcopy.ParentId = copyIds[descendant.ParentId]; + var descendantParentId = descendant.ParentId; + ((Content) dcopy).SetLazyParentId(new Lazy(() => copyIds[descendantParentId].Id)); + + if (dcopy.Published) + dcopy.ChangePublishedState(PublishedState.Saving); + dcopy.CreatorId = userId; + dcopy.WriterId = userId; + + repository.AddOrUpdate(dcopy); + repository.AddOrUpdatePreviewXml(dcopy, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); + + copyIds[descendant.Id] = dcopy; } } - Copied.RaiseEvent(new CopyEventArgs(content, copy, false, parentId, relateToOriginal), this); + // fixme tag & tree issue + // tags code handling has been removed here + // - tags should be handled by the content repository + // - a copy is unpublished and therefore has no impact on tags in DB - Audit(AuditType.Copy, "Copy Content performed by user", content.WriterId, content.Id); - return copy; + uow.Complete(); } - } + Copied.RaiseEvent(new CopyEventArgs(content, copy, false, parentId, relateToOriginal), this); + Audit(AuditType.Copy, "Copy Content performed by user", content.WriterId, content.Id); + return copy; + } /// /// Sends an to Publication, which executes handlers and events for the 'Send to Publication' action. @@ -1568,13 +1719,22 @@ namespace Umbraco.Core.Services if (RollingBack.IsRaisedEventCancelled(new RollbackEventArgs(content), this)) return content; - content.WriterId = userId; content.CreatorId = userId; + // need to make sure that the repository is going to save a new version + // but if we're not changing anything, the repository would not save anything + // so - make sure the property IS dirty, doing a flip-flop with an impossible value + content.WriterId = -1; + content.WriterId = userId; + using (var uow = UowProvider.CreateUnitOfWork()) { + uow.WriteLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); - content.ChangePublishedState(PublishedState.Unpublished); + + // a rolled back version is .Saving and will be .Unpublished + content.ChangePublishedState(PublishedState.Saving); + repository.AddOrUpdate(content); repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); uow.Complete(); @@ -1600,126 +1760,66 @@ namespace Umbraco.Core.Services /// True if sorting succeeded, otherwise False public bool Sort(IEnumerable items, int userId = 0, bool raiseEvents = true) { - if (raiseEvents) - { - if (Saving.IsRaisedEventCancelled(new SaveEventArgs(items), this)) + var itemsA = items.ToArray(); + if (itemsA.Length == 0) return true; + + if (raiseEvents && Saving.IsRaisedEventCancelled(new SaveEventArgs(itemsA), this)) return false; - } - var shouldBePublished = new List(); - var shouldBeSaved = new List(); + var published = new List(); + var saved = new List(); - var asArray = items.ToArray(); - using (new WriteLock(Locker)) + using (var uow = UowProvider.CreateUnitOfWork()) { - using (var uow = UowProvider.CreateUnitOfWork()) + uow.WriteLock(Constants.Locks.ContentTree); + var repository = uow.CreateRepository(); + var sortOrder = 0; + + foreach (var content in itemsA) { - var repository = uow.CreateRepository(); - int i = 0; - foreach (var content in asArray) + // if the current sort order equals that of the content we don't + // need to update it, so just increment the sort order and continue. + if (content.SortOrder == sortOrder) { - //If the current sort order equals that of the content - //we don't need to update it, so just increment the sort order - //and continue. - if (content.SortOrder == i) - { - i++; - continue; - } - - content.SortOrder = i; - content.WriterId = userId; - i++; - - if (content.Published) - { - //TODO: This should not be an inner operation, but if we do this, it cannot raise events and cannot be cancellable! - var published = _publishingStrategy.Publish(content, userId); - shouldBePublished.Add(content); - } - else - shouldBeSaved.Add(content); - - repository.AddOrUpdate(content); - //add or update a preview - repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); + sortOrder++; + continue; } - foreach (var content in shouldBePublished) - { - //Create and Save ContentXml DTO - repository.AddOrUpdateContentXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); - } + // else update + content.SortOrder = sortOrder++; + content.WriterId = userId; - uow.Complete(); + // 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); + else if (content.HasPublishedVersion) + published.Add(GetByVersion(content.PublishedVersionGuid)); + + // save + saved.Add(content); + repository.AddOrUpdate(content); + repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); } + + foreach (var content in published) + repository.AddOrUpdateContentXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); + + uow.Complete(); } if (raiseEvents) - Saved.RaiseEvent(new SaveEventArgs(asArray, false), this); - - if (shouldBePublished.Any()) - { - //TODO: This should not be an inner operation, but if we do this, it cannot raise events and cannot be cancellable! - _publishingStrategy.PublishingFinalized(shouldBePublished, false); - } + Saved.RaiseEvent(new SaveEventArgs(saved, false), this); + if (raiseEvents && published.Any()) + Published.RaiseEvent(new PublishEventArgs(published, false, false), this); Audit(AuditType.Sort, "Sorting content performed by user", userId, 0); return true; } - /// - /// Returns the persisted content's XML structure - /// - /// - /// - public XElement GetContentXml(int contentId) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - return repository.GetContentXml(contentId); - } - } - - /// - /// Returns the persisted content's preview XML structure - /// - /// - /// - /// - public XElement GetContentPreviewXml(int contentId, Guid version) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - return repository.GetContentPreviewXml(contentId, version); - } - } - - /// - /// Rebuilds all xml content in the cmsContentXml table for all documents - /// - /// - /// Only rebuild the xml structures for the content type ids passed in, if none then rebuilds the structures - /// for all content - /// - public void RebuildXmlStructures(params int[] contentTypeIds) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - repository.RebuildXmlStructures( - content => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, content), - contentTypeIds: contentTypeIds.Length == 0 ? null : contentTypeIds); - uow.Complete(); - } - - Audit(AuditType.Publish, "ContentService.RebuildXmlStructures completed, the xml has been regenerated in the database", 0, Constants.System.Root); - - } + #endregion #region Internal Methods @@ -1732,11 +1832,32 @@ namespace Umbraco.Core.Services { using (var uow = UowProvider.CreateUnitOfWork()) { + uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); - var query = repository.Query.Where(x => x.Id != content.Id && x.Path.StartsWith(content.Path) && x.Trashed == false); - var contents = repository.GetByPublishedVersion(query); + var descendants = GetPublishedDescendantsLocked(repository, content); + uow.Complete(); + return descendants; + } + } - return contents; + internal IEnumerable GetPublishedDescendantsLocked(IContentRepository repository, IContent content) + { + var pathMatch = content.Path + ","; + var query = repository.Query.Where(x => x.Id != content.Id && x.Path.StartsWith(pathMatch) /*&& x.Trashed == false*/); + var contents = repository.GetByPublishedVersion(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); + } } } @@ -1754,83 +1875,6 @@ namespace Umbraco.Core.Services } } - //TODO: All of this needs to be moved to the repository - private void PerformMove(IContent content, int parentId, int userId, ICollection> moveInfo) - { - //add a tracking item to use in the Moved event - moveInfo.Add(new MoveEventInfo(content, content.Path, parentId)); - - content.WriterId = userId; - if (parentId == Constants.System.Root) - { - content.Path = string.Concat(Constants.System.Root, ",", content.Id); - content.Level = 1; - } - else - { - var parent = GetById(parentId); - content.Path = string.Concat(parent.Path, ",", content.Id); - content.Level = parent.Level + 1; - } - - //If Content is being moved away from Recycle Bin, its state should be un-trashed - if (content.Trashed && parentId != Constants.System.RecycleBinContent) - { - content.ChangeTrashedState(false, parentId); - } - else - { - content.ParentId = parentId; - } - - //If Content is published, it should be (re)published from its new location - if (content.Published) - { - //If Content is Publishable its saved and published - //otherwise we save the content without changing the publish state, and generate new xml because the Path, Level and Parent has changed. - if (IsPublishable(content)) - { - //TODO: This is raising events, probably not desirable as this costs performance for event listeners like Examine - SaveAndPublish(content, userId); - } - else - { - //TODO: This is raising events, probably not desirable as this costs performance for event listeners like Examine - Save(content, false, userId); - - //TODO: This shouldn't be here! This needs to be part of the repository logic but in order to fix this we need to - // change how this method calls "Save" as it needs to save using an internal method - using (var uow = UowProvider.CreateUnitOfWork()) - { - var xml = _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, content); - - var poco = new ContentXmlDto { NodeId = content.Id, Xml = xml.ToDataString() }; - var exists = - uow.Database.FirstOrDefault("WHERE nodeId = @Id", new { Id = content.Id }) != - null; - int result = exists - ? uow.Database.Update(poco) - : Convert.ToInt32(uow.Database.Insert(poco)); - } - } - } - else - { - //TODO: This is raising events, probably not desirable as this costs performance for event listeners like Examine - Save(content, userId); - } - - //Ensure that Path and Level is updated on children - var children = GetChildren(content.Id).ToArray(); - if (children.Any()) - { - foreach (var child in children) - { - PerformMove(child, content.Id, userId, moveInfo); - } - } - } - /// /// Publishes a object and all its children /// @@ -1842,90 +1886,57 @@ namespace Umbraco.Core.Services /// then the list will only contain one status item, otherwise it will contain status items for it and all of it's descendants that /// are to be published. /// - private IEnumerable> PublishWithChildrenDo( - IContent content, int userId = 0, bool includeUnpublished = false) + private IEnumerable> PublishWithChildrenDo(IContent content, int userId = 0, bool includeUnpublished = false) { - if (content == null) throw new ArgumentNullException("content"); + if (content == null) throw new ArgumentNullException(nameof(content)); var evtMsgs = EventMessagesFactory.Get(); + var publishedItems = new List(); // this is for events + Attempt[] attempts; - using (new WriteLock(Locker)) + using (var uow = UowProvider.CreateUnitOfWork()) { - var result = new List>(); + uow.WriteLock(Constants.Locks.ContentTree); + var repository = uow.CreateRepository(); - //Check if parent is published (although not if its a root node) - if parent isn't published this Content cannot be published - if (content.ParentId != Constants.System.Root && content.ParentId != Constants.System.RecycleBinContent && IsPublishable(content) == false) + // fail fast + use in alreadyChecked below to avoid duplicate checks + var attempt = EnsurePublishable(content, evtMsgs); + if (attempt.Success) + attempt = StrategyCanPublish(content, userId, evtMsgs); + if (attempt.Success == false) + return new[] { attempt }; // causes rollback + + var contents = new List { content }; //include parent item + contents.AddRange(GetDescendants(content)); + + // publish using the strategy - for descendants, + // - published w/out changes: nothing to do + // - published w/changes: publish those changes + // - unpublished: publish if includeUnpublished, otherwise ignore + var alreadyChecked = new[] { content }; + attempts = StrategyPublishWithChildren(contents, alreadyChecked, userId, evtMsgs, includeUnpublished).ToArray(); + + foreach (var status in attempts.Where(x => x.Success).Select(x => x.Result)) { - Logger.Info( - string.Format( - "Content '{0}' with Id '{1}' could not be published because its parent or one of its ancestors is not published.", - content.Name, content.Id)); - result.Add(Attempt.Fail(new PublishStatus(content, PublishStatusType.FailedPathNotPublished, evtMsgs))); - return result; + // save them all, even those that are .Success because of (.StatusType == PublishStatusType.SuccessAlreadyPublished) + // so we bump the date etc + var publishedItem = status.ContentItem; + publishedItem.WriterId = userId; + repository.AddOrUpdate(publishedItem); + repository.AddOrUpdatePreviewXml(publishedItem, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); + repository.AddOrUpdateContentXml(publishedItem, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); + publishedItems.Add(publishedItem); } - //Content contains invalid property values and can therefore not be published - fire event? - if (!content.IsValid()) - { - Logger.Info( - string.Format("Content '{0}' with Id '{1}' could not be published because of invalid properties.", - content.Name, content.Id)); - result.Add( - Attempt.Fail( - new PublishStatus(content, PublishStatusType.FailedContentInvalid, evtMsgs) - { - InvalidProperties = ((ContentBase)content).LastInvalidProperties - })); - return result; - } - - //Consider creating a Path query instead of recursive method: - //var query = repository.Query.Where(x => x.Path.StartsWith(content.Path)); - - var updated = new List(); - var list = new List(); - list.Add(content); //include parent item - list.AddRange(GetDescendants(content)); - - var internalStrategy = (PublishingStrategy)_publishingStrategy; - - //Publish and then update the database with new status - var publishedOutcome = internalStrategy.PublishWithChildrenInternal(list, userId, includeUnpublished).ToArray(); - var published = publishedOutcome - .Where(x => x.Success || x.Result.StatusType == PublishStatusType.SuccessAlreadyPublished) - // ensure proper order (for events) - cannot publish a child before its parent! - .OrderBy(x => x.Result.ContentItem.Level) - .ThenBy(x => x.Result.ContentItem.SortOrder); - - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - //NOTE The Publish with subpages-dialog was used more as a republish-type-thing, so we'll have to include PublishStatusType.SuccessAlreadyPublished - //in the updated-list, so the Published event is triggered with the expected set of pages and the xml is updated. - foreach (var item in published) - { - item.Result.ContentItem.WriterId = userId; - repository.AddOrUpdate(item.Result.ContentItem); - //add or update a preview - repository.AddOrUpdatePreviewXml(item.Result.ContentItem, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); - //add or update the published xml - repository.AddOrUpdateContentXml(item.Result.ContentItem, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); - updated.Add(item.Result.ContentItem); - } - - uow.Complete(); - - } - //Save xml to db and call following method to fire event: - _publishingStrategy.PublishingFinalized(updated, false); - - Audit(AuditType.Publish, "Publish with Children performed by user", userId, content.Id); - - - return publishedOutcome; + uow.Complete(); } + + Published.RaiseEvent(new PublishEventArgs(publishedItems, false, false), this); + Audit(AuditType.Publish, "Publish with Children performed by user", userId, content.Id); + return attempts; } + /// /// UnPublishes a single object /// @@ -1935,40 +1946,40 @@ namespace Umbraco.Core.Services /// True if unpublishing succeeded, otherwise False private Attempt UnPublishDo(IContent content, bool omitCacheRefresh = false, int userId = 0) { - var newest = GetById(content.Id); // ensure we have the newest version - if (content.Version != newest.Version) // but use the original object if it's already the newest version - content = newest; + // fixme kill omitCacheRefresh! var evtMsgs = EventMessagesFactory.Get(); - var published = content.Published ? content : GetPublishedVersion(content.Id); // get the published version - if (published == null) - { - return Attempt.Succeed(new UnPublishStatus(content, UnPublishedStatusType.SuccessAlreadyUnPublished, evtMsgs)); // already unpublished - } - - var unpublished = _publishingStrategy.UnPublish(content, userId); - if (unpublished == false) return Attempt.Fail(new UnPublishStatus(content, UnPublishedStatusType.FailedCancelledByEvent, evtMsgs)); - - content.WriterId = userId; - using (var uow = UowProvider.CreateUnitOfWork()) { + uow.WriteLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); + + var newest = GetById(content.Id); // ensure we have the newest version + if (content.Version != newest.Version) // but use the original object if it's already the newest version + content = newest; + if (content.Published == false && content.HasPublishedVersion == false) + { + uow.Complete(); + return Attempt.Succeed(new UnPublishStatus(UnPublishedStatusType.SuccessAlreadyUnPublished, evtMsgs, content)); // already unpublished + } + + // strategy + var attempt = StrategyCanUnPublish(content, userId, evtMsgs); + if (attempt == false) return attempt; // causes rollback + attempt = StrategyUnPublish(content, true, userId, evtMsgs); + if (attempt == false) return attempt; // causes rollback + + content.WriterId = userId; repository.AddOrUpdate(content); - // is published is not newest, reset the published flag on published version - if (published.Version != content.Version) - repository.ClearPublished(published); + // fixme delete xml from database! was in _publishingStrategy.UnPublishingFinalized(content); repository.DeleteContentXml(content); + uow.Complete(); } - //Delete xml from db? and call following method to fire event through PublishingStrategy to update cache - if (omitCacheRefresh == false) - _publishingStrategy.UnPublishingFinalized(content); - Audit(AuditType.UnPublish, "UnPublish performed by user", userId, content.Id); - - return Attempt.Succeed(new UnPublishStatus(content, UnPublishedStatusType.Success, evtMsgs)); + UnPublished.RaiseEvent(new PublishEventArgs(content, false, false), this); + return Attempt.Succeed(new UnPublishStatus(UnPublishedStatusType.Success, evtMsgs, content)); } /// @@ -1982,295 +1993,117 @@ namespace Umbraco.Core.Services { var evtMsgs = EventMessagesFactory.Get(); - if (raiseEvents) - { - if (Saving.IsRaisedEventCancelled( - new SaveEventArgs(content, evtMsgs), this)) - { - return Attempt.Fail(new PublishStatus(content, PublishStatusType.FailedCancelledByEvent, evtMsgs)); - } - } + if (raiseEvents && Saving.IsRaisedEventCancelled(new SaveEventArgs(content), this)) + return Attempt.Fail(new PublishStatus(PublishStatusType.FailedCancelledByEvent, evtMsgs, content)); - using (new WriteLock(Locker)) - { - //Has this content item previously been published? If so, we don't need to refresh the children - var previouslyPublished = content.HasIdentity && HasPublishedVersion(content.Id); //content might not have an id - var publishStatus = new PublishStatus(content, PublishStatusType.Success, evtMsgs); //initially set to success + var isNew = content.IsNewEntity(); + var previouslyPublished = content.HasIdentity && content.HasPublishedVersion; + var status = default(Attempt); - //Check if parent is published (although not if its a root node) - if parent isn't published this Content cannot be published - publishStatus.StatusType = CheckAndLogIsPublishable(content); - //if it is not successful, then check if the props are valid - if ((int)publishStatus.StatusType < 10) - { - //Content contains invalid property values and can therefore not be published - fire event? - publishStatus.StatusType = CheckAndLogIsValid(content); - //set the invalid properties (if there are any) - publishStatus.InvalidProperties = ((ContentBase)content).LastInvalidProperties; - } - //if we're still successful, then publish using the strategy - if (publishStatus.StatusType == PublishStatusType.Success) - { - var internalStrategy = (PublishingStrategy)_publishingStrategy; - //Publish and then update the database with new status - var publishResult = internalStrategy.PublishInternal(content, userId); - //set the status type to the publish result - publishStatus.StatusType = publishResult.Result.StatusType; - } - - //we are successfully published if our publishStatus is still Successful - bool published = publishStatus.StatusType == PublishStatusType.Success; - - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - if (published == false) - { - content.ChangePublishedState(PublishedState.Saved); - } - //Since this is the Save and Publish method, the content should be saved even though the publish fails or isn't allowed - if (content.HasIdentity == false) - { - content.CreatorId = userId; - } - content.WriterId = userId; - - repository.AddOrUpdate(content); - - //Generate a new preview - repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); - - if (published) - { - //Content Xml - repository.AddOrUpdateContentXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); - } - - uow.Complete(); - } - - if (raiseEvents) - Saved.RaiseEvent(new SaveEventArgs(content, false, evtMsgs), this); - - //Save xml to db and call following method to fire event through PublishingStrategy to update cache - if (published) - { - _publishingStrategy.PublishingFinalized(content); - } - - //We need to check if children and their publish state to ensure that we 'republish' content that was previously published - if (published && previouslyPublished == false && HasChildren(content.Id)) - { - var descendants = GetPublishedDescendants(content); - - _publishingStrategy.PublishingFinalized(descendants, false); - } - - Audit(AuditType.Publish, "Save and Publish performed by user", userId, content.Id); - - return Attempt.If(publishStatus.StatusType == PublishStatusType.Success, publishStatus); - } - } - - /// - /// Saves a single object - /// - /// The to save - /// Boolean indicating whether or not to change the Published state upon saving - /// Optional Id of the User saving the Content - /// Optional boolean indicating whether or not to raise events. - private Attempt Save(IContent content, bool changeState, int userId = 0, bool raiseEvents = true) - { - var evtMsgs = EventMessagesFactory.Get(); - - if (raiseEvents) - { - if (Saving.IsRaisedEventCancelled( - new SaveEventArgs(content, evtMsgs), - this)) - { - return OperationStatus.Cancelled(evtMsgs); - } - } - - using (new WriteLock(Locker)) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - if (content.HasIdentity == false) - { - content.CreatorId = userId; - } - content.WriterId = userId; - - //Only change the publish state if the "previous" version was actually published or marked as unpublished - if (changeState && (content.Published || ((Content)content).PublishedState == PublishedState.Unpublished)) - content.ChangePublishedState(PublishedState.Saved); - - repository.AddOrUpdate(content); - - //Generate a new preview - repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); - - uow.Complete(); - } - - if (raiseEvents) - Saved.RaiseEvent(new SaveEventArgs(content, false, evtMsgs), this); - - Audit(AuditType.Save, "Save Content performed by user", userId, content.Id); - - return OperationStatus.Success(evtMsgs); - } - } - - /// - /// Checks if the passed in can be published based on the anscestors publish state. - /// - /// - /// Check current is only used when falling back to checking the Parent of non-saved content, as - /// non-saved content doesn't have a valid path yet. - /// - /// to check if anscestors are published - /// Boolean indicating whether the passed in content should also be checked for published versions - /// True if the Content can be published, otherwise False - private bool IsPublishable(IContent content, bool checkCurrent) - { - var ids = content.Path.Split(',').Select(int.Parse).ToList(); - foreach (var id in ids) - { - //If Id equals that of the recycle bin we return false because nothing in the bin can be published - if (id == Constants.System.RecycleBinContent) - return false; - - //We don't check the System Root, so just continue - if (id == Constants.System.Root) continue; - - //If the current id equals that of the passed in content and if current shouldn't be checked we skip it. - if (checkCurrent == false && id == content.Id) continue; - - //Check if the content for the current id is published - escape the loop if we encounter content that isn't published - var hasPublishedVersion = HasPublishedVersion(id); - if (hasPublishedVersion == false) - return false; - } - - return true; - } - - private PublishStatusType CheckAndLogIsPublishable(IContent content) - { - //Check if parent is published (although not if its a root node) - if parent isn't published this Content cannot be published - if (content.ParentId != Constants.System.Root && content.ParentId != Constants.System.RecycleBinContent && IsPublishable(content) == false) - { - Logger.Info( - string.Format( - "Content '{0}' with Id '{1}' could not be published because its parent is not published.", - content.Name, content.Id)); - return PublishStatusType.FailedPathNotPublished; - } - else if (content.ExpireDate.HasValue && content.ExpireDate.Value > DateTime.MinValue && DateTime.Now > content.ExpireDate.Value) - { - Logger.Info( - string.Format( - "Content '{0}' with Id '{1}' has expired and could not be published.", - content.Name, content.Id)); - return PublishStatusType.FailedHasExpired; - } - else if (content.ReleaseDate.HasValue && content.ReleaseDate.Value > DateTime.MinValue && content.ReleaseDate.Value > DateTime.Now) - { - Logger.Info( - string.Format( - "Content '{0}' with Id '{1}' is awaiting release and could not be published.", - content.Name, content.Id)); - return PublishStatusType.FailedAwaitingRelease; - } - - return PublishStatusType.Success; - } - - private PublishStatusType CheckAndLogIsValid(IContent content) - { - //Content contains invalid property values and can therefore not be published - fire event? - if (content.IsValid() == false) - { - Logger.Info( - string.Format( - "Content '{0}' with Id '{1}' could not be published because of invalid properties.", - content.Name, content.Id)); - return PublishStatusType.FailedContentInvalid; - } - - return PublishStatusType.Success; - } - - private IContentType FindContentTypeByAlias(string contentTypeAlias) - { using (var uow = UowProvider.CreateUnitOfWork()) { - var repository = uow.CreateRepository(); - var query = repository.Query.Where(x => x.Alias == contentTypeAlias); - var types = repository.GetByQuery(query); + uow.WriteLock(Constants.Locks.ContentTree); + var repository = uow.CreateRepository(); - if (types.Any() == false) - throw new Exception( - string.Format("No ContentType matching the passed in Alias: '{0}' was found", - contentTypeAlias)); + // fixme - EnsurePublishable vs StrategyCanPublish? + // EnsurePublishable ensures that path published is ok + // StrategyCanPublish ensures other things including valid properties + // should we merge or?! - var contentType = types.First(); + // ensure content is publishable, and try to publish + status = EnsurePublishable(content, evtMsgs); + if (status.Success) + { + // strategy handles events, and various business rules eg release & expire + // dates, trashed status... + status = StrategyPublish(content, false, userId, evtMsgs); + } - if (contentType == null) - throw new Exception(string.Format("ContentType matching the passed in Alias: '{0}' was null", - contentTypeAlias)); + // save - always, even if not publishing (this is SaveAndPublish) + if (content.HasIdentity == false) + content.CreatorId = userId; + content.WriterId = userId; - return contentType; + repository.AddOrUpdate(content); + repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); + if (content.Published) + repository.AddOrUpdateContentXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c)); + + uow.Complete(); } + + if (status.Success == false) + { + // fixme what about the saved event? + return status; + } + + Published.RaiseEvent(new PublishEventArgs(content, false, false), this); + + // 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) + { + if (HasChildren(content.Id)) + { + var descendants = GetPublishedDescendants(content).ToArray(); + Published.RaiseEvent(new PublishEventArgs(descendants, false, false), this); + } + } + + Audit(AuditType.Publish, "Save and Publish performed by user", userId, content.Id); + return status; } - #endregion - - #region Proxy Event Handlers - /// - /// Occurs before publish. - /// - /// Proxy to the real event on the - public static event TypedEventHandler> Publishing + private Attempt EnsurePublishable(IContent content, EventMessages evtMsgs) { - add { PublishingStrategy.Publishing += value; } - remove { PublishingStrategy.Publishing -= value; } + // root content can be published + var checkParents = content.ParentId == Constants.System.Root; + + // trashed content cannot be published + if (checkParents == false && content.ParentId != Constants.System.RecycleBinContent) + { + // ensure all ancestors are published + // because content may be new its Path may be null - start with parent + var path = content.Path ?? content.Parent().Path; + if (path != null) // if parent is also null, give up + { + var ancestorIds = path.Split(',') + .Skip(1) // remove leading "-1" + .Reverse() + .Select(int.Parse); + if (content.Path != null) + ancestorIds = ancestorIds.Skip(1); // remove trailing content.Id + + if (ancestorIds.All(HasPublishedVersion)) + checkParents = true; + } + } + + if (checkParents == false) + { + Logger.Info($"Content '{content.Name}' with Id '{content.Id}' could not be published because its parent is not published."); + return Attempt.Fail(new PublishStatus(PublishStatusType.FailedPathNotPublished, evtMsgs, content)); + } + + // fixme - should we do it - are we doing it for descendants too? + if (content.IsValid() == false) + { + Logger.Info($"Content '{content.Name}' with Id '{content.Id}' could not be published because of invalid properties."); + return Attempt.Fail(new PublishStatus(PublishStatusType.FailedContentInvalid, evtMsgs, content) + { + InvalidProperties = ((ContentBase)content).LastInvalidProperties + }); + } + + return Attempt.Succeed(new PublishStatus(PublishStatusType.Success, evtMsgs, content)); } - /// - /// Occurs after publish. - /// - /// Proxy to the real event on the - public static event TypedEventHandler> Published - { - add { PublishingStrategy.Published += value; } - remove { PublishingStrategy.Published -= value; } - } - /// - /// Occurs before unpublish. - /// - /// Proxy to the real event on the - public static event TypedEventHandler> UnPublishing - { - add { PublishingStrategy.UnPublishing += value; } - remove { PublishingStrategy.UnPublishing -= value; } - } - - /// - /// Occurs after unpublish. - /// - /// Proxy to the real event on the - public static event TypedEventHandler> UnPublished - { - add { PublishingStrategy.UnPublished += value; } - remove { PublishingStrategy.UnPublished -= value; } - } #endregion #region Event Handlers + /// /// Occurs before Delete /// @@ -2375,6 +2208,384 @@ namespace Umbraco.Core.Services /// 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; + + #endregion + + #region Publishing Strategies + + // prob. want to find nicer names? + + internal Attempt StrategyCanPublish(IContent content, int userId, EventMessages evtMsgs) + { + if (Publishing.IsRaisedEventCancelled(new PublishEventArgs(content, evtMsgs), this)) + { + Logger.Info($"Content '{content.Name}' with Id '{content.Id}' will not be published, the event was cancelled."); + return Attempt.Fail(new PublishStatus(PublishStatusType.FailedCancelledByEvent, evtMsgs, content)); + } + + // check if the content is valid + if (content.IsValid() == false) + { + Logger.Info($"Content '{content.Name}' with Id '{content.Id}' could not be published because of invalid properties."); + return Attempt.Fail(new PublishStatus(PublishStatusType.FailedContentInvalid, evtMsgs, content) + { + InvalidProperties = ((ContentBase)content).LastInvalidProperties + }); + } + + // check if the Content is Expired + if (content.Status == ContentStatus.Expired) + { + Logger.Info($"Content '{content.Name}' with Id '{content.Id}' has expired and could not be published."); + return Attempt.Fail(new PublishStatus(PublishStatusType.FailedHasExpired, evtMsgs, content)); + } + + // check if the Content is Awaiting Release + if (content.Status == ContentStatus.AwaitingRelease) + { + Logger.Info($"Content '{content.Name}' with Id '{content.Id}' is awaiting release and could not be published."); + return Attempt.Fail(new PublishStatus(PublishStatusType.FailedAwaitingRelease, evtMsgs, content)); + } + + // check if the Content is Trashed + if (content.Status == ContentStatus.Trashed) + { + Logger.Info($"Content '{content.Name}' with Id '{content.Id}' is trashed and could not be published."); + return Attempt.Fail(new PublishStatus(PublishStatusType.FailedIsTrashed, evtMsgs, content)); + } + + return Attempt.Succeed(new PublishStatus(content, evtMsgs)); + } + + internal Attempt StrategyPublish(IContent content, bool alreadyCheckedCanPublish, int userId, EventMessages evtMsgs) + { + var attempt = alreadyCheckedCanPublish + ? Attempt.Succeed(new PublishStatus(content, evtMsgs)) // already know we can + : StrategyCanPublish(content, userId, evtMsgs); // else check + if (attempt.Success == false) + return attempt; + + // change state to publishing + content.ChangePublishedState(PublishedState.Publishing); + + Logger.Info($"Content '{content.Name}' with Id '{content.Id}' has been published."); + + return attempt; + } + + /// + /// Publishes a list of content items + /// + /// Contents, ordered by level ASC + /// Contents for which we've already checked CanPublish + /// + /// + /// Indicates whether to publish content that is completely unpublished (has no published + /// version). If false, will only publish already published content with changes. Also impacts what happens if publishing + /// fails (see remarks). + /// + /// + /// Navigate content & descendants top-down and for each, + /// - if it is published + /// - and unchanged, do nothing + /// - else (has changes), publish those changes + /// - if it is not published + /// - and at top-level, publish + /// - or includeUnpublished is true, publish + /// - else do nothing & skip the underlying branch + /// + /// When publishing fails + /// - if content has no published version, skip the underlying branch + /// - else (has published version), + /// - if includeUnpublished is true, process the underlying branch + /// - else, do not process the underlying branch + /// + internal IEnumerable> StrategyPublishWithChildren(IEnumerable contents, IEnumerable alreadyChecked, int userId, EventMessages evtMsgs, bool includeUnpublished = true) + { + var statuses = new List>(); + var alreadyCheckedA = (alreadyChecked ?? Enumerable.Empty()).ToArray(); + + // list of ids that we exclude because they could not be published + var excude = new List(); + + var topLevel = -1; + foreach (var content in contents) + { + // initialize - content is ordered by level ASC + if (topLevel < 0) + topLevel = content.Level; + + if (excude.Contains(content.ParentId)) + { + // parent is excluded, so exclude content too + Logger.Info($"Content '{content.Name}' with Id '{content.Id}' will not be published because it's parent's publishing action failed or was cancelled."); + excude.Add(content.Id); + // status has been reported for an ancestor and that one is excluded => no status + continue; + } + + if (content.Published && content.Level > topLevel) // topLevel we DO want to (re)publish + { + // newest is published already + statuses.Add(Attempt.Succeed(new PublishStatus(PublishStatusType.SuccessAlreadyPublished, evtMsgs, content))); + continue; + } + + if (content.HasPublishedVersion) + { + // newest is published already but we are topLevel, or + // newest is not published, but another version is - publish newest + var r = StrategyPublish(content, alreadyCheckedA.Contains(content), userId, evtMsgs); + if (r.Success == false) + { + // we tried to publish and it failed, but it already had / still has a published version, + // the rule in remarks says that we should skip the underlying branch if includeUnpublished + // is false, else process it - not that it makes much sense, but keep it like that for now + if (includeUnpublished == false) + excude.Add(content.Id); + } + + statuses.Add(r); + continue; + } + + if (content.Level == topLevel || includeUnpublished) + { + // content has no published version, and we want to publish it, either + // because it is top-level or because we include unpublished. + // if publishing fails, and because content does not have a published + // version at all, ensure we do not process its descendants + var r = StrategyPublish(content, alreadyCheckedA.Contains(content), userId, evtMsgs); + if (r.Success == false) + excude.Add(content.Id); + + statuses.Add(r); + continue; + } + + // content has no published version, and we don't want to publish it + excude.Add(content.Id); // ignore everything below it + // content is not even considered, really => no status + } + + return statuses; + } + + internal Attempt StrategyCanUnPublish(IContent content, int userId, EventMessages evtMsgs) + { + // fire UnPublishing event + if (UnPublishing.IsRaisedEventCancelled(new PublishEventArgs(content, evtMsgs), this)) + { + Logger.Info($"Content '{content.Name}' with Id '{content.Id}' will not be unpublished, the event was cancelled."); + return Attempt.Fail(new UnPublishStatus(UnPublishedStatusType.FailedCancelledByEvent, evtMsgs, content)); + } + + return Attempt.Succeed(new UnPublishStatus(content, evtMsgs)); + } + + internal Attempt StrategyUnPublish(IContent content, bool alreadyCheckedCanUnPublish, int userId, EventMessages evtMsgs) + { + // content should (is assumed to) be the newest version, which may not be published, + // don't know how to test this, so it's not verified + + var attempt = alreadyCheckedCanUnPublish + ? Attempt.Succeed(new UnPublishStatus(content, evtMsgs)) // already know we can + : StrategyCanUnPublish(content, userId, evtMsgs); + if (attempt.Success == false) + return attempt; + + // if Content 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($"Content '{content.Name}' with Id '{content.Id}' had its release date removed, because it was unpublished."); + } + + // version is published or unpublished, but content is published + // change state to unpublishing + content.ChangePublishedState(PublishedState.Unpublishing); + + Logger.Info($"Content '{content.Name}' with Id '{content.Id}' has been unpublished."); + + return attempt; + } + + internal IEnumerable> StrategyUnPublish(IEnumerable content, int userId, EventMessages evtMsgs) + { + return content.Select(x => StrategyUnPublish(x, false, userId, evtMsgs)); + } + + #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 + /// Id of the + /// Optional Id of the user issueing the delete operation + public void DeleteContentOfType(int contentTypeId, 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 moves = new List>(); + + using (var uow = UowProvider.CreateUnitOfWork()) + { + uow.WriteLock(Constants.Locks.ContentTree); + var repository = uow.CreateRepository(); + + // fixme what about content that has the contenttype as part of its composition? + var query = repository.Query.Where(x => x.ContentTypeId == contentTypeId); + var contents = repository.GetByQuery(query).ToArray(); + + if (Deleting.IsRaisedEventCancelled(new DeleteEventArgs(contents), this)) + return; // causes rollback + + // 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.HasPublishedVersion) + UnPublished.RaiseEvent(new PublishEventArgs(content, false, false), this); + + // if current content has children, move them to trash + var c = content; + var childQuery = repository.Query.Where(x => x.Path.StartsWith(c.Path)); + var children = repository.GetByQuery(childQuery); + foreach (var child in children.Where(x => x.ContentTypeId != contentTypeId)) + { + // see MoveToRecycleBin + PerformMoveLocked(repository, child, Constants.System.RecycleBinContent, null, userId, moves, true); + } + + // delete content + // triggers the deleted event (and handles the files) + DeleteLocked(repository, content); + } + + uow.Complete(); + } + + var moveInfos = moves + .Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId)) + .ToArray(); + if (moveInfos.Length > 0) + Trashed.RaiseEvent(new MoveEventArgs(false, moveInfos), this); + + Audit(AuditType.Delete, $"Delete Content of Type {contentTypeId} performed by user", userId, Constants.System.Root); + } + + private IContentType GetContentType(string contentTypeAlias) + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + uow.ReadLock(Constants.Locks.ContentTree); + + var repository = uow.CreateRepository(); + var query = repository.Query.Where(x => x.Alias == contentTypeAlias); + var contentType = repository.GetByQuery(query).FirstOrDefault(); + + if (contentType == null) + throw new Exception($"No ContentType matching the passed in Alias: '{contentTypeAlias}' was found"); // causes rollback + + uow.Complete(); + return contentType; + } + } + + #endregion + + #region Xml - Shoud Move! + + /// + /// Returns the persisted content's XML structure + /// + /// + /// + public XElement GetContentXml(int contentId) + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + uow.ReadLock(Constants.Locks.ContentTree); + var repository = uow.CreateRepository(); + var elt = repository.GetContentXml(contentId); + uow.Complete(); + return elt; + } + } + + /// + /// Returns the persisted content's preview XML structure + /// + /// + /// + /// + public XElement GetContentPreviewXml(int contentId, Guid version) + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + uow.ReadLock(Constants.Locks.ContentTree); + var repository = uow.CreateRepository(); + var elt = repository.GetContentPreviewXml(contentId, version); + uow.Complete(); + return elt; + } + } + + /// + /// Rebuilds all xml content in the cmsContentXml table for all documents + /// + /// + /// Only rebuild the xml structures for the content type ids passed in, if none then rebuilds the structures + /// for all content + /// + public void RebuildXmlStructures(params int[] contentTypeIds) + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + uow.WriteLock(Constants.Locks.ContentTree); + var repository = uow.CreateRepository(); + repository.RebuildXmlStructures( + content => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, content), + contentTypeIds: contentTypeIds.Length == 0 ? null : contentTypeIds); + uow.Complete(); + } + + Audit(AuditType.Publish, "ContentService.RebuildXmlStructures completed, the xml has been regenerated in the database", 0, Constants.System.Root); + + } + #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/ContentTypeService.cs b/src/Umbraco.Core/Services/ContentTypeService.cs index 4013f0a943..fcbad641cf 100644 --- a/src/Umbraco.Core/Services/ContentTypeService.cs +++ b/src/Umbraco.Core/Services/ContentTypeService.cs @@ -1,20 +1,12 @@ using System; using System.Collections.Generic; -using System.Data; -using System.Diagnostics; using System.Linq; using System.Text; -using System.Xml.Linq; -using System.Threading; -using AutoMapper; -using Umbraco.Core.Configuration; using Umbraco.Core.Events; using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models; -using Umbraco.Core.Models.Rdbms; using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.UnitOfWork; @@ -23,1287 +15,77 @@ namespace Umbraco.Core.Services /// /// Represents the ContentType Service, which is an easy access to operations involving /// - public class ContentTypeService : ContentTypeServiceBase, IContentTypeService + internal class ContentTypeService : ContentTypeServiceBase, IContentTypeService { - private readonly IContentService _contentService; - private readonly IMediaService _mediaService; + private IContentService _contentService; - //Support recursive locks because some of the methods that require locking call other methods that require locking. - //for example, the Move method needs to be locked but this calls the Save method which also needs to be locked. - private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); - - public ContentTypeService(IDatabaseUnitOfWorkProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory, IContentService contentService, IMediaService mediaService) + public ContentTypeService(IDatabaseUnitOfWorkProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory, IContentService contentService) : base(provider, logger, eventMessagesFactory) { - if (contentService == null) throw new ArgumentNullException("contentService"); - if (mediaService == null) throw new ArgumentNullException("mediaService"); _contentService = contentService; - _mediaService = mediaService; } - #region Containers - public Attempt> CreateContentTypeContainer(int parentId, string name, int userId = 0) + // 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 }; + + // don't change or remove this, will need it later + private IContentService ContentService => _contentService; + //// handle circular dependencies + //internal IContentService ContentService + //{ + // get + // { + // if (_contentService == null) + // throw new InvalidOperationException("ContentTypeService.ContentService has not been initialized."); + // return _contentService; + // } + // set { _contentService = value; } + //} + + protected override Guid ContainedObjectType => Constants.ObjectTypes.DocumentTypeGuid; + + protected override void DeleteItemsOfTypes(IEnumerable typeIds) { - var evtMsgs = EventMessagesFactory.Get(); - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repo = uow.CreateRepository(); - try - { - var container = new EntityContainer(Constants.ObjectTypes.DocumentTypeGuid) - { - Name = name, - ParentId = parentId, - CreatorId = userId - }; - - if (SavingContentTypeContainer.IsRaisedEventCancelled( - new SaveEventArgs(container, evtMsgs), - this)) - { - return Attempt.Fail(new OperationStatus(container, OperationStatusType.FailedCancelledByEvent, evtMsgs)); - } - - repo.AddOrUpdate(container); - uow.Complete(); - - SavedContentTypeContainer.RaiseEvent(new SaveEventArgs(container, evtMsgs), this); - //TODO: Audit trail ? - - return Attempt.Succeed(new OperationStatus(container, OperationStatusType.Success, evtMsgs)); - } - catch (Exception ex) - { - return Attempt.Fail(new OperationStatus(null, OperationStatusType.FailedExceptionThrown, evtMsgs), ex); - } - } + foreach (var typeId in typeIds) + ContentService.DeleteContentOfType(typeId); } - public Attempt> CreateMediaTypeContainer(int parentId, string name, int userId = 0) - { - var evtMsgs = EventMessagesFactory.Get(); - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repo = uow.CreateRepository(); - try - { - var container = new EntityContainer(Constants.ObjectTypes.MediaTypeGuid) - { - Name = name, - ParentId = parentId, - CreatorId = userId - }; - - if (SavingMediaTypeContainer.IsRaisedEventCancelled( - new SaveEventArgs(container, evtMsgs), - this)) - { - return Attempt.Fail(new OperationStatus(container, OperationStatusType.FailedCancelledByEvent, evtMsgs)); - } - - repo.AddOrUpdate(container); - uow.Complete(); - - SavedMediaTypeContainer.RaiseEvent(new SaveEventArgs(container, evtMsgs), this); - //TODO: Audit trail ? - - return Attempt.Succeed(new OperationStatus(container, OperationStatusType.Success, evtMsgs)); - } - catch (Exception ex) - { - return Attempt.Fail(new OperationStatus(null, OperationStatusType.FailedExceptionThrown, evtMsgs), ex); - } - } - } - - public Attempt SaveContentTypeContainer(EntityContainer container, int userId = 0) - { - return SaveContainer( - SavingContentTypeContainer, SavedContentTypeContainer, - container, Constants.ObjectTypes.DocumentTypeContainerGuid, "document type", userId); - } - - public Attempt SaveMediaTypeContainer(EntityContainer container, int userId = 0) - { - return SaveContainer( - SavingMediaTypeContainer, SavedMediaTypeContainer, - container, Constants.ObjectTypes.MediaTypeContainerGuid, "media type", userId); - } - - private Attempt SaveContainer( - TypedEventHandler> savingEvent, - TypedEventHandler> savedEvent, - EntityContainer container, - Guid containerObjectType, - string objectTypeName, int userId) - { - var evtMsgs = EventMessagesFactory.Get(); - - if (container.ContainedObjectType != containerObjectType) - { - var ex = new InvalidOperationException("Not a " + objectTypeName + " container."); - return OperationStatus.Exception(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 OperationStatus.Exception(evtMsgs, ex); - } - - if (savingEvent.IsRaisedEventCancelled( - new SaveEventArgs(container, evtMsgs), - this)) - { - return OperationStatus.Cancelled(evtMsgs); - } - - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repo = uow.CreateContainerRepository(containerObjectType); - repo.AddOrUpdate(container); - uow.Complete(); - } - - savedEvent.RaiseEvent(new SaveEventArgs(container, evtMsgs), this); - - //TODO: Audit trail ? - - return OperationStatus.Success(evtMsgs); - } - - public EntityContainer GetContentTypeContainer(int containerId) - { - return GetContainer(containerId, Constants.ObjectTypes.DocumentTypeContainerGuid); - } - - public EntityContainer GetMediaTypeContainer(int containerId) - { - return GetContainer(containerId, Constants.ObjectTypes.MediaTypeContainerGuid); - } - - private EntityContainer GetContainer(int containerId, Guid containerObjectType) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repo = uow.CreateContainerRepository(containerObjectType); - var container = repo.Get(containerId); - return container; - } - } - - public IEnumerable GetMediaTypeContainers(int[] containerIds) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repo = uow.CreateRepository(); - return repo.GetAll(containerIds); - } - } - - public IEnumerable GetMediaTypeContainers(string name, int level) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repo = uow.CreateRepository(); - return ((EntityContainerRepository) repo).Get(name, level); - } - } - - public IEnumerable GetMediaTypeContainers(IMediaType mediaType) - { - var ancestorIds = mediaType.Path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(x => - { - var asInt = x.TryConvertTo(); - if (asInt) return asInt.Result; - return int.MinValue; - }) - .Where(x => x != int.MinValue && x != mediaType.Id) - .ToArray(); - - return GetMediaTypeContainers(ancestorIds); - } - - public EntityContainer GetContentTypeContainer(Guid containerId) - { - return GetContainer(containerId, Constants.ObjectTypes.DocumentTypeContainerGuid); - } - - public IEnumerable GetContentTypeContainers(int[] containerIds) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repo = uow.CreateRepository(); - return repo.GetAll(containerIds); - } - } - - public IEnumerable GetContentTypeContainers(IContentType contentType) - { - var ancestorIds = contentType.Path.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries) - .Select(x => - { - var asInt = x.TryConvertTo(); - if (asInt) return asInt.Result; - return int.MinValue; - }) - .Where(x => x != int.MinValue && x != contentType.Id) - .ToArray(); - - return GetContentTypeContainers(ancestorIds); - } - - public EntityContainer GetMediaTypeContainer(Guid containerId) - { - return GetContainer(containerId, Constants.ObjectTypes.MediaTypeContainerGuid); - } - - private EntityContainer GetContainer(Guid containerId, Guid containerObjectType) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repo = uow.CreateContainerRepository(containerObjectType); - var container = ((EntityContainerRepository)repo).Get(containerId); - return container; - } - } - - public IEnumerable GetContentTypeContainers(string name, int level) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repo = uow.CreateRepository(); - return ((EntityContainerRepository)repo).Get(name, level); - } - } - - public Attempt DeleteContentTypeContainer(int containerId, int userId = 0) - { - var evtMsgs = EventMessagesFactory.Get(); - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repo = uow.CreateRepository(); - var container = repo.Get(containerId); - if (container == null) return OperationStatus.NoOperation(evtMsgs); - - if (DeletingContentTypeContainer.IsRaisedEventCancelled( - new DeleteEventArgs(container, evtMsgs), - this)) - { - return Attempt.Fail(new OperationStatus(OperationStatusType.FailedCancelledByEvent, evtMsgs)); - } - - repo.Delete(container); - uow.Complete(); - - DeletedContentTypeContainer.RaiseEvent(new DeleteEventArgs(container, evtMsgs), this); - - return OperationStatus.Success(evtMsgs); - //TODO: Audit trail ? - } - } - - public Attempt DeleteMediaTypeContainer(int containerId, int userId = 0) - { - var evtMsgs = EventMessagesFactory.Get(); - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repo = uow.CreateRepository(); - var container = repo.Get(containerId); - if (container == null) return OperationStatus.NoOperation(evtMsgs); - - if (DeletingMediaTypeContainer.IsRaisedEventCancelled( - new DeleteEventArgs(container, evtMsgs), - this)) - { - return Attempt.Fail(new OperationStatus(OperationStatusType.FailedCancelledByEvent, evtMsgs)); - } - - repo.Delete(container); - uow.Complete(); - - DeletedMediaTypeContainer.RaiseEvent(new DeleteEventArgs(container, evtMsgs), this); - - return OperationStatus.Success(evtMsgs); - //TODO: Audit trail ? - } - } - - #endregion - /// - /// Gets all property type aliases. + /// 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 uow = UowProvider.CreateUnitOfWork()) { - var repository = uow.CreateRepository(); - return repository.GetAllPropertyTypeAliases(); - } - } - - /// - /// Gets all content type aliases - /// - /// - /// If this list is empty, it will return all content type aliases for media, members and content, otherwise - /// it will only return content type aliases for the object types specified - /// - /// - public IEnumerable GetAllContentTypeAliases(params Guid[] objectTypes) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - return repository.GetAllContentTypeAliases(objectTypes); - } - } - - /// - /// Copies a content type as a child under the specified parent if specified (otherwise to the root) - /// - /// - /// The content type to copy - /// - /// - /// The new alias of the content type - /// - /// - /// The new name of the content type - /// - /// - /// The parent to copy the content type to, default is -1 (root) - /// - /// - public IContentType Copy(IContentType original, string alias, string name, int parentId = -1) - { - IContentType parent = null; - if (parentId > 0) - { - parent = GetContentType(parentId); - if (parent == null) - { - throw new InvalidOperationException("Could not find content type with id " + parentId); - } - } - return Copy(original, alias, name, parent); - } - - /// - /// Copies a content type as a child under the specified parent if specified (otherwise to the root) - /// - /// - /// The content type to copy - /// - /// - /// The new alias of the content type - /// - /// - /// The new name of the content type - /// - /// - /// The parent to copy the content type to, default is null (root) - /// - /// - public IContentType Copy(IContentType original, string alias, string name, IContentType parent) - { - Mandate.ParameterNotNull(original, "original"); - Mandate.ParameterNotNullOrEmpty(alias, "alias"); - if (parent != null) - { - Mandate.That(parent.HasIdentity, () => new InvalidOperationException("The parent content type must have an identity")); - } - - var clone = original.DeepCloneWithResetIdentities(alias); - - clone.Name = name; - - var compositionAliases = clone.CompositionAliases().Except(new[] { alias }).ToList(); - //remove all composition that is not it's current alias - foreach (var a in compositionAliases) - { - clone.RemoveContentType(a); - } - - //if a parent is specified set it's composition and parent - if (parent != null) - { - //add a new parent composition - clone.AddContentType(parent); - clone.ParentId = parent.Id; - } - else - { - //set to root - clone.ParentId = -1; - } - - Save(clone); - return clone; - } - - /// - /// Gets an object by its Id - /// - /// Id of the to retrieve - /// - public IContentType GetContentType(int id) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - return repository.Get(id); - } - } - - /// - /// Gets an object by its Alias - /// - /// Alias of the to retrieve - /// - public IContentType GetContentType(string alias) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - return repository.Get(alias); - } - } - - /// - /// Gets an object by its Key - /// - /// Alias of the to retrieve - /// - public IContentType GetContentType(Guid id) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - return repository.Get(id); - } - } - - /// - /// Gets a list of all available objects - /// - /// Optional list of ids - /// An Enumerable list of objects - public IEnumerable GetAllContentTypes(params int[] ids) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - return repository.GetAll(ids); - } - } - - /// - /// Gets a list of all available objects - /// - /// Optional list of ids - /// An Enumerable list of objects - public IEnumerable GetAllContentTypes(IEnumerable ids) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - return repository.GetAll(ids.ToArray()); - } - } - - /// - /// Gets a list of children for a object - /// - /// Id of the Parent - /// An Enumerable list of objects - public IEnumerable GetContentTypeChildren(int id) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - var query = repository.Query.Where(x => x.ParentId == id); - var contentTypes = repository.GetByQuery(query); - return contentTypes; - } - } - - /// - /// Gets a list of children for a object - /// - /// Id of the Parent - /// An Enumerable list of objects - public IEnumerable GetContentTypeChildren(Guid id) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - var found = GetContentType(id); - if (found == null) return Enumerable.Empty(); - var query = repository.Query.Where(x => x.ParentId == found.Id); - var contentTypes = repository.GetByQuery(query); - return contentTypes; - } - } - - /// - /// Checks whether an item has any children - /// - /// Id of the - /// True if the content type has any children otherwise False - public bool HasChildren(int id) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - var query = repository.Query.Where(x => x.ParentId == id); - int count = repository.Count(query); - return count > 0; - } - } - - /// - /// Checks whether an item has any children - /// - /// Id of the - /// True if the content type has any children otherwise False - public bool HasChildren(Guid id) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - var found = GetContentType(id); - if (found == null) return false; - var query = repository.Query.Where(x => x.ParentId == found.Id); - int count = repository.Count(query); - return count > 0; - } - } - - /// - /// This is called after an IContentType is saved and is used to update the content xml structures in the database - /// if they are required to be updated. - /// - /// A tuple of a content type and a boolean indicating if it is new (HasIdentity was false before committing) - private void UpdateContentXmlStructure(params IContentTypeBase[] contentTypes) - { - - var toUpdate = GetContentTypesForXmlUpdates(contentTypes).ToArray(); - - if (toUpdate.Any()) - { - var firstType = toUpdate.First(); - //if it is a content type then call the rebuilding methods or content - if (firstType is IContentType) - { - var typedContentService = _contentService as ContentService; - if (typedContentService != null) - { - typedContentService.RePublishAll(toUpdate.Select(x => x.Id).ToArray()); - } - else - { - //this should never occur, the content service should always be typed but we'll check anyways. - _contentService.RePublishAll(); - } - } - else if (firstType is IMediaType) - { - //if it is a media type then call the rebuilding methods for media - var typedContentService = _mediaService as MediaService; - if (typedContentService != null) - { - typedContentService.RebuildXmlStructures(toUpdate.Select(x => x.Id).ToArray()); - } - } - } - - } - - public int CountContentTypes() - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - return repository.Count(repository.Query); - } - } - - public int CountMediaTypes() - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - return repository.Count(repository.Query); - } - } - - /// - /// Validates the composition, if its invalid a list of property type aliases that were duplicated is returned - /// - /// - /// - public Attempt ValidateComposition(IContentTypeComposition compo) - { - using (new WriteLock(Locker)) - { - try - { - ValidateLocked(compo); - return Attempt.Succeed(); - } - catch (InvalidCompositionException ex) - { - return Attempt.Fail(ex.PropertyTypeAliases, ex); - } - } - } - - protected void ValidateLocked(IContentTypeComposition compositionContentType) - { - // performs business-level validation of the composition - // should ensure that it is absolutely safe to save the composition - - // eg maybe a property has been added, with an alias that's OK (no conflict with ancestors) - // but that cannot be used (conflict with descendants) - - var contentType = compositionContentType as IContentType; - var mediaType = compositionContentType as IMediaType; - var memberType = compositionContentType as IMemberType; // should NOT do it here but... v8! - - IContentTypeComposition[] allContentTypes; - if (contentType != null) - allContentTypes = GetAllContentTypes().Cast().ToArray(); - else if (mediaType != null) - allContentTypes = GetAllMediaTypes().Cast().ToArray(); - else if (memberType != null) - return; // no compositions on members, always validate - else - throw new Exception("Composition is neither IContentType nor IMediaType nor IMemberType?"); - - var compositionAliases = compositionContentType.CompositionAliases(); - var compositions = allContentTypes.Where(x => compositionAliases.Any(y => x.Alias.Equals(y))); - var propertyTypeAliases = compositionContentType.PropertyTypes.Select(x => x.Alias.ToLowerInvariant()).ToArray(); - var indirectReferences = allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == compositionContentType.Id)); - var comparer = new DelegateEqualityComparer((x, y) => x.Id == y.Id, x => x.Id); - var dependencies = new HashSet(compositions, comparer); - var stack = new Stack(); - indirectReferences.ForEach(stack.Push);//Push indirect references to a stack, so we can add recursively - while (stack.Count > 0) - { - var indirectReference = stack.Pop(); - dependencies.Add(indirectReference); - //Get all compositions for the current indirect reference - var directReferences = indirectReference.ContentTypeComposition; - - foreach (var directReference in directReferences) - { - if (directReference.Id == compositionContentType.Id || directReference.Alias.Equals(compositionContentType.Alias)) continue; - dependencies.Add(directReference); - //A direct reference has compositions of its own - these also need to be taken into account - var directReferenceGraph = directReference.CompositionAliases(); - allContentTypes.Where(x => directReferenceGraph.Any(y => x.Alias.Equals(y, StringComparison.InvariantCultureIgnoreCase))).ForEach(c => dependencies.Add(c)); - } - //Recursive lookup of indirect references - allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == indirectReference.Id)).ForEach(stack.Push); - } - - foreach (var dependency in dependencies) - { - if (dependency.Id == compositionContentType.Id) continue; - var contentTypeDependency = allContentTypes.FirstOrDefault(x => x.Alias.Equals(dependency.Alias, StringComparison.InvariantCultureIgnoreCase)); - if (contentTypeDependency == null) continue; - var intersect = contentTypeDependency.PropertyTypes.Select(x => x.Alias.ToLowerInvariant()).Intersect(propertyTypeAliases).ToArray(); - if (intersect.Length == 0) continue; - - throw new InvalidCompositionException(compositionContentType.Alias, intersect.ToArray()); - } - } - - /// - /// Saves a single object - /// - /// to save - /// Optional id of the user saving the ContentType - public void Save(IContentType contentType, int userId = 0) - { - if (SavingContentType.IsRaisedEventCancelled(new SaveEventArgs(contentType), this)) - return; - - using (new WriteLock(Locker)) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - ValidateLocked(contentType); // throws if invalid - contentType.CreatorId = userId; - repository.AddOrUpdate(contentType); - - uow.Complete(); - } - - UpdateContentXmlStructure(contentType); - } - SavedContentType.RaiseEvent(new SaveEventArgs(contentType, false), this); - Audit(AuditType.Save, string.Format("Save ContentType performed by user"), userId, contentType.Id); - } - - /// - /// Saves a collection of objects - /// - /// Collection of to save - /// Optional id of the user saving the ContentType - public void Save(IEnumerable contentTypes, int userId = 0) - { - var asArray = contentTypes.ToArray(); - - if (SavingContentType.IsRaisedEventCancelled(new SaveEventArgs(asArray), this)) - return; - - using (new WriteLock(Locker)) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - // all-or-nothing, validate them all first - foreach (var contentType in asArray) - { - ValidateLocked(contentType); // throws if invalid - } - foreach (var contentType in asArray) - { - contentType.CreatorId = userId; - repository.AddOrUpdate(contentType); - } - - //save it all in one go - uow.Complete(); - } - - UpdateContentXmlStructure(asArray.Cast().ToArray()); - } - SavedContentType.RaiseEvent(new SaveEventArgs(asArray, false), this); - Audit(AuditType.Save, string.Format("Save ContentTypes performed by user"), userId, -1); - } - - /// - /// Deletes a single object - /// - /// to delete - /// Optional id of the user issueing the delete - /// Deleting a will delete all the objects based on this - public void Delete(IContentType contentType, int userId = 0) - { - if (DeletingContentType.IsRaisedEventCancelled(new DeleteEventArgs(contentType), this)) - return; - - using (new WriteLock(Locker)) - { - - //TODO: This needs to change, if we are deleting a content type, we should just delete the data, - // 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. - - _contentService.DeleteContentOfType(contentType.Id); - - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - repository.Delete(contentType); - uow.Complete(); - - DeletedContentType.RaiseEvent(new DeleteEventArgs(contentType, false), this); - } - - Audit(AuditType.Delete, string.Format("Delete ContentType performed by user"), userId, contentType.Id); - } - } - - /// - /// Deletes a collection of objects. - /// - /// Collection of to delete - /// Optional id of the user issueing the delete - /// - /// Deleting a will delete all the objects based on this - /// - public void Delete(IEnumerable contentTypes, int userId = 0) - { - var asArray = contentTypes.ToArray(); - - if (DeletingContentType.IsRaisedEventCancelled(new DeleteEventArgs(asArray), this)) - return; - - using (new WriteLock(Locker)) - { - foreach (var contentType in asArray) - { - _contentService.DeleteContentOfType(contentType.Id); - } - - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - foreach (var contentType in asArray) - { - repository.Delete(contentType); - } - - uow.Complete(); - - DeletedContentType.RaiseEvent(new DeleteEventArgs(asArray, false), this); - } - - Audit(AuditType.Delete, string.Format("Delete ContentTypes performed by user"), userId, -1); - } - } - - /// - /// Gets an object by its Id - /// - /// Id of the to retrieve - /// - public IMediaType GetMediaType(int id) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - return repository.Get(id); - } - } - - /// - /// Gets an object by its Alias - /// - /// Alias of the to retrieve - /// - public IMediaType GetMediaType(string alias) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - return repository.Get(alias); - } - } - - /// - /// Gets an object by its Id - /// - /// Id of the to retrieve - /// - public IMediaType GetMediaType(Guid id) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - return repository.Get(id); - } - } - - /// - /// Gets a list of all available objects - /// - /// Optional list of ids - /// An Enumerable list of objects - public IEnumerable GetAllMediaTypes(params int[] ids) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - return repository.GetAll(ids); - } - } - - /// - /// Gets a list of all available objects - /// - /// Optional list of ids - /// An Enumerable list of objects - public IEnumerable GetAllMediaTypes(IEnumerable ids) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - return repository.GetAll(ids.ToArray()); - } - } - - /// - /// Gets a list of children for a object - /// - /// Id of the Parent - /// An Enumerable list of objects - public IEnumerable GetMediaTypeChildren(int id) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - var query = repository.Query.Where(x => x.ParentId == id); - var contentTypes = repository.GetByQuery(query); - return contentTypes; - } - } - - /// - /// Gets a list of children for a object - /// - /// Id of the Parent - /// An Enumerable list of objects - public IEnumerable GetMediaTypeChildren(Guid id) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - var found = GetMediaType(id); - if (found == null) return Enumerable.Empty(); - var query = repository.Query.Where(x => x.ParentId == found.Id); - var contentTypes = repository.GetByQuery(query); - return contentTypes; - } - } - - /// - /// Checks whether an item has any children - /// - /// Id of the - /// True if the media type has any children otherwise False - public bool MediaTypeHasChildren(int id) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - var query = repository.Query.Where(x => x.ParentId == id); - int count = repository.Count(query); - return count > 0; - } - } - - /// - /// Checks whether an item has any children - /// - /// Id of the - /// True if the media type has any children otherwise False - public bool MediaTypeHasChildren(Guid id) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - var found = GetMediaType(id); - if (found == null) return false; - var query = repository.Query.Where(x => x.ParentId == found.Id); - int count = repository.Count(query); - return count > 0; - } - } - - public Attempt> MoveMediaType(IMediaType toMove, int containerId) - { - var evtMsgs = EventMessagesFactory.Get(); - - if (MovingMediaType.IsRaisedEventCancelled( - new MoveEventArgs(evtMsgs, new MoveEventInfo(toMove, toMove.Path, containerId)), - this)) - { - return Attempt.Fail( - new OperationStatus( - MoveOperationStatusType.FailedCancelledByEvent, evtMsgs)); - } - - var moveInfo = new List>(); - using (var uow = UowProvider.CreateUnitOfWork()) - { - var containerRepository = uow.CreateRepository(); - var repository = uow.CreateRepository(); - - try - { - EntityContainer container = null; - if (containerId > 0) - { - container = containerRepository.Get(containerId); - if (container == null) - throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); - } - moveInfo.AddRange(repository.Move(toMove, container)); - } - catch (DataOperationException ex) - { - return Attempt.Fail( - new OperationStatus(ex.Operation, evtMsgs)); - } + // that one is special because it works accross content, media and member types + uow.ReadLock(Constants.Locks.ContentTypes, Constants.Locks.MediaTypes, Constants.Locks.MemberTypes); + var repo = uow.CreateRepository(); + var aliases = repo.GetAllPropertyTypeAliases(); uow.Complete(); + return aliases; } - - MovedMediaType.RaiseEvent(new MoveEventArgs(false, evtMsgs, moveInfo.ToArray()), this); - - return Attempt.Succeed( - new OperationStatus(MoveOperationStatusType.Success, evtMsgs)); } - public Attempt> MoveContentType(IContentType toMove, int containerId) + /// + /// 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 property type aliases. + /// Beware! Works accross content, media and member types. + public IEnumerable GetAllContentTypeAliases(params Guid[] guids) { - var evtMsgs = EventMessagesFactory.Get(); - - if (MovingContentType.IsRaisedEventCancelled( - new MoveEventArgs(evtMsgs, new MoveEventInfo(toMove, toMove.Path, containerId)), - this)) - { - return Attempt.Fail( - new OperationStatus( - MoveOperationStatusType.FailedCancelledByEvent, evtMsgs)); - } - - var moveInfo = new List>(); using (var uow = UowProvider.CreateUnitOfWork()) { - var containerRepository = uow.CreateRepository(); - var repository = uow.CreateRepository(); - - try - { - EntityContainer container = null; - if (containerId > 0) - { - container = containerRepository.Get(containerId); - if (container == null) - throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); - } - moveInfo.AddRange(repository.Move(toMove, container)); - } - catch (DataOperationException ex) - { - return Attempt.Fail( - new OperationStatus(ex.Operation, evtMsgs)); - } + // that one is special because it works accross content, media and member types + uow.ReadLock(Constants.Locks.ContentTypes, Constants.Locks.MediaTypes, Constants.Locks.MemberTypes); + var repo = uow.CreateRepository(); + var aliases = repo.GetAllContentTypeAliases(guids); uow.Complete(); - } - - MovedContentType.RaiseEvent(new MoveEventArgs(false, evtMsgs, moveInfo.ToArray()), this); - - return Attempt.Succeed( - new OperationStatus(MoveOperationStatusType.Success, evtMsgs)); - } - - public Attempt> CopyMediaType(IMediaType toCopy, int containerId) - { - var evtMsgs = EventMessagesFactory.Get(); - - IMediaType copy; - using (var uow = UowProvider.CreateUnitOfWork()) - { - var containerRepository = uow.CreateRepository(); - var repository = uow.CreateRepository(); - try - { - if (containerId > 0) - { - var container = containerRepository.Get(containerId); - if (container == null) - throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); - } - var alias = repository.GetUniqueAlias(toCopy.Alias); - copy = toCopy.DeepCloneWithResetIdentities(alias); - copy.Name = copy.Name + " (copy)"; // might not be unique - - // if it has a parent, and the parent is a content type, unplug composition - // all other compositions remain in place in the copied content type - if (copy.ParentId > 0) - { - var parent = repository.Get(copy.ParentId); - if (parent != null) - copy.RemoveContentType(parent.Alias); - } - - copy.ParentId = containerId; - repository.AddOrUpdate(copy); - } - catch (DataOperationException ex) - { - return Attempt.Fail(new OperationStatus(null, ex.Operation, evtMsgs)); - } - uow.Complete(); - } - - return Attempt.Succeed(new OperationStatus(copy, MoveOperationStatusType.Success, evtMsgs)); - } - - public Attempt> CopyContentType(IContentType toCopy, int containerId) - { - var evtMsgs = EventMessagesFactory.Get(); - - IContentType copy; - using (var uow = UowProvider.CreateUnitOfWork()) - { - var containerRepository = uow.CreateRepository(); - var repository = uow.CreateRepository(); - try - { - if (containerId > 0) - { - var container = containerRepository.Get(containerId); - if (container == null) - throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); - } - var alias = repository.GetUniqueAlias(toCopy.Alias); - copy = toCopy.DeepCloneWithResetIdentities(alias); - copy.Name = copy.Name + " (copy)"; // might not be unique - - // if it has a parent, and the parent is a content type, unplug composition - // all other compositions remain in place in the copied content type - if (copy.ParentId > 0) - { - var parent = repository.Get(copy.ParentId); - if (parent != null) - copy.RemoveContentType(parent.Alias); - } - - copy.ParentId = containerId; - repository.AddOrUpdate(copy); - } - catch (DataOperationException ex) - { - return Attempt.Fail(new OperationStatus(null, ex.Operation, evtMsgs)); - } - uow.Complete(); - } - - return Attempt.Succeed(new OperationStatus(copy, MoveOperationStatusType.Success, evtMsgs)); - } - - /// - /// Saves a single object - /// - /// to save - /// Optional Id of the user saving the MediaType - public void Save(IMediaType mediaType, int userId = 0) - { - if (SavingMediaType.IsRaisedEventCancelled(new SaveEventArgs(mediaType), this)) - return; - - using (new WriteLock(Locker)) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - ValidateLocked(mediaType); // throws if invalid - mediaType.CreatorId = userId; - repository.AddOrUpdate(mediaType); - uow.Complete(); - - } - - UpdateContentXmlStructure(mediaType); - } - - SavedMediaType.RaiseEvent(new SaveEventArgs(mediaType, false), this); - Audit(AuditType.Save, string.Format("Save MediaType performed by user"), userId, mediaType.Id); - } - - /// - /// Saves a collection of objects - /// - /// Collection of to save - /// Optional Id of the user savging the MediaTypes - public void Save(IEnumerable mediaTypes, int userId = 0) - { - var asArray = mediaTypes.ToArray(); - - if (SavingMediaType.IsRaisedEventCancelled(new SaveEventArgs(asArray), this)) - return; - - using (new WriteLock(Locker)) - { - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - // all-or-nothing, validate them all first - foreach (var mediaType in asArray) - { - ValidateLocked(mediaType); // throws if invalid - } - foreach (var mediaType in asArray) - { - mediaType.CreatorId = userId; - repository.AddOrUpdate(mediaType); - } - - //save it all in one go - uow.Complete(); - } - - UpdateContentXmlStructure(asArray.Cast().ToArray()); - } - - SavedMediaType.RaiseEvent(new SaveEventArgs(asArray, false), this); - Audit(AuditType.Save, string.Format("Save MediaTypes performed by user"), userId, -1); - } - - /// - /// Deletes a single object - /// - /// to delete - /// Optional Id of the user deleting the MediaType - /// Deleting a will delete all the objects based on this - public void Delete(IMediaType mediaType, int userId = 0) - { - if (DeletingMediaType.IsRaisedEventCancelled(new DeleteEventArgs(mediaType), this)) - return; - using (new WriteLock(Locker)) - { - _mediaService.DeleteMediaOfType(mediaType.Id, userId); - - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - - repository.Delete(mediaType); - uow.Complete(); - - DeletedMediaType.RaiseEvent(new DeleteEventArgs(mediaType, false), this); - } - - Audit(AuditType.Delete, string.Format("Delete MediaType performed by user"), userId, mediaType.Id); - } - } - - /// - /// Deletes a collection of objects - /// - /// Collection of to delete - /// - /// Deleting a will delete all the objects based on this - public void Delete(IEnumerable mediaTypes, int userId = 0) - { - var asArray = mediaTypes.ToArray(); - - if (DeletingMediaType.IsRaisedEventCancelled(new DeleteEventArgs(asArray), this)) - return; - using (new WriteLock(Locker)) - { - foreach (var mediaType in asArray) - { - _mediaService.DeleteMediaOfType(mediaType.Id); - } - - using (var uow = UowProvider.CreateUnitOfWork()) - { - var repository = uow.CreateRepository(); - foreach (var mediaType in asArray) - { - repository.Delete(mediaType); - } - uow.Complete(); - - DeletedMediaType.RaiseEvent(new DeleteEventArgs(asArray, false), this); - } - - Audit(AuditType.Delete, string.Format("Delete MediaTypes performed by user"), userId, -1); + return aliases; } } @@ -1333,14 +115,14 @@ namespace Umbraco.Core.Services { var strictSchemaBuilder = new StringBuilder(); - var contentTypes = GetAllContentTypes(); + var contentTypes = GetAll(new int[0]); foreach (ContentType contentType in contentTypes) { string safeAlias = contentType.Alias.ToSafeAlias(); if (safeAlias != null) { - strictSchemaBuilder.AppendLine(String.Format("", safeAlias)); - strictSchemaBuilder.AppendLine(String.Format("", safeAlias)); + strictSchemaBuilder.AppendLine($""); + strictSchemaBuilder.AppendLine($""); } } @@ -1354,88 +136,21 @@ namespace Umbraco.Core.Services return dtd.ToString(); } - private void Audit(AuditType type, string message, int userId, int objectId) + protected override void UpdateContentXmlStructure(params IContentTypeBase[] contentTypes) { - using (var uow = UowProvider.CreateUnitOfWork()) + var toUpdate = GetContentTypesForXmlUpdates(contentTypes).ToArray(); + if (toUpdate.Any() == false) return; + + var contentService = _contentService as ContentService; + if (contentService != null) { - var repo = uow.CreateRepository(); - repo.AddOrUpdate(new AuditItem(objectId, message, type, userId)); - uow.Complete(); + contentService.RePublishAll(toUpdate.Select(x => x.Id).ToArray()); + } + else + { + //this should never occur, the content service should always be typed but we'll check anyways. + _contentService.RePublishAll(); } } - - #region Event Handlers - - public static event TypedEventHandler> SavingContentTypeContainer; - public static event TypedEventHandler> SavedContentTypeContainer; - public static event TypedEventHandler> DeletingContentTypeContainer; - public static event TypedEventHandler> DeletedContentTypeContainer; - public static event TypedEventHandler> SavingMediaTypeContainer; - public static event TypedEventHandler> SavedMediaTypeContainer; - public static event TypedEventHandler> DeletingMediaTypeContainer; - public static event TypedEventHandler> DeletedMediaTypeContainer; - - - /// - /// Occurs before Delete - /// - public static event TypedEventHandler> DeletingContentType; - - /// - /// Occurs after Delete - /// - public static event TypedEventHandler> DeletedContentType; - - /// - /// Occurs before Delete - /// - public static event TypedEventHandler> DeletingMediaType; - - /// - /// Occurs after Delete - /// - public static event TypedEventHandler> DeletedMediaType; - - /// - /// Occurs before Save - /// - public static event TypedEventHandler> SavingContentType; - - /// - /// Occurs after Save - /// - public static event TypedEventHandler> SavedContentType; - - /// - /// Occurs before Save - /// - public static event TypedEventHandler> SavingMediaType; - - /// - /// Occurs after Save - /// - public static event TypedEventHandler> SavedMediaType; - - /// - /// Occurs before Move - /// - public static event TypedEventHandler> MovingMediaType; - - /// - /// Occurs after Move - /// - public static event TypedEventHandler> MovedMediaType; - - /// - /// Occurs before Move - /// - public static event TypedEventHandler> MovingContentType; - - /// - /// Occurs after Move - /// - public static event TypedEventHandler> MovedContentType; - - #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/ContentTypeServiceBase.cs b/src/Umbraco.Core/Services/ContentTypeServiceBase.cs index cf6a542fa0..93b323b1a7 100644 --- a/src/Umbraco.Core/Services/ContentTypeServiceBase.cs +++ b/src/Umbraco.Core/Services/ContentTypeServiceBase.cs @@ -1,21 +1,23 @@ +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.Models.EntityBase; using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Services { - public class ContentTypeServiceBase : RepositoryService + internal abstract class ContentTypeServiceBase : RepositoryService { - public ContentTypeServiceBase(IDatabaseUnitOfWorkProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory) + protected ContentTypeServiceBase(IDatabaseUnitOfWorkProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory) : base(provider, logger, eventMessagesFactory) - { - } - + { } + /// /// This is called after an content type is saved and is used to update the content xml structures in the database /// if they are required to be updated. @@ -33,7 +35,7 @@ namespace Umbraco.Core.Services // - a content type changes it's alias OR // - if a content type has it's property removed OR // - if a content type has a property whose alias has changed - //here we need to check if the alias of the content type changed or if one of the properties was removed. + //here we need to check if the alias of the content type changed or if one of the properties was removed. var dirty = contentType as IRememberBeingDirty; if (dirty == null) continue; @@ -50,18 +52,18 @@ namespace Umbraco.Core.Services && (dirty.WasPropertyDirty("Alias") || dirty.WasPropertyDirty("HasPropertyTypeBeenRemoved") || hasAnyPropertiesChangedAlias)) { //If the alias was changed then we only need to update the xml structures for content of the current content type. - //If a property was deleted or a property alias was changed then we need to update the xml structures for any + //If a property was deleted or a property alias was changed then we need to update the xml structures for any // content of the current content type and any of the content type's child content types. if (dirty.WasPropertyDirty("Alias") && dirty.WasPropertyDirty("HasPropertyTypeBeenRemoved") == false && hasAnyPropertiesChangedAlias == false) { - //if only the alias changed then only update the current content type + //if only the alias changed then only update the current content type toUpdate.Add(contentType); } else { //if a property was deleted or alias changed, then update all content of the current content type - // and all of it's desscendant doc types. + // and all of it's desscendant doc types. toUpdate.AddRange(contentType.DescendantsAndSelf()); } } @@ -71,4 +73,887 @@ namespace Umbraco.Core.Services } } + + internal abstract class ContentTypeServiceBase : ContentTypeServiceBase + where TItem : class, IContentTypeComposition + where TService : class, IContentTypeServiceBase + { + protected ContentTypeServiceBase(IDatabaseUnitOfWorkProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory) + : base(provider, logger, eventMessagesFactory) + { + _this = this as TService; + if (_this == null) throw new Exception("Oops."); + } + + private readonly TService _this; + + public static event TypedEventHandler> Saving; + public static event TypedEventHandler> Saved; + + protected void OnSaving(SaveEventArgs args) + { + Saving.RaiseEvent(args, _this); + } + + protected bool OnSavingCancelled(SaveEventArgs args) + { + return Saving.IsRaisedEventCancelled(args, _this); + } + + protected void OnSaved(SaveEventArgs args) + { + Saved.RaiseEvent(args, _this); + } + + public static event TypedEventHandler> Deleting; + public static event TypedEventHandler> Deleted; + + protected void OnDeleting(DeleteEventArgs args) + { + Deleting.RaiseEvent(args, _this); + } + + protected bool OnDeletingCancelled(DeleteEventArgs args) + { + return Deleting.IsRaisedEventCancelled(args, _this); + } + + protected void OnDeleted(DeleteEventArgs args) + { + Deleted.RaiseEvent(args, (TService)(object)this); + } + + public static event TypedEventHandler> Moving; + public static event TypedEventHandler> Moved; + + protected void OnMoving(MoveEventArgs args) + { + Moving.RaiseEvent(args, _this); + } + + protected bool OnMovingCancelled(MoveEventArgs args) + { + return Moving.IsRaisedEventCancelled(args, _this); + } + + protected void OnMoved(MoveEventArgs args) + { + Moved.RaiseEvent(args, _this); + } + + public static event TypedEventHandler> SavingContainer; + public static event TypedEventHandler> SavedContainer; + + protected void OnSavingContainer(SaveEventArgs args) + { + SavingContainer.RaiseEvent(args, _this); + } + + protected bool OnSavingContainerCancelled(SaveEventArgs args) + { + return SavingContainer.IsRaisedEventCancelled(args, _this); + } + + protected void OnSavedContainer(SaveEventArgs args) + { + SavedContainer.RaiseEvent(args, _this); + } + + public static event TypedEventHandler> DeletingContainer; + public static event TypedEventHandler> DeletedContainer; + + protected void OnDeletingContainer(DeleteEventArgs args) + { + DeletingContainer.RaiseEvent(args, _this); + } + + protected bool OnDeletingContainerCancelled(DeleteEventArgs args) + { + return DeletingContainer.IsRaisedEventCancelled(args, _this); + } + + protected void OnDeletedContainer(DeleteEventArgs args) + { + DeletedContainer.RaiseEvent(args, _this); + } + + // for later usage + //public static event TypedEventHandler TxRefreshed; + + //protected void OnTxRefreshed(Change.EventArgs args) + //{ + // TxRefreshed.RaiseEvent(args, this); + //} + } + + internal abstract class ContentTypeServiceBase : ContentTypeServiceBase, IContentTypeServiceBase + where TRepository : IContentTypeRepositoryBase + where TItem : class, IContentTypeComposition + where TService : class, IContentTypeServiceBase + { + protected ContentTypeServiceBase(IDatabaseUnitOfWorkProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory) + : base(provider, logger, eventMessagesFactory) + { } + + protected abstract int[] WriteLockIds { get; } + protected abstract int[] ReadLockIds { get; } + + #region Validation + + public Attempt ValidateComposition(TItem compo) + { + try + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + var repo = uow.CreateRepository(); + uow.ReadLock(ReadLockIds); + ValidateLocked(repo, compo); + uow.Complete(); + } + return Attempt.Succeed(); + } + catch (InvalidCompositionException ex) + { + return Attempt.Fail(ex.PropertyTypeAliases, ex); + } + } + + protected void ValidateLocked(TRepository repository, TItem compositionContentType) + { + // performs business-level validation of the composition + // should ensure that it is absolutely safe to save the composition + + // eg maybe a property has been added, with an alias that's OK (no conflict with ancestors) + // but that cannot be used (conflict with descendants) + + var allContentTypes = repository.GetAll(new int[0]).Cast().ToArray(); + + var compositionAliases = compositionContentType.CompositionAliases(); + var compositions = allContentTypes.Where(x => compositionAliases.Any(y => x.Alias.Equals(y))); + var propertyTypeAliases = compositionContentType.PropertyTypes.Select(x => x.Alias.ToLowerInvariant()).ToArray(); + var indirectReferences = allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == compositionContentType.Id)); + var comparer = new DelegateEqualityComparer((x, y) => x.Id == y.Id, x => x.Id); + var dependencies = new HashSet(compositions, comparer); + var stack = new Stack(); + indirectReferences.ForEach(stack.Push); // push indirect references to a stack, so we can add recursively + while (stack.Count > 0) + { + var indirectReference = stack.Pop(); + dependencies.Add(indirectReference); + // get all compositions for the current indirect reference + var directReferences = indirectReference.ContentTypeComposition; + + foreach (var directReference in directReferences) + { + if (directReference.Id == compositionContentType.Id || directReference.Alias.Equals(compositionContentType.Alias)) continue; + dependencies.Add(directReference); + // a direct reference has compositions of its own - these also need to be taken into account + var directReferenceGraph = directReference.CompositionAliases(); + allContentTypes.Where(x => directReferenceGraph.Any(y => x.Alias.Equals(y, StringComparison.InvariantCultureIgnoreCase))).ForEach(c => dependencies.Add(c)); + } + // recursive lookup of indirect references + allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == indirectReference.Id)).ForEach(stack.Push); + } + + foreach (var dependency in dependencies) + { + if (dependency.Id == compositionContentType.Id) continue; + var contentTypeDependency = allContentTypes.FirstOrDefault(x => x.Alias.Equals(dependency.Alias, StringComparison.InvariantCultureIgnoreCase)); + if (contentTypeDependency == null) continue; + var intersect = contentTypeDependency.PropertyTypes.Select(x => x.Alias.ToLowerInvariant()).Intersect(propertyTypeAliases).ToArray(); + if (intersect.Length == 0) continue; + + throw new InvalidCompositionException(compositionContentType.Alias, intersect.ToArray()); + } + } + + #endregion + + #region Composition + #endregion + + #region Get, Has, Is, Count + + public TItem Get(int id) + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + var repo = uow.CreateRepository(); + uow.ReadLock(ReadLockIds); + var item = repo.Get(id); + uow.Complete(); + return item; + } + } + + public TItem Get(string alias) + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + var repo = uow.CreateRepository(); + uow.ReadLock(ReadLockIds); + var item = repo.Get(alias); + uow.Complete(); + return item; + } + } + + public TItem Get(Guid id) + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + var repo = uow.CreateRepository(); + uow.ReadLock(ReadLockIds); + var item = repo.Get(id); + uow.Complete(); + return item; + } + } + + public IEnumerable GetAll(params int[] ids) + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + var repo = uow.CreateRepository(); + uow.ReadLock(ReadLockIds); + var items = repo.GetAll(ids); + uow.Complete(); + return items; + } + } + + public IEnumerable GetAll(params Guid[] ids) + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + var repo = uow.CreateRepository(); + uow.ReadLock(ReadLockIds); + // IReadRepository is explicitely implemented, need to cast the repo + var items = ((IReadRepository) repo).GetAll(ids); + uow.Complete(); + return items; + } + } + + public IEnumerable GetChildren(int id) + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + var repo = uow.CreateRepository(); + uow.ReadLock(ReadLockIds); + var query = repo.Query.Where(x => x.ParentId == id); + var items = repo.GetByQuery(query); + uow.Complete(); + return items; + } + } + + public IEnumerable GetChildren(Guid id) + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + var repo = uow.CreateRepository(); + uow.ReadLock(ReadLockIds); + var found = Get(id); + if (found == null) return Enumerable.Empty(); + var query = repo.Query.Where(x => x.ParentId == found.Id); + var items = repo.GetByQuery(query); + uow.Complete(); + return items; + } + } + + public bool HasChildren(int id) + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + var repo = uow.CreateRepository(); + uow.ReadLock(ReadLockIds); + var query = repo.Query.Where(x => x.ParentId == id); + var count = repo.Count(query); + uow.Complete(); + return count > 0; + } + } + + public bool HasChildren(Guid id) + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + var repo = uow.CreateRepository(); + uow.ReadLock(ReadLockIds); + var found = Get(id); + if (found == null) return false; + var query = repo.Query.Where(x => x.ParentId == found.Id); + var count = repo.Count(query); + uow.Complete(); + return count > 0; + } + } + + public IEnumerable GetDescendants(int id, bool andSelf) + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + var repo = uow.CreateRepository(); + uow.ReadLock(ReadLockIds); + + var descendants = new List(); + if (andSelf) descendants.Add(repo.Get(id)); + var ids = new Stack(); + ids.Push(id); + + while (ids.Count > 0) + { + var i = ids.Pop(); + var query = repo.Query.Where(x => x.ParentId == i); + var result = repo.GetByQuery(query).ToArray(); + + foreach (var c in result) + { + descendants.Add(c); + ids.Push(c.Id); + } + } + + var descendantsA = descendants.ToArray(); + uow.Complete(); + return descendantsA; + } + } + + public IEnumerable GetComposedOf(int id) + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + var repo = uow.CreateRepository(); + uow.ReadLock(ReadLockIds); + + // hash set handles duplicates + var composed = new HashSet(new DelegateEqualityComparer( + (x, y) => x.Id == y.Id, + x => x.Id.GetHashCode())); + + var ids = new Stack(); + ids.Push(id); + + while (ids.Count > 0) + { + var i = ids.Pop(); + var result = repo.GetTypesDirectlyComposedOf(i).ToArray(); + + foreach (var c in result) + { + composed.Add(c); + ids.Push(c.Id); + } + } + + var composedA = composed.ToArray(); + uow.Complete(); + return composedA; + } + } + + public int Count() + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + var repo = uow.CreateRepository(); + uow.ReadLock(ReadLockIds); + var count = repo.Count(repo.Query); + uow.Complete(); + return count; + } + } + + #endregion + + #region Save + + public void Save(TItem item, int userId = 0) + { + if (OnSavingCancelled(new SaveEventArgs(item))) + return; + + using (var uow = UowProvider.CreateUnitOfWork()) + { + var repo = uow.CreateRepository(); + uow.WriteLock(WriteLockIds); + + // validate the DAG transform, within the lock + ValidateLocked(repo, item); // throws if invalid + + item.CreatorId = userId; + repo.AddOrUpdate(item); // also updates content/media/member items + uow.Flush(); // to db but no commit yet + + // ... + + uow.Complete(); + } + + // todo: should use TxRefreshed event within the transaction instead, see CC branch + UpdateContentXmlStructure(item); + + OnSaved(new SaveEventArgs(item, false)); + Audit(AuditType.Save, $"Save {typeof(TItem).Name} performed by user", userId, item.Id); + } + + public void Save(IEnumerable items, int userId = 0) + { + var itemsA = items.ToArray(); + + if (OnSavingCancelled(new SaveEventArgs(itemsA))) + return; + + using (var uow = UowProvider.CreateUnitOfWork()) + { + var repo = uow.CreateRepository(); + uow.WriteLock(WriteLockIds); + + // all-or-nothing, validate them all first + foreach (var contentType in itemsA) + { + ValidateLocked(repo, contentType); // throws if invalid + } + foreach (var contentType in itemsA) + { + contentType.CreatorId = userId; + repo.AddOrUpdate(contentType); + } + + //save it all in one go + uow.Complete(); + } + + // todo: should use TxRefreshed event within the transaction instead, see CC branch + UpdateContentXmlStructure(itemsA.Cast().ToArray()); + + OnSaved(new SaveEventArgs(itemsA, false)); + Audit(AuditType.Save, $"Save {typeof(TItem).Name} performed by user", userId, -1); + } + + #endregion + + #region Delete + + public void Delete(TItem item, int userId = 0) + { + if (OnDeletingCancelled(new DeleteEventArgs(item))) + return; + + using (var uow = UowProvider.CreateUnitOfWork()) + { + var repo = uow.CreateRepository(); + uow.WriteLock(WriteLockIds); + + // all descendants are going to be deleted + var descendantsAndSelf = item.DescendantsAndSelf() + .ToArray(); + + // delete content + DeleteItemsOfTypes(descendantsAndSelf.Select(x => x.Id)); + + // finally delete the content type + // - recursively deletes all descendants + // - deletes all associated property data + // (contents of any descendant type have been deleted but + // contents of any composed (impacted) type remain but + // need to have their property data cleared) + repo.Delete(item); + uow.Flush(); // to db but no commit yet + + //... + + uow.Complete(); + } + + OnDeleted(new DeleteEventArgs(item, false)); + Audit(AuditType.Delete, $"Delete {typeof(TItem).Name} performed by user", userId, item.Id); + } + + public void Delete(IEnumerable items, int userId = 0) + { + var itemsA = items.ToArray(); + + if (OnDeletingCancelled(new DeleteEventArgs(itemsA))) + return; + + using (var uow = UowProvider.CreateUnitOfWork()) + { + var repo = uow.CreateRepository(); + uow.WriteLock(WriteLockIds); + + // all descendants are going to be deleted + var allDescendantsAndSelf = itemsA.SelectMany(xx => xx.DescendantsAndSelf()) + .Distinct() + .ToArray(); + + // delete content + DeleteItemsOfTypes(allDescendantsAndSelf.Select(x => x.Id)); + + // finally delete the content types + // (see notes in overload) + foreach (var item in itemsA) + repo.Delete(item); + + uow.Flush(); // to db but no commit yet + + // ... + + uow.Complete(); + + } + + OnDeleted(new DeleteEventArgs(itemsA, false)); + Audit(AuditType.Delete, $"Delete {typeof(TItem).Name} performed by user", userId, -1); + } + + protected abstract void DeleteItemsOfTypes(IEnumerable typeIds); + + #endregion + + #region Copy + + public TItem Copy(TItem original, string alias, string name, int parentId = -1) + { + TItem parent = null; + if (parentId > 0) + { + parent = Get(parentId); + if (parent == null) + { + throw new InvalidOperationException("Could not find parent with id " + parentId); + } + } + return Copy(original, alias, name, parent); + } + + public TItem Copy(TItem original, string alias, string name, TItem parent) + { + Mandate.ParameterNotNull(original, "original"); + Mandate.ParameterNotNullOrEmpty(alias, "alias"); + + if (parent != null) + Mandate.That(parent.HasIdentity, () => new InvalidOperationException("The parent must have an identity")); + + // this is illegal + //var originalb = (ContentTypeCompositionBase)original; + // but we *know* it has to be a ContentTypeCompositionBase anyways + var originalb = (ContentTypeCompositionBase) (object) original; + var clone = (TItem) originalb.DeepCloneWithResetIdentities(alias); + + clone.Name = name; + + //remove all composition that is not it's current alias + var compositionAliases = clone.CompositionAliases().Except(new[] { alias }).ToList(); + foreach (var a in compositionAliases) + { + clone.RemoveContentType(a); + } + + //if a parent is specified set it's composition and parent + if (parent != null) + { + //add a new parent composition + clone.AddContentType(parent); + clone.ParentId = parent.Id; + } + else + { + //set to root + clone.ParentId = -1; + } + + Save(clone); + return clone; + } + + public Attempt> Copy(TItem copying, int containerId) + { + var evtMsgs = EventMessagesFactory.Get(); + + TItem copy; + using (var uow = UowProvider.CreateUnitOfWork()) + { + var repo = uow.CreateRepository(); + uow.WriteLock(WriteLockIds); + + var containerRepository = uow.CreateContainerRepository(ContainerObjectType); + try + { + if (containerId > 0) + { + var container = containerRepository.Get(containerId); + if (container == null) + throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); // causes rollback + } + var alias = repo.GetUniqueAlias(copying.Alias); + + // this is illegal + //var copyingb = (ContentTypeCompositionBase) copying; + // but we *know* it has to be a ContentTypeCompositionBase anyways + var copyingb = (ContentTypeCompositionBase) (object)copying; + copy = (TItem) copyingb.DeepCloneWithResetIdentities(alias); + + copy.Name = copy.Name + " (copy)"; // might not be unique + + // if it has a parent, and the parent is a content type, unplug composition + // all other compositions remain in place in the copied content type + if (copy.ParentId > 0) + { + var parent = repo.Get(copy.ParentId); + if (parent != null) + copy.RemoveContentType(parent.Alias); + } + + copy.ParentId = containerId; + repo.AddOrUpdate(copy); + uow.Complete(); + } + catch (DataOperationException ex) + { + return OperationStatus.Attempt.Fail(ex.Operation, evtMsgs); // causes rollback + } + } + + return OperationStatus.Attempt.Succeed(MoveOperationStatusType.Success, evtMsgs, copy); + } + + #endregion + + #region Move + + public Attempt> Move(TItem moving, int containerId) + { + var evtMsgs = EventMessagesFactory.Get(); + + if (OnMovingCancelled(new MoveEventArgs(evtMsgs, new MoveEventInfo(moving, moving.Path, containerId)))) + return OperationStatus.Attempt.Fail(MoveOperationStatusType.FailedCancelledByEvent, evtMsgs); + + var moveInfo = new List>(); + using (var uow = UowProvider.CreateUnitOfWork()) + { + uow.WriteLock(WriteLockIds); // also for containers + + var repo = uow.CreateRepository(); + var containerRepo = uow.CreateRepository(); + + try + { + EntityContainer container = null; + if (containerId > 0) + { + container = containerRepo.Get(containerId); + if (container == null) + throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); // causes rollback + } + moveInfo.AddRange(repo.Move(moving, container)); + uow.Complete(); + } + catch (DataOperationException ex) + { + return OperationStatus.Attempt.Fail(ex.Operation, evtMsgs); // causes rollback + } + } + + OnMoved(new MoveEventArgs(false, evtMsgs, moveInfo.ToArray())); + + return OperationStatus.Attempt.Succeed(MoveOperationStatusType.Success, evtMsgs); + } + + #endregion + + #region Containers + + protected abstract Guid ContainedObjectType { get; } + + protected Guid ContainerObjectType => EntityContainer.GetContainerObjectType(ContainedObjectType); + + public Attempt> CreateContainer(int parentId, string name, int userId = 0) + { + var evtMsgs = EventMessagesFactory.Get(); + using (var uow = UowProvider.CreateUnitOfWork()) + { + uow.WriteLock(WriteLockIds); // also for containers + + var repo = uow.CreateContainerRepository(ContainerObjectType); + try + { + var container = new EntityContainer(Constants.ObjectTypes.DocumentTypeGuid) + { + Name = name, + ParentId = parentId, + CreatorId = userId + }; + + if (OnSavingContainerCancelled(new SaveEventArgs(container, evtMsgs))) + return OperationStatus.Attempt.Cancel(evtMsgs, container); // causes rollback + + repo.AddOrUpdate(container); + uow.Complete(); + + OnSavedContainer(new SaveEventArgs(container, evtMsgs)); + //TODO: Audit trail ? + + return OperationStatus.Attempt.Succeed(evtMsgs, container); + } + catch (Exception ex) + { + return OperationStatus.Attempt.Fail(OperationStatusType.FailedCancelledByEvent, evtMsgs, ex); + } + } + } + + public Attempt SaveContainer(EntityContainer container, int userId = 0) + { + var evtMsgs = EventMessagesFactory.Get(); + + var containerObjectType = ContainerObjectType; + if (container.ContainedObjectType != containerObjectType) + { + var ex = new InvalidOperationException("Not a container of the proper type."); + return OperationStatus.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 OperationStatus.Attempt.Fail(evtMsgs, ex); + } + + if (OnSavingContainerCancelled(new SaveEventArgs(container, evtMsgs))) + return OperationStatus.Attempt.Cancel(evtMsgs); + + using (var uow = UowProvider.CreateUnitOfWork()) + { + uow.WriteLock(WriteLockIds); // also for containers + + var repo = uow.CreateContainerRepository(containerObjectType); + repo.AddOrUpdate(container); + uow.Complete(); + } + + OnSavedContainer(new SaveEventArgs(container, evtMsgs)); + + //TODO: Audit trail ? + + return OperationStatus.Attempt.Succeed(evtMsgs); + } + + public EntityContainer GetContainer(int containerId) + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + uow.ReadLock(ReadLockIds); // also for containers + + var repo = uow.CreateContainerRepository(ContainerObjectType); + var container = repo.Get(containerId); + uow.Complete(); + return container; + } + } + + public EntityContainer GetContainer(Guid containerId) + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + uow.ReadLock(ReadLockIds); // also for containers + + var repo = uow.CreateContainerRepository(ContainerObjectType); + var container = ((EntityContainerRepository) repo).Get(containerId); + uow.Complete(); + return container; + } + } + + public IEnumerable GetContainers(int[] containerIds) + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + uow.ReadLock(ReadLockIds); // also for containers + + var repo = uow.CreateContainerRepository(ContainerObjectType); + var containers = repo.GetAll(containerIds); + uow.Complete(); + return containers; + } + } + + public IEnumerable GetContainers(TItem item) + { + var ancestorIds = item.Path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(x => + { + var asInt = x.TryConvertTo(); + return asInt ? asInt.Result : int.MinValue; + }) + .Where(x => x != int.MinValue && x != item.Id) + .ToArray(); + + return GetContainers(ancestorIds); + } + + public IEnumerable GetContainers(string name, int level) + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + uow.ReadLock(ReadLockIds); // also for containers + + var repo = uow.CreateContainerRepository(ContainerObjectType); + var containers = ((EntityContainerRepository) repo).Get(name, level); + uow.Complete(); + return containers; + } + } + + // fixme - what happens if deleting a non-empty container? + public Attempt DeleteContainer(int containerId, int userId = 0) + { + var evtMsgs = EventMessagesFactory.Get(); + using (var uow = UowProvider.CreateUnitOfWork()) + { + uow.WriteLock(WriteLockIds); // also for containers + + var repo = uow.CreateContainerRepository(ContainerObjectType); + var container = repo.Get(containerId); + if (container == null) return OperationStatus.Attempt.NoOperation(evtMsgs); + + if (OnDeletingContainerCancelled(new DeleteEventArgs(container, evtMsgs))) + return Attempt.Fail(new OperationStatus(OperationStatusType.FailedCancelledByEvent, evtMsgs)); // causes rollback + + repo.Delete(container); + uow.Complete(); + + OnDeletedContainer(new DeleteEventArgs(container, evtMsgs)); + + return OperationStatus.Attempt.Succeed(evtMsgs); + //TODO: Audit trail ? + } + } + + #endregion + + #region Audit + + private void Audit(AuditType type, string message, int userId, int objectId) + { + using (var uow = UowProvider.CreateUnitOfWork()) + { + var repo = uow.CreateRepository(); + repo.AddOrUpdate(new AuditItem(objectId, message, type, userId)); + uow.Complete(); + } + } + + #endregion + + #region Xml - Should Move! + + protected abstract void UpdateContentXmlStructure(params IContentTypeBase[] contentTypes); + + #endregion + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/DataTypeService.cs b/src/Umbraco.Core/Services/DataTypeService.cs index f17a245d38..754f26762e 100644 --- a/src/Umbraco.Core/Services/DataTypeService.cs +++ b/src/Umbraco.Core/Services/DataTypeService.cs @@ -28,7 +28,7 @@ namespace Umbraco.Core.Services #region Containers - public Attempt> CreateContainer(int parentId, string name, int userId = 0) + public Attempt> CreateContainer(int parentId, string name, int userId = 0) { var evtMsgs = EventMessagesFactory.Get(); using (var uow = UowProvider.CreateUnitOfWork()) @@ -43,12 +43,8 @@ namespace Umbraco.Core.Services CreatorId = userId }; - if (SavingContainer.IsRaisedEventCancelled( - new SaveEventArgs(container, evtMsgs), - this)) - { - return Attempt.Fail(new OperationStatus(container, OperationStatusType.FailedCancelledByEvent, evtMsgs)); - } + if (SavingContainer.IsRaisedEventCancelled(new SaveEventArgs(container, evtMsgs), this)) + return OperationStatus.Attempt.Cancel(evtMsgs, container); // causes rollback repo.AddOrUpdate(container); uow.Complete(); @@ -56,11 +52,11 @@ namespace Umbraco.Core.Services SavedContainer.RaiseEvent(new SaveEventArgs(container, evtMsgs), this); //TODO: Audit trail ? - return Attempt.Succeed(new OperationStatus(container, OperationStatusType.Success, evtMsgs)); + return OperationStatus.Attempt.Succeed(evtMsgs, container); } catch (Exception ex) { - return Attempt.Fail(new OperationStatus(null, OperationStatusType.FailedExceptionThrown, evtMsgs), ex); + return OperationStatus.Attempt.Fail(evtMsgs, ex); } } } @@ -71,6 +67,7 @@ namespace Umbraco.Core.Services { var repo = uow.CreateRepository(); var container = repo.Get(containerId); + uow.Complete(); return container; } } @@ -81,6 +78,7 @@ namespace Umbraco.Core.Services { var repo = uow.CreateRepository(); var container = ((EntityContainerRepository)repo).Get(containerId); + uow.Complete(); return container; } } @@ -90,7 +88,9 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork()) { var repo = uow.CreateRepository(); - return ((EntityContainerRepository)repo).Get(name, level); + var containers = ((EntityContainerRepository)repo).Get(name, level); + uow.Complete(); + return containers; } } @@ -114,7 +114,9 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork()) { var repo = uow.CreateRepository(); - return repo.GetAll(containerIds); + var containers = repo.GetAll(containerIds); + uow.Complete(); + return containers; } } @@ -125,20 +127,20 @@ namespace Umbraco.Core.Services if (container.ContainedObjectType != Constants.ObjectTypes.DataTypeGuid) { var ex = new InvalidOperationException("Not a " + Constants.ObjectTypes.DataTypeGuid + " container."); - return OperationStatus.Exception(evtMsgs, ex); + return OperationStatus.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 OperationStatus.Exception(evtMsgs, ex); + return OperationStatus.Attempt.Fail(evtMsgs, ex); } if (SavingContainer.IsRaisedEventCancelled( new SaveEventArgs(container, evtMsgs), this)) { - return OperationStatus.Cancelled(evtMsgs); + return OperationStatus.Attempt.Cancel(evtMsgs); } using (var uow = UowProvider.CreateUnitOfWork()) @@ -152,7 +154,7 @@ namespace Umbraco.Core.Services //TODO: Audit trail ? - return OperationStatus.Success(evtMsgs); + return OperationStatus.Attempt.Succeed(evtMsgs); } public Attempt DeleteContainer(int containerId, int userId = 0) @@ -162,21 +164,17 @@ namespace Umbraco.Core.Services { var repo = uow.CreateRepository(); var container = repo.Get(containerId); - if (container == null) return OperationStatus.NoOperation(evtMsgs); + if (container == null) return OperationStatus.Attempt.NoOperation(evtMsgs); - if (DeletingContainer.IsRaisedEventCancelled( - new DeleteEventArgs(container, evtMsgs), - this)) - { - return Attempt.Fail(new OperationStatus(OperationStatusType.FailedCancelledByEvent, evtMsgs)); - } + if (DeletingContainer.IsRaisedEventCancelled(new DeleteEventArgs(container, evtMsgs), this)) + return Attempt.Fail(new OperationStatus(OperationStatusType.FailedCancelledByEvent, evtMsgs)); // causes rollback repo.Delete(container); uow.Complete(); DeletedContainer.RaiseEvent(new DeleteEventArgs(container, evtMsgs), this); - return OperationStatus.Success(evtMsgs); + return OperationStatus.Attempt.Succeed(evtMsgs); //TODO: Audit trail ? } } @@ -193,7 +191,9 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork()) { var repository = uow.CreateRepository(); - return repository.GetByQuery(repository.Query.Where(x => x.Name == name)).FirstOrDefault(); + var def = repository.GetByQuery(repository.Query.Where(x => x.Name == name)).FirstOrDefault(); + uow.Complete(); + return def; } } @@ -207,7 +207,9 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork()) { var repository = uow.CreateRepository(); - return repository.Get(id); + var def = repository.Get(id); + uow.Complete(); + return def; } } @@ -222,8 +224,9 @@ namespace Umbraco.Core.Services { var repository = uow.CreateRepository(); var query = repository.Query.Where(x => x.Key == id); - var definitions = repository.GetByQuery(query); - return definitions.FirstOrDefault(); + var definition = repository.GetByQuery(query).FirstOrDefault(); + uow.Complete(); + return definition; } } @@ -239,6 +242,7 @@ namespace Umbraco.Core.Services var repository = uow.CreateRepository(); var query = repository.Query.Where(x => x.PropertyEditorAlias == propertyEditorAlias); var definitions = repository.GetByQuery(query); + uow.Complete(); return definitions; } } @@ -253,7 +257,9 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork()) { var repository = uow.CreateRepository(); - return repository.GetAll(ids); + var defs = repository.GetAll(ids); + uow.Complete(); + return defs; } } @@ -272,6 +278,7 @@ namespace Umbraco.Core.Services var list = collection.FormatAsDictionary() .Select(x => x.Value.Value) .ToList(); + uow.Complete(); return list; } } @@ -286,7 +293,9 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork()) { var repository = uow.CreateRepository(); - return repository.GetPreValuesCollectionByDataTypeId(id); + var vals = repository.GetPreValuesCollectionByDataTypeId(id); + uow.Complete(); + return vals; } } @@ -300,7 +309,9 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork()) { var repository = uow.CreateRepository(); - return repository.GetPreValueAsString(id); + var val = repository.GetPreValueAsString(id); + uow.Complete(); + return val; } } @@ -312,9 +323,7 @@ namespace Umbraco.Core.Services new MoveEventArgs(evtMsgs, new MoveEventInfo(toMove, toMove.Path, parentId)), this)) { - return Attempt.Fail( - new OperationStatus( - MoveOperationStatusType.FailedCancelledByEvent, evtMsgs)); + return OperationStatus.Attempt.Fail(MoveOperationStatusType.FailedCancelledByEvent, evtMsgs); } var moveInfo = new List>(); @@ -330,22 +339,20 @@ namespace Umbraco.Core.Services { container = containerRepository.Get(parentId); if (container == null) - throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); + throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); // causes rollback } moveInfo.AddRange(repository.Move(toMove, container)); + uow.Complete(); } catch (DataOperationException ex) { - return Attempt.Fail( - new OperationStatus(ex.Operation, evtMsgs)); + return OperationStatus.Attempt.Fail(ex.Operation, evtMsgs); } - uow.Complete(); } Moved.RaiseEvent(new MoveEventArgs(false, evtMsgs, moveInfo.ToArray()), this); - return Attempt.Succeed( - new OperationStatus(MoveOperationStatusType.Success, evtMsgs)); + return OperationStatus.Attempt.Succeed(MoveOperationStatusType.Success, evtMsgs); } /// @@ -424,26 +431,21 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork()) { - using (var transaction = uow.Database.GetTransaction()) + var sortOrderObj = uow.Database.ExecuteScalar( + "SELECT max(sortorder) FROM cmsDataTypePreValues WHERE datatypeNodeId = @DataTypeId", new { DataTypeId = dataTypeId }); + + int sortOrder; + if (sortOrderObj == null || int.TryParse(sortOrderObj.ToString(), out sortOrder) == false) + sortOrder = 1; + + foreach (var value in values) { - var sortOrderObj = - uow.Database.ExecuteScalar( - "SELECT max(sortorder) FROM cmsDataTypePreValues WHERE datatypeNodeId = @DataTypeId", new { DataTypeId = dataTypeId }); - int sortOrder; - if (sortOrderObj == null || int.TryParse(sortOrderObj.ToString(), out sortOrder) == false) - { - sortOrder = 1; - } - - foreach (var value in values) - { - var dto = new DataTypePreValueDto { DataTypeNodeId = dataTypeId, Value = value, SortOrder = sortOrder }; - uow.Database.Insert(dto); - sortOrder++; - } - - transaction.Complete(); + var dto = new DataTypePreValueDto { DataTypeNodeId = dataTypeId, Value = value, SortOrder = sortOrder }; + uow.Database.Insert(dto); + sortOrder++; } + + uow.Complete(); } } diff --git a/src/Umbraco.Core/Services/DomainService.cs b/src/Umbraco.Core/Services/DomainService.cs index 9100fb9511..ca0184b259 100644 --- a/src/Umbraco.Core/Services/DomainService.cs +++ b/src/Umbraco.Core/Services/DomainService.cs @@ -23,7 +23,9 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork()) { var repo = uow.CreateRepository(); - return repo.Exists(domainName); + var exists = repo.Exists(domainName); + uow.Complete(); + return exists; } } @@ -34,7 +36,7 @@ namespace Umbraco.Core.Services new DeleteEventArgs(domain, evtMsgs), this)) { - return OperationStatus.Cancelled(evtMsgs); + return OperationStatus.Attempt.Cancel(evtMsgs); } using (var uow = UowProvider.CreateUnitOfWork()) @@ -46,7 +48,7 @@ namespace Umbraco.Core.Services var args = new DeleteEventArgs(domain, false, evtMsgs); Deleted.RaiseEvent(args, this); - return OperationStatus.Success(evtMsgs); + return OperationStatus.Attempt.Succeed(evtMsgs); } public IDomain GetByName(string name) @@ -54,7 +56,9 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork()) { var repository = uow.CreateRepository(); - return repository.GetByName(name); + var domain = repository.GetByName(name); + uow.Complete(); + return domain; } } @@ -63,7 +67,9 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork()) { var repo = uow.CreateRepository(); - return repo.Get(id); + var domain = repo.Get(id); + uow.Complete(); + return domain; } } @@ -72,7 +78,9 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork()) { var repo = uow.CreateRepository(); - return repo.GetAll(includeWildcards); + var domains = repo.GetAll(includeWildcards); + uow.Complete(); + return domains; } } @@ -81,7 +89,9 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork()) { var repo = uow.CreateRepository(); - return repo.GetAssignedDomains(contentId, includeWildcards); + var domains = repo.GetAssignedDomains(contentId, includeWildcards); + uow.Complete(); + return domains; } } @@ -92,7 +102,7 @@ namespace Umbraco.Core.Services new SaveEventArgs(domainEntity, evtMsgs), this)) { - return OperationStatus.Cancelled(evtMsgs); + return OperationStatus.Attempt.Cancel(evtMsgs); } using (var uow = UowProvider.CreateUnitOfWork()) @@ -103,7 +113,7 @@ namespace Umbraco.Core.Services } Saved.RaiseEvent(new SaveEventArgs(domainEntity, false, evtMsgs), this); - return OperationStatus.Success(evtMsgs); + return OperationStatus.Attempt.Succeed(evtMsgs); } #region Event Handlers diff --git a/src/Umbraco.Core/Services/EntityService.cs b/src/Umbraco.Core/Services/EntityService.cs index e18e9056f9..2154241524 100644 --- a/src/Umbraco.Core/Services/EntityService.cs +++ b/src/Umbraco.Core/Services/EntityService.cs @@ -23,20 +23,22 @@ namespace Umbraco.Core.Services public EntityService(IDatabaseUnitOfWorkProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory, - IContentService contentService, IContentTypeService contentTypeService, IMediaService mediaService, IDataTypeService dataTypeService, - IMemberService memberService, IMemberTypeService memberTypeService, IRuntimeCacheProvider runtimeCache) + IContentService contentService, IContentTypeService contentTypeService, + IMediaService mediaService, IMediaTypeService mediaTypeService, + IDataTypeService dataTypeService, + IMemberService memberService, IMemberTypeService memberTypeService, + IRuntimeCacheProvider runtimeCache) : base(provider, logger, eventMessagesFactory) { _runtimeCache = runtimeCache; - IContentTypeService contentTypeService1 = contentTypeService; _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, contentTypeService1.GetContentType)}, + {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, contentTypeService1.GetMediaType)}, + {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)}, //{typeof (IUmbracoEntity).FullName, new Tuple>(UmbracoObjectTypes.EntityContainer, id => @@ -74,6 +76,7 @@ namespace Umbraco.Core.Services { var result = _runtimeCache.GetCacheItem(CacheKeys.IdToKeyCacheKey + key, () => { + int? id; using (var uow = UowProvider.CreateUnitOfWork()) { switch (umbracoObjectType) @@ -87,11 +90,12 @@ namespace Umbraco.Core.Services case UmbracoObjectTypes.Member: case UmbracoObjectTypes.DataType: case UmbracoObjectTypes.DocumentTypeContainer: - return uow.Database.ExecuteScalar( + id = uow.Database.ExecuteScalar( uow.Database.Sql() .Select("id") .From() .Where(dto => dto.UniqueId == key)); + break; case UmbracoObjectTypes.RecycleBin: case UmbracoObjectTypes.Stylesheet: case UmbracoObjectTypes.MemberGroup: @@ -102,6 +106,8 @@ namespace Umbraco.Core.Services default: throw new NotSupportedException(); } + uow.Complete(); + return id; } }); return result.HasValue ? Attempt.Succeed(result.Value) : Attempt.Fail(); @@ -119,6 +125,7 @@ namespace Umbraco.Core.Services { using (var uow = UowProvider.CreateUnitOfWork()) { + Guid? guid; switch (umbracoObjectType) { case UmbracoObjectTypes.Document: @@ -129,11 +136,12 @@ namespace Umbraco.Core.Services case UmbracoObjectTypes.DocumentType: case UmbracoObjectTypes.Member: case UmbracoObjectTypes.DataType: - return uow.Database.ExecuteScalar( + guid = uow.Database.ExecuteScalar( uow.Database.Sql() .Select("uniqueID") .From() .Where(dto => dto.NodeId == id)); + break; case UmbracoObjectTypes.RecycleBin: case UmbracoObjectTypes.Stylesheet: case UmbracoObjectTypes.MemberGroup: @@ -144,6 +152,8 @@ namespace Umbraco.Core.Services default: throw new NotSupportedException(); } + uow.Complete(); + return guid; } }); return result.HasValue ? Attempt.Succeed(result.Value) : Attempt.Fail(); @@ -156,7 +166,9 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork()) { var repository = uow.CreateRepository(); - return repository.GetByKey(key); + var entity = repository.GetByKey(key); + uow.Complete(); + return entity; } } @@ -187,7 +199,9 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork()) { var repository = uow.CreateRepository(); - return repository.Get(id); + var e = repository.Get(id); + uow.Complete(); + return e; } } @@ -207,7 +221,9 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork()) { var repository = uow.CreateRepository(); - return repository.GetByKey(key, objectTypeId); + var entity = repository.GetByKey(key, objectTypeId); + uow.Complete(); + return entity; } } @@ -239,7 +255,9 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork()) { var repository = uow.CreateRepository(); - return repository.Get(id, objectTypeId); + var e = repository.Get(id, objectTypeId); + uow.Complete(); + return e; } } @@ -272,7 +290,9 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork()) { var repository = uow.CreateRepository(); - return repository.Get(id); + var e = repository.Get(id); + uow.Complete(); + return e; } } @@ -301,7 +321,9 @@ namespace Umbraco.Core.Services if (entity.ParentId == -1 || entity.ParentId == -20 || entity.ParentId == -21) return null; - return repository.Get(entity.ParentId); + var e = repository.Get(entity.ParentId); + uow.Complete(); + return e; } } @@ -321,7 +343,9 @@ namespace Umbraco.Core.Services return null; var objectTypeId = umbracoObjectType.GetGuid(); - return repository.Get(entity.ParentId, objectTypeId); + var e = repository.Get(entity.ParentId, objectTypeId); + uow.Complete(); + return e; } } @@ -337,7 +361,7 @@ namespace Umbraco.Core.Services var repository = uow.CreateRepository(); var query = repository.Query.Where(x => x.ParentId == parentId); var contents = repository.GetByQuery(query); - + uow.Complete(); return contents; } } @@ -356,7 +380,7 @@ namespace Umbraco.Core.Services var repository = uow.CreateRepository(); var query = repository.Query.Where(x => x.ParentId == parentId); var contents = repository.GetByQuery(query, objectTypeId).ToList(); // run within using! - + uow.Complete(); return contents; } } @@ -375,7 +399,7 @@ namespace Umbraco.Core.Services var pathMatch = entity.Path + ","; var query = repository.Query.Where(x => x.Path.StartsWith(pathMatch) && x.Id != id); var entities = repository.GetByQuery(query); - + uow.Complete(); return entities; } } @@ -395,7 +419,7 @@ namespace Umbraco.Core.Services var entity = repository.Get(id); var query = repository.Query.Where(x => x.Path.StartsWith(entity.Path) && x.Id != id); var entities = repository.GetByQuery(query, objectTypeId); - + uow.Complete(); return entities; } } @@ -413,7 +437,7 @@ namespace Umbraco.Core.Services var repository = uow.CreateRepository(); var query = repository.Query.Where(x => x.ParentId == -1); var entities = repository.GetByQuery(query, objectTypeId); - + uow.Complete(); return entities; } } @@ -456,7 +480,9 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork()) { var repository = uow.CreateRepository(); - return repository.GetAll(objectTypeId, ids); + var entities = repository.GetAll(objectTypeId, ids); + uow.Complete(); + return entities; } } @@ -474,7 +500,9 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork()) { var repository = uow.CreateRepository(); - return repository.GetAll(objectTypeId, keys); + var entities = repository.GetAll(objectTypeId, keys); + uow.Complete(); + return entities; } } @@ -498,7 +526,9 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork()) { var repository = uow.CreateRepository(); - return repository.GetAll(objectTypeId, ids); + var entities = repository.GetAll(objectTypeId, ids); + uow.Complete(); + return entities; } } @@ -517,7 +547,9 @@ namespace Umbraco.Core.Services .Where(x => x.NodeId == id); var nodeObjectTypeId = uow.Database.ExecuteScalar(sql); var objectTypeId = nodeObjectTypeId; - return UmbracoObjectTypesExtensions.GetUmbracoObjectType(objectTypeId); + var t = UmbracoObjectTypesExtensions.GetUmbracoObjectType(objectTypeId); + uow.Complete(); + return t; } } @@ -536,7 +568,9 @@ namespace Umbraco.Core.Services .Where(x => x.UniqueId == key); var nodeObjectTypeId = uow.Database.ExecuteScalar(sql); var objectTypeId = nodeObjectTypeId; - return UmbracoObjectTypesExtensions.GetUmbracoObjectType(objectTypeId); + var t = UmbracoObjectTypesExtensions.GetUmbracoObjectType(objectTypeId); + uow.Complete(); + return t; } } diff --git a/src/Umbraco.Core/Services/EntityXmlSerializer.cs b/src/Umbraco.Core/Services/EntityXmlSerializer.cs index 7449e890ad..a290774829 100644 --- a/src/Umbraco.Core/Services/EntityXmlSerializer.cs +++ b/src/Umbraco.Core/Services/EntityXmlSerializer.cs @@ -446,7 +446,7 @@ namespace Umbraco.Core.Services if (contentType.Level != 1 && masterContentType == null) { //get url encoded folder names - var folders = contentTypeService.GetContentTypeContainers(contentType) + var folders = contentTypeService.GetContainers(contentType) .OrderBy(x => x.Level) .Select(x => HttpUtility.UrlEncode(x.Name)); diff --git a/src/Umbraco.Core/Services/ExternalLoginService.cs b/src/Umbraco.Core/Services/ExternalLoginService.cs index c97ec85717..c38daafc0f 100644 --- a/src/Umbraco.Core/Services/ExternalLoginService.cs +++ b/src/Umbraco.Core/Services/ExternalLoginService.cs @@ -25,7 +25,9 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork()) { var repo = uow.CreateRepository(); - return repo.GetByQuery(repo.Query.Where(x => x.UserId == userId)); + var ident = repo.GetByQuery(repo.Query.Where(x => x.UserId == userId)); + uow.Complete(); + return ident; } } @@ -40,8 +42,10 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork()) { var repo = uow.CreateRepository(); - return repo.GetByQuery(repo.Query + var idents = repo.GetByQuery(repo.Query .Where(x => x.ProviderKey == login.ProviderKey && x.LoginProvider == login.LoginProvider)); + uow.Complete(); + return idents; } } diff --git a/src/Umbraco.Core/Services/FileService.cs b/src/Umbraco.Core/Services/FileService.cs index 1368908dbd..2395f719ae 100644 --- a/src/Umbraco.Core/Services/FileService.cs +++ b/src/Umbraco.Core/Services/FileService.cs @@ -51,7 +51,9 @@ namespace Umbraco.Core.Services using (var uow = _fileUowProvider.CreateUnitOfWork()) { var repository = uow.CreateRepository(); - return repository.GetAll(names); + var stylesheets = repository.GetAll(names); + uow.Complete(); + return stylesheets; } } @@ -65,7 +67,9 @@ namespace Umbraco.Core.Services using (var uow = _fileUowProvider.CreateUnitOfWork()) { var repository = uow.CreateRepository(); - return repository.Get(name); + var stylesheet = repository.Get(name); + uow.Complete(); + return stylesheet; } } @@ -102,10 +106,14 @@ namespace Umbraco.Core.Services { var repository = uow.CreateRepository(); stylesheet = repository.Get(path); - if (stylesheet == null) return; + if (stylesheet == null) + { + uow.Complete(); + return; + } if (DeletingStylesheet.IsRaisedEventCancelled(new DeleteEventArgs(stylesheet), this)) - return; + return; // causes rollback repository.Delete(stylesheet); uow.Complete(); @@ -125,7 +133,9 @@ namespace Umbraco.Core.Services using (var uow = _fileUowProvider.CreateUnitOfWork()) { var repository = uow.CreateRepository(); - return repository.ValidateStylesheet(stylesheet); + var valid = repository.ValidateStylesheet(stylesheet); + uow.Complete(); + return valid; } } @@ -141,7 +151,9 @@ namespace Umbraco.Core.Services using (var uow = _fileUowProvider.CreateUnitOfWork()) { var repository = uow.CreateRepository(); - return repository.GetAll(names); + var scripts = repository.GetAll(names); + uow.Complete(); + return scripts; } } @@ -155,7 +167,9 @@ namespace Umbraco.Core.Services using (var uow = _fileUowProvider.CreateUnitOfWork()) { var repository = uow.CreateRepository(); - return repository.Get(name); + var script = repository.Get(name); + uow.Complete(); + return script; } } @@ -193,10 +207,14 @@ namespace Umbraco.Core.Services { var repository = uow.CreateRepository(); script = repository.Get(path); - if (script == null) return; + if (script == null) + { + uow.Complete(); + return; + } if (DeletingScript.IsRaisedEventCancelled(new DeleteEventArgs diff --git a/src/Umbraco.Web.UI/umbraco/dialogs/ChangeDocType.aspx.cs b/src/Umbraco.Web.UI/umbraco/dialogs/ChangeDocType.aspx.cs index 44db417dab..7cead27241 100644 --- a/src/Umbraco.Web.UI/umbraco/dialogs/ChangeDocType.aspx.cs +++ b/src/Umbraco.Web.UI/umbraco/dialogs/ChangeDocType.aspx.cs @@ -66,7 +66,7 @@ namespace Umbraco.Web.UI.Umbraco.Dialogs private bool PopulateListOfValidAlternateDocumentTypes() { // Start with all content types - var documentTypes = Services.ContentTypeService.GetAllContentTypes().ToArray(); + var documentTypes = Services.ContentTypeService.GetAll().ToArray(); // Remove invalid ones from list of potential alternatives documentTypes = RemoveCurrentDocumentTypeFromAlternatives(documentTypes).ToArray(); @@ -189,7 +189,7 @@ namespace Umbraco.Web.UI.Umbraco.Dialogs private IContentType GetSelectedDocumentType() { - return Services.ContentTypeService.GetContentType(int.Parse(NewDocumentTypeList.SelectedItem.Value)); + return Services.ContentTypeService.Get(int.Parse(NewDocumentTypeList.SelectedItem.Value)); } private IEnumerable GetPropertiesOfContentType(IContentType contentType) diff --git a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs index b38fab829f..1e63c67e3e 100644 --- a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs +++ b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs @@ -67,10 +67,10 @@ namespace Umbraco.Web.Cache //Bind to content type events - ContentTypeService.SavedContentType += ContentTypeServiceSavedContentType; - ContentTypeService.SavedMediaType += ContentTypeServiceSavedMediaType; - ContentTypeService.DeletedContentType += ContentTypeServiceDeletedContentType; - ContentTypeService.DeletedMediaType += ContentTypeServiceDeletedMediaType; + ContentTypeService.Saved += ContentTypeServiceSavedContentType; + MediaTypeService.Saved += ContentTypeServiceSavedMediaType; + ContentTypeService.Deleted += ContentTypeServiceDeletedContentType; + MediaTypeService.Deleted += ContentTypeServiceDeletedMediaType; MemberTypeService.Saved += MemberTypeServiceSaved; MemberTypeService.Deleted += MemberTypeServiceDeleted; @@ -113,8 +113,8 @@ namespace Umbraco.Web.Cache ContentService.Trashed += ContentServiceTrashed; ContentService.EmptiedRecycleBin += ContentServiceEmptiedRecycleBin; - PublishingStrategy.Published += PublishingStrategy_Published; - PublishingStrategy.UnPublished += PublishingStrategy_UnPublished; + ContentService.Published += ContentService_Published; + ContentService.UnPublished += ContentService_UnPublished; //public access events PublicAccessService.Saved += PublicAccessService_Saved; @@ -123,7 +123,7 @@ namespace Umbraco.Web.Cache #region Publishing - void PublishingStrategy_UnPublished(IPublishingStrategy sender, PublishEventArgs e) + void ContentService_UnPublished(IContentService sender, PublishEventArgs e) { if (e.PublishedEntities.Any()) { @@ -150,7 +150,7 @@ namespace Umbraco.Web.Cache DistributedCache.Instance.RemovePageCache(content); } - void PublishingStrategy_Published(IPublishingStrategy sender, PublishEventArgs e) + void ContentService_Published(IContentService sender, PublishEventArgs e) { if (e.PublishedEntities.Any()) { @@ -439,7 +439,7 @@ namespace Umbraco.Web.Cache /// /// /// - static void ContentTypeServiceDeletedMediaType(IContentTypeService sender, DeleteEventArgs e) + static void ContentTypeServiceDeletedMediaType(IMediaTypeService sender, DeleteEventArgs e) { e.DeletedEntities.ForEach(x => DistributedCache.Instance.RemoveMediaTypeCache(x)); } @@ -469,7 +469,7 @@ namespace Umbraco.Web.Cache /// /// /// - static void ContentTypeServiceSavedMediaType(IContentTypeService sender, SaveEventArgs e) + static void ContentTypeServiceSavedMediaType(IMediaTypeService sender, SaveEventArgs e) { e.SavedEntities.ForEach(x => DistributedCache.Instance.RefreshMediaTypeCache(x)); } diff --git a/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs b/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs index 44a6efe9ff..a95eef157d 100644 --- a/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs @@ -300,7 +300,7 @@ namespace Umbraco.Web.Cache ClearContentTypeCache( ids.Select( x => - ApplicationContext.Current.Services.ContentTypeService.GetContentType(x) as IContentTypeBase) + ApplicationContext.Current.Services.ContentTypeService.Get(x) as IContentTypeBase) .WhereNotNull() .Select(x => FromContentType(x, isDeleted)) .ToArray()); diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index d1604f6a8e..93407bcbbd 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -144,7 +144,7 @@ namespace Umbraco.Web.Editors [OutgoingEditorModelEvent] public ContentItemDisplay GetEmpty(string contentTypeAlias, int parentId) { - var contentType = Services.ContentTypeService.GetContentType(contentTypeAlias); + var contentType = Services.ContentTypeService.Get(contentTypeAlias); if (contentType == null) { throw new HttpResponseException(HttpStatusCode.NotFound); diff --git a/src/Umbraco.Web/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs index 45a2821130..70f8ca249c 100644 --- a/src/Umbraco.Web/Editors/ContentTypeController.cs +++ b/src/Umbraco.Web/Editors/ContentTypeController.cs @@ -34,7 +34,7 @@ namespace Umbraco.Web.Editors [PluginController("UmbracoApi")] [UmbracoTreeAuthorize(Constants.Trees.DocumentTypes)] [EnableOverrideAuthorization] - public class ContentTypeController : ContentTypeControllerBase + public class ContentTypeController : ContentTypeControllerBase { /// /// Constructor @@ -55,12 +55,12 @@ namespace Umbraco.Web.Editors public int GetCount() { - return Services.ContentTypeService.CountContentTypes(); + return Services.ContentTypeService.Count(); } public DocumentTypeDisplay GetById(int id) { - var ct = Services.ContentTypeService.GetContentType(id); + var ct = Services.ContentTypeService.Get(id); if (ct == null) { throw new HttpResponseException(HttpStatusCode.NotFound); @@ -79,7 +79,7 @@ namespace Umbraco.Web.Editors [HttpPost] public HttpResponseMessage DeleteById(int id) { - var foundType = Services.ContentTypeService.GetContentType(id); + var foundType = Services.ContentTypeService.Get(id); if (foundType == null) { throw new HttpResponseException(HttpStatusCode.NotFound); @@ -163,14 +163,14 @@ namespace Umbraco.Web.Editors [HttpPost] public HttpResponseMessage DeleteContainer(int id) { - Services.ContentTypeService.DeleteContentTypeContainer(id, Security.CurrentUser.Id); + Services.ContentTypeService.DeleteContainer(id, Security.CurrentUser.Id); return Request.CreateResponse(HttpStatusCode.OK); } public HttpResponseMessage PostCreateContainer(int parentId, string name) { - var result = Services.ContentTypeService.CreateContentTypeContainer(parentId, name, Security.CurrentUser.Id); + var result = Services.ContentTypeService.CreateContainer(parentId, name, Security.CurrentUser.Id); return result ? Request.CreateResponse(HttpStatusCode.OK, result.Result) //return the id @@ -179,9 +179,9 @@ namespace Umbraco.Web.Editors public DocumentTypeDisplay PostSave(DocumentTypeSave contentTypeSave) { - var savedCt = PerformPostSave( + var savedCt = PerformPostSave( contentTypeSave: contentTypeSave, - getContentType: i => Services.ContentTypeService.GetContentType(i), + getContentType: i => Services.ContentTypeService.Get(i), saveContentType: type => Services.ContentTypeService.Save(type), beforeCreateNew: ctSave => { @@ -199,7 +199,7 @@ namespace Umbraco.Web.Editors () => ctSave.Alias, () => tryCreateTemplate.Result.StatusType); } - template = tryCreateTemplate.Result.Entity; + template = tryCreateTemplate.Result.Value; } //make sure the template alias is set on the default and allowed template so we can map it back @@ -227,7 +227,7 @@ namespace Umbraco.Web.Editors IContentType ct; if (parentId != Constants.System.Root) { - var parent = Services.ContentTypeService.GetContentType(parentId); + var parent = Services.ContentTypeService.Get(parentId); ct = parent != null ? new ContentType(parent, string.Empty) : new ContentType(parentId); } else @@ -245,7 +245,7 @@ namespace Umbraco.Web.Editors /// public IEnumerable GetAll() { - var types = Services.ContentTypeService.GetAllContentTypes(); + var types = Services.ContentTypeService.GetAll(); var basics = types.Select(Mapper.Map); return basics.Select(basic => @@ -269,7 +269,7 @@ namespace Umbraco.Web.Editors IEnumerable types; if (contentId == Constants.System.Root) { - types = Services.ContentTypeService.GetAllContentTypes().ToList(); + types = Services.ContentTypeService.GetAll().ToList(); //if no allowed root types are set, just return everything if (types.Any(x => x.AllowedAsRoot)) @@ -287,7 +287,7 @@ namespace Umbraco.Web.Editors if (ids.Any() == false) return Enumerable.Empty(); - types = Services.ContentTypeService.GetAllContentTypes(ids).ToList(); + types = Services.ContentTypeService.GetAll(ids).ToList(); } var basics = types.Select(Mapper.Map).ToList(); @@ -311,8 +311,8 @@ namespace Umbraco.Web.Editors { return PerformMove( move, - getContentType: i => Services.ContentTypeService.GetContentType(i), - doMove: (type, i) => Services.ContentTypeService.MoveContentType(type, i)); + getContentType: i => Services.ContentTypeService.Get(i), + doMove: (type, i) => Services.ContentTypeService.Move(type, i)); } /// @@ -324,8 +324,8 @@ namespace Umbraco.Web.Editors { return PerformCopy( copy, - getContentType: i => Services.ContentTypeService.GetContentType(i), - doCopy: (type, i) => Services.ContentTypeService.CopyContentType(type, i)); + getContentType: i => Services.ContentTypeService.Get(i), + doCopy: (type, i) => Services.ContentTypeService.Copy(type, i)); } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs index 0e699b591a..62bf5092e0 100644 --- a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs @@ -4,21 +4,16 @@ using System.Linq; using System.Net; using System.Net.Http; using System.Text; -using System.Text.RegularExpressions; using System.Web.Http; using AutoMapper; -using Newtonsoft.Json; using Umbraco.Core; -using Umbraco.Core.Configuration; using Umbraco.Core.Dictionary; using Umbraco.Core.Exceptions; using Umbraco.Core.Models; -using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; -using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.Editors { @@ -27,7 +22,8 @@ namespace Umbraco.Web.Editors /// [PluginController("UmbracoApi")] [PrefixlessBodyModelValidator] - public abstract class ContentTypeControllerBase : UmbracoAuthorizedJsonController + public abstract class ContentTypeControllerBase : UmbracoAuthorizedJsonController + where TContentType : class, IContentTypeComposition { private ICultureDictionary _cultureDictionary; @@ -79,19 +75,19 @@ namespace Umbraco.Web.Editors case UmbracoObjectTypes.DocumentType: if (contentTypeId > 0) { - source = Services.ContentTypeService.GetContentType(contentTypeId); + source = Services.ContentTypeService.Get(contentTypeId); if (source == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); } - allContentTypes = Services.ContentTypeService.GetAllContentTypes().Cast().ToArray(); + allContentTypes = Services.ContentTypeService.GetAll().Cast().ToArray(); break; case UmbracoObjectTypes.MediaType: if (contentTypeId > 0) { - source = Services.ContentTypeService.GetMediaType(contentTypeId); + source = Services.MediaTypeService.Get(contentTypeId); if (source == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); } - allContentTypes = Services.ContentTypeService.GetAllMediaTypes().Cast().ToArray(); + allContentTypes = Services.MediaTypeService.GetAll().Cast().ToArray(); break; case UmbracoObjectTypes.MemberType: @@ -148,12 +144,11 @@ namespace Umbraco.Web.Editors return CultureDictionary[text].IfNullOrWhiteSpace(text); } - protected TContentType PerformPostSave( + protected TContentType PerformPostSave( TContentTypeSave contentTypeSave, Func getContentType, Action saveContentType, Action beforeCreateNew = null) - where TContentType : class, IContentTypeComposition where TContentTypeDisplay : ContentTypeCompositionDisplay where TContentTypeSave : ContentTypeSave where TPropertyType : PropertyTypeBasic @@ -312,11 +307,10 @@ namespace Umbraco.Web.Editors /// /// /// - protected HttpResponseMessage PerformCopy( + protected HttpResponseMessage PerformCopy( MoveOrCopy move, Func getContentType, - Func>> doCopy) - where TContentType : IContentTypeComposition + Func>> doCopy) { var toMove = getContentType(move.Id); if (toMove == null) @@ -327,7 +321,7 @@ namespace Umbraco.Web.Editors var result = doCopy(toMove, move.ParentId); if (result.Success) { - var copy = result.Result.Entity; + var copy = result.Result.Value; var response = Request.CreateResponse(HttpStatusCode.OK); response.Content = new StringContent(copy.Path, Encoding.UTF8, "application/json"); return response; @@ -356,12 +350,13 @@ namespace Umbraco.Web.Editors /// /// /// - private HttpResponseException CreateCompositionValidationExceptionIfInvalid(TContentTypeSave contentTypeSave, IContentTypeComposition composition) + private HttpResponseException CreateCompositionValidationExceptionIfInvalid(TContentTypeSave contentTypeSave, TContentType composition) where TContentTypeSave : ContentTypeSave where TPropertyType : PropertyTypeBasic where TContentTypeDisplay : ContentTypeCompositionDisplay { - var validateAttempt = Services.ContentTypeService.ValidateComposition(composition); + var service = ApplicationContext.Services.GetContentTypeService(); + var validateAttempt = service.ValidateComposition(composition); if (validateAttempt == false) { //if it's not successful then we need to return some model state for the property aliases that diff --git a/src/Umbraco.Web/Editors/EntityController.cs b/src/Umbraco.Web/Editors/EntityController.cs index 596e27e3a5..9d8955a374 100644 --- a/src/Umbraco.Web/Editors/EntityController.cs +++ b/src/Umbraco.Web/Editors/EntityController.cs @@ -566,8 +566,8 @@ namespace Umbraco.Web.Editors case UmbracoEntityTypes.PropertyType: //get all document types, then combine all property types into one list - var propertyTypes = Services.ContentTypeService.GetAllContentTypes().Cast() - .Concat(Services.ContentTypeService.GetAllMediaTypes()) + var propertyTypes = Services.ContentTypeService.GetAll().Cast() + .Concat(Services.MediaTypeService.GetAll()) .ToArray() .SelectMany(x => x.PropertyTypes) .DistinctBy(composition => composition.Alias); @@ -577,8 +577,8 @@ namespace Umbraco.Web.Editors case UmbracoEntityTypes.PropertyGroup: //get all document types, then combine all property types into one list - var propertyGroups = Services.ContentTypeService.GetAllContentTypes().Cast() - .Concat(Services.ContentTypeService.GetAllMediaTypes()) + var propertyGroups = Services.ContentTypeService.GetAll().Cast() + .Concat(Services.MediaTypeService.GetAll()) .ToArray() .SelectMany(x => x.PropertyGroups) .DistinctBy(composition => composition.Name); diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index dbdd9bea40..d50b10ef48 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -72,7 +72,7 @@ namespace Umbraco.Web.Editors [OutgoingEditorModelEvent] public MediaItemDisplay GetEmpty(string contentTypeAlias, int parentId) { - var contentType = Services.ContentTypeService.GetMediaType(contentTypeAlias); + var contentType = Services.MediaTypeService.Get(contentTypeAlias); if (contentType == null) { throw new HttpResponseException(HttpStatusCode.NotFound); @@ -152,7 +152,7 @@ namespace Umbraco.Web.Editors { //Suggested convention for folder mediatypes - we can make this more or less complicated as long as we document it... //if you create a media type, which has an alias that ends with ...Folder then its a folder: ex: "secureFolder", "bannerFolder", "Folder" - var folderTypes = Services.ContentTypeService.GetAllMediaTypes().ToArray().Where(x => x.Alias.EndsWith("Folder")).Select(x => x.Id); + var folderTypes = Services.MediaTypeService.GetAll().ToArray().Where(x => x.Alias.EndsWith("Folder")).Select(x => x.Id); var children = (id < 0) ? Services.MediaService.GetRootMedia() : Services.MediaService.GetById(id).Children(); return children.Where(x => folderTypes.Contains(x.ContentTypeId)).Select(Mapper.Map>); diff --git a/src/Umbraco.Web/Editors/MediaTypeController.cs b/src/Umbraco.Web/Editors/MediaTypeController.cs index 31be4509fb..bf5ea1c9e6 100644 --- a/src/Umbraco.Web/Editors/MediaTypeController.cs +++ b/src/Umbraco.Web/Editors/MediaTypeController.cs @@ -32,7 +32,7 @@ namespace Umbraco.Web.Editors [PluginController("UmbracoApi")] [UmbracoTreeAuthorize(Constants.Trees.MediaTypes)] [EnableOverrideAuthorization] - public class MediaTypeController : ContentTypeControllerBase + public class MediaTypeController : ContentTypeControllerBase { /// /// Constructor @@ -54,12 +54,12 @@ namespace Umbraco.Web.Editors public int GetCount() { - return Services.ContentTypeService.CountContentTypes(); + return Services.ContentTypeService.Count(); } public MediaTypeDisplay GetById(int id) { - var ct = Services.ContentTypeService.GetMediaType(id); + var ct = Services.MediaTypeService.Get(id); if (ct == null) { throw new HttpResponseException(HttpStatusCode.NotFound); @@ -78,13 +78,13 @@ namespace Umbraco.Web.Editors [HttpPost] public HttpResponseMessage DeleteById(int id) { - var foundType = Services.ContentTypeService.GetMediaType(id); + var foundType = Services.MediaTypeService.Get(id); if (foundType == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } - Services.ContentTypeService.Delete(foundType, Security.CurrentUser.Id); + Services.MediaTypeService.Delete(foundType, Security.CurrentUser.Id); return Request.CreateResponse(HttpStatusCode.OK); } @@ -131,7 +131,7 @@ namespace Umbraco.Web.Editors public IEnumerable GetAll() { - return Services.ContentTypeService.GetAllMediaTypes() + return Services.MediaTypeService.GetAll() .Select(Mapper.Map); } @@ -144,14 +144,14 @@ namespace Umbraco.Web.Editors [HttpPost] public HttpResponseMessage DeleteContainer(int id) { - Services.ContentTypeService.DeleteMediaTypeContainer(id, Security.CurrentUser.Id); + Services.MediaTypeService.DeleteContainer(id, Security.CurrentUser.Id); return Request.CreateResponse(HttpStatusCode.OK); } public HttpResponseMessage PostCreateContainer(int parentId, string name) { - var result = Services.ContentTypeService.CreateMediaTypeContainer(parentId, name, Security.CurrentUser.Id); + var result = Services.MediaTypeService.CreateContainer(parentId, name, Security.CurrentUser.Id); return result ? Request.CreateResponse(HttpStatusCode.OK, result.Result) //return the id @@ -160,10 +160,10 @@ namespace Umbraco.Web.Editors public MediaTypeDisplay PostSave(MediaTypeSave contentTypeSave) { - var savedCt = PerformPostSave( + var savedCt = PerformPostSave( contentTypeSave: contentTypeSave, - getContentType: i => Services.ContentTypeService.GetMediaType(i), - saveContentType: type => Services.ContentTypeService.Save(type)); + getContentType: i => Services.MediaTypeService.Get(i), + saveContentType: type => Services.MediaTypeService.Save(type)); var display = Mapper.Map(savedCt); @@ -188,7 +188,7 @@ namespace Umbraco.Web.Editors IEnumerable types; if (contentId == Constants.System.Root) { - types = Services.ContentTypeService.GetAllMediaTypes().ToList(); + types = Services.MediaTypeService.GetAll().ToList(); //if no allowed root types are set, just return everything if (types.Any(x => x.AllowedAsRoot)) @@ -206,7 +206,7 @@ namespace Umbraco.Web.Editors if (ids.Any() == false) return Enumerable.Empty(); - types = Services.ContentTypeService.GetAllMediaTypes(ids).ToList(); + types = Services.MediaTypeService.GetAll(ids).ToList(); } var basics = types.Select(Mapper.Map).ToList(); @@ -229,8 +229,8 @@ namespace Umbraco.Web.Editors { return PerformMove( move, - getContentType: i => Services.ContentTypeService.GetMediaType(i), - doMove: (type, i) => Services.ContentTypeService.MoveMediaType(type, i)); + getContentType: i => Services.MediaTypeService.Get(i), + doMove: (type, i) => Services.MediaTypeService.Move(type, i)); } /// @@ -242,8 +242,8 @@ namespace Umbraco.Web.Editors { return PerformCopy( copy, - getContentType: i => Services.ContentTypeService.GetMediaType(i), - doCopy: (type, i) => Services.ContentTypeService.CopyMediaType(type, i)); + getContentType: i => Services.MediaTypeService.Get(i), + doCopy: (type, i) => Services.MediaTypeService.Copy(type, i)); } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/MemberTypeController.cs b/src/Umbraco.Web/Editors/MemberTypeController.cs index 1d8766f253..4ce6766a63 100644 --- a/src/Umbraco.Web/Editors/MemberTypeController.cs +++ b/src/Umbraco.Web/Editors/MemberTypeController.cs @@ -25,7 +25,7 @@ namespace Umbraco.Web.Editors /// [PluginController("UmbracoApi")] [UmbracoTreeAuthorize(Constants.Trees.MemberTypes)] - public class MemberTypeController : ContentTypeControllerBase + public class MemberTypeController : ContentTypeControllerBase { /// /// Constructor @@ -131,7 +131,7 @@ namespace Umbraco.Web.Editors public MemberTypeDisplay PostSave(MemberTypeSave contentTypeSave) { - var savedCt = PerformPostSave( + var savedCt = PerformPostSave( contentTypeSave: contentTypeSave, getContentType: i => Services.MemberTypeService.Get(i), saveContentType: type => Services.MemberTypeService.Save(type)); diff --git a/src/Umbraco.Web/Editors/TemplateQueryController.cs b/src/Umbraco.Web/Editors/TemplateQueryController.cs index 2eaf63f159..e03e8e8f56 100644 --- a/src/Umbraco.Web/Editors/TemplateQueryController.cs +++ b/src/Umbraco.Web/Editors/TemplateQueryController.cs @@ -288,7 +288,7 @@ namespace Umbraco.Web.Editors public IEnumerable GetContentTypes() { var contentTypes = - ApplicationContext.Services.ContentTypeService.GetAllContentTypes() + ApplicationContext.Services.ContentTypeService.GetAll() .Select(x => new ContentTypeModel() { Alias = x.Alias, Name = x.Name }) .OrderBy(x => x.Name).ToList(); contentTypes.Insert(0, new ContentTypeModel() { Alias = string.Empty, Name = "Everything" }); diff --git a/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs index d9f1f25189..a859ba0813 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs @@ -219,7 +219,7 @@ namespace Umbraco.Web.Models.Mapping if (HttpContext.Current != null && UmbracoContext.Current != null && UmbracoContext.Current.Security.CurrentUser != null && UmbracoContext.Current.Security.CurrentUser.AllowedSections.Any(x => x.Equals(Constants.Applications.Settings))) { - var currentDocumentType = contentTypeService.GetContentType(display.ContentTypeAlias); + var currentDocumentType = contentTypeService.Get(display.ContentTypeAlias); var currentDocumentTypeName = currentDocumentType == null ? string.Empty : currentDocumentType.Name; var currentDocumentTypeId = currentDocumentType == null ? string.Empty : currentDocumentType.Id.ToString(CultureInfo.InvariantCulture); diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs index 52f2dbad4b..f86f813385 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs @@ -69,7 +69,7 @@ namespace Umbraco.Web.Models.Mapping foreach (var a in add) { //TODO: Remove N+1 lookup - var addCt = applicationContext.Services.ContentTypeService.GetContentType(a); + var addCt = applicationContext.Services.ContentTypeService.Get(a); if (addCt != null) dest.AddContentType(addCt); } @@ -96,7 +96,7 @@ namespace Umbraco.Web.Models.Mapping foreach (var a in add) { //TODO: Remove N+1 lookup - var addCt = applicationContext.Services.ContentTypeService.GetMediaType(a); + var addCt = applicationContext.Services.MediaTypeService.Get(a); if (addCt != null) dest.AddContentType(addCt); } diff --git a/src/Umbraco.Web/Models/Mapping/LockedCompositionsResolver.cs b/src/Umbraco.Web/Models/Mapping/LockedCompositionsResolver.cs index d3d692d10d..cc3007ef29 100644 --- a/src/Umbraco.Web/Models/Mapping/LockedCompositionsResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/LockedCompositionsResolver.cs @@ -21,12 +21,12 @@ namespace Umbraco.Web.Models.Mapping // get ancestor ids from path of parent if not root if (source.ParentId != Constants.System.Root) { - var parent = _applicationContext.Services.ContentTypeService.GetContentType(source.ParentId); + var parent = _applicationContext.Services.ContentTypeService.Get(source.ParentId); if (parent != null) { var ancestorIds = parent.Path.Split(',').Select(int.Parse); // loop through all content types and return ordered aliases of ancestors - var allContentTypes = _applicationContext.Services.ContentTypeService.GetAllContentTypes().ToArray(); + var allContentTypes = _applicationContext.Services.ContentTypeService.GetAll().ToArray(); foreach (var ancestorId in ancestorIds) { var ancestor = allContentTypes.FirstOrDefault(x => x.Id == ancestorId); diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 578aab2755..66d16f6bd3 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -634,10 +634,10 @@ namespace Umbraco.Web private static bool IsDocumentTypeRecursive(IPublishedContent content, string docTypeAlias) { var contentTypeService = UmbracoContext.Current.Application.Services.ContentTypeService; - var type = contentTypeService.GetContentType(content.DocumentTypeAlias); + var type = contentTypeService.Get(content.DocumentTypeAlias); while (type != null && type.ParentId > 0) { - type = contentTypeService.GetContentType(type.ParentId); + type = contentTypeService.Get(type.ParentId); if (type.Alias.InvariantEquals(docTypeAlias)) return true; } diff --git a/src/Umbraco.Web/Services/SectionService.cs b/src/Umbraco.Web/Services/SectionService.cs index 6f31727a4d..39f7aa9d5c 100644 --- a/src/Umbraco.Web/Services/SectionService.cs +++ b/src/Umbraco.Web/Services/SectionService.cs @@ -213,9 +213,12 @@ namespace Umbraco.Web.Services lock (Locker) { //delete the assigned applications - _uowProvider.CreateUnitOfWork().Database.Execute( - "delete from umbracoUser2App where app = @appAlias", - new { appAlias = section.Alias }); + using (var uow = _uowProvider.CreateUnitOfWork()) + { + uow.Database.Execute("delete from umbracoUser2App where app = @appAlias", + new { appAlias = section.Alias }); + uow.Complete(); + } //delete the assigned trees var trees = _applicationTreeService.GetApplicationTrees(section.Alias); diff --git a/src/Umbraco.Web/Trees/ContentTypeTreeController.cs b/src/Umbraco.Web/Trees/ContentTypeTreeController.cs index 29dd5179ef..4a20b727f5 100644 --- a/src/Umbraco.Web/Trees/ContentTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTypeTreeController.cs @@ -99,9 +99,9 @@ namespace Umbraco.Web.Trees } else { - var ct = Services.ContentTypeService.GetContentType(int.Parse(id)); + var ct = Services.ContentTypeService.Get(int.Parse(id)); IContentType parent = null; - parent = ct == null ? null : Services.ContentTypeService.GetContentType(ct.ParentId); + parent = ct == null ? null : Services.ContentTypeService.Get(ct.ParentId); if (enableInheritedDocumentTypes) { diff --git a/src/Umbraco.Web/WebApi/Binders/ContentItemBinder.cs b/src/Umbraco.Web/WebApi/Binders/ContentItemBinder.cs index 5afc7cee19..d832f743c3 100644 --- a/src/Umbraco.Web/WebApi/Binders/ContentItemBinder.cs +++ b/src/Umbraco.Web/WebApi/Binders/ContentItemBinder.cs @@ -30,7 +30,7 @@ namespace Umbraco.Web.WebApi.Binders protected override IContent CreateNew(ContentItemSave model) { - var contentType = ApplicationContext.Services.ContentTypeService.GetContentType(model.ContentTypeAlias); + var contentType = ApplicationContext.Services.ContentTypeService.Get(model.ContentTypeAlias); if (contentType == null) { throw new InvalidOperationException("No content type found wth alias " + model.ContentTypeAlias); diff --git a/src/Umbraco.Web/WebApi/Binders/MediaItemBinder.cs b/src/Umbraco.Web/WebApi/Binders/MediaItemBinder.cs index 726994d43e..2a335fdc98 100644 --- a/src/Umbraco.Web/WebApi/Binders/MediaItemBinder.cs +++ b/src/Umbraco.Web/WebApi/Binders/MediaItemBinder.cs @@ -29,7 +29,7 @@ namespace Umbraco.Web.WebApi.Binders protected override IMedia CreateNew(MediaItemSave model) { - var contentType = ApplicationContext.Services.ContentTypeService.GetMediaType(model.ContentTypeAlias); + var contentType = ApplicationContext.Services.MediaTypeService.Get(model.ContentTypeAlias); if (contentType == null) { throw new InvalidOperationException("No content type found wth alias " + model.ContentTypeAlias); diff --git a/src/Umbraco.Web/WebServices/BulkPublishController.cs b/src/Umbraco.Web/WebServices/BulkPublishController.cs index 8459cdbcf8..65e0078ace 100644 --- a/src/Umbraco.Web/WebServices/BulkPublishController.cs +++ b/src/Umbraco.Web/WebServices/BulkPublishController.cs @@ -56,7 +56,7 @@ namespace Umbraco.Web.WebServices private string GetMessageForStatuses(IEnumerable statuses, IContent doc) { //if all are successful then just say it was successful - if (statuses.All(x => ((int) x.StatusType) < 10)) + if (statuses.All(x => x.StatusType.IsSuccess())) { return Services.TextService.Localize("publish/nodePublishAll", new[] { doc.Name}); } diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/installedPackage.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/installedPackage.aspx.cs index 9bcee907e4..7b20ee25c2 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/installedPackage.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/installedPackage.aspx.cs @@ -431,7 +431,7 @@ namespace umbraco.presentation.developer.packages if (int.TryParse(li.Value, out nId)) { - var contentType = contentTypeService.GetContentType(nId); + var contentType = contentTypeService.Get(nId); if (contentType != null) { contentTypes.Add(contentType); diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/exportDocumenttype.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/exportDocumenttype.aspx.cs index 545a8493ee..3e3454d58f 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/exportDocumenttype.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/exportDocumenttype.aspx.cs @@ -34,7 +34,7 @@ namespace umbraco.presentation.dialogs int documentTypeId = Request.GetItemAs("nodeID"); if (documentTypeId > 0) { - var contentType = Services.ContentTypeService.GetContentType(documentTypeId); + var contentType = Services.ContentTypeService.Get(documentTypeId); if (contentType == null) throw new NullReferenceException("No content type found with id " + documentTypeId); Response.AddHeader("Content-Disposition", "attachment;filename=" + contentType.Alias + ".udt"); diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/protectPage.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/protectPage.aspx.cs index 4e8c6f0a35..49df700caf 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/protectPage.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/protectPage.aspx.cs @@ -165,7 +165,7 @@ namespace umbraco.presentation.umbraco.dialogs Constants.Conventions.PublicAccess.MemberRoleRuleType, role); - if (entry.Success == false && entry.Result.Entity == null) + if (entry.Success == false && entry.Result.Value == null) { throw new Exception("Document is not protected!"); } @@ -180,7 +180,7 @@ namespace umbraco.presentation.umbraco.dialogs Constants.Conventions.PublicAccess.MemberUsernameRuleType, membershipUserName); - if (entry.Success == false && entry.Result.Entity == null) + if (entry.Success == false && entry.Result.Value == null) { throw new Exception("Document is not protected!"); } diff --git a/src/umbraco.cms/businesslogic/ContentType.cs b/src/umbraco.cms/businesslogic/ContentType.cs index 12956efb8d..05df73b1b5 100644 --- a/src/umbraco.cms/businesslogic/ContentType.cs +++ b/src/umbraco.cms/businesslogic/ContentType.cs @@ -1165,11 +1165,11 @@ namespace umbraco.cms.businesslogic { if (nodeObjectType == new Guid(Constants.ObjectTypes.DocumentType)) { - return ApplicationContext.Current.Services.ContentTypeService.GetContentType; + return ApplicationContext.Current.Services.ContentTypeService.Get; } if (nodeObjectType == new Guid(Constants.ObjectTypes.MediaType)) { - return ApplicationContext.Current.Services.ContentTypeService.GetMediaType; + return ApplicationContext.Current.Services.MediaTypeService.Get; } if (nodeObjectType == new Guid(Constants.ObjectTypes.MemberType)) { @@ -1177,7 +1177,8 @@ namespace umbraco.cms.businesslogic } //default to content - return ApplicationContext.Current.Services.ContentTypeService.GetContentType; + // should throw! + return ApplicationContext.Current.Services.ContentTypeService.Get; } } diff --git a/src/umbraco.cms/businesslogic/media/MediaType.cs b/src/umbraco.cms/businesslogic/media/MediaType.cs index 25e47b4ea4..059b86e696 100644 --- a/src/umbraco.cms/businesslogic/media/MediaType.cs +++ b/src/umbraco.cms/businesslogic/media/MediaType.cs @@ -66,7 +66,7 @@ namespace umbraco.cms.businesslogic.media [Obsolete("Obsolete, Use Umbraco.Core.Services.ContentTypeService.GetMediaType()", false)] public static new MediaType GetByAlias(string Alias) { - var mediaType = ApplicationContext.Current.Services.ContentTypeService.GetMediaType(Alias); + var mediaType = ApplicationContext.Current.Services.MediaTypeService.Get(Alias); return new MediaType(mediaType); } @@ -85,7 +85,7 @@ namespace umbraco.cms.businesslogic.media [Obsolete("Obsolete, Use Umbraco.Core.Services.ContentTypeService.GetMediaType()", false)] public static IEnumerable GetAllAsList() { - var mediaTypes = ApplicationContext.Current.Services.ContentTypeService.GetAllMediaTypes(); + var mediaTypes = ApplicationContext.Current.Services.MediaTypeService.GetAll(); return mediaTypes.OrderBy(x => x.Name).Select(x => new MediaType(x)); } @@ -98,7 +98,7 @@ namespace umbraco.cms.businesslogic.media internal static MediaType MakeNew(IUser u, string text, int parentId) { var mediaType = new Umbraco.Core.Models.MediaType(parentId) { Name = text, Alias = text, CreatorId = u.Id, Thumbnail = "icon-folder", Icon = "icon-folder" }; - ApplicationContext.Current.Services.ContentTypeService.Save(mediaType, u.Id); + ApplicationContext.Current.Services.MediaTypeService.Save(mediaType, u.Id); var mt = new MediaType(mediaType.Id); @@ -114,7 +114,7 @@ namespace umbraco.cms.businesslogic.media { var current = Thread.CurrentPrincipal != null ? Thread.CurrentPrincipal.Identity as UmbracoBackOfficeIdentity : null; var userId = current == null ? Attempt.Fail() : current.Id.TryConvertTo(); - ApplicationContext.Current.Services.ContentTypeService.Save(MediaTypeItem, userId.Success ? userId.Result : 0); + ApplicationContext.Current.Services.MediaTypeService.Save(MediaTypeItem, userId.Success ? userId.Result : 0); base.Save(); @@ -132,7 +132,7 @@ namespace umbraco.cms.businesslogic.media throw new ArgumentException("Can't delete a Media Type used as a Master Content Type. Please remove all references first!"); } - ApplicationContext.Current.Services.ContentTypeService.Delete(MediaTypeItem); + ApplicationContext.Current.Services.MediaTypeService.Delete(MediaTypeItem); } @@ -142,7 +142,7 @@ namespace umbraco.cms.businesslogic.media protected override void setupNode() { - var mediaType = ApplicationContext.Current.Services.ContentTypeService.GetMediaType(Id); + var mediaType = ApplicationContext.Current.Services.MediaTypeService.Get(Id); // If it's null, it's probably a folder if (mediaType != null) SetupNode(mediaType); diff --git a/src/umbraco.cms/businesslogic/web/DocumentType.cs b/src/umbraco.cms/businesslogic/web/DocumentType.cs index 5833b1d198..97fcbd85c4 100644 --- a/src/umbraco.cms/businesslogic/web/DocumentType.cs +++ b/src/umbraco.cms/businesslogic/web/DocumentType.cs @@ -72,7 +72,7 @@ namespace umbraco.cms.businesslogic.web { try { - var contentType = ApplicationContext.Current.Services.ContentTypeService.GetContentType(Alias); + var contentType = ApplicationContext.Current.Services.ContentTypeService.Get(Alias); return new DocumentType(contentType.Id); } catch @@ -104,7 +104,7 @@ namespace umbraco.cms.businesslogic.web [Obsolete("Obsolete, Use Umbraco.Core.Services.ContentTypeService.GetAllContentTypes()", false)] public static List GetAllAsList() { - var contentTypes = ApplicationContext.Current.Services.ContentTypeService.GetAllContentTypes(); + var contentTypes = ApplicationContext.Current.Services.ContentTypeService.GetAll(); var documentTypes = contentTypes.Select(x => new DocumentType(x)); return documentTypes.OrderBy(x => x.Text).ToList(); @@ -423,7 +423,7 @@ namespace umbraco.cms.businesslogic.web protected override void setupNode() { - var contentType = ApplicationContext.Current.Services.ContentTypeService.GetContentType(Id); + var contentType = ApplicationContext.Current.Services.ContentTypeService.Get(Id); // If it's null, it's probably a folder if (contentType != null)