Merge remote-tracking branch 'origin/v13/dev' into v14/dev

# Conflicts:
#	Directory.Build.props
#	src/Umbraco.Web.BackOffice/Controllers/ContentController.cs
This commit is contained in:
Bjarke Berg
2023-12-22 14:19:04 +01:00
11 changed files with 349 additions and 35 deletions

View File

@@ -34,7 +34,7 @@ public class ArtifactDependency
/// <value>
/// <c>true</c> if the dependency is included when building a dependency tree and gets deployed in the correct order; otherwise, <c>false</c>.
/// </value>
public bool Ordering { get; }
public bool Ordering { get; internal set; }
/// <summary>
/// Gets the dependency mode.
@@ -42,7 +42,7 @@ public class ArtifactDependency
/// <value>
/// The dependency mode.
/// </value>
public ArtifactDependencyMode Mode { get; }
public ArtifactDependencyMode Mode { get; internal set; }
/// <summary>
/// Gets or sets the checksum.

View File

@@ -21,25 +21,39 @@ public class ArtifactDependencyCollection : ICollection<ArtifactDependency>
/// <inheritdoc />
public void Add(ArtifactDependency item)
{
if (item.Mode == ArtifactDependencyMode.Exist &&
_dependencies.TryGetValue(item.Udi, out ArtifactDependency? existingItem) &&
existingItem.Mode == ArtifactDependencyMode.Match)
if (_dependencies.TryGetValue(item.Udi, out ArtifactDependency? existingItem))
{
// Don't downgrade dependency mode from Match to Exist
return;
}
// Update existing item
if (existingItem.Mode is ArtifactDependencyMode.Exist)
{
// Allow updating dependency mode from Exist to Match
existingItem.Mode = item.Mode;
}
_dependencies[item.Udi] = item;
if (existingItem.Ordering is false)
{
// Allow updating non-ordering to ordering
existingItem.Ordering = item.Ordering;
}
if (string.IsNullOrEmpty(item.Checksum) is false)
{
// Allow updating checksum if set
existingItem.Checksum = item.Checksum;
}
}
else
{
// Add new item
_dependencies[item.Udi] = item;
}
}
/// <inheritdoc />
public void Clear() => _dependencies.Clear();
/// <inheritdoc />
public bool Contains(ArtifactDependency item)
=> _dependencies.TryGetValue(item.Udi, out ArtifactDependency? existingItem) &&
// Check whether it has the same or higher dependency mode
(existingItem.Mode == item.Mode || existingItem.Mode == ArtifactDependencyMode.Match);
public bool Contains(ArtifactDependency item) => _dependencies.ContainsKey(item.Udi);
/// <inheritdoc />
public void CopyTo(ArtifactDependency[] array, int arrayIndex) => _dependencies.Values.CopyTo(array, arrayIndex);

View File

