Merge branch 'v13/dev' into v14/dev
# Conflicts: # Directory.Packages.props # build/azure-pipelines.yml # src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs # src/Umbraco.Cms.Persistence.EFCore/Locking/SqlServerEFCoreDistributedLockingMechanism.cs # src/Umbraco.Core/Configuration/Models/RichTextEditorSettings.cs # src/Umbraco.Core/EmbeddedResources/Lang/da.xml # src/Umbraco.Core/EmbeddedResources/Lang/en.xml # src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml # src/Umbraco.Core/Services/ContentService.cs # src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandler.cs # src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsResourceHandler.cs # src/Umbraco.Web.BackOffice/Controllers/ContentController.cs # src/Umbraco.Web.BackOffice/Controllers/ExamineManagementController.cs # src/Umbraco.Web.BackOffice/Controllers/MediaController.cs # src/Umbraco.Web.BackOffice/Trees/StaticFilesTreeController.cs # src/Umbraco.Web.UI.Client/package-lock.json # src/Umbraco.Web.UI.Client/package.json # src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbuttongroup.directive.js # src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js # src/Umbraco.Web.UI.Client/src/common/filters/simpleMarkdown.filter.js # src/Umbraco.Web.UI.Client/src/common/filters/simpleMarkdown.filter.js.js # src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js # src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less # src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.less # src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html # src/Umbraco.Web.UI.Client/src/views/common/overlays/ysod/ysod.controller.js # src/Umbraco.Web.UI.Client/src/views/common/overlays/ysod/ysod.html # src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button-group.html # src/Umbraco.Web.UI.Client/src/views/content/overlays/sendtopublish.controller.js # src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.overlay.controller.js # src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.overlay.html # src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.controller.js # src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html # src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.component.js # src/Umbraco.Web.UI.Client~HEAD # src/Umbraco.Web.UI.Login/package-lock.json # src/Umbraco.Web.UI.Login/package.json # tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockGridEditor/Content/blockGridEditorContent.spec.ts # tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceNotificationTests.cs # tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/ContentControllerTests.cs # tools/Umbraco.JsonSchema/UmbracoCmsSchema.cs # version.json
This commit is contained in:
@@ -12,25 +12,25 @@
|
||||
</ItemGroup>
|
||||
<!-- Microsoft packages -->
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.8" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.8" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.11" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.11" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.8" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.8" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.8" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.11" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.11" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.11" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="8.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="8.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="8.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="8.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="8.0.0" />
|
||||
@@ -45,14 +45,14 @@
|
||||
<PackageVersion Include="Asp.Versioning.Mvc" Version="8.1.0" />
|
||||
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
|
||||
<PackageVersion Include="Dazinator.Extensions.FileProviders" Version="2.0.0" />
|
||||
<PackageVersion Include="Examine" Version="3.3.0" />
|
||||
<PackageVersion Include="Examine.Core" Version="3.3.0" />
|
||||
<PackageVersion Include="HtmlAgilityPack" Version="1.11.65" />
|
||||
<PackageVersion Include="Examine" Version="3.5.0" />
|
||||
<PackageVersion Include="Examine.Core" Version="3.5.0" />
|
||||
<PackageVersion Include="HtmlAgilityPack" Version="1.11.71" />
|
||||
<PackageVersion Include="JsonPatch.Net" Version="3.1.1" />
|
||||
<PackageVersion Include="K4os.Compression.LZ4" Version="1.3.8" />
|
||||
<PackageVersion Include="MailKit" Version="4.7.1.1" />
|
||||
<PackageVersion Include="MailKit" Version="4.8.0" />
|
||||
<PackageVersion Include="Markdown" Version="2.2.1" />
|
||||
<PackageVersion Include="MessagePack" Version="2.5.187" />
|
||||
<PackageVersion Include="MessagePack" Version="2.5.192" />
|
||||
<PackageVersion Include="MiniProfiler.AspNetCore.Mvc" Version="4.3.8" />
|
||||
<PackageVersion Include="MiniProfiler.Shared" Version="4.3.8" />
|
||||
<PackageVersion Include="ncrontab" Version="3.3.3" />
|
||||
@@ -62,32 +62,34 @@
|
||||
<PackageVersion Include="OpenIddict.AspNetCore" Version="5.7.0" />
|
||||
<PackageVersion Include="OpenIddict.EntityFrameworkCore" Version="5.7.0" />
|
||||
<PackageVersion Include="Serilog" Version="3.1.1" />
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.2" />
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.3" />
|
||||
<PackageVersion Include="Serilog.Enrichers.Process" Version="2.0.2" />
|
||||
<PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" />
|
||||
<PackageVersion Include="Serilog.Expressions" Version="4.0.0" />
|
||||
<PackageVersion Include="Serilog.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageVersion Include="Serilog.Formatting.Compact" Version="2.0.0" />
|
||||
<PackageVersion Include="Serilog.Formatting.Compact.Reader" Version="3.0.0" />
|
||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.2" />
|
||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.4" />
|
||||
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Map" Version="1.0.2" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.5" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.6" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp.Web" Version="3.1.3" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.7.3" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.9.0" />
|
||||
</ItemGroup>
|
||||
<!-- Transitive pinned versions (only required because our direct dependencies have vulnerable versions of transitive dependencies) -->
|
||||
<ItemGroup>
|
||||
<!-- Both Microsoft.EntityFrameworkCore.SqlServer and NPoco.SqlServer bring in a vulnerable version of Azure.Identity -->
|
||||
<PackageVersion Include="Azure.Identity" Version="1.12.0" />
|
||||
<PackageVersion Include="Azure.Identity" Version="1.13.1" />
|
||||
<!-- Dazinator.Extensions.FileProviders brings in a vulnerable version of System.Net.Http -->
|
||||
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
|
||||
<!-- Examine brings in a vulnerable version of System.Security.Cryptography.Xml -->
|
||||
<PackageVersion Include="System.Security.Cryptography.Xml" Version="8.0.1" />
|
||||
<PackageVersion Include="System.Security.Cryptography.Xml" Version="8.0.2" />
|
||||
<!-- Both Dazinator.Extensions.FileProviders and MiniProfiler.AspNetCore.Mvc bring in a vulnerable version of System.Text.RegularExpressions -->
|
||||
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||
<!-- Both OpenIddict.AspNetCore, Npoco.SqlServer and Microsoft.EntityFrameworkCore.SqlServer bring in a vulnerable version of Microsoft.IdentityModel.JsonWebTokens -->
|
||||
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.7.1" />
|
||||
<!-- Examine.Lucene bring in a vulnerable version of Lucene.Net.Replicator -->
|
||||
<PackageVersion Include="Lucene.Net.Replicator" Version="4.8.0-beta00017" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -46,7 +46,9 @@ public static class UmbracoBuilderAuthExtensions
|
||||
Paths.BackOfficeApi.LogoutEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
|
||||
.SetRevocationEndpointUris(
|
||||
Paths.MemberApi.RevokeEndpoint.TrimStart(Constants.CharArrays.ForwardSlash),
|
||||
Paths.BackOfficeApi.RevokeEndpoint.TrimStart(Constants.CharArrays.ForwardSlash));
|
||||
Paths.BackOfficeApi.RevokeEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
|
||||
.SetUserinfoEndpointUris(
|
||||
Paths.MemberApi.UserinfoEndpoint.TrimStart(Constants.CharArrays.ForwardSlash));
|
||||
|
||||
// Enable authorization code flow with PKCE
|
||||
options
|
||||
@@ -58,7 +60,8 @@ public static class UmbracoBuilderAuthExtensions
|
||||
options
|
||||
.UseAspNetCore()
|
||||
.EnableAuthorizationEndpointPassthrough()
|
||||
.EnableLogoutEndpointPassthrough();
|
||||
.EnableLogoutEndpointPassthrough()
|
||||
.EnableUserinfoEndpointPassthrough();
|
||||
|
||||
// Enable reference tokens
|
||||
// - see https://documentation.openiddict.com/configuration/token-storage.html
|
||||
|
||||
@@ -31,6 +31,8 @@ public static class Paths
|
||||
|
||||
public static readonly string RevokeEndpoint = EndpointPath($"{EndpointTemplate}/revoke");
|
||||
|
||||
public static readonly string UserinfoEndpoint = EndpointPath($"{EndpointTemplate}/userinfo");
|
||||
|
||||
// NOTE: we're NOT using /api/v1.0/ here because it will clash with the Delivery API docs
|
||||
private static string EndpointPath(string relativePath) => $"/umbraco/delivery/api/v1/{relativePath}";
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace Umbraco.Cms.Api.Delivery.Controllers.Content;
|
||||
[ApiExplorerSettings(GroupName = "Content")]
|
||||
[LocalizeFromAcceptLanguageHeader]
|
||||
[ValidateStartItem]
|
||||
[AddVaryHeader]
|
||||
[OutputCache(PolicyName = Constants.DeliveryApi.OutputCache.ContentCachePolicy)]
|
||||
public abstract class ContentApiControllerBase : DeliveryApiControllerBase
|
||||
{
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using OpenIddict.Server.AspNetCore;
|
||||
using Umbraco.Cms.Api.Delivery.Routing;
|
||||
using Umbraco.Cms.Api.Delivery.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers.Security;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[ApiController]
|
||||
[VersionedDeliveryApiRoute(Common.Security.Paths.MemberApi.EndpointTemplate)]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
[Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)]
|
||||
public class CurrentMemberController : DeliveryApiControllerBase
|
||||
{
|
||||
private readonly ICurrentMemberClaimsProvider _currentMemberClaimsProvider;
|
||||
|
||||
public CurrentMemberController(ICurrentMemberClaimsProvider currentMemberClaimsProvider)
|
||||
=> _currentMemberClaimsProvider = currentMemberClaimsProvider;
|
||||
|
||||
[HttpGet("userinfo")]
|
||||
public async Task<IActionResult> Userinfo()
|
||||
{
|
||||
Dictionary<string, object> claims = await _currentMemberClaimsProvider.GetClaimsAsync();
|
||||
return Ok(claims);
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,7 @@ public static class UmbracoBuilderExtensions
|
||||
builder.Services.AddSingleton<IApiMediaQueryService, ApiMediaQueryService>();
|
||||
builder.Services.AddTransient<IMemberApplicationManager, MemberApplicationManager>();
|
||||
builder.Services.AddTransient<IRequestMemberAccessService, RequestMemberAccessService>();
|
||||
builder.Services.AddTransient<ICurrentMemberClaimsProvider, CurrentMemberClaimsProvider>();
|
||||
|
||||
builder.Services.ConfigureOptions<ConfigureUmbracoDeliveryApiSwaggerGenOptions>();
|
||||
builder.AddUmbracoApiOpenApiUI();
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Filters;
|
||||
|
||||
public sealed class AddVaryHeaderAttribute : ActionFilterAttribute
|
||||
{
|
||||
private const string Vary = "Accept-Language, Preview, Start-Item";
|
||||
|
||||
public override void OnResultExecuting(ResultExecutingContext context)
|
||||
=> context.HttpContext.Response.Headers.Vary = context.HttpContext.Response.Headers.Vary.Count > 0
|
||||
? $"{context.HttpContext.Response.Headers.Vary}, {Vary}"
|
||||
: Vary;
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
using Umbraco.Cms.Api.Delivery.Indexing.Filters;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Querying.Filters;
|
||||
|
||||
@@ -15,15 +14,15 @@ public sealed class ContentTypeFilter : IFilterHandler
|
||||
/// <inheritdoc/>
|
||||
public FilterOption BuildFilterOption(string filter)
|
||||
{
|
||||
var alias = filter.Substring(ContentTypeSpecifier.Length);
|
||||
var filterValue = filter.Substring(ContentTypeSpecifier.Length);
|
||||
var negate = filterValue.StartsWith('!');
|
||||
var aliases = filterValue.TrimStart('!').Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
return new FilterOption
|
||||
{
|
||||
FieldName = ContentTypeFilterIndexer.FieldName,
|
||||
Values = alias.IsNullOrWhiteSpace() == false
|
||||
? new[] { alias.TrimStart('!') }
|
||||
: Array.Empty<string>(),
|
||||
Operator = alias.StartsWith('!')
|
||||
Values = aliases,
|
||||
Operator = negate
|
||||
? FilterOperation.IsNot
|
||||
: FilterOperation.Is
|
||||
};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Delivery.Indexing.Selectors;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Extensions;
|
||||
@@ -10,10 +12,22 @@ public sealed class AncestorsSelector : QueryOptionBase, ISelectorHandler
|
||||
{
|
||||
private const string AncestorsSpecifier = "ancestors:";
|
||||
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
|
||||
private readonly IRequestPreviewService _requestPreviewService;
|
||||
|
||||
public AncestorsSelector(IPublishedSnapshotAccessor publishedSnapshotAccessor, IRequestRoutingService requestRoutingService)
|
||||
: base(publishedSnapshotAccessor, requestRoutingService) =>
|
||||
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
|
||||
public AncestorsSelector(
|
||||
IPublishedSnapshotAccessor publishedSnapshotAccessor,
|
||||
IRequestRoutingService requestRoutingService)
|
||||
: this(publishedSnapshotAccessor, requestRoutingService, StaticServiceProvider.Instance.GetRequiredService<IRequestPreviewService>())
|
||||
{
|
||||
}
|
||||
|
||||
public AncestorsSelector(IPublishedSnapshotAccessor publishedSnapshotAccessor, IRequestRoutingService requestRoutingService, IRequestPreviewService requestPreviewService)
|
||||
: base(publishedSnapshotAccessor, requestRoutingService)
|
||||
{
|
||||
_publishedSnapshotAccessor = publishedSnapshotAccessor;
|
||||
_requestPreviewService = requestPreviewService;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanHandle(string query)
|
||||
@@ -37,11 +51,21 @@ public sealed class AncestorsSelector : QueryOptionBase, ISelectorHandler
|
||||
};
|
||||
}
|
||||
|
||||
IPublishedSnapshot publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot();
|
||||
|
||||
IPublishedContent contentItem = publishedSnapshot.Content?.GetById((Guid)id)
|
||||
IPublishedContentCache contentCache = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot()?.Content
|
||||
?? throw new InvalidOperationException("Could not obtain the content cache");
|
||||
|
||||
IPublishedContent? contentItem = contentCache.GetById(_requestPreviewService.IsPreview(), id.Value);
|
||||
|
||||
if (contentItem is null)
|
||||
{
|
||||
// no such content item, make sure the selector does not yield any results
|
||||
return new SelectorOption
|
||||
{
|
||||
FieldName = AncestorsSelectorIndexer.FieldName,
|
||||
Values = Array.Empty<string>()
|
||||
};
|
||||
}
|
||||
|
||||
var ancestorKeys = contentItem.Ancestors().Select(a => a.Key.ToString("D")).ToArray();
|
||||
|
||||
return new SelectorOption
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
using OpenIddict.Abstractions;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Services;
|
||||
|
||||
// NOTE: this is public and unsealed to allow overriding the default claims with minimal effort.
|
||||
public class CurrentMemberClaimsProvider : ICurrentMemberClaimsProvider
|
||||
{
|
||||
private readonly IMemberManager _memberManager;
|
||||
|
||||
public CurrentMemberClaimsProvider(IMemberManager memberManager)
|
||||
=> _memberManager = memberManager;
|
||||
|
||||
public virtual async Task<Dictionary<string, object>> GetClaimsAsync()
|
||||
{
|
||||
MemberIdentityUser? memberIdentityUser = await _memberManager.GetCurrentMemberAsync();
|
||||
return memberIdentityUser is not null
|
||||
? await GetClaimsForMemberIdentityAsync(memberIdentityUser)
|
||||
: throw new InvalidOperationException("Could not retrieve the current member. This method should only ever be invoked when a member has been authorized.");
|
||||
}
|
||||
|
||||
protected virtual async Task<Dictionary<string, object>> GetClaimsForMemberIdentityAsync(MemberIdentityUser memberIdentityUser)
|
||||
{
|
||||
var claims = new Dictionary<string, object>
|
||||
{
|
||||
[OpenIddictConstants.Claims.Subject] = memberIdentityUser.Key
|
||||
};
|
||||
|
||||
if (memberIdentityUser.Name is not null)
|
||||
{
|
||||
claims[OpenIddictConstants.Claims.Name] = memberIdentityUser.Name;
|
||||
}
|
||||
|
||||
if (memberIdentityUser.Email is not null)
|
||||
{
|
||||
claims[OpenIddictConstants.Claims.Email] = memberIdentityUser.Email;
|
||||
}
|
||||
|
||||
claims[OpenIddictConstants.Claims.Role] = await _memberManager.GetRolesAsync(memberIdentityUser);
|
||||
|
||||
return claims;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Umbraco.Cms.Api.Delivery.Services;
|
||||
|
||||
public interface ICurrentMemberClaimsProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves the claims for the currently logged in member.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is used by the OIDC user info endpoint to supply "current user" info.
|
||||
/// </remarks>
|
||||
Task<Dictionary<string, object>> GetClaimsAsync();
|
||||
}
|
||||
@@ -32906,6 +32906,9 @@
|
||||
},
|
||||
"401": {
|
||||
"description": "The resource is protected and requires an authentication token"
|
||||
},
|
||||
"403": {
|
||||
"description": "The authenticated user do not have access to this resource"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
@@ -33107,6 +33110,9 @@
|
||||
},
|
||||
"401": {
|
||||
"description": "The resource is protected and requires an authentication token"
|
||||
},
|
||||
"403": {
|
||||
"description": "The authenticated user do not have access to this resource"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
@@ -33408,6 +33414,9 @@
|
||||
},
|
||||
"401": {
|
||||
"description": "The resource is protected and requires an authentication token"
|
||||
},
|
||||
"403": {
|
||||
"description": "The authenticated user do not have access to this resource"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
|
||||
@@ -170,7 +170,9 @@ internal class SqlServerEFCoreDistributedLockingMechanism<T> : IDistributedLocki
|
||||
"A transaction with minimum ReadCommitted isolation level is required.");
|
||||
}
|
||||
|
||||
var rowsAffected = await dbContext.Database.ExecuteSqlAsync(@$"SET LOCK_TIMEOUT {(int)_timeout.TotalMilliseconds};UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id={LockId}");
|
||||
#pragma warning disable EF1002
|
||||
var rowsAffected = await dbContext.Database.ExecuteSqlRawAsync(@$"SET LOCK_TIMEOUT {(int)_timeout.TotalMilliseconds};UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id={LockId}");
|
||||
#pragma warning restore EF1002
|
||||
|
||||
if (rowsAffected == 0)
|
||||
{
|
||||
|
||||
@@ -140,7 +140,9 @@ public static class DistributedCacheExtensions
|
||||
Id = x.Item.Id,
|
||||
Key = x.Item.Key,
|
||||
ChangeTypes = x.ChangeTypes,
|
||||
Blueprint = x.Item.Blueprint
|
||||
Blueprint = x.Item.Blueprint,
|
||||
PublishedCultures = x.PublishedCultures?.ToArray(),
|
||||
UnpublishedCultures = x.UnpublishedCultures?.ToArray()
|
||||
});
|
||||
|
||||
dc.RefreshByPayload(ContentCacheRefresher.UniqueId, payloads);
|
||||
|
||||
@@ -182,6 +182,10 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
|
||||
public TreeChangeTypes ChangeTypes { get; init; }
|
||||
|
||||
public bool Blueprint { get; init; }
|
||||
|
||||
public string[]? PublishedCultures { get; init; }
|
||||
|
||||
public string[]? UnpublishedCultures { get; init; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
2455
src/Umbraco.Core/EmbeddedResources/Lang/de_ch.xml
Normal file
2455
src/Umbraco.Core/EmbeddedResources/Lang/de_ch.xml
Normal file
File diff suppressed because it is too large
Load Diff
2357
src/Umbraco.Core/EmbeddedResources/Lang/fr_ch.xml
Normal file
2357
src/Umbraco.Core/EmbeddedResources/Lang/fr_ch.xml
Normal file
File diff suppressed because it is too large
Load Diff
3170
src/Umbraco.Core/EmbeddedResources/Lang/it_ch.xml
Normal file
3170
src/Umbraco.Core/EmbeddedResources/Lang/it_ch.xml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -384,7 +384,10 @@ public static class ClaimsIdentityExtensions
|
||||
var firstValue = identity.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
if (firstValue is not null)
|
||||
{
|
||||
return int.Parse(firstValue, CultureInfo.InvariantCulture);
|
||||
if (int.TryParse(firstValue, CultureInfo.InvariantCulture, out var id))
|
||||
{
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -32,4 +32,14 @@ public class ContentTreeChangeNotification : TreeChangeNotification<IContent>
|
||||
: base(new TreeChange<IContent>(target, changeTypes), messages)
|
||||
{
|
||||
}
|
||||
|
||||
public ContentTreeChangeNotification(
|
||||
IContent target,
|
||||
TreeChangeTypes changeTypes,
|
||||
IEnumerable<string>? publishedCultures,
|
||||
IEnumerable<string>? unpublishedCultures,
|
||||
EventMessages messages)
|
||||
: base(new TreeChange<IContent>(target, changeTypes, publishedCultures, unpublishedCultures), messages)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,22 @@ public class TreeChange<TItem>
|
||||
ChangeTypes = changeTypes;
|
||||
}
|
||||
|
||||
public TreeChange(TItem changedItem, TreeChangeTypes changeTypes, IEnumerable<string>? publishedCultures, IEnumerable<string>? unpublishedCultures)
|
||||
{
|
||||
Item = changedItem;
|
||||
ChangeTypes = changeTypes;
|
||||
PublishedCultures = publishedCultures;
|
||||
UnpublishedCultures = unpublishedCultures;
|
||||
}
|
||||
|
||||
public TItem Item { get; }
|
||||
|
||||
public TreeChangeTypes ChangeTypes { get; }
|
||||
|
||||
public IEnumerable<string>? PublishedCultures { get; }
|
||||
|
||||
public IEnumerable<string>? UnpublishedCultures { get; }
|
||||
|
||||
public EventArgs ToEventArgs() => new EventArgs(this);
|
||||
|
||||
public class EventArgs : System.EventArgs
|
||||
|
||||
@@ -1552,7 +1552,12 @@ public class ContentService : RepositoryService, IContentService
|
||||
// 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 ContentTreeChangeNotification(
|
||||
content,
|
||||
TreeChangeTypes.RefreshBranch,
|
||||
variesByCulture ? culturesPublishing.IsCollectionEmpty() ? null : culturesPublishing : null,
|
||||
variesByCulture ? culturesUnpublishing.IsCollectionEmpty() ? null : culturesUnpublishing : ["*"],
|
||||
eventMessages));
|
||||
|
||||
if (culturesUnpublishing != null)
|
||||
{
|
||||
@@ -1611,7 +1616,12 @@ public class ContentService : RepositoryService, IContentService
|
||||
if (!branchOne)
|
||||
{
|
||||
scope.Notifications.Publish(
|
||||
new ContentTreeChangeNotification(content, changeType, eventMessages));
|
||||
new ContentTreeChangeNotification(
|
||||
content,
|
||||
changeType,
|
||||
variesByCulture ? culturesPublishing.IsCollectionEmpty() ? null : culturesPublishing : ["*"],
|
||||
variesByCulture ? culturesUnpublishing.IsCollectionEmpty() ? null : culturesUnpublishing : null,
|
||||
eventMessages));
|
||||
scope.Notifications.Publish(
|
||||
new ContentPublishedNotification(content, eventMessages).WithState(notificationState));
|
||||
}
|
||||
@@ -2058,7 +2068,6 @@ public class ContentService : RepositoryService, IContentService
|
||||
var results = new List<PublishResult>();
|
||||
var publishedDocuments = new List<IContent>();
|
||||
|
||||
IDictionary<string, object?>? initialNotificationState = null;
|
||||
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
|
||||
{
|
||||
scope.WriteLock(Constants.Locks.ContentTree);
|
||||
@@ -2077,7 +2086,8 @@ public class ContentService : RepositoryService, IContentService
|
||||
}
|
||||
|
||||
// deal with the branch root - if it fails, abort
|
||||
PublishResult? result = PublishBranchItem(scope, document, shouldPublish, publishCultures, true, publishedDocuments, eventMessages, userId, allLangs, out initialNotificationState);
|
||||
HashSet<string>? culturesToPublish = shouldPublish(document);
|
||||
PublishResult? result = PublishBranchItem(scope, document, culturesToPublish, publishCultures, true, publishedDocuments, eventMessages, userId, allLangs, out IDictionary<string, object?>? notificationState);
|
||||
if (result != null)
|
||||
{
|
||||
results.Add(result);
|
||||
@@ -2087,6 +2097,8 @@ public class ContentService : RepositoryService, IContentService
|
||||
}
|
||||
}
|
||||
|
||||
HashSet<string> culturesPublished = culturesToPublish ?? [];
|
||||
|
||||
// deal with descendants
|
||||
// if one fails, abort its branch
|
||||
var exclude = new HashSet<int>();
|
||||
@@ -2112,12 +2124,14 @@ public class ContentService : RepositoryService, IContentService
|
||||
}
|
||||
|
||||
// no need to check path here, parent has to be published here
|
||||
result = PublishBranchItem(scope, d, shouldPublish, publishCultures, false, publishedDocuments, eventMessages, userId, allLangs, out _);
|
||||
culturesToPublish = shouldPublish(d);
|
||||
result = PublishBranchItem(scope, d, culturesToPublish, publishCultures, false, publishedDocuments, eventMessages, userId, allLangs, out _);
|
||||
if (result != null)
|
||||
{
|
||||
results.Add(result);
|
||||
if (result.Success)
|
||||
{
|
||||
culturesPublished.UnionWith(culturesToPublish ?? []);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -2134,9 +2148,15 @@ public class ContentService : RepositoryService, IContentService
|
||||
|
||||
// trigger events for the entire branch
|
||||
// (SaveAndPublishBranchOne does *not* do it)
|
||||
var variesByCulture = document.ContentType.VariesByCulture();
|
||||
scope.Notifications.Publish(
|
||||
new ContentTreeChangeNotification(document, TreeChangeTypes.RefreshBranch, eventMessages));
|
||||
scope.Notifications.Publish(new ContentPublishedNotification(publishedDocuments, eventMessages, true).WithState(initialNotificationState));
|
||||
new ContentTreeChangeNotification(
|
||||
document,
|
||||
TreeChangeTypes.RefreshBranch,
|
||||
variesByCulture ? culturesPublished.IsCollectionEmpty() ? null : culturesPublished : ["*"],
|
||||
null,
|
||||
eventMessages));
|
||||
scope.Notifications.Publish(new ContentPublishedNotification(publishedDocuments, eventMessages).WithState(notificationState));
|
||||
|
||||
scope.Complete();
|
||||
}
|
||||
@@ -2150,7 +2170,7 @@ public class ContentService : RepositoryService, IContentService
|
||||
private PublishResult? PublishBranchItem(
|
||||
ICoreScope scope,
|
||||
IContent document,
|
||||
Func<IContent, HashSet<string>?> shouldPublish,
|
||||
HashSet<string>? culturesToPublish,
|
||||
Func<IContent, HashSet<string>, IReadOnlyCollection<ILanguage>,
|
||||
bool> publishCultures,
|
||||
bool isRoot,
|
||||
@@ -2160,9 +2180,7 @@ public class ContentService : RepositoryService, IContentService
|
||||
IReadOnlyCollection<ILanguage> allLangs,
|
||||
out IDictionary<string, object?>? initialNotificationState)
|
||||
{
|
||||
HashSet<string>? culturesToPublish = shouldPublish(document);
|
||||
|
||||
initialNotificationState = null;
|
||||
initialNotificationState = new Dictionary<string, object?>();
|
||||
|
||||
// we need to guard against unsaved changes before proceeding; the document will be saved, but we're not firing any saved notifications
|
||||
if (HasUnsavedChanges(document))
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
<PackageReference Include="Examine" />
|
||||
<!-- Take top-level depedendency on System.Security.Cryptography.Xml, because Examine depends on a vulnerable version -->
|
||||
<PackageReference Include="System.Security.Cryptography.Xml" />
|
||||
<!-- Take top-level depedendency on Lucene.Net.Replicator-->
|
||||
<PackageReference Include="Lucene.Net.Replicator" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -101,8 +101,9 @@ internal sealed class ApiRichTextElementParser : ApiRichTextParserBase, IApiRich
|
||||
// - non-#comment nodes
|
||||
// - non-#text nodes
|
||||
// - non-empty #text nodes
|
||||
// - empty #text between inline elements (see #17037)
|
||||
HtmlNode[] childNodes = element.ChildNodes
|
||||
.Where(c => c.Name != CommentNodeName && (c.Name != TextNodeName || string.IsNullOrWhiteSpace(c.InnerText) is false))
|
||||
.Where(c => c.Name != CommentNodeName && (c.Name != TextNodeName || c.NextSibling is not null || string.IsNullOrWhiteSpace(c.InnerText) is false))
|
||||
.ToArray();
|
||||
|
||||
var tag = TagName(element);
|
||||
|
||||
@@ -187,8 +187,7 @@ public class ExamineIndexRebuilder : IIndexRebuilder
|
||||
{
|
||||
// If an index exists but it has zero docs we'll consider it empty and rebuild
|
||||
IIndex[] indexes = (onlyEmptyIndexes
|
||||
? _examineManager.Indexes.Where(x =>
|
||||
!x.IndexExists() || (x is IIndexStats stats && stats.GetDocumentCount() == 0))
|
||||
? _examineManager.Indexes.Where(ShouldRebuild)
|
||||
: _examineManager.Indexes).ToArray();
|
||||
|
||||
if (indexes.Length == 0)
|
||||
@@ -228,4 +227,17 @@ public class ExamineIndexRebuilder : IIndexRebuilder
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool ShouldRebuild(IIndex index)
|
||||
{
|
||||
try
|
||||
{
|
||||
return !index.IndexExists() || (index is IIndexStats stats && stats.GetDocumentCount() == 0);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "An error occured trying to get determine index shouldRebuild status for index {IndexName}. The index will NOT be considered for rebuilding", index.Name);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1272,14 +1272,9 @@ public class ContentStore
|
||||
|
||||
// try to find the content
|
||||
// if it is not there, nothing to do
|
||||
_contentNodes.TryGetValue(id, out LinkedNode<ContentNode?>? link); // else null
|
||||
if (link?.Value == null)
|
||||
if (_contentNodes.TryGetValue(id, out LinkedNode<ContentNode?>? link) &&
|
||||
link.Value is ContentNode content)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ContentNode? content = link.Value;
|
||||
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Clear content ID: {ContentId}", content.Id);
|
||||
@@ -1294,26 +1289,25 @@ public class ContentStore
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ClearBranchLocked(int id)
|
||||
{
|
||||
_contentNodes.TryGetValue(id, out LinkedNode<ContentNode?>? link);
|
||||
if (link?.Value == null)
|
||||
if (_contentNodes.TryGetValue(id, out LinkedNode<ContentNode?>? link) &&
|
||||
link.Value is ContentNode content)
|
||||
{
|
||||
return;
|
||||
// clear the entire branch
|
||||
ClearBranchLocked(content);
|
||||
|
||||
// manage the tree
|
||||
RemoveTreeNodeLocked(content);
|
||||
}
|
||||
}
|
||||
|
||||
ClearBranchLocked(link.Value);
|
||||
}
|
||||
|
||||
private void ClearBranchLocked(ContentNode? content)
|
||||
private void ClearBranchLocked(ContentNode content)
|
||||
{
|
||||
// This should never be null, all code that calls this method is null checking but we've seen
|
||||
// issues of null ref exceptions in issue reports so we'll double check here
|
||||
if (content == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(content));
|
||||
}
|
||||
|
||||
// Clear content node
|
||||
SetValueLocked(_contentNodes, content.Id, null);
|
||||
if (_localDb != null)
|
||||
{
|
||||
@@ -1322,14 +1316,21 @@ public class ContentStore
|
||||
|
||||
_contentKeyToIdMap.TryRemove(content.Uid, out _);
|
||||
|
||||
var id = content.FirstChildContentId;
|
||||
while (id > 0)
|
||||
// Clear children
|
||||
int childId = content.FirstChildContentId;
|
||||
if (childId > 0)
|
||||
{
|
||||
// get the required link node, this ensures that both `link` and `link.Value` are not null
|
||||
LinkedNode<ContentNode> link = GetRequiredLinkedNode(id, "child", null);
|
||||
ContentNode? linkValue = link.Value; // capture local since clearing in recurse can clear it
|
||||
ClearBranchLocked(linkValue); // recurse
|
||||
id = linkValue?.NextSiblingContentId ?? 0;
|
||||
ContentNode childContent = GetRequiredLinkedNode(childId, "first child", null).Value!;
|
||||
ClearBranchLocked(childContent); // recurse
|
||||
|
||||
// Clear all siblings of child
|
||||
int siblingId = childContent.NextSiblingContentId;
|
||||
while (siblingId > 0)
|
||||
{
|
||||
ContentNode siblingContent = GetRequiredLinkedNode(siblingId, "next sibling", null).Value!;
|
||||
ClearBranchLocked(siblingContent); // recurse
|
||||
siblingId = siblingContent.NextSiblingContentId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,10 @@ using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.Routing;
|
||||
using Umbraco.Cms.Core.Web;
|
||||
using Umbraco.Cms.Web.Common.Controllers;
|
||||
using Umbraco.Cms.Web.Common.Extensions;
|
||||
|
||||
@@ -21,6 +23,7 @@ public class UmbracoVirtualPageRoute : IUmbracoVirtualPageRoute
|
||||
private readonly LinkParser _linkParser;
|
||||
private readonly UriUtility _uriUtility;
|
||||
private readonly IPublishedRouter _publishedRouter;
|
||||
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
@@ -29,16 +32,29 @@ public class UmbracoVirtualPageRoute : IUmbracoVirtualPageRoute
|
||||
/// <param name="linkParser">The link parser.</param>
|
||||
/// <param name="uriUtility">The Uri utility.</param>
|
||||
/// <param name="publishedRouter">The published router.</param>
|
||||
/// <param name="umbracoContextAccessor">The umbraco context accessor.</param>
|
||||
public UmbracoVirtualPageRoute(
|
||||
EndpointDataSource endpointDataSource,
|
||||
LinkParser linkParser,
|
||||
UriUtility uriUtility,
|
||||
IPublishedRouter publishedRouter)
|
||||
IPublishedRouter publishedRouter,
|
||||
IUmbracoContextAccessor umbracoContextAccessor)
|
||||
{
|
||||
_endpointDataSource = endpointDataSource;
|
||||
_linkParser = linkParser;
|
||||
_uriUtility = uriUtility;
|
||||
_publishedRouter = publishedRouter;
|
||||
_umbracoContextAccessor = umbracoContextAccessor;
|
||||
}
|
||||
|
||||
[Obsolete("Please use constructor that takes an IUmbracoContextAccessor instead, scheduled for removal in v17")]
|
||||
public UmbracoVirtualPageRoute(
|
||||
EndpointDataSource endpointDataSource,
|
||||
LinkParser linkParser,
|
||||
UriUtility uriUtility,
|
||||
IPublishedRouter publishedRouter)
|
||||
: this(endpointDataSource, linkParser, uriUtility, publishedRouter, StaticServiceProvider.Instance.GetRequiredService<IUmbracoContextAccessor>())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -157,7 +173,8 @@ public class UmbracoVirtualPageRoute : IUmbracoVirtualPageRoute
|
||||
requestBuilder.SetPublishedContent(publishedContent);
|
||||
_publishedRouter.RouteDomain(requestBuilder);
|
||||
|
||||
return requestBuilder.Build();
|
||||
// Ensure the culture and domain is set correctly for the published request
|
||||
return await _publishedRouter.RouteRequestAsync(requestBuilder, new RouteRequestOptions(Core.Routing.RouteDirection.Inbound));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -171,6 +188,12 @@ public class UmbracoVirtualPageRoute : IUmbracoVirtualPageRoute
|
||||
{
|
||||
IPublishedRequest publishedRequest = await CreatePublishedRequest(httpContext, publishedContent);
|
||||
|
||||
// Ensure the published request is set to the UmbracoContext
|
||||
if (_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext))
|
||||
{
|
||||
umbracoContext.PublishedRequest = publishedRequest;
|
||||
}
|
||||
|
||||
var umbracoRouteValues = new UmbracoRouteValues(
|
||||
publishedRequest,
|
||||
controllerActionDescriptor);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Logging;
|
||||
@@ -29,6 +30,7 @@ public class UmbLoginStatusController : SurfaceController
|
||||
=> _signInManager = signInManager;
|
||||
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
[ValidateUmbracoFormRouteString]
|
||||
public async Task<IActionResult> HandleLogout([Bind(Prefix = "logoutModel")] PostRedirectModel model)
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
<ItemGroup>
|
||||
<!-- Microsoft packages -->
|
||||
<PackageVersion Include="BenchmarkDotNet" Version="0.14.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.8" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Debug" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
|
||||
<PackageVersion Include="System.Data.DataSetExtensions" Version="4.5.0" />
|
||||
<PackageVersion Include="System.Data.Odbc" Version="8.0.0" />
|
||||
<PackageVersion Include="System.Data.OleDb" Version="8.0.0" />
|
||||
<PackageVersion Include="System.Data.Odbc" Version="8.0.1" />
|
||||
<PackageVersion Include="System.Data.OleDb" Version="8.0.1" />
|
||||
<PackageVersion Include="System.Reflection.Emit" Version="4.7.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -84,7 +84,11 @@ public abstract class UmbracoIntegrationTest : UmbracoIntegrationTestBase
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDownAsync() => _host.StopAsync();
|
||||
public void TearDownAsync()
|
||||
{
|
||||
_host.StopAsync();
|
||||
Services.DisposeIfDisposable();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create the Generic Host and execute startup ConfigureServices/Configure calls
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -54,7 +54,8 @@ public class ContentServiceNotificationTests : UmbracoIntegrationTest
|
||||
.AddNotificationHandler<ContentPublishingNotification, ContentNotificationHandler>()
|
||||
.AddNotificationHandler<ContentPublishedNotification, ContentNotificationHandler>()
|
||||
.AddNotificationHandler<ContentUnpublishingNotification, ContentNotificationHandler>()
|
||||
.AddNotificationHandler<ContentUnpublishedNotification, ContentNotificationHandler>();
|
||||
.AddNotificationHandler<ContentUnpublishedNotification, ContentNotificationHandler>()
|
||||
.AddNotificationHandler<ContentTreeChangeNotification, ContentNotificationHandler>();
|
||||
|
||||
private void CreateTestData()
|
||||
{
|
||||
@@ -176,6 +177,69 @@ public class ContentServiceNotificationTests : UmbracoIntegrationTest
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Publishing_Invariant()
|
||||
{
|
||||
IContent document = new Content("content", -1, _contentType);
|
||||
ContentService.Save(document);
|
||||
|
||||
var treeChangeWasCalled = false;
|
||||
|
||||
ContentNotificationHandler.TreeChange += notification =>
|
||||
{
|
||||
var change = notification.Changes.FirstOrDefault();
|
||||
var publishedCultures = change?.PublishedCultures?.ToArray();
|
||||
Assert.IsNotNull(publishedCultures);
|
||||
Assert.AreEqual(1, publishedCultures.Length);
|
||||
Assert.IsTrue(publishedCultures.InvariantContains("*"));
|
||||
Assert.IsNull(change.UnpublishedCultures);
|
||||
|
||||
treeChangeWasCalled = true;
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
ContentService.Publish(document, ["*"]);
|
||||
Assert.IsTrue(treeChangeWasCalled);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ContentNotificationHandler.TreeChange = null;
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Unpublishing_Invariant()
|
||||
{
|
||||
IContent document = new Content("content", -1, _contentType);
|
||||
ContentService.Save(document);
|
||||
ContentService.Publish(document, ["*"]);
|
||||
|
||||
var treeChangeWasCalled = false;
|
||||
|
||||
ContentNotificationHandler.TreeChange += notification =>
|
||||
{
|
||||
var change = notification.Changes.FirstOrDefault();
|
||||
Assert.IsNull(change?.PublishedCultures);
|
||||
var unpublishedCultures = change?.UnpublishedCultures?.ToArray();
|
||||
Assert.IsNotNull(unpublishedCultures);
|
||||
Assert.AreEqual(1, unpublishedCultures.Length);
|
||||
Assert.IsTrue(unpublishedCultures.InvariantContains("*"));
|
||||
|
||||
treeChangeWasCalled = true;
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
ContentService.Unpublish(document);
|
||||
Assert.IsTrue(treeChangeWasCalled);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ContentNotificationHandler.TreeChange = null;
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Publishing_Culture()
|
||||
{
|
||||
@@ -202,6 +266,7 @@ public class ContentServiceNotificationTests : UmbracoIntegrationTest
|
||||
|
||||
var publishingWasCalled = false;
|
||||
var publishedWasCalled = false;
|
||||
var treeChangeWasCalled = false;
|
||||
|
||||
ContentNotificationHandler.PublishingContent += notification =>
|
||||
{
|
||||
@@ -227,16 +292,30 @@ public class ContentServiceNotificationTests : UmbracoIntegrationTest
|
||||
publishedWasCalled = true;
|
||||
};
|
||||
|
||||
ContentNotificationHandler.TreeChange += notification =>
|
||||
{
|
||||
var change = notification.Changes.FirstOrDefault();
|
||||
var publishedCultures = change?.PublishedCultures?.ToArray();
|
||||
Assert.IsNotNull(publishedCultures);
|
||||
Assert.AreEqual(1, publishedCultures.Length);
|
||||
Assert.IsTrue(publishedCultures.InvariantContains("fr-FR"));
|
||||
Assert.IsNull(change.UnpublishedCultures);
|
||||
|
||||
treeChangeWasCalled = true;
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
ContentService.Publish(document, new[] { "fr-FR" });
|
||||
Assert.IsTrue(publishingWasCalled);
|
||||
Assert.IsTrue(publishedWasCalled);
|
||||
Assert.IsTrue(treeChangeWasCalled);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ContentNotificationHandler.PublishingContent = null;
|
||||
ContentNotificationHandler.PublishedContent = null;
|
||||
ContentNotificationHandler.TreeChange = null;
|
||||
}
|
||||
|
||||
document = ContentService.GetById(document.Id);
|
||||
@@ -399,6 +478,7 @@ public class ContentServiceNotificationTests : UmbracoIntegrationTest
|
||||
|
||||
var publishingWasCalled = false;
|
||||
var publishedWasCalled = false;
|
||||
var treeChangeWasCalled = false;
|
||||
|
||||
// TODO: revisit this - it was migrated when removing static events, but the expected result seems illogic - why does this test bind to Published and not Unpublished?
|
||||
|
||||
@@ -432,16 +512,30 @@ public class ContentServiceNotificationTests : UmbracoIntegrationTest
|
||||
publishedWasCalled = true;
|
||||
};
|
||||
|
||||
ContentNotificationHandler.TreeChange += notification =>
|
||||
{
|
||||
var change = notification.Changes.FirstOrDefault();
|
||||
var unpublishedCultures = change?.UnpublishedCultures?.ToArray();
|
||||
Assert.IsNotNull(unpublishedCultures);
|
||||
Assert.AreEqual(1, unpublishedCultures.Length);
|
||||
Assert.IsTrue(unpublishedCultures.InvariantContains("fr-FR"));
|
||||
Assert.IsNull(change.PublishedCultures);
|
||||
|
||||
treeChangeWasCalled = true;
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
ContentService.CommitDocumentChanges(document);
|
||||
Assert.IsTrue(publishingWasCalled);
|
||||
Assert.IsTrue(publishedWasCalled);
|
||||
Assert.IsTrue(treeChangeWasCalled);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ContentNotificationHandler.PublishingContent = null;
|
||||
ContentNotificationHandler.PublishedContent = null;
|
||||
ContentNotificationHandler.TreeChange = null;
|
||||
}
|
||||
|
||||
document = ContentService.GetById(document.Id);
|
||||
@@ -456,7 +550,8 @@ public class ContentServiceNotificationTests : UmbracoIntegrationTest
|
||||
INotificationHandler<ContentPublishingNotification>,
|
||||
INotificationHandler<ContentPublishedNotification>,
|
||||
INotificationHandler<ContentUnpublishingNotification>,
|
||||
INotificationHandler<ContentUnpublishedNotification>
|
||||
INotificationHandler<ContentUnpublishedNotification>,
|
||||
INotificationHandler<ContentTreeChangeNotification>
|
||||
{
|
||||
public static Action<ContentSavingNotification> SavingContent { get; set; }
|
||||
|
||||
@@ -470,6 +565,8 @@ public class ContentServiceNotificationTests : UmbracoIntegrationTest
|
||||
|
||||
public static Action<ContentUnpublishedNotification> UnpublishedContent { get; set; }
|
||||
|
||||
public static Action<ContentTreeChangeNotification> TreeChange { get; set; }
|
||||
|
||||
public void Handle(ContentPublishedNotification notification) => PublishedContent?.Invoke(notification);
|
||||
|
||||
public void Handle(ContentPublishingNotification notification) => PublishingContent?.Invoke(notification);
|
||||
@@ -480,5 +577,7 @@ public class ContentServiceNotificationTests : UmbracoIntegrationTest
|
||||
public void Handle(ContentUnpublishedNotification notification) => UnpublishedContent?.Invoke(notification);
|
||||
|
||||
public void Handle(ContentUnpublishingNotification notification) => UnpublishingContent?.Invoke(notification);
|
||||
|
||||
public void Handle(ContentTreeChangeNotification notification) => TreeChange?.Invoke(notification);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,26 +27,68 @@ public class RefresherTests
|
||||
Assert.AreEqual(source[0].ChangeTypes, payload[0].ChangeTypes);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ContentCacheRefresherCanDeserializeJsonPayload()
|
||||
[TestCase(TreeChangeTypes.None, false)]
|
||||
[TestCase(TreeChangeTypes.RefreshAll, true)]
|
||||
[TestCase(TreeChangeTypes.RefreshBranch, false)]
|
||||
[TestCase(TreeChangeTypes.Remove, true)]
|
||||
[TestCase(TreeChangeTypes.RefreshNode, false)]
|
||||
public void ContentCacheRefresherCanDeserializeJsonPayload(TreeChangeTypes changeTypes, bool blueprint)
|
||||
{
|
||||
var key = Guid.NewGuid();
|
||||
ContentCacheRefresher.JsonPayload[] source =
|
||||
{
|
||||
new ContentCacheRefresher.JsonPayload()
|
||||
{
|
||||
Id = 1234,
|
||||
Key = Guid.NewGuid(),
|
||||
ChangeTypes = TreeChangeTypes.None
|
||||
Key = key,
|
||||
ChangeTypes = changeTypes,
|
||||
Blueprint = blueprint
|
||||
}
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(source);
|
||||
var payload = JsonSerializer.Deserialize<ContentCacheRefresher.JsonPayload[]>(json);
|
||||
|
||||
Assert.AreEqual(source[0].Id, payload[0].Id);
|
||||
Assert.AreEqual(source[0].Key, payload[0].Key);
|
||||
Assert.AreEqual(source[0].ChangeTypes, payload[0].ChangeTypes);
|
||||
Assert.AreEqual(source[0].Blueprint, payload[0].Blueprint);
|
||||
Assert.AreEqual(1234, payload[0].Id);
|
||||
Assert.AreEqual(key, payload[0].Key);
|
||||
Assert.AreEqual(changeTypes, payload[0].ChangeTypes);
|
||||
Assert.AreEqual(blueprint, payload[0].Blueprint);
|
||||
Assert.IsNull(payload[0].PublishedCultures);
|
||||
Assert.IsNull(payload[0].UnpublishedCultures);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ContentCacheRefresherCanDeserializeJsonPayloadWithCultures()
|
||||
{
|
||||
var key = Guid.NewGuid();
|
||||
ContentCacheRefresher.JsonPayload[] source =
|
||||
{
|
||||
new ContentCacheRefresher.JsonPayload()
|
||||
{
|
||||
Id = 1234,
|
||||
Key = key,
|
||||
PublishedCultures = ["en-US", "da-DK"],
|
||||
UnpublishedCultures = ["de-DE"]
|
||||
}
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(source);
|
||||
var payload = JsonSerializer.Deserialize<ContentCacheRefresher.JsonPayload[]>(json);
|
||||
|
||||
Assert.IsNotNull(payload[0].PublishedCultures);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.AreEqual(2, payload[0].PublishedCultures.Length);
|
||||
Assert.AreEqual("en-US", payload[0].PublishedCultures.First());
|
||||
Assert.AreEqual("da-DK", payload[0].PublishedCultures.Last());
|
||||
});
|
||||
|
||||
Assert.IsNotNull(payload[0].UnpublishedCultures);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.AreEqual(1, payload[0].UnpublishedCultures.Length);
|
||||
Assert.AreEqual("de-DE", payload[0].UnpublishedCultures.First());
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -357,6 +357,48 @@ public class RichTextParserTests : PropertyValueConverterTests
|
||||
Assert.IsEmpty(blockLevelBlock.Elements);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ParseElement_CanHandleWhitespaceAroundInlineElemements()
|
||||
{
|
||||
var parser = CreateRichTextElementParser();
|
||||
|
||||
var element = parser.Parse("<p>What follows from <strong>here</strong> <em>is</em> <a href=\"#\">just</a> a bunch of text.</p>") as RichTextRootElement;
|
||||
Assert.IsNotNull(element);
|
||||
var paragraphElement = element.Elements.Single() as RichTextGenericElement;
|
||||
Assert.IsNotNull(paragraphElement);
|
||||
|
||||
var childElements = paragraphElement.Elements.ToArray();
|
||||
Assert.AreEqual(7, childElements.Length);
|
||||
|
||||
var childElementCounter = 0;
|
||||
|
||||
void AssertNextChildElementIsText(string expectedText)
|
||||
{
|
||||
var textElement = childElements[childElementCounter++] as RichTextTextElement;
|
||||
Assert.IsNotNull(textElement);
|
||||
Assert.AreEqual(expectedText, textElement.Text);
|
||||
}
|
||||
|
||||
void AssertNextChildElementIsGeneric(string expectedTag, string expectedInnerText)
|
||||
{
|
||||
var genericElement = childElements[childElementCounter++] as RichTextGenericElement;
|
||||
Assert.IsNotNull(genericElement);
|
||||
Assert.AreEqual(expectedTag, genericElement.Tag);
|
||||
Assert.AreEqual(1, genericElement.Elements.Count());
|
||||
var textElement = genericElement.Elements.First() as RichTextTextElement;
|
||||
Assert.IsNotNull(textElement);
|
||||
Assert.AreEqual(expectedInnerText, textElement.Text);
|
||||
}
|
||||
|
||||
AssertNextChildElementIsText("What follows from ");
|
||||
AssertNextChildElementIsGeneric("strong", "here");
|
||||
AssertNextChildElementIsText(" ");
|
||||
AssertNextChildElementIsGeneric("em", "is");
|
||||
AssertNextChildElementIsText(" ");
|
||||
AssertNextChildElementIsGeneric("a", "just");
|
||||
AssertNextChildElementIsText(" a bunch of text.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ParseMarkup_CanParseContentLink()
|
||||
{
|
||||
|
||||
@@ -77,6 +77,19 @@ internal class UmbracoCmsSchema
|
||||
|
||||
public required MarketplaceSettings Marketplace { get; set; }
|
||||
|
||||
public InstallDefaultDataNamedOptions InstallDefaultData { get; set; } = null!;
|
||||
|
||||
public required WebhookSettings Webhook { get; set; }
|
||||
}
|
||||
|
||||
public class InstallDefaultDataNamedOptions
|
||||
{
|
||||
public InstallDefaultDataSettings Languages { get; set; } = null!;
|
||||
|
||||
public InstallDefaultDataSettings DataTypes { get; set; } = null!;
|
||||
|
||||
public InstallDefaultDataSettings MediaTypes { get; set; } = null!;
|
||||
|
||||
public InstallDefaultDataSettings MemberTypes { get; set; } = null!;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user