diff --git a/build/templates/UmbracoProject/appsettings.json b/build/templates/UmbracoProject/appsettings.json index feb6b07d9531..d282abc86f1a 100644 --- a/build/templates/UmbracoProject/appsettings.json +++ b/build/templates/UmbracoProject/appsettings.json @@ -29,6 +29,11 @@ //#endif "Hosting": { "Debug": false + }, + "Content": { + "ContentVersionCleanupPolicy": { + "EnableCleanup": true + } } } } 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/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/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 8ed2205f5950..56cb1e6d01eb 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -15,20 +15,20 @@ - - - - - - - + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + all @@ -56,6 +56,10 @@ - + + + + + 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/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/DocumentRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs index 75d66cf09d7f..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) 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/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.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/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/views/documentTypes/views/permissions/permissions.controller.js b/src/Umbraco.Web.UI.Client/src/views/documentTypes/views/permissions/permissions.controller.js index da293a6820d9..dc2520020124 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documentTypes/views/permissions/permissions.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/documentTypes/views/permissions/permissions.controller.js @@ -27,6 +27,7 @@ vm.toggleAllowSegmentVariants = toggleAllowSegmentVariants; vm.canToggleIsElement = false; vm.toggleIsElement = toggleIsElement; + vm.toggleHistoryCleanupPreventCleanup = toggleHistoryCleanupPreventCleanup; /* ---------- INIT ---------- */ @@ -55,6 +56,11 @@ } else { vm.canToggleIsElement = true; } + + if(!$scope.model.historyCleanup){ + $scope.model.historyCleanup = {}; + } + } function addChild($event) { @@ -62,7 +68,7 @@ var editor = { multiPicker: true, filterCssClass: 'not-allowed not-published', - filter: item => + filter: item => !vm.contentTypes.some(x => x.udi == item.udi) || vm.selectedChildren.some(x => x.udi === item.udi), submit: model => { model.selection.forEach(item => @@ -73,7 +79,7 @@ editorService.close(); }, - close: () => editorService.close() + close: () => editorService.close() }; editorService.contentTypePicker(editor); @@ -111,6 +117,10 @@ $scope.model.isElement = $scope.model.isElement ? false : true; } + function toggleHistoryCleanupPreventCleanup() { + $scope.model.historyCleanup.preventCleanup = $scope.model.historyCleanup.preventCleanup ? false : true; + } + } angular.module('umbraco').controller('Umbraco.Editors.DocumentType.PermissionsController', PermissionsController); diff --git a/src/Umbraco.Web.UI.Client/src/views/documentTypes/views/permissions/permissions.html b/src/Umbraco.Web.UI.Client/src/views/documentTypes/views/permissions/permissions.html index 96cf9b8ec471..fdbca299b26b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documentTypes/views/permissions/permissions.html +++ b/src/Umbraco.Web.UI.Client/src/views/documentTypes/views/permissions/permissions.html @@ -92,6 +92,36 @@
Is an Element Type + +
+ +
+
+ +
+ +
+ +
+

+
+ + + + + + + + + + + + +
+
+ +
+ diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index 72ad387baed4..141cdc48fbd2 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -1824,6 +1824,12 @@ To manage your website, simply open the Umbraco backoffice and start adding cont 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/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index eb8538af2078..c9764c0d3ccd 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -1881,6 +1881,12 @@ To manage your website, simply open the Umbraco backoffice and start adding cont 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/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/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(),