@@ -0,0 +1,169 @@
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.Configuration;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Manifest;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Sync;
namespace Umbraco.Cms.Infrastructure.Migrations.PreMigration;
// we use a composer here so its easier to clean this up when we no longer need it.
// same for additional classes in the same file, nice and self contained
// should only be used to migrate from v13 to v14
// ⚠️ FIXME: PLEASE DELETE THIS IN V14! ⚠️
public class DataTypeSplitDataCollectorComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.AddNotificationHandler<UmbracoApplicationStartedNotification, DataTypeSplitDataCollector>();
}
}
public class DataTypeSplitDataCollector : INotificationHandler<UmbracoApplicationStartedNotification>
{
private readonly DataEditorCollection _dataEditors;
private readonly IManifestParser _manifestParser;
private readonly IDataTypeService _dataTypeService;
private readonly IKeyValueService _keyValueService;
private readonly IJsonSerializer _jsonSerializer;
private readonly ILogger<DataTypeSplitDataCollector> _logger;
private readonly ICoreScopeProvider _coreScopeProvider;
private readonly IRuntimeState _runtimeState;
private readonly IServerRoleAccessor _serverRoleAccessor;
private readonly IUmbracoVersion _umbracoVersion;
public DataTypeSplitDataCollector(
DataEditorCollection dataEditors,
IManifestParser manifestParser,
IDataTypeService dataTypeService,
IKeyValueService keyValueService,
IJsonSerializer jsonSerializer,
ILogger<DataTypeSplitDataCollector> logger,
ICoreScopeProvider coreScopeProvider,
IRuntimeState runtimeState,
IServerRoleAccessor serverRoleAccessor,
IUmbracoVersion umbracoVersion)
{
_dataEditors = dataEditors;
_manifestParser = manifestParser;
_dataTypeService = dataTypeService;
_keyValueService = keyValueService;
_jsonSerializer = jsonSerializer;
_logger = logger;
_coreScopeProvider = coreScopeProvider;
_runtimeState = runtimeState;
_serverRoleAccessor = serverRoleAccessor;
_umbracoVersion = umbracoVersion;
}
public void Handle(UmbracoApplicationStartedNotification notification)
{
// should only be used to collect data in v13
if (_umbracoVersion.Version.Major is not 13)
{
return;
}
// only run this if the application is actually running and not in an install/upgrade state
if (_runtimeState.Level != RuntimeLevel.Run)
{
return;
}
// do not run on load balanced subscribers
if (_serverRoleAccessor.CurrentServerRole == ServerRole.Subscriber)
{
return;
}
var manifestEditorsData = _manifestParser.CombinedManifest.PropertyEditors
.Select(pe => new EditorAliasSplitData(pe.Alias){EditorUiAlias = pe.Alias, EditorAlias = EditorAliasFromValueEditorValueType(pe.GetValueEditor().ValueType)})
.ToDictionary(data => data.OriginalEditorAlias);
_logger.LogDebug("Found {count} custom PropertyEditor(s) configured trough manifest files",manifestEditorsData.Count);
var fromCodeEditorsData = _dataEditors
.Where(de =>
de.GetType().Assembly.GetName().FullName
.StartsWith("umbraco.core", StringComparison.InvariantCultureIgnoreCase) is false
&& de.GetType().Assembly.GetName().FullName
.StartsWith("umbraco.infrastructure", StringComparison.InvariantCultureIgnoreCase) is false)
.Select(de => new EditorAliasSplitData(de.Alias) { EditorAlias = de.Alias })
.ToDictionary(data => data.OriginalEditorAlias);
_logger.LogDebug("Found {count} custom PropertyEditor(s) configured trough code",fromCodeEditorsData.Count);
var combinedEditorsData = new Dictionary<string, EditorAliasSplitData>(manifestEditorsData);
foreach (KeyValuePair<string,EditorAliasSplitData> pair in fromCodeEditorsData)
{
combinedEditorsData.Add(pair.Key,pair.Value);
}
if (combinedEditorsData.Any() == false)
{
_logger.LogDebug("No custom PropertyEditors found, skipping collection datatype migration data.");
return;
}
using ICoreScope coreScope = _coreScopeProvider.CreateCoreScope(autoComplete: true);
IEnumerable<IDataType> dataTypes = _dataTypeService.GetAll();
DataTypeEditorAliasMigrationData[] migrationData = dataTypes
.Where(dt => combinedEditorsData.ContainsKey(dt.EditorAlias))
.Select(dt => new DataTypeEditorAliasMigrationData
{
DataTypeId = dt.Id,
EditorAlias = combinedEditorsData[dt.EditorAlias].EditorAlias,
EditorUiAlias = combinedEditorsData[dt.EditorAlias].EditorUiAlias
}).ToArray();
_logger.LogDebug("Collected migration data for {count} DataType(s) that use custom PropertyEditors",migrationData.Length);
_keyValueService.SetValue("migrateDataEditorSplitCollectionData",_jsonSerializer.Serialize(migrationData));
}
private class EditorAliasSplitData
{
public EditorAliasSplitData(string originalEditorAlias)
{
OriginalEditorAlias = originalEditorAlias;
}
public string OriginalEditorAlias { get; init; }
public string? EditorUiAlias { get; init; }
public string? EditorAlias { get; init; }
}
private class DataTypeEditorAliasMigrationData
{
public int DataTypeId { get; set; }
public string? EditorUiAlias { get; init; }
public string? EditorAlias { get; init; }
}
private string EditorAliasFromValueEditorValueType(string valueType)
{
switch (valueType)
{
case ValueTypes.Date: return "Umbraco.Plain.DateTime";
case ValueTypes.DateTime: return "Umbraco.Plain.DateTime";
case ValueTypes.Decimal: return "Umbraco.Plain.Decimal";
case ValueTypes.Integer: return "Umbraco.Plain.Integer";
case ValueTypes.Bigint: return "Umbraco.Plain.Integer";
case ValueTypes.Json: return "Umbraco.Plain.Json";
case ValueTypes.Time: return "Umbraco.Plain.Time";
case ValueTypes.String: return "Umbraco.Plain.String";
case ValueTypes.Xml: return "Umbraco.Plain.String";
default:return string.Empty;
}
}
}

