diff --git a/build/templates/UmbracoProject/appsettings.json b/build/templates/UmbracoProject/appsettings.json index feb6b07d9531..99e877812cee 100644 --- a/build/templates/UmbracoProject/appsettings.json +++ b/build/templates/UmbracoProject/appsettings.json @@ -17,6 +17,7 @@ "CMS": { //#if (HasNoNodesViewPath || UseHttpsRedirect) "Global": { + "SanitizeTinyMce": true, //#if (!HasNoNodesViewPath && UseHttpsRedirect) "UseHttps": true //#elseif (UseHttpsRedirect) @@ -25,10 +26,16 @@ //#if (HasNoNodesViewPath) "NoNodesViewPath": "NO_NODES_VIEW_PATH_FROM_TEMPLATE" //#endif + }, //#endif "Hosting": { "Debug": false + }, + "Content": { + "ContentVersionCleanupPolicy": { + "EnableCleanup": true + } } } } diff --git a/src/Umbraco.Core/Collections/StackQueue.cs b/src/Umbraco.Core/Collections/StackQueue.cs index 9bf9c365f077..b760ffe646a5 100644 --- a/src/Umbraco.Core/Collections/StackQueue.cs +++ b/src/Umbraco.Core/Collections/StackQueue.cs @@ -3,58 +3,37 @@ namespace Umbraco.Core.Collections { /// - /// Collection that can be both a queue and a stack. + /// Collection that can be both a queue and a stack. /// /// public class StackQueue { - private readonly LinkedList _linkedList = new LinkedList(); + private readonly LinkedList _linkedList = new(); - public void Clear() - { - _linkedList.Clear(); - } + public int Count => _linkedList.Count; - public void Push(T obj) - { - _linkedList.AddFirst(obj); - } + public void Clear() => _linkedList.Clear(); - public void Enqueue(T obj) - { - _linkedList.AddFirst(obj); - } + public void Push(T obj) => _linkedList.AddFirst(obj); + + public void Enqueue(T obj) => _linkedList.AddFirst(obj); public T Pop() { - var obj = _linkedList.First.Value; + T obj = _linkedList.First.Value; _linkedList.RemoveFirst(); return obj; } public T Dequeue() { - var obj = _linkedList.Last.Value; + T obj = _linkedList.Last.Value; _linkedList.RemoveLast(); return obj; } - public T PeekStack() - { - return _linkedList.First.Value; - } + public T PeekStack() => _linkedList.First.Value; - public T PeekQueue() - { - return _linkedList.Last.Value; - } - - public int Count - { - get - { - return _linkedList.Count; - } - } + public T PeekQueue() => _linkedList.Last.Value; } } diff --git a/src/Umbraco.Core/Configuration/Models/ContentSettings.cs b/src/Umbraco.Core/Configuration/Models/ContentSettings.cs index dcca8c2adbe0..1caa81d80a03 100644 --- a/src/Umbraco.Core/Configuration/Models/ContentSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ContentSettings.cs @@ -219,6 +219,9 @@ @keyframes umbraco-preview-badge--effect {{ [DefaultValue(StaticLoginLogoImage)] public string LoginLogoImage { get; set; } = StaticLoginLogoImage; - + /// + /// Get or sets the model representing the global content version cleanup policy + /// + public ContentVersionCleanupPolicySettings ContentVersionCleanupPolicy { get; set; } = new ContentVersionCleanupPolicySettings(); } } diff --git a/src/Umbraco.Core/Configuration/Models/ContentVersionCleanupPolicySettings.cs b/src/Umbraco.Core/Configuration/Models/ContentVersionCleanupPolicySettings.cs new file mode 100644 index 000000000000..bd460058eb3b --- /dev/null +++ b/src/Umbraco.Core/Configuration/Models/ContentVersionCleanupPolicySettings.cs @@ -0,0 +1,33 @@ +using System.ComponentModel; + +namespace Umbraco.Cms.Core.Configuration.Models +{ + /// + /// Model representing the global content version cleanup policy + /// + public class ContentVersionCleanupPolicySettings + { + private const bool StaticEnableCleanup = false; + private const int StaticKeepAllVersionsNewerThanDays = 7; + private const int StaticKeepLatestVersionPerDayForDays = 90; + + /// + /// Gets or sets a value indicating whether or not the cleanup job should be executed. + /// + [DefaultValue(StaticEnableCleanup)] + public bool EnableCleanup { get; set; } = StaticEnableCleanup; + + /// + /// Gets or sets the number of days where all historical content versions are kept. + /// + [DefaultValue(StaticKeepAllVersionsNewerThanDays)] + public int KeepAllVersionsNewerThanDays { get; set; } = StaticKeepAllVersionsNewerThanDays; + + /// + /// Gets or sets the number of days where the latest historical content version for that day are kept. + /// + [DefaultValue(StaticKeepLatestVersionPerDayForDays)] + public int KeepLatestVersionPerDayForDays { get; set; } = StaticKeepLatestVersionPerDayForDays; + + } +} diff --git a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs index 7799fec5eacd..c88083027488 100644 --- a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs @@ -28,6 +28,7 @@ public class GlobalSettings internal const bool StaticDisableElectionForSingleServer = false; internal const string StaticNoNodesViewPath = "~/umbraco/UmbracoWebsite/NoNodes.cshtml"; internal const string StaticSqlWriteLockTimeOut = "00:00:05"; + internal const bool StaticSanitizeTinyMce = false; /// /// Gets or sets a value for the reserved URLs. @@ -157,6 +158,12 @@ public class GlobalSettings /// public bool IsSmtpServerConfigured => !string.IsNullOrWhiteSpace(Smtp?.Host); + /// + /// Gets a value indicating whether TinyMCE scripting sanitization should be applied + /// + [DefaultValue(StaticSanitizeTinyMce)] + public bool SanitizeTinyMce => StaticSanitizeTinyMce; + /// /// An int value representing the time in milliseconds to lock the database for a write operation /// diff --git a/src/Umbraco.Core/Constants-Sql.cs b/src/Umbraco.Core/Constants-Sql.cs new file mode 100644 index 000000000000..b57861c92ac8 --- /dev/null +++ b/src/Umbraco.Core/Constants-Sql.cs @@ -0,0 +1,18 @@ +namespace Umbraco.Cms.Core +{ + public static partial class Constants + { + public static class Sql + { + /// + /// The maximum amount of parameters that can be used in a query. + /// + /// + /// The actual limit is 2100 + /// (https://docs.microsoft.com/en-us/sql/sql-server/maximum-capacity-specifications-for-sql-server), + /// but we want to ensure there's room for additional parameters if this value is used to create groups/batches. + /// + public const int MaxParameterCount = 2000; + } + } +} diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/BaseHttpHeaderCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/BaseHttpHeaderCheck.cs index eeb291c41faf..a3e861e18041 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/Security/BaseHttpHeaderCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Security/BaseHttpHeaderCheck.cs @@ -79,7 +79,7 @@ protected async Task CheckForHeader() var success = false; // Access the site home page and check for the click-jack protection header or meta tag - Uri url = _hostingEnvironment.ApplicationMainUrl; + var url = _hostingEnvironment.ApplicationMainUrl.GetLeftPart(UriPartial.Authority); try { diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentTypeSave.cs b/src/Umbraco.Core/Models/ContentEditing/ContentTypeSave.cs index 79f3fba1fd0f..6198f0c6641c 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentTypeSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentTypeSave.cs @@ -29,6 +29,9 @@ protected ContentTypeSave() [DataMember(Name = "allowedContentTypes")] public IEnumerable AllowedContentTypes { get; set; } + [DataMember(Name = "historyCleanup")] + public HistoryCleanupViewModel HistoryCleanup { get; set; } + /// /// Custom validation /// diff --git a/src/Umbraco.Core/Models/ContentEditing/DocumentTypeDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/DocumentTypeDisplay.cs index 7d45c466002a..3bb52c39b724 100644 --- a/src/Umbraco.Core/Models/ContentEditing/DocumentTypeDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/DocumentTypeDisplay.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Runtime.Serialization; namespace Umbraco.Cms.Core.Models.ContentEditing @@ -6,11 +6,9 @@ namespace Umbraco.Cms.Core.Models.ContentEditing [DataContract(Name = "contentType", Namespace = "")] public class DocumentTypeDisplay : ContentTypeCompositionDisplay { - public DocumentTypeDisplay() - { + public DocumentTypeDisplay() => //initialize collections so at least their never null AllowedTemplates = new List(); - } //name, alias, icon, thumb, desc, inherited from the content type @@ -29,5 +27,8 @@ public DocumentTypeDisplay() [DataMember(Name = "apps")] public IEnumerable ContentApps { get; set; } + + [DataMember(Name = "historyCleanup")] + public HistoryCleanupViewModel HistoryCleanup { get; set; } } } diff --git a/src/Umbraco.Core/Models/ContentEditing/HistoryCleanup.cs b/src/Umbraco.Core/Models/ContentEditing/HistoryCleanup.cs new file mode 100644 index 000000000000..b7bfb328087f --- /dev/null +++ b/src/Umbraco.Core/Models/ContentEditing/HistoryCleanup.cs @@ -0,0 +1,17 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Cms.Core.Models.ContentEditing +{ + [DataContract(Name = "historyCleanup", Namespace = "")] + public class HistoryCleanup + { + [DataMember(Name = "preventCleanup")] + public bool PreventCleanup { get; set; } + + [DataMember(Name = "keepAllVersionsNewerThanDays")] + public int? KeepAllVersionsNewerThanDays { get; set; } + + [DataMember(Name = "keepLatestVersionPerDayForDays")] + public int? KeepLatestVersionPerDayForDays { get; set; } + } +} diff --git a/src/Umbraco.Core/Models/ContentEditing/HistoryCleanupViewModel.cs b/src/Umbraco.Core/Models/ContentEditing/HistoryCleanupViewModel.cs new file mode 100644 index 000000000000..303ff4eda375 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentEditing/HistoryCleanupViewModel.cs @@ -0,0 +1,18 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Cms.Core.Models.ContentEditing +{ + [DataContract(Name = "historyCleanup", Namespace = "")] + public class HistoryCleanupViewModel : HistoryCleanup + { + + [DataMember(Name = "globalEnableCleanup")] + public bool GlobalEnableCleanup { get; set; } + + [DataMember(Name = "globalKeepAllVersionsNewerThanDays")] + public int? GlobalKeepAllVersionsNewerThanDays { get; set; } + + [DataMember(Name = "globalKeepLatestVersionPerDayForDays")] + public int? GlobalKeepLatestVersionPerDayForDays { get; set; } + } +} diff --git a/src/Umbraco.Core/Models/ContentType.cs b/src/Umbraco.Core/Models/ContentType.cs index 9a0e1a6854ce..6ff94f57f363 100644 --- a/src/Umbraco.Core/Models/ContentType.cs +++ b/src/Umbraco.Core/Models/ContentType.cs @@ -1,37 +1,45 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; +using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Models { /// - /// Represents the content type that a object is based on + /// Represents the content type that a object is based on /// [Serializable] [DataContract(IsReference = true)] - public class ContentType : ContentTypeCompositionBase, IContentType + public class ContentType : ContentTypeCompositionBase, IContentTypeWithHistoryCleanup { public const bool SupportsPublishingConst = true; - private int _defaultTemplate; + // Custom comparer for enumerable + private static readonly DelegateEqualityComparer> TemplateComparer = new ( + (templates, enumerable) => templates.UnsortedSequenceEqual(enumerable), + templates => templates.GetHashCode()); + private IEnumerable _allowedTemplates; + private int _defaultTemplate; + /// - /// Constuctor for creating a ContentType with the parent's id. + /// Constuctor for creating a ContentType with the parent's id. /// /// Only use this for creating ContentTypes at the root (with ParentId -1). /// public ContentType(IShortStringHelper shortStringHelper, int parentId) : base(shortStringHelper, parentId) { _allowedTemplates = new List(); + HistoryCleanup = new HistoryCleanup(); } /// - /// Constuctor for creating a ContentType with the parent as an inherited type. + /// Constuctor for creating a ContentType with the parent as an inherited type. /// /// Use this to ensure inheritance from parent. /// @@ -40,30 +48,24 @@ public ContentType(IShortStringHelper shortStringHelper, IContentType parent, st : base(shortStringHelper, parent, alias) { _allowedTemplates = new List(); + HistoryCleanup = new HistoryCleanup(); } - /// - public override ISimpleContentType ToSimple() => new SimpleContentType(this); - /// public override bool SupportsPublishing => SupportsPublishingConst; - //Custom comparer for enumerable - private static readonly DelegateEqualityComparer> TemplateComparer = new DelegateEqualityComparer>( - (templates, enumerable) => templates.UnsortedSequenceEqual(enumerable), - templates => templates.GetHashCode()); + /// + public override ISimpleContentType ToSimple() => new SimpleContentType(this); /// - /// Gets or sets the alias of the default Template. - /// TODO: This should be ignored from cloning!!!!!!!!!!!!!! - /// - but to do that we have to implement callback hacks, this needs to be fixed in v8, + /// Gets or sets the alias of the default Template. + /// TODO: This should be ignored from cloning!!!!!!!!!!!!!! + /// - but to do that we have to implement callback hacks, this needs to be fixed in v8, /// we should not store direct entity /// [IgnoreDataMember] - public ITemplate DefaultTemplate - { - get { return AllowedTemplates.FirstOrDefault(x => x != null && x.Id == DefaultTemplateId); } - } + public ITemplate DefaultTemplate => + AllowedTemplates.FirstOrDefault(x => x != null && x.Id == DefaultTemplateId); [DataMember] @@ -74,9 +76,9 @@ public int DefaultTemplateId } /// - /// Gets or Sets a list of Templates which are allowed for the ContentType - /// TODO: This should be ignored from cloning!!!!!!!!!!!!!! - /// - but to do that we have to implement callback hacks, this needs to be fixed in v8, + /// Gets or Sets a list of Templates which are allowed for the ContentType + /// TODO: This should be ignored from cloning!!!!!!!!!!!!!! + /// - but to do that we have to implement callback hacks, this needs to be fixed in v8, /// we should not store direct entity /// [DataMember] @@ -88,38 +90,38 @@ public IEnumerable AllowedTemplates SetPropertyValueAndDetectChanges(value, ref _allowedTemplates, nameof(AllowedTemplates), TemplateComparer); if (_allowedTemplates.Any(x => x.Id == _defaultTemplate) == false) + { DefaultTemplateId = 0; + } } } + public HistoryCleanup HistoryCleanup { get; set; } + /// - /// Determines if AllowedTemplates contains templateId + /// Determines if AllowedTemplates contains templateId /// /// The template id to check /// True if AllowedTemplates contains the templateId else False - public bool IsAllowedTemplate(int templateId) - { - return AllowedTemplates == null + public bool IsAllowedTemplate(int templateId) => + AllowedTemplates == null ? false : AllowedTemplates.Any(t => t.Id == templateId); - } /// - /// Determines if AllowedTemplates contains templateId + /// Determines if AllowedTemplates contains templateId /// /// The template alias to check /// True if AllowedTemplates contains the templateAlias else False - public bool IsAllowedTemplate(string templateAlias) - { - return AllowedTemplates == null + public bool IsAllowedTemplate(string templateAlias) => + AllowedTemplates == null ? false : AllowedTemplates.Any(t => t.Alias.Equals(templateAlias, StringComparison.InvariantCultureIgnoreCase)); - } /// - /// Sets the default template for the ContentType + /// Sets the default template for the ContentType /// - /// Default + /// Default public void SetDefaultTemplate(ITemplate template) { if (template == null) @@ -138,17 +140,19 @@ public void SetDefaultTemplate(ITemplate template) } /// - /// Removes a template from the list of allowed templates + /// Removes a template from the list of allowed templates /// - /// to remove + /// to remove /// True if template was removed, otherwise False public bool RemoveTemplate(ITemplate template) { if (DefaultTemplateId == template.Id) - DefaultTemplateId = default(int); + { + DefaultTemplateId = default; + } var templates = AllowedTemplates.ToList(); - var remove = templates.FirstOrDefault(x => x.Id == template.Id); + ITemplate remove = templates.FirstOrDefault(x => x.Id == template.Id); var result = templates.Remove(remove); AllowedTemplates = templates; @@ -156,6 +160,7 @@ public bool RemoveTemplate(ITemplate template) } /// - IContentType IContentType.DeepCloneWithResetIdentities(string newAlias) => (IContentType)DeepCloneWithResetIdentities(newAlias); + IContentType IContentType.DeepCloneWithResetIdentities(string newAlias) => + (IContentType)DeepCloneWithResetIdentities(newAlias); } } diff --git a/src/Umbraco.Core/Models/ContentVersionCleanupPolicySettings.cs b/src/Umbraco.Core/Models/ContentVersionCleanupPolicySettings.cs new file mode 100644 index 000000000000..5fa0e9895822 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentVersionCleanupPolicySettings.cs @@ -0,0 +1,17 @@ +using System; + +namespace Umbraco.Cms.Core.Models +{ + public class ContentVersionCleanupPolicySettings + { + public int ContentTypeId { get; set; } + + public bool PreventCleanup { get; set; } + + public int? KeepAllVersionsNewerThanDays { get; set; } + + public int? KeepLatestVersionPerDayForDays { get; set; } + + public DateTime Updated { get; set; } + } +} diff --git a/src/Umbraco.Core/Models/HistoricContentVersionMeta.cs b/src/Umbraco.Core/Models/HistoricContentVersionMeta.cs new file mode 100644 index 000000000000..ce2c771ec6bf --- /dev/null +++ b/src/Umbraco.Core/Models/HistoricContentVersionMeta.cs @@ -0,0 +1,24 @@ +using System; + +namespace Umbraco.Cms.Core.Models +{ + public class HistoricContentVersionMeta + { + public int ContentId { get; } + public int ContentTypeId { get; } + public int VersionId { get; } + public DateTime VersionDate { get; } + + public HistoricContentVersionMeta() { } + + public HistoricContentVersionMeta(int contentId, int contentTypeId, int versionId, DateTime versionDate) + { + ContentId = contentId; + ContentTypeId = contentTypeId; + VersionId = versionId; + VersionDate = versionDate; + } + + public override string ToString() => $"HistoricContentVersionMeta(versionId: {VersionId}, versionDate: {VersionDate:s}"; + } +} diff --git a/src/Umbraco.Core/Models/IContentType.cs b/src/Umbraco.Core/Models/IContentType.cs index f04a73d5e032..a01e6128878b 100644 --- a/src/Umbraco.Core/Models/IContentType.cs +++ b/src/Umbraco.Core/Models/IContentType.cs @@ -1,56 +1,67 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using Umbraco.Cms.Core.Models.ContentEditing; namespace Umbraco.Cms.Core.Models { + [Obsolete("This will be merged into IContentType in Umbraco 10")] + public interface IContentTypeWithHistoryCleanup : IContentType + { + /// + /// Gets or Sets the history cleanup configuration + /// + HistoryCleanup HistoryCleanup { get; set; } + } + /// - /// Defines a ContentType, which Content is based on + /// Defines a ContentType, which Content is based on /// public interface IContentType : IContentTypeComposition { /// - /// Internal property to store the Id of the default template + /// Internal property to store the Id of the default template /// int DefaultTemplateId { get; set; } /// - /// Gets the default Template of the ContentType + /// Gets the default Template of the ContentType /// ITemplate DefaultTemplate { get; } /// - /// Gets or Sets a list of Templates which are allowed for the ContentType + /// Gets or Sets a list of Templates which are allowed for the ContentType /// IEnumerable AllowedTemplates { get; set; } /// - /// Determines if AllowedTemplates contains templateId + /// Determines if AllowedTemplates contains templateId /// /// The template id to check /// True if AllowedTemplates contains the templateId else False bool IsAllowedTemplate(int templateId); /// - /// Determines if AllowedTemplates contains templateId + /// Determines if AllowedTemplates contains templateId /// /// The template alias to check /// True if AllowedTemplates contains the templateAlias else False bool IsAllowedTemplate(string templateAlias); /// - /// Sets the default template for the ContentType + /// Sets the default template for the ContentType /// - /// Default + /// Default void SetDefaultTemplate(ITemplate template); /// - /// Removes a template from the list of allowed templates + /// Removes a template from the list of allowed templates /// - /// to remove + /// to remove /// True if template was removed, otherwise False bool RemoveTemplate(ITemplate template); /// - /// Creates a deep clone of the current entity with its identity/alias and it's property identities reset + /// Creates a deep clone of the current entity with its identity/alias and it's property identities reset /// /// /// diff --git a/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs index d3269c2c0b54..debaa976c510 100644 --- a/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs @@ -2,9 +2,9 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Exceptions; using Umbraco.Cms.Core.Hosting; @@ -13,31 +13,50 @@ using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Models.Mapping { /// - /// Defines mappings for content/media/members type mappings + /// Defines mappings for content/media/members type mappings /// public class ContentTypeMapDefinition : IMapDefinition { private readonly CommonMapper _commonMapper; - private readonly PropertyEditorCollection _propertyEditors; + private readonly IContentTypeService _contentTypeService; private readonly IDataTypeService _dataTypeService; private readonly IFileService _fileService; - private readonly IContentTypeService _contentTypeService; + private readonly GlobalSettings _globalSettings; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly ILogger _logger; + private readonly ILoggerFactory _loggerFactory; private readonly IMediaTypeService _mediaTypeService; private readonly IMemberTypeService _memberTypeService; - private readonly ILoggerFactory _loggerFactory; - private readonly ILogger _logger; + private readonly PropertyEditorCollection _propertyEditors; private readonly IShortStringHelper _shortStringHelper; - private readonly GlobalSettings _globalSettings; - private readonly IHostingEnvironment _hostingEnvironment; + private ContentSettings _contentSettings; - public ContentTypeMapDefinition(CommonMapper commonMapper, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IFileService fileService, - IContentTypeService contentTypeService, IMediaTypeService mediaTypeService, IMemberTypeService memberTypeService, - ILoggerFactory loggerFactory, IShortStringHelper shortStringHelper, IOptions globalSettings, IHostingEnvironment hostingEnvironment) + + [Obsolete("Use ctor with all params injected")] + public ContentTypeMapDefinition(CommonMapper commonMapper, PropertyEditorCollection propertyEditors, + IDataTypeService dataTypeService, IFileService fileService, + IContentTypeService contentTypeService, IMediaTypeService mediaTypeService, + IMemberTypeService memberTypeService, + ILoggerFactory loggerFactory, IShortStringHelper shortStringHelper, IOptions globalSettings, + IHostingEnvironment hostingEnvironment) + : this(commonMapper, propertyEditors, dataTypeService, fileService, contentTypeService, mediaTypeService, + memberTypeService, loggerFactory, shortStringHelper, globalSettings, hostingEnvironment, + StaticServiceProvider.Instance.GetRequiredService>()) + { + } + + public ContentTypeMapDefinition(CommonMapper commonMapper, PropertyEditorCollection propertyEditors, + IDataTypeService dataTypeService, IFileService fileService, + IContentTypeService contentTypeService, IMediaTypeService mediaTypeService, + IMemberTypeService memberTypeService, + ILoggerFactory loggerFactory, IShortStringHelper shortStringHelper, IOptions globalSettings, + IHostingEnvironment hostingEnvironment, IOptionsMonitor contentSettings) { _commonMapper = commonMapper; _propertyEditors = propertyEditors; @@ -51,13 +70,19 @@ public ContentTypeMapDefinition(CommonMapper commonMapper, PropertyEditorCollect _shortStringHelper = shortStringHelper; _globalSettings = globalSettings.Value; _hostingEnvironment = hostingEnvironment; + + _contentSettings = contentSettings.CurrentValue; + contentSettings.OnChange(x => _contentSettings = x); } public void DefineMaps(IUmbracoMapper mapper) { - mapper.Define((source, context) => new ContentType(_shortStringHelper, source.ParentId), Map); - mapper.Define((source, context) => new MediaType(_shortStringHelper, source.ParentId), Map); - mapper.Define((source, context) => new MemberType(_shortStringHelper, source.ParentId), Map); + mapper.Define( + (source, context) => new ContentType(_shortStringHelper, source.ParentId), Map); + mapper.Define( + (source, context) => new MediaType(_shortStringHelper, source.ParentId), Map); + mapper.Define( + (source, context) => new MemberType(_shortStringHelper, source.ParentId), Map); mapper.Define((source, context) => new DocumentTypeDisplay(), Map); mapper.Define((source, context) => new MediaTypeDisplay(), Map); @@ -66,14 +91,20 @@ public void DefineMaps(IUmbracoMapper mapper) mapper.Define( (source, context) => { - var dataType = _dataTypeService.GetDataType(source.DataTypeId); - if (dataType == null) throw new NullReferenceException("No data type found with id " + source.DataTypeId); + IDataType dataType = _dataTypeService.GetDataType(source.DataTypeId); + if (dataType == null) + { + throw new NullReferenceException("No data type found with id " + source.DataTypeId); + } + return new PropertyType(_shortStringHelper, dataType, source.Alias); }, Map); // TODO: isPublishing in ctor? - mapper.Define, PropertyGroup>((source, context) => new PropertyGroup(false), Map); - mapper.Define, PropertyGroup>((source, context) => new PropertyGroup(false), Map); + mapper.Define, PropertyGroup>( + (source, context) => new PropertyGroup(false), Map); + mapper.Define, PropertyGroup>( + (source, context) => new PropertyGroup(false), Map); mapper.Define((source, context) => new ContentTypeBasic(), Map); mapper.Define((source, context) => new ContentTypeBasic(), Map); @@ -84,11 +115,14 @@ public void DefineMaps(IUmbracoMapper mapper) mapper.Define((source, context) => new MediaTypeDisplay(), Map); mapper.Define((source, context) => new MemberTypeDisplay(), Map); - mapper.Define, PropertyGroupDisplay>((source, context) => new PropertyGroupDisplay(), Map); - mapper.Define, PropertyGroupDisplay>((source, context) => new PropertyGroupDisplay(), Map); + mapper.Define, PropertyGroupDisplay>( + (source, context) => new PropertyGroupDisplay(), Map); + mapper.Define, PropertyGroupDisplay>( + (source, context) => new PropertyGroupDisplay(), Map); mapper.Define((source, context) => new PropertyTypeDisplay(), Map); - mapper.Define((source, context) => new MemberPropertyTypeDisplay(), Map); + mapper.Define( + (source, context) => new MemberPropertyTypeDisplay(), Map); } // no MapAll - take care @@ -97,13 +131,20 @@ private void Map(DocumentTypeSave source, IContentType target, MapperContext con MapSaveToTypeBase(source, target, context); MapComposition(source, target, alias => _contentTypeService.Get(alias)); + if (target is IContentTypeWithHistoryCleanup targetWithHistoryCleanup) + { + targetWithHistoryCleanup.HistoryCleanup = source.HistoryCleanup; + } + target.AllowedTemplates = source.AllowedTemplates .Where(x => x != null) .Select(_fileService.GetTemplate) .Where(x => x != null) .ToArray(); - target.SetDefaultTemplate(source.DefaultTemplate == null ? null : _fileService.GetTemplate(source.DefaultTemplate)); + target.SetDefaultTemplate(source.DefaultTemplate == null + ? null + : _fileService.GetTemplate(source.DefaultTemplate)); } // no MapAll - take care @@ -119,11 +160,16 @@ private void Map(MemberTypeSave source, IMemberType target, MapperContext contex MapSaveToTypeBase(source, target, context); MapComposition(source, target, alias => _memberTypeService.Get(alias)); - foreach (var propertyType in source.Groups.SelectMany(x => x.Properties)) + foreach (MemberPropertyTypeBasic propertyType in source.Groups.SelectMany(x => x.Properties)) { - var localCopy = propertyType; - var destProp = target.PropertyTypes.SingleOrDefault(x => x.Alias.InvariantEquals(localCopy.Alias)); - if (destProp == null) continue; + MemberPropertyTypeBasic localCopy = propertyType; + IPropertyType destProp = + target.PropertyTypes.SingleOrDefault(x => x.Alias.InvariantEquals(localCopy.Alias)); + if (destProp == null) + { + continue; + } + target.SetMemberCanEditProperty(localCopy.Alias, localCopy.MemberCanEditProperty); target.SetMemberCanViewProperty(localCopy.Alias, localCopy.MemberCanViewProperty); target.SetIsSensitiveProperty(localCopy.Alias, localCopy.IsSensitiveData); @@ -135,6 +181,24 @@ private void Map(IContentType source, DocumentTypeDisplay target, MapperContext { MapTypeToDisplayBase(source, target); + if (source is IContentTypeWithHistoryCleanup sourceWithHistoryCleanup) + { + target.HistoryCleanup = new HistoryCleanupViewModel + { + PreventCleanup = sourceWithHistoryCleanup.HistoryCleanup.PreventCleanup, + KeepAllVersionsNewerThanDays = + sourceWithHistoryCleanup.HistoryCleanup.KeepAllVersionsNewerThanDays, + KeepLatestVersionPerDayForDays = + sourceWithHistoryCleanup.HistoryCleanup.KeepLatestVersionPerDayForDays, + GlobalKeepAllVersionsNewerThanDays = + _contentSettings.ContentVersionCleanupPolicy.KeepAllVersionsNewerThanDays, + GlobalKeepLatestVersionPerDayForDays = + _contentSettings.ContentVersionCleanupPolicy.KeepLatestVersionPerDayForDays, + GlobalEnableCleanup = _contentSettings.ContentVersionCleanupPolicy.EnableCleanup + }; + } + + target.AllowCultureVariant = source.VariesByCulture(); target.AllowSegmentVariant = source.VariesBySegment(); target.ContentApps = _commonMapper.GetContentApps(source); @@ -143,16 +207,23 @@ private void Map(IContentType source, DocumentTypeDisplay target, MapperContext target.AllowedTemplates = context.MapEnumerable(source.AllowedTemplates); if (source.DefaultTemplate != null) + { target.DefaultTemplate = context.Map(source.DefaultTemplate); + } //default listview target.ListViewEditorName = Constants.Conventions.DataTypes.ListViewPrefix + "Content"; - if (string.IsNullOrEmpty(source.Alias)) return; + if (string.IsNullOrEmpty(source.Alias)) + { + return; + } var name = Constants.Conventions.DataTypes.ListViewPrefix + source.Alias; if (_dataTypeService.GetDataType(name) != null) + { target.ListViewEditorName = name; + } } // no MapAll - take care @@ -164,11 +235,16 @@ private void Map(IMediaType source, MediaTypeDisplay target, MapperContext conte target.ListViewEditorName = Constants.Conventions.DataTypes.ListViewPrefix + "Media"; target.IsSystemMediaType = source.IsSystemMediaType(); - if (string.IsNullOrEmpty(source.Name)) return; + if (string.IsNullOrEmpty(source.Name)) + { + return; + } var name = Constants.Conventions.DataTypes.ListViewPrefix + source.Name; if (_dataTypeService.GetDataType(name) != null) + { target.ListViewEditorName = name; + } } // no MapAll - take care @@ -177,11 +253,16 @@ private void Map(IMemberType source, MemberTypeDisplay target, MapperContext con MapTypeToDisplayBase(source, target); //map the MemberCanEditProperty,MemberCanViewProperty,IsSensitiveData - foreach (var propertyType in source.PropertyTypes) + foreach (IPropertyType propertyType in source.PropertyTypes) { - var localCopy = propertyType; - var displayProp = target.Groups.SelectMany(dest => dest.Properties).SingleOrDefault(dest => dest.Alias.InvariantEquals(localCopy.Alias)); - if (displayProp == null) continue; + IPropertyType localCopy = propertyType; + MemberPropertyTypeDisplay displayProp = target.Groups.SelectMany(dest => dest.Properties) + .SingleOrDefault(dest => dest.Alias.InvariantEquals(localCopy.Alias)); + if (displayProp == null) + { + continue; + } + displayProp.MemberCanEditProperty = source.MemberCanEditProperty(localCopy.Alias); displayProp.MemberCanViewProperty = source.MemberCanViewProperty(localCopy.Alias); displayProp.IsSensitiveData = source.IsSensitiveProperty(localCopy.Alias); @@ -216,28 +297,20 @@ private void Map(IContentTypeBase source, ContentTypeBasic target, string entity } // no MapAll - uses the IContentTypeBase map method, which has MapAll - private void Map(IContentTypeComposition source, ContentTypeBasic target, MapperContext context) - { + private void Map(IContentTypeComposition source, ContentTypeBasic target, MapperContext context) => Map(source, target, Constants.UdiEntityType.MemberType); - } // no MapAll - uses the IContentTypeBase map method, which has MapAll - private void Map(IContentType source, ContentTypeBasic target, MapperContext context) - { + private void Map(IContentType source, ContentTypeBasic target, MapperContext context) => Map(source, target, Constants.UdiEntityType.DocumentType); - } // no MapAll - uses the IContentTypeBase map method, which has MapAll - private void Map(IMediaType source, ContentTypeBasic target, MapperContext context) - { + private void Map(IMediaType source, ContentTypeBasic target, MapperContext context) => Map(source, target, Constants.UdiEntityType.MediaType); - } // no MapAll - uses the IContentTypeBase map method, which has MapAll - private void Map(IMemberType source, ContentTypeBasic target, MapperContext context) - { + private void Map(IMemberType source, ContentTypeBasic target, MapperContext context) => Map(source, target, Constants.UdiEntityType.MemberType); - } // Umbraco.Code.MapAll -CreateDate -DeleteDate -UpdateDate // Umbraco.Code.MapAll -SupportsPublishing -Key -PropertyEditorAlias -ValueStorageType -Variations @@ -254,10 +327,14 @@ private static void Map(PropertyTypeBasic source, IPropertyType target, MapperCo target.SetVariesBy(ContentVariation.Segment, source.AllowSegmentVariant); if (source.Id > 0) + { target.Id = source.Id; + } if (source.GroupId > 0) + { target.PropertyGroupId = new Lazy(() => source.GroupId, false); + } target.Alias = source.Alias; target.Description = source.Description; @@ -268,18 +345,19 @@ private static void Map(PropertyTypeBasic source, IPropertyType target, MapperCo // no MapAll - take care private void Map(DocumentTypeSave source, DocumentTypeDisplay target, MapperContext context) { - MapTypeToDisplayBase(source, target, context); + MapTypeToDisplayBase(source, + target, context); //sync templates - var destAllowedTemplateAliases = target.AllowedTemplates.Select(x => x.Alias); + IEnumerable destAllowedTemplateAliases = target.AllowedTemplates.Select(x => x.Alias); //if the dest is set and it's the same as the source, then don't change if (destAllowedTemplateAliases.SequenceEqual(source.AllowedTemplates) == false) { - var templates = _fileService.GetTemplates(source.AllowedTemplates.ToArray()); + IEnumerable templates = _fileService.GetTemplates(source.AllowedTemplates.ToArray()); target.AllowedTemplates = source.AllowedTemplates .Select(x => { - var template = templates.SingleOrDefault(t => t.Alias == x); + ITemplate template = templates.SingleOrDefault(t => t.Alias == x); return template != null ? context.Map(template) : null; @@ -293,7 +371,7 @@ private void Map(DocumentTypeSave source, DocumentTypeDisplay target, MapperCont //if the dest is set and it's the same as the source, then don't change if (target.DefaultTemplate == null || source.DefaultTemplate != target.DefaultTemplate.Alias) { - var template = _fileService.GetTemplate(source.DefaultTemplate); + ITemplate template = _fileService.GetTemplate(source.DefaultTemplate); target.DefaultTemplate = template == null ? null : context.Map(template); } } @@ -304,22 +382,24 @@ private void Map(DocumentTypeSave source, DocumentTypeDisplay target, MapperCont } // no MapAll - take care - private void Map(MediaTypeSave source, MediaTypeDisplay target, MapperContext context) - { - MapTypeToDisplayBase(source, target, context); - } + private void Map(MediaTypeSave source, MediaTypeDisplay target, MapperContext context) => + MapTypeToDisplayBase(source, + target, context); // no MapAll - take care - private void Map(MemberTypeSave source, MemberTypeDisplay target, MapperContext context) - { - MapTypeToDisplayBase(source, target, context); - } + private void Map(MemberTypeSave source, MemberTypeDisplay target, MapperContext context) => + MapTypeToDisplayBase( + source, target, context); // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate -Key -PropertyTypes - private static void Map(PropertyGroupBasic source, PropertyGroup target, MapperContext context) + private static void Map(PropertyGroupBasic source, PropertyGroup target, + MapperContext context) { if (source.Id > 0) + { target.Id = source.Id; + } + target.Key = source.Key; target.Type = source.Type; target.Name = source.Name; @@ -328,10 +408,14 @@ private static void Map(PropertyGroupBasic source, PropertyGr } // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate -Key -PropertyTypes - private static void Map(PropertyGroupBasic source, PropertyGroup target, MapperContext context) + private static void Map(PropertyGroupBasic source, PropertyGroup target, + MapperContext context) { if (source.Id > 0) + { target.Id = source.Id; + } + target.Key = source.Key; target.Type = source.Type; target.Name = source.Name; @@ -340,11 +424,15 @@ private static void Map(PropertyGroupBasic source, Prop } // Umbraco.Code.MapAll -ContentTypeId -ParentTabContentTypes -ParentTabContentTypeNames - private static void Map(PropertyGroupBasic source, PropertyGroupDisplay target, MapperContext context) + private static void Map(PropertyGroupBasic source, + PropertyGroupDisplay target, MapperContext context) { target.Inherited = source.Inherited; if (source.Id > 0) + { target.Id = source.Id; + } + target.Key = source.Key; target.Type = source.Type; target.Name = source.Name; @@ -354,17 +442,22 @@ private static void Map(PropertyGroupBasic source, PropertyGr } // Umbraco.Code.MapAll -ContentTypeId -ParentTabContentTypes -ParentTabContentTypeNames - private static void Map(PropertyGroupBasic source, PropertyGroupDisplay target, MapperContext context) + private static void Map(PropertyGroupBasic source, + PropertyGroupDisplay target, MapperContext context) { target.Inherited = source.Inherited; if (source.Id > 0) + { target.Id = source.Id; + } + target.Key = source.Key; target.Type = source.Type; target.Name = source.Name; target.Alias = source.Alias; target.SortOrder = source.SortOrder; - target.Properties = context.MapEnumerable(source.Properties); + target.Properties = + context.MapEnumerable(source.Properties); } // Umbraco.Code.MapAll -Editor -View -Config -ContentTypeId -ContentTypeName -Locked -DataTypeIcon -DataTypeName @@ -409,7 +502,8 @@ private static void Map(MemberPropertyTypeBasic source, MemberPropertyTypeDispla // Umbraco.Code.MapAll -CreatorId -Level -SortOrder -Variations // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate // Umbraco.Code.MapAll -ContentTypeComposition (done by AfterMapSaveToType) - private static void MapSaveToTypeBase(TSource source, IContentTypeComposition target, MapperContext context) + private static void MapSaveToTypeBase(TSource source, + IContentTypeComposition target, MapperContext context) where TSource : ContentTypeSave where TSourcePropertyType : PropertyTypeBasic { @@ -418,7 +512,9 @@ private static void MapSaveToTypeBase(TSource sour var id = Convert.ToInt32(source.Id); if (id > 0) + { target.Id = id; + } target.Alias = source.Alias; target.Description = source.Description; @@ -457,18 +553,19 @@ private static void MapSaveToTypeBase(TSource sour // - managing the content type's PropertyTypes collection (for generic properties) // handle actual groups (non-generic-properties) - var destOrigGroups = target.PropertyGroups.ToArray(); // local groups - var destOrigProperties = target.PropertyTypes.ToArray(); // all properties, in groups or not + PropertyGroup[] destOrigGroups = target.PropertyGroups.ToArray(); // local groups + IPropertyType[] destOrigProperties = target.PropertyTypes.ToArray(); // all properties, in groups or not var destGroups = new List(); - var sourceGroups = source.Groups.Where(x => x.IsGenericProperties == false).ToArray(); + PropertyGroupBasic[] sourceGroups = + source.Groups.Where(x => x.IsGenericProperties == false).ToArray(); var sourceGroupParentAliases = sourceGroups.Select(x => x.GetParentAlias()).Distinct().ToArray(); - foreach (var sourceGroup in sourceGroups) + foreach (PropertyGroupBasic sourceGroup in sourceGroups) { // get the dest group - var destGroup = MapSaveGroup(sourceGroup, destOrigGroups, context); + PropertyGroup destGroup = MapSaveGroup(sourceGroup, destOrigGroups, context); // handle local properties - var destProperties = sourceGroup.Properties + IPropertyType[] destProperties = sourceGroup.Properties .Where(x => x.Inherited == false) .Select(x => MapSaveProperty(x, destOrigProperties, context)) .ToArray(); @@ -476,7 +573,9 @@ private static void MapSaveToTypeBase(TSource sour // if the group has no local properties and is not used as parent, skip it, ie sort-of garbage-collect // local groups which would not have local properties anymore if (destProperties.Length == 0 && !sourceGroupParentAliases.Contains(sourceGroup.Alias)) + { continue; + } // ensure no duplicate alias, then assign the group properties collection EnsureUniqueAliases(destProperties); @@ -492,11 +591,12 @@ private static void MapSaveToTypeBase(TSource sour // the old groups - they are just gone and will be cleared by the repository // handle non-grouped (ie generic) properties - var genericPropertiesGroup = source.Groups.FirstOrDefault(x => x.IsGenericProperties); + PropertyGroupBasic genericPropertiesGroup = + source.Groups.FirstOrDefault(x => x.IsGenericProperties); if (genericPropertiesGroup != null) { // handle local properties - var destProperties = genericPropertiesGroup.Properties + IPropertyType[] destProperties = genericPropertiesGroup.Properties .Where(x => x.Inherited == false) .Select(x => MapSaveProperty(x, destOrigProperties, context)) .ToArray(); @@ -547,7 +647,8 @@ private void MapTypeToDisplayBase(IContentTypeComp { MapTypeToDisplayBase(source, target); - var groupsMapper = new PropertyTypeGroupMapper(_propertyEditors, _dataTypeService, _shortStringHelper, _loggerFactory.CreateLogger>()); + var groupsMapper = new PropertyTypeGroupMapper(_propertyEditors, _dataTypeService, + _shortStringHelper, _loggerFactory.CreateLogger>()); target.Groups = groupsMapper.Map(source); } @@ -580,7 +681,8 @@ private void MapTypeToDisplayBase(ContentTypeSave source, ContentTypeComposition } // no MapAll - relies on the non-generic method - private void MapTypeToDisplayBase(TSource source, TTarget target, MapperContext context) + private void MapTypeToDisplayBase(TSource source, + TTarget target, MapperContext context) where TSource : ContentTypeSave where TSourcePropertyType : PropertyTypeBasic where TTarget : ContentTypeCompositionDisplay @@ -588,35 +690,49 @@ private void MapTypeToDisplayBase, PropertyGroupDisplay>(source.Groups); + target.Groups = + context + .MapEnumerable, PropertyGroupDisplay>( + source.Groups); } private IEnumerable MapLockedCompositions(IContentTypeComposition source) { // get ancestor ids from path of parent if not root if (source.ParentId == Constants.System.Root) + { return Enumerable.Empty(); + } - var parent = _contentTypeService.Get(source.ParentId); + IContentType parent = _contentTypeService.Get(source.ParentId); if (parent == null) + { return Enumerable.Empty(); + } var aliases = new List(); - var ancestorIds = parent.Path.Split(Constants.CharArrays.Comma).Select(s => int.Parse(s, CultureInfo.InvariantCulture)); + IEnumerable ancestorIds = parent.Path.Split(Constants.CharArrays.Comma) + .Select(s => int.Parse(s, CultureInfo.InvariantCulture)); // loop through all content types and return ordered aliases of ancestors - var allContentTypes = _contentTypeService.GetAll().ToArray(); + IContentType[] allContentTypes = _contentTypeService.GetAll().ToArray(); foreach (var ancestorId in ancestorIds) { - var ancestor = allContentTypes.FirstOrDefault(x => x.Id == ancestorId); + IContentType ancestor = allContentTypes.FirstOrDefault(x => x.Id == ancestorId); if (ancestor != null) + { aliases.Add(ancestor.Alias); + } } + return aliases.OrderBy(x => x); } public static Udi MapContentTypeUdi(IContentTypeComposition source) { - if (source == null) return null; + if (source == null) + { + return null; + } string udiType; switch (source) @@ -637,7 +753,8 @@ public static Udi MapContentTypeUdi(IContentTypeComposition source) return Udi.Create(udiType, source.Key); } - private static PropertyGroup MapSaveGroup(PropertyGroupBasic sourceGroup, IEnumerable destOrigGroups, MapperContext context) + private static PropertyGroup MapSaveGroup(PropertyGroupBasic sourceGroup, + IEnumerable destOrigGroups, MapperContext context) where TPropertyType : PropertyTypeBasic { PropertyGroup destGroup; @@ -663,7 +780,8 @@ private static PropertyGroup MapSaveGroup(PropertyGroupBasic destOrigProperties, MapperContext context) + private static IPropertyType MapSaveProperty(PropertyTypeBasic sourceProperty, + IEnumerable destOrigProperties, MapperContext context) { IPropertyType destProperty; if (sourceProperty.Id > 0) @@ -690,43 +808,52 @@ private static IPropertyType MapSaveProperty(PropertyTypeBasic sourceProperty, I private static void EnsureUniqueAliases(IEnumerable properties) { - var propertiesA = properties.ToArray(); + IPropertyType[] propertiesA = properties.ToArray(); var distinctProperties = propertiesA .Select(x => x.Alias?.ToUpperInvariant()) .Distinct() .Count(); if (distinctProperties != propertiesA.Length) + { throw new InvalidOperationException("Cannot map properties due to alias conflict."); + } } private static void EnsureUniqueAliases(IEnumerable groups) { - var groupsA = groups.ToArray(); + PropertyGroup[] groupsA = groups.ToArray(); var distinctProperties = groupsA .Select(x => x.Alias) .Distinct() .Count(); if (distinctProperties != groupsA.Length) + { throw new InvalidOperationException("Cannot map groups due to alias conflict."); + } } - private static void MapComposition(ContentTypeSave source, IContentTypeComposition target, Func getContentType) + private static void MapComposition(ContentTypeSave source, IContentTypeComposition target, + Func getContentType) { var current = target.CompositionAliases().ToArray(); - var proposed = source.CompositeContentTypes; + IEnumerable proposed = source.CompositeContentTypes; - var remove = current.Where(x => !proposed.Contains(x)); - var add = proposed.Where(x => !current.Contains(x)); + IEnumerable remove = current.Where(x => !proposed.Contains(x)); + IEnumerable add = proposed.Where(x => !current.Contains(x)); foreach (var alias in remove) + { target.RemoveContentType(alias); + } foreach (var alias in add) { // TODO: Remove N+1 lookup - var contentType = getContentType(alias); + IContentTypeComposition contentType = getContentType(alias); if (contentType != null) + { target.AddContentType(contentType); + } } } } diff --git a/src/Umbraco.Core/Models/Mapping/UserMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/UserMapDefinition.cs index 238065718002..e4d101ff062d 100644 --- a/src/Umbraco.Core/Models/Mapping/UserMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/UserMapDefinition.cs @@ -251,7 +251,7 @@ private void Map(IUserGroup source, UserGroupDisplay target, MapperContext conte // the entity service due to too many Sql parameters. var list = new List(); - foreach (var idGroup in allContentPermissions.Keys.InGroupsOf(2000)) + foreach (var idGroup in allContentPermissions.Keys.InGroupsOf(Constants.Sql.MaxParameterCount)) list.AddRange(_entityService.GetAll(UmbracoObjectTypes.Document, idGroup.ToArray())); contentEntities = list.ToArray(); } diff --git a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs index f0f891d48e0a..34c6284993ad 100644 --- a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs +++ b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs @@ -26,6 +26,8 @@ public static class Tables public const string Content = TableNamePrefix + "Content"; public const string ContentVersion = TableNamePrefix + "ContentVersion"; public const string ContentVersionCultureVariation = TableNamePrefix + "ContentVersionCultureVariation"; + public const string ContentVersionCleanupPolicy = TableNamePrefix + "ContentVersionCleanupPolicy"; + public const string Document = TableNamePrefix + "Document"; public const string DocumentCultureVariation = TableNamePrefix + "DocumentCultureVariation"; public const string DocumentVersion = TableNamePrefix + "DocumentVersion"; diff --git a/src/Umbraco.Core/Persistence/Repositories/IDocumentVersionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDocumentVersionRepository.cs new file mode 100644 index 000000000000..8175ba0d99a3 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/IDocumentVersionRepository.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Persistence.Repositories +{ + public interface IDocumentVersionRepository : IRepository + { + /// + /// Gets a list of all historic content versions. + /// + public IReadOnlyCollection GetDocumentVersionsEligibleForCleanup(); + + /// + /// Gets cleanup policy override settings per content type. + /// + public IReadOnlyCollection GetCleanupPolicies(); + + /// + /// Deletes multiple content versions by ID. + /// + void DeleteVersions(IEnumerable versionIds); + } +} diff --git a/src/Umbraco.Core/Services/IContentVersionCleanupPolicy.cs b/src/Umbraco.Core/Services/IContentVersionCleanupPolicy.cs new file mode 100644 index 000000000000..d2f70206b087 --- /dev/null +++ b/src/Umbraco.Core/Services/IContentVersionCleanupPolicy.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Services +{ + /// + /// Used to filter historic content versions for cleanup. + /// + public interface IContentVersionCleanupPolicy + { + /// + /// Filters a set of candidates historic content versions for cleanup according to policy settings. + /// + IEnumerable Apply(DateTime asAtDate, IEnumerable items); + } +} diff --git a/src/Umbraco.Core/Services/IContentVersionService.cs b/src/Umbraco.Core/Services/IContentVersionService.cs new file mode 100644 index 000000000000..882618780fe6 --- /dev/null +++ b/src/Umbraco.Core/Services/IContentVersionService.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Services +{ + public interface IContentVersionService + { + /// + /// Removes historic content versions according to a policy. + /// + IReadOnlyCollection PerformContentVersionCleanup(DateTime asAtDate); + } +} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 734ed2261d18..56cb1e6d01eb 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -1,61 +1,65 @@ - - netstandard2.0 - Umbraco.Cms.Core - Umbraco CMS - Umbraco.Cms.Core - Umbraco CMS Core - Contains the core assembly needed to run Umbraco Cms. This package only contains the assembly, and can be used for package development. Use the template in the Umbraco.Templates package to setup Umbraco - Umbraco CMS - + + netstandard2.0 + Umbraco.Cms.Core + Umbraco CMS + Umbraco.Cms.Core + Umbraco CMS Core + Contains the core assembly needed to run Umbraco Cms. This package only contains the assembly, and can be used for package development. Use the template in the Umbraco.Templates package to setup Umbraco + Umbraco CMS + - - bin\Release\Umbraco.Core.xml - + + bin\Release\Umbraco.Core.xml + - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - all - - + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + + - - - <_Parameter1>Umbraco.Tests - - - <_Parameter1>Umbraco.Tests.Common - - - <_Parameter1>Umbraco.Tests.UnitTests - - - <_Parameter1>Umbraco.Tests.Benchmarks - - - <_Parameter1>Umbraco.Tests.Integration - - - <_Parameter1>DynamicProxyGenAssembly2 - - + + + <_Parameter1>Umbraco.Tests + + + <_Parameter1>Umbraco.Tests.Common + + + <_Parameter1>Umbraco.Tests.UnitTests + + + <_Parameter1>Umbraco.Tests.Benchmarks + + + <_Parameter1>Umbraco.Tests.Integration + + + <_Parameter1>DynamicProxyGenAssembly2 + + - - - + + + + + + + diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs index 360e7859068c..d3ebb28f9c53 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs @@ -25,6 +25,7 @@ internal static IUmbracoBuilder AddRepositories(this IUmbracoBuilder builder) builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); + builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs index 861a05b45907..661ed9329292 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs @@ -6,7 +6,6 @@ using Microsoft.Extensions.Options; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; -using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Hosting; @@ -16,6 +15,7 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Implement; using Umbraco.Cms.Infrastructure.Packaging; +using Umbraco.Cms.Infrastructure.Services.Implement; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.DependencyInjection @@ -43,6 +43,8 @@ internal static IUmbracoBuilder AddServices(this IUmbracoBuilder builder) builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); diff --git a/src/Umbraco.Infrastructure/HostedServices/ContentVersionCleanup.cs b/src/Umbraco.Infrastructure/HostedServices/ContentVersionCleanup.cs new file mode 100644 index 000000000000..5f3aba5f3f3d --- /dev/null +++ b/src/Umbraco.Infrastructure/HostedServices/ContentVersionCleanup.cs @@ -0,0 +1,96 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Runtime; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Sync; + +namespace Umbraco.Cms.Infrastructure.HostedServices +{ + /// + /// Recurring hosted service that executes the content history cleanup. + /// + public class ContentVersionCleanup : RecurringHostedServiceBase + { + private readonly IRuntimeState _runtimeState; + private readonly ILogger _logger; + private readonly IOptionsMonitor _settingsMonitor; + private readonly IContentVersionService _service; + private readonly IMainDom _mainDom; + private readonly IServerRoleAccessor _serverRoleAccessor; + + /// + /// Initializes a new instance of the class. + /// + public ContentVersionCleanup( + IRuntimeState runtimeState, + ILogger logger, + IOptionsMonitor settingsMonitor, + IContentVersionService service, + IMainDom mainDom, + IServerRoleAccessor serverRoleAccessor) + : base(TimeSpan.FromHours(1), TimeSpan.FromMinutes(3)) + { + _runtimeState = runtimeState; + _logger = logger; + _settingsMonitor = settingsMonitor; + _service = service; + _mainDom = mainDom; + _serverRoleAccessor = serverRoleAccessor; + } + + /// + public override Task PerformExecuteAsync(object state) + { + // Globally disabled by feature flag + if (!_settingsMonitor.CurrentValue.ContentVersionCleanupPolicy.EnableCleanup) + { + _logger.LogInformation("ContentVersionCleanup task will not run as it has been globally disabled via configuration"); + return Task.CompletedTask; + } + + if (_runtimeState.Level != RuntimeLevel.Run) + { + return Task.FromResult(true); // repeat... + } + + // Don't run on replicas nor unknown role servers + switch (_serverRoleAccessor.CurrentServerRole) + { + case ServerRole.Subscriber: + _logger.LogDebug("Does not run on subscriber servers"); + return Task.CompletedTask; + case ServerRole.Unknown: + _logger.LogDebug("Does not run on servers with unknown role"); + return Task.CompletedTask; + case ServerRole.Single: + case ServerRole.SchedulingPublisher: + default: + break; + } + + // Ensure we do not run if not main domain, but do NOT lock it + if (!_mainDom.IsMainDom) + { + _logger.LogDebug("Does not run if not MainDom"); + return Task.FromResult(false); // do NOT repeat, going down + } + + var count = _service.PerformContentVersionCleanup(DateTime.Now).Count; + + if (count > 0) + { + _logger.LogInformation("Deleted {count} ContentVersion(s)", count); + } + else + { + _logger.LogDebug("Task complete, no items were Deleted"); + } + + return Task.FromResult(true); + } + } +} diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs index 4052a8ae56f1..c20c157604b4 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs @@ -12,21 +12,82 @@ using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Extensions; +using ColumnInfo = Umbraco.Cms.Infrastructure.Persistence.SqlSyntax.ColumnInfo; namespace Umbraco.Cms.Infrastructure.Migrations.Install { /// - /// Creates the initial database schema during install. + /// Creates the initial database schema during install. /// public class DatabaseSchemaCreator { + // all tables, in order + internal static readonly List OrderedTables = new() + { + typeof(UserDto), + typeof(NodeDto), + typeof(ContentTypeDto), + typeof(TemplateDto), + typeof(ContentDto), + typeof(ContentVersionDto), + typeof(MediaVersionDto), + typeof(DocumentDto), + typeof(ContentTypeTemplateDto), + typeof(DataTypeDto), + typeof(DictionaryDto), + typeof(LanguageDto), + typeof(LanguageTextDto), + typeof(DomainDto), + typeof(LogDto), + typeof(MacroDto), + typeof(MacroPropertyDto), + typeof(MemberPropertyTypeDto), + typeof(MemberDto), + typeof(Member2MemberGroupDto), + typeof(PropertyTypeGroupDto), + typeof(PropertyTypeDto), + typeof(PropertyDataDto), + typeof(RelationTypeDto), + typeof(RelationDto), + typeof(TagDto), + typeof(TagRelationshipDto), + typeof(ContentType2ContentTypeDto), + typeof(ContentTypeAllowedContentTypeDto), + typeof(User2NodeNotifyDto), + typeof(ServerRegistrationDto), + typeof(AccessDto), + typeof(AccessRuleDto), + typeof(CacheInstructionDto), + typeof(ExternalLoginDto), + typeof(ExternalLoginTokenDto), + typeof(RedirectUrlDto), + typeof(LockDto), + typeof(UserGroupDto), + typeof(User2UserGroupDto), + typeof(UserGroup2NodePermissionDto), + typeof(UserGroup2AppDto), + typeof(UserStartNodeDto), + typeof(ContentNuDto), + typeof(DocumentVersionDto), + typeof(KeyValueDto), + typeof(UserLoginDto), + typeof(ConsentDto), + typeof(AuditEntryDto), + typeof(ContentVersionCultureVariationDto), + typeof(DocumentCultureVariationDto), + typeof(ContentScheduleDto), + typeof(LogViewerQueryDto), + typeof(ContentVersionCleanupPolicyDto) + }; + private readonly IUmbracoDatabase _database; + private readonly IEventAggregator _eventAggregator; private readonly ILogger _logger; private readonly ILoggerFactory _loggerFactory; private readonly IUmbracoVersion _umbracoVersion; - private readonly IEventAggregator _eventAggregator; - public DatabaseSchemaCreator(IUmbracoDatabase database, ILogger logger, ILoggerFactory loggerFactory, IUmbracoVersion umbracoVersion, IEventAggregator eventAggregator) + public DatabaseSchemaCreator(IUmbracoDatabase database, ILogger logger, + ILoggerFactory loggerFactory, IUmbracoVersion umbracoVersion, IEventAggregator eventAggregator) { _database = database ?? throw new ArgumentNullException(nameof(database)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -42,74 +103,16 @@ public DatabaseSchemaCreator(IUmbracoDatabase database, ILogger _database.SqlContext.SqlSyntax; - // all tables, in order - internal static readonly List OrderedTables = new List - { - typeof (UserDto), - typeof (NodeDto), - typeof (ContentTypeDto), - typeof (TemplateDto), - typeof (ContentDto), - typeof (ContentVersionDto), - typeof (MediaVersionDto), - typeof (DocumentDto), - typeof (ContentTypeTemplateDto), - typeof (DataTypeDto), - typeof (DictionaryDto), - typeof (LanguageDto), - typeof (LanguageTextDto), - typeof (DomainDto), - typeof (LogDto), - typeof (MacroDto), - typeof (MacroPropertyDto), - typeof (MemberPropertyTypeDto), - typeof (MemberDto), - typeof (Member2MemberGroupDto), - typeof (PropertyTypeGroupDto), - typeof (PropertyTypeDto), - typeof (PropertyDataDto), - typeof (RelationTypeDto), - typeof (RelationDto), - typeof (TagDto), - typeof (TagRelationshipDto), - typeof (ContentType2ContentTypeDto), - typeof (ContentTypeAllowedContentTypeDto), - typeof (User2NodeNotifyDto), - typeof (ServerRegistrationDto), - typeof (AccessDto), - typeof (AccessRuleDto), - typeof (CacheInstructionDto), - typeof (ExternalLoginDto), - typeof (ExternalLoginTokenDto), - typeof (RedirectUrlDto), - typeof (LockDto), - typeof (UserGroupDto), - typeof (User2UserGroupDto), - typeof (UserGroup2NodePermissionDto), - typeof (UserGroup2AppDto), - typeof (UserStartNodeDto), - typeof (ContentNuDto), - typeof (DocumentVersionDto), - typeof (KeyValueDto), - typeof (UserLoginDto), - typeof (ConsentDto), - typeof (AuditEntryDto), - typeof (ContentVersionCultureVariationDto), - typeof (DocumentCultureVariationDto), - typeof (ContentScheduleDto), - typeof (LogViewerQueryDto) - }; - /// - /// Drops all Umbraco tables in the db. + /// Drops all Umbraco tables in the db. /// internal void UninstallDatabaseSchema() { _logger.LogInformation("Start UninstallDatabaseSchema"); - foreach (var table in OrderedTables.AsEnumerable().Reverse()) + foreach (Type table in OrderedTables.AsEnumerable().Reverse()) { - var tableNameAttribute = table.FirstAttribute(); + TableNameAttribute tableNameAttribute = table.FirstAttribute(); var tableName = tableNameAttribute == null ? table.Name : tableNameAttribute.Value; _logger.LogInformation("Uninstall {TableName}", tableName); @@ -117,7 +120,9 @@ internal void UninstallDatabaseSchema() try { if (TableExists(tableName)) + { DropTable(tableName); + } } catch (Exception ex) { @@ -129,13 +134,15 @@ internal void UninstallDatabaseSchema() } /// - /// Initializes the database by creating the umbraco db schema. + /// Initializes the database by creating the umbraco db schema. /// /// This needs to execute as part of a transaction. public void InitializeDatabaseSchema() { if (!_database.InTransaction) + { throw new InvalidOperationException("Database is not in a transaction."); + } var eventMessages = new EventMessages(); var creatingNotification = new DatabaseSchemaCreatingNotification(eventMessages); @@ -143,22 +150,23 @@ public void InitializeDatabaseSchema() if (creatingNotification.Cancel == false) { - var dataCreation = new DatabaseDataCreator(_database, _loggerFactory.CreateLogger(), _umbracoVersion); - foreach (var table in OrderedTables) + var dataCreation = new DatabaseDataCreator(_database, + _loggerFactory.CreateLogger(), _umbracoVersion); + foreach (Type table in OrderedTables) + { CreateTable(false, table, dataCreation); + } } - DatabaseSchemaCreatedNotification createdNotification = new DatabaseSchemaCreatedNotification(eventMessages).WithStateFrom(creatingNotification); + DatabaseSchemaCreatedNotification createdNotification = + new DatabaseSchemaCreatedNotification(eventMessages).WithStateFrom(creatingNotification); FireAfterCreation(createdNotification); } /// - /// Validates the schema of the current database. + /// Validates the schema of the current database. /// - internal DatabaseSchemaResult ValidateSchema() - { - return ValidateSchema(OrderedTables); - } + internal DatabaseSchemaResult ValidateSchema() => ValidateSchema(OrderedTables); internal DatabaseSchemaResult ValidateSchema(IEnumerable orderedTables) { @@ -179,26 +187,30 @@ internal DatabaseSchemaResult ValidateSchema(IEnumerable orderedTables) } /// - /// This validates the Primary/Foreign keys in the database + /// This validates the Primary/Foreign keys in the database /// /// /// - /// This does not validate any database constraints that are not PKs or FKs because Umbraco does not create a database with non PK/FK constraints. - /// Any unique "constraints" in the database are done with unique indexes. + /// This does not validate any database constraints that are not PKs or FKs because Umbraco does not create a database + /// with non PK/FK constraints. + /// Any unique "constraints" in the database are done with unique indexes. /// private void ValidateDbConstraints(DatabaseSchemaResult result) { //Check constraints in configured database against constraints in schema var constraintsInDatabase = SqlSyntax.GetConstraintsPerColumn(_database).DistinctBy(x => x.Item3).ToList(); - var foreignKeysInDatabase = constraintsInDatabase.Where(x => x.Item3.InvariantStartsWith("FK_")).Select(x => x.Item3).ToList(); - var primaryKeysInDatabase = constraintsInDatabase.Where(x => x.Item3.InvariantStartsWith("PK_")).Select(x => x.Item3).ToList(); + var foreignKeysInDatabase = constraintsInDatabase.Where(x => x.Item3.InvariantStartsWith("FK_")) + .Select(x => x.Item3).ToList(); + var primaryKeysInDatabase = constraintsInDatabase.Where(x => x.Item3.InvariantStartsWith("PK_")) + .Select(x => x.Item3).ToList(); var unknownConstraintsInDatabase = constraintsInDatabase.Where( x => - x.Item3.InvariantStartsWith("FK_") == false && x.Item3.InvariantStartsWith("PK_") == false && - x.Item3.InvariantStartsWith("IX_") == false).Select(x => x.Item3).ToList(); - var foreignKeysInSchema = result.TableDefinitions.SelectMany(x => x.ForeignKeys.Select(y => y.Name)).ToList(); + x.Item3.InvariantStartsWith("FK_") == false && x.Item3.InvariantStartsWith("PK_") == false && + x.Item3.InvariantStartsWith("IX_") == false).Select(x => x.Item3).ToList(); + var foreignKeysInSchema = + result.TableDefinitions.SelectMany(x => x.ForeignKeys.Select(y => y.Name)).ToList(); var primaryKeysInSchema = result.TableDefinitions.SelectMany(x => x.Columns.Select(y => y.PrimaryKeyName)) .Where(x => x.IsNullOrWhiteSpace() == false).ToList(); @@ -219,14 +231,17 @@ private void ValidateDbConstraints(DatabaseSchemaResult result) //Foreign keys: - var validForeignKeyDifferences = foreignKeysInDatabase.Intersect(foreignKeysInSchema, StringComparer.InvariantCultureIgnoreCase); + IEnumerable validForeignKeyDifferences = + foreignKeysInDatabase.Intersect(foreignKeysInSchema, StringComparer.InvariantCultureIgnoreCase); foreach (var foreignKey in validForeignKeyDifferences) { result.ValidConstraints.Add(foreignKey); } - var invalidForeignKeyDifferences = + + IEnumerable invalidForeignKeyDifferences = foreignKeysInDatabase.Except(foreignKeysInSchema, StringComparer.InvariantCultureIgnoreCase) - .Union(foreignKeysInSchema.Except(foreignKeysInDatabase, StringComparer.InvariantCultureIgnoreCase)); + .Union(foreignKeysInSchema.Except(foreignKeysInDatabase, + StringComparer.InvariantCultureIgnoreCase)); foreach (var foreignKey in invalidForeignKeyDifferences) { result.Errors.Add(new Tuple("Constraint", foreignKey)); @@ -236,37 +251,43 @@ private void ValidateDbConstraints(DatabaseSchemaResult result) //Primary keys: //Add valid and invalid primary key differences to the result object - var validPrimaryKeyDifferences = primaryKeysInDatabase.Intersect(primaryKeysInSchema, StringComparer.InvariantCultureIgnoreCase); + IEnumerable validPrimaryKeyDifferences = + primaryKeysInDatabase.Intersect(primaryKeysInSchema, StringComparer.InvariantCultureIgnoreCase); foreach (var primaryKey in validPrimaryKeyDifferences) { result.ValidConstraints.Add(primaryKey); } - var invalidPrimaryKeyDifferences = + + IEnumerable invalidPrimaryKeyDifferences = primaryKeysInDatabase.Except(primaryKeysInSchema, StringComparer.InvariantCultureIgnoreCase) - .Union(primaryKeysInSchema.Except(primaryKeysInDatabase, StringComparer.InvariantCultureIgnoreCase)); + .Union(primaryKeysInSchema.Except(primaryKeysInDatabase, + StringComparer.InvariantCultureIgnoreCase)); foreach (var primaryKey in invalidPrimaryKeyDifferences) { result.Errors.Add(new Tuple("Constraint", primaryKey)); } - } private void ValidateDbColumns(DatabaseSchemaResult result) { //Check columns in configured database against columns in schema - var columnsInDatabase = SqlSyntax.GetColumnsInSchema(_database); - var columnsPerTableInDatabase = columnsInDatabase.Select(x => string.Concat(x.TableName, ",", x.ColumnName)).ToList(); - var columnsPerTableInSchema = result.TableDefinitions.SelectMany(x => x.Columns.Select(y => string.Concat(y.TableName, ",", y.Name))).ToList(); + IEnumerable columnsInDatabase = SqlSyntax.GetColumnsInSchema(_database); + var columnsPerTableInDatabase = + columnsInDatabase.Select(x => string.Concat(x.TableName, ",", x.ColumnName)).ToList(); + var columnsPerTableInSchema = result.TableDefinitions + .SelectMany(x => x.Columns.Select(y => string.Concat(y.TableName, ",", y.Name))).ToList(); //Add valid and invalid column differences to the result object - var validColumnDifferences = columnsPerTableInDatabase.Intersect(columnsPerTableInSchema, StringComparer.InvariantCultureIgnoreCase); + IEnumerable validColumnDifferences = + columnsPerTableInDatabase.Intersect(columnsPerTableInSchema, StringComparer.InvariantCultureIgnoreCase); foreach (var column in validColumnDifferences) { result.ValidColumns.Add(column); } - var invalidColumnDifferences = + IEnumerable invalidColumnDifferences = columnsPerTableInDatabase.Except(columnsPerTableInSchema, StringComparer.InvariantCultureIgnoreCase) - .Union(columnsPerTableInSchema.Except(columnsPerTableInDatabase, StringComparer.InvariantCultureIgnoreCase)); + .Union(columnsPerTableInSchema.Except(columnsPerTableInDatabase, + StringComparer.InvariantCultureIgnoreCase)); foreach (var column in invalidColumnDifferences) { result.Errors.Add(new Tuple("Column", column)); @@ -279,15 +300,16 @@ private void ValidateDbTables(DatabaseSchemaResult result) var tablesInDatabase = SqlSyntax.GetTablesInSchema(_database).ToList(); var tablesInSchema = result.TableDefinitions.Select(x => x.Name).ToList(); //Add valid and invalid table differences to the result object - var validTableDifferences = tablesInDatabase.Intersect(tablesInSchema, StringComparer.InvariantCultureIgnoreCase); + IEnumerable validTableDifferences = + tablesInDatabase.Intersect(tablesInSchema, StringComparer.InvariantCultureIgnoreCase); foreach (var tableName in validTableDifferences) { result.ValidTables.Add(tableName); } - var invalidTableDifferences = + IEnumerable invalidTableDifferences = tablesInDatabase.Except(tablesInSchema, StringComparer.InvariantCultureIgnoreCase) - .Union(tablesInSchema.Except(tablesInDatabase, StringComparer.InvariantCultureIgnoreCase)); + .Union(tablesInSchema.Except(tablesInDatabase, StringComparer.InvariantCultureIgnoreCase)); foreach (var tableName in invalidTableDifferences) { result.Errors.Add(new Tuple("Table", tableName)); @@ -302,15 +324,16 @@ private void ValidateDbIndexes(DatabaseSchemaResult result) var indexesInSchema = result.TableDefinitions.SelectMany(x => x.Indexes.Select(y => y.Name)).ToList(); //Add valid and invalid index differences to the result object - var validColIndexDifferences = colIndexesInDatabase.Intersect(indexesInSchema, StringComparer.InvariantCultureIgnoreCase); + IEnumerable validColIndexDifferences = + colIndexesInDatabase.Intersect(indexesInSchema, StringComparer.InvariantCultureIgnoreCase); foreach (var index in validColIndexDifferences) { result.ValidIndexes.Add(index); } - var invalidColIndexDifferences = + IEnumerable invalidColIndexDifferences = colIndexesInDatabase.Except(indexesInSchema, StringComparer.InvariantCultureIgnoreCase) - .Union(indexesInSchema.Except(colIndexesInDatabase, StringComparer.InvariantCultureIgnoreCase)); + .Union(indexesInSchema.Except(colIndexesInDatabase, StringComparer.InvariantCultureIgnoreCase)); foreach (var index in invalidColIndexDifferences) { result.Errors.Add(new Tuple("Index", index)); @@ -320,14 +343,14 @@ private void ValidateDbIndexes(DatabaseSchemaResult result) #region Notifications /// - /// Publishes the notification. + /// Publishes the notification. /// /// Cancelable notification marking the creation having begun. internal virtual void FireBeforeCreation(DatabaseSchemaCreatingNotification notification) => _eventAggregator.Publish(notification); /// - /// Publishes the notification. + /// Publishes the notification. /// /// Notification marking the creation having completed. internal virtual void FireAfterCreation(DatabaseSchemaCreatedNotification notification) => @@ -338,30 +361,27 @@ internal virtual void FireAfterCreation(DatabaseSchemaCreatedNotification notifi #region Utilities /// - /// Returns whether a table with the specified exists in the database. + /// Returns whether a table with the specified exists in the database. /// /// The name of the table. /// true if the table exists; otherwise false. /// - /// + /// /// if (schemaHelper.TableExist("MyTable")) /// { /// // do something when the table exists /// } /// /// - public bool TableExists(string tableName) - { - return SqlSyntax.DoesTableExist(_database, tableName); - } + public bool TableExists(string tableName) => SqlSyntax.DoesTableExist(_database, tableName); /// - /// Returns whether the table for the specified exists in the database. + /// Returns whether the table for the specified exists in the database. /// /// The type representing the DTO/table. /// true if the table exists; otherwise false. /// - /// + /// /// if (schemaHelper.TableExist<MyDto>) /// { /// // do something when the table exists @@ -369,66 +389,67 @@ public bool TableExists(string tableName) /// /// /// - /// If has been decorated with an , the name from that - /// attribute will be used for the table name. If the attribute is not present, the name - /// will be used instead. + /// If has been decorated with an , the name from that + /// attribute will be used for the table name. If the attribute is not present, the name + /// will be used instead. /// public bool TableExists() { - var table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); + TableDefinition table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); return table != null && TableExists(table.Name); } /// - /// Creates a new table in the database based on the type of . + /// Creates a new table in the database based on the type of . /// /// The type representing the DTO/table. /// Whether the table should be overwritten if it already exists. /// - /// If has been decorated with an , the name from that - /// attribute will be used for the table name. If the attribute is not present, the name - /// will be used instead. - /// - /// If a table with the same name already exists, the parameter will determine - /// whether the table is overwritten. If true, the table will be overwritten, whereas this method will - /// not do anything if the parameter is false. + /// If has been decorated with an , the name from that + /// attribute will be used for the table name. If the attribute is not present, the name + /// will be used instead. + /// If a table with the same name already exists, the parameter will determine + /// whether the table is overwritten. If true, the table will be overwritten, whereas this method will + /// not do anything if the parameter is false. /// internal void CreateTable(bool overwrite = false) where T : new() { - var tableType = typeof(T); - CreateTable(overwrite, tableType, new DatabaseDataCreator(_database, _loggerFactory.CreateLogger(), _umbracoVersion)); + Type tableType = typeof(T); + CreateTable(overwrite, tableType, + new DatabaseDataCreator(_database, _loggerFactory.CreateLogger(), + _umbracoVersion)); } /// - /// Creates a new table in the database for the specified . + /// Creates a new table in the database for the specified . /// /// Whether the table should be overwritten if it already exists. /// The representing the table. /// /// - /// If has been decorated with an , the name from - /// that attribute will be used for the table name. If the attribute is not present, the name - /// will be used instead. - /// - /// If a table with the same name already exists, the parameter will determine - /// whether the table is overwritten. If true, the table will be overwritten, whereas this method will - /// not do anything if the parameter is false. - /// - /// This need to execute as part of a transaction. + /// If has been decorated with an , the name from + /// that attribute will be used for the table name. If the attribute is not present, the name + /// will be used instead. + /// If a table with the same name already exists, the parameter will determine + /// whether the table is overwritten. If true, the table will be overwritten, whereas this method will + /// not do anything if the parameter is false. + /// This need to execute as part of a transaction. /// internal void CreateTable(bool overwrite, Type modelType, DatabaseDataCreator dataCreation) { if (!_database.InTransaction) + { throw new InvalidOperationException("Database is not in a transaction."); + } - var tableDefinition = DefinitionFactory.GetTableDefinition(modelType, SqlSyntax); + TableDefinition tableDefinition = DefinitionFactory.GetTableDefinition(modelType, SqlSyntax); var tableName = tableDefinition.Name; var createSql = SqlSyntax.Format(tableDefinition); var createPrimaryKeySql = SqlSyntax.FormatPrimaryKey(tableDefinition); - var foreignSql = SqlSyntax.Format(tableDefinition.ForeignKeys); - var indexSql = SqlSyntax.Format(tableDefinition.Indexes); + List foreignSql = SqlSyntax.Format(tableDefinition.ForeignKeys); + List indexSql = SqlSyntax.Format(tableDefinition.Indexes); var tableExist = TableExists(tableName); if (overwrite && tableExist) @@ -458,7 +479,9 @@ internal void CreateTable(bool overwrite, Type modelType, DatabaseDataCreator da } if (SqlSyntax.SupportsIdentityInsert() && tableDefinition.Columns.Any(x => x.IsIdentity)) + { _database.Execute(new Sql($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(tableName)} ON ")); + } //Call the NewTable-event to trigger the insert of base/default data //OnNewTable(tableName, _db, e, _logger); @@ -466,7 +489,9 @@ internal void CreateTable(bool overwrite, Type modelType, DatabaseDataCreator da dataCreation.InitializeBaseData(tableName); if (SqlSyntax.SupportsIdentityInsert() && tableDefinition.Columns.Any(x => x.IsIdentity)) + { _database.Execute(new Sql($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(tableName)} OFF;")); + } //Loop through index statements and execute sql foreach (var sql in indexSql) @@ -489,23 +514,22 @@ internal void CreateTable(bool overwrite, Type modelType, DatabaseDataCreator da else { _logger.LogInformation("New table {TableName} was created", tableName); - } } /// - /// Drops the table for the specified . + /// Drops the table for the specified . /// /// The type representing the DTO/table. /// - /// + /// /// schemaHelper.DropTable<MyDto>); /// /// /// - /// If has been decorated with an , the name from that - /// attribute will be used for the table name. If the attribute is not present, the name - /// will be used instead. + /// If has been decorated with an , the name from that + /// attribute will be used for the table name. If the attribute is not present, the name + /// will be used instead. /// public void DropTable(string tableName) { diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index ea629e5c3afb..76a67fd33078 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -1,4 +1,5 @@ using System; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Semver; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.Common; @@ -12,47 +13,88 @@ using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_7_0; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_9_0; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0; +using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_1_0; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade { /// - /// Represents the Umbraco CMS migration plan. + /// Represents the Umbraco CMS migration plan. /// /// public class UmbracoPlan : MigrationPlan { - private readonly IUmbracoVersion _umbracoVersion; private const string InitPrefix = "{init-"; private const string InitSuffix = "}"; + private readonly IUmbracoVersion _umbracoVersion; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The Umbraco version. public UmbracoPlan(IUmbracoVersion umbracoVersion) - : base(Core.Constants.Conventions.Migrations.UmbracoUpgradePlanName) + : base(Constants.Conventions.Migrations.UmbracoUpgradePlanName) { _umbracoVersion = umbracoVersion; DefinePlan(); } + /// + /// + /// The default initial state in plans is string.Empty. + /// + /// When upgrading from version 7, we want to use specific initial states + /// that are e.g. "{init-7.9.3}", "{init-7.11.1}", etc. so we can chain the proper + /// migrations. + /// + /// + /// This is also where we detect the current version, and reject invalid + /// upgrades (from a tool old version, or going back in time, etc). + /// + /// + public override string InitialState + { + get + { + SemVersion currentVersion = _umbracoVersion.SemanticVersion; + + // only from 8.0.0 and above + var minVersion = new SemVersion(8); + if (currentVersion < minVersion) + { + throw new InvalidOperationException( + $"Version {currentVersion} cannot be migrated to {_umbracoVersion.SemanticVersion}." + + $" Please upgrade first to at least {minVersion}."); + } + + // Force versions between 7.14.*-7.15.* into into 7.14 initial state. Because there is no db-changes, + // and we don't want users to workaround my putting in version 7.14.0 them self. + if (minVersion <= currentVersion && currentVersion < new SemVersion(7, 16)) + { + return GetInitState(minVersion); + } + + // initial state is eg "{init-7.14.0}" + return GetInitState(currentVersion); + } + } + /// - /// Gets the initial state corresponding to a version. + /// Gets the initial state corresponding to a version. /// /// The version. /// - /// The initial state. + /// The initial state. /// private static string GetInitState(SemVersion version) => InitPrefix + version + InitSuffix; /// - /// Tries to extract a version from an initial state. + /// Tries to extract a version from an initial state. /// /// The state. /// The version. /// - /// true when the state contains a version; otherwise, false.D + /// true when the state contains a version; otherwise, false.D /// private static bool TryGetInitStateVersion(string state, out string version) { @@ -66,51 +108,21 @@ private static bool TryGetInitStateVersion(string state, out string version) return false; } - /// - /// - /// The default initial state in plans is string.Empty. - /// When upgrading from version 7, we want to use specific initial states - /// that are e.g. "{init-7.9.3}", "{init-7.11.1}", etc. so we can chain the proper - /// migrations. - /// This is also where we detect the current version, and reject invalid - /// upgrades (from a tool old version, or going back in time, etc). - /// - public override string InitialState - { - get - { - var currentVersion = _umbracoVersion.SemanticVersion; - - // only from 8.0.0 and above - var minVersion = new SemVersion(8, 0); - if (currentVersion < minVersion) - throw new InvalidOperationException($"Version {currentVersion} cannot be migrated to {_umbracoVersion.SemanticVersion}." - + $" Please upgrade first to at least {minVersion}."); - - // Force versions between 7.14.*-7.15.* into into 7.14 initial state. Because there is no db-changes, - // and we don't want users to workaround my putting in version 7.14.0 them self. - if (minVersion <= currentVersion && currentVersion < new SemVersion(7, 16)) - return GetInitState(minVersion); - - // initial state is eg "{init-7.14.0}" - return GetInitState(currentVersion); - } - } - /// public override void ThrowOnUnknownInitialState(string state) { if (TryGetInitStateVersion(state, out var initVersion)) { - throw new InvalidOperationException($"Version {_umbracoVersion.SemanticVersion} does not support migrating from {initVersion}." - + $" Please verify which versions support migrating from {initVersion}."); + throw new InvalidOperationException( + $"Version {_umbracoVersion.SemanticVersion} does not support migrating from {initVersion}." + + $" Please verify which versions support migrating from {initVersion}."); } base.ThrowOnUnknownInitialState(state); } /// - /// Defines the plan. + /// Defines the plan. /// protected void DefinePlan() { @@ -131,7 +143,7 @@ protected void DefinePlan() // plan starts at 7.14.0 (anything before 7.14.0 is not supported) - From(GetInitState(new SemVersion(7, 14, 0))); + From(GetInitState(new SemVersion(7, 14))); // begin migrating from v7 - remove all keys and indexes To("{B36B9ABD-374E-465B-9C5F-26AB0D39326F}"); @@ -146,14 +158,15 @@ protected void DefinePlan() To("{9DF05B77-11D1-475C-A00A-B656AF7E0908}"); To("{6FE3EF34-44A0-4992-B379-B40BC4EF1C4D}"); To("{7F59355A-0EC9-4438-8157-EB517E6D2727}"); - ToWithReplace("{941B2ABA-2D06-4E04-81F5-74224F1DB037}", "{76DF5CD7-A884-41A5-8DC6-7860D95B1DF5}"); // kill AddVariationTable1 + ToWithReplace("{941B2ABA-2D06-4E04-81F5-74224F1DB037}", + "{76DF5CD7-A884-41A5-8DC6-7860D95B1DF5}"); // kill AddVariationTable1 To("{A7540C58-171D-462A-91C5-7A9AA5CB8BFD}"); Merge() .To("{3E44F712-E2E3-473A-AE49-5D7F8E67CE3F}") - .With() + .With() .To("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}") - .As("{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); + .As("{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); To("{1350617A-4930-4D61-852F-E3AA9E692173}"); To("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}"); @@ -175,9 +188,9 @@ protected void DefinePlan() Merge() .To("{CDBEDEE4-9496-4903-9CF2-4104E00FF960}") - .With() + .With() .To("{940FD19A-00A8-4D5C-B8FF-939143585726}") - .As("{0576E786-5C30-4000-B969-302B61E90CA3}"); + .As("{0576E786-5C30-4000-B969-302B61E90CA3}"); To("{48AD6CCD-C7A4-4305-A8AB-38728AD23FC5}"); To("{DF470D86-E5CA-42AC-9780-9D28070E25F9}"); @@ -241,13 +254,15 @@ protected void DefinePlan() To("{3D8DADEF-0FDA-4377-A5F0-B52C2110E8F2}"); To("{1303BDCF-2295-4645-9526-2F32E8B35ABD}"); To("{5060F3D2-88BE-4D30-8755-CF51F28EAD12}"); - To("{A2686B49-A082-4B22-97FD-AAB154D46A57}"); // Re-run this migration to make sure it has executed to account for migrations going out of sync between versions. + To( + "{A2686B49-A082-4B22-97FD-AAB154D46A57}"); // Re-run this migration to make sure it has executed to account for migrations going out of sync between versions. // TO 9.0.0-rc4 - To("5E02F241-5253-403D-B5D3-7DB00157E20F"); // Jaddie: This GUID is missing the { }, although this likely can't be changed now as it will break installs going forwards - - // TO 9.0.0 + To( + "5E02F241-5253-403D-B5D3-7DB00157E20F"); // Jaddie: This GUID is missing the { }, although this likely can't be changed now as it will break installs going forwards + // TO 9.1.0 + To("{8BAF5E6C-DCB7-41AE-824F-4215AE4F1F98}"); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_1_0/AddContentVersionCleanupFeature.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_1_0/AddContentVersionCleanupFeature.cs new file mode 100644 index 000000000000..aa0d4472e813 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_1_0/AddContentVersionCleanupFeature.cs @@ -0,0 +1,28 @@ +using Umbraco.Cms.Infrastructure.Migrations; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_1_0 +{ + class AddContentVersionCleanupFeature : MigrationBase + { + public AddContentVersionCleanupFeature(IMigrationContext context) + : base(context) { } + + /// + /// The conditionals are useful to enable the same migration to be used in multiple + /// migration paths x.x -> 8.18 and x.x -> 9.x + /// + protected override void Migrate() + { + var tables = SqlSyntax.GetTablesInSchema(Context.Database); + if (!tables.InvariantContains(ContentVersionCleanupPolicyDto.TableName)) + { + Create.Table().Do(); + } + + var columns = SqlSyntax.GetColumnsInSchema(Context.Database); + AddColumnIfNotExists(columns, "preventCleanup"); + } + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionCleanupPolicyDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionCleanupPolicyDto.cs new file mode 100644 index 000000000000..4b2faa166f5b --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionCleanupPolicyDto.cs @@ -0,0 +1,34 @@ +using System; +using System.Data; +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; + +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +{ + [TableName(TableName)] + [PrimaryKey("contentTypeId", AutoIncrement = false)] + [ExplicitColumns] + internal class ContentVersionCleanupPolicyDto + { + public const string TableName = Constants.DatabaseSchema.Tables.ContentVersionCleanupPolicy; + + [Column("contentTypeId")] + [ForeignKey(typeof(ContentTypeDto), Column = "nodeId", OnDelete = Rule.Cascade)] + public int ContentTypeId { get; set; } + + [Column("preventCleanup")] + public bool PreventCleanup { get; set; } + + [Column("keepAllVersionsNewerThanDays")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? KeepAllVersionsNewerThanDays { get; set; } + + [Column("keepLatestVersionPerDayForDays")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? KeepLatestVersionPerDayForDays { get; set; } + + [Column("updated")] + public DateTime Updated { get; set; } + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionDto.cs index 53e90859d9c3..63f2802af625 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionDto.cs @@ -49,5 +49,9 @@ public class ContentVersionDto [ResultColumn] [Reference(ReferenceType.OneToOne, ColumnName = "NodeId", ReferenceMemberName = "NodeId")] public ContentDto ContentDto { get; set; } + + [Column("preventCleanup")] + [Constraint(Default = "0")] + public bool PreventCleanup { get; set; } } } diff --git a/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions-Bulk.cs b/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions-Bulk.cs index 443032c67ae8..f07867cccc83 100644 --- a/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions-Bulk.cs +++ b/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions-Bulk.cs @@ -1,38 +1,43 @@ using System; using System.Collections.Generic; using System.Data; +using System.Data.Common; using System.Data.SqlClient; using System.Linq; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence; namespace Umbraco.Extensions { /// - /// Provides extension methods to NPoco Database class. + /// Provides extension methods to NPoco Database class. /// public static partial class NPocoDatabaseExtensions { /// - /// Configures NPoco's SqlBulkCopyHelper to use the correct SqlConnection and SqlTransaction instances from the underlying RetryDbConnection and ProfiledDbTransaction + /// Configures NPoco's SqlBulkCopyHelper to use the correct SqlConnection and SqlTransaction instances from the + /// underlying RetryDbConnection and ProfiledDbTransaction /// /// - /// This is required to use NPoco's own method because we use wrapped DbConnection and DbTransaction instances. - /// NPoco's InsertBulk method only caters for efficient bulk inserting records for Sql Server, it does not cater for bulk inserting of records for - /// any other database type and in which case will just insert records one at a time. - /// NPoco's InsertBulk method also deals with updating the passed in entity's PK/ID once it's inserted whereas our own BulkInsertRecords methods - /// do not handle this scenario. + /// This is required to use NPoco's own method because we use + /// wrapped DbConnection and DbTransaction instances. + /// NPoco's InsertBulk method only caters for efficient bulk inserting records for Sql Server, it does not cater for + /// bulk inserting of records for + /// any other database type and in which case will just insert records one at a time. + /// NPoco's InsertBulk method also deals with updating the passed in entity's PK/ID once it's inserted whereas our own + /// BulkInsertRecords methods + /// do not handle this scenario. /// public static void ConfigureNPocoBulkExtensions() { - SqlBulkCopyHelper.SqlConnectionResolver = dbConn => GetTypedConnection(dbConn); SqlBulkCopyHelper.SqlTransactionResolver = dbTran => GetTypedTransaction(dbTran); } /// - /// Creates bulk-insert commands. + /// Creates bulk-insert commands. /// /// The type of the records. /// The database. @@ -40,17 +45,22 @@ public static void ConfigureNPocoBulkExtensions() /// The sql commands to execute. internal static IDbCommand[] GenerateBulkInsertCommands(this IUmbracoDatabase database, T[] records) { - if (database?.Connection == null) throw new ArgumentException("Null database?.connection.", nameof(database)); + if (database?.Connection == null) + { + throw new ArgumentException("Null database?.connection.", nameof(database)); + } - var pocoData = database.PocoDataFactory.ForType(typeof(T)); + PocoData pocoData = database.PocoDataFactory.ForType(typeof(T)); // get columns to include, = number of parameters per row - var columns = pocoData.Columns.Where(c => IncludeColumn(pocoData, c)).ToArray(); + KeyValuePair[] columns = + pocoData.Columns.Where(c => IncludeColumn(pocoData, c)).ToArray(); var paramsPerRecord = columns.Length; // format columns to sql var tableName = database.DatabaseType.EscapeTableName(pocoData.TableInfo.TableName); - var columnNames = string.Join(", ", columns.Select(c => tableName + "." + database.DatabaseType.EscapeSqlIdentifier(c.Key))); + var columnNames = string.Join(", ", + columns.Select(c => tableName + "." + database.DatabaseType.EscapeSqlIdentifier(c.Key))); // example: // assume 4168 records, each record containing 8 fields, ie 8 command parameters @@ -58,7 +68,9 @@ internal static IDbCommand[] GenerateBulkInsertCommands(this IUmbracoDatabase // Math.Floor(2100 / 8) = 262 record per command // 4168 / 262 = 15.908... = there will be 16 command in total // (if we have disabled db parameters, then all records will be included, in only one command) - var recordsPerCommand = paramsPerRecord == 0 ? int.MaxValue : Convert.ToInt32(Math.Floor(2000.00 / paramsPerRecord)); + var recordsPerCommand = paramsPerRecord == 0 + ? int.MaxValue + : Convert.ToInt32(Math.Floor((double)Constants.Sql.MaxParameterCount / paramsPerRecord)); var commandsCount = Convert.ToInt32(Math.Ceiling((double)records.Length / recordsPerCommand)); var commands = new IDbCommand[commandsCount]; @@ -67,23 +79,27 @@ internal static IDbCommand[] GenerateBulkInsertCommands(this IUmbracoDatabase var prefix = database.DatabaseType.GetParameterPrefix(database.ConnectionString); for (var commandIndex = 0; commandIndex < commandsCount; commandIndex++) { - var command = database.CreateCommand(database.Connection, CommandType.Text, string.Empty); + DbCommand command = database.CreateCommand(database.Connection, CommandType.Text, string.Empty); var parameterIndex = 0; var commandRecords = Math.Min(recordsPerCommand, recordsLeftToInsert); var recordsValues = new string[commandRecords]; - for (var commandRecordIndex = 0; commandRecordIndex < commandRecords; commandRecordIndex++, recordsIndex++, recordsLeftToInsert--) + for (var commandRecordIndex = 0; + commandRecordIndex < commandRecords; + commandRecordIndex++, recordsIndex++, recordsLeftToInsert--) { - var record = records[recordsIndex]; + T record = records[recordsIndex]; var recordValues = new string[columns.Length]; for (var columnIndex = 0; columnIndex < columns.Length; columnIndex++) { database.AddParameter(command, columns[columnIndex].Value.GetValue(record)); recordValues[columnIndex] = prefix + parameterIndex++; } + recordsValues[commandRecordIndex] = "(" + string.Join(",", recordValues) + ")"; } - command.CommandText = $"INSERT INTO {tableName} ({columnNames}) VALUES {string.Join(", ", recordsValues)}"; + command.CommandText = + $"INSERT INTO {tableName} ({columnNames}) VALUES {string.Join(", ", recordsValues)}"; commands[commandIndex] = command; } @@ -91,19 +107,14 @@ internal static IDbCommand[] GenerateBulkInsertCommands(this IUmbracoDatabase } /// - /// Determines whether a column should be part of a bulk-insert. + /// Determines whether a column should be part of a bulk-insert. /// /// The PocoData object corresponding to the record's type. /// The column. /// A value indicating whether the column should be part of the bulk-insert. /// Columns that are primary keys and auto-incremental, or result columns, are excluded from bulk-inserts. - public static bool IncludeColumn(PocoData pocoData, KeyValuePair column) - { - return column.Value.ResultColumn == false - && (pocoData.TableInfo.AutoIncrement == false || column.Key != pocoData.TableInfo.PrimaryKey); - } - - - + public static bool IncludeColumn(PocoData pocoData, KeyValuePair column) => + column.Value.ResultColumn == false + && (pocoData.TableInfo.AutoIncrement == false || column.Key != pocoData.TableInfo.PrimaryKey); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditEntryRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditEntryRepository.cs index 16c411c772a5..e3685dd32ccf 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditEntryRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditEntryRepository.cs @@ -3,6 +3,7 @@ using System.Linq; using Microsoft.Extensions.Logging; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; @@ -16,26 +17,47 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement { /// - /// Represents the NPoco implementation of . + /// Represents the NPoco implementation of . /// internal class AuditEntryRepository : EntityRepositoryBase, IAuditEntryRepository { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public AuditEntryRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) : base(scopeAccessor, cache, logger) - { } + { + } + + /// + public IEnumerable GetPage(long pageIndex, int pageCount, out long records) + { + Sql sql = Sql() + .Select() + .From() + .OrderByDescending(x => x.EventDateUtc); + + Page page = Database.Page(pageIndex + 1, pageCount, sql); + records = page.TotalItems; + return page.Items.Select(AuditEntryFactory.BuildEntity); + } + + /// + public bool IsAvailable() + { + var tables = SqlSyntax.GetTablesInSchema(Database).ToArray(); + return tables.InvariantContains(Constants.DatabaseSchema.Tables.AuditEntry); + } /// protected override IAuditEntry PerformGet(int id) { - var sql = Sql() + Sql sql = Sql() .Select() .From() .Where(x => x.Id == id); - var dto = Database.FirstOrDefault(sql); + AuditEntryDto dto = Database.FirstOrDefault(sql); return dto == null ? null : AuditEntryFactory.BuildEntity(dto); } @@ -44,7 +66,7 @@ protected override IEnumerable PerformGetAll(params int[] ids) { if (ids.Length == 0) { - var sql = Sql() + Sql sql = Sql() .Select() .From(); @@ -53,9 +75,9 @@ protected override IEnumerable PerformGetAll(params int[] ids) var entries = new List(); - foreach (var group in ids.InGroupsOf(2000)) + foreach (IEnumerable group in ids.InGroupsOf(Constants.Sql.MaxParameterCount)) { - var sql = Sql() + Sql sql = Sql() .Select() .From() .WhereIn(x => x.Id, group); @@ -69,68 +91,41 @@ protected override IEnumerable PerformGetAll(params int[] ids) /// protected override IEnumerable PerformGetByQuery(IQuery query) { - var sqlClause = GetBaseQuery(false); + Sql sqlClause = GetBaseQuery(false); var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); + Sql sql = translator.Translate(); return Database.Fetch(sql).Select(AuditEntryFactory.BuildEntity); } /// protected override Sql GetBaseQuery(bool isCount) { - var sql = Sql(); + Sql sql = Sql(); sql = isCount ? sql.SelectCount() : sql.Select(); sql = sql.From(); return sql; } /// - protected override string GetBaseWhereClause() - { - return $"{Cms.Core.Constants.DatabaseSchema.Tables.AuditEntry}.id = @id"; - } + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.AuditEntry}.id = @id"; /// - protected override IEnumerable GetDeleteClauses() - { + protected override IEnumerable GetDeleteClauses() => throw new NotSupportedException("Audit entries cannot be deleted."); - } /// protected override void PersistNewItem(IAuditEntry entity) { entity.AddingEntity(); - var dto = AuditEntryFactory.BuildDto(entity); + AuditEntryDto dto = AuditEntryFactory.BuildDto(entity); Database.Insert(dto); entity.Id = dto.Id; entity.ResetDirtyProperties(); } /// - protected override void PersistUpdatedItem(IAuditEntry entity) - { + protected override void PersistUpdatedItem(IAuditEntry entity) => throw new NotSupportedException("Audit entries cannot be updated."); - } - - /// - public IEnumerable GetPage(long pageIndex, int pageCount, out long records) - { - var sql = Sql() - .Select() - .From() - .OrderByDescending(x => x.EventDateUtc); - - var page = Database.Page(pageIndex + 1, pageCount, sql); - records = page.TotalItems; - return page.Items.Select(AuditEntryFactory.BuildEntity); - } - - /// - public bool IsAvailable() - { - var tables = SqlSyntax.GetTablesInSchema(Database).ToArray(); - return tables.InvariantContains(Cms.Core.Constants.DatabaseSchema.Tables.AuditEntry); - } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs index 239f0a89ed71..fd4d1c33b9ca 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -657,7 +657,7 @@ protected IDictionary GetPropertyCollections(List(versions, 2000, batch => + var allPropertyDataDtos = Database.FetchByGroups(versions, Constants.Sql.MaxParameterCount, batch => SqlContext.Sql() .Select() .From() @@ -666,7 +666,7 @@ protected IDictionary GetPropertyCollections(List x.PropertyTypeId).Distinct().ToList(); - var allPropertyTypeDtos = Database.FetchByGroups(allPropertyTypeIds, 2000, batch => + var allPropertyTypeDtos = Database.FetchByGroups(allPropertyTypeIds, Constants.Sql.MaxParameterCount, batch => SqlContext.Sql() .Select(r => r.Select(x => x.DataTypeDto)) .From() diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs index e171a3a54f77..2a14a745c9a1 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs @@ -6,31 +6,35 @@ using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Exceptions; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Factories; using Umbraco.Extensions; +using Enumerable = System.Linq.Enumerable; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement { /// - /// Implements . + /// Implements . /// internal class ContentTypeCommonRepository : IContentTypeCommonRepository { - private const string CacheKey = "Umbraco.Core.Persistence.Repositories.Implement.ContentTypeCommonRepository::AllTypes"; + private const string CacheKey = + "Umbraco.Core.Persistence.Repositories.Implement.ContentTypeCommonRepository::AllTypes"; private readonly AppCaches _appCaches; - private readonly IShortStringHelper _shortStringHelper; private readonly IScopeAccessor _scopeAccessor; + private readonly IShortStringHelper _shortStringHelper; private readonly ITemplateRepository _templateRepository; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public ContentTypeCommonRepository(IScopeAccessor scopeAccessor, ITemplateRepository templateRepository, AppCaches appCaches, IShortStringHelper shortStringHelper) + public ContentTypeCommonRepository(IScopeAccessor scopeAccessor, ITemplateRepository templateRepository, + AppCaches appCaches, IShortStringHelper shortStringHelper) { _scopeAccessor = scopeAccessor; _templateRepository = templateRepository; @@ -40,83 +44,98 @@ public ContentTypeCommonRepository(IScopeAccessor scopeAccessor, ITemplateReposi private IScope AmbientScope => _scopeAccessor.AmbientScope; private IUmbracoDatabase Database => AmbientScope.Database; + private ISqlContext SqlContext => AmbientScope.SqlContext; - private Sql Sql() => SqlContext.Sql(); //private Sql Sql(string sql, params object[] args) => SqlContext.Sql(sql, args); //private ISqlSyntaxProvider SqlSyntax => SqlContext.SqlSyntax; //private IQuery Query() => SqlContext.Query(); /// - public IEnumerable GetAllTypes() - { + public IEnumerable GetAllTypes() => // use a 5 minutes sliding cache - same as FullDataSet cache policy - return _appCaches.RuntimeCache.GetCacheItem(CacheKey, GetAllTypesInternal, TimeSpan.FromMinutes(5), true); - } + _appCaches.RuntimeCache.GetCacheItem(CacheKey, GetAllTypesInternal, TimeSpan.FromMinutes(5), true); /// - public void ClearCache() - { - _appCaches.RuntimeCache.Clear(CacheKey); - } + public void ClearCache() => _appCaches.RuntimeCache.Clear(CacheKey); + + private Sql Sql() => SqlContext.Sql(); private IEnumerable GetAllTypesInternal() { var contentTypes = new Dictionary(); // get content types - var sql1 = Sql() + Sql sql1 = Sql() .Select(r => r.Select(x => x.NodeDto)) .From() .InnerJoin().On((ct, n) => ct.NodeId == n.NodeId) .OrderBy(x => x.NodeId); - var contentTypeDtos = Database.Fetch(sql1); + List contentTypeDtos = Database.Fetch(sql1); // get allowed content types - var sql2 = Sql() + Sql sql2 = Sql() .Select() .From() .OrderBy(x => x.Id); - var allowedDtos = Database.Fetch(sql2); + List allowedDtos = Database.Fetch(sql2); // prepare // note: same alias could be used for media, content... but always different ids = ok - var aliases = contentTypeDtos.ToDictionary(x => x.NodeId, x => x.Alias); + var aliases = Enumerable.ToDictionary(contentTypeDtos, x => x.NodeId, x => x.Alias); // create var allowedDtoIx = 0; - foreach (var contentTypeDto in contentTypeDtos) + foreach (ContentTypeDto contentTypeDto in contentTypeDtos) { // create content type IContentTypeComposition contentType; - if (contentTypeDto.NodeDto.NodeObjectType == Cms.Core.Constants.ObjectTypes.MediaType) + if (contentTypeDto.NodeDto.NodeObjectType == Constants.ObjectTypes.MediaType) + { contentType = ContentTypeFactory.BuildMediaTypeEntity(_shortStringHelper, contentTypeDto); - else if (contentTypeDto.NodeDto.NodeObjectType == Cms.Core.Constants.ObjectTypes.DocumentType) + } + else if (contentTypeDto.NodeDto.NodeObjectType == Constants.ObjectTypes.DocumentType) + { contentType = ContentTypeFactory.BuildContentTypeEntity(_shortStringHelper, contentTypeDto); - else if (contentTypeDto.NodeDto.NodeObjectType == Cms.Core.Constants.ObjectTypes.MemberType) + } + else if (contentTypeDto.NodeDto.NodeObjectType == Constants.ObjectTypes.MemberType) + { contentType = ContentTypeFactory.BuildMemberTypeEntity(_shortStringHelper, contentTypeDto); - else throw new PanicException($"The node object type {contentTypeDto.NodeDto.NodeObjectType} is not supported"); + } + else + { + throw new PanicException( + $"The node object type {contentTypeDto.NodeDto.NodeObjectType} is not supported"); + } + contentTypes.Add(contentType.Id, contentType); // map allowed content types var allowedContentTypes = new List(); while (allowedDtoIx < allowedDtos.Count && allowedDtos[allowedDtoIx].Id == contentTypeDto.NodeId) { - var allowedDto = allowedDtos[allowedDtoIx]; - if (!aliases.TryGetValue(allowedDto.AllowedId, out var alias)) continue; - allowedContentTypes.Add(new ContentTypeSort(new Lazy(() => allowedDto.AllowedId), allowedDto.SortOrder, alias)); + ContentTypeAllowedContentTypeDto allowedDto = allowedDtos[allowedDtoIx]; + if (!aliases.TryGetValue(allowedDto.AllowedId, out var alias)) + { + continue; + } + + allowedContentTypes.Add(new ContentTypeSort(new Lazy(() => allowedDto.AllowedId), + allowedDto.SortOrder, alias)); allowedDtoIx++; } + contentType.AllowedContentTypes = allowedContentTypes; } MapTemplates(contentTypes); MapComposition(contentTypes); MapGroupsAndProperties(contentTypes); + MapHistoryCleanup(contentTypes); // finalize - foreach (var contentType in contentTypes.Values) + foreach (IContentTypeComposition contentType in contentTypes.Values) { contentType.ResetDirtyProperties(false); } @@ -124,36 +143,79 @@ private IEnumerable GetAllTypesInternal() return contentTypes.Values; } + private void MapHistoryCleanup(Dictionary contentTypes) + { + // get templates + Sql sql1 = Sql() + .Select() + .From() + .OrderBy(x => x.ContentTypeId); + + var contentVersionCleanupPolicyDtos = Database.Fetch(sql1); + + var contentVersionCleanupPolicyDictionary = + contentVersionCleanupPolicyDtos.ToDictionary(x => x.ContentTypeId); + foreach (IContentTypeComposition c in contentTypes.Values) + { + if (!(c is ContentType contentType)) + { + continue; + } + + var historyCleanup = new HistoryCleanup(); + + if (contentVersionCleanupPolicyDictionary.TryGetValue(contentType.Id, out var versionCleanup)) + { + historyCleanup.PreventCleanup = versionCleanup.PreventCleanup; + historyCleanup.KeepAllVersionsNewerThanDays = versionCleanup.KeepAllVersionsNewerThanDays; + historyCleanup.KeepLatestVersionPerDayForDays = versionCleanup.KeepLatestVersionPerDayForDays; + } + + contentType.HistoryCleanup = historyCleanup; + } + } + private void MapTemplates(Dictionary contentTypes) { // get templates - var sql1 = Sql() + Sql sql1 = Sql() .Select() .From() .OrderBy(x => x.ContentTypeNodeId); - var templateDtos = Database.Fetch(sql1); + List templateDtos = Database.Fetch(sql1); //var templates = templateRepository.GetMany(templateDtos.Select(x => x.TemplateNodeId).ToArray()).ToDictionary(x => x.Id, x => x); - var templates = _templateRepository.GetMany().ToDictionary(x => x.Id, x => x); + var templates = Enumerable.ToDictionary(_templateRepository.GetMany(), x => x.Id, x => x); var templateDtoIx = 0; - foreach (var c in contentTypes.Values) + foreach (IContentTypeComposition c in contentTypes.Values) { - if (!(c is IContentType contentType)) continue; + if (!(c is IContentType contentType)) + { + continue; + } // map allowed templates var allowedTemplates = new List(); var defaultTemplateId = 0; - while (templateDtoIx < templateDtos.Count && templateDtos[templateDtoIx].ContentTypeNodeId == contentType.Id) + while (templateDtoIx < templateDtos.Count && + templateDtos[templateDtoIx].ContentTypeNodeId == contentType.Id) { - var allowedDto = templateDtos[templateDtoIx]; + ContentTypeTemplateDto allowedDto = templateDtos[templateDtoIx]; templateDtoIx++; - if (!templates.TryGetValue(allowedDto.TemplateNodeId, out var template)) continue; + if (!templates.TryGetValue(allowedDto.TemplateNodeId, out ITemplate template)) + { + continue; + } + allowedTemplates.Add(template); if (allowedDto.IsDefault) + { defaultTemplateId = template.Id; + } } + contentType.AllowedTemplates = allowedTemplates; contentType.DefaultTemplateId = defaultTemplateId; } @@ -162,24 +224,28 @@ private void MapTemplates(Dictionary contentTypes) private void MapComposition(IDictionary contentTypes) { // get parent/child - var sql1 = Sql() + Sql sql1 = Sql() .Select() .From() .OrderBy(x => x.ChildId); - var compositionDtos = Database.Fetch(sql1); + List compositionDtos = Database.Fetch(sql1); // map var compositionIx = 0; - foreach (var contentType in contentTypes.Values) + foreach (IContentTypeComposition contentType in contentTypes.Values) { - while (compositionIx < compositionDtos.Count && compositionDtos[compositionIx].ChildId == contentType.Id) + while (compositionIx < compositionDtos.Count && + compositionDtos[compositionIx].ChildId == contentType.Id) { - var parentDto = compositionDtos[compositionIx]; + ContentType2ContentTypeDto parentDto = compositionDtos[compositionIx]; compositionIx++; - if (!contentTypes.TryGetValue(parentDto.ParentId, out var parentContentType)) + if (!contentTypes.TryGetValue(parentDto.ParentId, out IContentTypeComposition parentContentType)) + { continue; + } + contentType.AddContentType(parentContentType); } } @@ -187,41 +253,49 @@ private void MapComposition(IDictionary contentTyp private void MapGroupsAndProperties(IDictionary contentTypes) { - var sql1 = Sql() + Sql sql1 = Sql() .Select() .From() - .InnerJoin().On((ptg, ct) => ptg.ContentTypeNodeId == ct.NodeId) + .InnerJoin() + .On((ptg, ct) => ptg.ContentTypeNodeId == ct.NodeId) .OrderBy(x => x.NodeId) .AndBy(x => x.SortOrder, x => x.Id); - var groupDtos = Database.Fetch(sql1); + List groupDtos = Database.Fetch(sql1); - var sql2 = Sql() + Sql sql2 = Sql() .Select(r => r.Select(x => x.DataTypeDto, r1 => r1.Select(x => x.NodeDto))) .AndSelect() .From() .InnerJoin().On((pt, dt) => pt.DataTypeId == dt.NodeId) .InnerJoin().On((dt, n) => dt.NodeId == n.NodeId) - .InnerJoin().On((pt, ct) => pt.ContentTypeId == ct.NodeId) - .LeftJoin().On((pt, ptg) => pt.PropertyTypeGroupId == ptg.Id) - .LeftJoin().On((pt, mpt) => pt.Id == mpt.PropertyTypeId) + .InnerJoin() + .On((pt, ct) => pt.ContentTypeId == ct.NodeId) + .LeftJoin() + .On((pt, ptg) => pt.PropertyTypeGroupId == ptg.Id) + .LeftJoin() + .On((pt, mpt) => pt.Id == mpt.PropertyTypeId) .OrderBy(x => x.NodeId) - .AndBy(x => x.SortOrder, x => x.Id) // NULLs will come first or last, never mind, we deal with it below + .AndBy< + PropertyTypeGroupDto>(x => x.SortOrder, + x => x.Id) // NULLs will come first or last, never mind, we deal with it below .AndBy(x => x.SortOrder, x => x.Id); - var propertyDtos = Database.Fetch(sql2); - var builtinProperties = ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper); + List propertyDtos = Database.Fetch(sql2); + Dictionary builtinProperties = + ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper); var groupIx = 0; var propertyIx = 0; - foreach (var contentType in contentTypes.Values) + foreach (IContentTypeComposition contentType in contentTypes.Values) { // only IContentType is publishing var isPublishing = contentType is IContentType; // get group-less properties (in case NULL is ordered first) var noGroupPropertyTypes = new PropertyTypeCollection(isPublishing); - while (propertyIx < propertyDtos.Count && propertyDtos[propertyIx].ContentTypeId == contentType.Id && propertyDtos[propertyIx].PropertyTypeGroupId == null) + while (propertyIx < propertyDtos.Count && propertyDtos[propertyIx].ContentTypeId == contentType.Id && + propertyDtos[propertyIx].PropertyTypeGroupId == null) { noGroupPropertyTypes.Add(MapPropertyType(contentType, propertyDtos[propertyIx], builtinProperties)); propertyIx++; @@ -231,36 +305,45 @@ private void MapGroupsAndProperties(IDictionary co var groupCollection = new PropertyGroupCollection(); while (groupIx < groupDtos.Count && groupDtos[groupIx].ContentTypeNodeId == contentType.Id) { - var group = MapPropertyGroup(groupDtos[groupIx], isPublishing); + PropertyGroup group = MapPropertyGroup(groupDtos[groupIx], isPublishing); groupCollection.Add(group); groupIx++; - while (propertyIx < propertyDtos.Count && propertyDtos[propertyIx].ContentTypeId == contentType.Id && propertyDtos[propertyIx].PropertyTypeGroupId == group.Id) + while (propertyIx < propertyDtos.Count && + propertyDtos[propertyIx].ContentTypeId == contentType.Id && + propertyDtos[propertyIx].PropertyTypeGroupId == group.Id) { - group.PropertyTypes.Add(MapPropertyType(contentType, propertyDtos[propertyIx], builtinProperties)); + group.PropertyTypes.Add(MapPropertyType(contentType, propertyDtos[propertyIx], + builtinProperties)); propertyIx++; } } + contentType.PropertyGroups = groupCollection; // get group-less properties (in case NULL is ordered last) - while (propertyIx < propertyDtos.Count && propertyDtos[propertyIx].ContentTypeId == contentType.Id && propertyDtos[propertyIx].PropertyTypeGroupId == null) + while (propertyIx < propertyDtos.Count && propertyDtos[propertyIx].ContentTypeId == contentType.Id && + propertyDtos[propertyIx].PropertyTypeGroupId == null) { noGroupPropertyTypes.Add(MapPropertyType(contentType, propertyDtos[propertyIx], builtinProperties)); propertyIx++; } + contentType.NoGroupPropertyTypes = noGroupPropertyTypes; // ensure builtin properties if (contentType is IMemberType memberType) { // ensure that the group exists (ok if it already exists) - memberType.AddPropertyGroup(Cms.Core.Constants.Conventions.Member.StandardPropertiesGroupAlias, Cms.Core.Constants.Conventions.Member.StandardPropertiesGroupName); + memberType.AddPropertyGroup(Constants.Conventions.Member.StandardPropertiesGroupAlias, + Constants.Conventions.Member.StandardPropertiesGroupName); // ensure that property types exist (ok if they already exist) - foreach (var (alias, propertyType) in builtinProperties) + foreach ((var alias, PropertyType propertyType) in builtinProperties) { - var added = memberType.AddPropertyType(propertyType, Cms.Core.Constants.Conventions.Member.StandardPropertiesGroupAlias, Cms.Core.Constants.Conventions.Member.StandardPropertiesGroupName); + var added = memberType.AddPropertyType(propertyType, + Constants.Conventions.Member.StandardPropertiesGroupAlias, + Constants.Conventions.Member.StandardPropertiesGroupName); if (added) { @@ -273,9 +356,8 @@ private void MapGroupsAndProperties(IDictionary co } } - private PropertyGroup MapPropertyGroup(PropertyTypeGroupDto dto, bool isPublishing) - { - return new PropertyGroup(new PropertyTypeCollection(isPublishing)) + private PropertyGroup MapPropertyGroup(PropertyTypeGroupDto dto, bool isPublishing) => + new PropertyGroup(new PropertyTypeCollection(isPublishing)) { Id = dto.Id, Key = dto.UniqueId, @@ -284,14 +366,16 @@ private PropertyGroup MapPropertyGroup(PropertyTypeGroupDto dto, bool isPublishi Alias = dto.Alias, SortOrder = dto.SortOrder }; - } - private PropertyType MapPropertyType(IContentTypeComposition contentType, PropertyTypeCommonDto dto, IDictionary builtinProperties) + private PropertyType MapPropertyType(IContentTypeComposition contentType, PropertyTypeCommonDto dto, + IDictionary builtinProperties) { var groupId = dto.PropertyTypeGroupId; - var readonlyStorageType = builtinProperties.TryGetValue(dto.Alias, out var propertyType); - var storageType = readonlyStorageType ? propertyType.ValueStorageType : Enum.Parse(dto.DataTypeDto.DbType); + var readonlyStorageType = builtinProperties.TryGetValue(dto.Alias, out PropertyType propertyType); + ValueStorageType storageType = readonlyStorageType + ? propertyType.ValueStorageType + : Enum.Parse(dto.DataTypeDto.DbType); if (contentType is IMemberType memberType) { @@ -300,23 +384,25 @@ private PropertyType MapPropertyType(IContentTypeComposition contentType, Proper memberType.SetMemberCanViewProperty(dto.Alias, dto.ViewOnProfile); } - return new PropertyType(_shortStringHelper, dto.DataTypeDto.EditorAlias, storageType, readonlyStorageType, dto.Alias) - { - Description = dto.Description, - DataTypeId = dto.DataTypeId, - DataTypeKey = dto.DataTypeDto.NodeDto.UniqueId, - Id = dto.Id, - Key = dto.UniqueId, - Mandatory = dto.Mandatory, - MandatoryMessage = dto.MandatoryMessage, - Name = dto.Name, - PropertyGroupId = groupId.HasValue ? new Lazy(() => groupId.Value) : null, - SortOrder = dto.SortOrder, - ValidationRegExp = dto.ValidationRegExp, - ValidationRegExpMessage = dto.ValidationRegExpMessage, - Variations = (ContentVariation)dto.Variations, - LabelOnTop = dto.LabelOnTop - }; + return new + PropertyType(_shortStringHelper, dto.DataTypeDto.EditorAlias, storageType, readonlyStorageType, + dto.Alias) + { + Description = dto.Description, + DataTypeId = dto.DataTypeId, + DataTypeKey = dto.DataTypeDto.NodeDto.UniqueId, + Id = dto.Id, + Key = dto.UniqueId, + Mandatory = dto.Mandatory, + MandatoryMessage = dto.MandatoryMessage, + Name = dto.Name, + PropertyGroupId = groupId.HasValue ? new Lazy(() => groupId.Value) : null, + SortOrder = dto.SortOrder, + ValidationRegExp = dto.ValidationRegExp, + ValidationRegExpMessage = dto.ValidationRegExpMessage, + Variations = (ContentVariation)dto.Variations, + LabelOnTop = dto.LabelOnTop + }; } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs index 1977e0fce128..6ab97c971f8b 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; @@ -236,6 +236,7 @@ protected override void PersistNewItem(IContentType entity) PersistNewBaseContentType(entity); PersistTemplates(entity, false); + PersistHistoryCleanup(entity); entity.ResetDirtyProperties(); } @@ -289,8 +290,26 @@ protected override void PersistUpdatedItem(IContentType entity) PersistUpdatedBaseContentType(entity); PersistTemplates(entity, true); + PersistHistoryCleanup(entity); entity.ResetDirtyProperties(); } + + private void PersistHistoryCleanup(IContentType entity) + { + if (entity is IContentTypeWithHistoryCleanup entityWithHistoryCleanup) + { + ContentVersionCleanupPolicyDto dto = new ContentVersionCleanupPolicyDto() + { + ContentTypeId = entity.Id, + Updated = DateTime.Now, + PreventCleanup = entityWithHistoryCleanup.HistoryCleanup.PreventCleanup, + KeepAllVersionsNewerThanDays = entityWithHistoryCleanup.HistoryCleanup.KeepAllVersionsNewerThanDays, + KeepLatestVersionPerDayForDays = entityWithHistoryCleanup.HistoryCleanup.KeepLatestVersionPerDayForDays + }; + Database.InsertOrUpdate(dto); + } + + } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index d93c2c832280..d7be081fe144 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -795,7 +795,7 @@ private void CopyTagData(int? sourceLanguageId, int? targetLanguageId, IReadOnly // note: important to use SqlNullableEquals for nullable types, cannot directly compare language identifiers var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0); - if (whereInArgsCount > 2000) + if (whereInArgsCount > Constants.Sql.MaxParameterCount) throw new NotSupportedException("Too many property/content types."); // delete existing relations (for target language) @@ -933,7 +933,7 @@ private void CopyPropertyData(int? sourceLanguageId, int? targetLanguageId, IRea // note: important to use SqlNullableEquals for nullable types, cannot directly compare language identifiers // var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0); - if (whereInArgsCount > 2000) + if (whereInArgsCount > Constants.Sql.MaxParameterCount) throw new NotSupportedException("Too many property/content types."); //first clear out any existing property data that might already exists under the target language @@ -1032,7 +1032,7 @@ private void RenormalizeDocumentEditedFlags(IReadOnlyCollection propertyTyp //based on the current variance of each item to see if it's 'edited' value should be true/false. var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0); - if (whereInArgsCount > 2000) + if (whereInArgsCount > Constants.Sql.MaxParameterCount) throw new NotSupportedException("Too many property/content types."); var propertySql = Sql() @@ -1121,14 +1121,20 @@ private void RenormalizeDocumentEditedFlags(IReadOnlyCollection propertyTyp } } - //lookup all matching rows in umbracoDocumentCultureVariation - var docCultureVariationsToUpdate = editedLanguageVersions.InGroupsOf(2000) - .SelectMany(_ => Database.Fetch( - Sql().Select().From() - .WhereIn(x => x.LanguageId, editedLanguageVersions.Keys.Select(x => x.langId).ToList()) - .WhereIn(x => x.NodeId, editedLanguageVersions.Keys.Select(x => x.nodeId)))) - //convert to dictionary with the same key type - .ToDictionary(x => (x.NodeId, (int?)x.LanguageId), x => x); + // lookup all matching rows in umbracoDocumentCultureVariation + // fetch in batches to account for maximum parameter count (distinct languages can't exceed 2000) + var languageIds = editedLanguageVersions.Keys.Select(x => x.langId).Distinct().ToArray(); + var nodeIds = editedLanguageVersions.Keys.Select(x => x.nodeId).Distinct(); + var docCultureVariationsToUpdate = nodeIds.InGroupsOf(Constants.Sql.MaxParameterCount - languageIds.Length) + .SelectMany(group => + { + var sql = Sql().Select().From() + .WhereIn(x => x.LanguageId, languageIds) + .WhereIn(x => x.NodeId, group); + + return Database.Fetch(sql); + }) + .ToDictionary(x => (x.NodeId, (int?)x.LanguageId), x => x); //convert to dictionary with the same key type var toUpdate = new List(); foreach (var ev in editedLanguageVersions) diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs index 0ec31d843fe3..bc9892b1ee0e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs @@ -263,13 +263,12 @@ public IEnumerable GetDictionaryItemDescendants(Guid? parentId) Func>> getItemsFromParents = guids => { - //needs to be in groups of 2000 because we are doing an IN clause and there's a max parameter count that can be used. - return guids.InGroupsOf(2000) - .Select(@group => + return guids.InGroupsOf(Constants.Sql.MaxParameterCount) + .Select(group => { var sqlClause = GetBaseQuery(false) .Where(x => x.Parent != null) - .Where($"{SqlSyntax.GetQuotedColumnName("parent")} IN (@parentIds)", new { parentIds = @group }); + .WhereIn(x => x.Parent, group); var translator = new SqlTranslator(sqlClause, Query()); var sql = translator.Translate(); diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs index 4c9b19f1a91f..12e094cd7cb7 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs @@ -292,9 +292,11 @@ public override IEnumerable GetAllVersionsSlim(int nodeId, int skip, i .OrderByDescending(x => x.Current) .AndByDescending(x => x.VersionDate); - return MapDtosToContent(Database.Fetch(sql), true, + var pageIndex = skip / take; + + return MapDtosToContent(Database.Page(pageIndex+1, take, sql).Items, true, // load bare minimum, need variants though since this is used to rollback with variants - false, false, false, true).Skip(skip).Take(take); + false, false, false, true); } public override IContent GetVersion(int versionId) @@ -1390,7 +1392,7 @@ private IDictionary GetContentSchedule(params in { var result = new Dictionary(); - var scheduleDtos = Database.FetchByGroups(contentIds, 2000, batch => Sql() + var scheduleDtos = Database.FetchByGroups(contentIds, Constants.Sql.MaxParameterCount, batch => Sql() .Select() .From() .WhereIn(x => x.NodeId, batch)); @@ -1440,7 +1442,7 @@ private IDictionary> GetContentVariations(List>(); - var dtos = Database.FetchByGroups(versions, 2000, batch + var dtos = Database.FetchByGroups(versions, Constants.Sql.MaxParameterCount, batch => Sql() .Select() .From() @@ -1469,7 +1471,7 @@ private IDictionary> GetDocumentVariations(List< { var ids = temps.Select(x => x.Id); - var dtos = Database.FetchByGroups(ids, 2000, batch => + var dtos = Database.FetchByGroups(ids, Constants.Sql.MaxParameterCount, batch => Sql() .Select() .From() diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentVersionRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentVersionRepository.cs new file mode 100644 index 000000000000..5040ee5d7a70 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentVersionRepository.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +{ + internal class DocumentVersionRepository : IDocumentVersionRepository + { + private readonly IScopeAccessor _scopeAccessor; + + public DocumentVersionRepository(IScopeAccessor scopeAccessor) + { + _scopeAccessor = scopeAccessor ?? throw new ArgumentNullException(nameof(scopeAccessor)); + } + + /// + /// + /// Never includes current draft version.
+ /// Never includes current published version.
+ /// Never includes versions marked as "preventCleanup".
+ ///
+ public IReadOnlyCollection GetDocumentVersionsEligibleForCleanup() + { + var query = _scopeAccessor.AmbientScope.SqlContext.Sql(); + + query.Select(@"umbracoDocument.nodeId as contentId, + umbracoContent.contentTypeId as contentTypeId, + umbracoContentVersion.id as versionId, + umbracoContentVersion.versionDate as versionDate") + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.Id, right => right.Id) + .Where(x => !x.Current) // Never delete current draft version + .Where(x => !x.PreventCleanup) // Never delete "pinned" versions + .Where(x => !x.Published); // Never delete published version + + return _scopeAccessor.AmbientScope.Database.Fetch(query); + } + + /// + public IReadOnlyCollection GetCleanupPolicies() + { + var query = _scopeAccessor.AmbientScope.SqlContext.Sql(); + + query.Select() + .From(); + + return _scopeAccessor.AmbientScope.Database.Fetch(query); + } + + /// + /// + /// Deletes in batches of + /// + public void DeleteVersions(IEnumerable versionIds) + { + foreach (var group in versionIds.InGroupsOf(Constants.Sql.MaxParameterCount)) + { + var groupedVersionIds = group.ToList(); + + // Note: We had discussed doing this in a single SQL Command. + // If you can work out how to make that work with SQL CE, let me know! + // Can use test PerformContentVersionCleanup_WithNoKeepPeriods_DeletesEverythingExceptActive to try things out. + + var query = _scopeAccessor.AmbientScope.SqlContext.Sql() + .Delete() + .WhereIn(x => x.VersionId, groupedVersionIds); + _scopeAccessor.AmbientScope.Database.Execute(query); + + query = _scopeAccessor.AmbientScope.SqlContext.Sql() + .Delete() + .WhereIn(x => x.VersionId, groupedVersionIds); + _scopeAccessor.AmbientScope.Database.Execute(query); + + query = _scopeAccessor.AmbientScope.SqlContext.Sql() + .Delete() + .WhereIn(x => x.Id, groupedVersionIds); + _scopeAccessor.AmbientScope.Database.Execute(query); + + query = _scopeAccessor.AmbientScope.SqlContext.Sql() + .Delete() + .WhereIn(x => x.Id, groupedVersionIds); + _scopeAccessor.AmbientScope.Database.Execute(query); + } + } + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityContainerRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityContainerRepository.cs index bc1b1b1881c8..b30c5ae1a44d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityContainerRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityContainerRepository.cs @@ -3,6 +3,7 @@ using System.Linq; using Microsoft.Extensions.Logging; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; @@ -14,47 +15,55 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement { /// - /// An internal repository for managing entity containers such as doc type, media type, data type containers. + /// An internal repository for managing entity containers such as doc type, media type, data type containers. /// internal class EntityContainerRepository : EntityRepositoryBase, IEntityContainerRepository { - private readonly Guid _containerObjectType; - - public EntityContainerRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, Guid containerObjectType) + public EntityContainerRepository(IScopeAccessor scopeAccessor, AppCaches cache, + ILogger logger, Guid containerObjectType) : base(scopeAccessor, cache, logger) { - var allowedContainers = new[] { Cms.Core.Constants.ObjectTypes.DocumentTypeContainer, Cms.Core.Constants.ObjectTypes.MediaTypeContainer, Cms.Core.Constants.ObjectTypes.DataTypeContainer }; - _containerObjectType = containerObjectType; - if (allowedContainers.Contains(_containerObjectType) == false) - throw new InvalidOperationException("No container type exists with ID: " + _containerObjectType); + Guid[] allowedContainers = new[] + { + Constants.ObjectTypes.DocumentTypeContainer, Constants.ObjectTypes.MediaTypeContainer, + Constants.ObjectTypes.DataTypeContainer + }; + NodeObjectTypeId = containerObjectType; + if (allowedContainers.Contains(NodeObjectTypeId) == false) + { + throw new InvalidOperationException("No container type exists with ID: " + NodeObjectTypeId); + } } + protected Guid NodeObjectTypeId { get; } + // never cache - protected override IRepositoryCachePolicy CreateCachePolicy() - { - return NoCacheRepositoryCachePolicy.Instance; - } + protected override IRepositoryCachePolicy CreateCachePolicy() => + NoCacheRepositoryCachePolicy.Instance; protected override EntityContainer PerformGet(int id) { - var sql = GetBaseQuery(false).Where(GetBaseWhereClause(), new { id = id, NodeObjectType = NodeObjectTypeId }); + Sql sql = GetBaseQuery(false) + .Where(GetBaseWhereClause(), new { id, NodeObjectType = NodeObjectTypeId }); - var nodeDto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); + NodeDto nodeDto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); return nodeDto == null ? null : CreateEntity(nodeDto); } // temp - so we don't have to implement GetByQuery public EntityContainer Get(Guid id) { - var sql = GetBaseQuery(false).Where("UniqueId=@uniqueId", new { uniqueId = id }); + Sql sql = GetBaseQuery(false).Where("UniqueId=@uniqueId", new { uniqueId = id }); - var nodeDto = Database.Fetch(sql).FirstOrDefault(); + NodeDto nodeDto = Database.Fetch(sql).FirstOrDefault(); return nodeDto == null ? null : CreateEntity(nodeDto); } public IEnumerable Get(string name, int level) { - var sql = GetBaseQuery(false).Where("text=@name AND level=@level AND nodeObjectType=@umbracoObjectTypeId", new { name, level, umbracoObjectTypeId = NodeObjectTypeId }); + Sql sql = GetBaseQuery(false) + .Where("text=@name AND level=@level AND nodeObjectType=@umbracoObjectTypeId", + new { name, level, umbracoObjectTypeId = NodeObjectTypeId }); return Database.Fetch(sql).Select(CreateEntity); } @@ -62,39 +71,39 @@ protected override IEnumerable PerformGetAll(params int[] ids) { if (ids.Any()) { - return Database.FetchByGroups(ids, 2000, batch => - GetBaseQuery(false) - .Where(x => x.NodeObjectType == NodeObjectTypeId) - .WhereIn(x => x.NodeId, batch)) + return Database.FetchByGroups(ids, Constants.Sql.MaxParameterCount, batch => + GetBaseQuery(false) + .Where(x => x.NodeObjectType == NodeObjectTypeId) + .WhereIn(x => x.NodeId, batch)) .Select(CreateEntity); } // else - var sql = GetBaseQuery(false) + Sql sql = GetBaseQuery(false) .Where("nodeObjectType=@umbracoObjectTypeId", new { umbracoObjectTypeId = NodeObjectTypeId }) .OrderBy(x => x.Level); return Database.Fetch(sql).Select(CreateEntity); } - protected override IEnumerable PerformGetByQuery(IQuery query) - { + protected override IEnumerable PerformGetByQuery(IQuery query) => throw new NotImplementedException(); - } private static EntityContainer CreateEntity(NodeDto nodeDto) { if (nodeDto.NodeObjectType.HasValue == false) + { throw new InvalidOperationException("Node with id " + nodeDto.NodeId + " has no object type."); + } // throws if node is not a container - var containedObjectType = EntityContainer.GetContainedObjectType(nodeDto.NodeObjectType.Value); + Guid containedObjectType = EntityContainer.GetContainedObjectType(nodeDto.NodeObjectType.Value); var entity = new EntityContainer(nodeDto.NodeId, nodeDto.UniqueId, nodeDto.ParentId, nodeDto.Path, nodeDto.Level, nodeDto.SortOrder, containedObjectType, - nodeDto.Text, nodeDto.UserId ?? Cms.Core.Constants.Security.UnknownUserId); + nodeDto.Text, nodeDto.UserId ?? Constants.Security.UnknownUserId); // reset dirty initial properties (U4-1946) entity.ResetDirtyProperties(false); @@ -104,11 +113,16 @@ private static EntityContainer CreateEntity(NodeDto nodeDto) protected override Sql GetBaseQuery(bool isCount) { - var sql = Sql(); + Sql sql = Sql(); if (isCount) + { sql.SelectCount(); + } else + { sql.SelectAll(); + } + sql.From(); return sql; } @@ -117,23 +131,29 @@ protected override Sql GetBaseQuery(bool isCount) protected override IEnumerable GetDeleteClauses() => throw new NotImplementedException(); - protected Guid NodeObjectTypeId => _containerObjectType; - protected override void PersistDeletedItem(EntityContainer entity) { - if (entity == null) throw new ArgumentNullException(nameof(entity)); + if (entity == null) + { + throw new ArgumentNullException(nameof(entity)); + } + EnsureContainerType(entity); - var nodeDto = Database.FirstOrDefault(Sql().SelectAll() + NodeDto nodeDto = Database.FirstOrDefault(Sql().SelectAll() .From() .Where(dto => dto.NodeId == entity.Id && dto.NodeObjectType == entity.ContainerObjectType)); - if (nodeDto == null) return; + if (nodeDto == null) + { + return; + } // move children to the parent so they are not orphans - var childDtos = Database.Fetch(Sql().SelectAll() + List childDtos = Database.Fetch(Sql().SelectAll() .From() - .Where("parentID=@parentID AND (nodeObjectType=@containedObjectType OR nodeObjectType=@containerObjectType)", + .Where( + "parentID=@parentID AND (nodeObjectType=@containedObjectType OR nodeObjectType=@containerObjectType)", new { parentID = entity.Id, @@ -141,7 +161,7 @@ protected override void PersistDeletedItem(EntityContainer entity) containerObjectType = entity.ContainerObjectType })); - foreach (var childDto in childDtos) + foreach (NodeDto childDto in childDtos) { childDto.ParentId = nodeDto.ParentId; Database.Update(childDto); @@ -155,31 +175,51 @@ protected override void PersistDeletedItem(EntityContainer entity) protected override void PersistNewItem(EntityContainer entity) { - if (entity == null) throw new ArgumentNullException(nameof(entity)); + if (entity == null) + { + throw new ArgumentNullException(nameof(entity)); + } + EnsureContainerType(entity); - if (entity.Name == null) throw new InvalidOperationException("Entity name can't be null."); - if (string.IsNullOrWhiteSpace(entity.Name)) throw new InvalidOperationException("Entity name can't be empty or consist only of white-space characters."); + if (entity.Name == null) + { + throw new InvalidOperationException("Entity name can't be null."); + } + + if (string.IsNullOrWhiteSpace(entity.Name)) + { + throw new InvalidOperationException( + "Entity name can't be empty or consist only of white-space characters."); + } + entity.Name = entity.Name.Trim(); // guard against duplicates - var nodeDto = Database.FirstOrDefault(Sql().SelectAll() + NodeDto nodeDto = Database.FirstOrDefault(Sql().SelectAll() .From() - .Where(dto => dto.ParentId == entity.ParentId && dto.Text == entity.Name && dto.NodeObjectType == entity.ContainerObjectType)); + .Where(dto => + dto.ParentId == entity.ParentId && dto.Text == entity.Name && + dto.NodeObjectType == entity.ContainerObjectType)); if (nodeDto != null) + { throw new InvalidOperationException("A container with the same name already exists."); + } // create var level = 0; var path = "-1"; if (entity.ParentId > -1) { - var parentDto = Database.FirstOrDefault(Sql().SelectAll() + NodeDto parentDto = Database.FirstOrDefault(Sql().SelectAll() .From() - .Where(dto => dto.NodeId == entity.ParentId && dto.NodeObjectType == entity.ContainerObjectType)); + .Where(dto => + dto.NodeId == entity.ParentId && dto.NodeObjectType == entity.ContainerObjectType)); if (parentDto == null) + { throw new InvalidOperationException("Could not find parent container with id " + entity.ParentId); + } level = parentDto.Level; path = parentDto.Path; @@ -203,7 +243,7 @@ protected override void PersistNewItem(EntityContainer entity) // insert, get the id, update the path with the id var id = Convert.ToInt32(Database.Insert(nodeDto)); nodeDto.Path = nodeDto.Path + "," + nodeDto.NodeId; - Database.Save(nodeDto); + Database.Save(nodeDto); // refresh the entity entity.Id = id; @@ -218,26 +258,45 @@ protected override void PersistNewItem(EntityContainer entity) // protected override void PersistUpdatedItem(EntityContainer entity) { - if (entity == null) throw new ArgumentNullException(nameof(entity)); + if (entity == null) + { + throw new ArgumentNullException(nameof(entity)); + } + EnsureContainerType(entity); - if (entity.Name == null) throw new InvalidOperationException("Entity name can't be null."); - if (string.IsNullOrWhiteSpace(entity.Name)) throw new InvalidOperationException("Entity name can't be empty or consist only of white-space characters."); + if (entity.Name == null) + { + throw new InvalidOperationException("Entity name can't be null."); + } + + if (string.IsNullOrWhiteSpace(entity.Name)) + { + throw new InvalidOperationException( + "Entity name can't be empty or consist only of white-space characters."); + } + entity.Name = entity.Name.Trim(); // find container to update - var nodeDto = Database.FirstOrDefault(Sql().SelectAll() + NodeDto nodeDto = Database.FirstOrDefault(Sql().SelectAll() .From() .Where(dto => dto.NodeId == entity.Id && dto.NodeObjectType == entity.ContainerObjectType)); if (nodeDto == null) + { throw new InvalidOperationException("Could not find container with id " + entity.Id); + } // guard against duplicates - var dupNodeDto = Database.FirstOrDefault(Sql().SelectAll() + NodeDto dupNodeDto = Database.FirstOrDefault(Sql().SelectAll() .From() - .Where(dto => dto.ParentId == entity.ParentId && dto.Text == entity.Name && dto.NodeObjectType == entity.ContainerObjectType)); + .Where(dto => + dto.ParentId == entity.ParentId && dto.Text == entity.Name && + dto.NodeObjectType == entity.ContainerObjectType)); if (dupNodeDto != null && dupNodeDto.NodeId != nodeDto.NodeId) + { throw new InvalidOperationException("A container with the same name already exists."); + } // update nodeDto.Text = entity.Name; @@ -247,16 +306,21 @@ protected override void PersistUpdatedItem(EntityContainer entity) nodeDto.Path = "-1"; if (entity.ParentId > -1) { - var parent = Database.FirstOrDefault( Sql().SelectAll() + NodeDto parent = Database.FirstOrDefault(Sql().SelectAll() .From() - .Where(dto => dto.NodeId == entity.ParentId && dto.NodeObjectType == entity.ContainerObjectType)); + .Where(dto => + dto.NodeId == entity.ParentId && dto.NodeObjectType == entity.ContainerObjectType)); if (parent == null) - throw new InvalidOperationException("Could not find parent container with id " + entity.ParentId); + { + throw new InvalidOperationException( + "Could not find parent container with id " + entity.ParentId); + } nodeDto.Level = Convert.ToInt16(parent.Level + 1); nodeDto.Path = parent.Path + "," + nodeDto.NodeId; } + nodeDto.ParentId = entity.ParentId; } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs index 4eb4f108ce80..f1b9c77d0aa8 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs @@ -279,7 +279,7 @@ private IEnumerable BuildVariants(IEnumerable(v.Select(x => x.Id), 2000, GetVariantInfos); + var dtos = Database.FetchByGroups(v.Select(x => x.Id), Constants.Sql.MaxParameterCount, GetVariantInfos); // group by node id (each group contains all languages) var xdtos = dtos.GroupBy(x => x.NodeId).ToDictionary(x => x.Key, x => x); diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepositoryBase.cs index 25927213e542..79e6f732a2a9 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepositoryBase.cs @@ -3,6 +3,7 @@ using System.Linq; using Microsoft.Extensions.Logging; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Persistence; @@ -14,38 +15,37 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement { /// - /// Provides a base class to all based repositories. + /// Provides a base class to all based repositories. /// /// The type of the entity's unique identifier. /// The type of the entity managed by this repository. public abstract class EntityRepositoryBase : RepositoryBase, IReadWriteQueryRepository where TEntity : class, IEntity { + private static RepositoryCachePolicyOptions s_defaultOptions; private IRepositoryCachePolicy _cachePolicy; private IQuery _hasIdQuery; - private static RepositoryCachePolicyOptions s_defaultOptions; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - protected EntityRepositoryBase(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger> logger) - : base(scopeAccessor, appCaches) - { + protected EntityRepositoryBase(IScopeAccessor scopeAccessor, AppCaches appCaches, + ILogger> logger) + : base(scopeAccessor, appCaches) => Logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } /// - /// Gets the logger + /// Gets the logger /// protected ILogger> Logger { get; } /// - /// Gets the isolated cache for the + /// Gets the isolated cache for the /// protected IAppPolicyCache GlobalIsolatedCache => AppCaches.IsolatedCaches.GetOrCreate(); /// - /// Gets the isolated cache. + /// Gets the isolated cache. /// /// Depends on the ambient scope cache mode. protected IAppPolicyCache IsolatedCache @@ -67,19 +67,20 @@ protected IAppPolicyCache IsolatedCache } /// - /// Gets the default + /// Gets the default /// protected virtual RepositoryCachePolicyOptions DefaultOptions => s_defaultOptions ?? (s_defaultOptions - = new RepositoryCachePolicyOptions(() => - { - // get count of all entities of current type (TEntity) to ensure cached result is correct - // create query once if it is needed (no need for locking here) - query is static! - IQuery query = _hasIdQuery ?? (_hasIdQuery = AmbientScope.SqlContext.Query().Where(x => x.Id != 0)); - return PerformCount(query); - })); + = new RepositoryCachePolicyOptions(() => + { + // get count of all entities of current type (TEntity) to ensure cached result is correct + // create query once if it is needed (no need for locking here) - query is static! + IQuery query = _hasIdQuery ?? + (_hasIdQuery = AmbientScope.SqlContext.Query().Where(x => x.Id != 0)); + return PerformCount(query); + })); /// - /// Gets the repository cache policy + /// Gets the repository cache policy /// protected IRepositoryCachePolicy CachePolicy { @@ -110,21 +111,9 @@ protected IRepositoryCachePolicy CachePolicy } /// - /// Get the entity id for the - /// - protected virtual TId GetEntityId(TEntity entity) - => (TId)(object)entity.Id; - - /// - /// Create the repository cache policy + /// Adds or Updates an entity of type TEntity /// - protected virtual IRepositoryCachePolicy CreateCachePolicy() - => new DefaultRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, DefaultOptions); - - /// - /// Adds or Updates an entity of type TEntity - /// - /// This method is backed by an cache + /// This method is backed by an cache public virtual void Save(TEntity entity) { if (entity.HasIdentity == false) @@ -138,64 +127,19 @@ public virtual void Save(TEntity entity) } /// - /// Deletes the passed in entity + /// Deletes the passed in entity /// public virtual void Delete(TEntity entity) => CachePolicy.Delete(entity, PersistDeletedItem); - protected abstract TEntity PerformGet(TId id); - - protected abstract IEnumerable PerformGetAll(params TId[] ids); - - protected abstract IEnumerable PerformGetByQuery(IQuery query); - - protected abstract void PersistNewItem(TEntity item); - - protected abstract void PersistUpdatedItem(TEntity item); - - // TODO: obsolete, use QueryType instead everywhere like GetBaseQuery(QueryType queryType); - protected abstract Sql GetBaseQuery(bool isCount); - - protected abstract string GetBaseWhereClause(); - - protected abstract IEnumerable GetDeleteClauses(); - - protected virtual bool PerformExists(TId id) - { - var sql = GetBaseQuery(true); - sql.Where(GetBaseWhereClause(), new { id = id }); - var count = Database.ExecuteScalar(sql); - return count == 1; - } - - protected virtual int PerformCount(IQuery query) - { - var sqlClause = GetBaseQuery(true); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - - return Database.ExecuteScalar(sql); - } - - protected virtual void PersistDeletedItem(TEntity entity) - { - var deletes = GetDeleteClauses(); - foreach (var delete in deletes) - { - Database.Execute(delete, new { id = GetEntityId(entity) }); - } - - entity.DeleteDate = DateTime.Now; - } - /// - /// Gets an entity by the passed in Id utilizing the repository's cache policy + /// Gets an entity by the passed in Id utilizing the repository's cache policy /// public TEntity Get(TId id) => CachePolicy.Get(id, PerformGet, PerformGetAll); /// - /// Gets all entities of type TEntity or a list according to the passed in Ids + /// Gets all entities of type TEntity or a list according to the passed in Ids /// public IEnumerable GetMany(params TId[] ids) { @@ -209,38 +153,94 @@ public IEnumerable GetMany(params TId[] ids) // can't query more than 2000 ids at a time... but if someone is really querying 2000+ entities, // the additional overhead of fetching them in groups is minimal compared to the lookup time of each group - const int maxParams = 2000; - if (ids.Length <= maxParams) + if (ids.Length <= Constants.Sql.MaxParameterCount) { return CachePolicy.GetAll(ids, PerformGetAll); } var entities = new List(); - foreach (var groupOfIds in ids.InGroupsOf(maxParams)) + foreach (IEnumerable group in ids.InGroupsOf(Constants.Sql.MaxParameterCount)) { - entities.AddRange(CachePolicy.GetAll(groupOfIds.ToArray(), PerformGetAll)); + entities.AddRange(CachePolicy.GetAll(group.ToArray(), PerformGetAll)); } return entities; } /// - /// Gets a list of entities by the passed in query + /// Gets a list of entities by the passed in query /// public IEnumerable Get(IQuery query) => PerformGetByQuery(query) .WhereNotNull(); // ensure we don't include any null refs in the returned collection! /// - /// Returns a boolean indicating whether an entity with the passed Id exists + /// Returns a boolean indicating whether an entity with the passed Id exists /// public bool Exists(TId id) => CachePolicy.Exists(id, PerformExists, PerformGetAll); /// - /// Returns an integer with the count of entities found with the passed in query + /// Returns an integer with the count of entities found with the passed in query /// public int Count(IQuery query) => PerformCount(query); + + /// + /// Get the entity id for the + /// + protected virtual TId GetEntityId(TEntity entity) + => (TId)(object)entity.Id; + + /// + /// Create the repository cache policy + /// + protected virtual IRepositoryCachePolicy CreateCachePolicy() + => new DefaultRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, DefaultOptions); + + protected abstract TEntity PerformGet(TId id); + + protected abstract IEnumerable PerformGetAll(params TId[] ids); + + protected abstract IEnumerable PerformGetByQuery(IQuery query); + + protected abstract void PersistNewItem(TEntity item); + + protected abstract void PersistUpdatedItem(TEntity item); + + // TODO: obsolete, use QueryType instead everywhere like GetBaseQuery(QueryType queryType); + protected abstract Sql GetBaseQuery(bool isCount); + + protected abstract string GetBaseWhereClause(); + + protected abstract IEnumerable GetDeleteClauses(); + + protected virtual bool PerformExists(TId id) + { + Sql sql = GetBaseQuery(true); + sql.Where(GetBaseWhereClause(), new { id }); + var count = Database.ExecuteScalar(sql); + return count == 1; + } + + protected virtual int PerformCount(IQuery query) + { + Sql sqlClause = GetBaseQuery(true); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + + return Database.ExecuteScalar(sql); + } + + protected virtual void PersistDeletedItem(TEntity entity) + { + IEnumerable deletes = GetDeleteClauses(); + foreach (var delete in deletes) + { + Database.Execute(delete, new { id = GetEntityId(entity) }); + } + + entity.DeleteDate = DateTime.Now; + } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs index 22083eae30f3..4031971ddcc7 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Events; @@ -26,17 +27,17 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement { /// - /// Represents a repository for doing CRUD operations for + /// Represents a repository for doing CRUD operations for /// public class MemberRepository : ContentRepositoryBase, IMemberRepository { - private readonly MemberPasswordConfigurationSettings _passwordConfiguration; - private readonly IMemberTypeRepository _memberTypeRepository; - private readonly ITagRepository _tagRepository; - private readonly IPasswordHasher _passwordHasher; private readonly IJsonSerializer _jsonSerializer; - private readonly IMemberGroupRepository _memberGroupRepository; private readonly IRepositoryCachePolicy _memberByUsernameCachePolicy; + private readonly IMemberGroupRepository _memberGroupRepository; + private readonly IMemberTypeRepository _memberTypeRepository; + private readonly MemberPasswordConfigurationSettings _passwordConfiguration; + private readonly IPasswordHasher _passwordHasher; + private readonly ITagRepository _tagRepository; private bool _passwordConfigInitialized; private string _passwordConfigJson; @@ -57,19 +58,22 @@ public MemberRepository( IJsonSerializer serializer, IEventAggregator eventAggregator, IOptions passwordConfiguration) - : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferenceFactories, dataTypeService, eventAggregator) + : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, + propertyEditors, dataValueReferenceFactories, dataTypeService, eventAggregator) { - _memberTypeRepository = memberTypeRepository ?? throw new ArgumentNullException(nameof(memberTypeRepository)); + _memberTypeRepository = + memberTypeRepository ?? throw new ArgumentNullException(nameof(memberTypeRepository)); _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); _passwordHasher = passwordHasher; _jsonSerializer = serializer; _memberGroupRepository = memberGroupRepository; _passwordConfiguration = passwordConfiguration.Value; - _memberByUsernameCachePolicy = new DefaultRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, DefaultOptions); + _memberByUsernameCachePolicy = + new DefaultRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, DefaultOptions); } /// - /// Returns a serialized dictionary of the password configuration that is stored against the member in the database + /// Returns a serialized dictionary of the password configuration that is stored against the member in the database /// private string DefaultPasswordConfigJson { @@ -95,17 +99,341 @@ private string DefaultPasswordConfigJson public override int RecycleBinId => throw new NotSupportedException(); + public IEnumerable FindMembersInRole(string roleName, string usernameToMatch, + StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) + { + //get the group id + IQuery grpQry = Query().Where(group => group.Name.Equals(roleName)); + IMemberGroup memberGroup = _memberGroupRepository.Get(grpQry).FirstOrDefault(); + if (memberGroup == null) + { + return Enumerable.Empty(); + } + + // get the members by username + IQuery query = Query(); + switch (matchType) + { + case StringPropertyMatchType.Exact: + query.Where(member => member.Username.Equals(usernameToMatch)); + break; + case StringPropertyMatchType.Contains: + query.Where(member => member.Username.Contains(usernameToMatch)); + break; + case StringPropertyMatchType.StartsWith: + query.Where(member => member.Username.StartsWith(usernameToMatch)); + break; + case StringPropertyMatchType.EndsWith: + query.Where(member => member.Username.EndsWith(usernameToMatch)); + break; + case StringPropertyMatchType.Wildcard: + query.Where(member => member.Username.SqlWildcard(usernameToMatch, TextColumnType.NVarchar)); + break; + default: + throw new ArgumentOutOfRangeException(nameof(matchType)); + } + + IMember[] matchedMembers = Get(query).ToArray(); + + var membersInGroup = new List(); + + //then we need to filter the matched members that are in the role + foreach (IEnumerable group in matchedMembers.Select(x => x.Id) + .InGroupsOf(Constants.Sql.MaxParameterCount)) + { + Sql sql = Sql().SelectAll().From() + .Where(dto => dto.MemberGroup == memberGroup.Id) + .WhereIn(dto => dto.Member, group); + + var memberIdsInGroup = Database.Fetch(sql) + .Select(x => x.Member).ToArray(); + + membersInGroup.AddRange(matchedMembers.Where(x => memberIdsInGroup.Contains(x.Id))); + } + + return membersInGroup; + } + + /// + /// Get all members in a specific group + /// + /// + /// + public IEnumerable GetByMemberGroup(string groupName) + { + IQuery grpQry = Query().Where(group => group.Name.Equals(groupName)); + IMemberGroup memberGroup = _memberGroupRepository.Get(grpQry).FirstOrDefault(); + if (memberGroup == null) + { + return Enumerable.Empty(); + } + + Sql subQuery = Sql().Select("Member").From() + .Where(dto => dto.MemberGroup == memberGroup.Id); + + Sql sql = GetBaseQuery(false) + // TODO: An inner join would be better, though I've read that the query optimizer will always turn a + // subquery with an IN clause into an inner join anyways. + .Append("WHERE umbracoNode.id IN (" + subQuery.SQL + ")", subQuery.Arguments) + .OrderByDescending(x => x.VersionDate) + .OrderBy(x => x.SortOrder); + + return MapDtosToContent(Database.Fetch(sql)); + } + + public bool Exists(string username) + { + Sql sql = Sql() + .SelectCount() + .From() + .Where(x => x.LoginName == username); + + return Database.ExecuteScalar(sql) > 0; + } + + public int GetCountByQuery(IQuery query) + { + Sql sqlWithProps = GetNodeIdQueryWithPropertyData(); + var translator = new SqlTranslator(sqlWithProps, query); + Sql sql = translator.Translate(); + + //get the COUNT base query + Sql fullSql = GetBaseQuery(true) + .Append(new Sql("WHERE umbracoNode.id IN (" + sql.SQL + ")", sql.Arguments)); + + return Database.ExecuteScalar(fullSql); + } + + /// + public void SetLastLogin(string username, DateTime date) + { + // Important - these queries are designed to execute without an exclusive WriteLock taken in our distributed lock + // table. However due to the data that we are updating which relies on version data we cannot update this data + // without taking some locks, otherwise we'll end up with strange situations because when a member is updated, that operation + // deletes and re-inserts all property data. So if there are concurrent transactions, one deleting and re-inserting and another trying + // to update there can be problems. This is only an issue for cmsPropertyData, not umbracoContentVersion because that table just + // maintains a single row and it isn't deleted/re-inserted. + // So the important part here is the ForUpdate() call on the select to fetch the property data to update. + + // Update the cms property value for the member + + SqlTemplate sqlSelectTemplateProperty = SqlContext.Templates.Get( + "Umbraco.Core.MemberRepository.SetLastLogin1", s => s + .Select(x => x.Id) + .From() + .InnerJoin() + .On((l, r) => l.Id == r.PropertyTypeId) + .InnerJoin() + .On((l, r) => l.Id == r.VersionId) + .InnerJoin().On((l, r) => l.NodeId == r.NodeId) + .InnerJoin().On((l, r) => l.NodeId == r.NodeId) + .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType")) + .Where(x => x.Alias == SqlTemplate.Arg("propertyTypeAlias")) + .Where(x => x.LoginName == SqlTemplate.Arg("username")) + .ForUpdate()); + Sql sqlSelectProperty = sqlSelectTemplateProperty.Sql(Constants.ObjectTypes.Member, + Constants.Conventions.Member.LastLoginDate, username); + + Sql update = Sql() + .Update(u => u + .Set(x => x.DateValue, date)) + .WhereIn(x => x.Id, sqlSelectProperty); + + Database.Execute(update); + + // Update the umbracoContentVersion value for the member + + SqlTemplate sqlSelectTemplateVersion = SqlContext.Templates.Get( + "Umbraco.Core.MemberRepository.SetLastLogin2", s => s + .Select(x => x.Id) + .From() + .InnerJoin().On((l, r) => l.NodeId == r.NodeId) + .InnerJoin().On((l, r) => l.NodeId == r.NodeId) + .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType")) + .Where(x => x.LoginName == SqlTemplate.Arg("username"))); + Sql sqlSelectVersion = sqlSelectTemplateVersion.Sql(Constants.ObjectTypes.Member, username); + + Database.Execute(Sql() + .Update(u => u + .Set(x => x.VersionDate, date)) + .WhereIn(x => x.Id, sqlSelectVersion)); + } + + /// + /// Gets paged member results. + /// + public override IEnumerable GetPage(IQuery query, + long pageIndex, int pageSize, out long totalRecords, + IQuery filter, + Ordering ordering) + { + Sql filterSql = null; + + if (filter != null) + { + filterSql = Sql(); + foreach (Tuple clause in filter.GetWhereClauses()) + { + filterSql = filterSql.Append($"AND ({clause.Item1})", clause.Item2); + } + } + + return GetPage(query, pageIndex, pageSize, out totalRecords, + x => MapDtosToContent(x), + filterSql, + ordering); + } + + public IMember GetByUsername(string username) => + _memberByUsernameCachePolicy.Get(username, PerformGetByUsername, PerformGetAllByUsername); + + public int[] GetMemberIds(string[] usernames) + { + Guid memberObjectType = Constants.ObjectTypes.Member; + + Sql memberSql = Sql() + .Select("umbracoNode.id") + .From() + .InnerJoin() + .On(dto => dto.NodeId, dto => dto.NodeId) + .Where(x => x.NodeObjectType == memberObjectType) + .Where("cmsMember.LoginName in (@usernames)", new + { + /*usernames =*/ + usernames + }); + return Database.Fetch(memberSql).ToArray(); + } + + protected override string ApplySystemOrdering(ref Sql sql, Ordering ordering) + { + if (ordering.OrderBy.InvariantEquals("email")) + { + return SqlSyntax.GetFieldName(x => x.Email); + } + + if (ordering.OrderBy.InvariantEquals("loginName")) + { + return SqlSyntax.GetFieldName(x => x.LoginName); + } + + if (ordering.OrderBy.InvariantEquals("userName")) + { + return SqlSyntax.GetFieldName(x => x.LoginName); + } + + if (ordering.OrderBy.InvariantEquals("updateDate")) + { + return SqlSyntax.GetFieldName(x => x.VersionDate); + } + + if (ordering.OrderBy.InvariantEquals("createDate")) + { + return SqlSyntax.GetFieldName(x => x.CreateDate); + } + + if (ordering.OrderBy.InvariantEquals("contentTypeAlias")) + { + return SqlSyntax.GetFieldName(x => x.Alias); + } + + return base.ApplySystemOrdering(ref sql, ordering); + } + + private IEnumerable MapDtosToContent(List dtos, bool withCache = false) + { + var temps = new List>(); + var contentTypes = new Dictionary(); + var content = new Member[dtos.Count]; + + for (var i = 0; i < dtos.Count; i++) + { + MemberDto dto = dtos[i]; + + if (withCache) + { + // if the cache contains the (proper version of the) item, use it + IMember cached = + IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); + if (cached != null && cached.VersionId == dto.ContentVersionDto.Id) + { + content[i] = (Member)cached; + continue; + } + } + + // else, need to build it + + // get the content type - the repository is full cache *but* still deep-clones + // whatever comes out of it, so use our own local index here to avoid this + var contentTypeId = dto.ContentDto.ContentTypeId; + if (contentTypes.TryGetValue(contentTypeId, out IMemberType contentType) == false) + { + contentTypes[contentTypeId] = contentType = _memberTypeRepository.Get(contentTypeId); + } + + Member c = content[i] = ContentBaseFactory.BuildEntity(dto, contentType); + + // need properties + var versionId = dto.ContentVersionDto.Id; + temps.Add(new TempContent(dto.NodeId, versionId, 0, contentType, c)); + } + + // load all properties for all documents from database in 1 query - indexed by version id + IDictionary properties = GetPropertyCollections(temps); + + // assign properties + foreach (TempContent temp in temps) + { + temp.Content.Properties = properties[temp.VersionId]; + + // reset dirty initial properties (U4-1946) + temp.Content.ResetDirtyProperties(false); + } + + return content; + } + + private IMember MapDtoToContent(MemberDto dto) + { + IMemberType memberType = _memberTypeRepository.Get(dto.ContentDto.ContentTypeId); + Member member = ContentBaseFactory.BuildEntity(dto, memberType); + + // get properties - indexed by version id + var versionId = dto.ContentVersionDto.Id; + var temp = new TempContent(dto.ContentDto.NodeId, versionId, 0, memberType); + IDictionary properties = + GetPropertyCollections(new List> { temp }); + member.Properties = properties[versionId]; + + // reset dirty initial properties (U4-1946) + member.ResetDirtyProperties(false); + return member; + } + + private IMember PerformGetByUsername(string username) + { + IQuery query = Query().Where(x => x.Username.Equals(username)); + return PerformGetByQuery(query).FirstOrDefault(); + } + + private IEnumerable PerformGetAllByUsername(params string[] usernames) + { + IQuery query = Query().WhereIn(x => x.Username, usernames); + return PerformGetByQuery(query); + } + #region Repository Base - protected override Guid NodeObjectTypeId => Cms.Core.Constants.ObjectTypes.Member; + protected override Guid NodeObjectTypeId => Constants.ObjectTypes.Member; protected override IMember PerformGet(int id) { - var sql = GetBaseQuery(QueryType.Single) + Sql sql = GetBaseQuery(QueryType.Single) .Where(x => x.NodeId == id) .SelectTop(1); - var dto = Database.Fetch(sql).FirstOrDefault(); + MemberDto dto = Database.Fetch(sql).FirstOrDefault(); return dto == null ? null : MapDtoToContent(dto); @@ -113,29 +441,31 @@ protected override IMember PerformGet(int id) protected override IEnumerable PerformGetAll(params int[] ids) { - var sql = GetBaseQuery(QueryType.Many); + Sql sql = GetBaseQuery(QueryType.Many); if (ids.Any()) + { sql.WhereIn(x => x.NodeId, ids); + } return MapDtosToContent(Database.Fetch(sql)); } protected override IEnumerable PerformGetByQuery(IQuery query) { - var baseQuery = GetBaseQuery(false); + Sql baseQuery = GetBaseQuery(false); // TODO: why is this different from content/media?! // check if the query is based on properties or not - var wheres = query.GetWhereClauses(); + IEnumerable> wheres = query.GetWhereClauses(); //this is a pretty rudimentary check but will work, we just need to know if this query requires property // level queries if (wheres.Any(x => x.Item1.Contains("cmsPropertyType"))) { - var sqlWithProps = GetNodeIdQueryWithPropertyData(); + Sql sqlWithProps = GetNodeIdQueryWithPropertyData(); var translator = new SqlTranslator(sqlWithProps, query); - var sql = translator.Translate(); + Sql sql = translator.Translate(); baseQuery.Append("WHERE umbracoNode.id IN (" + sql.SQL + ")", sql.Arguments) .OrderBy(x => x.SortOrder); @@ -145,22 +475,18 @@ protected override IEnumerable PerformGetByQuery(IQuery query) else { var translator = new SqlTranslator(baseQuery, query); - var sql = translator.Translate() + Sql sql = translator.Translate() .OrderBy(x => x.SortOrder); return MapDtosToContent(Database.Fetch(sql)); } - } - protected override Sql GetBaseQuery(QueryType queryType) - { - return GetBaseQuery(queryType, true); - } + protected override Sql GetBaseQuery(QueryType queryType) => GetBaseQuery(queryType, true); protected virtual Sql GetBaseQuery(QueryType queryType, bool current) { - var sql = SqlContext.Sql(); + Sql sql = SqlContext.Sql(); switch (queryType) // TODO: pretend we still need these queries for now { @@ -187,52 +513,52 @@ protected virtual Sql GetBaseQuery(QueryType queryType, bool curren .From() .InnerJoin().On(left => left.NodeId, right => right.NodeId) .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) // joining the type so we can do a query against the member type - not sure if this adds much overhead or not? // the execution plan says it doesn't so we'll go with that and in that case, it might be worth joining the content // types by default on the document and media repos so we can query by content type there too. - .InnerJoin().On(left => left.ContentTypeId, right => right.NodeId); + .InnerJoin() + .On(left => left.ContentTypeId, right => right.NodeId); sql.Where(x => x.NodeObjectType == NodeObjectTypeId); if (current) + { sql.Where(x => x.Current); // always get the current version + } return sql; } // TODO: move that one up to Versionable! or better: kill it! - protected override Sql GetBaseQuery(bool isCount) - { - return GetBaseQuery(isCount ? QueryType.Count : QueryType.Single); - } + protected override Sql GetBaseQuery(bool isCount) => + GetBaseQuery(isCount ? QueryType.Count : QueryType.Single); protected override string GetBaseWhereClause() // TODO: can we kill / refactor this? - { - return "umbracoNode.id = @id"; - } + => + "umbracoNode.id = @id"; // TODO: document/understand that one - protected Sql GetNodeIdQueryWithPropertyData() - { - return Sql() + protected Sql GetNodeIdQueryWithPropertyData() => + Sql() .Select("DISTINCT(umbracoNode.id)") .From() .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - .InnerJoin().On((left, right) => left.ContentTypeId == right.NodeId) - .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .InnerJoin() + .On((left, right) => left.ContentTypeId == right.NodeId) + .InnerJoin() + .On((left, right) => left.NodeId == right.NodeId) .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - - .LeftJoin().On(left => left.ContentTypeId, right => right.ContentTypeId) - .LeftJoin().On(left => left.DataTypeId, right => right.NodeId) - + .LeftJoin() + .On(left => left.ContentTypeId, right => right.ContentTypeId) + .LeftJoin() + .On(left => left.DataTypeId, right => right.NodeId) .LeftJoin().On(x => x .Where((left, right) => left.PropertyTypeId == right.Id) .Where((left, right) => left.VersionId == right.Id)) - .Where(x => x.NodeObjectType == NodeObjectTypeId); - } protected override IEnumerable GetDeleteClauses() { @@ -243,11 +569,13 @@ protected override IEnumerable GetDeleteClauses() "DELETE FROM umbracoRelation WHERE parentId = @id", "DELETE FROM umbracoRelation WHERE childId = @id", "DELETE FROM cmsTagRelationship WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.PropertyData + " WHERE versionId IN (SELECT id FROM " + Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", + "DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyData + + " WHERE versionId IN (SELECT id FROM " + Constants.DatabaseSchema.Tables.ContentVersion + + " WHERE nodeId = @id)", "DELETE FROM cmsMember2MemberGroup WHERE Member = @id", "DELETE FROM cmsMember WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Content + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Content + " WHERE nodeId = @id", "DELETE FROM umbracoNode WHERE id = @id" }; return list; @@ -259,7 +587,7 @@ protected override IEnumerable GetDeleteClauses() public override IEnumerable GetAllVersions(int nodeId) { - var sql = GetBaseQuery(QueryType.Many, false) + Sql sql = GetBaseQuery(QueryType.Many, false) .Where(x => x.NodeId == nodeId) .OrderByDescending(x => x.Current) .AndByDescending(x => x.VersionDate); @@ -269,10 +597,10 @@ public override IEnumerable GetAllVersions(int nodeId) public override IMember GetVersion(int versionId) { - var sql = GetBaseQuery(QueryType.Single) + Sql sql = GetBaseQuery(QueryType.Single) .Where(x => x.Id == versionId); - var dto = Database.Fetch(sql).FirstOrDefault(); + MemberDto dto = Database.Fetch(sql).FirstOrDefault(); return dto == null ? null : MapDtoToContent(dto); } @@ -346,13 +674,13 @@ protected override void PersistNewItem(IMember entity) entity.Level = level; // persist the content dto - var contentDto = memberDto.ContentDto; + ContentDto contentDto = memberDto.ContentDto; contentDto.NodeId = nodeDto.NodeId; Database.Insert(contentDto); // persist the content version dto // assumes a new version id and version date (modified date) has been set - var contentVersionDto = memberDto.ContentVersionDto; + ContentVersionDto contentVersionDto = memberDto.ContentVersionDto; contentVersionDto.NodeId = nodeDto.NodeId; contentVersionDto.Current = true; Database.Insert(contentVersionDto); @@ -365,8 +693,8 @@ protected override void PersistNewItem(IMember entity) // this will hash the guid with a salt so should be nicely random if (entity.RawPasswordValue.IsNullOrWhiteSpace()) { - - memberDto.Password = Cms.Core.Constants.Security.EmptyPasswordPrefix + _passwordHasher.HashPassword(Guid.NewGuid().ToString("N")); + memberDto.Password = Constants.Security.EmptyPasswordPrefix + + _passwordHasher.HashPassword(Guid.NewGuid().ToString("N")); entity.RawPasswordValue = memberDto.Password; } @@ -496,301 +824,5 @@ protected override void PersistUpdatedItem(IMember entity) } #endregion - - public IEnumerable FindMembersInRole(string roleName, string usernameToMatch, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) - { - //get the group id - var grpQry = Query().Where(group => group.Name.Equals(roleName)); - var memberGroup = _memberGroupRepository.Get(grpQry).FirstOrDefault(); - if (memberGroup == null) - return Enumerable.Empty(); - - // get the members by username - var query = Query(); - switch (matchType) - { - case StringPropertyMatchType.Exact: - query.Where(member => member.Username.Equals(usernameToMatch)); - break; - case StringPropertyMatchType.Contains: - query.Where(member => member.Username.Contains(usernameToMatch)); - break; - case StringPropertyMatchType.StartsWith: - query.Where(member => member.Username.StartsWith(usernameToMatch)); - break; - case StringPropertyMatchType.EndsWith: - query.Where(member => member.Username.EndsWith(usernameToMatch)); - break; - case StringPropertyMatchType.Wildcard: - query.Where(member => member.Username.SqlWildcard(usernameToMatch, TextColumnType.NVarchar)); - break; - default: - throw new ArgumentOutOfRangeException(nameof(matchType)); - } - var matchedMembers = Get(query).ToArray(); - - var membersInGroup = new List(); - //then we need to filter the matched members that are in the role - //since the max sql params are 2100 on sql server, we'll reduce that to be safe for potentially other servers and run the queries in batches - var inGroups = matchedMembers.InGroupsOf(1000); - foreach (var batch in inGroups) - { - var memberIdBatch = batch.Select(x => x.Id); - - var sql = Sql().SelectAll().From() - .Where(dto => dto.MemberGroup == memberGroup.Id) - .WhereIn(dto => dto.Member, memberIdBatch); - - var memberIdsInGroup = Database.Fetch(sql) - .Select(x => x.Member).ToArray(); - - membersInGroup.AddRange(matchedMembers.Where(x => memberIdsInGroup.Contains(x.Id))); - } - - return membersInGroup; - - } - - /// - /// Get all members in a specific group - /// - /// - /// - public IEnumerable GetByMemberGroup(string groupName) - { - var grpQry = Query().Where(group => group.Name.Equals(groupName)); - var memberGroup = _memberGroupRepository.Get(grpQry).FirstOrDefault(); - if (memberGroup == null) - return Enumerable.Empty(); - - var subQuery = Sql().Select("Member").From().Where(dto => dto.MemberGroup == memberGroup.Id); - - var sql = GetBaseQuery(false) - // TODO: An inner join would be better, though I've read that the query optimizer will always turn a - // subquery with an IN clause into an inner join anyways. - .Append("WHERE umbracoNode.id IN (" + subQuery.SQL + ")", subQuery.Arguments) - .OrderByDescending(x => x.VersionDate) - .OrderBy(x => x.SortOrder); - - return MapDtosToContent(Database.Fetch(sql)); - - } - - public bool Exists(string username) - { - var sql = Sql() - .SelectCount() - .From() - .Where(x => x.LoginName == username); - - return Database.ExecuteScalar(sql) > 0; - } - - public int GetCountByQuery(IQuery query) - { - var sqlWithProps = GetNodeIdQueryWithPropertyData(); - var translator = new SqlTranslator(sqlWithProps, query); - var sql = translator.Translate(); - - //get the COUNT base query - var fullSql = GetBaseQuery(true) - .Append(new Sql("WHERE umbracoNode.id IN (" + sql.SQL + ")", sql.Arguments)); - - return Database.ExecuteScalar(fullSql); - } - - /// - public void SetLastLogin(string username, DateTime date) - { - // Important - these queries are designed to execute without an exclusive WriteLock taken in our distributed lock - // table. However due to the data that we are updating which relies on version data we cannot update this data - // without taking some locks, otherwise we'll end up with strange situations because when a member is updated, that operation - // deletes and re-inserts all property data. So if there are concurrent transactions, one deleting and re-inserting and another trying - // to update there can be problems. This is only an issue for cmsPropertyData, not umbracoContentVersion because that table just - // maintains a single row and it isn't deleted/re-inserted. - // So the important part here is the ForUpdate() call on the select to fetch the property data to update. - - // Update the cms property value for the member - - var sqlSelectTemplateProperty = SqlContext.Templates.Get("Umbraco.Core.MemberRepository.SetLastLogin1", s => s - .Select(x => x.Id) - .From() - .InnerJoin().On((l, r) => l.Id == r.PropertyTypeId) - .InnerJoin().On((l, r) => l.Id == r.VersionId) - .InnerJoin().On((l, r) => l.NodeId == r.NodeId) - .InnerJoin().On((l, r) => l.NodeId == r.NodeId) - .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType")) - .Where(x => x.Alias == SqlTemplate.Arg("propertyTypeAlias")) - .Where(x => x.LoginName == SqlTemplate.Arg("username")) - .ForUpdate()); - var sqlSelectProperty = sqlSelectTemplateProperty.Sql(Cms.Core.Constants.ObjectTypes.Member, Cms.Core.Constants.Conventions.Member.LastLoginDate, username); - - var update = Sql() - .Update(u => u - .Set(x => x.DateValue, date)) - .WhereIn(x => x.Id, sqlSelectProperty); - - Database.Execute(update); - - // Update the umbracoContentVersion value for the member - - var sqlSelectTemplateVersion = SqlContext.Templates.Get("Umbraco.Core.MemberRepository.SetLastLogin2", s => s - .Select(x => x.Id) - .From() - .InnerJoin().On((l, r) => l.NodeId == r.NodeId) - .InnerJoin().On((l, r) => l.NodeId == r.NodeId) - .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType")) - .Where(x => x.LoginName == SqlTemplate.Arg("username"))); - var sqlSelectVersion = sqlSelectTemplateVersion.Sql(Cms.Core.Constants.ObjectTypes.Member, username); - - Database.Execute(Sql() - .Update(u => u - .Set(x => x.VersionDate, date)) - .WhereIn(x => x.Id, sqlSelectVersion)); - } - - /// - /// Gets paged member results. - /// - public override IEnumerable GetPage(IQuery query, - long pageIndex, int pageSize, out long totalRecords, - IQuery filter, - Ordering ordering) - { - Sql filterSql = null; - - if (filter != null) - { - filterSql = Sql(); - foreach (var clause in filter.GetWhereClauses()) - filterSql = filterSql.Append($"AND ({clause.Item1})", clause.Item2); - } - - return GetPage(query, pageIndex, pageSize, out totalRecords, - x => MapDtosToContent(x), - filterSql, - ordering); - } - - protected override string ApplySystemOrdering(ref Sql sql, Ordering ordering) - { - if (ordering.OrderBy.InvariantEquals("email")) - return SqlSyntax.GetFieldName(x => x.Email); - - if (ordering.OrderBy.InvariantEquals("loginName")) - return SqlSyntax.GetFieldName(x => x.LoginName); - - if (ordering.OrderBy.InvariantEquals("userName")) - return SqlSyntax.GetFieldName(x => x.LoginName); - - if (ordering.OrderBy.InvariantEquals("updateDate")) - return SqlSyntax.GetFieldName(x => x.VersionDate); - - if (ordering.OrderBy.InvariantEquals("createDate")) - return SqlSyntax.GetFieldName(x => x.CreateDate); - - if (ordering.OrderBy.InvariantEquals("contentTypeAlias")) - return SqlSyntax.GetFieldName(x => x.Alias); - - return base.ApplySystemOrdering(ref sql, ordering); - } - - private IEnumerable MapDtosToContent(List dtos, bool withCache = false) - { - var temps = new List>(); - var contentTypes = new Dictionary(); - var content = new Member[dtos.Count]; - - for (var i = 0; i < dtos.Count; i++) - { - var dto = dtos[i]; - - if (withCache) - { - // if the cache contains the (proper version of the) item, use it - var cached = IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); - if (cached != null && cached.VersionId == dto.ContentVersionDto.Id) - { - content[i] = (Member)cached; - continue; - } - } - - // else, need to build it - - // get the content type - the repository is full cache *but* still deep-clones - // whatever comes out of it, so use our own local index here to avoid this - var contentTypeId = dto.ContentDto.ContentTypeId; - if (contentTypes.TryGetValue(contentTypeId, out var contentType) == false) - contentTypes[contentTypeId] = contentType = _memberTypeRepository.Get(contentTypeId); - - var c = content[i] = ContentBaseFactory.BuildEntity(dto, contentType); - - // need properties - var versionId = dto.ContentVersionDto.Id; - temps.Add(new TempContent(dto.NodeId, versionId, 0, contentType, c)); - } - - // load all properties for all documents from database in 1 query - indexed by version id - var properties = GetPropertyCollections(temps); - - // assign properties - foreach (var temp in temps) - { - temp.Content.Properties = properties[temp.VersionId]; - - // reset dirty initial properties (U4-1946) - temp.Content.ResetDirtyProperties(false); - } - - return content; - } - - private IMember MapDtoToContent(MemberDto dto) - { - IMemberType memberType = _memberTypeRepository.Get(dto.ContentDto.ContentTypeId); - Member member = ContentBaseFactory.BuildEntity(dto, memberType); - - // get properties - indexed by version id - var versionId = dto.ContentVersionDto.Id; - var temp = new TempContent(dto.ContentDto.NodeId, versionId, 0, memberType); - var properties = GetPropertyCollections(new List> { temp }); - member.Properties = properties[versionId]; - - // reset dirty initial properties (U4-1946) - member.ResetDirtyProperties(false); - return member; - } - - public IMember GetByUsername(string username) - { - return _memberByUsernameCachePolicy.Get(username, PerformGetByUsername, PerformGetAllByUsername); - } - - public int[] GetMemberIds(string[] usernames) - { - var memberObjectType = Cms.Core.Constants.ObjectTypes.Member; - - var memberSql = Sql() - .Select("umbracoNode.id") - .From() - .InnerJoin() - .On(dto => dto.NodeId, dto => dto.NodeId) - .Where(x => x.NodeObjectType == memberObjectType) - .Where("cmsMember.LoginName in (@usernames)", new { /*usernames =*/ usernames }); - return Database.Fetch(memberSql).ToArray(); - } - - private IMember PerformGetByUsername(string username) - { - var query = Query().Where(x => x.Username.Equals(username)); - return PerformGetByQuery(query).FirstOrDefault(); - } - - private IEnumerable PerformGetAllByUsername(params string[] usernames) - { - var query = Query().WhereIn(x => x.Username, usernames); - return PerformGetByQuery(query); - } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PermissionRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PermissionRepository.cs index 582120992b10..a1cfee69a905 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PermissionRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PermissionRepository.cs @@ -4,6 +4,7 @@ using System.Linq; using Microsoft.Extensions.Logging; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Models.Membership; @@ -15,67 +16,72 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement { /// - /// A (sub) repository that exposes functionality to modify assigned permissions to a node + /// A (sub) repository that exposes functionality to modify assigned permissions to a node /// /// /// - /// This repo implements the base class so that permissions can be queued to be persisted - /// like the normal repository pattern but the standard repository Get commands don't apply and will throw + /// This repo implements the base class so that permissions can be + /// queued to be persisted + /// like the normal repository pattern but the standard repository Get commands don't apply and will throw + /// /// internal class PermissionRepository : EntityRepositoryBase where TEntity : class, IEntity { - public PermissionRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger> logger) + public PermissionRepository(IScopeAccessor scopeAccessor, AppCaches cache, + ILogger> logger) : base(scopeAccessor, cache, logger) - { } + { + } /// - /// Returns explicitly defined permissions for a user group for any number of nodes + /// Returns explicitly defined permissions for a user group for any number of nodes /// /// - /// The group ids to lookup permissions for + /// The group ids to lookup permissions for /// /// /// /// - /// This method will not support passing in more than 2000 group Ids + /// This method will not support passing in more than 2000 group IDs when also passing in entity IDs. /// public EntityPermissionCollection GetPermissionsForEntities(int[] groupIds, params int[] entityIds) { var result = new EntityPermissionCollection(); - foreach (var groupOfGroupIds in groupIds.InGroupsOf(2000)) + if (entityIds.Length == 0) { - //copy local - var localIds = groupOfGroupIds.ToArray(); - - if (entityIds.Length == 0) + foreach (IEnumerable group in groupIds.InGroupsOf(Constants.Sql.MaxParameterCount)) { - var sql = Sql() + Sql sql = Sql() .SelectAll() .From() - .Where(dto => localIds.Contains(dto.UserGroupId)); - var permissions = AmbientScope.Database.Fetch(sql); - foreach (var permission in ConvertToPermissionList(permissions)) + .Where(dto => group.Contains(dto.UserGroupId)); + + List permissions = + AmbientScope.Database.Fetch(sql); + foreach (EntityPermission permission in ConvertToPermissionList(permissions)) { result.Add(permission); } } - else + } + else + { + foreach (IEnumerable group in entityIds.InGroupsOf(Constants.Sql.MaxParameterCount - + groupIds.Length)) { - //iterate in groups of 2000 since we don't want to exceed the max SQL param count - foreach (var groupOfEntityIds in entityIds.InGroupsOf(2000)) + Sql sql = Sql() + .SelectAll() + .From() + .Where(dto => + groupIds.Contains(dto.UserGroupId) && group.Contains(dto.NodeId)); + + List permissions = + AmbientScope.Database.Fetch(sql); + foreach (EntityPermission permission in ConvertToPermissionList(permissions)) { - var ids = groupOfEntityIds; - var sql = Sql() - .SelectAll() - .From() - .Where(dto => localIds.Contains(dto.UserGroupId) && ids.Contains(dto.NodeId)); - var permissions = AmbientScope.Database.Fetch(sql); - foreach (var permission in ConvertToPermissionList(permissions)) - { - result.Add(permission); - } + result.Add(permission); } } } @@ -84,60 +90,62 @@ public EntityPermissionCollection GetPermissionsForEntities(int[] groupIds, para } /// - /// Returns permissions directly assigned to the content items for all user groups + /// Returns permissions directly assigned to the content items for all user groups /// /// /// public IEnumerable GetPermissionsForEntities(int[] entityIds) { - var sql = Sql() + Sql sql = Sql() .SelectAll() .From() .Where(dto => entityIds.Contains(dto.NodeId)) .OrderBy(dto => dto.NodeId); - var result = AmbientScope.Database.Fetch(sql); + List result = AmbientScope.Database.Fetch(sql); return ConvertToPermissionList(result); } /// - /// Returns permissions directly assigned to the content item for all user groups + /// Returns permissions directly assigned to the content item for all user groups /// /// /// public EntityPermissionCollection GetPermissionsForEntity(int entityId) { - var sql = Sql() + Sql sql = Sql() .SelectAll() .From() .Where(dto => dto.NodeId == entityId) .OrderBy(dto => dto.NodeId); - var result = AmbientScope.Database.Fetch(sql); + List result = AmbientScope.Database.Fetch(sql); return ConvertToPermissionList(result); } /// - /// Assigns the same permission set for a single group to any number of entities + /// Assigns the same permission set for a single group to any number of entities /// /// /// /// /// - /// This will first clear the permissions for this user and entities and recreate them + /// This will first clear the permissions for this user and entities and recreate them /// public void ReplacePermissions(int groupId, IEnumerable permissions, params int[] entityIds) { if (entityIds.Length == 0) + { return; + } - var db = AmbientScope.Database; + IUmbracoDatabase db = AmbientScope.Database; - //we need to batch these in groups of 2000 so we don't exceed the max 2100 limit - var sql = "DELETE FROM umbracoUserGroup2NodePermission WHERE userGroupId = @groupId AND nodeId in (@nodeIds)"; - foreach (var idGroup in entityIds.InGroupsOf(2000)) + var sql = + "DELETE FROM umbracoUserGroup2NodePermission WHERE userGroupId = @groupId AND nodeId in (@nodeIds)"; + foreach (IEnumerable group in entityIds.InGroupsOf(Constants.Sql.MaxParameterCount)) { - db.Execute(sql, new { groupId, nodeIds = idGroup }); + db.Execute(sql, new { groupId, nodeIds = group }); } var toInsert = new List(); @@ -147,9 +155,7 @@ public void ReplacePermissions(int groupId, IEnumerable permissions, param { toInsert.Add(new UserGroup2NodePermissionDto { - NodeId = e, - Permission = p.ToString(CultureInfo.InvariantCulture), - UserGroupId = groupId + NodeId = e, Permission = p.ToString(CultureInfo.InvariantCulture), UserGroupId = groupId }); } } @@ -158,46 +164,41 @@ public void ReplacePermissions(int groupId, IEnumerable permissions, param } /// - /// Assigns one permission for a user to many entities + /// Assigns one permission for a user to many entities /// /// /// /// public void AssignPermission(int groupId, char permission, params int[] entityIds) { - var db = AmbientScope.Database; + IUmbracoDatabase db = AmbientScope.Database; - var sql = "DELETE FROM umbracoUserGroup2NodePermission WHERE userGroupId = @groupId AND permission=@permission AND nodeId in (@entityIds)"; + var sql = + "DELETE FROM umbracoUserGroup2NodePermission WHERE userGroupId = @groupId AND permission=@permission AND nodeId in (@entityIds)"; db.Execute(sql, - new - { - groupId, - permission = permission.ToString(CultureInfo.InvariantCulture), - entityIds - }); + new { groupId, permission = permission.ToString(CultureInfo.InvariantCulture), entityIds }); - var actions = entityIds.Select(id => new UserGroup2NodePermissionDto + UserGroup2NodePermissionDto[] actions = entityIds.Select(id => new UserGroup2NodePermissionDto { - NodeId = id, - Permission = permission.ToString(CultureInfo.InvariantCulture), - UserGroupId = groupId + NodeId = id, Permission = permission.ToString(CultureInfo.InvariantCulture), UserGroupId = groupId }).ToArray(); db.BulkInsertRecords(actions); } /// - /// Assigns one permission to an entity for multiple groups + /// Assigns one permission to an entity for multiple groups /// /// /// /// public void AssignEntityPermission(TEntity entity, char permission, IEnumerable groupIds) { - var db = AmbientScope.Database; + IUmbracoDatabase db = AmbientScope.Database; var groupIdsA = groupIds.ToArray(); - const string sql = "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @nodeId AND permission = @permission AND userGroupId in (@groupIds)"; + const string sql = + "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @nodeId AND permission = @permission AND userGroupId in (@groupIds)"; db.Execute(sql, new { @@ -206,33 +207,31 @@ public void AssignEntityPermission(TEntity entity, char permission, IEnumerable< groupIds = groupIdsA }); - var actions = groupIdsA.Select(id => new UserGroup2NodePermissionDto + UserGroup2NodePermissionDto[] actions = groupIdsA.Select(id => new UserGroup2NodePermissionDto { - NodeId = entity.Id, - Permission = permission.ToString(CultureInfo.InvariantCulture), - UserGroupId = id + NodeId = entity.Id, Permission = permission.ToString(CultureInfo.InvariantCulture), UserGroupId = id }).ToArray(); db.BulkInsertRecords(actions); } /// - /// Assigns permissions to an entity for multiple group/permission entries + /// Assigns permissions to an entity for multiple group/permission entries /// /// /// /// - /// This will first clear the permissions for this entity then re-create them + /// This will first clear the permissions for this entity then re-create them /// public void ReplaceEntityPermissions(EntityPermissionSet permissionSet) { - var db = AmbientScope.Database; + IUmbracoDatabase db = AmbientScope.Database; const string sql = "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @nodeId"; db.Execute(sql, new { nodeId = permissionSet.EntityId }); var toInsert = new List(); - foreach (var entityPermission in permissionSet.PermissionsSet) + foreach (EntityPermission entityPermission in permissionSet.PermissionsSet) { foreach (var permission in entityPermission.AssignedPermissions) { @@ -248,62 +247,21 @@ public void ReplaceEntityPermissions(EntityPermissionSet permissionSet) db.BulkInsertRecords(toInsert); } - #region Not implemented (don't need to for the purposes of this repo) - - protected override ContentPermissionSet PerformGet(int id) - { - throw new InvalidOperationException("This method won't be implemented."); - } - - protected override IEnumerable PerformGetAll(params int[] ids) - { - throw new InvalidOperationException("This method won't be implemented."); - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - throw new InvalidOperationException("This method won't be implemented."); - } - - protected override Sql GetBaseQuery(bool isCount) - { - throw new InvalidOperationException("This method won't be implemented."); - } - - protected override string GetBaseWhereClause() - { - throw new InvalidOperationException("This method won't be implemented."); - } - - protected override IEnumerable GetDeleteClauses() - { - return new List(); - } - - protected override void PersistDeletedItem(ContentPermissionSet entity) - { - throw new InvalidOperationException("This method won't be implemented."); - } - - #endregion - /// - /// Used to add or update entity permissions during a content item being updated + /// Used to add or update entity permissions during a content item being updated /// /// - protected override void PersistNewItem(ContentPermissionSet entity) - { + protected override void PersistNewItem(ContentPermissionSet entity) => //does the same thing as update PersistUpdatedItem(entity); - } /// - /// Used to add or update entity permissions during a content item being updated + /// Used to add or update entity permissions during a content item being updated /// /// protected override void PersistUpdatedItem(ContentPermissionSet entity) { - var asIEntity = (IEntity) entity; + var asIEntity = (IEntity)entity; if (asIEntity.HasIdentity == false) { throw new InvalidOperationException("Cannot create permissions for an entity without an Id"); @@ -312,14 +270,16 @@ protected override void PersistUpdatedItem(ContentPermissionSet entity) ReplaceEntityPermissions(entity); } - private static EntityPermissionCollection ConvertToPermissionList(IEnumerable result) + private static EntityPermissionCollection ConvertToPermissionList( + IEnumerable result) { var permissions = new EntityPermissionCollection(); - var nodePermissions = result.GroupBy(x => x.NodeId); - foreach (var np in nodePermissions) + IEnumerable> nodePermissions = result.GroupBy(x => x.NodeId); + foreach (IGrouping np in nodePermissions) { - var userGroupPermissions = np.GroupBy(x => x.UserGroupId); - foreach (var permission in userGroupPermissions) + IEnumerable> userGroupPermissions = + np.GroupBy(x => x.UserGroupId); + foreach (IGrouping permission in userGroupPermissions) { var perms = permission.Select(x => x.Permission).Distinct().ToArray(); permissions.Add(new EntityPermission(permission.Key, np.Key, perms)); @@ -328,5 +288,29 @@ private static EntityPermissionCollection ConvertToPermissionList(IEnumerable + throw new InvalidOperationException("This method won't be implemented."); + + protected override IEnumerable PerformGetAll(params int[] ids) => + throw new InvalidOperationException("This method won't be implemented."); + + protected override IEnumerable PerformGetByQuery(IQuery query) => + throw new InvalidOperationException("This method won't be implemented."); + + protected override Sql GetBaseQuery(bool isCount) => + throw new InvalidOperationException("This method won't be implemented."); + + protected override string GetBaseWhereClause() => + throw new InvalidOperationException("This method won't be implemented."); + + protected override IEnumerable GetDeleteClauses() => new List(); + + protected override void PersistDeletedItem(ContentPermissionSet entity) => + throw new InvalidOperationException("This method won't be implemented."); + + #endregion } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs index 1536768bae97..6ab29aa47e4a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs @@ -4,6 +4,7 @@ using System.Security.Cryptography; using Microsoft.Extensions.Logging; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; @@ -16,85 +17,183 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement { internal class RedirectUrlRepository : EntityRepositoryBase, IRedirectUrlRepository { - public RedirectUrlRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) + public RedirectUrlRepository(IScopeAccessor scopeAccessor, AppCaches cache, + ILogger logger) : base(scopeAccessor, cache, logger) - { } + { + } - protected override int PerformCount(IQuery query) + public IRedirectUrl Get(string url, Guid contentKey, string culture) { - throw new NotSupportedException("This repository does not support this method."); + var urlHash = url.GenerateHash(); + Sql sql = GetBaseQuery(false).Where(x => + x.Url == url && x.UrlHash == urlHash && x.ContentKey == contentKey && x.Culture == culture); + RedirectUrlDto dto = Database.Fetch(sql).FirstOrDefault(); + return dto == null ? null : Map(dto); + } + + public void DeleteAll() => Database.Execute("DELETE FROM umbracoRedirectUrl"); + + public void DeleteContentUrls(Guid contentKey) => + Database.Execute("DELETE FROM umbracoRedirectUrl WHERE contentKey=@contentKey", new { contentKey }); + + public void Delete(Guid id) => Database.Delete(id); + + public IRedirectUrl GetMostRecentUrl(string url) + { + var urlHash = url.GenerateHash(); + Sql sql = GetBaseQuery(false) + .Where(x => x.Url == url && x.UrlHash == urlHash) + .OrderByDescending(x => x.CreateDateUtc); + List dtos = Database.Fetch(sql); + RedirectUrlDto dto = dtos.FirstOrDefault(); + return dto == null ? null : Map(dto); + } + + public IRedirectUrl GetMostRecentUrl(string url, string culture) + { + if (string.IsNullOrWhiteSpace(culture)) + { + return GetMostRecentUrl(url); + } + + var urlHash = url.GenerateHash(); + Sql sql = GetBaseQuery(false) + .Where(x => x.Url == url && x.UrlHash == urlHash && + (x.Culture == culture.ToLower() || x.Culture == string.Empty)) + .OrderByDescending(x => x.CreateDateUtc); + List dtos = Database.Fetch(sql); + RedirectUrlDto dto = dtos.FirstOrDefault(f => f.Culture == culture.ToLower()); + + if (dto == null) + { + dto = dtos.FirstOrDefault(f => f.Culture == string.Empty); + } + + return dto == null ? null : Map(dto); + } + + public IEnumerable GetContentUrls(Guid contentKey) + { + Sql sql = GetBaseQuery(false) + .Where(x => x.ContentKey == contentKey) + .OrderByDescending(x => x.CreateDateUtc); + List dtos = Database.Fetch(sql); + return dtos.Select(Map); } - protected override bool PerformExists(Guid id) + public IEnumerable GetAllUrls(long pageIndex, int pageSize, out long total) { - return PerformGet(id) != null; + Sql sql = GetBaseQuery(false) + .OrderByDescending(x => x.CreateDateUtc); + Page result = Database.Page(pageIndex + 1, pageSize, sql); + total = Convert.ToInt32(result.TotalItems); + return result.Items.Select(Map); } + public IEnumerable GetAllUrls(int rootContentId, long pageIndex, int pageSize, out long total) + { + Sql sql = GetBaseQuery(false) + .Where( + string.Format("{0}.{1} LIKE @path", SqlSyntax.GetQuotedTableName("umbracoNode"), + SqlSyntax.GetQuotedColumnName("path")), new { path = "%," + rootContentId + ",%" }) + .OrderByDescending(x => x.CreateDateUtc); + Page result = Database.Page(pageIndex + 1, pageSize, sql); + total = Convert.ToInt32(result.TotalItems); + + IEnumerable rules = result.Items.Select(Map); + return rules; + } + + public IEnumerable SearchUrls(string searchTerm, long pageIndex, int pageSize, out long total) + { + Sql sql = GetBaseQuery(false) + .Where( + string.Format("{0}.{1} LIKE @url", SqlSyntax.GetQuotedTableName("umbracoRedirectUrl"), + SqlSyntax.GetQuotedColumnName("Url")), + new { url = "%" + searchTerm.Trim().ToLowerInvariant() + "%" }) + .OrderByDescending(x => x.CreateDateUtc); + Page result = Database.Page(pageIndex + 1, pageSize, sql); + total = Convert.ToInt32(result.TotalItems); + + IEnumerable rules = result.Items.Select(Map); + return rules; + } + + protected override int PerformCount(IQuery query) => + throw new NotSupportedException("This repository does not support this method."); + + protected override bool PerformExists(Guid id) => PerformGet(id) != null; + protected override IRedirectUrl PerformGet(Guid id) { - var sql = GetBaseQuery(false).Where(x => x.Id == id); - var dto = Database.Fetch(sql.SelectTop(1)).FirstOrDefault(); + Sql sql = GetBaseQuery(false).Where(x => x.Id == id); + RedirectUrlDto dto = Database.Fetch(sql.SelectTop(1)).FirstOrDefault(); return dto == null ? null : Map(dto); } protected override IEnumerable PerformGetAll(params Guid[] ids) { - if (ids.Length > 2000) - throw new NotSupportedException("This repository does not support more than 2000 ids."); - var sql = GetBaseQuery(false).WhereIn(x => x.Id, ids); - var dtos = Database.Fetch(sql); + if (ids.Length > Constants.Sql.MaxParameterCount) + { + throw new NotSupportedException( + $"This repository does not support more than {Constants.Sql.MaxParameterCount} ids."); + } + + Sql sql = GetBaseQuery(false).WhereIn(x => x.Id, ids); + List dtos = Database.Fetch(sql); return dtos.WhereNotNull().Select(Map); } - protected override IEnumerable PerformGetByQuery(IQuery query) - { + protected override IEnumerable PerformGetByQuery(IQuery query) => throw new NotSupportedException("This repository does not support this method."); - } protected override Sql GetBaseQuery(bool isCount) { - var sql = Sql(); + Sql sql = Sql(); if (isCount) + { sql.Select(@"COUNT(*) FROM umbracoRedirectUrl JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID"); + } else + { sql.Select(@"umbracoRedirectUrl.*, umbracoNode.id AS contentId FROM umbracoRedirectUrl JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID"); + } + return sql; } - protected override string GetBaseWhereClause() - { - return "id = @id"; - } + protected override string GetBaseWhereClause() => "id = @id"; protected override IEnumerable GetDeleteClauses() { - var list = new List - { - "DELETE FROM umbracoRedirectUrl WHERE id = @id" - }; + var list = new List { "DELETE FROM umbracoRedirectUrl WHERE id = @id" }; return list; } protected override void PersistNewItem(IRedirectUrl entity) { - var dto = Map(entity); + RedirectUrlDto dto = Map(entity); Database.Insert(dto); entity.Id = entity.Key.GetHashCode(); } protected override void PersistUpdatedItem(IRedirectUrl entity) { - var dto = Map(entity); + RedirectUrlDto dto = Map(entity); Database.Update(dto); } private static RedirectUrlDto Map(IRedirectUrl redirectUrl) { - if (redirectUrl == null) return null; + if (redirectUrl == null) + { + return null; + } return new RedirectUrlDto { @@ -109,7 +208,10 @@ private static RedirectUrlDto Map(IRedirectUrl redirectUrl) private static IRedirectUrl Map(RedirectUrlDto dto) { - if (dto == null) return null; + if (dto == null) + { + return null; + } var url = new RedirectUrl(); try @@ -129,98 +231,5 @@ private static IRedirectUrl Map(RedirectUrlDto dto) url.EnableChangeTracking(); } } - - public IRedirectUrl Get(string url, Guid contentKey, string culture) - { - var urlHash = url.GenerateHash(); - var sql = GetBaseQuery(false).Where(x => x.Url == url && x.UrlHash == urlHash && x.ContentKey == contentKey && x.Culture == culture); - var dto = Database.Fetch(sql).FirstOrDefault(); - return dto == null ? null : Map(dto); - } - - public void DeleteAll() - { - Database.Execute("DELETE FROM umbracoRedirectUrl"); - } - - public void DeleteContentUrls(Guid contentKey) - { - Database.Execute("DELETE FROM umbracoRedirectUrl WHERE contentKey=@contentKey", new { contentKey }); - } - - public void Delete(Guid id) - { - Database.Delete(id); - } - - public IRedirectUrl GetMostRecentUrl(string url) - { - var urlHash = url.GenerateHash(); - var sql = GetBaseQuery(false) - .Where(x => x.Url == url && x.UrlHash == urlHash) - .OrderByDescending(x => x.CreateDateUtc); - var dtos = Database.Fetch(sql); - var dto = dtos.FirstOrDefault(); - return dto == null ? null : Map(dto); - } - - public IRedirectUrl GetMostRecentUrl(string url, string culture) - { - if (string.IsNullOrWhiteSpace(culture)) return GetMostRecentUrl(url); - var urlHash = url.GenerateHash(); - var sql = GetBaseQuery(false) - .Where(x => x.Url == url && x.UrlHash == urlHash && - (x.Culture == culture.ToLower() || x.Culture == string.Empty)) - .OrderByDescending(x => x.CreateDateUtc); - var dtos = Database.Fetch(sql); - var dto = dtos.FirstOrDefault(f => f.Culture == culture.ToLower()); - - if (dto == null) - dto = dtos.FirstOrDefault(f => f.Culture == string.Empty); - - return dto == null ? null : Map(dto); - } - - public IEnumerable GetContentUrls(Guid contentKey) - { - var sql = GetBaseQuery(false) - .Where(x => x.ContentKey == contentKey) - .OrderByDescending(x => x.CreateDateUtc); - var dtos = Database.Fetch(sql); - return dtos.Select(Map); - } - - public IEnumerable GetAllUrls(long pageIndex, int pageSize, out long total) - { - var sql = GetBaseQuery(false) - .OrderByDescending(x => x.CreateDateUtc); - var result = Database.Page(pageIndex + 1, pageSize, sql); - total = Convert.ToInt32(result.TotalItems); - return result.Items.Select(Map); - } - - public IEnumerable GetAllUrls(int rootContentId, long pageIndex, int pageSize, out long total) - { - var sql = GetBaseQuery(false) - .Where(string.Format("{0}.{1} LIKE @path", SqlSyntax.GetQuotedTableName("umbracoNode"), SqlSyntax.GetQuotedColumnName("path")), new { path = "%," + rootContentId + ",%" }) - .OrderByDescending(x => x.CreateDateUtc); - var result = Database.Page(pageIndex + 1, pageSize, sql); - total = Convert.ToInt32(result.TotalItems); - - var rules = result.Items.Select(Map); - return rules; - } - - public IEnumerable SearchUrls(string searchTerm, long pageIndex, int pageSize, out long total) - { - var sql = GetBaseQuery(false) - .Where(string.Format("{0}.{1} LIKE @url", SqlSyntax.GetQuotedTableName("umbracoRedirectUrl"), SqlSyntax.GetQuotedColumnName("Url")), new { url = "%" + searchTerm.Trim().ToLowerInvariant() + "%" }) - .OrderByDescending(x => x.CreateDateUtc); - var result = Database.Page(pageIndex + 1, pageSize, sql); - total = Convert.ToInt32(result.TotalItems); - - var rules = result.Items.Select(Map); - return rules; - } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs index 8fbc7845769b..919bbeea312d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs @@ -4,6 +4,7 @@ using System.Text; using Microsoft.Extensions.Logging; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; @@ -21,24 +22,26 @@ internal class TagRepository : EntityRepositoryBase, ITagRepository { public TagRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) : base(scopeAccessor, cache, logger) - { } + { + } #region Manage Tag Entities /// protected override ITag PerformGet(int id) { - var sql = Sql().Select().From().Where(x => x.Id == id); - var dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); + Sql sql = Sql().Select().From().Where(x => x.Id == id); + TagDto dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); return dto == null ? null : TagFactory.BuildEntity(dto); } /// protected override IEnumerable PerformGetAll(params int[] ids) { - var dtos = ids.Length == 0 + IEnumerable dtos = ids.Length == 0 ? Database.Fetch(Sql().Select().From()) - : Database.FetchByGroups(ids, 2000, batch => Sql().Select().From().WhereIn(x => x.Id, batch)); + : Database.FetchByGroups(ids, Constants.Sql.MaxParameterCount, + batch => Sql().Select().From().WhereIn(x => x.Id, batch)); return dtos.Select(TagFactory.BuildEntity).ToList(); } @@ -46,7 +49,7 @@ protected override IEnumerable PerformGetAll(params int[] ids) /// protected override IEnumerable PerformGetByQuery(IQuery query) { - var sql = Sql().Select().From(); + Sql sql = Sql().Select().From(); var translator = new SqlTranslator(sql, query); sql = translator.Translate(); @@ -54,30 +57,21 @@ protected override IEnumerable PerformGetByQuery(IQuery query) } /// - protected override Sql GetBaseQuery(bool isCount) - { - return isCount ? Sql().SelectCount().From() : GetBaseQuery(); - } + protected override Sql GetBaseQuery(bool isCount) => + isCount ? Sql().SelectCount().From() : GetBaseQuery(); - private Sql GetBaseQuery() - { - return Sql().Select().From(); - } + private Sql GetBaseQuery() => Sql().Select().From(); /// - protected override string GetBaseWhereClause() - { - return "id = @id"; - } + protected override string GetBaseWhereClause() => "id = @id"; /// protected override IEnumerable GetDeleteClauses() { var list = new List - { - "DELETE FROM cmsTagRelationship WHERE tagId = @id", - "DELETE FROM cmsTags WHERE id = @id" - }; + { + "DELETE FROM cmsTagRelationship WHERE tagId = @id", "DELETE FROM cmsTags WHERE id = @id" + }; return list; } @@ -86,7 +80,7 @@ protected override void PersistNewItem(ITag entity) { entity.AddingEntity(); - var dto = TagFactory.BuildDto(entity); + TagDto dto = TagFactory.BuildDto(entity); var id = Convert.ToInt32(Database.Insert(dto)); entity.Id = id; @@ -98,7 +92,7 @@ protected override void PersistUpdatedItem(ITag entity) { entity.UpdatingEntity(); - var dto = TagFactory.BuildDto(entity); + TagDto dto = TagFactory.BuildDto(entity); Database.Update(dto); entity.ResetDirtyProperties(); @@ -113,18 +107,21 @@ protected override void PersistUpdatedItem(ITag entity) public void Assign(int contentId, int propertyTypeId, IEnumerable tags, bool replaceTags = true) { // to no-duplicates array - var tagsA = tags.Distinct(new TagComparer()).ToArray(); + ITag[] tagsA = tags.Distinct(new TagComparer()).ToArray(); // replacing = clear all if (replaceTags) { - var sql0 = Sql().Delete().Where(x => x.NodeId == contentId && x.PropertyTypeId == propertyTypeId); + Sql sql0 = Sql().Delete() + .Where(x => x.NodeId == contentId && x.PropertyTypeId == propertyTypeId); Database.Execute(sql0); } // no tags? nothing else to do if (tagsA.Length == 0) + { return; + } // tags // using some clever logic (?) to insert tags that don't exist in 1 query @@ -163,7 +160,8 @@ public void Remove(int contentId, int propertyTypeId, IEnumerable tags) var tagSetSql = GetTagSet(tags); var group = SqlSyntax.GetQuotedColumnName("group"); - var deleteSql = $@"DELETE FROM cmsTagRelationship WHERE nodeId = {contentId} AND propertyTypeId = {propertyTypeId} AND tagId IN ( + var deleteSql = + $@"DELETE FROM cmsTagRelationship WHERE nodeId = {contentId} AND propertyTypeId = {propertyTypeId} AND tagId IN ( SELECT id FROM cmsTags INNER JOIN {tagSetSql} ON ( tagSet.tag = cmsTags.tag AND tagSet.{group} = cmsTags.{group} AND COALESCE(tagSet.languageId, -1) = COALESCE(cmsTags.languageId, -1) ) @@ -173,18 +171,15 @@ public void Remove(int contentId, int propertyTypeId, IEnumerable tags) } /// - public void RemoveAll(int contentId, int propertyTypeId) - { - Database.Execute("DELETE FROM cmsTagRelationship WHERE nodeId = @nodeId AND propertyTypeId = @propertyTypeId", - new { nodeId = contentId, propertyTypeId = propertyTypeId }); - } + public void RemoveAll(int contentId, int propertyTypeId) => + Database.Execute( + "DELETE FROM cmsTagRelationship WHERE nodeId = @nodeId AND propertyTypeId = @propertyTypeId", + new { nodeId = contentId, propertyTypeId }); /// - public void RemoveAll(int contentId) - { + public void RemoveAll(int contentId) => Database.Execute("DELETE FROM cmsTagRelationship WHERE nodeId = @nodeId", new { nodeId = contentId }); - } // this is a clever way to produce an SQL statement like this: // @@ -204,10 +199,16 @@ private string GetTagSet(IEnumerable tags) sql.Append("("); - foreach (var tag in tags) + foreach (ITag tag in tags) { - if (first) first = false; - else sql.Append(" UNION "); + if (first) + { + first = false; + } + else + { + sql.Append(" UNION "); + } sql.Append("SELECT N'"); sql.Append(SqlSyntax.EscapeString(tag.Text)); @@ -217,9 +218,14 @@ private string GetTagSet(IEnumerable tags) sql.Append(group); sql.Append(" , "); if (tag.LanguageId.HasValue) + { sql.Append(tag.LanguageId); + } else + { sql.Append("NULL"); + } + sql.Append(" AS languageId"); } @@ -231,19 +237,17 @@ private string GetTagSet(IEnumerable tags) // used to run Distinct() on tags private class TagComparer : IEqualityComparer { - public bool Equals(ITag x, ITag y) - { - return ReferenceEquals(x, y) // takes care of both being null - || x != null && y != null && x.Text == y.Text && x.Group == y.Group && x.LanguageId == y.LanguageId; - } + public bool Equals(ITag x, ITag y) => + ReferenceEquals(x, y) // takes care of both being null + || (x != null && y != null && x.Text == y.Text && x.Group == y.Group && x.LanguageId == y.LanguageId); public int GetHashCode(ITag obj) { unchecked { var h = obj.Text.GetHashCode(); - h = h * 397 ^ obj.Group.GetHashCode(); - h = h * 397 ^ (obj.LanguageId?.GetHashCode() ?? 0); + h = (h * 397) ^ obj.Group.GetHashCode(); + h = (h * 397) ^ (obj.LanguageId?.GetHashCode() ?? 0); return h; } } @@ -273,7 +277,7 @@ private class TaggedEntityDto /// public TaggedEntity GetTaggedEntityByKey(Guid key) { - var sql = GetTaggedEntitiesSql(TaggableObjectTypes.All, "*"); + Sql sql = GetTaggedEntitiesSql(TaggableObjectTypes.All, "*"); sql = sql .Where(dto => dto.UniqueId == key); @@ -284,7 +288,7 @@ public TaggedEntity GetTaggedEntityByKey(Guid key) /// public TaggedEntity GetTaggedEntityById(int id) { - var sql = GetTaggedEntitiesSql(TaggableObjectTypes.All, "*"); + Sql sql = GetTaggedEntitiesSql(TaggableObjectTypes.All, "*"); sql = sql .Where(dto => dto.NodeId == id); @@ -293,9 +297,10 @@ public TaggedEntity GetTaggedEntityById(int id) } /// - public IEnumerable GetTaggedEntitiesByTagGroup(TaggableObjectTypes objectType, string group, string culture = null) + public IEnumerable GetTaggedEntitiesByTagGroup(TaggableObjectTypes objectType, string group, + string culture = null) { - var sql = GetTaggedEntitiesSql(objectType, culture); + Sql sql = GetTaggedEntitiesSql(objectType, culture); sql = sql .Where(x => x.Group == group); @@ -304,30 +309,37 @@ public IEnumerable GetTaggedEntitiesByTagGroup(TaggableObjectTypes } /// - public IEnumerable GetTaggedEntitiesByTag(TaggableObjectTypes objectType, string tag, string group = null, string culture = null) + public IEnumerable GetTaggedEntitiesByTag(TaggableObjectTypes objectType, string tag, + string group = null, string culture = null) { - var sql = GetTaggedEntitiesSql(objectType, culture); + Sql sql = GetTaggedEntitiesSql(objectType, culture); sql = sql .Where(dto => dto.Text == tag); if (group.IsNullOrWhiteSpace() == false) + { sql = sql .Where(dto => dto.Group == group); + } return Map(Database.Fetch(sql)); } private Sql GetTaggedEntitiesSql(TaggableObjectTypes objectType, string culture) { - var sql = Sql() + Sql sql = Sql() .Select(x => Alias(x.NodeId, "NodeId")) - .AndSelect(x => Alias(x.Alias, "PropertyTypeAlias"), x => Alias(x.Id, "PropertyTypeId")) - .AndSelect(x => Alias(x.Id, "TagId"), x => Alias(x.Text, "TagText"), x => Alias(x.Group, "TagGroup"), x => Alias(x.LanguageId, "TagLanguage")) + .AndSelect(x => Alias(x.Alias, "PropertyTypeAlias"), + x => Alias(x.Id, "PropertyTypeId")) + .AndSelect(x => Alias(x.Id, "TagId"), x => Alias(x.Text, "TagText"), + x => Alias(x.Group, "TagGroup"), x => Alias(x.LanguageId, "TagLanguage")) .From() .InnerJoin().On((tag, rel) => tag.Id == rel.TagId) - .InnerJoin().On((rel, content) => rel.NodeId == content.NodeId) - .InnerJoin().On((rel, prop) => rel.PropertyTypeId == prop.Id) + .InnerJoin() + .On((rel, content) => rel.NodeId == content.NodeId) + .InnerJoin() + .On((rel, prop) => rel.PropertyTypeId == prop.Id) .InnerJoin().On((content, node) => content.NodeId == node.NodeId); if (culture == null) @@ -344,16 +356,15 @@ private Sql GetTaggedEntitiesSql(TaggableObjectTypes objectType, st if (objectType != TaggableObjectTypes.All) { - var nodeObjectType = GetNodeObjectType(objectType); + Guid nodeObjectType = GetNodeObjectType(objectType); sql = sql.Where(dto => dto.NodeObjectType == nodeObjectType); } return sql; } - private static IEnumerable Map(IEnumerable dtos) - { - return dtos.GroupBy(x => x.NodeId).Select(dtosForNode => + private static IEnumerable Map(IEnumerable dtos) => + dtos.GroupBy(x => x.NodeId).Select(dtosForNode => { var taggedProperties = dtosForNode.GroupBy(x => x.PropertyTypeId).Select(dtosForProperty => { @@ -368,25 +379,27 @@ private static IEnumerable Map(IEnumerable dtos) return new TaggedEntity(dtosForNode.Key, taggedProperties); }).ToList(); - } /// - public IEnumerable GetTagsForEntityType(TaggableObjectTypes objectType, string group = null, string culture = null) + public IEnumerable GetTagsForEntityType(TaggableObjectTypes objectType, string group = null, + string culture = null) { - var sql = GetTagsSql(culture, true); + Sql sql = GetTagsSql(culture, true); AddTagsSqlWhere(sql, culture); if (objectType != TaggableObjectTypes.All) { - var nodeObjectType = GetNodeObjectType(objectType); + Guid nodeObjectType = GetNodeObjectType(objectType); sql = sql .Where(dto => dto.NodeObjectType == nodeObjectType); } if (group.IsNullOrWhiteSpace() == false) + { sql = sql .Where(dto => dto.Group == group); + } sql = sql .GroupBy(x => x.Id, x => x.Text, x => x.Group, x => x.LanguageId); @@ -397,7 +410,7 @@ public IEnumerable GetTagsForEntityType(TaggableObjectTypes objectType, st /// public IEnumerable GetTagsForEntity(int contentId, string group = null, string culture = null) { - var sql = GetTagsSql(culture); + Sql sql = GetTagsSql(culture); AddTagsSqlWhere(sql, culture); @@ -405,8 +418,10 @@ public IEnumerable GetTagsForEntity(int contentId, string group = null, st .Where(dto => dto.NodeId == contentId); if (group.IsNullOrWhiteSpace() == false) + { sql = sql .Where(dto => dto.Group == group); + } return ExecuteTagsQuery(sql); } @@ -414,7 +429,7 @@ public IEnumerable GetTagsForEntity(int contentId, string group = null, st /// public IEnumerable GetTagsForEntity(Guid contentId, string group = null, string culture = null) { - var sql = GetTagsSql(culture); + Sql sql = GetTagsSql(culture); AddTagsSqlWhere(sql, culture); @@ -422,63 +437,76 @@ public IEnumerable GetTagsForEntity(Guid contentId, string group = null, s .Where(dto => dto.UniqueId == contentId); if (group.IsNullOrWhiteSpace() == false) + { sql = sql .Where(dto => dto.Group == group); + } return ExecuteTagsQuery(sql); } /// - public IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null, string culture = null) + public IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null, + string culture = null) { - var sql = GetTagsSql(culture); + Sql sql = GetTagsSql(culture); sql = sql - .InnerJoin().On((prop, rel) => prop.Id == rel.PropertyTypeId) + .InnerJoin() + .On((prop, rel) => prop.Id == rel.PropertyTypeId) .Where(x => x.NodeId == contentId) .Where(x => x.Alias == propertyTypeAlias); AddTagsSqlWhere(sql, culture); if (group.IsNullOrWhiteSpace() == false) + { sql = sql .Where(dto => dto.Group == group); + } return ExecuteTagsQuery(sql); } /// - public IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string group = null, string culture = null) + public IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string group = null, + string culture = null) { - var sql = GetTagsSql(culture); + Sql sql = GetTagsSql(culture); sql = sql - .InnerJoin().On((prop, rel) => prop.Id == rel.PropertyTypeId) + .InnerJoin() + .On((prop, rel) => prop.Id == rel.PropertyTypeId) .Where(dto => dto.UniqueId == contentId) .Where(dto => dto.Alias == propertyTypeAlias); AddTagsSqlWhere(sql, culture); if (group.IsNullOrWhiteSpace() == false) + { sql = sql .Where(dto => dto.Group == group); + } return ExecuteTagsQuery(sql); } private Sql GetTagsSql(string culture, bool withGrouping = false) { - var sql = Sql() + Sql sql = Sql() .Select(); if (withGrouping) + { sql = sql .AndSelectCount("NodeCount"); + } sql = sql .From() .InnerJoin().On((rel, tag) => tag.Id == rel.TagId) - .InnerJoin().On((content, rel) => content.NodeId == rel.NodeId) + .InnerJoin() + .On((content, rel) => content.NodeId == rel.NodeId) .InnerJoin().On((node, content) => node.NodeId == content.NodeId); if (culture != null && culture != "*") @@ -506,21 +534,19 @@ private Sql AddTagsSqlWhere(Sql sql, string culture) return sql; } - private IEnumerable ExecuteTagsQuery(Sql sql) - { - return Database.Fetch(sql).Select(TagFactory.BuildEntity); - } + private IEnumerable ExecuteTagsQuery(Sql sql) => + Database.Fetch(sql).Select(TagFactory.BuildEntity); private Guid GetNodeObjectType(TaggableObjectTypes type) { switch (type) { case TaggableObjectTypes.Content: - return Cms.Core.Constants.ObjectTypes.Document; + return Constants.ObjectTypes.Document; case TaggableObjectTypes.Media: - return Cms.Core.Constants.ObjectTypes.Media; + return Constants.ObjectTypes.Media; case TaggableObjectTypes.Member: - return Cms.Core.Constants.ObjectTypes.Member; + return Constants.ObjectTypes.Member; default: throw new ArgumentOutOfRangeException(nameof(type)); } diff --git a/src/Umbraco.Infrastructure/Scoping/Scope.cs b/src/Umbraco.Infrastructure/Scoping/Scope.cs index e0423cc340d3..2383e8eb928e 100644 --- a/src/Umbraco.Infrastructure/Scoping/Scope.cs +++ b/src/Umbraco.Infrastructure/Scoping/Scope.cs @@ -5,65 +5,56 @@ using System.Text; using System.Threading; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Umbraco.Extensions; -using Umbraco.Core.Collections; using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Infrastructure.Persistence; -using CoreDebugSettings = Umbraco.Cms.Core.Configuration.Models.CoreDebugSettings; +using Umbraco.Core.Collections; +using Umbraco.Extensions; namespace Umbraco.Cms.Core.Scoping { /// - /// Implements . + /// Implements . /// /// Not thread-safe obviously. internal class Scope : IScope { - private enum LockType - { - ReadLock, - WriteLock - } - - private readonly ScopeProvider _scopeProvider; + private readonly bool _autoComplete; private readonly CoreDebugSettings _coreDebugSettings; - private readonly MediaFileManager _mediaFileManager; + + private readonly object _dictionaryLocker; private readonly IEventAggregator _eventAggregator; - private readonly ILogger _logger; private readonly IsolationLevel _isolationLevel; + private readonly object _lockQueueLocker = new(); + private readonly ILogger _logger; + private readonly MediaFileManager _mediaFileManager; private readonly RepositoryCacheMode _repositoryCacheMode; private readonly bool? _scopeFileSystem; - private readonly bool _autoComplete; + + private readonly ScopeProvider _scopeProvider; private bool _callContext; + private bool? _completed; + private IUmbracoDatabase _database; private bool _disposed; - private bool? _completed; + private IEventDispatcher _eventDispatcher; + private ICompletable _fscope; private IsolatedCaches _isolatedCaches; - private IUmbracoDatabase _database; private EventMessages _messages; - private ICompletable _fscope; - private IEventDispatcher _eventDispatcher; private IScopedNotificationPublisher _notificationPublisher; - private readonly object _dictionaryLocker; - private readonly object _lockQueueLocker = new object(); + private StackQueue<(LockType lockType, TimeSpan timeout, Guid instanceId, int lockId)> _queuedLocks; // This is all used to safely track read/write locks at given Scope levels so that // when we dispose we can verify that everything has been cleaned up correctly. private HashSet _readLocks; - private HashSet _writeLocks; private Dictionary> _readLocksDictionary; + private HashSet _writeLocks; private Dictionary> _writeLocksDictionary; - private StackQueue<(LockType lockType, TimeSpan timeout, Guid instanceId, int lockId)> _queuedLocks; - - internal Dictionary> ReadLocks => _readLocksDictionary; - internal Dictionary> WriteLocks => _writeLocksDictionary; - // initializes a new scope private Scope( ScopeProvider scopeProvider, @@ -103,7 +94,8 @@ private Scope( #if DEBUG_SCOPES _scopeProvider.RegisterScope(this); #endif - logger.LogTrace("Create {InstanceId} on thread {ThreadId}", InstanceId.ToString("N").Substring(0, 8), Thread.CurrentThread.ManagedThreadId); + logger.LogTrace("Create {InstanceId} on thread {ThreadId}", InstanceId.ToString("N").Substring(0, 8), + Thread.CurrentThread.ManagedThreadId); if (detachable) { @@ -141,9 +133,12 @@ private Scope( // cannot specify a different mode! // TODO: means that it's OK to go from L2 to None for reading purposes, but writing would be BAD! // this is for XmlStore that wants to bypass caches when rebuilding XML (same for NuCache) - if (repositoryCacheMode != RepositoryCacheMode.Unspecified && parent.RepositoryCacheMode > repositoryCacheMode) + if (repositoryCacheMode != RepositoryCacheMode.Unspecified && + parent.RepositoryCacheMode > repositoryCacheMode) { - throw new ArgumentException($"Value '{repositoryCacheMode}' cannot be lower than parent value '{parent.RepositoryCacheMode}'.", nameof(repositoryCacheMode)); + throw new ArgumentException( + $"Value '{repositoryCacheMode}' cannot be lower than parent value '{parent.RepositoryCacheMode}'.", + nameof(repositoryCacheMode)); } // cannot specify a dispatcher! @@ -155,14 +150,17 @@ private Scope( // Only the outermost scope can specify the notification publisher if (_notificationPublisher != null) { - throw new ArgumentException("Value cannot be specified on nested scope.", nameof(notificationPublisher)); + throw new ArgumentException("Value cannot be specified on nested scope.", + nameof(notificationPublisher)); } // cannot specify a different fs scope! // can be 'true' only on outer scope (and false does not make much sense) if (scopeFileSystems != null && parent._scopeFileSystem != scopeFileSystems) { - throw new ArgumentException($"Value '{scopeFileSystems.Value}' be different from parent value '{parent._scopeFileSystem}'.", nameof(scopeFileSystems)); + throw new ArgumentException( + $"Value '{scopeFileSystems.Value}' be different from parent value '{parent._scopeFileSystem}'.", + nameof(scopeFileSystems)); } } else @@ -194,8 +192,11 @@ public Scope( bool? scopeFileSystems = null, bool callContext = false, bool autoComplete = false) - : this(scopeProvider, coreDebugSettings, mediaFileManager, eventAggregator, logger, fileSystems, null, scopeContext, detachable, isolationLevel, repositoryCacheMode, eventDispatcher, scopedNotificationPublisher, scopeFileSystems, callContext, autoComplete) - { } + : this(scopeProvider, coreDebugSettings, mediaFileManager, eventAggregator, logger, fileSystems, null, + scopeContext, detachable, isolationLevel, repositoryCacheMode, eventDispatcher, + scopedNotificationPublisher, scopeFileSystems, callContext, autoComplete) + { + } // initializes a new scope in a nested scopes chain, with its parent public Scope( @@ -213,60 +214,14 @@ public Scope( bool? scopeFileSystems = null, bool callContext = false, bool autoComplete = false) - : this(scopeProvider, coreDebugSettings, mediaFileManager, eventAggregator, logger, fileSystems, parent, null, false, isolationLevel, repositoryCacheMode, eventDispatcher, notificationPublisher, scopeFileSystems, callContext, autoComplete) - { } - - /// - /// Used for testing. Ensures and gets any queued read locks. - /// - /// - internal Dictionary> GetReadLocks() - { - EnsureDbLocks(); - // always delegate to root/parent scope. - if (ParentScope is not null) - { - return ParentScope.GetReadLocks(); - } - else - { - return _readLocksDictionary; - } - } - - /// - /// Used for testing. Ensures and gets and queued write locks. - /// - /// - internal Dictionary> GetWriteLocks() + : this(scopeProvider, coreDebugSettings, mediaFileManager, eventAggregator, logger, fileSystems, parent, + null, false, isolationLevel, repositoryCacheMode, eventDispatcher, notificationPublisher, + scopeFileSystems, callContext, autoComplete) { - EnsureDbLocks(); - // always delegate to root/parent scope. - if (ParentScope is not null) - { - return ParentScope.GetWriteLocks(); - } - else - { - return _writeLocksDictionary; - } } - public Guid InstanceId { get; } = Guid.NewGuid(); - - public int CreatedThreadId { get; } = Thread.CurrentThread.ManagedThreadId; - - public ISqlContext SqlContext - { - get - { - if (_scopeProvider.SqlContext == null) - { - throw new InvalidOperationException($"The {nameof(_scopeProvider.SqlContext)} on the scope provider is null"); - } - return _scopeProvider.SqlContext; - } - } + internal Dictionary> ReadLocks => _readLocksDictionary; + internal Dictionary> WriteLocks => _writeLocksDictionary; // a value indicating whether to force call-context public bool CallContext @@ -301,72 +256,121 @@ public bool ScopedFileSystems } } - /// - public RepositoryCacheMode RepositoryCacheMode + // a value indicating whether the scope is detachable + // ie whether it was created by CreateDetachedScope + public bool Detachable { get; } + + // the parent scope (in a nested scopes chain) + public Scope ParentScope { get; set; } + + public bool Attached { get; set; } + + // the original scope (when attaching a detachable scope) + public Scope OrigScope { get; set; } + + // the original context (when attaching a detachable scope) + public IScopeContext OrigContext { get; set; } + + // the context (for attaching & detaching only) + public IScopeContext Context { get; } + + public IsolationLevel IsolationLevel { get { - if (_repositoryCacheMode != RepositoryCacheMode.Unspecified) + if (_isolationLevel != IsolationLevel.Unspecified) { - return _repositoryCacheMode; + return _isolationLevel; } if (ParentScope != null) { - return ParentScope.RepositoryCacheMode; + return ParentScope.IsolationLevel; } - return RepositoryCacheMode.Default; + return SqlContext.SqlSyntax.DefaultIsolationLevel; } } - /// - public IsolatedCaches IsolatedCaches + public IUmbracoDatabase DatabaseOrNull { get { - if (ParentScope != null) + EnsureNotDisposed(); + if (ParentScope == null) { - return ParentScope.IsolatedCaches; + if (_database != null) + { + EnsureDbLocks(); + } + + return _database; } - return _isolatedCaches ??= new IsolatedCaches(type => new DeepCloneAppCache(new ObjectCacheAppCache())); + return ParentScope.DatabaseOrNull; } } - // a value indicating whether the scope is detachable - // ie whether it was created by CreateDetachedScope - public bool Detachable { get; } + public EventMessages MessagesOrNull + { + get + { + EnsureNotDisposed(); + return ParentScope == null ? _messages : ParentScope.MessagesOrNull; + } + } - // the parent scope (in a nested scopes chain) - public Scope ParentScope { get; set; } + // true if Umbraco.CoreDebugSettings.LogUncompletedScope appSetting is set to "true" + private bool LogUncompletedScopes => _coreDebugSettings.LogIncompletedScopes; - public bool Attached { get; set; } + public Guid InstanceId { get; } = Guid.NewGuid(); - // the original scope (when attaching a detachable scope) - public Scope OrigScope { get; set; } + public int CreatedThreadId { get; } = Thread.CurrentThread.ManagedThreadId; - // the original context (when attaching a detachable scope) - public IScopeContext OrigContext { get; set; } + public ISqlContext SqlContext + { + get + { + if (_scopeProvider.SqlContext == null) + { + throw new InvalidOperationException( + $"The {nameof(_scopeProvider.SqlContext)} on the scope provider is null"); + } - // the context (for attaching & detaching only) - public IScopeContext Context { get; } + return _scopeProvider.SqlContext; + } + } - public IsolationLevel IsolationLevel + /// + public RepositoryCacheMode RepositoryCacheMode { get { - if (_isolationLevel != IsolationLevel.Unspecified) + if (_repositoryCacheMode != RepositoryCacheMode.Unspecified) { - return _isolationLevel; + return _repositoryCacheMode; } if (ParentScope != null) { - return ParentScope.IsolationLevel; + return ParentScope.RepositoryCacheMode; } - return SqlContext.SqlSyntax.DefaultIsolationLevel; + return RepositoryCacheMode.Default; + } + } + + /// + public IsolatedCaches IsolatedCaches + { + get + { + if (ParentScope != null) + { + return ParentScope.IsolatedCaches; + } + + return _isolatedCaches ??= new IsolatedCaches(type => new DeepCloneAppCache(new ObjectCacheAppCache())); } } @@ -403,7 +407,8 @@ public IUmbracoDatabase Database IsolationLevel currentLevel = database.GetCurrentTransactionIsolationLevel(); if (_isolationLevel > IsolationLevel.Unspecified && currentLevel < _isolationLevel) { - throw new Exception("Scope requires isolation level " + _isolationLevel + ", but got " + currentLevel + " from parent."); + throw new Exception("Scope requires isolation level " + _isolationLevel + ", but got " + + currentLevel + " from parent."); } return _database = database; @@ -428,24 +433,6 @@ public IUmbracoDatabase Database } } - public IUmbracoDatabase DatabaseOrNull - { - get - { - EnsureNotDisposed(); - if (ParentScope == null) - { - if (_database != null) - { - EnsureDbLocks(); - } - return _database; - } - - return ParentScope.DatabaseOrNull; - } - } - /// public EventMessages Messages { @@ -468,15 +455,6 @@ public EventMessages Messages } } - public EventMessages MessagesOrNull - { - get - { - EnsureNotDisposed(); - return ParentScope == null ? _messages : ParentScope.MessagesOrNull; - } - } - /// public IEventDispatcher Events { @@ -502,7 +480,8 @@ public IScopedNotificationPublisher Notifications return ParentScope.Notifications; } - return _notificationPublisher ?? (_notificationPublisher = new ScopedNotificationPublisher(_eventAggregator)); + return _notificationPublisher ?? + (_notificationPublisher = new ScopedNotificationPublisher(_eventAggregator)); } } @@ -517,6 +496,130 @@ public bool Complete() return _completed.Value; } + public void Dispose() + { + EnsureNotDisposed(); + + if (this != _scopeProvider.AmbientScope) + { + var failedMessage = + $"The {nameof(Scope)} {InstanceId} being disposed is not the Ambient {nameof(Scope)} {_scopeProvider.AmbientScope?.InstanceId.ToString() ?? "NULL"}. This typically indicates that a child {nameof(Scope)} was not disposed, or flowed to a child thread that was not awaited, or concurrent threads are accessing the same {nameof(Scope)} (Ambient context) which is not supported. If using Task.Run (or similar) as a fire and forget tasks or to run threads in parallel you must suppress execution context flow with ExecutionContext.SuppressFlow() and ExecutionContext.RestoreFlow()."; + +#if DEBUG_SCOPES + Scope ambient = _scopeProvider.AmbientScope; + _logger.LogWarning("Dispose error (" + (ambient == null ? "no" : "other") + " ambient)"); + if (ambient == null) + { + throw new InvalidOperationException("Not the ambient scope (no ambient scope)."); + } + + ScopeInfo ambientInfos = _scopeProvider.GetScopeInfo(ambient); + ScopeInfo disposeInfos = _scopeProvider.GetScopeInfo(this); + throw new InvalidOperationException($"{failedMessage} (see ctor stack traces).\r\n" + + "- ambient ->\r\n" + ambientInfos.ToString() + "\r\n" + + "- dispose ->\r\n" + disposeInfos.ToString() + "\r\n"); +#else + throw new InvalidOperationException(failedMessage); +#endif + } + + // Decrement the lock counters on the parent if any. + ClearLocks(InstanceId); + if (ParentScope is null) + { + // We're the parent scope, make sure that locks of all scopes has been cleared + // Since we're only reading we don't have to be in a lock + if (_readLocksDictionary?.Count > 0 || _writeLocksDictionary?.Count > 0) + { + var exception = new InvalidOperationException( + $"All scopes has not been disposed from parent scope: {InstanceId}, see log for more details."); + _logger.LogError(exception, GenerateUnclearedScopesLogMessage()); + throw exception; + } + } + + _scopeProvider.PopAmbientScope(this); // might be null = this is how scopes are removed from context objects + +#if DEBUG_SCOPES + _scopeProvider.Disposed(this); +#endif + + if (_autoComplete && _completed == null) + { + _completed = true; + } + + if (ParentScope != null) + { + ParentScope.ChildCompleted(_completed); + } + else + { + DisposeLastScope(); + } + + lock (_lockQueueLocker) + { + _queuedLocks?.Clear(); + } + + _disposed = true; + } + + public void EagerReadLock(params int[] lockIds) => EagerReadLockInner(Database, InstanceId, null, lockIds); + + /// + public void ReadLock(params int[] lockIds) => LazyReadLockInner(InstanceId, lockIds); + + public void EagerReadLock(TimeSpan timeout, int lockId) => + EagerReadLockInner(Database, InstanceId, timeout, lockId); + + /// + public void ReadLock(TimeSpan timeout, int lockId) => LazyReadLockInner(InstanceId, timeout, lockId); + + public void EagerWriteLock(params int[] lockIds) => EagerWriteLockInner(Database, InstanceId, null, lockIds); + + /// + public void WriteLock(params int[] lockIds) => LazyWriteLockInner(InstanceId, lockIds); + + public void EagerWriteLock(TimeSpan timeout, int lockId) => + EagerWriteLockInner(Database, InstanceId, timeout, lockId); + + /// + public void WriteLock(TimeSpan timeout, int lockId) => LazyWriteLockInner(InstanceId, timeout, lockId); + + /// + /// Used for testing. Ensures and gets any queued read locks. + /// + /// + internal Dictionary> GetReadLocks() + { + EnsureDbLocks(); + // always delegate to root/parent scope. + if (ParentScope is not null) + { + return ParentScope.GetReadLocks(); + } + + return _readLocksDictionary; + } + + /// + /// Used for testing. Ensures and gets and queued write locks. + /// + /// + internal Dictionary> GetWriteLocks() + { + EnsureDbLocks(); + // always delegate to root/parent scope. + if (ParentScope is not null) + { + return ParentScope.GetWriteLocks(); + } + + return _writeLocksDictionary; + } + public void Reset() => _completed = null; public void ChildCompleted(bool? completed) @@ -534,14 +637,14 @@ public void ChildCompleted(bool? completed) } /// - /// When we require a ReadLock or a WriteLock we don't immediately request these locks from the database, - /// instead we only request them when necessary (lazily). - /// To do this, we queue requests for read/write locks. - /// This is so that if there's a request for either of these - /// locks, but the service/repository returns an item from the cache, we don't end up making a DB call to make the - /// read/write lock. - /// This executes the queue of requested locks in order in an efficient way lazily whenever the database instance is - /// resolved. + /// When we require a ReadLock or a WriteLock we don't immediately request these locks from the database, + /// instead we only request them when necessary (lazily). + /// To do this, we queue requests for read/write locks. + /// This is so that if there's a request for either of these + /// locks, but the service/repository returns an item from the cache, we don't end up making a DB call to make the + /// read/write lock. + /// This executes the queue of requested locks in order in an efficient way lazily whenever the database instance is + /// resolved. /// private void EnsureDbLocks() { @@ -556,15 +659,15 @@ private void EnsureDbLocks() { if (_queuedLocks?.Count > 0) { - var currentType = LockType.ReadLock; - var currentTimeout = TimeSpan.Zero; - var currentInstanceId = InstanceId; + LockType currentType = LockType.ReadLock; + TimeSpan currentTimeout = TimeSpan.Zero; + Guid currentInstanceId = InstanceId; var collectedIds = new HashSet(); var i = 0; while (_queuedLocks.Count > 0) { - var (lockType, timeout, instanceId, lockId) = _queuedLocks.Dequeue(); + (LockType lockType, TimeSpan timeout, Guid instanceId, var lockId) = _queuedLocks.Dequeue(); if (i == 0) { @@ -572,25 +675,32 @@ private void EnsureDbLocks() currentTimeout = timeout; currentInstanceId = instanceId; } - else if (lockType != currentType || timeout != currentTimeout || instanceId != currentInstanceId) + else if (lockType != currentType || timeout != currentTimeout || + instanceId != currentInstanceId) { // the lock type, instanceId or timeout switched. // process the lock ids collected switch (currentType) { case LockType.ReadLock: - EagerReadLockInner(_database, currentInstanceId, currentTimeout == TimeSpan.Zero ? null : currentTimeout, collectedIds.ToArray()); + EagerReadLockInner(_database, currentInstanceId, + currentTimeout == TimeSpan.Zero ? null : currentTimeout, + collectedIds.ToArray()); break; case LockType.WriteLock: - EagerWriteLockInner(_database, currentInstanceId, currentTimeout == TimeSpan.Zero ? null : currentTimeout, collectedIds.ToArray()); + EagerWriteLockInner(_database, currentInstanceId, + currentTimeout == TimeSpan.Zero ? null : currentTimeout, + collectedIds.ToArray()); break; } + // clear the collected and set new type collectedIds.Clear(); currentType = lockType; currentTimeout = timeout; currentInstanceId = instanceId; } + collectedIds.Add(lockId); i++; } @@ -599,10 +709,12 @@ private void EnsureDbLocks() switch (currentType) { case LockType.ReadLock: - EagerReadLockInner(_database, currentInstanceId, currentTimeout == TimeSpan.Zero ? null : currentTimeout, collectedIds.ToArray()); + EagerReadLockInner(_database, currentInstanceId, + currentTimeout == TimeSpan.Zero ? null : currentTimeout, collectedIds.ToArray()); break; case LockType.WriteLock: - EagerWriteLockInner(_database, currentInstanceId, currentTimeout == TimeSpan.Zero ? null : currentTimeout, collectedIds.ToArray()); + EagerWriteLockInner(_database, currentInstanceId, + currentTimeout == TimeSpan.Zero ? null : currentTimeout, collectedIds.ToArray()); break; } } @@ -629,103 +741,38 @@ private void EnsureNotDisposed() // throw new ObjectDisposedException(GetType().FullName); } - public void Dispose() - { - EnsureNotDisposed(); - - if (this != _scopeProvider.AmbientScope) - { - var failedMessage = $"The {nameof(Scope)} {this.InstanceId} being disposed is not the Ambient {nameof(Scope)} {(_scopeProvider.AmbientScope?.InstanceId.ToString() ?? "NULL")}. This typically indicates that a child {nameof(Scope)} was not disposed, or flowed to a child thread that was not awaited, or concurrent threads are accessing the same {nameof(Scope)} (Ambient context) which is not supported. If using Task.Run (or similar) as a fire and forget tasks or to run threads in parallel you must suppress execution context flow with ExecutionContext.SuppressFlow() and ExecutionContext.RestoreFlow()."; - -#if DEBUG_SCOPES - Scope ambient = _scopeProvider.AmbientScope; - _logger.LogWarning("Dispose error (" + (ambient == null ? "no" : "other") + " ambient)"); - if (ambient == null) - { - throw new InvalidOperationException("Not the ambient scope (no ambient scope)."); - } - - ScopeInfo ambientInfos = _scopeProvider.GetScopeInfo(ambient); - ScopeInfo disposeInfos = _scopeProvider.GetScopeInfo(this); - throw new InvalidOperationException($"{failedMessage} (see ctor stack traces).\r\n" - + "- ambient ->\r\n" + ambientInfos.ToString() + "\r\n" - + "- dispose ->\r\n" + disposeInfos.ToString() + "\r\n"); -#else - throw new InvalidOperationException(failedMessage); -#endif - } - - // Decrement the lock counters on the parent if any. - ClearLocks(InstanceId); - if (ParentScope is null) - { - // We're the parent scope, make sure that locks of all scopes has been cleared - // Since we're only reading we don't have to be in a lock - if (_readLocksDictionary?.Count > 0 || _writeLocksDictionary?.Count > 0) - { - var exception = new InvalidOperationException($"All scopes has not been disposed from parent scope: {InstanceId}, see log for more details."); - _logger.LogError(exception, GenerateUnclearedScopesLogMessage()); - throw exception; - } - } - - _scopeProvider.PopAmbientScope(this); // might be null = this is how scopes are removed from context objects - -#if DEBUG_SCOPES - _scopeProvider.Disposed(this); -#endif - - if (_autoComplete && _completed == null) - { - _completed = true; - } - - if (ParentScope != null) - { - ParentScope.ChildCompleted(_completed); - } - else - { - DisposeLastScope(); - } - - lock (_lockQueueLocker) - { - _queuedLocks?.Clear(); - } - - _disposed = true; - } - /// - /// Generates a log message with all scopes that hasn't cleared their locks, including how many, and what locks they have requested. + /// Generates a log message with all scopes that hasn't cleared their locks, including how many, and what locks they + /// have requested. /// /// Log message. private string GenerateUnclearedScopesLogMessage() { // Dump the dicts into a message for the locks. - StringBuilder builder = new StringBuilder(); - builder.AppendLine($"Lock counters aren't empty, suggesting a scope hasn't been properly disposed, parent id: {InstanceId}"); + var builder = new StringBuilder(); + builder.AppendLine( + $"Lock counters aren't empty, suggesting a scope hasn't been properly disposed, parent id: {InstanceId}"); WriteLockDictionaryToString(_readLocksDictionary, builder, "read locks"); WriteLockDictionaryToString(_writeLocksDictionary, builder, "write locks"); return builder.ToString(); } /// - /// Writes a locks dictionary to a for logging purposes. + /// Writes a locks dictionary to a for logging purposes. /// /// Lock dictionary to report on. /// String builder to write to. /// The name to report the dictionary as. - private void WriteLockDictionaryToString(Dictionary> dict, StringBuilder builder, string dictName) + private void WriteLockDictionaryToString(Dictionary> dict, StringBuilder builder, + string dictName) { if (dict?.Count > 0) { builder.AppendLine($"Remaining {dictName}:"); - foreach (var instance in dict) + foreach (KeyValuePair> instance in dict) { builder.AppendLine($"Scope {instance.Key}"); - foreach (var lockCounter in instance.Value) + foreach (KeyValuePair lockCounter in instance.Value) { builder.AppendLine($"\tLock ID: {lockCounter.Key} - times requested: {lockCounter.Value}"); } @@ -840,6 +887,7 @@ private void RobustExit(bool completed, bool onException) { _scopeProvider.PopAmbientScope(_scopeProvider.AmbientScope); } + if (OrigContext != _scopeProvider.AmbientContext) { _scopeProvider.PopAmbientScopeContext(); @@ -871,12 +919,9 @@ private static void TryFinally(int index, Action[] actions) } } - // true if Umbraco.CoreDebugSettings.LogUncompletedScope appSetting is set to "true" - private bool LogUncompletedScopes => _coreDebugSettings.LogIncompletedScopes; - /// - /// Increment the counter of a locks dictionary, either ReadLocks or WriteLocks, - /// for a specific scope instance and lock identifier. Must be called within a lock. + /// Increment the counter of a locks dictionary, either ReadLocks or WriteLocks, + /// for a specific scope instance and lock identifier. Must be called within a lock. /// /// Lock ID to increment. /// Instance ID of the scope requesting the lock. @@ -888,7 +933,7 @@ private void IncrementLock(int lockId, Guid instanceId, ref Dictionary>(); // Try and get the dict associated with the scope id. - var locksDictFound = locks.TryGetValue(instanceId, out var locksDict); + var locksDictFound = locks.TryGetValue(instanceId, out Dictionary locksDict); if (locksDictFound) { locksDict.TryGetValue(lockId, out var value); @@ -903,7 +948,7 @@ private void IncrementLock(int lockId, Guid instanceId, ref Dictionary - /// Clears all lock counters for a given scope instance, signalling that the scope has been disposed. + /// Clears all lock counters for a given scope instance, signalling that the scope has been disposed. /// /// Instance ID of the scope to clear. private void ClearLocks(Guid instanceId) @@ -924,7 +969,8 @@ private void ClearLocks(Guid instanceId) { // It's safe to assume that the locks on the top of the stack belong to this instance, // since any child scopes that might have added locks to the stack must be disposed before we try and dispose this instance. - var top = _queuedLocks.PeekStack(); + (LockType lockType, TimeSpan timeout, Guid instanceId, int lockId) top = + _queuedLocks.PeekStack(); if (top.instanceId == instanceId) { _queuedLocks.Pop(); @@ -938,26 +984,6 @@ private void ClearLocks(Guid instanceId) } } - public void EagerReadLock(params int[] lockIds) => EagerReadLockInner(Database, InstanceId, null, lockIds); - - /// - public void ReadLock(params int[] lockIds) => LazyReadLockInner(InstanceId, lockIds); - - public void EagerReadLock(TimeSpan timeout, int lockId) => EagerReadLockInner(Database, InstanceId, timeout, lockId); - - /// - public void ReadLock(TimeSpan timeout, int lockId) => LazyReadLockInner(InstanceId, timeout, lockId); - - public void EagerWriteLock(params int[] lockIds) => EagerWriteLockInner(Database, InstanceId, null, lockIds); - - /// - public void WriteLock(params int[] lockIds) => LazyWriteLockInner(InstanceId, lockIds); - - public void EagerWriteLock(TimeSpan timeout, int lockId) => EagerWriteLockInner(Database, InstanceId, timeout, lockId); - - /// - public void WriteLock(TimeSpan timeout, int lockId) => LazyWriteLockInner(InstanceId, timeout, lockId); - public void LazyReadLockInner(Guid instanceId, params int[] lockIds) { if (ParentScope != null) @@ -1014,6 +1040,7 @@ private void LazyLockInner(LockType lockType, Guid instanceId, params int[] lock { _queuedLocks = new StackQueue<(LockType, TimeSpan, Guid, int)>(); } + foreach (var lockId in lockIds) { _queuedLocks.Enqueue((lockType, TimeSpan.Zero, instanceId, lockId)); @@ -1029,12 +1056,13 @@ private void LazyLockInner(LockType lockType, Guid instanceId, TimeSpan timeout, { _queuedLocks = new StackQueue<(LockType, TimeSpan, Guid, int)>(); } + _queuedLocks.Enqueue((lockType, timeout, instanceId, lockId)); } } /// - /// Handles acquiring a read lock, will delegate it to the parent if there are any. + /// Handles acquiring a read lock, will delegate it to the parent if there are any. /// /// Instance ID of the requesting scope. /// Optional database timeout in milliseconds. @@ -1049,12 +1077,13 @@ private void EagerReadLockInner(IUmbracoDatabase db, Guid instanceId, TimeSpan? else { // We are the outermost scope, handle the lock request. - LockInner(db, instanceId, ref _readLocksDictionary, ref _readLocks, ObtainReadLock, ObtainTimeoutReadLock, timeout, lockIds); + LockInner(db, instanceId, ref _readLocksDictionary, ref _readLocks, ObtainReadLock, + ObtainTimeoutReadLock, timeout, lockIds); } } /// - /// Handles acquiring a write lock with a specified timeout, will delegate it to the parent if there are any. + /// Handles acquiring a write lock with a specified timeout, will delegate it to the parent if there are any. /// /// Instance ID of the requesting scope. /// Optional database timeout in milliseconds. @@ -1069,12 +1098,13 @@ private void EagerWriteLockInner(IUmbracoDatabase db, Guid instanceId, TimeSpan? else { // We are the outermost scope, handle the lock request. - LockInner(db, instanceId, ref _writeLocksDictionary, ref _writeLocks, ObtainWriteLock, ObtainTimeoutWriteLock, timeout, lockIds); + LockInner(db, instanceId, ref _writeLocksDictionary, ref _writeLocks, ObtainWriteLock, + ObtainTimeoutWriteLock, timeout, lockIds); } } /// - /// Handles acquiring a lock, this should only be called from the outermost scope. + /// Handles acquiring a lock, this should only be called from the outermost scope. /// /// Instance ID of the scope requesting the lock. /// Reference to the applicable locks dictionary (ReadLocks or WriteLocks). @@ -1083,8 +1113,10 @@ private void EagerWriteLockInner(IUmbracoDatabase db, Guid instanceId, TimeSpan? /// Delegate used to request the lock from the database with a timeout. /// Optional timeout parameter to specify a timeout. /// Lock identifiers to lock on. - private void LockInner(IUmbracoDatabase db, Guid instanceId, ref Dictionary> locks, ref HashSet locksSet, - Action obtainLock, Action obtainLockTimeout, TimeSpan? timeout, + private void LockInner(IUmbracoDatabase db, Guid instanceId, ref Dictionary> locks, + ref HashSet locksSet, + Action obtainLock, Action obtainLockTimeout, + TimeSpan? timeout, params int[] lockIds) { lock (_dictionaryLocker) @@ -1130,41 +1162,37 @@ private void LockInner(IUmbracoDatabase db, Guid instanceId, ref Dictionary - /// Obtains an ordinary read lock. + /// Obtains an ordinary read lock. /// /// Lock object identifier to lock. - private void ObtainReadLock(IUmbracoDatabase db, int lockId) - { - SqlContext.SqlSyntax.ReadLock(db, lockId); - } + private void ObtainReadLock(IUmbracoDatabase db, int lockId) => SqlContext.SqlSyntax.ReadLock(db, lockId); /// - /// Obtains a read lock with a custom timeout. + /// Obtains a read lock with a custom timeout. /// /// Lock object identifier to lock. /// TimeSpan specifying the timout period. - private void ObtainTimeoutReadLock(IUmbracoDatabase db, int lockId, TimeSpan timeout) - { + private void ObtainTimeoutReadLock(IUmbracoDatabase db, int lockId, TimeSpan timeout) => SqlContext.SqlSyntax.ReadLock(db, timeout, lockId); - } /// - /// Obtains an ordinary write lock. + /// Obtains an ordinary write lock. /// /// Lock object identifier to lock. - private void ObtainWriteLock(IUmbracoDatabase db, int lockId) - { - SqlContext.SqlSyntax.WriteLock(db, lockId); - } + private void ObtainWriteLock(IUmbracoDatabase db, int lockId) => SqlContext.SqlSyntax.WriteLock(db, lockId); /// - /// Obtains a write lock with a custom timeout. + /// Obtains a write lock with a custom timeout. /// /// Lock object identifier to lock. /// TimeSpan specifying the timout period. - private void ObtainTimeoutWriteLock(IUmbracoDatabase db, int lockId, TimeSpan timeout) - { + private void ObtainTimeoutWriteLock(IUmbracoDatabase db, int lockId, TimeSpan timeout) => SqlContext.SqlSyntax.WriteLock(db, timeout, lockId); + + private enum LockType + { + ReadLock, + WriteLock } } } diff --git a/src/Umbraco.Infrastructure/Search/UmbracoTreeSearcherFields.cs b/src/Umbraco.Infrastructure/Search/UmbracoTreeSearcherFields.cs index 26b638a436d7..f662000cd0b4 100644 --- a/src/Umbraco.Infrastructure/Search/UmbracoTreeSearcherFields.cs +++ b/src/Umbraco.Infrastructure/Search/UmbracoTreeSearcherFields.cs @@ -23,28 +23,28 @@ public UmbracoTreeSearcherFields(ILocalizationService localizationService) } /// - public IEnumerable GetBackOfficeFields() => _backOfficeFields; + public virtual IEnumerable GetBackOfficeFields() => _backOfficeFields; /// - public IEnumerable GetBackOfficeMembersFields() => _backOfficeMembersFields; + public virtual IEnumerable GetBackOfficeMembersFields() => _backOfficeMembersFields; /// - public IEnumerable GetBackOfficeMediaFields() => _backOfficeMediaFields; + public virtual IEnumerable GetBackOfficeMediaFields() => _backOfficeMediaFields; /// - public IEnumerable GetBackOfficeDocumentFields() => Enumerable.Empty(); + public virtual IEnumerable GetBackOfficeDocumentFields() => Enumerable.Empty(); /// - public ISet GetBackOfficeFieldsToLoad() => _backOfficeFieldsToLoad; + public virtual ISet GetBackOfficeFieldsToLoad() => _backOfficeFieldsToLoad; /// - public ISet GetBackOfficeMembersFieldsToLoad() => _backOfficeMembersFieldsToLoad; + public virtual ISet GetBackOfficeMembersFieldsToLoad() => _backOfficeMembersFieldsToLoad; /// - public ISet GetBackOfficeMediaFieldsToLoad() => _backOfficeMediaFieldsToLoad; + public virtual ISet GetBackOfficeMediaFieldsToLoad() => _backOfficeMediaFieldsToLoad; /// - public ISet GetBackOfficeDocumentFieldsToLoad() + public virtual ISet GetBackOfficeDocumentFieldsToLoad() { var fields = _backOfficeDocumentFieldsToLoad; diff --git a/src/Umbraco.Infrastructure/Services/Implement/ContentService.cs b/src/Umbraco.Infrastructure/Services/Implement/ContentService.cs index 21c365dd8e5f..04b93bdd9f51 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/ContentService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/ContentService.cs @@ -1,11 +1,11 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Exceptions; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Persistence.Querying; @@ -19,27 +19,29 @@ namespace Umbraco.Cms.Core.Services.Implement { /// - /// Implements the content service. + /// Implements the content service. /// public class ContentService : RepositoryService, IContentService { - private readonly IDocumentRepository _documentRepository; - private readonly IEntityRepository _entityRepository; private readonly IAuditRepository _auditRepository; private readonly IContentTypeRepository _contentTypeRepository; private readonly IDocumentBlueprintRepository _documentBlueprintRepository; + private readonly IDocumentRepository _documentRepository; + private readonly IEntityRepository _entityRepository; private readonly ILanguageRepository _languageRepository; + private readonly ILogger _logger; private readonly Lazy _propertyValidationService; private readonly IShortStringHelper _shortStringHelper; - private readonly ILogger _logger; private IQuery _queryNotTrashed; #region Constructors public ContentService(IScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, - IDocumentRepository documentRepository, IEntityRepository entityRepository, IAuditRepository auditRepository, - IContentTypeRepository contentTypeRepository, IDocumentBlueprintRepository documentBlueprintRepository, ILanguageRepository languageRepository, + IDocumentRepository documentRepository, IEntityRepository entityRepository, + IAuditRepository auditRepository, + IContentTypeRepository contentTypeRepository, IDocumentBlueprintRepository documentBlueprintRepository, + ILanguageRepository languageRepository, Lazy propertyValidationService, IShortStringHelper shortStringHelper) : base(provider, loggerFactory, eventMessagesFactory) { @@ -60,7 +62,73 @@ public ContentService(IScopeProvider provider, ILoggerFactory loggerFactory, // lazy-constructed because when the ctor runs, the query factory may not be ready - private IQuery QueryNotTrashed => _queryNotTrashed ?? (_queryNotTrashed = Query().Where(x => x.Trashed == false)); + private IQuery QueryNotTrashed => + _queryNotTrashed ?? (_queryNotTrashed = Query().Where(x => x.Trashed == false)); + + #endregion + + #region Rollback + + public OperationResult Rollback(int id, int versionId, string culture = "*", + int userId = Constants.Security.SuperUserId) + { + EventMessages evtMsgs = EventMessagesFactory.Get(); + + // Get the current copy of the node + IContent content = GetById(id); + + // Get the version + IContent version = GetVersion(versionId); + + // Good old null checks + if (content == null || version == null || content.Trashed) + { + return new OperationResult(OperationResultType.FailedCannot, evtMsgs); + } + + // Store the result of doing the save of content for the rollback + OperationResult rollbackSaveResult; + + using (IScope scope = ScopeProvider.CreateScope()) + { + var rollingBackNotification = new ContentRollingBackNotification(content, evtMsgs); + if (scope.Notifications.PublishCancelable(rollingBackNotification)) + { + scope.Complete(); + return OperationResult.Cancel(evtMsgs); + } + + // Copy the changes from the version + content.CopyFrom(version, culture); + + // Save the content for the rollback + rollbackSaveResult = Save(content, userId); + + // Depending on the save result - is what we log & audit along with what we return + if (rollbackSaveResult.Success == false) + { + // Log the error/warning + _logger.LogError( + "User '{UserId}' was unable to rollback content '{ContentId}' to version '{VersionId}'", userId, + id, versionId); + } + else + { + scope.Notifications.Publish( + new ContentRolledBackNotification(content, evtMsgs).WithStateFrom(rollingBackNotification)); + + // Logging & Audit message + _logger.LogInformation("User '{UserId}' rolled back content '{ContentId}' to version '{VersionId}'", + userId, id, versionId); + Audit(AuditType.RollBack, userId, id, + $"Content '{content.Name}' was rolled back to version '{versionId}'"); + } + + scope.Complete(); + } + + return rollbackSaveResult; + } #endregion @@ -68,36 +136,36 @@ public ContentService(IScopeProvider provider, ILoggerFactory loggerFactory, public int CountPublished(string contentTypeAlias = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { - scope.ReadLock(Cms.Core.Constants.Locks.ContentTree); + scope.ReadLock(Constants.Locks.ContentTree); return _documentRepository.CountPublished(contentTypeAlias); } } public int Count(string contentTypeAlias = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { - scope.ReadLock(Cms.Core.Constants.Locks.ContentTree); + scope.ReadLock(Constants.Locks.ContentTree); return _documentRepository.Count(contentTypeAlias); } } public int CountChildren(int parentId, string contentTypeAlias = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { - scope.ReadLock(Cms.Core.Constants.Locks.ContentTree); + scope.ReadLock(Constants.Locks.ContentTree); return _documentRepository.CountChildren(parentId, contentTypeAlias); } } public int CountDescendants(int parentId, string contentTypeAlias = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { - scope.ReadLock(Cms.Core.Constants.Locks.ContentTree); + scope.ReadLock(Constants.Locks.ContentTree); return _documentRepository.CountDescendants(parentId, contentTypeAlias); } } @@ -107,46 +175,46 @@ public int CountDescendants(int parentId, string contentTypeAlias = null) #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. + /// Used to bulk update the permissions set for a content item. This will replace all permissions + /// assigned to an entity with a list of user id & permission pairs. /// /// public void SetPermissions(EntityPermissionSet permissionSet) { - using (var scope = ScopeProvider.CreateScope()) + using (IScope scope = ScopeProvider.CreateScope()) { - scope.WriteLock(Cms.Core.Constants.Locks.ContentTree); + scope.WriteLock(Constants.Locks.ContentTree); _documentRepository.ReplaceContentPermissions(permissionSet); scope.Complete(); } } /// - /// Assigns a single permission to the current content item for the specified group ids + /// Assigns a single permission to the current content item for the specified group ids /// /// /// /// public void SetPermission(IContent entity, char permission, IEnumerable groupIds) { - using (var scope = ScopeProvider.CreateScope()) + using (IScope scope = ScopeProvider.CreateScope()) { - scope.WriteLock(Cms.Core.Constants.Locks.ContentTree); + scope.WriteLock(Constants.Locks.ContentTree); _documentRepository.AssignEntityPermission(entity, permission, groupIds); scope.Complete(); } } /// - /// Returns implicit/inherited permissions assigned to the content item for all user groups + /// Returns implicit/inherited permissions assigned to the content item for all user groups /// /// /// public EntityPermissionCollection GetPermissions(IContent content) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { - scope.ReadLock(Cms.Core.Constants.Locks.ContentTree); + scope.ReadLock(Constants.Locks.ContentTree); return _documentRepository.GetPermissionsForEntity(content.Id); } } @@ -156,53 +224,59 @@ public EntityPermissionCollection GetPermissions(IContent content) #region Create /// - /// Creates an object using the alias of the - /// that this Content should based on. + /// Creates an object using the alias of the + /// that this Content should based on. /// /// - /// Note that using this method will simply return a new IContent without any identity - /// as it has not yet been persisted. It is intended as a shortcut to creating new content objects - /// that does not invoke a save operation against the database. + /// Note that using this method will simply return a new IContent without any identity + /// as it has not yet been persisted. It is intended as a shortcut to creating new content objects + /// that does not invoke a save operation against the database. /// /// Name of the Content object /// Id of Parent for the new Content - /// Alias of the + /// Alias of the /// Optional id of the user creating the content - /// - public IContent Create(string name, Guid parentId, string contentTypeAlias, int userId = Cms.Core.Constants.Security.SuperUserId) + /// + /// + /// + public IContent Create(string name, Guid parentId, string contentTypeAlias, + int userId = Constants.Security.SuperUserId) { // TODO: what about culture? - var parent = GetById(parentId); + IContent parent = GetById(parentId); return Create(name, parent, contentTypeAlias, userId); } /// - /// Creates an object of a specified content type. + /// Creates an object of a specified content type. /// - /// This method simply returns a new, non-persisted, IContent without any identity. It - /// is intended as a shortcut to creating new content objects that does not invoke a save - /// operation against the database. + /// + /// This method simply returns a new, non-persisted, IContent without any identity. It + /// is intended as a shortcut to creating new content objects that does not invoke a save + /// operation against the database. /// /// The name of the content object. /// The identifier of the parent, or -1. /// The alias of the content type. /// The optional id of the user creating the content. /// The content object. - public IContent Create(string name, int parentId, string contentTypeAlias, int userId = Cms.Core.Constants.Security.SuperUserId) + public IContent Create(string name, int parentId, string contentTypeAlias, + int userId = Constants.Security.SuperUserId) { // TODO: what about culture? - var contentType = GetContentType(contentTypeAlias); + IContentType contentType = GetContentType(contentTypeAlias); return Create(name, parentId, contentType, userId); } /// - /// Creates an object of a specified content type. + /// Creates an object of a specified content type. /// - /// This method simply returns a new, non-persisted, IContent without any identity. It - /// is intended as a shortcut to creating new content objects that does not invoke a save - /// operation against the database. + /// + /// This method simply returns a new, non-persisted, IContent without any identity. It + /// is intended as a shortcut to creating new content objects that does not invoke a save + /// operation against the database. /// /// The name of the content object. /// The identifier of the parent, or -1. @@ -217,7 +291,7 @@ public IContent Create(string name, int parentId, IContentType contentType, throw new ArgumentException("Content type must be specified", nameof(contentType)); } - var parent = parentId > 0 ? GetById(parentId) : null; + IContent parent = parentId > 0 ? GetById(parentId) : null; if (parentId > 0 && parent is null) { throw new ArgumentException("No content with that id.", nameof(parentId)); @@ -229,26 +303,34 @@ public IContent Create(string name, int parentId, IContentType contentType, } /// - /// Creates an object of a specified content type, under a parent. + /// 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. + /// + /// This method simply returns a new, non-persisted, IContent without any identity. It + /// is intended as a shortcut to creating new content objects that does not invoke a save + /// operation against the database. /// /// The name of the content object. /// The parent content object. /// The alias of the content type. /// The optional id of the user creating the content. /// The content object. - public IContent Create(string name, IContent parent, string contentTypeAlias, int userId = Cms.Core.Constants.Security.SuperUserId) + public IContent Create(string name, IContent parent, string contentTypeAlias, + int userId = Constants.Security.SuperUserId) { // TODO: what about culture? - if (parent == null) throw new ArgumentNullException(nameof(parent)); + if (parent == null) + { + throw new ArgumentNullException(nameof(parent)); + } - var contentType = GetContentType(contentTypeAlias); + IContentType contentType = GetContentType(contentTypeAlias); if (contentType == null) - throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias)); // causes rollback + { + throw new ArgumentException("No content type with that alias.", + nameof(contentTypeAlias)); // causes rollback + } var content = new Content(name, parent, contentType, userId); @@ -256,7 +338,7 @@ public IContent Create(string name, IContent parent, string contentTypeAlias, in } /// - /// Creates an object of a specified content type. + /// Creates an object of a specified content type. /// /// This method returns a new, persisted, IContent with an identity. /// The name of the content object. @@ -264,24 +346,32 @@ public IContent Create(string name, IContent parent, string contentTypeAlias, in /// The alias of the content type. /// The optional id of the user creating the content. /// The content object. - public IContent CreateAndSave(string name, int parentId, string contentTypeAlias, int userId = Cms.Core.Constants.Security.SuperUserId) + public IContent CreateAndSave(string name, int parentId, string contentTypeAlias, + int userId = Constants.Security.SuperUserId) { // TODO: what about culture? - using (var scope = ScopeProvider.CreateScope(autoComplete:true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { // locking the content tree secures content types too - scope.WriteLock(Cms.Core.Constants.Locks.ContentTree); + scope.WriteLock(Constants.Locks.ContentTree); - var contentType = GetContentType(contentTypeAlias); // + locks + IContentType contentType = GetContentType(contentTypeAlias); // + locks if (contentType == null) - throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias)); // causes rollback + { + throw new ArgumentException("No content type with that alias.", + nameof(contentTypeAlias)); // causes rollback + } - var parent = parentId > 0 ? GetById(parentId) : null; // + locks + IContent 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, userId) : new Content(name, parentId, contentType, userId); + Content content = parentId > 0 + ? new Content(name, parent, contentType, userId) + : new Content(name, parentId, contentType, userId); Save(content, userId); @@ -290,7 +380,7 @@ public IContent CreateAndSave(string name, int parentId, string contentTypeAlias } /// - /// Creates an object of a specified content type, under a parent. + /// Creates an object of a specified content type, under a parent. /// /// This method returns a new, persisted, IContent with an identity. /// The name of the content object. @@ -298,20 +388,27 @@ public IContent CreateAndSave(string name, int parentId, string contentTypeAlias /// The alias of the content type. /// The optional id of the user creating the content. /// The content object. - public IContent CreateAndSave(string name, IContent parent, string contentTypeAlias, int userId = Cms.Core.Constants.Security.SuperUserId) + public IContent CreateAndSave(string name, IContent parent, string contentTypeAlias, + int userId = Constants.Security.SuperUserId) { // TODO: what about culture? - if (parent == null) throw new ArgumentNullException(nameof(parent)); + if (parent == null) + { + throw new ArgumentNullException(nameof(parent)); + } - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { // locking the content tree secures content types too - scope.WriteLock(Cms.Core.Constants.Locks.ContentTree); + scope.WriteLock(Constants.Locks.ContentTree); - var contentType = GetContentType(contentTypeAlias); // + locks + IContentType contentType = GetContentType(contentTypeAlias); // + locks if (contentType == null) - throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias)); // causes rollback + { + throw new ArgumentException("No content type with that alias.", + nameof(contentTypeAlias)); // causes rollback + } var content = new Content(name, parent, contentType, userId); @@ -326,96 +423,120 @@ public IContent CreateAndSave(string name, IContent parent, string contentTypeAl #region Get, Has, Is /// - /// Gets an object by Id + /// Gets an object by Id /// /// Id of the Content to retrieve - /// + /// + /// + /// public IContent GetById(int id) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { - scope.ReadLock(Cms.Core.Constants.Locks.ContentTree); + scope.ReadLock(Constants.Locks.ContentTree); return _documentRepository.Get(id); } } /// - /// Gets an object by Id + /// Gets an object by Id /// /// Ids of the Content to retrieve - /// + /// + /// + /// public IEnumerable GetByIds(IEnumerable ids) { var idsA = ids.ToArray(); - if (idsA.Length == 0) return Enumerable.Empty(); + if (idsA.Length == 0) + { + return Enumerable.Empty(); + } - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { - scope.ReadLock(Cms.Core.Constants.Locks.ContentTree); - var items = _documentRepository.GetMany(idsA); + scope.ReadLock(Constants.Locks.ContentTree); + IEnumerable items = _documentRepository.GetMany(idsA); var index = items.ToDictionary(x => x.Id, x => x); - return idsA.Select(x => index.TryGetValue(x, out var c) ? c : null).WhereNotNull(); + return idsA.Select(x => index.TryGetValue(x, out IContent c) ? c : null).WhereNotNull(); } } /// - /// Gets an object by its 'UniqueId' + /// Gets an object by its 'UniqueId' /// /// Guid key of the Content to retrieve - /// + /// + /// + /// public IContent GetById(Guid key) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { - scope.ReadLock(Cms.Core.Constants.Locks.ContentTree); + scope.ReadLock(Constants.Locks.ContentTree); return _documentRepository.Get(key); } } /// - /// /// /// /// /// - Attempt IContentServiceBase.Save(IEnumerable contents, int userId) => Attempt.Succeed(Save(contents, userId)); + Attempt IContentServiceBase.Save(IEnumerable contents, int userId) => + Attempt.Succeed(Save(contents, userId)); /// - /// Gets objects by Ids + /// Gets objects by Ids /// /// Ids of the Content to retrieve - /// + /// + /// + /// public IEnumerable GetByIds(IEnumerable ids) { - var idsA = ids.ToArray(); - if (idsA.Length == 0) return Enumerable.Empty(); + Guid[] idsA = ids.ToArray(); + if (idsA.Length == 0) + { + return Enumerable.Empty(); + } - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { - scope.ReadLock(Cms.Core.Constants.Locks.ContentTree); - var items = _documentRepository.GetMany(idsA); + scope.ReadLock(Constants.Locks.ContentTree); + IEnumerable items = _documentRepository.GetMany(idsA); var index = items.ToDictionary(x => x.Key, x => x); - return idsA.Select(x => index.TryGetValue(x, out var c) ? c : null).WhereNotNull(); + return idsA.Select(x => index.TryGetValue(x, out IContent c) ? c : null).WhereNotNull(); } } /// - public IEnumerable GetPagedOfType(int contentTypeId, long pageIndex, int pageSize, out long totalRecords + public IEnumerable GetPagedOfType(int contentTypeId, long pageIndex, int pageSize, + out long totalRecords , IQuery filter = null, Ordering ordering = null) { - if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex)); - if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize)); + if (pageIndex < 0) + { + throw new ArgumentOutOfRangeException(nameof(pageIndex)); + } + + if (pageSize <= 0) + { + throw new ArgumentOutOfRangeException(nameof(pageSize)); + } if (ordering == null) + { ordering = Ordering.By("sortOrder"); + } - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { - scope.ReadLock(Cms.Core.Constants.Locks.ContentTree); + scope.ReadLock(Constants.Locks.ContentTree); return _documentRepository.GetPage( Query().Where(x => x.ContentTypeId == contentTypeId), pageIndex, pageSize, out totalRecords, filter, ordering); @@ -423,17 +544,27 @@ public IEnumerable GetPagedOfType(int contentTypeId, long pageIndex, i } /// - public IEnumerable GetPagedOfTypes(int[] contentTypeIds, long pageIndex, int pageSize, out long totalRecords, IQuery filter, Ordering ordering = null) + public IEnumerable GetPagedOfTypes(int[] contentTypeIds, long pageIndex, int pageSize, + out long totalRecords, IQuery filter, Ordering ordering = null) { - if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex)); - if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize)); + if (pageIndex < 0) + { + throw new ArgumentOutOfRangeException(nameof(pageIndex)); + } + + if (pageSize <= 0) + { + throw new ArgumentOutOfRangeException(nameof(pageSize)); + } if (ordering == null) + { ordering = Ordering.By("sortOrder"); + } - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { - scope.ReadLock(Cms.Core.Constants.Locks.ContentTree); + scope.ReadLock(Constants.Locks.ContentTree); return _documentRepository.GetPage( Query().Where(x => contentTypeIds.Contains(x.ContentTypeId)), pageIndex, pageSize, out totalRecords, filter, ordering); @@ -441,64 +572,64 @@ public IEnumerable GetPagedOfTypes(int[] contentTypeIds, long pageInde } /// - /// Gets a collection of objects by Level + /// Gets a collection of objects by Level /// /// The level to retrieve Content from - /// An Enumerable list of objects + /// An Enumerable list of objects /// Contrary to most methods, this method filters out trashed content items. public IEnumerable GetByLevel(int level) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { - scope.ReadLock(Cms.Core.Constants.Locks.ContentTree); - var query = Query().Where(x => x.Level == level && x.Trashed == false); + scope.ReadLock(Constants.Locks.ContentTree); + IQuery query = Query().Where(x => x.Level == level && x.Trashed == false); return _documentRepository.Get(query); } } /// - /// Gets a specific version of an item. + /// Gets a specific version of an item. /// /// Id of the version to retrieve - /// An item + /// An item public IContent GetVersion(int versionId) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { - scope.ReadLock(Cms.Core.Constants.Locks.ContentTree); + scope.ReadLock(Constants.Locks.ContentTree); return _documentRepository.GetVersion(versionId); } } /// - /// Gets a collection of an objects versions by Id + /// Gets a collection of an objects versions by Id /// /// - /// An Enumerable list of objects + /// An Enumerable list of objects public IEnumerable GetVersions(int id) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { - scope.ReadLock(Cms.Core.Constants.Locks.ContentTree); + scope.ReadLock(Constants.Locks.ContentTree); return _documentRepository.GetAllVersions(id); } } /// - /// Gets a collection of an objects versions by Id + /// Gets a collection of an objects versions by Id /// - /// An Enumerable list of objects + /// An Enumerable list of objects public IEnumerable GetVersionsSlim(int id, int skip, int take) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { - scope.ReadLock(Cms.Core.Constants.Locks.ContentTree); + scope.ReadLock(Constants.Locks.ContentTree); return _documentRepository.GetAllVersionsSlim(id, skip, take); } } /// - /// Gets a list of all version Ids for the given content item ordered so latest is first + /// Gets a list of all version Ids for the given content item ordered so latest is first /// /// /// The maximum number of rows to return @@ -512,22 +643,22 @@ public IEnumerable GetVersionIds(int id, int maxRows) } /// - /// Gets a collection of objects, which are ancestors of the current content. + /// Gets a collection of objects, which are ancestors of the current content. /// - /// Id of the to retrieve ancestors for - /// An Enumerable list of objects + /// Id of the to retrieve ancestors for + /// An Enumerable list of objects public IEnumerable GetAncestors(int id) { // intentionally not locking - var content = GetById(id); + IContent content = GetById(id); return GetAncestors(content); } /// - /// Gets a collection of objects, which are ancestors of the current content. + /// Gets a collection of objects, which are ancestors of the current content. /// - /// to retrieve ancestors for - /// An Enumerable list of objects + /// to retrieve ancestors for + /// An Enumerable list of objects public IEnumerable GetAncestors(IContent content) { //null check otherwise we get exceptions @@ -550,16 +681,16 @@ public IEnumerable GetAncestors(IContent content) } /// - /// Gets a collection of published objects by Parent Id + /// Gets a collection of published objects by Parent Id /// /// Id of the Parent to retrieve Children from - /// An Enumerable list of published objects + /// An Enumerable list of published objects public IEnumerable GetPublishedChildren(int id) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { - scope.ReadLock(Cms.Core.Constants.Locks.ContentTree); - var query = Query().Where(x => x.ParentId == id && x.Published); + scope.ReadLock(Constants.Locks.ContentTree); + IQuery query = Query().Where(x => x.ParentId == id && x.Published); return _documentRepository.Get(query).OrderBy(x => x.SortOrder); } } @@ -568,17 +699,26 @@ public IEnumerable GetPublishedChildren(int id) public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, IQuery filter = null, Ordering ordering = null) { - if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex)); - if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize)); + if (pageIndex < 0) + { + throw new ArgumentOutOfRangeException(nameof(pageIndex)); + } + + if (pageSize <= 0) + { + throw new ArgumentOutOfRangeException(nameof(pageSize)); + } if (ordering == null) + { ordering = Ordering.By("sortOrder"); + } - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { - scope.ReadLock(Cms.Core.Constants.Locks.ContentTree); + scope.ReadLock(Constants.Locks.ContentTree); - var query = Query().Where(x => x.ParentId == id); + IQuery query = Query().Where(x => x.ParentId == id); return _documentRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering); } } @@ -588,93 +728,116 @@ public IEnumerable GetPagedDescendants(int id, long pageIndex, int pag IQuery filter = null, Ordering ordering = null) { if (ordering == null) + { ordering = Ordering.By("Path"); + } - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { - scope.ReadLock(Cms.Core.Constants.Locks.ContentTree); + scope.ReadLock(Constants.Locks.ContentTree); //if the id is System Root, then just get all - if (id != Cms.Core.Constants.System.Root) + if (id != Constants.System.Root) { - var contentPath = _entityRepository.GetAllPaths(Cms.Core.Constants.ObjectTypes.Document, id).ToArray(); + TreeEntityPath[] contentPath = + _entityRepository.GetAllPaths(Constants.ObjectTypes.Document, id).ToArray(); if (contentPath.Length == 0) { totalChildren = 0; return Enumerable.Empty(); } - return GetPagedLocked(GetPagedDescendantQuery(contentPath[0].Path), pageIndex, pageSize, out totalChildren, filter, ordering); + + return GetPagedLocked(GetPagedDescendantQuery(contentPath[0].Path), pageIndex, pageSize, + out totalChildren, filter, ordering); } + return GetPagedLocked(null, pageIndex, pageSize, out totalChildren, filter, ordering); } } private IQuery GetPagedDescendantQuery(string contentPath) { - var query = Query(); + IQuery query = Query(); if (!contentPath.IsNullOrWhiteSpace()) + { query.Where(x => x.Path.SqlStartsWith($"{contentPath},", TextColumnType.NVarchar)); + } + return query; } - private IEnumerable GetPagedLocked(IQuery query, long pageIndex, int pageSize, out long totalChildren, + private IEnumerable GetPagedLocked(IQuery query, long pageIndex, int pageSize, + out long totalChildren, IQuery filter, Ordering ordering) { - if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex)); - if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize)); - if (ordering == null) throw new ArgumentNullException(nameof(ordering)); + if (pageIndex < 0) + { + throw new ArgumentOutOfRangeException(nameof(pageIndex)); + } + + if (pageSize <= 0) + { + throw new ArgumentOutOfRangeException(nameof(pageSize)); + } + + if (ordering == null) + { + throw new ArgumentNullException(nameof(ordering)); + } return _documentRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering); } /// - /// Gets the parent of the current content as an item. + /// Gets the parent of the current content as an item. /// - /// Id of the to retrieve the parent from - /// Parent object + /// Id of the to retrieve the parent from + /// Parent object public IContent GetParent(int id) { // intentionally not locking - var content = GetById(id); + IContent content = GetById(id); return GetParent(content); } /// - /// Gets the parent of the current content as an item. + /// Gets the parent of the current content as an item. /// - /// to retrieve the parent from - /// Parent object + /// to retrieve the parent from + /// Parent object public IContent GetParent(IContent content) { - if (content.ParentId == Cms.Core.Constants.System.Root || content.ParentId == Cms.Core.Constants.System.RecycleBinContent) + if (content.ParentId == Constants.System.Root || content.ParentId == Constants.System.RecycleBinContent) + { return null; + } return GetById(content.ParentId); } /// - /// Gets a collection of objects, which reside at the first level / root + /// Gets a collection of objects, which reside at the first level / root /// - /// An Enumerable list of objects + /// An Enumerable list of objects public IEnumerable GetRootContent() { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { - scope.ReadLock(Cms.Core.Constants.Locks.ContentTree); - var query = Query().Where(x => x.ParentId == Cms.Core.Constants.System.Root); + scope.ReadLock(Constants.Locks.ContentTree); + IQuery query = Query().Where(x => x.ParentId == Constants.System.Root); return _documentRepository.Get(query); } } /// - /// Gets all published content items + /// Gets all published content items /// /// internal IEnumerable GetAllPublished() { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { - scope.ReadLock(Cms.Core.Constants.Locks.ContentTree); + scope.ReadLock(Constants.Locks.ContentTree); return _documentRepository.Get(QueryNotTrashed); } } @@ -682,9 +845,9 @@ internal IEnumerable GetAllPublished() /// public IEnumerable GetContentForExpiration(DateTime date) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { - scope.ReadLock(Cms.Core.Constants.Locks.ContentTree); + scope.ReadLock(Constants.Locks.ContentTree); return _documentRepository.GetContentForExpiration(date); } } @@ -692,62 +855,69 @@ public IEnumerable GetContentForExpiration(DateTime date) /// public IEnumerable GetContentForRelease(DateTime date) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { - scope.ReadLock(Cms.Core.Constants.Locks.ContentTree); + scope.ReadLock(Constants.Locks.ContentTree); return _documentRepository.GetContentForRelease(date); } } /// - /// Gets a collection of an objects, which resides in the Recycle Bin + /// Gets a collection of an objects, which resides in the Recycle Bin /// - /// An Enumerable list of objects + /// An Enumerable list of objects public IEnumerable GetPagedContentInRecycleBin(long pageIndex, int pageSize, out long totalRecords, IQuery filter = null, Ordering ordering = null) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { if (ordering == null) + { ordering = Ordering.By("Path"); + } - scope.ReadLock(Cms.Core.Constants.Locks.ContentTree); - var query = Query().Where(x => x.Path.StartsWith(Cms.Core.Constants.System.RecycleBinContentPathPrefix)); + scope.ReadLock(Constants.Locks.ContentTree); + IQuery query = Query() + .Where(x => x.Path.StartsWith(Constants.System.RecycleBinContentPathPrefix)); return _documentRepository.GetPage(query, pageIndex, pageSize, out totalRecords, filter, ordering); } } /// - /// Checks whether an item has any children + /// Checks whether an item has any children /// - /// Id of the + /// Id of the /// True if the content has any children otherwise False - public bool HasChildren(int id) - { - return CountChildren(id) > 0; - } + public bool HasChildren(int id) => CountChildren(id) > 0; /// - /// Checks if the passed in can be published based on the ancestors publish state. + /// Checks if the passed in can be published based on the ancestors publish state. /// - /// to check if ancestors are published + /// to check if ancestors are published /// True if the Content can be published, otherwise False public bool IsPathPublishable(IContent content) { // fast - if (content.ParentId == Cms.Core.Constants.System.Root) return true; // root content is always publishable - if (content.Trashed) return false; // trashed content is never publishable + if (content.ParentId == Constants.System.Root) + { + return true; // root content is always publishable + } + + if (content.Trashed) + { + return false; // trashed content is never publishable + } // not trashed and has a parent: publishable if the parent is path-published - var parent = GetById(content.ParentId); + IContent parent = GetById(content.ParentId); return parent == null || IsPathPublished(parent); } public bool IsPathPublished(IContent content) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { - scope.ReadLock(Cms.Core.Constants.Locks.ContentTree); + scope.ReadLock(Constants.Locks.ContentTree); return _documentRepository.IsPathPublished(content); } } @@ -757,17 +927,19 @@ public bool IsPathPublished(IContent content) #region Save, Publish, Unpublish /// - public OperationResult Save(IContent content, int userId = Cms.Core.Constants.Security.SuperUserId) + public OperationResult Save(IContent content, int userId = Constants.Security.SuperUserId) { PublishedState publishedState = content.PublishedState; if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished) { - throw new InvalidOperationException($"Cannot save (un)publishing content with name: {content.Name} - and state: {content.PublishedState}, use the dedicated SavePublished method."); + throw new InvalidOperationException( + $"Cannot save (un)publishing content with name: {content.Name} - and state: {content.PublishedState}, use the dedicated SavePublished method."); } if (content.Name != null && content.Name.Length > 255) { - throw new InvalidOperationException($"Content with the name {content.Name} cannot be more than 255 characters in length."); + throw new InvalidOperationException( + $"Content with the name {content.Name} cannot be more than 255 characters in length."); } EventMessages eventMessages = EventMessagesFactory.Get(); @@ -801,13 +973,15 @@ public OperationResult Save(IContent content, int userId = Cms.Core.Constants.Se _documentRepository.Save(content); - scope.Notifications.Publish(new ContentSavedNotification(content, eventMessages).WithStateFrom(savingNotification)); + scope.Notifications.Publish( + new ContentSavedNotification(content, eventMessages).WithStateFrom(savingNotification)); // TODO: we had code here to FORCE that this event can never be suppressed. But that just doesn't make a ton of sense?! // I understand that if its suppressed that the caches aren't updated, but that would be expected. If someone // is supressing events then I think it's expected that nothing will happen. They are probably doing it for perf // reasons like bulk import and in those cases we don't want this occuring. - scope.Notifications.Publish(new ContentTreeChangeNotification(content, TreeChangeTypes.RefreshNode, eventMessages)); + scope.Notifications.Publish( + new ContentTreeChangeNotification(content, TreeChangeTypes.RefreshNode, eventMessages)); if (culturesChanging != null) { @@ -817,7 +991,9 @@ public OperationResult Save(IContent content, int userId = Cms.Core.Constants.Se Audit(AuditType.SaveVariant, userId, content.Id, $"Saved languages: {langs}", langs); } else + { Audit(AuditType.Save, userId, content.Id); + } scope.Complete(); } @@ -826,7 +1002,7 @@ public OperationResult Save(IContent content, int userId = Cms.Core.Constants.Se } /// - public OperationResult Save(IEnumerable contents, int userId = Cms.Core.Constants.Security.SuperUserId) + public OperationResult Save(IEnumerable contents, int userId = Constants.Security.SuperUserId) { EventMessages eventMessages = EventMessagesFactory.Get(); IContent[] contentsA = contents.ToArray(); @@ -840,7 +1016,7 @@ public OperationResult Save(IEnumerable contents, int userId = Cms.Cor return OperationResult.Cancel(eventMessages); } - scope.WriteLock(Cms.Core.Constants.Locks.ContentTree); + scope.WriteLock(Constants.Locks.ContentTree); foreach (IContent content in contentsA) { if (content.HasIdentity == false) @@ -853,11 +1029,13 @@ public OperationResult Save(IEnumerable contents, int userId = Cms.Cor _documentRepository.Save(content); } - scope.Notifications.Publish(new ContentSavedNotification(contentsA, eventMessages).WithStateFrom(savingNotification)); + scope.Notifications.Publish( + new ContentSavedNotification(contentsA, eventMessages).WithStateFrom(savingNotification)); // TODO: See note above about supressing events - scope.Notifications.Publish(new ContentTreeChangeNotification(contentsA, TreeChangeTypes.RefreshNode, eventMessages)); + scope.Notifications.Publish( + new ContentTreeChangeNotification(contentsA, TreeChangeTypes.RefreshNode, eventMessages)); - Audit(AuditType.Save, userId == -1 ? 0 : userId, Cms.Core.Constants.System.Root, "Saved multiple content"); + Audit(AuditType.Save, userId == -1 ? 0 : userId, Constants.System.Root, "Saved multiple content"); scope.Complete(); } @@ -866,25 +1044,34 @@ public OperationResult Save(IEnumerable contents, int userId = Cms.Cor } /// - public PublishResult SaveAndPublish(IContent content, string culture = "*", int userId = Cms.Core.Constants.Security.SuperUserId) + public PublishResult SaveAndPublish(IContent content, string culture = "*", + int userId = Constants.Security.SuperUserId) { - var evtMsgs = EventMessagesFactory.Get(); + EventMessages evtMsgs = EventMessagesFactory.Get(); - var publishedState = content.PublishedState; + PublishedState publishedState = content.PublishedState; if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished) - throw new InvalidOperationException($"Cannot save-and-publish (un)publishing content, use the dedicated {nameof(CommitDocumentChanges)} method."); + { + throw new InvalidOperationException( + $"Cannot save-and-publish (un)publishing content, use the dedicated {nameof(CommitDocumentChanges)} method."); + } // cannot accept invariant (null or empty) culture for variant content type // cannot accept a specific culture for invariant content type (but '*' is ok) if (content.ContentType.VariesByCulture()) { if (culture.IsNullOrWhiteSpace()) + { throw new NotSupportedException("Invariant culture is not supported by variant content types."); + } } else { if (!culture.IsNullOrWhiteSpace() && culture != "*") - throw new NotSupportedException($"Culture \"{culture}\" is not supported by invariant content types."); + { + throw new NotSupportedException( + $"Culture \"{culture}\" is not supported by invariant content types."); + } } if (content.Name != null && content.Name.Length > 255) @@ -892,9 +1079,9 @@ public PublishResult SaveAndPublish(IContent content, string culture = "*", int throw new InvalidOperationException("Name cannot be more than 255 characters in length."); } - using (var scope = ScopeProvider.CreateScope()) + using (IScope scope = ScopeProvider.CreateScope()) { - scope.WriteLock(Cms.Core.Constants.Locks.ContentTree); + scope.WriteLock(Constants.Locks.ContentTree); var allLangs = _languageRepository.GetMany().ToList(); @@ -914,30 +1101,39 @@ public PublishResult SaveAndPublish(IContent content, string culture = "*", int // we don't care about the response here, this response will be rechecked below but we need to set the culture info values now. content.PublishCulture(impact); - var result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId); + PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, + savingNotification.State, userId); scope.Complete(); return result; } } /// - public PublishResult SaveAndPublish(IContent content, string[] cultures, int userId = Cms.Core.Constants.Security.SuperUserId) + public PublishResult SaveAndPublish(IContent content, string[] cultures, + int userId = Constants.Security.SuperUserId) { - if (content == null) throw new ArgumentNullException(nameof(content)); - if (cultures == null) throw new ArgumentNullException(nameof(cultures)); + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + if (cultures == null) + { + throw new ArgumentNullException(nameof(cultures)); + } if (content.Name != null && content.Name.Length > 255) { throw new InvalidOperationException("Name cannot be more than 255 characters in length."); } - using (var scope = ScopeProvider.CreateScope()) + using (IScope scope = ScopeProvider.CreateScope()) { - scope.WriteLock(Cms.Core.Constants.Locks.ContentTree); + scope.WriteLock(Constants.Locks.ContentTree); var allLangs = _languageRepository.GetMany().ToList(); - var evtMsgs = EventMessagesFactory.Get(); + EventMessages evtMsgs = EventMessagesFactory.Get(); var savingNotification = new ContentSavingNotification(content, evtMsgs); if (scope.Notifications.PublishCancelable(savingNotification)) @@ -954,56 +1150,75 @@ public PublishResult SaveAndPublish(IContent content, string[] cultures, int use } if (cultures.Any(x => x == null || x == "*")) - throw new InvalidOperationException("Only valid cultures are allowed to be used in this method, wildcards or nulls are not allowed"); + { + throw new InvalidOperationException( + "Only valid cultures are allowed to be used in this method, wildcards or nulls are not allowed"); + } - var impacts = cultures.Select(x => CultureImpact.Explicit(x, IsDefaultCulture(allLangs, x))); + IEnumerable impacts = + cultures.Select(x => CultureImpact.Explicit(x, IsDefaultCulture(allLangs, x))); // publish the culture(s) // we don't care about the response here, this response will be rechecked below but we need to set the culture info values now. - foreach (var impact in impacts) + foreach (CultureImpact impact in impacts) { content.PublishCulture(impact); } - var result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId); + PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, + savingNotification.State, userId); scope.Complete(); return result; } } /// - public PublishResult Unpublish(IContent content, string culture = "*", int userId = Cms.Core.Constants.Security.SuperUserId) + public PublishResult Unpublish(IContent content, string culture = "*", + int userId = Constants.Security.SuperUserId) { - if (content == null) throw new ArgumentNullException(nameof(content)); + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } - var evtMsgs = EventMessagesFactory.Get(); + EventMessages evtMsgs = EventMessagesFactory.Get(); culture = culture.NullOrWhiteSpaceAsNull(); - var publishedState = content.PublishedState; + PublishedState publishedState = content.PublishedState; if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished) - throw new InvalidOperationException($"Cannot save-and-publish (un)publishing content, use the dedicated {nameof(CommitDocumentChanges)} method."); + { + throw new InvalidOperationException( + $"Cannot save-and-publish (un)publishing content, use the dedicated {nameof(CommitDocumentChanges)} method."); + } // cannot accept invariant (null or empty) culture for variant content type // cannot accept a specific culture for invariant content type (but '*' is ok) if (content.ContentType.VariesByCulture()) { if (culture == null) + { throw new NotSupportedException("Invariant culture is not supported by variant content types."); + } } else { if (culture != null && culture != "*") - throw new NotSupportedException($"Culture \"{culture}\" is not supported by invariant content types."); + { + throw new NotSupportedException( + $"Culture \"{culture}\" is not supported by invariant content types."); + } } // if the content is not published, nothing to do if (!content.Published) + { return new PublishResult(PublishResultType.SuccessUnpublishAlready, evtMsgs, content); + } - using (var scope = ScopeProvider.CreateScope()) + using (IScope scope = ScopeProvider.CreateScope()) { - scope.WriteLock(Cms.Core.Constants.Locks.ContentTree); + scope.WriteLock(Constants.Locks.ContentTree); var allLangs = _languageRepository.GetMany().ToList(); @@ -1022,7 +1237,8 @@ public PublishResult Unpublish(IContent content, string culture = "*", int userI // to be non-routable so that when it's re-published all variants were as they were. content.PublishedState = PublishedState.Unpublishing; - var result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId); + PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, + savingNotification.State, userId); scope.Complete(); return result; } @@ -1036,7 +1252,8 @@ public PublishResult Unpublish(IContent content, string culture = "*", int userI var removed = content.UnpublishCulture(culture); //save and publish any changes - var result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId); + PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, + savingNotification.State, userId); scope.Complete(); @@ -1044,7 +1261,9 @@ public PublishResult Unpublish(IContent content, string culture = "*", int userI // were specified to be published which will be the case when removed is false. In that case // we want to swap the result type to PublishResultType.SuccessUnpublishAlready (that was the expectation before). if (result.Result == PublishResultType.FailedPublishNothingToPublish && !removed) + { return new PublishResult(PublishResultType.SuccessUnpublishAlready, evtMsgs, content); + } return result; } @@ -1052,32 +1271,39 @@ public PublishResult Unpublish(IContent content, string culture = "*", int userI } /// - /// Saves a document and publishes/unpublishes any pending publishing changes made to the document. + /// Saves a document and publishes/unpublishes any pending publishing changes made to the document. /// /// - /// - /// This MUST NOT be called from within this service, this used to be a public API and must only be used outside of this service. - /// Internally in this service, calls must be made to CommitDocumentChangesInternal - /// - /// - /// This is the underlying logic for both publishing and unpublishing any document - /// Pending publishing/unpublishing changes on a document are made with calls to and - /// . - /// When publishing or unpublishing a single culture, or all cultures, use - /// and . But if the flexibility to both publish and unpublish in a single operation is required - /// then this method needs to be used in combination with and - /// on the content itself - this prepares the content, but does not commit anything - and then, invoke - /// to actually commit the changes to the database. - /// The document is *always* saved, even when publishing fails. + /// + /// This MUST NOT be called from within this service, this used to be a public API and must only be used outside of + /// this service. + /// Internally in this service, calls must be made to CommitDocumentChangesInternal + /// + /// This is the underlying logic for both publishing and unpublishing any document + /// + /// Pending publishing/unpublishing changes on a document are made with calls to + /// and + /// . + /// + /// + /// When publishing or unpublishing a single culture, or all cultures, use + /// and . But if the flexibility to both publish and unpublish in a single operation is + /// required + /// then this method needs to be used in combination with + /// and + /// on the content itself - this prepares the content, but does not commit anything - and then, invoke + /// to actually commit the changes to the database. + /// + /// The document is *always* saved, even when publishing fails. /// internal PublishResult CommitDocumentChanges(IContent content, - int userId = Cms.Core.Constants.Security.SuperUserId) + int userId = Constants.Security.SuperUserId) { - using (var scope = ScopeProvider.CreateScope()) + using (IScope scope = ScopeProvider.CreateScope()) { - var evtMsgs = EventMessagesFactory.Get(); + EventMessages evtMsgs = EventMessagesFactory.Get(); - scope.WriteLock(Cms.Core.Constants.Locks.ContentTree); + scope.WriteLock(Constants.Locks.ContentTree); var savingNotification = new ContentSavingNotification(content, evtMsgs); if (scope.Notifications.PublishCancelable(savingNotification)) @@ -1087,14 +1313,15 @@ internal PublishResult CommitDocumentChanges(IContent content, var allLangs = _languageRepository.GetMany().ToList(); - var result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId); + PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, + savingNotification.State, userId); scope.Complete(); return result; } } /// - /// Handles a lot of business logic cases for how the document should be persisted + /// Handles a lot of business logic cases for how the document should be persisted /// /// /// @@ -1106,10 +1333,12 @@ internal PublishResult CommitDocumentChanges(IContent content, /// /// /// - /// - /// Business logic cases such: as unpublishing a mandatory culture, or unpublishing the last culture, checking for pending scheduled publishing, etc... is dealt with in this method. - /// There is quite a lot of cases to take into account along with logic that needs to deal with scheduled saving/publishing, branch saving/publishing, etc... - /// + /// + /// Business logic cases such: as unpublishing a mandatory culture, or unpublishing the last culture, checking for + /// pending scheduled publishing, etc... is dealt with in this method. + /// There is quite a lot of cases to take into account along with logic that needs to deal with scheduled + /// saving/publishing, branch saving/publishing, etc... + /// /// private PublishResult CommitDocumentChangesInternal(IScope scope, IContent content, EventMessages eventMessages, IReadOnlyCollection allLangs, @@ -1136,7 +1365,8 @@ private PublishResult CommitDocumentChangesInternal(IScope scope, IContent conte PublishResult unpublishResult = null; // nothing set = republish it all - if (content.PublishedState != PublishedState.Publishing && content.PublishedState != PublishedState.Unpublishing) + if (content.PublishedState != PublishedState.Publishing && + content.PublishedState != PublishedState.Unpublishing) { content.PublishedState = PublishedState.Publishing; } @@ -1179,18 +1409,20 @@ void SaveDocument(IContent c) //determine cultures publishing/unpublishing which will be based on previous calls to content.PublishCulture and ClearPublishInfo culturesUnpublishing = content.GetCulturesUnpublishing(); culturesPublishing = variesByCulture - ? content.PublishCultureInfos.Values.Where(x => x.IsDirty()).Select(x => x.Culture).ToList() - : null; + ? content.PublishCultureInfos.Values.Where(x => x.IsDirty()).Select(x => x.Culture).ToList() + : null; // ensure that the document can be published, and publish handling events, business rules, etc - publishResult = StrategyCanPublish(scope, content, /*checkPath:*/ (!branchOne || branchRoot), culturesPublishing, culturesUnpublishing, eventMessages, allLangs, notificationState); + publishResult = StrategyCanPublish(scope, content, /*checkPath:*/ !branchOne || branchRoot, + culturesPublishing, culturesUnpublishing, eventMessages, allLangs, notificationState); if (publishResult.Success) { // note: StrategyPublish flips the PublishedState to Publishing! publishResult = StrategyPublish(content, culturesPublishing, culturesUnpublishing, eventMessages); //check if a culture has been unpublished and if there are no cultures left, and then unpublish document as a whole - if (publishResult.Result == PublishResultType.SuccessUnpublishCulture && content.PublishCultureInfos.Count == 0) + if (publishResult.Result == PublishResultType.SuccessUnpublishCulture && + content.PublishCultureInfos.Count == 0) { // This is a special case! We are unpublishing the last culture and to persist that we need to re-publish without any cultures // so the state needs to remain Publishing to do that. However, we then also need to unpublish the document and to do that @@ -1235,7 +1467,8 @@ void SaveDocument(IContent c) IContent newest = GetById(content.Id); // ensure we have the newest version - in scope if (content.VersionId != newest.VersionId) { - return new PublishResult(PublishResultType.FailedPublishConcurrencyViolation, eventMessages, content); + return new PublishResult(PublishResultType.FailedPublishConcurrencyViolation, eventMessages, + content); } if (content.Published) @@ -1272,27 +1505,32 @@ void SaveDocument(IContent c) SaveDocument(content); // raise the Saved event, always - scope.Notifications.Publish(new ContentSavedNotification(content, eventMessages).WithState(notificationState)); + scope.Notifications.Publish( + new ContentSavedNotification(content, eventMessages).WithState(notificationState)); if (unpublishing) // we have tried to unpublish - won't happen in a branch { if (unpublishResult.Success) // and succeeded, trigger events { // events and audit - scope.Notifications.Publish(new ContentUnpublishedNotification(content, eventMessages).WithState(notificationState)); - scope.Notifications.Publish(new ContentTreeChangeNotification(content, TreeChangeTypes.RefreshBranch, eventMessages)); + scope.Notifications.Publish( + new ContentUnpublishedNotification(content, eventMessages).WithState(notificationState)); + scope.Notifications.Publish(new ContentTreeChangeNotification(content, + TreeChangeTypes.RefreshBranch, eventMessages)); if (culturesUnpublishing != null) { // This will mean that that we unpublished a mandatory culture or we unpublished the last culture. var langs = string.Join(", ", allLangs - .Where(x => culturesUnpublishing.InvariantContains(x.IsoCode)) - .Select(x => x.CultureName)); + .Where(x => culturesUnpublishing.InvariantContains(x.IsoCode)) + .Select(x => x.CultureName)); Audit(AuditType.UnpublishVariant, userId, content.Id, $"Unpublished languages: {langs}", langs); if (publishResult == null) + { throw new PanicException("publishResult == null - should not happen"); + } switch (publishResult.Result) { @@ -1300,15 +1538,18 @@ void SaveDocument(IContent c) //occurs when a mandatory culture was unpublished (which means we tried publishing the document without a mandatory culture) //log that the whole content item has been unpublished due to mandatory culture unpublished - Audit(AuditType.Unpublish, userId, content.Id, "Unpublished (mandatory language unpublished)"); - return new PublishResult(PublishResultType.SuccessUnpublishMandatoryCulture, eventMessages, content); + Audit(AuditType.Unpublish, userId, content.Id, + "Unpublished (mandatory language unpublished)"); + return new PublishResult(PublishResultType.SuccessUnpublishMandatoryCulture, + eventMessages, content); case PublishResultType.SuccessUnpublishCulture: //occurs when the last culture is unpublished - Audit(AuditType.Unpublish, userId, content.Id, "Unpublished (last language unpublished)"); - return new PublishResult(PublishResultType.SuccessUnpublishLastCulture, eventMessages, content); + Audit(AuditType.Unpublish, userId, content.Id, + "Unpublished (last language unpublished)"); + return new PublishResult(PublishResultType.SuccessUnpublishLastCulture, eventMessages, + content); } - } Audit(AuditType.Unpublish, userId, content.Id); @@ -1337,8 +1578,10 @@ void SaveDocument(IContent c) // invalidate the node/branch if (!branchOne) // for branches, handled by SaveAndPublishBranch { - scope.Notifications.Publish(new ContentTreeChangeNotification(content, changeType, eventMessages)); - scope.Notifications.Publish(new ContentPublishedNotification(content, eventMessages).WithState(notificationState)); + scope.Notifications.Publish( + new ContentTreeChangeNotification(content, changeType, eventMessages)); + scope.Notifications.Publish( + new ContentPublishedNotification(content, eventMessages).WithState(notificationState)); } // it was not published and now is... descendants that were 'published' (but @@ -1347,7 +1590,8 @@ void SaveDocument(IContent c) if (!branchOne && isNew == false && previouslyPublished == false && HasChildren(content.Id)) { IContent[] descendants = GetPublishedDescendantsLocked(content).ToArray(); - scope.Notifications.Publish(new ContentPublishedNotification(descendants, eventMessages).WithState(notificationState)); + scope.Notifications.Publish( + new ContentPublishedNotification(descendants, eventMessages).WithState(notificationState)); } switch (publishResult.Result) @@ -1361,17 +1605,21 @@ void SaveDocument(IContent c) var langs = string.Join(", ", allLangs .Where(x => culturesPublishing.InvariantContains(x.IsoCode)) .Select(x => x.CultureName)); - Audit(AuditType.PublishVariant, userId, content.Id, $"Published languages: {langs}", langs); + Audit(AuditType.PublishVariant, userId, content.Id, $"Published languages: {langs}", + langs); } + break; case PublishResultType.SuccessUnpublishCulture: if (culturesUnpublishing != null) { var langs = string.Join(", ", allLangs - .Where(x => culturesUnpublishing.InvariantContains(x.IsoCode)) - .Select(x => x.CultureName)); - Audit(AuditType.UnpublishVariant, userId, content.Id, $"Unpublished languages: {langs}", langs); + .Where(x => culturesUnpublishing.InvariantContains(x.IsoCode)) + .Select(x => x.CultureName)); + Audit(AuditType.UnpublishVariant, userId, content.Id, $"Unpublished languages: {langs}", + langs); } + break; } @@ -1410,7 +1658,7 @@ void SaveDocument(IContent c) public IEnumerable PerformScheduledPublish(DateTime date) { var allLangs = new Lazy>(() => _languageRepository.GetMany().ToList()); - var evtMsgs = EventMessagesFactory.Get(); + EventMessages evtMsgs = EventMessagesFactory.Get(); var results = new List(); PerformScheduledPublishingRelease(date, results, evtMsgs, allLangs); @@ -1419,17 +1667,18 @@ public IEnumerable PerformScheduledPublish(DateTime date) return results; } - private void PerformScheduledPublishingExpiration(DateTime date, List results, EventMessages evtMsgs, Lazy> allLangs) + private void PerformScheduledPublishingExpiration(DateTime date, List results, + EventMessages evtMsgs, Lazy> allLangs) { - using var scope = ScopeProvider.CreateScope(); + using IScope scope = ScopeProvider.CreateScope(); // do a fast read without any locks since this executes often to see if we even need to proceed if (_documentRepository.HasContentForExpiration(date)) { // now take a write lock since we'll be updating - scope.WriteLock(Cms.Core.Constants.Locks.ContentTree); + scope.WriteLock(Constants.Locks.ContentTree); - foreach (var d in _documentRepository.GetContentForExpiration(date)) + foreach (IContent d in _documentRepository.GetContentForExpiration(date)) { if (d.ContentType.VariesByCulture()) { @@ -1440,7 +1689,9 @@ private void PerformScheduledPublishingExpiration(DateTime date, List results, EventMessages evtMsgs, Lazy> allLangs) + private void PerformScheduledPublishingRelease(DateTime date, List results, + EventMessages evtMsgs, Lazy> allLangs) { - using var scope = ScopeProvider.CreateScope(); + using IScope scope = ScopeProvider.CreateScope(); // do a fast read without any locks since this executes often to see if we even need to proceed if (_documentRepository.HasContentForRelease(date)) { // now take a write lock since we'll be updating - scope.WriteLock(Cms.Core.Constants.Locks.ContentTree); + scope.WriteLock(Constants.Locks.ContentTree); - foreach (var d in _documentRepository.GetContentForRelease(date)) + foreach (IContent d in _documentRepository.GetContentForRelease(date)) { if (d.ContentType.VariesByCulture()) { @@ -1501,7 +1761,9 @@ private void PerformScheduledPublishingRelease(DateTime date, List 0) - _logger.LogWarning("Scheduled publishing will fail for document {DocumentId} and culture {Culture} because of invalid properties {InvalidProperties}", + { + _logger.LogWarning( + "Scheduled publishing will fail for document {DocumentId} and culture {Culture} because of invalid properties {InvalidProperties}", d.Id, culture, string.Join(",", invalidProperties.Select(x => x.Alias))); + } publishing &= tryPublish; //set the culture to be published - if (!publishing) continue; // move to next document + if (!publishing) + { + continue; + } } PublishResult result; if (d.Trashed) + { result = new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, d); + } else if (!publishing) + { result = new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, d); + } else - result = CommitDocumentChangesInternal(scope, d, evtMsgs, allLangs.Value, savingNotification.State, d.WriterId); + { + result = CommitDocumentChangesInternal(scope, d, evtMsgs, allLangs.Value, + savingNotification.State, d.WriterId); + } if (result.Success == false) - _logger.LogError(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); + { + _logger.LogError(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, + result.Result); + } results.Add(result); } @@ -1549,26 +1832,29 @@ private void PerformScheduledPublishingRelease(DateTime date, List culturesToPublish, IReadOnlyCollection allLangs) + private bool SaveAndPublishBranch_PublishCultures(IContent content, HashSet culturesToPublish, + IReadOnlyCollection allLangs) { //TODO: This does not support being able to return invalid property details to bubble up to the UI @@ -1579,7 +1865,8 @@ private bool SaveAndPublishBranch_PublishCultures(IContent content, HashSet { var impact = CultureImpact.Create(culture, IsDefaultCulture(allLangs, culture), content); - return content.PublishCulture(impact) && _propertyValidationService.Value.IsPropertyDataValid(content, out _, impact); + return content.PublishCulture(impact) && + _propertyValidationService.Value.IsPropertyDataValid(content, out _, impact); }); } @@ -1588,26 +1875,43 @@ private bool SaveAndPublishBranch_PublishCultures(IContent content, HashSet SaveAndPublishBranch_ShouldPublish(ref HashSet cultures, string c, bool published, bool edited, bool isRoot, bool force) + private HashSet SaveAndPublishBranch_ShouldPublish(ref HashSet cultures, string c, + bool published, bool edited, bool isRoot, bool force) { // if published, republish if (published) { - if (cultures == null) cultures = new HashSet(); // empty means 'already published' - if (edited) cultures.Add(c); // means 'republish this culture' + if (cultures == null) + { + cultures = new HashSet(); // empty means 'already published' + } + + if (edited) + { + cultures.Add(c); // means 'republish this culture' + } + return cultures; } // if not published, publish if force/root else do nothing - if (!force && !isRoot) return cultures; // null means 'nothing to do' + if (!force && !isRoot) + { + return cultures; // null means 'nothing to do' + } + + if (cultures == null) + { + cultures = new HashSet(); + } - if (cultures == null) cultures = new HashSet(); cultures.Add(c); // means 'publish this culture' return cultures; } /// - public IEnumerable SaveAndPublishBranch(IContent content, bool force, string culture = "*", int userId = Cms.Core.Constants.Security.SuperUserId) + public IEnumerable SaveAndPublishBranch(IContent content, bool force, string culture = "*", + int userId = Constants.Security.SuperUserId) { // note: EditedValue and PublishedValue are objects here, so it is important to .Equals() // and not to == them, else we would be comparing references, and that is a bad thing @@ -1624,10 +1928,16 @@ HashSet ShouldPublish(IContent c) HashSet culturesToPublish = null; if (!c.ContentType.VariesByCulture()) // invariant content type - return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, force); + { + return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, + force); + } if (culture != "*") // variant content type, specific culture - return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, culture, c.IsCulturePublished(culture), c.IsCultureEdited(culture), isRoot, force); + { + return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, culture, + c.IsCulturePublished(culture), c.IsCultureEdited(culture), isRoot, force); + } // variant content type, all cultures if (c.Published) @@ -1635,7 +1945,11 @@ HashSet ShouldPublish(IContent c) // then some (and maybe all) cultures will be 'already published' (unless forcing), // others will have to 'republish this culture' foreach (var x in c.AvailableCultures) - SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, x, c.IsCulturePublished(x), c.IsCultureEdited(x), isRoot, force); + { + SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, x, c.IsCulturePublished(x), + c.IsCultureEdited(x), isRoot, force); + } + return culturesToPublish; } @@ -1649,7 +1963,8 @@ HashSet ShouldPublish(IContent c) } /// - public IEnumerable SaveAndPublishBranch(IContent content, bool force, string[] cultures, int userId = Cms.Core.Constants.Security.SuperUserId) + public IEnumerable SaveAndPublishBranch(IContent content, bool force, string[] cultures, + int userId = Constants.Security.SuperUserId) { // note: EditedValue and PublishedValue are objects here, so it is important to .Equals() // and not to == them, else we would be comparing references, and that is a bad thing @@ -1664,7 +1979,10 @@ HashSet ShouldPublish(IContent c) HashSet culturesToPublish = null; if (!c.ContentType.VariesByCulture()) // invariant content type - return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, force); + { + return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, + force); + } // variant content type, specific cultures if (c.Published) @@ -1672,7 +1990,11 @@ HashSet ShouldPublish(IContent c) // then some (and maybe all) cultures will be 'already published' (unless forcing), // others will have to 'republish this culture' foreach (var x in cultures) - SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, x, c.IsCulturePublished(x), c.IsCultureEdited(x), isRoot, force); + { + SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, x, c.IsCulturePublished(x), + c.IsCultureEdited(x), isRoot, force); + } + return culturesToPublish; } @@ -1688,7 +2010,7 @@ HashSet ShouldPublish(IContent c) internal IEnumerable SaveAndPublishBranch(IContent document, bool force, Func> shouldPublish, Func, IReadOnlyCollection, bool> publishCultures, - int userId = Cms.Core.Constants.Security.SuperUserId) + int userId = Constants.Security.SuperUserId) { if (shouldPublish == null) { @@ -1706,7 +2028,7 @@ internal IEnumerable SaveAndPublishBranch(IContent document, bool using (IScope scope = ScopeProvider.CreateScope()) { - scope.WriteLock(Cms.Core.Constants.Locks.ContentTree); + scope.WriteLock(Constants.Locks.ContentTree); var allLangs = _languageRepository.GetMany().ToList(); @@ -1722,7 +2044,8 @@ internal IEnumerable SaveAndPublishBranch(IContent document, bool } // deal with the branch root - if it fails, abort - PublishResult result = SaveAndPublishBranchItem(scope, document, shouldPublish, publishCultures, true, publishedDocuments, eventMessages, userId, allLangs); + PublishResult result = SaveAndPublishBranchItem(scope, document, shouldPublish, publishCultures, true, + publishedDocuments, eventMessages, userId, allLangs); if (result != null) { results.Add(result); @@ -1744,7 +2067,8 @@ internal IEnumerable SaveAndPublishBranch(IContent document, bool count = 0; // important to order by Path ASC so make it explicit in case defaults change // ReSharper disable once RedundantArgumentDefaultValue - foreach (IContent d in GetPagedDescendants(document.Id, page, pageSize, out _, ordering: Ordering.By("Path", Direction.Ascending))) + foreach (IContent d in GetPagedDescendants(document.Id, page, pageSize, out _, + ordering: Ordering.By("Path", Direction.Ascending))) { count++; @@ -1756,7 +2080,8 @@ internal IEnumerable SaveAndPublishBranch(IContent document, bool } // no need to check path here, parent has to be published here - result = SaveAndPublishBranchItem(scope, d, shouldPublish, publishCultures, false, publishedDocuments, eventMessages, userId, allLangs); + result = SaveAndPublishBranchItem(scope, d, shouldPublish, publishCultures, false, + publishedDocuments, eventMessages, userId, allLangs); if (result != null) { results.Add(result); @@ -1777,7 +2102,8 @@ internal IEnumerable SaveAndPublishBranch(IContent document, bool // trigger events for the entire branch // (SaveAndPublishBranchOne does *not* do it) - scope.Notifications.Publish(new ContentTreeChangeNotification(document, TreeChangeTypes.RefreshBranch, eventMessages)); + scope.Notifications.Publish( + new ContentTreeChangeNotification(document, TreeChangeTypes.RefreshBranch, eventMessages)); scope.Notifications.Publish(new ContentPublishedNotification(publishedDocuments, eventMessages)); scope.Complete(); @@ -1796,11 +2122,16 @@ private PublishResult SaveAndPublishBranchItem(IScope scope, IContent document, ICollection publishedDocuments, EventMessages evtMsgs, int userId, IReadOnlyCollection allLangs) { - var culturesToPublish = shouldPublish(document); + HashSet culturesToPublish = shouldPublish(document); if (culturesToPublish == null) // null = do not include + { return null; + } + if (culturesToPublish.Count == 0) // empty = already published + { return new PublishResult(PublishResultType.SuccessPublishAlready, evtMsgs, document); + } var savingNotification = new ContentSavingNotification(document, evtMsgs); if (scope.Notifications.PublishCancelable(savingNotification)) @@ -1815,9 +2146,13 @@ private PublishResult SaveAndPublishBranchItem(IScope scope, IContent document, return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, document); } - var result = CommitDocumentChangesInternal(scope, document, evtMsgs, allLangs, savingNotification.State, userId, branchOne: true, branchRoot: isRoot); + PublishResult result = CommitDocumentChangesInternal(scope, document, evtMsgs, allLangs, + savingNotification.State, userId, true, isRoot); if (result.Success) + { publishedDocuments.Add(document); + } + return result; } @@ -1826,7 +2161,7 @@ private PublishResult SaveAndPublishBranchItem(IScope scope, IContent document, #region Delete /// - public OperationResult Delete(IContent content, int userId = Cms.Core.Constants.Security.SuperUserId) + public OperationResult Delete(IContent content, int userId = Constants.Security.SuperUserId) { EventMessages eventMessages = EventMessagesFactory.Get(); @@ -1838,7 +2173,7 @@ public OperationResult Delete(IContent content, int userId = Cms.Core.Constants. return OperationResult.Cancel(eventMessages); } - scope.WriteLock(Cms.Core.Constants.Locks.ContentTree); + scope.WriteLock(Constants.Locks.ContentTree); // if it's not trashed yet, and published, we should unpublish // but... Unpublishing event makes no sense (not going to cancel?) and no need to save @@ -1850,7 +2185,8 @@ public OperationResult Delete(IContent content, int userId = Cms.Core.Constants. DeleteLocked(scope, content, eventMessages); - scope.Notifications.Publish(new ContentTreeChangeNotification(content, TreeChangeTypes.Remove, eventMessages)); + scope.Notifications.Publish( + new ContentTreeChangeNotification(content, TreeChangeTypes.Remove, eventMessages)); Audit(AuditType.Delete, userId, content.Id); scope.Complete(); @@ -1874,10 +2210,14 @@ void DoDelete(IContent c) while (total > 0) { //get descendants - ordered from deepest to shallowest - var descendants = GetPagedDescendants(content.Id, 0, pageSize, out total, ordering: Ordering.By("Path", Direction.Descending)); - foreach (var c in descendants) + IEnumerable descendants = GetPagedDescendants(content.Id, 0, pageSize, out total, + ordering: Ordering.By("Path", Direction.Descending)); + foreach (IContent c in descendants) + { DoDelete(c); + } } + DoDelete(content); } @@ -1887,50 +2227,54 @@ void DoDelete(IContent c) // 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. + /// Permanently deletes versions from an object prior to a specific date. + /// This method will never delete the latest version of a content item. /// - /// Id of the object to delete versions from + /// Id of the object to delete versions from /// Latest version date /// Optional Id of the User deleting versions of a Content object - public void DeleteVersions(int id, DateTime versionDate, int userId = Cms.Core.Constants.Security.SuperUserId) + public void DeleteVersions(int id, DateTime versionDate, int userId = Constants.Security.SuperUserId) { - var evtMsgs = EventMessagesFactory.Get(); + EventMessages evtMsgs = EventMessagesFactory.Get(); - using (var scope = ScopeProvider.CreateScope()) + using (IScope scope = ScopeProvider.CreateScope()) { - var deletingVersionsNotification = new ContentDeletingVersionsNotification(id, evtMsgs, dateToRetain: versionDate); + var deletingVersionsNotification = + new ContentDeletingVersionsNotification(id, evtMsgs, dateToRetain: versionDate); if (scope.Notifications.PublishCancelable(deletingVersionsNotification)) { scope.Complete(); return; } - scope.WriteLock(Cms.Core.Constants.Locks.ContentTree); + scope.WriteLock(Constants.Locks.ContentTree); _documentRepository.DeleteVersions(id, versionDate); - scope.Notifications.Publish(new ContentDeletedVersionsNotification(id, evtMsgs, dateToRetain: versionDate).WithStateFrom(deletingVersionsNotification)); - Audit(AuditType.Delete, userId, Cms.Core.Constants.System.Root, "Delete (by version date)"); + scope.Notifications.Publish( + new ContentDeletedVersionsNotification(id, evtMsgs, dateToRetain: versionDate).WithStateFrom( + deletingVersionsNotification)); + Audit(AuditType.Delete, userId, Constants.System.Root, "Delete (by version date)"); scope.Complete(); } } /// - /// Permanently deletes specific version(s) from an object. - /// This method will never delete the latest version of a content item. + /// Permanently deletes specific version(s) from an object. + /// This method will never delete the latest version of a content item. /// - /// Id of the object to delete a version from + /// Id of the object to delete a version from /// Id of the version to delete /// Boolean indicating whether to delete versions prior to the versionId /// Optional Id of the User deleting versions of a Content object - public void DeleteVersion(int id, int versionId, bool deletePriorVersions, int userId = Cms.Core.Constants.Security.SuperUserId) + public void DeleteVersion(int id, int versionId, bool deletePriorVersions, + int userId = Constants.Security.SuperUserId) { - var evtMsgs = EventMessagesFactory.Get(); + EventMessages evtMsgs = EventMessagesFactory.Get(); - using (var scope = ScopeProvider.CreateScope()) + using (IScope scope = ScopeProvider.CreateScope()) { - var deletingVersionsNotification = new ContentDeletingVersionsNotification(id, evtMsgs, specificVersion: versionId); + var deletingVersionsNotification = new ContentDeletingVersionsNotification(id, evtMsgs, versionId); if (scope.Notifications.PublishCancelable(deletingVersionsNotification)) { scope.Complete(); @@ -1939,17 +2283,22 @@ public void DeleteVersion(int id, int versionId, bool deletePriorVersions, int u if (deletePriorVersions) { - var content = GetVersion(versionId); + IContent content = GetVersion(versionId); DeleteVersions(id, content.UpdateDate, userId); } - scope.WriteLock(Cms.Core.Constants.Locks.ContentTree); - var c = _documentRepository.Get(id); - if (c.VersionId != versionId && c.PublishedVersionId != versionId) // don't delete the current or published version + scope.WriteLock(Constants.Locks.ContentTree); + IContent c = _documentRepository.Get(id); + if (c.VersionId != versionId && + c.PublishedVersionId != versionId) // don't delete the current or published version + { _documentRepository.DeleteVersion(versionId); + } - scope.Notifications.Publish(new ContentDeletedVersionsNotification(id, evtMsgs, specificVersion: versionId).WithStateFrom(deletingVersionsNotification)); - Audit(AuditType.Delete, userId, Cms.Core.Constants.System.Root, "Delete (by version)"); + scope.Notifications.Publish( + new ContentDeletedVersionsNotification(id, evtMsgs, versionId).WithStateFrom( + deletingVersionsNotification)); + Audit(AuditType.Delete, userId, Constants.System.Root, "Delete (by version)"); scope.Complete(); } @@ -1960,19 +2309,21 @@ public void DeleteVersion(int id, int versionId, bool deletePriorVersions, int u #region Move, RecycleBin /// - public OperationResult MoveToRecycleBin(IContent content, int userId = Cms.Core.Constants.Security.SuperUserId) + public OperationResult MoveToRecycleBin(IContent content, int userId = Constants.Security.SuperUserId) { EventMessages eventMessages = EventMessagesFactory.Get(); var moves = new List<(IContent, string)>(); using (IScope scope = ScopeProvider.CreateScope()) { - scope.WriteLock(Cms.Core.Constants.Locks.ContentTree); + scope.WriteLock(Constants.Locks.ContentTree); var originalPath = content.Path; - var moveEventInfo = new MoveEventInfo(content, originalPath, Cms.Core.Constants.System.RecycleBinContent); + var moveEventInfo = + new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent); - var movingToRecycleBinNotification = new ContentMovingToRecycleBinNotification(moveEventInfo, eventMessages); + var movingToRecycleBinNotification = + new ContentMovingToRecycleBinNotification(moveEventInfo, eventMessages); if (scope.Notifications.PublishCancelable(movingToRecycleBinNotification)) { scope.Complete(); @@ -1985,14 +2336,17 @@ public OperationResult MoveToRecycleBin(IContent content, int userId = Cms.Core. //if (content.HasPublishedVersion) //{ } - PerformMoveLocked(content, Cms.Core.Constants.System.RecycleBinContent, null, userId, moves, true); - scope.Notifications.Publish(new ContentTreeChangeNotification(content, TreeChangeTypes.RefreshBranch, eventMessages)); + PerformMoveLocked(content, Constants.System.RecycleBinContent, null, userId, moves, true); + scope.Notifications.Publish( + new ContentTreeChangeNotification(content, TreeChangeTypes.RefreshBranch, eventMessages)); MoveEventInfo[] moveInfo = moves .Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId)) .ToArray(); - scope.Notifications.Publish(new ContentMovedToRecycleBinNotification(moveInfo, eventMessages).WithStateFrom(movingToRecycleBinNotification)); + scope.Notifications.Publish( + new ContentMovedToRecycleBinNotification(moveInfo, eventMessages).WithStateFrom( + movingToRecycleBinNotification)); Audit(AuditType.Move, userId, content.Id, "Moved to recycle bin"); scope.Complete(); @@ -2002,20 +2356,20 @@ public OperationResult MoveToRecycleBin(IContent content, int userId = Cms.Core. } /// - /// Moves an object to a new location by changing its parent id. + /// Moves an object to a new location by changing its parent id. /// /// - /// If the object is already published it will be - /// published after being moved to its new location. Otherwise it'll just - /// be saved with a new parent id. + /// If the object is already published it will be + /// published after being moved to its new location. Otherwise it'll just + /// be saved with a new parent id. /// - /// The to move + /// The to move /// Id of the Content's new Parent /// Optional Id of the User moving the Content - public void Move(IContent content, int parentId, int userId = Cms.Core.Constants.Security.SuperUserId) + public void Move(IContent content, int parentId, int userId = Constants.Security.SuperUserId) { // if moving to the recycle bin then use the proper method - if (parentId == Cms.Core.Constants.System.RecycleBinContent) + if (parentId == Constants.System.RecycleBinContent) { MoveToRecycleBin(content, userId); return; @@ -2027,10 +2381,10 @@ public void Move(IContent content, int parentId, int userId = Cms.Core.Constants using (IScope scope = ScopeProvider.CreateScope()) { - scope.WriteLock(Cms.Core.Constants.Locks.ContentTree); + scope.WriteLock(Constants.Locks.ContentTree); - IContent parent = parentId == Cms.Core.Constants.System.Root ? null : GetById(parentId); - if (parentId != Cms.Core.Constants.System.Root && (parent == null || parent.Trashed)) + IContent 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 } @@ -2061,13 +2415,15 @@ public void Move(IContent content, int parentId, int userId = Cms.Core.Constants PerformMoveLocked(content, parentId, parent, userId, moves, trashed); - scope.Notifications.Publish(new ContentTreeChangeNotification(content, TreeChangeTypes.RefreshBranch, eventMessages)); + scope.Notifications.Publish( + new ContentTreeChangeNotification(content, TreeChangeTypes.RefreshBranch, eventMessages)); MoveEventInfo[] moveInfo = moves //changes .Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId)) .ToArray(); - scope.Notifications.Publish(new ContentMovedNotification(moveInfo, eventMessages).WithStateFrom(movingNotification)); + scope.Notifications.Publish( + new ContentMovedNotification(moveInfo, eventMessages).WithStateFrom(movingNotification)); Audit(AuditType.Move, userId, content.Id); @@ -2104,17 +2460,21 @@ private void PerformMoveLocked(IContent content, int parentId, IContent parent, // if uow is not immediate, content.Path will be updated only when the UOW commits, // and because we want it now, we have to calculate it by ourselves //paths[content.Id] = content.Path; - paths[content.Id] = (parent == null ? (parentId == Cms.Core.Constants.System.RecycleBinContent ? "-1,-20" : Cms.Core.Constants.System.RootString) : parent.Path) + "," + content.Id; + paths[content.Id] = + (parent == null + ? parentId == Constants.System.RecycleBinContent ? "-1,-20" : Constants.System.RootString + : parent.Path) + "," + content.Id; const int pageSize = 500; - var query = GetPagedDescendantQuery(originalPath); + IQuery query = GetPagedDescendantQuery(originalPath); long total; do { // We always page a page 0 because for each page, we are moving the result so the resulting total will be reduced - var descendants = GetPagedLocked(query, 0, pageSize, out total, null, Ordering.By("Path", Direction.Ascending)); + IEnumerable descendants = + GetPagedLocked(query, 0, pageSize, out total, null, Ordering.By("Path")); - foreach (var descendant in descendants) + foreach (IContent descendant in descendants) { moves.Add((descendant, descendant.Path)); // capture original path @@ -2123,32 +2483,34 @@ private void PerformMoveLocked(IContent content, int parentId, IContent parent, descendant.Level += levelDelta; PerformMoveContentLocked(descendant, userId, trash); } - } while (total > pageSize); - } private void PerformMoveContentLocked(IContent content, int userId, bool? trash) { - if (trash.HasValue) ((ContentBase)content).Trashed = trash.Value; + if (trash.HasValue) + { + ((ContentBase)content).Trashed = trash.Value; + } + content.WriterId = userId; _documentRepository.Save(content); } /// - /// Empties the Recycle Bin by deleting all that resides in the bin + /// Empties the Recycle Bin by deleting all that resides in the bin /// - public OperationResult EmptyRecycleBin(int userId = Cms.Core.Constants.Security.SuperUserId) + public OperationResult EmptyRecycleBin(int userId = Constants.Security.SuperUserId) { var deleted = new List(); EventMessages eventMessages = EventMessagesFactory.Get(); using (IScope scope = ScopeProvider.CreateScope()) { - scope.WriteLock(Cms.Core.Constants.Locks.ContentTree); + scope.WriteLock(Constants.Locks.ContentTree); // emptying the recycle bin means deleting whatever is in there - do it properly! - IQuery query = Query().Where(x => x.ParentId == Cms.Core.Constants.System.RecycleBinContent); + IQuery query = Query().Where(x => x.ParentId == Constants.System.RecycleBinContent); IContent[] contents = _documentRepository.Get(query).ToArray(); var emptyingRecycleBinNotification = new ContentEmptyingRecycleBinNotification(contents, eventMessages); @@ -2164,9 +2526,12 @@ public OperationResult EmptyRecycleBin(int userId = Cms.Core.Constants.Security. deleted.Add(content); } - scope.Notifications.Publish(new ContentEmptiedRecycleBinNotification(deleted, eventMessages).WithStateFrom(emptyingRecycleBinNotification)); - scope.Notifications.Publish(new ContentTreeChangeNotification(deleted, TreeChangeTypes.Remove, eventMessages)); - Audit(AuditType.Delete, userId, Cms.Core.Constants.System.RecycleBinContent, "Recycle bin emptied"); + scope.Notifications.Publish( + new ContentEmptiedRecycleBinNotification(deleted, eventMessages).WithStateFrom( + emptyingRecycleBinNotification)); + scope.Notifications.Publish( + new ContentTreeChangeNotification(deleted, TreeChangeTypes.Remove, eventMessages)); + Audit(AuditType.Delete, userId, Constants.System.RecycleBinContent, "Recycle bin emptied"); scope.Complete(); } @@ -2176,7 +2541,7 @@ public OperationResult EmptyRecycleBin(int userId = Cms.Core.Constants.Security. public bool RecycleBinSmells() { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.ContentTree); return _documentRepository.RecycleBinSmells(); @@ -2188,30 +2553,31 @@ public bool RecycleBinSmells() #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. + /// Copies an object by creating a new Content object of the same type and copies all data from + /// the current + /// to the new copy which is returned. Recursively copies all children. /// - /// The to copy + /// The to copy /// Id of the Content's new Parent /// Boolean indicating whether the copy should be related to the original /// Optional Id of the User copying the Content - /// The newly created object - public IContent Copy(IContent content, int parentId, bool relateToOriginal, int userId = Cms.Core.Constants.Security.SuperUserId) - { - return Copy(content, parentId, relateToOriginal, true, userId); - } + /// The newly created object + public IContent Copy(IContent content, int parentId, bool relateToOriginal, + int userId = Constants.Security.SuperUserId) => Copy(content, parentId, relateToOriginal, true, userId); /// - /// Copies an object by creating a new Content object of the same type and copies all data from the current - /// to the new copy which is returned. + /// Copies an object by creating a new Content object of the same type and copies all data from + /// the current + /// to the new copy which is returned. /// - /// The to copy + /// The to copy /// Id of the Content's new Parent /// Boolean indicating whether the copy should be related to the original /// A value indicating whether to recursively copy children. /// Optional Id of the User copying the Content - /// The newly created object - public IContent Copy(IContent content, int parentId, bool relateToOriginal, bool recursive, int userId = Cms.Core.Constants.Security.SuperUserId) + /// The newly created object + public IContent Copy(IContent content, int parentId, bool relateToOriginal, bool recursive, + int userId = Constants.Security.SuperUserId) { EventMessages eventMessages = EventMessagesFactory.Get(); @@ -2220,7 +2586,8 @@ public IContent Copy(IContent content, int parentId, bool relateToOriginal, bool using (IScope scope = ScopeProvider.CreateScope()) { - if (scope.Notifications.PublishCancelable(new ContentCopyingNotification(content, copy, parentId, eventMessages))) + if (scope.Notifications.PublishCancelable( + new ContentCopyingNotification(content, copy, parentId, eventMessages))) { scope.Complete(); return null; @@ -2232,7 +2599,7 @@ public IContent Copy(IContent content, int parentId, bool relateToOriginal, bool var copies = new List>(); - scope.WriteLock(Cms.Core.Constants.Locks.ContentTree); + scope.WriteLock(Constants.Locks.ContentTree); // a copy is not published (but not really unpublishing either) // update the create author and last edit author @@ -2269,7 +2636,8 @@ public IContent Copy(IContent content, int parentId, bool relateToOriginal, bool var total = long.MaxValue; while (page * pageSize < total) { - IEnumerable descendants = GetPagedDescendants(content.Id, page++, pageSize, out total); + IEnumerable descendants = + GetPagedDescendants(content.Id, page++, pageSize, out total); foreach (IContent descendant in descendants) { // if parent has not been copied, skip, else gets its copy id @@ -2281,7 +2649,9 @@ public IContent Copy(IContent content, int parentId, bool relateToOriginal, bool IContent descendantCopy = descendant.DeepCloneWithResetIdentities(); descendantCopy.ParentId = parentId; - if (scope.Notifications.PublishCancelable(new ContentCopyingNotification(descendant, descendantCopy, parentId, eventMessages))) + if (scope.Notifications.PublishCancelable( + new ContentCopyingNotification(descendant, descendantCopy, parentId, + eventMessages))) { continue; } @@ -2309,11 +2679,14 @@ public IContent Copy(IContent content, int parentId, bool relateToOriginal, bool // - tags should be handled by the content repository // - a copy is unpublished and therefore has no impact on tags in DB - scope.Notifications.Publish(new ContentTreeChangeNotification(copy, TreeChangeTypes.RefreshBranch, eventMessages)); + scope.Notifications.Publish( + new ContentTreeChangeNotification(copy, TreeChangeTypes.RefreshBranch, eventMessages)); foreach (Tuple x in copies) { - scope.Notifications.Publish(new ContentCopiedNotification(x.Item1, x.Item2, parentId, relateToOriginal, eventMessages)); + scope.Notifications.Publish(new ContentCopiedNotification(x.Item1, x.Item2, parentId, + relateToOriginal, eventMessages)); } + Audit(AuditType.Copy, userId, content.Id); scope.Complete(); @@ -2323,16 +2696,17 @@ public IContent Copy(IContent content, int parentId, bool relateToOriginal, bool } /// - /// Sends an to Publication, which executes handlers and events for the 'Send to Publication' action. + /// Sends an to Publication, which executes handlers and events for the 'Send to Publication' + /// action. /// - /// The to send to publication + /// The to send to publication /// Optional Id of the User issuing the send to publication /// True if sending publication was successful otherwise false - public bool SendToPublication(IContent content, int userId = Cms.Core.Constants.Security.SuperUserId) + public bool SendToPublication(IContent content, int userId = Constants.Security.SuperUserId) { - var evtMsgs = EventMessagesFactory.Get(); + EventMessages evtMsgs = EventMessagesFactory.Get(); - using (var scope = ScopeProvider.CreateScope()) + using (IScope scope = ScopeProvider.CreateScope()) { var sendingToPublishNotification = new ContentSendingToPublishNotification(content, evtMsgs); if (scope.Notifications.PublishCancelable(sendingToPublishNotification)) @@ -2352,77 +2726,91 @@ public bool SendToPublication(IContent content, int userId = Cms.Core.Constants. // have always changed if it's been saved in the back office but that's not really fail safe. //Save before raising event - var saveResult = Save(content, userId); + OperationResult saveResult = Save(content, userId); // always complete (but maybe return a failed status) scope.Complete(); if (!saveResult.Success) + { return saveResult.Success; + } - scope.Notifications.Publish(new ContentSentToPublishNotification(content, evtMsgs).WithStateFrom(sendingToPublishNotification)); + scope.Notifications.Publish( + new ContentSentToPublishNotification(content, evtMsgs).WithStateFrom(sendingToPublishNotification)); if (culturesChanging != null) - Audit(AuditType.SendToPublishVariant, userId, content.Id, $"Send To Publish for cultures: {culturesChanging}", culturesChanging); + { + Audit(AuditType.SendToPublishVariant, userId, content.Id, + $"Send To Publish for cultures: {culturesChanging}", culturesChanging); + } else + { Audit(AuditType.SendToPublish, content.WriterId, content.Id); + } return saveResult.Success; } } /// - /// Sorts a collection of objects by updating the SortOrder according - /// to the ordering of items in the passed in . + /// Sorts a collection of objects by updating the SortOrder according + /// to the ordering of items in the passed in . /// /// - /// Using this method will ensure that the Published-state is maintained upon sorting - /// so the cache is updated accordingly - as needed. + /// Using this method will ensure that the Published-state is maintained upon sorting + /// so the cache is updated accordingly - as needed. /// /// /// /// Result indicating what action was taken when handling the command. - public OperationResult Sort(IEnumerable items, int userId = Cms.Core.Constants.Security.SuperUserId) + public OperationResult Sort(IEnumerable items, int userId = Constants.Security.SuperUserId) { - var evtMsgs = EventMessagesFactory.Get(); + EventMessages evtMsgs = EventMessagesFactory.Get(); - var itemsA = items.ToArray(); - if (itemsA.Length == 0) return new OperationResult(OperationResultType.NoOperation, evtMsgs); + IContent[] itemsA = items.ToArray(); + if (itemsA.Length == 0) + { + return new OperationResult(OperationResultType.NoOperation, evtMsgs); + } - using (var scope = ScopeProvider.CreateScope()) + using (IScope scope = ScopeProvider.CreateScope()) { - scope.WriteLock(Cms.Core.Constants.Locks.ContentTree); + scope.WriteLock(Constants.Locks.ContentTree); - var ret = Sort(scope, itemsA, userId, evtMsgs); + OperationResult ret = Sort(scope, itemsA, userId, evtMsgs); scope.Complete(); return ret; } } /// - /// Sorts a collection of objects by updating the SortOrder according - /// to the ordering of items identified by the . + /// Sorts a collection of objects by updating the SortOrder according + /// to the ordering of items identified by the . /// /// - /// Using this method will ensure that the Published-state is maintained upon sorting - /// so the cache is updated accordingly - as needed. + /// Using this method will ensure that the Published-state is maintained upon sorting + /// so the cache is updated accordingly - as needed. /// /// /// /// Result indicating what action was taken when handling the command. - public OperationResult Sort(IEnumerable ids, int userId = Cms.Core.Constants.Security.SuperUserId) + public OperationResult Sort(IEnumerable ids, int userId = Constants.Security.SuperUserId) { - var evtMsgs = EventMessagesFactory.Get(); + EventMessages evtMsgs = EventMessagesFactory.Get(); var idsA = ids.ToArray(); - if (idsA.Length == 0) return new OperationResult(OperationResultType.NoOperation, evtMsgs); + if (idsA.Length == 0) + { + return new OperationResult(OperationResultType.NoOperation, evtMsgs); + } - using (var scope = ScopeProvider.CreateScope()) + using (IScope scope = ScopeProvider.CreateScope()) { - scope.WriteLock(Cms.Core.Constants.Locks.ContentTree); - var itemsA = GetByIds(idsA).ToArray(); + scope.WriteLock(Constants.Locks.ContentTree); + IContent[] itemsA = GetByIds(idsA).ToArray(); - var ret = Sort(scope, itemsA, userId, evtMsgs); + OperationResult ret = Sort(scope, itemsA, userId, evtMsgs); scope.Complete(); return ret; } @@ -2476,10 +2864,13 @@ private OperationResult Sort(IScope scope, IContent[] itemsA, int userId, EventM } //first saved, then sorted - scope.Notifications.Publish(new ContentSavedNotification(itemsA, eventMessages).WithStateFrom(savingNotification)); - scope.Notifications.Publish(new ContentSortedNotification(itemsA, eventMessages).WithStateFrom(sortingNotification)); + scope.Notifications.Publish( + new ContentSavedNotification(itemsA, eventMessages).WithStateFrom(savingNotification)); + scope.Notifications.Publish( + new ContentSortedNotification(itemsA, eventMessages).WithStateFrom(sortingNotification)); - scope.Notifications.Publish(new ContentTreeChangeNotification(saved, TreeChangeTypes.RefreshNode, eventMessages)); + scope.Notifications.Publish( + new ContentTreeChangeNotification(saved, TreeChangeTypes.RefreshNode, eventMessages)); if (published.Any()) { @@ -2494,15 +2885,19 @@ public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportO { using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { - scope.WriteLock(Cms.Core.Constants.Locks.ContentTree); + scope.WriteLock(Constants.Locks.ContentTree); ContentDataIntegrityReport report = _documentRepository.CheckDataIntegrity(options); if (report.FixedIssues.Count > 0) { //The event args needs a content item so we'll make a fake one with enough properties to not cause a null ref - var root = new Content("root", -1, new ContentType(_shortStringHelper, -1)) {Id = -1, Key = Guid.Empty}; - scope.Notifications.Publish(new ContentTreeChangeNotification(root, TreeChangeTypes.RefreshAll, EventMessagesFactory.Get())); + var root = new Content("root", -1, new ContentType(_shortStringHelper, -1)) + { + Id = -1, Key = Guid.Empty + }; + scope.Notifications.Publish(new ContentTreeChangeNotification(root, TreeChangeTypes.RefreshAll, + EventMessagesFactory.Get())); } return report; @@ -2514,15 +2909,15 @@ public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportO #region Internal Methods /// - /// Gets a collection of descendants by the first Parent. + /// Gets a collection of descendants by the first Parent. /// - /// item to retrieve Descendants from - /// An Enumerable list of objects + /// item to retrieve Descendants from + /// An Enumerable list of objects internal IEnumerable GetPublishedDescendants(IContent content) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { - scope.ReadLock(Cms.Core.Constants.Locks.ContentTree); + scope.ReadLock(Constants.Locks.ContentTree); return GetPublishedDescendantsLocked(content).ToArray(); // ToArray important in uow! } } @@ -2530,15 +2925,16 @@ internal IEnumerable GetPublishedDescendants(IContent content) internal IEnumerable GetPublishedDescendantsLocked(IContent content) { var pathMatch = content.Path + ","; - var query = Query().Where(x => x.Id != content.Id && x.Path.StartsWith(pathMatch) /*&& x.Trashed == false*/); - var contents = _documentRepository.Get(query); + IQuery query = Query() + .Where(x => x.Id != content.Id && x.Path.StartsWith(pathMatch) /*&& x.Trashed == false*/); + IEnumerable contents = _documentRepository.Get(query); // beware! contents contains all published version below content // including those that are not directly published because below an unpublished content // these must be filtered out here var parents = new List { content.Id }; - foreach (var c in contents) + foreach (IContent c in contents) { if (parents.Contains(c.ParentId)) { @@ -2552,20 +2948,22 @@ internal IEnumerable GetPublishedDescendantsLocked(IContent content) #region Private Methods - private void Audit(AuditType type, int userId, int objectId, string message = null, string parameters = null) - { - _auditRepository.Save(new AuditItem(objectId, type, userId, ObjectTypes.GetName(UmbracoObjectTypes.Document), message, parameters)); - } + private void Audit(AuditType type, int userId, int objectId, string message = null, string parameters = null) => + _auditRepository.Save(new AuditItem(objectId, type, userId, UmbracoObjectTypes.Document.GetName(), message, + parameters)); + + private bool IsDefaultCulture(IReadOnlyCollection langs, string culture) => + langs.Any(x => x.IsDefault && x.IsoCode.InvariantEquals(culture)); - private bool IsDefaultCulture(IReadOnlyCollection langs, string culture) => langs.Any(x => x.IsDefault && x.IsoCode.InvariantEquals(culture)); - private bool IsMandatoryCulture(IReadOnlyCollection langs, string culture) => langs.Any(x => x.IsMandatory && x.IsoCode.InvariantEquals(culture)); + private bool IsMandatoryCulture(IReadOnlyCollection langs, string culture) => + langs.Any(x => x.IsMandatory && x.IsoCode.InvariantEquals(culture)); #endregion #region Publishing Strategies /// - /// Ensures that a document can be published + /// Ensures that a document can be published /// /// /// @@ -2576,40 +2974,54 @@ private void Audit(AuditType type, int userId, int objectId, string message = nu /// /// /// - private PublishResult StrategyCanPublish(IScope scope, IContent content, bool checkPath, IReadOnlyList culturesPublishing, - IReadOnlyCollection culturesUnpublishing, EventMessages evtMsgs, IReadOnlyCollection allLangs, IDictionary notificationState) + private PublishResult StrategyCanPublish(IScope scope, IContent content, bool checkPath, + IReadOnlyList culturesPublishing, + IReadOnlyCollection culturesUnpublishing, EventMessages evtMsgs, + IReadOnlyCollection allLangs, IDictionary notificationState) { // raise Publishing notification - if (scope.Notifications.PublishCancelable(new ContentPublishingNotification(content, evtMsgs).WithState(notificationState))) + if (scope.Notifications.PublishCancelable( + new ContentPublishingNotification(content, evtMsgs).WithState(notificationState))) { - _logger.LogInformation("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "publishing was cancelled"); + _logger.LogInformation("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", + content.Name, content.Id, "publishing was cancelled"); return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content); } var variesByCulture = content.ContentType.VariesByCulture(); - var impactsToPublish = culturesPublishing == null + CultureImpact[] impactsToPublish = culturesPublishing == null ? new[] { CultureImpact.Invariant } //if it's null it's invariant - : culturesPublishing.Select(x => CultureImpact.Explicit(x, allLangs.Any(lang => lang.IsoCode.InvariantEquals(x) && lang.IsMandatory))).ToArray(); + : culturesPublishing.Select(x => + CultureImpact.Explicit(x, + allLangs.Any(lang => lang.IsoCode.InvariantEquals(x) && lang.IsMandatory))).ToArray(); // publish the culture(s) if (!impactsToPublish.All(content.PublishCulture)) + { return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, content); + } //validate the property values IProperty[] invalidProperties = null; - if (!impactsToPublish.All(x => _propertyValidationService.Value.IsPropertyDataValid(content, out invalidProperties, x))) + if (!impactsToPublish.All(x => + _propertyValidationService.Value.IsPropertyDataValid(content, out invalidProperties, x))) + { return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, content) { InvalidProperties = invalidProperties }; + } //Check if mandatory languages fails, if this fails it will mean anything that the published flag on the document will // be changed to Unpublished and any culture currently published will not be visible. if (variesByCulture) { if (culturesPublishing == null) - throw new InvalidOperationException("Internal error, variesByCulture but culturesPublishing is null."); + { + throw new InvalidOperationException( + "Internal error, variesByCulture but culturesPublishing is null."); + } if (content.Published && culturesPublishing.Count == 0 && culturesUnpublishing.Count == 0) { @@ -2621,25 +3033,31 @@ private PublishResult StrategyCanPublish(IScope scope, IContent content, bool ch // missing mandatory culture = cannot be published - var mandatoryCultures = allLangs.Where(x => x.IsMandatory).Select(x => x.IsoCode); - var mandatoryMissing = mandatoryCultures.Any(x => !content.PublishedCultures.Contains(x, StringComparer.OrdinalIgnoreCase)); + IEnumerable mandatoryCultures = allLangs.Where(x => x.IsMandatory).Select(x => x.IsoCode); + var mandatoryMissing = mandatoryCultures.Any(x => + !content.PublishedCultures.Contains(x, StringComparer.OrdinalIgnoreCase)); if (mandatoryMissing) + { return new PublishResult(PublishResultType.FailedPublishMandatoryCultureMissing, evtMsgs, content); + } if (culturesPublishing.Count == 0 && culturesUnpublishing.Count > 0) + { return new PublishResult(PublishResultType.SuccessUnpublishCulture, evtMsgs, content); + } } // ensure that the document has published values // either because it is 'publishing' or because it already has a published version if (content.PublishedState != PublishedState.Publishing && content.PublishedVersionId == 0) { - _logger.LogInformation("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "document does not have published values"); + _logger.LogInformation("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", + content.Name, content.Id, "document does not have published values"); return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content); } //loop over each culture publishing - or string.Empty for invariant - foreach (var culture in culturesPublishing ?? (new[] { string.Empty })) + foreach (var culture in culturesPublishing ?? new[] { string.Empty }) { // ensure that the document status is correct // note: culture will be string.Empty for invariant @@ -2647,20 +3065,45 @@ private PublishResult StrategyCanPublish(IScope scope, IContent content, bool ch { case ContentStatus.Expired: if (!variesByCulture) - _logger.LogInformation("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "document has expired"); + { + _logger.LogInformation( + "Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, + content.Id, "document has expired"); + } else - _logger.LogInformation("Document {ContentName} (id={ContentId}) culture {Culture} cannot be published: {Reason}", content.Name, content.Id, culture, "document culture has expired"); - return new PublishResult(!variesByCulture ? PublishResultType.FailedPublishHasExpired : PublishResultType.FailedPublishCultureHasExpired, evtMsgs, content); + { + _logger.LogInformation( + "Document {ContentName} (id={ContentId}) culture {Culture} cannot be published: {Reason}", + content.Name, content.Id, culture, "document culture has expired"); + } + + return new PublishResult( + !variesByCulture + ? PublishResultType.FailedPublishHasExpired + : PublishResultType.FailedPublishCultureHasExpired, evtMsgs, content); case ContentStatus.AwaitingRelease: if (!variesByCulture) - _logger.LogInformation("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "document is awaiting release"); + { + _logger.LogInformation( + "Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, + content.Id, "document is awaiting release"); + } else - _logger.LogInformation("Document {ContentName} (id={ContentId}) culture {Culture} cannot be published: {Reason}", content.Name, content.Id, culture, "document is culture awaiting release"); - return new PublishResult(!variesByCulture ? PublishResultType.FailedPublishAwaitingRelease : PublishResultType.FailedPublishCultureAwaitingRelease, evtMsgs, content); + { + _logger.LogInformation( + "Document {ContentName} (id={ContentId}) culture {Culture} cannot be published: {Reason}", + content.Name, content.Id, culture, "document is culture awaiting release"); + } + + return new PublishResult( + !variesByCulture + ? PublishResultType.FailedPublishAwaitingRelease + : PublishResultType.FailedPublishCultureAwaitingRelease, evtMsgs, content); case ContentStatus.Trashed: - _logger.LogInformation("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "document is trashed"); + _logger.LogInformation("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", + content.Name, content.Id, "document is trashed"); return new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, content); } } @@ -2670,23 +3113,26 @@ private PublishResult StrategyCanPublish(IScope scope, IContent content, bool ch // check if the content can be path-published // root content can be published // else check ancestors - we know we are not trashed - var pathIsOk = content.ParentId == Cms.Core.Constants.System.Root || IsPathPublished(GetParent(content)); + var pathIsOk = content.ParentId == Constants.System.Root || IsPathPublished(GetParent(content)); if (!pathIsOk) { - _logger.LogInformation("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "parent is not published"); + _logger.LogInformation("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", + content.Name, content.Id, "parent is not published"); return new PublishResult(PublishResultType.FailedPublishPathNotPublished, evtMsgs, content); } } //If we are both publishing and unpublishing cultures, then return a mixed status if (variesByCulture && culturesPublishing.Count > 0 && culturesUnpublishing.Count > 0) + { return new PublishResult(PublishResultType.SuccessMixedCulture, evtMsgs, content); + } return new PublishResult(evtMsgs, content); } /// - /// Publishes a document + /// Publishes a document /// /// /// @@ -2694,7 +3140,8 @@ private PublishResult StrategyCanPublish(IScope scope, IContent content, bool ch /// /// /// - /// It is assumed that all publishing checks have passed before calling this method like + /// It is assumed that all publishing checks have passed before calling this method like + /// /// private PublishResult StrategyPublish(IContent content, IReadOnlyCollection culturesPublishing, IReadOnlyCollection culturesUnpublishing, @@ -2707,31 +3154,44 @@ private PublishResult StrategyPublish(IContent content, if (content.ContentType.VariesByCulture()) { if (content.Published && culturesUnpublishing.Count == 0 && culturesPublishing.Count == 0) + { return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content); + } if (culturesUnpublishing.Count > 0) - _logger.LogInformation("Document {ContentName} (id={ContentId}) cultures: {Cultures} have been unpublished.", + { + _logger.LogInformation( + "Document {ContentName} (id={ContentId}) cultures: {Cultures} have been unpublished.", content.Name, content.Id, string.Join(",", culturesUnpublishing)); + } if (culturesPublishing.Count > 0) - _logger.LogInformation("Document {ContentName} (id={ContentId}) cultures: {Cultures} have been published.", + { + _logger.LogInformation( + "Document {ContentName} (id={ContentId}) cultures: {Cultures} have been published.", content.Name, content.Id, string.Join(",", culturesPublishing)); + } if (culturesUnpublishing.Count > 0 && culturesPublishing.Count > 0) + { return new PublishResult(PublishResultType.SuccessMixedCulture, evtMsgs, content); + } if (culturesUnpublishing.Count > 0 && culturesPublishing.Count == 0) + { return new PublishResult(PublishResultType.SuccessUnpublishCulture, evtMsgs, content); + } return new PublishResult(PublishResultType.SuccessPublishCulture, evtMsgs, content); } - _logger.LogInformation("Document {ContentName} (id={ContentId}) has been published.", content.Name, content.Id); + _logger.LogInformation("Document {ContentName} (id={ContentId}) has been published.", content.Name, + content.Id); return new PublishResult(evtMsgs, content); } /// - /// Ensures that a document can be unpublished + /// Ensures that a document can be unpublished /// /// /// @@ -2742,7 +3202,9 @@ private PublishResult StrategyCanUnpublish(IScope scope, IContent content, Event // raise Unpublishing notification if (scope.Notifications.PublishCancelable(new ContentUnpublishingNotification(content, evtMsgs))) { - _logger.LogInformation("Document {ContentName} (id={ContentId}) cannot be unpublished: unpublishing was cancelled.", content.Name, content.Id); + _logger.LogInformation( + "Document {ContentName} (id={ContentId}) cannot be unpublished: unpublishing was cancelled.", + content.Name, content.Id); return new PublishResult(PublishResultType.FailedUnpublishCancelledByEvent, evtMsgs, content); } @@ -2750,13 +3212,14 @@ private PublishResult StrategyCanUnpublish(IScope scope, IContent content, Event } /// - /// Unpublishes a document + /// Unpublishes a document /// /// /// /// /// - /// It is assumed that all unpublishing checks have passed before calling this method like + /// It is assumed that all unpublishing checks have passed before calling this method like + /// /// private PublishResult StrategyUnpublish(IContent content, EventMessages evtMsgs) { @@ -2764,22 +3227,33 @@ private PublishResult StrategyUnpublish(IContent content, EventMessages evtMsgs) //TODO: What is this check?? we just created this attempt and of course it is Success?! if (attempt.Success == false) + { return attempt; + } // if the document has any release dates set to before now, // they should be removed so they don't interrupt an unpublish // otherwise it would remain released == published - var pastReleases = content.ContentSchedule.GetPending(ContentScheduleAction.Expire, DateTime.Now); - foreach (var p in pastReleases) + IReadOnlyList pastReleases = + content.ContentSchedule.GetPending(ContentScheduleAction.Expire, DateTime.Now); + foreach (ContentSchedule p in pastReleases) + { content.ContentSchedule.Remove(p); + } + if (pastReleases.Count > 0) - _logger.LogInformation("Document {ContentName} (id={ContentId}) had its release date removed, because it was unpublished.", content.Name, content.Id); + { + _logger.LogInformation( + "Document {ContentName} (id={ContentId}) had its release date removed, because it was unpublished.", + content.Name, content.Id); + } // change state to unpublishing content.PublishedState = PublishedState.Unpublishing; - _logger.LogInformation("Document {ContentName} (id={ContentId}) has been unpublished.", content.Name, content.Id); + _logger.LogInformation("Document {ContentName} (id={ContentId}) has been unpublished.", content.Name, + content.Id); return attempt; } @@ -2788,16 +3262,18 @@ private PublishResult StrategyUnpublish(IContent content, EventMessages evtMsgs) #region Content Types /// - /// Deletes all content of specified type. All children of deleted content is moved to Recycle Bin. + /// Deletes all content of specified type. All children of deleted content is moved to Recycle Bin. /// /// - /// This needs extra care and attention as its potentially a dangerous and extensive operation. - /// Deletes content items of the specified type, and only that type. Does *not* handle content types - /// inheritance and compositions, which need to be managed outside of this method. + /// This needs extra care and attention as its potentially a dangerous and extensive operation. + /// + /// Deletes content items of the specified type, and only that type. Does *not* handle content types + /// inheritance and compositions, which need to be managed outside of this method. + /// /// - /// Id of the + /// Id of the /// Optional Id of the user issuing the delete operation - public void DeleteOfTypes(IEnumerable contentTypeIds, int userId = Cms.Core.Constants.Security.SuperUserId) + public void DeleteOfTypes(IEnumerable contentTypeIds, int userId = Constants.Security.SuperUserId) { // 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. @@ -2817,7 +3293,7 @@ public void DeleteOfTypes(IEnumerable contentTypeIds, int userId = Cms.Core // using (IScope scope = ScopeProvider.CreateScope()) { - scope.WriteLock(Cms.Core.Constants.Locks.ContentTree); + scope.WriteLock(Constants.Locks.ContentTree); IQuery query = Query().WhereIn(x => x.ContentTypeId, contentTypeIdsA); IContent[] contents = _documentRepository.Get(query).ToArray(); @@ -2847,7 +3323,7 @@ public void DeleteOfTypes(IEnumerable contentTypeIds, int userId = Cms.Core foreach (IContent child in children) { // see MoveToRecycleBin - PerformMoveLocked(child, Cms.Core.Constants.System.RecycleBinContent, null, userId, moves, true); + PerformMoveLocked(child, Constants.System.RecycleBinContent, null, userId, moves, true); changes.Add(new TreeChange(content, TreeChangeTypes.RefreshBranch)); } @@ -2864,47 +3340,66 @@ public void DeleteOfTypes(IEnumerable contentTypeIds, int userId = Cms.Core { scope.Notifications.Publish(new ContentMovedToRecycleBinNotification(moveInfos, eventMessages)); } + scope.Notifications.Publish(new ContentTreeChangeNotification(changes, eventMessages)); - Audit(AuditType.Delete, userId, Cms.Core.Constants.System.Root, $"Delete content of type {string.Join(",", contentTypeIdsA)}"); + Audit(AuditType.Delete, userId, Constants.System.Root, + $"Delete content of type {string.Join(",", contentTypeIdsA)}"); scope.Complete(); } } /// - /// Deletes all content items of specified type. All children of deleted content item is moved to Recycle Bin. + /// Deletes all content items of specified type. All children of deleted content item is moved to Recycle Bin. /// /// This needs extra care and attention as its potentially a dangerous and extensive operation - /// Id of the + /// Id of the /// Optional id of the user deleting the media - public void DeleteOfType(int contentTypeId, int userId = Cms.Core.Constants.Security.SuperUserId) - { + public void DeleteOfType(int contentTypeId, int userId = Constants.Security.SuperUserId) => DeleteOfTypes(new[] { contentTypeId }, userId); - } private IContentType GetContentType(IScope scope, string contentTypeAlias) { - if (contentTypeAlias == null) throw new ArgumentNullException(nameof(contentTypeAlias)); - if (string.IsNullOrWhiteSpace(contentTypeAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(contentTypeAlias)); + if (contentTypeAlias == null) + { + throw new ArgumentNullException(nameof(contentTypeAlias)); + } - scope.ReadLock(Cms.Core.Constants.Locks.ContentTypes); + if (string.IsNullOrWhiteSpace(contentTypeAlias)) + { + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", + nameof(contentTypeAlias)); + } - var query = Query().Where(x => x.Alias == contentTypeAlias); - var contentType = _contentTypeRepository.Get(query).FirstOrDefault(); + scope.ReadLock(Constants.Locks.ContentTypes); + + IQuery query = Query().Where(x => x.Alias == contentTypeAlias); + IContentType contentType = _contentTypeRepository.Get(query).FirstOrDefault(); if (contentType == null) - throw new Exception($"No ContentType matching the passed in Alias: '{contentTypeAlias}' was found"); // causes rollback + { + throw new Exception( + $"No ContentType matching the passed in Alias: '{contentTypeAlias}' was found"); // causes rollback + } return contentType; } private IContentType GetContentType(string contentTypeAlias) { - if (contentTypeAlias == null) throw new ArgumentNullException(nameof(contentTypeAlias)); - if (string.IsNullOrWhiteSpace(contentTypeAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(contentTypeAlias)); + if (contentTypeAlias == null) + { + throw new ArgumentNullException(nameof(contentTypeAlias)); + } - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + if (string.IsNullOrWhiteSpace(contentTypeAlias)) + { + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", + nameof(contentTypeAlias)); + } + + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { return GetContentType(scope, contentTypeAlias); } @@ -2916,51 +3411,61 @@ private IContentType GetContentType(string contentTypeAlias) public IContent GetBlueprintById(int id) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { - scope.ReadLock(Cms.Core.Constants.Locks.ContentTree); - var blueprint = _documentBlueprintRepository.Get(id); + scope.ReadLock(Constants.Locks.ContentTree); + IContent blueprint = _documentBlueprintRepository.Get(id); if (blueprint != null) + { blueprint.Blueprint = true; + } + return blueprint; } } public IContent GetBlueprintById(Guid id) { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) { - scope.ReadLock(Cms.Core.Constants.Locks.ContentTree); - var blueprint = _documentBlueprintRepository.Get(id); + scope.ReadLock(Constants.Locks.ContentTree); + IContent blueprint = _documentBlueprintRepository.Get(id); if (blueprint != null) + { blueprint.Blueprint = true; + } + return blueprint; } } - public void SaveBlueprint(IContent content, int userId = Cms.Core.Constants.Security.SuperUserId) + public void SaveBlueprint(IContent content, int userId = Constants.Security.SuperUserId) { - var evtMsgs = EventMessagesFactory.Get(); + EventMessages evtMsgs = EventMessagesFactory.Get(); //always ensure the blueprint is at the root if (content.ParentId != -1) + { content.ParentId = -1; + } content.Blueprint = true; - using (var scope = ScopeProvider.CreateScope()) + using (IScope scope = ScopeProvider.CreateScope()) { - scope.WriteLock(Cms.Core.Constants.Locks.ContentTree); + scope.WriteLock(Constants.Locks.ContentTree); if (content.HasIdentity == false) { content.CreatorId = userId; } + content.WriterId = userId; _documentBlueprintRepository.Save(content); - Audit(AuditType.Save, Cms.Core.Constants.Security.SuperUserId, content.Id, $"Saved content template: {content.Name}"); + Audit(AuditType.Save, Constants.Security.SuperUserId, content.Id, + $"Saved content template: {content.Name}"); scope.Notifications.Publish(new ContentSavedBlueprintNotification(content, evtMsgs)); @@ -2968,13 +3473,13 @@ public void SaveBlueprint(IContent content, int userId = Cms.Core.Constants.Secu } } - public void DeleteBlueprint(IContent content, int userId = Cms.Core.Constants.Security.SuperUserId) + public void DeleteBlueprint(IContent content, int userId = Constants.Security.SuperUserId) { - var evtMsgs = EventMessagesFactory.Get(); + EventMessages evtMsgs = EventMessagesFactory.Get(); - using (var scope = ScopeProvider.CreateScope()) + using (IScope scope = ScopeProvider.CreateScope()) { - scope.WriteLock(Cms.Core.Constants.Locks.ContentTree); + scope.WriteLock(Constants.Locks.ContentTree); _documentBlueprintRepository.Delete(content); scope.Notifications.Publish(new ContentDeletedBlueprintNotification(content, evtMsgs)); scope.Complete(); @@ -2983,11 +3488,15 @@ public void DeleteBlueprint(IContent content, int userId = Cms.Core.Constants.Se private static readonly string[] ArrayOfOneNullString = { null }; - public IContent CreateContentFromBlueprint(IContent blueprint, string name, int userId = Cms.Core.Constants.Security.SuperUserId) + public IContent CreateContentFromBlueprint(IContent blueprint, string name, + int userId = Constants.Security.SuperUserId) { - if (blueprint == null) throw new ArgumentNullException(nameof(blueprint)); + if (blueprint == null) + { + throw new ArgumentNullException(nameof(blueprint)); + } - var contentType = GetContentType(blueprint.ContentType.Alias); + IContentType contentType = GetContentType(blueprint.ContentType.Alias); var content = new Content(name, -1, contentType); content.Path = string.Concat(content.ParentId.ToString(), ",", content.Id); @@ -2998,9 +3507,10 @@ public IContent CreateContentFromBlueprint(IContent blueprint, string name, int if (blueprint.CultureInfos.Count > 0) { cultures = blueprint.CultureInfos.Values.Select(x => x.Culture); - using (var scope = ScopeProvider.CreateScope()) + using (IScope scope = ScopeProvider.CreateScope()) { - if (blueprint.CultureInfos.TryGetValue(_languageRepository.GetDefaultIsoCode(), out var defaultCulture)) + if (blueprint.CultureInfos.TryGetValue(_languageRepository.GetDefaultIsoCode(), + out ContentCultureInfos defaultCulture)) { defaultCulture.Name = name; } @@ -3009,10 +3519,10 @@ public IContent CreateContentFromBlueprint(IContent blueprint, string name, int } } - var now = DateTime.Now; + DateTime now = DateTime.Now; foreach (var culture in cultures) { - foreach (var property in blueprint.Properties) + foreach (IProperty property in blueprint.Properties) { var propertyCulture = property.PropertyType.VariesByCulture() ? culture : null; content.SetValue(property.Alias, property.GetValue(propertyCulture), propertyCulture); @@ -3031,11 +3541,12 @@ public IEnumerable GetBlueprintsForContentTypes(params int[] contentTy { using (ScopeProvider.CreateScope(autoComplete: true)) { - var query = Query(); + IQuery query = Query(); if (contentTypeId.Length > 0) { query.Where(x => contentTypeId.Contains(x.ContentTypeId)); } + return _documentBlueprintRepository.Get(query).Select(x => { x.Blueprint = true; @@ -3044,26 +3555,29 @@ public IEnumerable GetBlueprintsForContentTypes(params int[] contentTy } } - public void DeleteBlueprintsOfTypes(IEnumerable contentTypeIds, int userId = Cms.Core.Constants.Security.SuperUserId) + public void DeleteBlueprintsOfTypes(IEnumerable contentTypeIds, + int userId = Constants.Security.SuperUserId) { - var evtMsgs = EventMessagesFactory.Get(); + EventMessages evtMsgs = EventMessagesFactory.Get(); - using (var scope = ScopeProvider.CreateScope()) + using (IScope scope = ScopeProvider.CreateScope()) { - scope.WriteLock(Cms.Core.Constants.Locks.ContentTree); + scope.WriteLock(Constants.Locks.ContentTree); var contentTypeIdsA = contentTypeIds.ToArray(); - var query = Query(); + IQuery query = Query(); if (contentTypeIdsA.Length > 0) + { query.Where(x => contentTypeIdsA.Contains(x.ContentTypeId)); + } - var blueprints = _documentBlueprintRepository.Get(query).Select(x => + IContent[] blueprints = _documentBlueprintRepository.Get(query).Select(x => { x.Blueprint = true; return x; }).ToArray(); - foreach (var blueprint in blueprints) + foreach (IContent blueprint in blueprints) { _documentBlueprintRepository.Delete(blueprint); } @@ -3073,69 +3587,8 @@ public void DeleteBlueprintsOfTypes(IEnumerable contentTypeIds, int userId } } - public void DeleteBlueprintsOfType(int contentTypeId, int userId = Cms.Core.Constants.Security.SuperUserId) - { + public void DeleteBlueprintsOfType(int contentTypeId, int userId = Constants.Security.SuperUserId) => DeleteBlueprintsOfTypes(new[] { contentTypeId }, userId); - } - - #endregion - - #region Rollback - - public OperationResult Rollback(int id, int versionId, string culture = "*", int userId = Cms.Core.Constants.Security.SuperUserId) - { - var evtMsgs = EventMessagesFactory.Get(); - - //Get the current copy of the node - var content = GetById(id); - - //Get the version - var version = GetVersion(versionId); - - //Good ole null checks - if (content == null || version == null || content.Trashed) - { - return new OperationResult(OperationResultType.FailedCannot, evtMsgs); - } - - //Store the result of doing the save of content for the rollback - OperationResult rollbackSaveResult; - - using (var scope = ScopeProvider.CreateScope()) - { - var rollingBackNotification = new ContentRollingBackNotification(content, evtMsgs); - if (scope.Notifications.PublishCancelable(rollingBackNotification)) - { - scope.Complete(); - return OperationResult.Cancel(evtMsgs); - } - - //Copy the changes from the version - content.CopyFrom(version, culture); - - //Save the content for the rollback - rollbackSaveResult = Save(content, userId); - - //Depending on the save result - is what we log & audit along with what we return - if (rollbackSaveResult.Success == false) - { - //Log the error/warning - _logger.LogError("User '{UserId}' was unable to rollback content '{ContentId}' to version '{VersionId}'", userId, id, versionId); - } - else - { - scope.Notifications.Publish(new ContentRolledBackNotification(content, evtMsgs).WithStateFrom(rollingBackNotification)); - - //Logging & Audit message - _logger.LogInformation("User '{UserId}' rolled back content '{ContentId}' to version '{VersionId}'", userId, id, versionId); - Audit(AuditType.RollBack, userId, id, $"Content '{content.Name}' was rolled back to version '{versionId}'"); - } - - scope.Complete(); - } - - return rollbackSaveResult; - } #endregion } diff --git a/src/Umbraco.Infrastructure/Services/Implement/ContentVersionService.cs b/src/Umbraco.Infrastructure/Services/Implement/ContentVersionService.cs new file mode 100644 index 000000000000..40c12a1fe53c --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Implement/ContentVersionService.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Services.Implement +{ + internal class ContentVersionService : IContentVersionService + { + private readonly ILogger _logger; + private readonly IDocumentVersionRepository _documentVersionRepository; + private readonly IContentVersionCleanupPolicy _contentVersionCleanupPolicy; + private readonly IScopeProvider _scopeProvider; + private readonly IEventMessagesFactory _eventMessagesFactory; + private readonly IAuditRepository _auditRepository; + + public ContentVersionService( + ILogger logger, + IDocumentVersionRepository documentVersionRepository, + IContentVersionCleanupPolicy contentVersionCleanupPolicy, + IScopeProvider scopeProvider, + IEventMessagesFactory eventMessagesFactory, + IAuditRepository auditRepository) + { + _logger = logger; + _documentVersionRepository = documentVersionRepository; + _contentVersionCleanupPolicy = contentVersionCleanupPolicy; + _scopeProvider = scopeProvider; + _eventMessagesFactory = eventMessagesFactory; + _auditRepository = auditRepository; + } + + /// + /// + /// In v9 this can live in another class as we publish the notifications via IEventAggregator. + /// But for v8 must be here for access to the static events. + /// + public IReadOnlyCollection PerformContentVersionCleanup(DateTime asAtDate) => + CleanupDocumentVersions(asAtDate); + + // Media - ignored + // Members - ignored + /// + /// v9 - move to another class + /// + private IReadOnlyCollection CleanupDocumentVersions(DateTime asAtDate) + { + List versionsToDelete; + + /* Why so many scopes? + * + * We could just work out the set to delete at SQL infra level which was the original plan, however we agreed that really we should fire + * ContentService.DeletingVersions so people can hook & cancel if required. + * + * On first time run of cleanup on a site with a lot of history there may be a lot of historic ContentVersions to remove e.g. 200K for our.umbraco.com. + * If we weren't supporting SQL CE we could do TVP, or use temp tables to bulk delete with joins to our list of version ids to nuke. + * (much nicer, we can kill 100k in sub second time-frames). + * + * However we are supporting SQL CE, so the easiest thing to do is use the Umbraco InGroupsOf helper to create a query with 2K args of version + * ids to delete at a time. + * + * This is already done at the repository level, however if we only had a single scope at service level we're still locking + * the ContentVersions table (and other related tables) for a couple of minutes which makes the back office unusable. + * + * As a quick fix, we can also use InGroupsOf at service level, create a scope per group to give other connections a chance + * to grab the locks and execute their queries. + * + * This makes the back office a tiny bit sluggish during first run but it is usable for loading tree and publishing content. + * + * There are optimizations we can do, we could add a bulk delete for SqlServerSyntaxProvider which differs in implementation + * and fallback to this naive approach only for SQL CE, however we agreed it is not worth the effort as this is a one time pain, + * subsequent runs shouldn't have huge numbers of versions to cleanup. + * + * tl;dr lots of scopes to enable other connections to use the DB whilst we work. + */ + using (IScope scope = _scopeProvider.CreateScope(autoComplete: true)) + { + var allHistoricVersions = _documentVersionRepository.GetDocumentVersionsEligibleForCleanup(); + + _logger.LogDebug("Discovered {count} candidate(s) for ContentVersion cleanup", allHistoricVersions.Count); + versionsToDelete = new List(allHistoricVersions.Count); + + var filteredContentVersions = _contentVersionCleanupPolicy.Apply(asAtDate, allHistoricVersions); + + foreach (var version in filteredContentVersions) + { + EventMessages evtMsgs = _eventMessagesFactory.Get(); + + if (scope.Notifications.PublishCancelable(new ContentDeletingVersionsNotification(version.ContentId, evtMsgs, version.VersionId))) + { + _logger.LogDebug("Delete cancelled for ContentVersion [{versionId}]", version.VersionId); + continue; + } + + versionsToDelete.Add(version); + } + } + + if (!versionsToDelete.Any()) + { + _logger.LogDebug("No remaining ContentVersions for cleanup", versionsToDelete.Count); + return Array.Empty(); + } + + _logger.LogDebug("Removing {count} ContentVersion(s)", versionsToDelete.Count); + + foreach (var group in versionsToDelete.InGroupsOf(Constants.Sql.MaxParameterCount)) + { + using (IScope scope = _scopeProvider.CreateScope(autoComplete: true)) + { + scope.WriteLock(Constants.Locks.ContentTree); + var groupEnumerated = group.ToList(); + _documentVersionRepository.DeleteVersions(groupEnumerated.Select(x => x.VersionId)); + + foreach (var version in groupEnumerated) + { + EventMessages evtMsgs = _eventMessagesFactory.Get(); + + scope.Notifications.Publish( + new ContentDeletedVersionsNotification(version.ContentId, evtMsgs, version.VersionId)); + } + } + } + + using (IScope scope = _scopeProvider.CreateScope(autoComplete: true)) + { + Audit(AuditType.Delete, Constants.Security.SuperUserId, -1, $"Removed {versionsToDelete.Count} ContentVersion(s) according to cleanup policy"); + } + + return versionsToDelete; + } + + private void Audit(AuditType type, int userId, int objectId, string message = null, string parameters = null) => + _auditRepository.Save(new AuditItem(objectId, type, userId, UmbracoObjectTypes.Document.GetName(), message, + parameters)); + } +} diff --git a/src/Umbraco.Infrastructure/Services/Implement/DefaultContentVersionCleanupPolicy.cs b/src/Umbraco.Infrastructure/Services/Implement/DefaultContentVersionCleanupPolicy.cs new file mode 100644 index 000000000000..383554c34c68 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Implement/DefaultContentVersionCleanupPolicy.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services; +using ContentVersionCleanupPolicySettings = Umbraco.Cms.Core.Models.ContentVersionCleanupPolicySettings; + +namespace Umbraco.Cms.Infrastructure.Services.Implement +{ + public class DefaultContentVersionCleanupPolicy : IContentVersionCleanupPolicy + { + private readonly IOptions _contentSettings; + private readonly IScopeProvider _scopeProvider; + private readonly IDocumentVersionRepository _documentVersionRepository; + + public DefaultContentVersionCleanupPolicy(IOptions contentSettings, IScopeProvider scopeProvider, IDocumentVersionRepository documentVersionRepository) + { + _contentSettings = contentSettings ?? throw new ArgumentNullException(nameof(contentSettings)); + _scopeProvider = scopeProvider ?? throw new ArgumentNullException(nameof(scopeProvider)); + _documentVersionRepository = documentVersionRepository ?? throw new ArgumentNullException(nameof(documentVersionRepository)); + } + + public IEnumerable Apply(DateTime asAtDate, IEnumerable items) + { + // Note: Not checking global enable flag, that's handled in the scheduled job. + // If this method is called and policy is globally disabled someone has chosen to run in code. + + var globalPolicy = _contentSettings.Value.ContentVersionCleanupPolicy; + + var theRest = new List(); + + using(_scopeProvider.CreateScope(autoComplete: true)) + { + var policyOverrides = _documentVersionRepository.GetCleanupPolicies() + .ToDictionary(x => x.ContentTypeId); + + foreach (var version in items) + { + var age = asAtDate - version.VersionDate; + + var overrides = GetOverridePolicy(version, policyOverrides); + + var keepAll = overrides?.KeepAllVersionsNewerThanDays ?? globalPolicy.KeepAllVersionsNewerThanDays!; + var keepLatest = overrides?.KeepLatestVersionPerDayForDays ?? globalPolicy.KeepLatestVersionPerDayForDays; + var preventCleanup = overrides?.PreventCleanup ?? false; + + if (preventCleanup) + { + continue; + } + + if (age.TotalDays <= keepAll) + { + continue; + } + + if (age.TotalDays > keepLatest) + { + + yield return version; + continue; + } + + theRest.Add(version); + } + + var grouped = theRest.GroupBy(x => new + { + x.ContentId, + x.VersionDate.Date + }); + + foreach (var group in grouped) + { + yield return group.OrderByDescending(x => x.VersionId).First(); + } + } + } + + private ContentVersionCleanupPolicySettings GetOverridePolicy( + HistoricContentVersionMeta version, + IDictionary overrides) + { + _ = overrides.TryGetValue(version.ContentTypeId, out var value); + + return value; + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs index 58f3622e67a8..43723207d39f 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs @@ -413,6 +413,7 @@ internal async Task> GetServerVariablesAsync() {"showAllowSegmentationForDocumentTypes", false}, {"minimumPasswordLength", _memberPasswordConfigurationSettings.RequiredLength}, {"minimumPasswordNonAlphaNum", _memberPasswordConfigurationSettings.GetMinNonAlphaNumericChars()}, + {"sanitizeTinyMce", _globalSettings.SanitizeTinyMce} } }, { diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs index b78ac1fdfdd0..828296901393 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs @@ -590,35 +590,43 @@ public ActionResult Upload(List file) var root = _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempFileUploads); var tempPath = Path.Combine(root,fileName); - - using (var stream = System.IO.File.Create(tempPath)) - { - formFile.CopyToAsync(stream).GetAwaiter().GetResult(); - } - - if (ext.InvariantEquals("udt")) + if (Path.GetFullPath(tempPath).StartsWith(Path.GetFullPath(root))) { - model.TempFileName = Path.Combine(root, fileName); + using (var stream = System.IO.File.Create(tempPath)) + { + formFile.CopyToAsync(stream).GetAwaiter().GetResult(); + } - var xd = new XmlDocument + if (ext.InvariantEquals("udt")) { - XmlResolver = null - }; - xd.Load(model.TempFileName); + model.TempFileName = Path.Combine(root, fileName); + + var xd = new XmlDocument + { + XmlResolver = null + }; + xd.Load(model.TempFileName); - model.Alias = xd.DocumentElement?.SelectSingleNode("//DocumentType/Info/Alias")?.FirstChild.Value; - model.Name = xd.DocumentElement?.SelectSingleNode("//DocumentType/Info/Name")?.FirstChild.Value; + model.Alias = xd.DocumentElement?.SelectSingleNode("//DocumentType/Info/Alias")?.FirstChild.Value; + model.Name = xd.DocumentElement?.SelectSingleNode("//DocumentType/Info/Name")?.FirstChild.Value; + } + else + { + model.Notifications.Add(new BackOfficeNotification( + _localizedTextService.Localize("speechBubbles","operationFailedHeader"), + _localizedTextService.Localize("media","disallowedFileType"), + NotificationStyle.Warning)); + } } else { model.Notifications.Add(new BackOfficeNotification( - _localizedTextService.Localize("speechBubbles","operationFailedHeader"), - _localizedTextService.Localize("media","disallowedFileType"), + _localizedTextService.Localize("speechBubbles", "operationFailedHeader"), + _localizedTextService.Localize("media", "invalidFileName"), NotificationStyle.Warning)); } - } - + } return model; diff --git a/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs b/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs index 91af3724343f..c3f36e92cbbb 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs @@ -204,7 +204,7 @@ public IDictionary SearchAll(string query) var allowedSections = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.AllowedSections.ToArray(); foreach (KeyValuePair searchableTree in _searchableTreeCollection - .SearchableApplicationTrees.OrderBy(t => t.Value.SortOrder)) + .SearchableApplicationTrees.OrderBy(t => t.Value.SortOrder)) { if (allowedSections.Contains(searchableTree.Value.AppAlias)) { @@ -1026,6 +1026,15 @@ private ActionResult GetResultForKey(Guid key, UmbracoEntityTypes e case UmbracoEntityTypes.Macro: + case UmbracoEntityTypes.Template: + ITemplate template = _fileService.GetTemplate(key); + if (template is null) + { + return NotFound(); + } + + return _umbracoMapper.Map(template); + default: throw new NotSupportedException("The " + typeof(EntityController) + " does not currently support data for the type " + entityType); @@ -1059,6 +1068,15 @@ private ActionResult GetResultForId(int id, UmbracoEntityTypes enti case UmbracoEntityTypes.Macro: + case UmbracoEntityTypes.Template: + ITemplate template = _fileService.GetTemplate(id); + if (template is null) + { + return NotFound(); + } + + return _umbracoMapper.Map(template); + default: throw new NotSupportedException("The " + typeof(EntityController) + " does not currently support data for the type " + entityType); @@ -1429,7 +1447,7 @@ private IEnumerable GetAllDictionaryItems() var list = new List(); foreach (IDictionaryItem dictionaryItem in _localizationService.GetRootDictionaryItems() - .OrderBy(DictionaryItemSort())) + .OrderBy(DictionaryItemSort())) { EntityBasic item = _umbracoMapper.Map(dictionaryItem); list.Add(item); @@ -1444,7 +1462,7 @@ private IEnumerable GetAllDictionaryItems() private void GetChildItemsForList(IDictionaryItem dictionaryItem, ICollection list) { foreach (IDictionaryItem childItem in _localizationService.GetDictionaryItemChildren(dictionaryItem.Key) - .OrderBy(DictionaryItemSort())) + .OrderBy(DictionaryItemSort())) { EntityBasic item = _umbracoMapper.Map(childItem); list.Add(item); diff --git a/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs b/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs index 310b1142c08d..a4001ce79ff4 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs @@ -9,8 +9,6 @@ using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Web.BackOffice.Extensions; -using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Extensions; @@ -23,7 +21,6 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// Backoffice controller supporting the dashboard for language administration. /// [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] - //[PrefixlessBodyModelValidator] public class LanguageController : UmbracoAuthorizedJsonController { private readonly ILocalizationService _localizationService; @@ -51,10 +48,12 @@ public IDictionary GetAllCultures() // (see notes in Language class about culture info names) // TODO: Fix this requirement, see https://github.com/umbraco/Umbraco-CMS/issues/3623 return CultureInfo.GetCultures(CultureTypes.AllCultures) - .Where(x => !x.Name.IsNullOrWhiteSpace()) - .Select(x => new CultureInfo(x.Name)) // important! - .OrderBy(x => x.EnglishName) - .ToDictionary(x => x.Name, x => x.EnglishName); + .Select(x=>x.Name) + .Distinct() + .Where(x => !x.IsNullOrWhiteSpace()) + .Select(x => new CultureInfo(x)) // important! + .OrderBy(x => x.EnglishName) + .ToDictionary(x => x.Name, x => x.EnglishName); } /// diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index 2d584f198ea5..e3e3462e6a0d 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -176,6 +176,7 @@ public static IUmbracoBuilder AddHostedServices(this IUmbracoBuilder builder) builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); + builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index b282b469a36d..a9b706a67761 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -8715,9 +8715,9 @@ "dev": true }, "nouislider": { - "version": "15.4.0", - "resolved": "https://registry.npmjs.org/nouislider/-/nouislider-15.4.0.tgz", - "integrity": "sha512-AV7UMhGhZ4Mj6ToMT812Ib8OJ4tAXR2/Um7C4l4ZvvsqujF0WpQTpqqHJ+9xt4174R7ueQOUrBR4yakJpAIPCA==" + "version": "15.5.0", + "resolved": "https://registry.npmjs.org/nouislider/-/nouislider-15.5.0.tgz", + "integrity": "sha512-p0Rn0a4XzrBJ+JZRhNDYpRYr6sDPkajsjbvEQoTp/AZlNI3NirO15s1t11D25Gk3zVyvNJAzc1DO48cq/KX5Sw==" }, "now-and-later": { "version": "2.0.1", diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index ec4ead689cff..43d7a3cecddb 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -44,7 +44,7 @@ "lazyload-js": "1.0.0", "moment": "2.22.2", "ng-file-upload": "12.2.13", - "nouislider": "15.4.0", + "nouislider": "15.5.0", "npm": "^6.14.7", "spectrum-colorpicker2": "2.0.8", "tinymce": "4.9.11", diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js index 4a1988cc275c..50a32e0b0576 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js @@ -200,6 +200,10 @@ } })); + evts.push(eventsService.on("rte.shortcut.saveAndPublish", function () { + $scope.saveAndPublish(); + })); + evts.push(eventsService.on("content.saved", function () { // Clear out localstorage keys that start with tinymce__ // When we save/perist a content node diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgridselector.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgridselector.directive.js index bf03749faa7c..4c628391cb97 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgridselector.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgridselector.directive.js @@ -1,9 +1,9 @@ (function () { 'use strict'; - function GridSelector($location, overlayService, editorService) { + function GridSelector(overlayService, editorService) { - function link(scope, el, attr, ctrl) { + function link(scope) { var eventBindings = []; scope.dialogModel = {}; @@ -33,26 +33,30 @@ }; scope.openItemPicker = function ($event) { - var dialogModel = { - view: "itempicker", - title: "Choose " + scope.itemLabel, - availableItems: scope.availableItems, - selectedItems: scope.selectedItems, - position: "target", - event: $event, - submit: function (model) { - scope.selectedItems.push(model.selectedItem); - // if no default item - set item as default - if (scope.defaultItem === null) { - scope.setAsDefaultItem(model.selectedItem); + if (scope.itemPicker) { + scope.itemPicker(); + } else { + var dialogModel = { + view: "itempicker", + title: "Choose " + scope.itemLabel, + availableItems: scope.availableItems, + selectedItems: scope.selectedItems, + position: "target", + event: $event, + submit: function (model) { + scope.selectedItems.push(model.selectedItem); + // if no default item - set item as default + if (scope.defaultItem === null) { + scope.setAsDefaultItem(model.selectedItem); + } + overlayService.close(); + }, + close: function () { + overlayService.close(); } - overlayService.close(); - }, - close: function() { - overlayService.close(); - } - }; - overlayService.open(dialogModel); + }; + overlayService.open(dialogModel); + } }; scope.openTemplate = function (selectedItem) { @@ -156,7 +160,8 @@ availableItems: "=", defaultItem: "=", itemName: "@", - updatePlaceholder: "=" + updatePlaceholder: "=", + itemPicker: "=" }, link: link }; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js index fe802a8a2872..1f65bd7cea01 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js @@ -179,7 +179,7 @@ When building a custom infinite editor view you can use the same components as a } else { focus(); } - }); + }); /** * @ngdoc method @@ -972,6 +972,28 @@ When building a custom infinite editor view you can use the same components as a open(editor); } + /** + * @ngdoc method + * @name umbraco.services.editorService#templatePicker + * @methodOf umbraco.services.editorService + * + * @description + * Opens a template picker in infinite editing, the submit callback returns an array of selected items. + * + * @param {object} editor rendering options. + * @param {boolean} editor.multiPicker Pick one or multiple items. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. + * @returns {object} editor object. + */ + function templatePicker(editor) { + editor.view = "views/common/infiniteeditors/treepicker/treepicker.html"; + if (!editor.size) editor.size = "small"; + editor.section = "settings"; + editor.treeAlias = "templates"; + open(editor); + } + /** * @ngdoc method * @name umbraco.services.editorService#macroPicker @@ -1134,6 +1156,7 @@ When building a custom infinite editor view you can use the same components as a templateSections: templateSections, userPicker: userPicker, itemPicker: itemPicker, + templatePicker: templatePicker, macroPicker: macroPicker, memberGroupPicker: memberGroupPicker, memberPicker: memberPicker, diff --git a/src/Umbraco.Web.UI.Client/src/common/services/overlay.service.js b/src/Umbraco.Web.UI.Client/src/common/services/overlay.service.js index 8a965f2c78d9..113b26d74cbb 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/overlay.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/overlay.service.js @@ -12,6 +12,58 @@ var currentOverlay = null; + /** + * @ngdoc method + * @name umbraco.services.overlayService#open + * @methodOf umbraco.services.overlayService + * + * @description + * Opens a new overlay. + * + * @param {object} overlay The rendering options for the overlay. + * @param {string=} overlay.view The URL to the view. Defaults to `views/common/overlays/default/default.html` if nothing is specified. + * @param {string=} overlay.position The alias of the position of the overlay. Defaults to `center`. + * + * Custom positions can be added by adding a CSS rule for the the underlying CSS rule. Eg. for the position `center`, the corresponding `umb-overlay-center` CSS rule is defined as: + * + *
+         * .umb-overlay.umb-overlay-center {
+         *     position: absolute;
+         *     width: 600px;
+         *     height: auto;
+         *     top: 50%;
+         *     left: 50%;
+         *     transform: translate(-50%,-50%);
+         *     border-radius: 3px;
+         * }
+         * 
+ * @param {string=} overlay.size Sets an alias for the size of the overlay to be opened. If set to `small` (default), an `umb-overlay--small` class name will be appended the the class list of the main overlay element in the DOM. + * + * Umbraco does not support any more sizes by default, but if you wish to introduce a `medium` size, you could do so by adding a CSS rule simlar to: + * + *
+         * .umb-overlay-center.umb-overlay--medium {
+         *     width: 800px;
+         * }
+         * 
+ * @param {booean=} overlay.disableBackdropClick A boolean value indicating whether the click event on the backdrop should be disabled. + * @param {string=} overlay.title The overall title of the overlay. The title will be omitted if not specified. + * @param {string=} overlay.subtitle The sub title of the overlay. The sub title will be omitted if not specified. + * @param {object=} overlay.itemDetails An item that will replace the header of the overlay. + * @param {string=} overlay.itemDetails.icon The icon of the item - eg. `icon-book`. + * @param {string=} overlay.itemDetails.title The title of the item. + * @param {string=} overlay.itemDetails.description Sets the description of the item. * + * @param {string=} overlay.submitButtonLabel The label of the submit button. To support localized values, it's recommended to use the `submitButtonLabelKey` instead. + * @param {string=} overlay.submitButtonLabelKey The key to be used for the submit button label. Defaults to `general_submit` if not specified. + * @param {string=} overlay.submitButtonState The state of the submit button. Possible values are inherited from the [umbButton directive](#/api/umbraco.directives.directive:umbButton) and are `init`, `busy", `success`, `error`. + * @param {string=} overlay.submitButtonStyle The styling of the submit button. Possible values are inherited from the [umbButton directive](#/api/umbraco.directives.directive:umbButton) and are `primary`, `info`, `success`, `warning`, `danger`, `inverse`, `link` and `block`. Defaults to `success` if not specified specified. + * @param {string=} overlay.hideSubmitButton A boolean value indicating whether the submit button should be hidden. Default is `false`. + * @param {string=} overlay.disableSubmitButton A boolean value indicating whether the submit button should be disabled, preventing the user from submitting the overlay. Default is `false`. + * @param {string=} overlay.closeButtonLabel The label of the close button. To support localized values, it's recommended to use the `closeButtonLabelKey` instead. + * @param {string=} overlay.closeButtonLabelKey The key to be used for the close button label. Defaults to `general_close` if not specified. + * @param {string=} overlay.submit A callback function that is invoked when the user submits the overlay. + * @param {string=} overlay.close A callback function that is invoked when the user closes the overlay. + */ function open(newOverlay) { // prevent two open overlays at the same time @@ -49,6 +101,14 @@ eventsService.emit("appState.overlay", overlay); } + /** + * @ngdoc method + * @name umbraco.services.overlayService#close + * @methodOf umbraco.services.overlayService + * + * @description + * Closes the current overlay. + */ function close() { focusLockService.removeInertAttribute(); @@ -61,6 +121,16 @@ eventsService.emit("appState.overlay", null); } + /** + * @ngdoc method + * @name umbraco.services.overlayService#ysod + * @methodOf umbraco.services.overlayService + * + * @description + * Opens a new overlay with an error message. + * + * @param {object} error The error to be shown. + */ function ysod(error) { const overlay = { view: "views/common/overlays/ysod/ysod.html", @@ -72,6 +142,36 @@ open(overlay); } + /** + * @ngdoc method + * @name umbraco.services.overlayService#confirm + * @methodOf umbraco.services.overlayService + * + * @description + * Opens a new overlay prompting the user to confirm the overlay. + * + * @param {object} overlay The options for the overlay. + * @param {string=} overlay.confirmType The type of the confirm dialog, which helps define standard styling and labels of the overlay. Supported values are `delete` and `remove`. + * @param {string=} overlay.closeButtonLabelKey The key to be used for the cancel button label. Defaults to `general_cancel` if not specified. + * @param {string=} overlay.view The URL to the view. Defaults to `views/common/overlays/confirm/confirm.html` if nothing is specified. + * @param {string=} overlay.confirmMessageStyle The styling of the confirm message. If `overlay.confirmType` is `delete`, the fallback value is `danger` - otherwise a message style isn't explicitly specified. + * @param {string=} overlay.submitButtonStyle The styling of the confirm button. Possible values are inherited from the [umbButton directive](#/api/umbraco.directives.directive:umbButton) and are `primary`, `info`, `success`, `warning`, `danger`, `inverse`, `link` and `block`. + * + * If not specified, the fallback value depends on the value specified for the `overlay.confirmType` parameter: + * + * - `delete`: fallback key is `danger` + * - `remove`: fallback key is `primary` + * - anything else: no fallback AKA default button style + * @param {string=} overlay.submitButtonLabelKey The key to be used for the confirm button label. + * + * If not specified, the fallback value depends on the value specified for the `overlay.confirmType` parameter: + * + * - `delete`: fallback key is `actions_delete` + * - `remove`: fallback key is `actions_remove` + * - anything else: fallback is `general_confirm` + * @param {function=} overlay.close A callback function that is invoked when the user closes the overlay. + * @param {function=} overlay.submit A callback function that is invoked when the user confirms the overlay. + */ function confirm(overlay) { if (!overlay.closeButtonLabelKey) overlay.closeButtonLabelKey = "general_cancel"; @@ -99,11 +199,45 @@ open(overlay); } + /** + * @ngdoc method + * @name umbraco.services.overlayService#confirmDelete + * @methodOf umbraco.services.overlayService + * + * @description + * Opens a new overlay prompting the user to confirm the overlay. The overlay will have styling and labels useful for when the user needs to confirm a delete action. + * + * @param {object} overlay The options for the overlay. + * @param {string=} overlay.closeButtonLabelKey The key to be used for the cancel button label. Defaults to `general_cancel` if not specified. + * @param {string=} overlay.view The URL to the view. Defaults to `views/common/overlays/confirm/confirm.html` if nothing is specified. + * @param {string=} overlay.confirmMessageStyle The styling of the confirm message. Defaults to `delete` if not specified specified. + * @param {string=} overlay.submitButtonStyle The styling of the confirm button. Possible values are inherited from the [umbButton directive](#/api/umbraco.directives.directive:umbButton) and are `primary`, `info`, `success`, `warning`, `danger`, `inverse`, `link` and `block`. Defaults to `danger` if not specified specified. + * @param {string=} overlay.submitButtonLabelKey The key to be used for the confirm button label. Defaults to `actions_delete` if not specified. + * @param {function=} overlay.close A callback function that is invoked when the user closes the overlay. + * @param {function=} overlay.submit A callback function that is invoked when the user confirms the overlay. + */ function confirmDelete(overlay) { overlay.confirmType = "delete"; confirm(overlay); } + /** + * @ngdoc method + * @name umbraco.services.overlayService#confirmRemove + * @methodOf umbraco.services.overlayService + * + * @description + * Opens a new overlay prompting the user to confirm the overlay. The overlay will have styling and labels useful for when the user needs to confirm a remove action. + * + * @param {object} overlay The options for the overlay. + * @param {string=} overlay.closeButtonLabelKey The key to be used for the cancel button label. Defaults to `general_cancel` if not specified. + * @param {string=} overlay.view The URL to the view. Defaults to `views/common/overlays/confirm/confirm.html` if nothing is specified. + * @param {string=} overlay.confirmMessageStyle The styling of the confirm message - eg. `danger`. + * @param {string=} overlay.submitButtonStyle The styling of the confirm button. Possible values are inherited from the [umbButton directive](#/api/umbraco.directives.directive:umbButton) and are `primary`, `info`, `success`, `warning`, `danger`, `inverse`, `link` and `block`. Defaults to `primary` if not specified specified. + * @param {string=} overlay.submitButtonLabelKey The key to be used for the confirm button label. Defaults to `actions_remove` if not specified. + * @param {function=} overlay.close A callback function that is invoked when the user closes the overlay. + * @param {function=} overlay.submit A callback function that is invoked when the user confirms the overlay. + */ function confirmRemove(overlay) { overlay.confirmType = "remove"; confirm(overlay); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js index 4c3901e63c48..6c6237263f49 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js @@ -1226,6 +1226,12 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s }); }); + editor.addShortcut('Ctrl+P', '', function () { + angularHelper.safeApply($rootScope, function () { + eventsService.emit("rte.shortcut.saveAndPublish"); + }); + }); + }, insertLinkInEditor: function (editor, target, anchorElm) { @@ -1496,6 +1502,19 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s }); } + + if(Umbraco.Sys.ServerVariables.umbracoSettings.sanitizeTinyMce === true){ + /** prevent injecting arbitrary JavaScript execution in on-attributes. */ + const allNodes = Array.prototype.slice.call(args.editor.dom.doc.getElementsByTagName("*")); + allNodes.forEach(node => { + for (var i = 0; i < node.attributes.length; i++) { + if(node.attributes[i].name.indexOf("on") === 0) { + node.removeAttribute(node.attributes[i].name) + } + } + }); + } + }); args.editor.on('init', function (e) { @@ -1507,6 +1526,60 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s //enable browser based spell checking args.editor.getBody().setAttribute('spellcheck', true); + + /** Setup sanitization for preventing injecting arbitrary JavaScript execution in attributes: + * https://github.com/advisories/GHSA-w7jx-j77m-wp65 + * https://github.com/advisories/GHSA-5vm8-hhgr-jcjp + */ + const uriAttributesToSanitize = ['src', 'href', 'data', 'background', 'action', 'formaction', 'poster', 'xlink:href']; + const parseUri = function() { + // Encapsulated JS logic. + const safeSvgDataUrlElements = [ 'img', 'video' ]; + const scriptUriRegExp = /((java|vb)script|mhtml):/i; + const trimRegExp = /[\s\u0000-\u001F]+/g; + const isInvalidUri = (uri, tagName) => { + if (/^data:image\//i.test(uri)) { + return safeSvgDataUrlElements.indexOf(tagName) !== -1 && /^data:image\/svg\+xml/i.test(uri); + } else { + return /^data:/i.test(uri); + } + }; + + return function parseUri(uri, tagName) { + uri = uri.replace(trimRegExp, ''); + try { + // Might throw malformed URI sequence + uri = decodeURIComponent(uri); + } catch (ex) { + // Fallback to non UTF-8 decoder + uri = unescape(uri); + } + + if (scriptUriRegExp.test(uri)) { + return; + } + + if (isInvalidUri(uri, tagName)) { + return; + } + + return uri; + } + }(); + + if(Umbraco.Sys.ServerVariables.umbracoSettings.sanitizeTinyMce === true){ + args.editor.serializer.addAttributeFilter(uriAttributesToSanitize, function (nodes) { + nodes.forEach(function(node) { + node.attributes.forEach(function(attr) { + const attrName = attr.name.toLowerCase(); + if(uriAttributesToSanitize.indexOf(attrName) !== -1) { + attr.value = parseUri(attr.value, node.name); + } + }); + }); + }); + } + //start watching the value startWatch(); }); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js index 40266f7ac5c5..4db5af883e41 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js @@ -63,7 +63,7 @@ var saveModel = _.pick(displayModel, 'compositeContentTypes', 'isContainer', 'allowAsRoot', 'allowedTemplates', 'allowedContentTypes', 'alias', 'description', 'thumbnail', 'name', 'id', 'icon', 'trashed', - 'key', 'parentId', 'alias', 'path', 'allowCultureVariant', 'allowSegmentVariant', 'isElement'); + 'key', 'parentId', 'alias', 'path', 'allowCultureVariant', 'allowSegmentVariant', 'isElement', 'historyCleanup'); saveModel.allowedTemplates = _.map(displayModel.allowedTemplates, function (t) { return t.alias; }); saveModel.defaultTemplate = displayModel.defaultTemplate ? displayModel.defaultTemplate.alias : null; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less index d0427cad0a3d..6c1e5058d267 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less @@ -55,6 +55,11 @@ input.umb-table__input { color: @ui-disabled-type; } +.umb-table-head__icon { + position: relative; + top: 2px; +} + .umb-table-head__link { background: transparent; border: 0 none; @@ -111,7 +116,7 @@ input.umb-table__input { .umb-table-body .umb-table-row.-selectable { cursor: pointer; } -.umb-table-row.-selected, +.umb-table-row.-selected, .umb-table-body .umb-table-row.-selectable:hover { &::before { content: ""; @@ -226,7 +231,7 @@ input.umb-table__input { &.umb-table-body__checkicon { display: inline-block; } - } + } } // Table Row Styles @@ -309,8 +314,8 @@ input.umb-table__input { .umb-table__loading-overlay { position: absolute; - width: 100%; - height: 100%; + width: 100%; + height: 100%; background-color: rgba(255, 255, 255, 0.7); z-index: 1; } @@ -330,7 +335,7 @@ input.umb-table__input { } .umb-table--condensed { - + .umb-table-cell:first-of-type:not(.not-fixed) { padding-top: 10px; padding-bottom: 10px; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/compositions.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/compositions.html index 7ec69018b64c..9ebb9d4e45d8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/compositions.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/compositions.html @@ -30,21 +30,22 @@
- + Inherit tabs and properties from an existing Document Type. New tabs will be added to the current Document Type or + merged if a tab with an identical name exists.
- + There are no Content Types available to use as a composition. - + This Content Type is used in a composition, and therefore cannot be composed itself.
-
-

+
Where is this composition used?
+

This composition is currently used in the composition of the following Content Types:

  • diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/overlays/confirmremove.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/overlays/confirmremove.html index 6fc3bd826d48..33f48b8c1ea8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/overlays/confirmremove.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/overlays/confirmremove.html @@ -1,9 +1,10 @@
    - + Removing a composition will delete all the associated property data. Once you save the Document Type there's no way + back.
    - + Are you sure?
    diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.html index a4fef2874073..d3c6a3538da0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.html @@ -23,22 +23,22 @@
    - using this editor will get updated with the new settings. + All Document Types using this editor will get updated with the new settings.
    - +
    - +
    -
    +
    Configuration
    diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/overlays/mediacropdetails.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/overlays/mediacropdetails.html index 99f4fc04e7a2..9a01d3329da1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/overlays/mediacropdetails.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/overlays/mediacropdetails.html @@ -12,21 +12,21 @@
    - + URL
    - + Alternative text (optional)
    - + Caption (optional)
    diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html index 8379062807a8..564f0911fe56 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html @@ -55,9 +55,11 @@
    @@ -87,7 +89,7 @@
    -
    +
    Validation
    ng-keypress="vm.submitOnEnter($event)" />
    Current version

    {{vm.currentVersion.name}} (Created: {{vm.currentVersion.createDate}})

    -
    +
    Rollback to
    - + Required
    - +
    - + If mandatory, the child template must contain a @section definition, otherwise an error is shown.
    - +
    - +
    - +
    -
    +
    Define a named section
    - + Defines a part of your template as a named section by wrapping it in @section { ... }. + This can be rendered in a specific area of the parent of this template, by using @RenderSection. +
    - +
    - + - + Required
    - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js index ed5c4096bcca..72eb504c600d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js @@ -141,6 +141,9 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", }); } } + else if (vm.treeAlias === "templates") { + vm.entityType = "Template"; + } // TODO: Seems odd this logic is here, i don't think it needs to be and should just exist on the property editor using this if ($scope.model.minNumber) { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html index c722424e3182..b926d9e26681 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html @@ -1,140 +1,144 @@
    -
    - -
    - - - - - - - - - - - +
    + +
    + Your profile +
    + + + + + + + + + + +
    + +
    + +
    + External login providers +
    + +
    + +
    + +
    +
    + + +
    + + +
    -
    - -
    - External login providers -
    - -
    - -
    - -
    -
    - - -
    - - -
    - -
    - -
    - - -
    -
    - -
    - -
    - -
    - Change password -
    - -
    - - - - - - - - - - -
    - -
    - -
    -
    -
    {{tab.label}}
    -
    -
    -
    -
    +
    + + +
    +
    + Your recent history +
    + +
    + +
    + +
    + Change password +
    + +
    + + + + + + + + + + +
    + +
    + +
    +
    +
    {{tab.label}}
    +
    +
    +
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html index 3ad4ebc18815..e0fb4aeb7793 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html @@ -10,7 +10,7 @@
    • -
    • -
    @@ -34,7 +34,7 @@
    - : {{ vm.group.inheritedFromName }} + Inherited from: {{ vm.group.inheritedFromName }} , @@ -46,9 +46,9 @@ -
    +
    Required
    -
    +
    Required
    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/contenttype/umb-content-type-property.html b/src/Umbraco.Web.UI.Client/src/views/components/contenttype/umb-content-type-property.html index 8751a4723fe5..14f647e761f4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/contenttype/umb-content-type-property.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/contenttype/umb-content-type-property.html @@ -28,7 +28,7 @@
    {{propertyTypeForm.groupName.errorMsg}}
    -
    +
    Required label
    @@ -61,37 +61,37 @@ {{vm.property.dataTypeName}} - + Preview
    * - + Mandatory
    - + Show on member profile
    - + Member can edit
    - + Is sensitive data
    - + Vary by culture
    - + Vary by segments
    @@ -100,13 +100,13 @@
    - + Inherited from {{vm.property.contentTypeName}}
    - + Locked
    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/contenttype/umb-content-type-tab.html b/src/Umbraco.Web.UI.Client/src/views/components/contenttype/umb-content-type-tab.html index 59476f7e26c6..b62e3f17d914 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/contenttype/umb-content-type-tab.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/contenttype/umb-content-type-tab.html @@ -1,6 +1,6 @@ -
    - : {{ vm.tab.inheritedFromName }} + Inherited from: {{ vm.tab.inheritedFromName }} , @@ -50,7 +50,7 @@
    {{tabNameForm.tabName.errorMsg}}
    -
    +
    Required
    @@ -61,9 +61,9 @@ -
    +
    Required
    -
    +
    Required
    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html index 993622e8807a..3575453bd8e9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html @@ -55,7 +55,7 @@ ng-if="sortingMode" class="umb-group-builder__convert-dropzone" umb-droppable="droppableOptionsConvert"> - + Convert to tab
    - + You have not added any groups
    @@ -118,7 +118,7 @@ data-element="property-add" class="umb-group-builder__group-add-property" ng-click="addNewProperty(tab)"> - + Add property - + Add property
    @@ -183,6 +183,6 @@ class="umb-group-builder__group -placeholder" ng-click="addGroupToActiveTab()" data-element="group-add"> - + Add group
    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html index e8f174516c13..77ace4af7004 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html @@ -60,20 +60,20 @@ ng-click="vm.selectAll($event)" ng-checked="vm.isSelectedAll()">
-
+
@@ -90,7 +90,7 @@ ng-show="item.isFolder" ng-class="{'-locked': item.selected || !item.file || !item.thumbnail}" ng-click="clickItemName(item, $event, $index)"> - + {{item.name}}
@@ -101,4 +101,3 @@
- diff --git a/src/Umbraco.Web.UI.Client/src/views/content/assigndomain.html b/src/Umbraco.Web.UI.Client/src/views/content/assigndomain.html index 3bf10d610e99..1625552955a9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/assigndomain.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/assigndomain.html @@ -8,7 +8,7 @@
Culture
- + @@ -25,18 +25,19 @@
CultureDomains
- + Valid domain names are: "example.com", "www.example.com", "example.com:8080", or "https://www.example.com/". + Furthermore also one-level paths in domains are supported, eg. "example.com/en" or "/en".
@@ -49,9 +50,9 @@
Domains - + Value cannot be empty - ({{domain.other}}) + Domain has already been assigned.({{domain.other}})
- + Domain * - + Language * @@ -72,7 +72,7 @@
- + Media Types
@@ -106,7 +106,7 @@
- + Member Types
diff --git a/src/Umbraco.Web.UI.Client/src/views/dataTypes/views/datatype.info.html b/src/Umbraco.Web.UI.Client/src/views/dataTypes/views/datatype.info.html index 7bf985ca5618..2b59a8c4e75f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dataTypes/views/datatype.info.html +++ b/src/Umbraco.Web.UI.Client/src/views/dataTypes/views/datatype.info.html @@ -24,7 +24,7 @@
- + Used in Document Types
@@ -85,7 +85,7 @@
- + Used in Member Types
diff --git a/src/Umbraco.Web.UI.Client/src/views/documentTypes/import.controller.js b/src/Umbraco.Web.UI.Client/src/views/documentTypes/import.controller.js index 617034824366..3983377550cf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documentTypes/import.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/documentTypes/import.controller.js @@ -9,7 +9,7 @@ angular.module("umbraco") vm.cancelButtonLabel = "cancel"; - $scope.handleFiles = function (files, event) { + $scope.handleFiles = function (files, event, invalidFiles) { if (files && files.length > 0) { $scope.upload(files[0]); } diff --git a/src/Umbraco.Web.UI.Client/src/views/documentTypes/importdocumenttype.html b/src/Umbraco.Web.UI.Client/src/views/documentTypes/importdocumenttype.html index 347f94039c3b..32322558bdd2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documentTypes/importdocumenttype.html +++ b/src/Umbraco.Web.UI.Client/src/views/documentTypes/importdocumenttype.html @@ -5,18 +5,19 @@

- To import a document type, find the '.udt' file on your computer by clicking the 'Browse' button and click - 'Import' (you'll be asked for confirmation on the next screen) + To import a document type, find the '.udt' file on your computer by clicking the 'Import' button (you'll be + asked for confirmation on the next screen)

-
-
- +
Allowed child node types
+ Allow content of the specified types to be created underneath content of this type.
@@ -43,8 +43,8 @@
-
- +
Allow vary by culture
+ Allow editors to create content of different languages.
@@ -61,8 +61,8 @@
-
- +
Allow segmentation
+ Allow editors to create segments of this content.
@@ -77,9 +77,9 @@
-
- -
+
Is an Element Type
+ An Element Type is meant to be used for instance in Nested Content, and not in the tree. +
A Document Type cannot be changed to an Element Type once it has been used to create one or more content items.
@@ -92,6 +92,36 @@
+ +
+ +
+
+ +
+ +
+ +
+

+
+ + + + + + + + + + + + +
+
+ +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/documentTypes/views/templates/templates.controller.js b/src/Umbraco.Web.UI.Client/src/views/documentTypes/views/templates/templates.controller.js index e2a964c29365..46e856410d94 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documentTypes/views/templates/templates.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/documentTypes/views/templates/templates.controller.js @@ -9,7 +9,7 @@ (function () { 'use strict'; - function TemplatesController($scope, entityResource, contentTypeHelper, templateResource, contentTypeResource, $routeParams) { + function TemplatesController($scope, entityResource, contentTypeHelper, contentTypeResource, editorService, $routeParams) { /* ----------- SCOPE VARIABLES ----------- */ @@ -22,6 +22,7 @@ vm.isElement = $scope.model.isElement; vm.createTemplate = createTemplate; + vm.openTemplatePicker = openTemplatePicker; /* ---------- INIT ---------- */ @@ -81,6 +82,29 @@ vm.canCreateTemplate = existingTemplate ? false : true; } + function openTemplatePicker() { + const editor = { + title: "Choose template", + filterCssClass: 'not-allowed', + multiPicker: true, + filter: item => { + return !vm.availableTemplates.some(template => template.id == item.id) || + $scope.model.allowedTemplates.some(template => template.id == item.id); + }, + submit: model => { + model.selection.forEach(item => { + $scope.model.allowedTemplates.push(item); + }); + editorService.close(); + }, + close: function() { + editorService.close(); + } + } + + editorService.templatePicker(editor); + } + var unbindWatcher = $scope.$watch("model.isElement", function(newValue, oldValue) { vm.isElement = newValue; diff --git a/src/Umbraco.Web.UI.Client/src/views/documentTypes/views/templates/templates.html b/src/Umbraco.Web.UI.Client/src/views/documentTypes/views/templates/templates.html index 279ffb73c0e4..04fd61be3cd7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documentTypes/views/templates/templates.html +++ b/src/Umbraco.Web.UI.Client/src/views/documentTypes/views/templates/templates.html @@ -17,7 +17,8 @@
item-name="template" name="model.name" alias="model.alias" - update-placeholder="vm.updateTemplatePlaceholder"> + update-placeholder="vm.updateTemplatePlaceholder" + item-picker="vm.openTemplatePicker"> Create under {{currentNode.name}}
-

+

The selected media in the tree doesn't allow for any other media to be created below it.

-

+

There are no allowed Media Types available for creating media here. You must enable these in Media Types + Types within the Settings section, by editing the Allowed child node + types under Permissions.

- + Edit permissions for this Media Type
diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/mediafolderpicker.html b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/mediafolderpicker.html index e3449f07b9fa..f72846ea9888 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/mediafolderpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/mediafolderpicker.html @@ -5,7 +5,7 @@
  • - + Trashed

    @@ -18,15 +18,21 @@
    -
  • -
  • diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js index 7334fbeadf5e..11ef37029c54 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js @@ -65,6 +65,9 @@ vm.layout = []; // The layout object specific to this Block Editor, will be a direct reference from Property Model. vm.availableBlockTypes = []; // Available block entries of this property editor. vm.labels = {}; + vm.options = { + createFlow: false + }; localizationService.localizeMany(["grid_addElement", "content_createEmpty"]).then(function (data) { vm.labels.grid_addElement = data[0]; @@ -380,7 +383,7 @@ function editBlock(blockObject, openSettings, blockIndex, parentForm, options) { - options = options || {}; + options = options || vm.options; // this must be set if (blockIndex === undefined) { @@ -560,7 +563,9 @@ if (inlineEditing === true) { blockObject.activate(); } else if (inlineEditing === false && blockObject.hideContentInOverlay !== true) { + vm.options.createFlow = true; blockObject.edit(); + vm.options.createFlow = false; } } } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html index e93637c67142..fa148ecfc725 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html @@ -1,7 +1,7 @@
    -

    -

    +

    You have picked a content item currently deleted or in the recycle bin

    +

    You have picked content items currently deleted or in the recycle bin

    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.html index f5a6191ad13d..698ff6daeb6b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.html @@ -18,12 +18,14 @@
    -

    +

    Adjust the row by setting cell widths and adding additional cells

    -

    Modifying a row configuration name will result in loss of - data for any existing content that is based on this configuration.

    -

    Modifying only the label will not result in data loss.

    + +

    Modifying a row configuration name will result in loss of + data for any existing content that is based on this configuration.

    +

    Modifying only the label will not result in data loss.

    +
    @@ -70,10 +72,16 @@
    {{currentCell.grid}}
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/overlays/rowdeleteconfirm.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/overlays/rowdeleteconfirm.html index 2ba56a5b8832..ded2e5b9408f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/overlays/rowdeleteconfirm.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/overlays/rowdeleteconfirm.html @@ -11,6 +11,6 @@

    - ? + Are you sure you want to delete?
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.html index 07d521579315..8bbcb2204c3f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.html @@ -17,12 +17,12 @@ - + Sorry, we can not find what you are looking for. - + Your recycle bin is empty
    @@ -69,12 +69,12 @@ - + Your recycle bin is empty - + Sorry, we can not find what you are looking for.
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/listviewpublish.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/listviewpublish.html index c6f7c85c0b3c..b6933dffc06e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/listviewpublish.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/listviewpublish.html @@ -2,7 +2,7 @@
    -

    +

    Publishing will make the selected items visible on the site.

    @@ -13,13 +13,13 @@
    -

    +

    What languages would you like to publish?

    - + Languages
    @@ -34,7 +34,7 @@
    - + Mandatory language
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/listviewunpublish.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/listviewunpublish.html index 5806bb8f0265..9a6af50f3e6c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/listviewunpublish.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/listviewunpublish.html @@ -2,7 +2,7 @@
    -

    +

    Unpublishing will remove the selected items and all their descendants from the site.

    @@ -13,13 +13,13 @@
    -

    +

    Select the languages to unpublish. Unpublishing a mandatory language will unpublish all languages.

    - + Languages
    @@ -35,7 +35,7 @@
    - + Mandatory language
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html index fa146f12f04b..f429c04f1d31 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html @@ -22,7 +22,7 @@
    - {{ph = placeholder(config);""}} + {{ph = placeholder(config);hasTabsOrFirstRender = (elemTypeTabs[config.ncAlias].length || config.ncAlias=='');""}} - + + + The selected element type does not contain any supported groups (tabs are not supported by this editor, either change them to groups or use the Block List editor). + + diff --git a/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js index 7b527804f586..856886a8701a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js @@ -559,13 +559,13 @@ } }); - localizationService.localize("template_mastertemplate").then(function (value) { - var title = value; - var masterTemplate = { - title: title, - availableItems: availableMasterTemplates, - submit: function (model) { - var template = model.selectedItem; + localizationService.localize("template_mastertemplate").then(title => { + const editor = { + title, + filterCssClass: 'not-allowed', + filter: item => !availableMasterTemplates.some(template => template.id == item.id), + submit: model => { + var template = model.selection[0]; if (template && template.alias) { vm.template.masterTemplateAlias = template.alias; setLayout(template.alias + ".cshtml"); @@ -575,14 +575,10 @@ } editorService.close(); }, - close: function (oldModel) { - // close dialog - editorService.close(); - // focus editor - vm.editor.focus(); - } - }; - editorService.itemPicker(masterTemplate); + close: () => editorService.close() + } + + editorService.templatePicker(editor); }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html index 29be78241535..5569e0a985be 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html @@ -181,7 +181,7 @@ - + Sorry, we can not find what you are looking for. @@ -305,7 +305,7 @@

    Invite User

    - + Invite new users to give them access to Umbraco. An invite email will be sent to the user with information on how to log in to Umbraco. Invites last for 72 hours.

    @@ -313,7 +313,7 @@

    Create user

    - + Create new users to give them access to Umbraco. When a new user is created a password will be generated that you can share with the user.

    @@ -437,7 +437,7 @@

    -

    +

    The new user has successfully been created. To log in to Umbraco use the password below.

    @@ -520,7 +520,7 @@

    -

    +

    An invitation has been sent to the new user with details about how to log in to Umbraco.

    diff --git a/src/Umbraco.Web.UI.Docs/umb-docs.css b/src/Umbraco.Web.UI.Docs/umb-docs.css index 850e0d4aa45b..80d1bbbd2a0d 100644 --- a/src/Umbraco.Web.UI.Docs/umb-docs.css +++ b/src/Umbraco.Web.UI.Docs/umb-docs.css @@ -34,7 +34,8 @@ a:hover { color: rgba(0, 0, 0, .8); } -.content p code { +.content p code, +.content li code { font-size: 85%; font-family: inherit; background-color: #f7f7f9; diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index 748220f671c0..97183643ac31 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -282,6 +282,7 @@ name. Use to display the item index + The selected element type does not contain any supported groups (tabs are not supported by this editor, either change them to groups or use the Block List editor). Add another text box Remove this text box Content root @@ -325,6 +326,7 @@ Click to upload or click here to choose files Cannot upload this file, it does not have an approved file type + Cannot upload this file, it does not have a valid file name Max file size is Media root Failed to create a folder under parent id %0% @@ -848,6 +850,7 @@ Avatar for Header system field + Last Updated Blue @@ -1168,7 +1171,6 @@ To manage your website, simply open the Umbraco backoffice and start adding cont %6% Have a nice day! - Cheers from the Umbraco robot ]]> The following languages have been modified %0% @@ -1398,6 +1400,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Remove all medias? Clipboard Not allowed + Open media picker enter external link @@ -1419,7 +1422,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Created Current version - Red text will not be shown in the selected version. , green means added]]> + Red text will be removed in the selected version, green text will be added]]> Document has been rolled back Select a version to compare with the current version This displays the selected version as HTML, if you wish to see the difference between 2 @@ -1458,7 +1461,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Default template To import a Document Type, find the ".udt" file on your computer by clicking the - "Browse" button and click "Import" (you'll be asked for confirmation on the next screen) + "Import" button (you'll be asked for confirmation on the next screen) New Tab Title Node type @@ -1711,6 +1714,9 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Choose extra Choose default are added + Warning + + Modifying a row configuration name will result in loss of data for any existing content that is based on this configuration.

    Modifying only the label will not result in data loss.

    ]]>
    You are deleting the row configuration Deleting a row configuration name will result in loss of data for any existing content that is based on this @@ -1819,6 +1825,13 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Removing a child node will limit the editors options to create different content types beneath a node. + using this editor will get updated with the new settings. + History cleanup + Allow override the global settings for when history versions are removed. + Keep all versions newer than days + Keep latest version per day for days + Prevent cleanup + NOTE! The cleanup of historically content versions is disabled globally. These settings will not take effect before it is enabled. Add language @@ -1901,7 +1914,6 @@ To manage your website, simply open the Umbraco backoffice and start adding cont http://%3% Have a nice day! - Cheers from the Umbraco robot ]]> No translator users found. Please create a translator user before you start sending diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index 7f139b2da83a..4611b8650d58 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -286,6 +286,7 @@ name. Use to display the item index + The selected element type does not contain any supported groups (tabs are not supported by this editor, either change them to groups or use the Block List editor). Add another text box Remove this text box Content root @@ -329,6 +330,7 @@ Click to upload or click here to choose files Cannot upload this file, it does not have an approved file type + Cannot upload this file, it does not have a valid file name Max file size is Media root Parent and destination folders cannot be the same @@ -869,6 +871,7 @@ Avatar for Header system field + Last Updated Blue @@ -1424,6 +1427,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Remove all medias? Clipboard Not allowed + Open media picker enter external link @@ -1446,7 +1450,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Select a version to compare with the current version Current version - Red text will not be shown in the selected version. , green means added]]> + Red text will be removed in the selected version, green text will be added]]>
    Document has been rolled back This displays the selected version as HTML, if you wish to see the difference between 2 versions at the same time, use the diff view @@ -1477,7 +1481,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Default template To import a Document Type, find the ".udt" file on your computer by clicking the - "Browse" button and click "Import" (you'll be asked for confirmation on the next screen) + "Import" button (you'll be asked for confirmation on the next screen) New Tab Title Node type @@ -1753,6 +1757,9 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Choose extra Choose default are added + Warning + + Modifying a row configuration name will result in loss of data for any existing content that is based on this configuration.

    Modifying only the label will not result in data loss.

    ]]>
    You are deleting the row configuration Deleting a row configuration name will result in loss of data for any existing content that is based on this @@ -1876,6 +1883,13 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Removing a child node will limit the editors options to create different content types beneath a node. + using this editor will get updated with the new settings. + History cleanup + Allow override the global settings for when history versions are removed. + Keep all versions newer than days + Keep latest version per day for days + Prevent cleanup + NOTE! The cleanup of historically content versions is disabled globally. These settings will not take effect before it is enabled. Add language diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/nl.xml b/src/Umbraco.Web.UI/umbraco/config/lang/nl.xml index bedf544ea003..3ef84f2354d2 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/nl.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/nl.xml @@ -832,6 +832,7 @@ Blauw + Tabblad toevoegen Groep toevoegen Eigenschap toevoegen Editor toevoegen @@ -1678,6 +1679,7 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Je hebt wijzigingen aangebracht aan deze eigenschap. Ben je zeker dat je ze wil weggooien? + Tabblad toevoegen Taal toevoegen diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index 2eb6cfa471fc..67ef5f16fe0d 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json @@ -1,7 +1,2336 @@ { "name": "acceptancetest", + "lockfileVersion": 2, "requires": true, - "lockfileVersion": 1, + "packages": { + "": { + "name": "acceptancetest", + "hasInstallScript": true, + "dependencies": { + "typescript": "^3.9.2" + }, + "devDependencies": { + "cross-env": "^7.0.2", + "cypress": "^8.7.0", + "del": "^6.0.0", + "ncp": "^2.0.0", + "prompt": "^1.2.0", + "umbraco-cypress-testhelpers": "^1.0.0-beta-60" + } + }, + "node_modules/@cypress/request": { + "version": "2.88.6", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.6.tgz", + "integrity": "sha512-z0UxBE/+qaESAHY9p9sM2h8Y4XqtsbDCt0/DPOrqA/RZgKi4PkxdpXyK4wCCnSk1xHqWHZZAE+gV6aDAR6+caQ==", + "dev": true, + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@cypress/xvfb": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", + "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==", + "dev": true, + "dependencies": { + "debug": "^3.1.0", + "lodash.once": "^4.1.1" + } + }, + "node_modules/@cypress/xvfb/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@types/node": { + "version": "14.17.32", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.32.tgz", + "integrity": "sha512-JcII3D5/OapPGx+eJ+Ik1SQGyt6WvuqdRfh9jUwL6/iHGjmyOriBDciBUu7lEIBTL2ijxwrR70WUnw5AEDmFvQ==", + "dev": true + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.4.tgz", + "integrity": "sha512-IFQTJARgMUBF+xVd2b+hIgXWrZEjND3vJtRCvIelcFB5SIXfjV4bOHbHJ0eXKh+0COrBRc8MqteKAz/j88rE0A==", + "dev": true + }, + "node_modules/@types/sizzle": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz", + "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==", + "dev": true + }, + "node_modules/@types/yauzl": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.2.tgz", + "integrity": "sha512-8uALY5LTvSuHgloDVUvWP3pIauILm+8/0pDMokuDYIoNsOkSwd5AiHBTSEJjKTDcZr5z8UpgOWZkxBF4iJftoA==", + "dev": true, + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aggregate-error/node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dev": true, + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.2.tgz", + "integrity": "sha512-H0E+qZaDEfx/FY4t7iLRv1W2fFI6+pyCeTw1uN20AQPiwqwM6ojPxHxdLv4z8hi2DtnW9BOckSspLucW7pIE5g==", + "dev": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dev": true, + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/blob-util": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz", + "integrity": "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==", + "dev": true + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/cachedir": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz", + "integrity": "sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelize": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz", + "integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs=", + "dev": true + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/check-more-types": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", + "integrity": "sha1-FCD/sQ/URNz8ebQ4kbv//TKoRgA=", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ci-info": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.2.0.tgz", + "integrity": "sha512-dVqRX7fLUm8J6FgHJ418XuIgDLZDkYcDFTeL6TA2gt5WlIZUQrrH6EZrNClwT/H0FateUsZkGIOPRrLbP+PR9A==", + "dev": true + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-table3": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.0.tgz", + "integrity": "sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ==", + "dev": true, + "dependencies": { + "object-assign": "^4.1.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "colors": "^1.1.2" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/colorette": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz", + "integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==", + "dev": true + }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/common-tags": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.0.tgz", + "integrity": "sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cycle": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", + "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/cypress": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-8.7.0.tgz", + "integrity": "sha512-b1bMC3VQydC6sXzBMFnSqcvwc9dTZMgcaOzT0vpSD+Gq1yFc+72JDWi55sfUK5eIeNLAtWOGy1NNb6UlhMvB+Q==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@cypress/request": "^2.88.6", + "@cypress/xvfb": "^1.2.4", + "@types/node": "^14.14.31", + "@types/sinonjs__fake-timers": "^6.0.2", + "@types/sizzle": "^2.3.2", + "arch": "^2.2.0", + "blob-util": "^2.0.2", + "bluebird": "^3.7.2", + "cachedir": "^2.3.0", + "chalk": "^4.1.0", + "check-more-types": "^2.24.0", + "cli-cursor": "^3.1.0", + "cli-table3": "~0.6.0", + "commander": "^5.1.0", + "common-tags": "^1.8.0", + "dayjs": "^1.10.4", + "debug": "^4.3.2", + "enquirer": "^2.3.6", + "eventemitter2": "^6.4.3", + "execa": "4.1.0", + "executable": "^4.1.1", + "extract-zip": "2.0.1", + "figures": "^3.2.0", + "fs-extra": "^9.1.0", + "getos": "^3.2.1", + "is-ci": "^3.0.0", + "is-installed-globally": "~0.4.0", + "lazy-ass": "^1.6.0", + "listr2": "^3.8.3", + "lodash": "^4.17.21", + "log-symbols": "^4.0.0", + "minimist": "^1.2.5", + "ospath": "^1.2.2", + "pretty-bytes": "^5.6.0", + "proxy-from-env": "1.0.0", + "ramda": "~0.27.1", + "request-progress": "^3.0.0", + "supports-color": "^8.1.1", + "tmp": "~0.2.1", + "untildify": "^4.0.0", + "url": "^0.11.0", + "yauzl": "^2.10.0" + }, + "bin": { + "cypress": "bin/cypress" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dayjs": { + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.7.tgz", + "integrity": "sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/del": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/del/-/del-6.0.0.tgz", + "integrity": "sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ==", + "dev": true, + "dependencies": { + "globby": "^11.0.1", + "graceful-fs": "^4.2.4", + "is-glob": "^4.0.1", + "is-path-cwd": "^2.2.0", + "is-path-inside": "^3.0.2", + "p-map": "^4.0.0", + "rimraf": "^3.0.2", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dev": true, + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eventemitter2": { + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.5.tgz", + "integrity": "sha512-bXE7Dyc1i6oQElDG0jMRZJrRAn9QR2xyyFGmBdZleNmyQX0FqGYmhZIrIrpPfm/w//LTo4tVQGOGQcGCb5q9uw==", + "dev": true + }, + "node_modules/execa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/executable": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", + "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", + "dev": true, + "dependencies": { + "pify": "^2.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true, + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=", + "dev": true, + "engines": { + "node": "> 0.1.90" + } + }, + "node_modules/faker": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/faker/-/faker-4.1.0.tgz", + "integrity": "sha1-HkW7vsxndLPBlfrSg1EJxtdIzD8=", + "dev": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", + "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "dev": true, + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/getos": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz", + "integrity": "sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==", + "dev": true, + "dependencies": { + "async": "^3.2.0" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/global-dirs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz", + "integrity": "sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==", + "dev": true, + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz", + "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", + "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", + "dev": true + }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "dev": true, + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true, + "engines": { + "node": ">=8.12.0" + } + }, + "node_modules/ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "dev": true, + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true + }, + "node_modules/json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "node_modules/lazy-ass": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz", + "integrity": "sha1-eZllXoZGwX8In90YfRUNMyTVRRM=", + "dev": true, + "engines": { + "node": "> 0.8" + } + }, + "node_modules/listr2": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.13.1.tgz", + "integrity": "sha512-pk4YBDA2cxtpM8iLHbz6oEsfZieJKHf6Pt19NlKaHZZVpqHyVs/Wqr7RfBBCeAFCJchGO7WQHVkUPZTvJMHk8w==", + "dev": true, + "dependencies": { + "cli-truncate": "^2.1.0", + "colorette": "^2.0.16", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "rxjs": "^6.6.7", + "through": "^2.3.8", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "enquirer": ">= 2.3.0 < 3" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "dev": true, + "dependencies": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.50.0.tgz", + "integrity": "sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.33", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.33.tgz", + "integrity": "sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g==", + "dev": true, + "dependencies": { + "mime-db": "1.50.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "node_modules/ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", + "dev": true, + "bin": { + "ncp": "bin/ncp" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ospath": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz", + "integrity": "sha1-EnZjl3Sj+O8lcvf+QoDg6kVQwHs=", + "dev": true + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "dev": true + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prompt": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/prompt/-/prompt-1.2.0.tgz", + "integrity": "sha512-iGerYRpRUg5ZyC+FJ/25G5PUKuWAGRjW1uOlhX7Pi3O5YygdK6R+KEaBjRbHSkU5vfS5PZCltSPZdDtUYwRCZA==", + "dev": true, + "dependencies": { + "async": "~0.9.0", + "colors": "^1.1.2", + "read": "1.0.x", + "revalidator": "0.1.x", + "winston": "2.x" + }, + "engines": { + "node": ">= 0.6.6" + } + }, + "node_modules/prompt/node_modules/async": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", + "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=", + "dev": true + }, + "node_modules/proxy-from-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", + "integrity": "sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4=", + "dev": true + }, + "node_modules/psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", + "dev": true + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "dev": true, + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ramda": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.1.tgz", + "integrity": "sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw==", + "dev": true + }, + "node_modules/read": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", + "dev": true, + "dependencies": { + "mute-stream": "~0.0.4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/request-progress": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", + "integrity": "sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4=", + "dev": true, + "dependencies": { + "throttleit": "^1.0.0" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/revalidator": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/revalidator/-/revalidator-0.1.8.tgz", + "integrity": "sha1-/s5hv6DBtSoga9axgZgYS91SOjs=", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz", + "integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==", + "dev": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "dev": true, + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/throttleit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", + "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=", + "dev": true + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz", + "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/umbraco-cypress-testhelpers": { + "version": "1.0.0-beta-60", + "resolved": "https://registry.npmjs.org/umbraco-cypress-testhelpers/-/umbraco-cypress-testhelpers-1.0.0-beta-60.tgz", + "integrity": "sha512-VEe6r7G9nBwtATxAZPFOY4utCpsmKV8oK5FbPV4TrEyMH08hmAYmEq+w84Bq1Q85khG01ivyMCR/8sYUEHTWIg==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "camelize": "^1.0.0", + "faker": "^4.1.0" + }, + "peerDependencies": { + "cross-env": "^7.0.2", + "cypress": "^8.7.0", + "ncp": "^2.0.0" + } + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/winston": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.5.tgz", + "integrity": "sha512-TWoamHt5yYvsMarGlGEQE59SbJHqGsZV8/lwC+iCcGeAe0vUaOh+Lv6SYM17ouzC/a/LB1/hz/7sxFBtlu1l4A==", + "dev": true, + "dependencies": { + "async": "~1.0.0", + "colors": "1.0.x", + "cycle": "1.0.x", + "eyes": "0.1.x", + "isstream": "0.1.x", + "stack-trace": "0.0.x" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/winston/node_modules/async": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async/-/async-1.0.0.tgz", + "integrity": "sha1-+PwEyjoTeErenhZBr5hXjPvWR6k=", + "dev": true + }, + "node_modules/winston/node_modules/colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + } + }, "dependencies": { "@cypress/request": { "version": "2.88.6", @@ -199,9 +2528,9 @@ "dev": true }, "async": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.1.tgz", - "integrity": "sha512-XdD5lRO/87udXCMC9meWdYiR+Nq6ZjUfXidViUZGu2F1MO4T3XwZ1et0hb2++BgLfhyJwy44BGB/yx80ABx8hg==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.2.tgz", + "integrity": "sha512-H0E+qZaDEfx/FY4t7iLRv1W2fFI6+pyCeTw1uN20AQPiwqwM6ojPxHxdLv4z8hi2DtnW9BOckSspLucW7pIE5g==", "dev": true }, "asynckit": { @@ -454,9 +2783,9 @@ "dev": true }, "cypress": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-8.4.1.tgz", - "integrity": "sha512-itJXq0Vx3sXCUrDyBi2IUrkxVu/gTTp1VhjB5tzGgkeCR8Ae+/T8WV63rsZ7fS8Tpq7LPPXiyoM/sEdOX7cR6A==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-8.7.0.tgz", + "integrity": "sha512-b1bMC3VQydC6sXzBMFnSqcvwc9dTZMgcaOzT0vpSD+Gq1yFc+72JDWi55sfUK5eIeNLAtWOGy1NNb6UlhMvB+Q==", "dev": true, "requires": { "@cypress/request": "^2.88.6", @@ -493,6 +2822,7 @@ "minimist": "^1.2.5", "ospath": "^1.2.2", "pretty-bytes": "^5.6.0", + "proxy-from-env": "1.0.0", "ramda": "~0.27.1", "request-progress": "^3.0.0", "supports-color": "^8.1.1", @@ -1321,6 +3651,12 @@ } } }, + "proxy-from-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", + "integrity": "sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4=", + "dev": true + }, "psl": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", @@ -1615,9 +3951,9 @@ "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==" }, "umbraco-cypress-testhelpers": { - "version": "1.0.0-beta-58", - "resolved": "https://registry.npmjs.org/umbraco-cypress-testhelpers/-/umbraco-cypress-testhelpers-1.0.0-beta-58.tgz", - "integrity": "sha512-qbkqGo+g4FzQTstYQXPYDjJyVRH+guNZt1WZXQ8v64dTucN/Ku/noowMA9y6q0mLOZ8+SR9xmMySHfa7E48Vfw==", + "version": "1.0.0-beta-60", + "resolved": "https://registry.npmjs.org/umbraco-cypress-testhelpers/-/umbraco-cypress-testhelpers-1.0.0-beta-60.tgz", + "integrity": "sha512-VEe6r7G9nBwtATxAZPFOY4utCpsmKV8oK5FbPV4TrEyMH08hmAYmEq+w84Bq1Q85khG01ivyMCR/8sYUEHTWIg==", "dev": true, "requires": { "camelize": "^1.0.0", diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index fd401c72c0cd..3b9b2864e1c9 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -9,11 +9,11 @@ }, "devDependencies": { "cross-env": "^7.0.2", - "cypress": "8.4.1", + "cypress": "^8.7.0", "del": "^6.0.0", "ncp": "^2.0.0", "prompt": "^1.2.0", - "umbraco-cypress-testhelpers": "^1.0.0-beta-58" + "umbraco-cypress-testhelpers": "^1.0.0-beta-60" }, "dependencies": { "typescript": "^3.9.2" diff --git a/tests/Umbraco.Tests.Common/Builders/ContentTypeBuilder.cs b/tests/Umbraco.Tests.Common/Builders/ContentTypeBuilder.cs index af51f6019ec2..d6202f27a497 100644 --- a/tests/Umbraco.Tests.Common/Builders/ContentTypeBuilder.cs +++ b/tests/Umbraco.Tests.Common/Builders/ContentTypeBuilder.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Tests.Common.Builders.Extensions; using Umbraco.Cms.Tests.Common.Builders.Interfaces; using Constants = Umbraco.Cms.Core.Constants; @@ -113,6 +114,7 @@ public override IContentType Build() contentType.CreatorId = GetCreatorId(); contentType.Trashed = GetTrashed(); contentType.IsContainer = GetIsContainer(); + contentType.HistoryCleanup = new HistoryCleanup(); contentType.Variations = contentVariation; diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs index dfe799e2b819..decd1d7c5c88 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs @@ -273,7 +273,7 @@ public void Can_Perform_Add_On_ContentTypeRepository() Assert.AreNotEqual(propertyType.Key, Guid.Empty); } - TestHelper.AssertPropertyValuesAreEqual(fetched, contentType, ignoreProperties: new[] { "DefaultTemplate", "AllowedTemplates", "UpdateDate" }); + TestHelper.AssertPropertyValuesAreEqual(fetched, contentType, ignoreProperties: new[] { "DefaultTemplate", "AllowedTemplates", "UpdateDate", "HistoryCleanup" }); } } @@ -378,6 +378,7 @@ private DocumentTypeSave MapToContentTypeSave(DocumentTypeDisplay display) => //// Alias = display.Alias, Path = display.Path, //// AdditionalData = display.AdditionalData, + HistoryCleanup = display.HistoryCleanup, // ContentTypeBasic Alias = display.Alias, diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentVersionRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentVersionRepositoryTest.cs new file mode 100644 index 000000000000..1649aa2fc6bd --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentVersionRepositoryTest.cs @@ -0,0 +1,134 @@ +using System.Diagnostics; +using System.Linq; +using NUnit.Framework; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repositories +{ + /// + /// v9 -> Tests.Integration + /// + [TestFixture] + [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] + public class DocumentVersionRepositoryTest : UmbracoIntegrationTest + { + public IFileService FileService => GetRequiredService(); + public IContentTypeService ContentTypeService => GetRequiredService(); + public IContentService ContentService => GetRequiredService(); + + [Test] + public void GetDocumentVersionsEligibleForCleanup_Always_ExcludesActiveVersions() + { + Template template = TemplateBuilder.CreateTextPageTemplate(); + FileService.SaveTemplate(template); + + var contentType = ContentTypeBuilder.CreateSimpleContentType("umbTextpage", "Textpage", defaultTemplateId: template.Id); + ContentTypeService.Save(contentType); + ContentTypeService.Save(contentType); + + var content = ContentBuilder.CreateSimpleContent(contentType); + + ContentService.SaveAndPublish(content); + // At this point content has 2 versions, a draft version and a published version. + + ContentService.SaveAndPublish(content); + // At this point content has 3 versions, a historic version, a draft version and a published version. + + using (ScopeProvider.CreateScope()) + { + var sut = new DocumentVersionRepository(ScopeAccessor); + var results = sut.GetDocumentVersionsEligibleForCleanup(); + + Assert.Multiple(() => + { + Assert.AreEqual(1, results.Count); + Assert.AreEqual(1, results.First().VersionId); + }); + } + } + + [Test] + public void GetDocumentVersionsEligibleForCleanup_Always_ExcludesPinnedVersions() + { + Template template = TemplateBuilder.CreateTextPageTemplate(); + FileService.SaveTemplate(template); + + var contentType = ContentTypeBuilder.CreateSimpleContentType("umbTextpage", "Textpage", defaultTemplateId: template.Id); + ContentTypeService.Save(contentType); + ContentTypeService.Save(contentType); + + var content = ContentBuilder.CreateSimpleContent(contentType); + + ContentService.SaveAndPublish(content); + // At this point content has 2 versions, a draft version and a published version. + ContentService.SaveAndPublish(content); + ContentService.SaveAndPublish(content); + ContentService.SaveAndPublish(content); + // At this point content has 5 versions, 3 historic versions, a draft version and a published version. + + var allVersions = ContentService.GetVersions(content.Id); + Debug.Assert(allVersions.Count() == 5); // Sanity check + + using (var scope = ScopeProvider.CreateScope()) + { + scope.Database.Update("set preventCleanup = 1 where id in (1,3)"); + + var sut = new DocumentVersionRepository(ScopeAccessor); + var results = sut.GetDocumentVersionsEligibleForCleanup(); + + Assert.Multiple(() => + { + Assert.AreEqual(1, results.Count); + + // We pinned 1 & 3 + // 4 is current + // 5 is published + // So all that is left is 2 + Assert.AreEqual(2, results.First().VersionId); + }); + } + } + + [Test] + public void DeleteVersions_Always_DeletesSpecifiedVersions() + { + Template template = TemplateBuilder.CreateTextPageTemplate(); + FileService.SaveTemplate(template); + + var contentType = ContentTypeBuilder.CreateSimpleContentType("umbTextpage", "Textpage", defaultTemplateId: template.Id); + ContentTypeService.Save(contentType); + + var content = ContentBuilder.CreateSimpleContent(contentType); + + ContentService.SaveAndPublish(content); + ContentService.SaveAndPublish(content); + ContentService.SaveAndPublish(content); + ContentService.SaveAndPublish(content); + using (var scope = ScopeProvider.CreateScope()) + { + var query = scope.SqlContext.Sql(); + + query.Select() + .From(); + + var sut = new DocumentVersionRepository(ScopeAccessor); + sut.DeleteVersions(new []{1,2,3}); + + var after = scope.Database.Fetch(query); + + Assert.Multiple(() => + { + Assert.AreEqual(2, after.Count); + Assert.True(after.All(x => x.Id > 3)); + }); + } + } + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs index 946d4a236a8d..04617ae5f32f 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs @@ -3,27 +3,26 @@ using System; using System.Linq; -using System.Threading; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using NPoco; using NUnit.Framework; -using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.PublishedCache; -using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; using Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping; using Umbraco.Extensions; +using Language = Umbraco.Cms.Core.Models.Language; namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services { @@ -1338,7 +1337,7 @@ private void CreateFrenchAndEnglishLangs() { Alias = alias, Name = alias, - Variations = variance + Variations = variance, }; private PropertyTypeCollection CreatePropertyCollection(params (string alias, ContentVariation variance)[] props) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentVersionCleanupServiceTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentVersionCleanupServiceTest.cs new file mode 100644 index 000000000000..08200a6f7e82 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentVersionCleanupServiceTest.cs @@ -0,0 +1,119 @@ +using System; +using System.Diagnostics; +using NUnit.Framework; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services +{ + [TestFixture] + [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] + internal class ContentVersionCleanupServiceTest : UmbracoIntegrationTest + { + public IFileService FileService => GetRequiredService(); + + public IContentTypeService ContentTypeService => GetRequiredService(); + + public IContentService ContentService => GetRequiredService(); + + public IContentVersionService ContentVersionService => GetRequiredService(); + + /// + /// This is covered by the unit tests, but nice to know it deletes on infra. + /// And proves implementation is compatible with SQL CE. + /// + [Test] + public void PerformContentVersionCleanup_WithNoKeepPeriods_DeletesEverythingExceptActive() + { + // For reference, Our currently has + // 5000 Documents + // With 200K Versions + // With 11M Property data + + Template template = TemplateBuilder.CreateTextPageTemplate(); + FileService.SaveTemplate(template); + + ContentType contentTypeA = ContentTypeBuilder.CreateSimpleContentType("contentTypeA", "contentTypeA", defaultTemplateId: template.Id); + + // Kill all historic + contentTypeA.HistoryCleanup.PreventCleanup = false; + contentTypeA.HistoryCleanup.KeepAllVersionsNewerThanDays = 0; + contentTypeA.HistoryCleanup.KeepLatestVersionPerDayForDays = 0; + + ContentTypeService.Save(contentTypeA); + + Content content = ContentBuilder.CreateSimpleContent(contentTypeA); + ContentService.SaveAndPublish(content); + + for (var i = 0; i < 10; i++) + { + ContentService.SaveAndPublish(content); + } + + Report before = GetReport(); + + Debug.Assert(before.ContentVersions == 12); // 10 historic + current draft + current published + Debug.Assert(before.PropertyData == 12 * 3); // CreateSimpleContentType = 3 props + + ContentVersionService.PerformContentVersionCleanup(DateTime.Now.AddHours(1)); + + Report after = GetReport(); + + Assert.Multiple(() => + { + Assert.AreEqual(2, after.ContentVersions); // current draft, current published + Assert.AreEqual(2, after.DocumentVersions); + Assert.AreEqual(6, after.PropertyData); // CreateSimpleContentType = 3 props + }); + } + + private Report GetReport() + { + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) + { + // SQL CE is fun! + var contentVersions = scope.Database.Single(@"select count(1) from umbracoContentVersion"); + var documentVersions = scope.Database.Single(@"select count(1) from umbracoDocumentVersion"); + var propertyData = scope.Database.Single(@"select count(1) from umbracoPropertyData"); + + return new Report + { + ContentVersions = contentVersions, + DocumentVersions = documentVersions, + PropertyData = propertyData + }; + } + } + + private void InsertCleanupPolicy(IContentType contentType, int daysToKeepAll, int daysToRollupAll, bool preventCleanup = false) + { + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) + { + var entity = new ContentVersionCleanupPolicyDto + { + ContentTypeId = contentType.Id, + KeepAllVersionsNewerThanDays = daysToKeepAll, + KeepLatestVersionPerDayForDays = daysToRollupAll, + PreventCleanup = preventCleanup, + Updated = DateTime.Today + }; + + scope.Database.Insert(entity); + } + } + + private class Report + { + public int ContentVersions { get; set; } + + public int DocumentVersions { get; set; } + + public int PropertyData { get; set; } + } + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Collections/StackQueueTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Collections/StackQueueTests.cs index 4caf7bd00536..6419463ca55b 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Collections/StackQueueTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Collections/StackQueueTests.cs @@ -10,7 +10,7 @@ public class StackQueueTests public void Queue() { var sq = new StackQueue(); - for (int i = 0; i < 3; i++) + for (var i = 0; i < 3; i++) { sq.Enqueue(i); } @@ -28,7 +28,7 @@ public void Queue() public void Stack() { var sq = new StackQueue(); - for (int i = 0; i < 3; i++) + for (var i = 0; i < 3; i++) { sq.Push(i); } @@ -46,7 +46,7 @@ public void Stack() public void Stack_And_Queue() { var sq = new StackQueue(); - for (int i = 0; i < 5; i++) + for (var i = 0; i < 5; i++) { if (i % 2 == 0) { diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Configurations/LanguageXmlTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Configurations/LanguageXmlTests.cs new file mode 100644 index 000000000000..623d207687c5 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Configurations/LanguageXmlTests.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml; +using NUnit.Framework; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Configurations +{ + [TestFixture] + public class LanguageXmlTests + { + [Test] + [Platform("Win")] //TODO figure out why Path.GetFullPath("/mnt/c/...") is not considered an absolute path on linux + mac + public void Can_Load_Language_Xml_Files() + { + var languageDirectoryPath = GetLanguageDirectory(); + var readFilesCount = 0; + var xmlDocument = new XmlDocument(); + + var directoryInfo = new DirectoryInfo(languageDirectoryPath); + + foreach (var languageFile in directoryInfo.GetFiles("*.xml", SearchOption.TopDirectoryOnly)) + { + // Load will throw an exception if the XML isn't valid. + xmlDocument.Load(languageFile.FullName); + readFilesCount++; + } + + // Ensure that at least one file was read. + Assert.AreNotEqual(0, readFilesCount); + } + + private static string GetLanguageDirectory() + { + var testDirectoryPathParts = Path.GetDirectoryName(TestContext.CurrentContext.TestDirectory) + .Split(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries); + + var solutionDirectoryPathParts = testDirectoryPathParts + .Take(Array.IndexOf(testDirectoryPathParts, "tests")); + var languageFolderPathParts = new List(solutionDirectoryPathParts); + var additionalPathParts = new[] { "Umbraco.Web.UI", "umbraco", "config", "lang" }; + languageFolderPathParts.AddRange(additionalPathParts); + + // Hack for build-server - when this path is generated in that envrionment it's missing the "src" folder. + // Not sure why, but if it's missing we'll add it in the right place. + if (!languageFolderPathParts.Contains("src")) + { + languageFolderPathParts.Insert(languageFolderPathParts.Count - additionalPathParts.Length, "src"); + } + + return string.Join(Path.DirectorySeparatorChar.ToString(), languageFolderPathParts); + } + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Scheduling/ContentVersionCleanupTest.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Scheduling/ContentVersionCleanupTest.cs new file mode 100644 index 000000000000..430cd184c155 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Scheduling/ContentVersionCleanupTest.cs @@ -0,0 +1,147 @@ +using System; +using System.Threading.Tasks; +using AutoFixture.NUnit3; +using Microsoft.Extensions.Options; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Runtime; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Infrastructure.HostedServices; +using Umbraco.Cms.Tests.UnitTests.AutoFixture; + +namespace Umbraco.Tests.Scheduling +{ + [TestFixture] + class ContentVersionCleanupTest + { + [Test, AutoMoqData] + public async Task ContentVersionCleanup_WhenNotEnabled_DoesNotCleanupWillRepeat( + [Frozen] Mock> settings, + [Frozen] Mock mainDom, + [Frozen] Mock serverRoleAccessor, + [Frozen] Mock runtimeState, + [Frozen] Mock cleanupService, + ContentVersionCleanup sut) + { + settings.Setup(x => x.CurrentValue).Returns(new ContentSettings() + { + ContentVersionCleanupPolicy = new ContentVersionCleanupPolicySettings() + { + EnableCleanup = false + } + }); + runtimeState.Setup(x => x.Level).Returns(RuntimeLevel.Run); + mainDom.Setup(x => x.IsMainDom).Returns(true); + serverRoleAccessor.Setup(x => x.CurrentServerRole).Returns(ServerRole.SchedulingPublisher); + + await sut.PerformExecuteAsync(null); + + cleanupService.Verify(x => x.PerformContentVersionCleanup(It.IsAny()), Times.Never); + } + + [Test, AutoMoqData] + public async Task ContentVersionCleanup_RuntimeLevelNotRun_DoesNotCleanupWillRepeat( + [Frozen] Mock> settings, + [Frozen] Mock mainDom, + [Frozen] Mock serverRoleAccessor, + [Frozen] Mock runtimeState, + [Frozen] Mock cleanupService, + ContentVersionCleanup sut) + { + settings.Setup(x => x.CurrentValue).Returns(new ContentSettings() + { + ContentVersionCleanupPolicy = new ContentVersionCleanupPolicySettings() + { + EnableCleanup = true + } + }); + runtimeState.Setup(x => x.Level).Returns(RuntimeLevel.Unknown); + mainDom.Setup(x => x.IsMainDom).Returns(true); + serverRoleAccessor.Setup(x => x.CurrentServerRole).Returns(ServerRole.SchedulingPublisher); + + await sut.PerformExecuteAsync(null); + + cleanupService.Verify(x => x.PerformContentVersionCleanup(It.IsAny()), Times.Never); + } + + [Test, AutoMoqData] + public async Task ContentVersionCleanup_ServerRoleUnknown_DoesNotCleanupWillRepeat( + [Frozen] Mock> settings, + [Frozen] Mock mainDom, + [Frozen] Mock serverRoleAccessor, + [Frozen] Mock runtimeState, + [Frozen] Mock cleanupService, + ContentVersionCleanup sut) + { + settings.Setup(x => x.CurrentValue).Returns(new ContentSettings() + { + ContentVersionCleanupPolicy = new ContentVersionCleanupPolicySettings() + { + EnableCleanup = true + } + }); + runtimeState.Setup(x => x.Level).Returns(RuntimeLevel.Run); + mainDom.Setup(x => x.IsMainDom).Returns(true); + serverRoleAccessor.Setup(x => x.CurrentServerRole).Returns(ServerRole.Unknown); + + await sut.PerformExecuteAsync(null); + + cleanupService.Verify(x => x.PerformContentVersionCleanup(It.IsAny()), Times.Never); + } + + [Test, AutoMoqData] + public async Task ContentVersionCleanup_NotMainDom_DoesNotCleanupWillNotRepeat( + [Frozen] Mock> settings, + [Frozen] Mock mainDom, + [Frozen] Mock serverRoleAccessor, + [Frozen] Mock runtimeState, + [Frozen] Mock cleanupService, + ContentVersionCleanup sut) + { + settings.Setup(x => x.CurrentValue).Returns(new ContentSettings() + { + ContentVersionCleanupPolicy = new ContentVersionCleanupPolicySettings() + { + EnableCleanup = true + } + }); + + runtimeState.Setup(x => x.Level).Returns(RuntimeLevel.Run); + mainDom.Setup(x => x.IsMainDom).Returns(false); + serverRoleAccessor.Setup(x => x.CurrentServerRole).Returns(ServerRole.SchedulingPublisher); + + await sut.PerformExecuteAsync(null); + + cleanupService.Verify(x => x.PerformContentVersionCleanup(It.IsAny()), Times.Never); + } + + [Test, AutoMoqData] + public async Task ContentVersionCleanup_Enabled_DelegatesToCleanupService( + [Frozen] Mock> settings, + [Frozen] Mock mainDom, + [Frozen] Mock serverRoleAccessor, + [Frozen] Mock runtimeState, + [Frozen] Mock cleanupService, + ContentVersionCleanup sut) + { + settings.Setup(x => x.CurrentValue).Returns(new ContentSettings() + { + ContentVersionCleanupPolicy = new ContentVersionCleanupPolicySettings() + { + EnableCleanup = true + } + }); + + runtimeState.Setup(x => x.Level).Returns(RuntimeLevel.Run); + mainDom.Setup(x => x.IsMainDom).Returns(true); + serverRoleAccessor.Setup(x => x.CurrentServerRole).Returns(ServerRole.SchedulingPublisher); + + await sut.PerformExecuteAsync(null); + + cleanupService.Verify(x => x.PerformContentVersionCleanup(It.IsAny()), Times.Once); + } + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentVersionCleanupServiceTest.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentVersionCleanupServiceTest.cs new file mode 100644 index 000000000000..00f5e53b47a5 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentVersionCleanupServiceTest.cs @@ -0,0 +1,266 @@ +// using System; +// using System.Collections.Generic; +// using System.Diagnostics; +// using System.Linq; +// using AutoFixture.NUnit3; +// using CSharpTest.Net.Interfaces; +// using Moq; +// using NUnit.Framework; +// using Umbraco.Cms.Core.Events; +// using Umbraco.Cms.Core.Models; +// using Umbraco.Cms.Core.Scoping; +// using Umbraco.Cms.Core.Services; +// using Umbraco.Cms.Core.Services.Implement; +// using Umbraco.Cms.Tests.UnitTests.AutoFixture; +// using Umbraco.Core.Composing; +// using Umbraco.Core.Events; +// using Umbraco.Core.Models; +// using Umbraco.Core.Persistence.Repositories; +// using Umbraco.Core.Scoping; +// using Umbraco.Core.Services; +// using Umbraco.Core.Services.Implement; +// using Umbraco.Tests.Testing; +// +// namespace Umbraco.Tests.Services +// { +// /// +// /// v9 -> Tests.UnitTests +// /// Sut here is ContentService, but in v9 should be a new class +// /// +// [TestFixture] +// public class ContentVersionCleanupServiceTest +// { +// +// +// /// +// /// For v9 this just needs a rewrite, no static events, no service location etc +// /// +// [Test, AutoMoqData] +// public void PerformContentVersionCleanup_Always_RespectsDeleteRevisionsCancellation( +// [Frozen] Mock scope, +// Mock documentVersionRepository, +// List someHistoricVersions, +// DateTime aDateTime, +// ContentService sut) +// { +// factory.Setup(x => x.GetInstance(typeof(IDocumentVersionRepository))) +// .Returns(documentVersionRepository.Object); +// +// factory.Setup(x => x.GetInstance(typeof(IContentVersionCleanupPolicy))) +// .Returns(new EchoingCleanupPolicyStub()); +// +// documentVersionRepository.Setup(x => x.GetDocumentVersionsEligibleForCleanup()) +// .Returns(someHistoricVersions); +// +// scope.Setup(x => x.Events).Returns(new PassThroughEventDispatcher()); +// +// // Wire up service locator +// Current.Factory = factory.Object; +// +// void OnDeletingVersions(IContentService sender, DeleteRevisionsEventArgs args) => args.Cancel = true; +// +// ContentService.DeletingVersions += OnDeletingVersions; +// +// // # Act +// var report = sut.PerformContentVersionCleanup(aDateTime); +// +// ContentService.DeletingVersions -= OnDeletingVersions; +// +// Assert.AreEqual(0, report.Count); +// } +// +// /// +// /// For v9 this just needs a rewrite, no static events, no service location etc +// /// +// [Test, AutoMoqData] +// public void PerformContentVersionCleanup_Always_FiresDeletedVersionsForEachDeletedVersion( +// [Frozen] Mock factory, +// [Frozen] Mock scope, +// Mock documentVersionRepository, +// List someHistoricVersions, +// DateTime aDateTime, +// ContentService sut) +// { +// factory.Setup(x => x.GetInstance(typeof(IDocumentVersionRepository))) +// .Returns(documentVersionRepository.Object); +// +// factory.Setup(x => x.GetInstance(typeof(IContentVersionCleanupPolicy))) +// .Returns(new EchoingCleanupPolicyStub()); +// +// documentVersionRepository.Setup(x => x.GetDocumentVersionsEligibleForCleanup()) +// .Returns(someHistoricVersions); +// +// scope.Setup(x => x.Events).Returns(new PassThroughEventDispatcher()); +// +// // Wire up service locator +// Current.Factory = factory.Object; +// +// // v9 can Mock + Verify +// var deletedAccordingToEvents = 0; +// void OnDeletedVersions(IContentService sender, DeleteRevisionsEventArgs args) => deletedAccordingToEvents++; +// +// ContentService.DeletedVersions += OnDeletedVersions; +// +// // # Act +// sut.PerformContentVersionCleanup(aDateTime); +// +// ContentService.DeletedVersions -= OnDeletedVersions; +// +// Assert.Multiple(() => +// { +// Assert.Greater(deletedAccordingToEvents, 0); +// Assert.AreEqual(someHistoricVersions.Count, deletedAccordingToEvents); +// }); +// } +// +// /// +// /// For v9 this just needs a rewrite, no static events, no service location etc +// /// +// [Test, AutoMoqData] +// public void PerformContentVersionCleanup_Always_ReturnsReportOfDeletedItems( +// [Frozen] Mock factory, +// [Frozen] Mock scope, +// Mock documentVersionRepository, +// List someHistoricVersions, +// DateTime aDateTime, +// ContentService sut) +// { +// factory.Setup(x => x.GetInstance(typeof(IDocumentVersionRepository))) +// .Returns(documentVersionRepository.Object); +// +// factory.Setup(x => x.GetInstance(typeof(IContentVersionCleanupPolicy))) +// .Returns(new EchoingCleanupPolicyStub()); +// +// documentVersionRepository.Setup(x => x.GetDocumentVersionsEligibleForCleanup()) +// .Returns(someHistoricVersions); +// +// scope.Setup(x => x.Events).Returns(new PassThroughEventDispatcher()); +// +// // Wire up service locator +// Current.Factory = factory.Object; +// +// // # Act +// var report = sut.PerformContentVersionCleanup(aDateTime); +// +// Assert.Multiple(() => +// { +// Assert.Greater(report.Count, 0); +// Assert.AreEqual(someHistoricVersions.Count, report.Count); +// }); +// } +// +// /// +// /// For v9 this just needs a rewrite, no static events, no service location etc +// /// +// [Test, AutoMoqData] +// public void PerformContentVersionCleanup_Always_AdheresToCleanupPolicy( +// [Frozen] Mock factory, +// [Frozen] Mock scope, +// Mock documentVersionRepository, +// Mock cleanupPolicy, +// List someHistoricVersions, +// DateTime aDateTime, +// ContentService sut) +// { +// factory.Setup(x => x.GetInstance(typeof(IDocumentVersionRepository))) +// .Returns(documentVersionRepository.Object); +// +// factory.Setup(x => x.GetInstance(typeof(IContentVersionCleanupPolicy))) +// .Returns(cleanupPolicy.Object); +// +// documentVersionRepository.Setup(x => x.GetDocumentVersionsEligibleForCleanup()) +// .Returns(someHistoricVersions); +// +// scope.Setup(x => x.Events).Returns(new PassThroughEventDispatcher()); +// +// cleanupPolicy.Setup(x => x.Apply(It.IsAny(), It.IsAny>())) +// .Returns>((_, items) => items.Take(1)); +// +// // Wire up service locator +// Current.Factory = factory.Object; +// +// // # Act +// var report = sut.PerformContentVersionCleanup(aDateTime); +// +// Debug.Assert(someHistoricVersions.Count > 1); +// +// Assert.Multiple(() => +// { +// cleanupPolicy.Verify(x => x.Apply(aDateTime, someHistoricVersions), Times.Once); +// Assert.AreEqual(someHistoricVersions.First(), report.Single()); +// }); +// } +// +// /// +// /// For v9 this just needs a rewrite, no static events, no service location etc +// /// +// [Test, AutoMoqData] +// public void PerformContentVersionCleanup_HasVersionsToDelete_CallsDeleteOnRepositoryWithFilteredSet( +// [Frozen] Mock factory, +// [Frozen] Mock scope, +// Mock documentVersionRepository, +// Mock cleanupPolicy, +// List someHistoricVersions, +// DateTime aDateTime, +// ContentService sut) +// { +// factory.Setup(x => x.GetInstance(typeof(IDocumentVersionRepository))) +// .Returns(documentVersionRepository.Object); +// +// factory.Setup(x => x.GetInstance(typeof(IContentVersionCleanupPolicy))) +// .Returns(cleanupPolicy.Object); +// +// documentVersionRepository.Setup(x => x.GetDocumentVersionsEligibleForCleanup()) +// .Returns(someHistoricVersions); +// +// scope.Setup(x => x.Events).Returns(new PassThroughEventDispatcher()); +// +// var filteredSet = someHistoricVersions.Take(1); +// +// cleanupPolicy.Setup(x => x.Apply(It.IsAny(), It.IsAny>())) +// .Returns>((_, items) => filteredSet); +// +// // Wire up service locator +// Current.Factory = factory.Object; +// +// // # Act +// var report = sut.PerformContentVersionCleanup(aDateTime); +// +// Debug.Assert(someHistoricVersions.Any()); +// +// var expectedId = filteredSet.First().VersionId; +// +// documentVersionRepository.Verify(x => x.DeleteVersions(It.Is>(y => y.Single() == expectedId)), Times.Once); +// } +// +// class EchoingCleanupPolicyStub : IContentVersionCleanupPolicy +// { +// /// +// /// What goes in, must come out +// /// +// public EchoingCleanupPolicyStub() { } +// +// /* Note: Could just wire up a mock but its quite wordy. +// * +// * cleanupPolicy.Setup(x => x.Apply(It.IsAny(), It.IsAny>())) +// * .Returns>((date, items) => items); +// */ +// public IEnumerable Apply( +// DateTime asAtDate, +// IEnumerable items +// ) => items; +// } +// +// /// +// /// NPoco < 5 requires a parameter-less constructor but plays nice with get-only properties. +// /// Moq won't play nice with get-only properties, but doesn't require a parameter-less constructor. +// /// +// /// Inheritance solves this so that we get values for test data without a specimen builder +// /// +// public class TestHistoricContentVersionMeta : HistoricContentVersionMeta +// { +// public TestHistoricContentVersionMeta(int contentId, int contentTypeId, int versionId, DateTime versionDate) +// : base(contentId, contentTypeId, versionId, versionDate) { } +// } +// } +// } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/DefaultContentVersionCleanupPolicyTest.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/DefaultContentVersionCleanupPolicyTest.cs new file mode 100644 index 000000000000..db258c5fa150 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/DefaultContentVersionCleanupPolicyTest.cs @@ -0,0 +1,265 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AutoFixture.NUnit3; +using Microsoft.Extensions.Options; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Infrastructure.Services.Implement; +using Umbraco.Cms.Tests.UnitTests.AutoFixture; +using ContentVersionCleanupPolicySettings = Umbraco.Cms.Core.Models.ContentVersionCleanupPolicySettings; + +namespace Umbraco.Tests.Services +{ + [TestFixture] + public class DefaultContentVersionCleanupPolicyTest + { + [Test, AutoMoqData] + public void Apply_AllOlderThanKeepSettings_AllVersionsReturned( + [Frozen] Mock documentVersionRepository, + [Frozen] Mock> contentSettings, + DefaultContentVersionCleanupPolicy sut) + { + var versionId = 0; + + var historicItems = new List + { + new HistoricContentVersionMeta(versionId: ++versionId, contentId: 1, contentTypeId: 1, versionDate: DateTime.Today.AddHours(-1)), + new HistoricContentVersionMeta(versionId: ++versionId, contentId: 1, contentTypeId: 1, versionDate: DateTime.Today.AddHours(-1)), + }; + + contentSettings.Setup(x => x.Value).Returns(new ContentSettings() + { + ContentVersionCleanupPolicy = new Cms.Core.Configuration.Models.ContentVersionCleanupPolicySettings() + { + EnableCleanup = true, + KeepAllVersionsNewerThanDays = 0, + KeepLatestVersionPerDayForDays = 0 + } + }); + + documentVersionRepository.Setup(x => x.GetCleanupPolicies()) + .Returns(Array.Empty()); + + documentVersionRepository.Setup(x => x.GetDocumentVersionsEligibleForCleanup()) + .Returns(historicItems); + + var results = sut.Apply(DateTime.Today, historicItems).ToList(); + + Assert.AreEqual(2, results.Count); + } + + [Test, AutoMoqData] + public void Apply_OverlappingKeepSettings_KeepAllVersionsNewerThanDaysTakesPriority( + [Frozen] Mock documentVersionRepository, + [Frozen] Mock> contentSettings, + DefaultContentVersionCleanupPolicy sut) + { + var versionId = 0; + + var historicItems = new List + { + new HistoricContentVersionMeta(versionId: ++versionId, contentId: 1, contentTypeId: 1, versionDate: DateTime.Today.AddHours(-1)), + new HistoricContentVersionMeta(versionId: ++versionId, contentId: 1, contentTypeId: 1, versionDate: DateTime.Today.AddHours(-1)), + }; + + contentSettings.Setup(x => x.Value).Returns(new ContentSettings() + { + ContentVersionCleanupPolicy = new Cms.Core.Configuration.Models.ContentVersionCleanupPolicySettings() + { + EnableCleanup = true, + KeepAllVersionsNewerThanDays = 2, + KeepLatestVersionPerDayForDays = 2 + } + }); + + documentVersionRepository.Setup(x => x.GetCleanupPolicies()) + .Returns(Array.Empty()); + + documentVersionRepository.Setup(x => x.GetDocumentVersionsEligibleForCleanup()) + .Returns(historicItems); + + var results = sut.Apply(DateTime.Today, historicItems).ToList(); + + Assert.AreEqual(0, results.Count); + } + + [Test, AutoMoqData] + public void Apply_WithinInKeepLatestPerDay_ReturnsSinglePerContentPerDay( + [Frozen] Mock documentVersionRepository, + [Frozen] Mock> contentSettings, + DefaultContentVersionCleanupPolicy sut) + { + var historicItems = new List + { + new HistoricContentVersionMeta(versionId: 1, contentId: 1, contentTypeId: 1, versionDate: DateTime.Today.AddHours(-3)), + new HistoricContentVersionMeta(versionId: 2, contentId: 1, contentTypeId: 1, versionDate: DateTime.Today.AddHours(-2)), + new HistoricContentVersionMeta(versionId: 3, contentId: 1, contentTypeId: 1, versionDate: DateTime.Today.AddHours(-1)), + + new HistoricContentVersionMeta(versionId: 4, contentId: 1, contentTypeId: 1, versionDate: DateTime.Today.AddDays(-1).AddHours(-3)), + new HistoricContentVersionMeta(versionId: 5, contentId: 1, contentTypeId: 1, versionDate: DateTime.Today.AddDays(-1).AddHours(-2)), + new HistoricContentVersionMeta(versionId: 6, contentId: 1, contentTypeId: 1, versionDate: DateTime.Today.AddDays(-1).AddHours(-1)), + // another content + new HistoricContentVersionMeta(versionId: 7, contentId: 2, contentTypeId: 1, versionDate: DateTime.Today.AddHours(-3)), + new HistoricContentVersionMeta(versionId: 8, contentId: 2, contentTypeId: 1, versionDate: DateTime.Today.AddHours(-2)), + new HistoricContentVersionMeta(versionId: 9, contentId: 2, contentTypeId: 1, versionDate: DateTime.Today.AddHours(-1)), + }; + + contentSettings.Setup(x => x.Value).Returns(new ContentSettings() + { + ContentVersionCleanupPolicy = new Cms.Core.Configuration.Models.ContentVersionCleanupPolicySettings() + { + EnableCleanup = true, + KeepAllVersionsNewerThanDays = 0, + KeepLatestVersionPerDayForDays = 3 + } + }); + + documentVersionRepository.Setup(x => x.GetCleanupPolicies()) + .Returns(Array.Empty()); + + documentVersionRepository.Setup(x => x.GetDocumentVersionsEligibleForCleanup()) + .Returns(historicItems); + + var results = sut.Apply(DateTime.Today, historicItems).ToList(); + + Assert.Multiple(() => + { + Assert.AreEqual(3, results.Count); + Assert.True(results.Exists(x => x.VersionId == 3)); + Assert.True(results.Exists(x => x.VersionId == 6)); + Assert.True(results.Exists(x => x.VersionId == 9)); + }); + } + + [Test, AutoMoqData] + public void Apply_HasOverridePolicy_RespectsPreventCleanup( + [Frozen] Mock documentVersionRepository, + [Frozen] Mock> contentSettings, + DefaultContentVersionCleanupPolicy sut) + { + var historicItems = new List + { + new HistoricContentVersionMeta(versionId: 1, contentId: 1, contentTypeId: 1, versionDate: DateTime.Today.AddHours(-3)), + new HistoricContentVersionMeta(versionId: 2, contentId: 1, contentTypeId: 1, versionDate: DateTime.Today.AddHours(-2)), + new HistoricContentVersionMeta(versionId: 3, contentId: 1, contentTypeId: 1, versionDate: DateTime.Today.AddHours(-1)), + // another content & type + new HistoricContentVersionMeta(versionId: 4, contentId: 2, contentTypeId: 2, versionDate: DateTime.Today.AddHours(-3)), + new HistoricContentVersionMeta(versionId: 5, contentId: 2, contentTypeId: 2, versionDate: DateTime.Today.AddHours(-2)), + new HistoricContentVersionMeta(versionId: 6, contentId: 2, contentTypeId: 2, versionDate: DateTime.Today.AddHours(-1)), + }; + + contentSettings.Setup(x => x.Value).Returns(new ContentSettings() + { + ContentVersionCleanupPolicy = new Cms.Core.Configuration.Models.ContentVersionCleanupPolicySettings() + { + EnableCleanup = true, + KeepAllVersionsNewerThanDays = 0, + KeepLatestVersionPerDayForDays = 0 + } + }); + + documentVersionRepository.Setup(x => x.GetCleanupPolicies()) + .Returns(new ContentVersionCleanupPolicySettings[] + { + new ContentVersionCleanupPolicySettings{ ContentTypeId = 2, PreventCleanup = true } + }); + + documentVersionRepository.Setup(x => x.GetDocumentVersionsEligibleForCleanup()) + .Returns(historicItems); + + var results = sut.Apply(DateTime.Today, historicItems).ToList(); + + Assert.True(results.All(x => x.ContentTypeId == 1)); + } + + [Test, AutoMoqData] + public void Apply_HasOverridePolicy_RespectsKeepAll( + [Frozen] Mock documentVersionRepository, + [Frozen] Mock> contentSettings, + DefaultContentVersionCleanupPolicy sut) + { + var historicItems = new List + { + new HistoricContentVersionMeta(versionId: 1, contentId: 1, contentTypeId: 1, versionDate: DateTime.Today.AddHours(-3)), + new HistoricContentVersionMeta(versionId: 2, contentId: 1, contentTypeId: 1, versionDate: DateTime.Today.AddHours(-2)), + new HistoricContentVersionMeta(versionId: 3, contentId: 1, contentTypeId: 1, versionDate: DateTime.Today.AddHours(-1)), + // another content & type + new HistoricContentVersionMeta(versionId: 4, contentId: 2, contentTypeId: 2, versionDate: DateTime.Today.AddHours(-3)), + new HistoricContentVersionMeta(versionId: 5, contentId: 2, contentTypeId: 2, versionDate: DateTime.Today.AddHours(-2)), + new HistoricContentVersionMeta(versionId: 6, contentId: 2, contentTypeId: 2, versionDate: DateTime.Today.AddHours(-1)), + }; + + contentSettings.Setup(x => x.Value).Returns(new ContentSettings() + { + ContentVersionCleanupPolicy = new Cms.Core.Configuration.Models.ContentVersionCleanupPolicySettings() + { + EnableCleanup = true, + KeepAllVersionsNewerThanDays = 0, + KeepLatestVersionPerDayForDays = 0 + } + }); + + documentVersionRepository.Setup(x => x.GetCleanupPolicies()) + .Returns(new ContentVersionCleanupPolicySettings[] + { + new ContentVersionCleanupPolicySettings{ ContentTypeId = 2, PreventCleanup = false, KeepAllVersionsNewerThanDays = 3 } + }); + + documentVersionRepository.Setup(x => x.GetDocumentVersionsEligibleForCleanup()) + .Returns(historicItems); + + var results = sut.Apply(DateTime.Today, historicItems).ToList(); + + Assert.True(results.All(x => x.ContentTypeId == 1)); + } + + [Test, AutoMoqData] + public void Apply_HasOverridePolicy_RespectsKeepLatest( + [Frozen] Mock documentVersionRepository, + [Frozen] Mock> contentSettings, + DefaultContentVersionCleanupPolicy sut) + { + var historicItems = new List + { + new HistoricContentVersionMeta(versionId: 1, contentId: 1, contentTypeId: 1, versionDate: DateTime.Today.AddHours(-3)), + new HistoricContentVersionMeta(versionId: 2, contentId: 1, contentTypeId: 1, versionDate: DateTime.Today.AddHours(-2)), + new HistoricContentVersionMeta(versionId: 3, contentId: 1, contentTypeId: 1, versionDate: DateTime.Today.AddHours(-1)), + // another content & type + new HistoricContentVersionMeta(versionId: 4, contentId: 2, contentTypeId: 2, versionDate: DateTime.Today.AddHours(-3)), + new HistoricContentVersionMeta(versionId: 5, contentId: 2, contentTypeId: 2, versionDate: DateTime.Today.AddHours(-2)), + new HistoricContentVersionMeta(versionId: 6, contentId: 2, contentTypeId: 2, versionDate: DateTime.Today.AddHours(-1)), + }; + + contentSettings.Setup(x => x.Value).Returns(new ContentSettings() + { + ContentVersionCleanupPolicy = new Cms.Core.Configuration.Models.ContentVersionCleanupPolicySettings() + { + EnableCleanup = true, + KeepAllVersionsNewerThanDays = 0, + KeepLatestVersionPerDayForDays = 0 + } + }); + + documentVersionRepository.Setup(x => x.GetCleanupPolicies()) + .Returns(new ContentVersionCleanupPolicySettings[] + { + new ContentVersionCleanupPolicySettings{ ContentTypeId = 2, PreventCleanup = false, KeepLatestVersionPerDayForDays = 3 } + }); + + documentVersionRepository.Setup(x => x.GetDocumentVersionsEligibleForCleanup()) + .Returns(historicItems); + + var results = sut.Apply(DateTime.Today, historicItems).ToList(); + + Assert.Multiple(() => + { + Assert.AreEqual(3, results.Count(x => x.ContentTypeId == 1)); + Assert.AreEqual(6, results.Single(x => x.ContentTypeId == 2).VersionId); + }); + } + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj index eee8083425b2..668741bb87a2 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj @@ -33,6 +33,7 @@ + diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs index 2da13a4bedfa..d1ce62242e6b 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs @@ -502,7 +502,8 @@ private MemberController CreateSut( new Mock().Object, mockShortStringHelper, globalSettings, - new Mock().Object) + new Mock().Object, + new Mock>().Object) }); var scopeProvider = Mock.Of(x => x.CreateScope( It.IsAny(),