View File

@@ -1229,8 +1229,11 @@ AND umbracoNode.id <> @id",
/// If this is not done, then in some cases the "edited" value for a particular culture for a document will remain true
/// when it should be false
/// if the property was changed to invariant. In order to do this we need to recalculate this value based on the values
/// stored for each
/// property, culture and current/published version.
/// stored for each property, culture and current/published version.
///
/// Some of the sql statements in this function have a tendency to take a lot of parameters (nodeIds)
/// as the WhereIn Npoco method translates all the nodeIds being passed in as parameters when using the SqlClient provider.
/// this results in to many parameters (>2100) error => We need to batch the calls
/// </remarks>
private void RenormalizeDocumentEditedFlags(
IReadOnlyCollection<int> propertyTypeIds,
@@ -1386,16 +1389,19 @@ AND umbracoNode.id <> @id",
// Now bulk update the table DocumentCultureVariationDto, once for edited = true, another for edited = false
foreach (IGrouping<bool, DocumentCultureVariationDto> editValue in toUpdate.GroupBy(x => x.Edited))
{
Database.Execute(Sql().Update<DocumentCultureVariationDto>(u => u.Set(x => x.Edited, editValue.Key))
.WhereIn<DocumentCultureVariationDto>(x => x.Id, editValue.Select(x => x.Id)));
// update in batches to account for maximum parameter count
foreach (IEnumerable<DocumentCultureVariationDto> batchedValues in editValue.InGroupsOf(Constants.Sql.MaxParameterCount))
{
Database.Execute(Sql().Update<DocumentCultureVariationDto>(u => u.Set(x => x.Edited, editValue.Key))
.WhereIn<DocumentCultureVariationDto>(x => x.Id, batchedValues.Select(x => x.Id)));
}
}
// Now bulk update the umbracoDocument table
// we need to do this in batches as the WhereIn Npoco method translates to all the nodeIds being passed in as parameters when using the SqlClient provider
// this results in to many parameters (>2100) being passed to the client when there are a lot of documents being normalized
foreach (IGrouping<bool, KeyValuePair<int, bool>> groupByValue in editedDocument.GroupBy(x => x.Value))
{
foreach (IEnumerable<KeyValuePair<int, bool>> batch in groupByValue.InGroupsOf(2000))
// update in batches to account for maximum parameter count
foreach (IEnumerable<KeyValuePair<int, bool>> batch in groupByValue.InGroupsOf(Constants.Sql.MaxParameterCount))
{
Database.Execute(Sql().Update<DocumentDto>(u => u.Set(x => x.Edited, groupByValue.Key))
.WhereIn<DocumentDto>(x => x.NodeId, batch.Select(x => x.Key)));

View File

@@ -778,6 +778,7 @@ public class ContentController : ContentControllerBase
/// <param name="contentId">The content id to copy</param>
/// <param name="name">The name of the blueprint</param>
/// <returns></returns>
[Authorize(Policy = AuthorizationPolicies.ContentPermissionCreateBlueprintFromId)]
[HttpPost]
public ActionResult<SimpleNotificationModel> CreateBlueprintFromContent(
[FromQuery] int contentId,
@@ -833,7 +834,9 @@ public class ContentController : ContentControllerBase
/// <summary>
/// Saves content
/// </summary>
[ContentSaveValidation]
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
[FileUploadCleanupFilter]
[ContentSaveValidation(skipUserAccessValidation:true)] // skip user access validation because we "only" require Settings access to create new blueprints from scratch
public async Task<ActionResult<ContentItemDisplay<ContentVariantDisplay>?>?> PostSaveBlueprint(
[ModelBinder(typeof(BlueprintItemBinder))] ContentItemSave contentItem)
{
@@ -1993,6 +1996,7 @@ public class ContentController : ContentControllerBase
return Ok();
}
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
[HttpDelete]
[HttpPost]
public IActionResult DeleteBlueprint(int id)

View File

@@ -180,6 +180,13 @@ public static partial class UmbracoBuilderExtensions
policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionDelete.ActionLetter));
});
options.AddPolicy(AuthorizationPolicies.ContentPermissionCreateBlueprintFromId, policy =>
{
policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme);
policy.Requirements.Add(
new ContentPermissionsQueryStringRequirement(ActionCreateBlueprintFromContent.ActionLetter, "contentId"));
});
options.AddPolicy(AuthorizationPolicies.BackOfficeAccess, policy =>
{
policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme);

View File

@@ -20,9 +20,12 @@ namespace Umbraco.Cms.Web.BackOffice.Filters;
/// </summary>
internal sealed class ContentSaveValidationAttribute : TypeFilterAttribute
{
public ContentSaveValidationAttribute() : base(typeof(ContentSaveValidationFilter)) =>
public ContentSaveValidationAttribute(bool skipUserAccessValidation = false)
: base(typeof(ContentSaveValidationFilter))
{
Order = -3000; // More important than ModelStateInvalidFilter.FilterOrder
Arguments = new object[] { skipUserAccessValidation };
}
private sealed class ContentSaveValidationFilter : IAsyncActionFilter
{
@@ -32,6 +35,7 @@ internal sealed class ContentSaveValidationAttribute : TypeFilterAttribute
private readonly ILocalizationService _localizationService;
private readonly ILoggerFactory _loggerFactory;
private readonly IPropertyValidationService _propertyValidationService;
private readonly bool _skipUserAccessValidation;
public ContentSaveValidationFilter(
@@ -40,7 +44,8 @@ internal sealed class ContentSaveValidationAttribute : TypeFilterAttribute
IPropertyValidationService propertyValidationService,
IAuthorizationService authorizationService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
ILocalizationService localizationService)
ILocalizationService localizationService,
bool skipUserAccessValidation)
{
_loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
_contentService = contentService ?? throw new ArgumentNullException(nameof(contentService));
@@ -49,6 +54,7 @@ internal sealed class ContentSaveValidationAttribute : TypeFilterAttribute
_authorizationService = authorizationService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_localizationService = localizationService;
_skipUserAccessValidation = skipUserAccessValidation;
}
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
@@ -88,7 +94,7 @@ internal sealed class ContentSaveValidationAttribute : TypeFilterAttribute
return;
}
if (!await ValidateUserAccessAsync(model, context))
if (_skipUserAccessValidation is false && await ValidateUserAccessAsync(model, context) is false)
{
return;
}

View File

@@ -23,6 +23,7 @@ public static class AuthorizationPolicies
public const string ContentPermissionProtectById = nameof(ContentPermissionProtectById);
public const string ContentPermissionBrowseById = nameof(ContentPermissionBrowseById);
public const string ContentPermissionDeleteById = nameof(ContentPermissionDeleteById);
public const string ContentPermissionCreateBlueprintFromId = nameof(ContentPermissionCreateBlueprintFromId);
public const string MediaPermissionByResource = nameof(MediaPermissionByResource);
public const string MediaPermissionPathById = nameof(MediaPermissionPathById);

View File

@@ -42,6 +42,7 @@
</Content>
</ItemGroup>
<!-- Update template.json files with the default UmbracoVersion value set to the current build version -->
<ItemGroup>
<PackageReference Include="Umbraco.JsonSchema.Extensions" PrivateAssets="all" />

View File

@@ -58,21 +58,64 @@ public class IndexInitializer
_contentTypeService = contentTypeService;
}
[Obsolete("Use ctor that is not obsolete. This will be removed in Umbraco 15.")]
public IndexInitializer(
IShortStringHelper shortStringHelper,
PropertyEditorCollection propertyEditors,
MediaUrlGeneratorCollection mediaUrlGenerators,
IScopeProvider scopeProvider,
ILoggerFactory loggerFactory,
IOptions<ContentSettings> contentSettings, IContentTypeService contentTypeService)
IOptions<ContentSettings> contentSettings,
ILocalizationService localizationService)
: this(
shortStringHelper,
propertyEditors,
mediaUrlGenerators,
scopeProvider,
loggerFactory,
contentSettings,
StaticServiceProvider.Instance.GetRequiredService<ILocalizationService>(), contentTypeService)
shortStringHelper,
propertyEditors,
mediaUrlGenerators,
scopeProvider,
loggerFactory,
contentSettings,
localizationService, StaticServiceProvider.Instance.GetRequiredService<IContentTypeService>())
{
}
[Obsolete("Use ctor that is not obsolete. This will be removed in Umbraco 15.")]
public IndexInitializer(
IShortStringHelper shortStringHelper,
PropertyEditorCollection propertyEditors,
MediaUrlGeneratorCollection mediaUrlGenerators,
IScopeProvider scopeProvider,
ILoggerFactory loggerFactory,
IOptions<ContentSettings> contentSettings,
IContentTypeService contentTypeService)
: this(
shortStringHelper,
propertyEditors,
mediaUrlGenerators,
scopeProvider,
loggerFactory,
contentSettings,
StaticServiceProvider.Instance.GetRequiredService<ILocalizationService>(), contentTypeService)
{
}
[Obsolete("Use ctor that is not obsolete. This will be removed in Umbraco 15.")]
public IndexInitializer(
IShortStringHelper shortStringHelper,
PropertyEditorCollection propertyEditors,
MediaUrlGeneratorCollection mediaUrlGenerators,
IScopeProvider scopeProvider,
ILoggerFactory loggerFactory,
IOptions<ContentSettings> contentSettings)
: this(
shortStringHelper,
propertyEditors,
mediaUrlGenerators,
scopeProvider,
loggerFactory,
contentSettings,
StaticServiceProvider.Instance.GetRequiredService<ILocalizationService>(),
StaticServiceProvider.Instance.GetRequiredService<IContentTypeService>())
{
}

View File

@@ -1,8 +1,6 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using NUnit.Framework;
using Umbraco.Cms.Core;
@@ -49,6 +47,71 @@ public class ArtifactBaseTests
string.Join(",", artifact.Dependencies.Select(x => x.Udi.ToString())));
}
[Test]
public void Dependencies_Correctly_Updates_Mode()
{
var udi = Udi.Create(Constants.UdiEntityType.AnyGuid, Guid.NewGuid());
var dependencies = new ArtifactDependencyCollection
{
// Keep Match
new ArtifactDependency(udi, false, ArtifactDependencyMode.Match),
new ArtifactDependency(udi, false, ArtifactDependencyMode.Exist),
};
Assert.AreEqual(1, dependencies.Count);
var dependency = dependencies.First();
Assert.AreEqual(udi, dependency.Udi);
Assert.AreEqual(false, dependency.Ordering);
Assert.AreEqual(ArtifactDependencyMode.Match, dependency.Mode);
Assert.AreEqual(null, dependency.Checksum);
}
[Test]
public void Dependencies_Correctly_Updates_Ordering()
{
var udi = Udi.Create(Constants.UdiEntityType.AnyGuid, Guid.NewGuid());
var dependencies = new ArtifactDependencyCollection
{
// Keep ordering (regardless of mode)
new ArtifactDependency(udi, false, ArtifactDependencyMode.Match),
new ArtifactDependency(udi, false, ArtifactDependencyMode.Exist),
new ArtifactDependency(udi, true, ArtifactDependencyMode.Match),
new ArtifactDependency(udi, true, ArtifactDependencyMode.Exist),
new ArtifactDependency(udi, false, ArtifactDependencyMode.Match),
new ArtifactDependency(udi, false, ArtifactDependencyMode.Exist),
};
Assert.AreEqual(1, dependencies.Count);
var dependency = dependencies.First();
Assert.AreEqual(udi, dependency.Udi);
Assert.AreEqual(true, dependency.Ordering);
Assert.AreEqual(ArtifactDependencyMode.Match, dependency.Mode);
Assert.AreEqual(null, dependency.Checksum);
}
[Test]
public void Dependencies_Correctly_Updates_Checksum()
{
var udi = Udi.Create(Constants.UdiEntityType.AnyGuid, Guid.NewGuid());
var dependencies = new ArtifactDependencyCollection
{
// Keep checksum
new ArtifactDependency(udi, true, ArtifactDependencyMode.Match, "123"),
new ArtifactDependency(udi, true, ArtifactDependencyMode.Match, string.Empty),
new ArtifactDependency(udi, true, ArtifactDependencyMode.Match),
};
Assert.AreEqual(1, dependencies.Count);
var dependency = dependencies.First();
Assert.AreEqual(udi, dependency.Udi);
Assert.AreEqual(true, dependency.Ordering);
Assert.AreEqual(ArtifactDependencyMode.Match, dependency.Mode);
Assert.AreEqual("123", dependency.Checksum);
}
private class TestArtifact : ArtifactBase<GuidUdi>
{
public TestArtifact(GuidUdi udi, IEnumerable<ArtifactDependency> dependencies = null)