Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09acba310f | ||
|
|
93a13648b7 | ||
|
|
417eae32a7 | ||
|
|
c45813bebd | ||
|
|
074d2ea9f0 | ||
|
|
c45a79bcd7 | ||
|
|
c6140ed807 | ||
|
|
1eb0ec9755 | ||
|
|
61ce9e70e9 | ||
|
|
ecce15c39b | ||
|
|
085fcda2ba | ||
|
|
e0abfcb4b0 | ||
|
|
5bd22410a6 | ||
|
|
16f6e3a4b2 | ||
|
|
308369452d | ||
|
|
f97db75db0 |
@@ -59,5 +59,4 @@
|
||||
# Generated files - hidden by default in GitHub diffs
|
||||
src/Umbraco.Web.UI.Client/src/packages/core/backend-api/** linguist-generated
|
||||
src/Umbraco.Web.UI.Login/src/api/** linguist-generated
|
||||
templates/UmbracoExtension/Client/src/api/** linguist-generated
|
||||
src/Umbraco.Cms.Api.Management/OpenApi.json linguist-generated
|
||||
|
||||
@@ -7,7 +7,7 @@ body:
|
||||
id: "version"
|
||||
attributes:
|
||||
label: "Which Umbraco version are you using?"
|
||||
description: "Please write the *exact* version, example: `10.1.0`. Click the Umbraco logo in the top left corner of the backoffice to find the version you're using."
|
||||
description: "Please write the *exact* version, example: `10.1.0`. Use the help icon in the Umbraco backoffice to find the version you're using"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
||||
+2
-2
@@ -80,7 +80,8 @@ tools/docfx/
|
||||
/src/Umbraco.Web.UI/wwwroot/[Mm]edia/
|
||||
/src/Umbraco.Web.UI/App_Code/
|
||||
/src/Umbraco.Web.UI/App_Plugins/
|
||||
/src/Umbraco.Web.UI/[Uu]mbraco/[Dd]ata/
|
||||
/src/Umbraco.Web.UI/[Uu]mbraco/[Dd]ata/*
|
||||
!/src/Umbraco.Web.UI/[Uu]mbraco/[Dd]ata/Umbraco.Sample.sqlite.db
|
||||
/src/Umbraco.Web.UI/[Uu]mbraco/[Ll]ogs/
|
||||
/src/Umbraco.Web.UI/[Uu]mbraco/[Mm]odels/
|
||||
/src/Umbraco.Web.UI/Views/
|
||||
@@ -120,4 +121,3 @@ trace.zip
|
||||
/tests/Umbraco.Tests.Integration/appsettings-schema.*.json
|
||||
/tests/Umbraco.Tests.Integration/umbraco-package-schema.json
|
||||
/src/Umbraco.Cms/appsettings-schema.json
|
||||
.playwright-mcp/
|
||||
|
||||
@@ -448,14 +448,6 @@ When a PR changes Management API controllers or models, the `OpenApi.json` file
|
||||
|
||||
The backoffice is published to npm as `@umbraco-cms/backoffice`. Runtime dependencies are provided via importmap; npm peerDependencies provide types only. For full details on dependency hoisting, version range logic, and plugin development, see `/src/Umbraco.Web.UI.Client/CLAUDE.md` → "npm Package Publishing".
|
||||
|
||||
### SQL Server 2100-parameter limit
|
||||
|
||||
Any `WHERE IN (@0, @1, ...)` built from a runtime-sized collection risks hitting SQL Server's 2100-parameter ceiling and throwing `SqlException` 8003 in production.
|
||||
|
||||
Batch with `IEnumerable<T>.InGroupsOf(Constants.Sql.MaxParameterCount)` or `Database.FetchByGroups(...)` whenever the collection size is driven by user data — not just when it currently fits. Watch for products of two scaling dimensions (documents × languages, properties × versions) and config-tunable batch sizes whose defaults are safe but ceilings aren't.
|
||||
|
||||
Full guidance, safe patterns and decision rule: see `/src/Umbraco.Infrastructure/CLAUDE.md` → "Avoiding the SQL Server 2100-parameter limit".
|
||||
|
||||
### Known Limitations
|
||||
|
||||
1. **Circular Dependencies**: Avoided via `Lazy<T>` or event notifications
|
||||
@@ -552,14 +544,6 @@ Allowed, but cheap to write and cheaper to leave behind. Keep them short and tra
|
||||
|
||||
---
|
||||
|
||||
## 9. Testing Practices
|
||||
|
||||
### Tests for a bug fix must fail before the fix
|
||||
|
||||
Verify any test you add for a bug fix actually catches the bug: either write the failing test first (TDD), or temporarily revert the production change and confirm the test fails before re-applying. A test that passes both ways proves nothing. Watch for coincidental passes — default seed/sort orders can make a buggy path produce the right answer for the test's specific inputs; construct inputs so the broken and fixed behaviours give visibly different results.
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Essential Commands
|
||||
|
||||
+11
-11
@@ -45,7 +45,7 @@ parameters:
|
||||
- name: integrationNonReleaseTestFilter
|
||||
displayName: TestFilter used for non-release type builds
|
||||
type: string
|
||||
default: "TestCategory!=LongRunning&TestCategory!=NonCritical"
|
||||
default: "--filter TestCategory!=LongRunning&TestCategory!=NonCritical"
|
||||
- name: integrationReleaseTestFilter
|
||||
displayName: TestFilter used for release type builds
|
||||
type: string
|
||||
@@ -53,7 +53,7 @@ parameters:
|
||||
- name: nonWindowsIntegrationNonReleaseTestFilter
|
||||
displayName: TestFilter used for non-release type builds on non Windows agents
|
||||
type: string
|
||||
default: "TestCategory!=LongRunning&TestCategory!=NonCritical"
|
||||
default: "--filter TestCategory!=LongRunning&TestCategory!=NonCritical"
|
||||
- name: nonWindowsIntegrationReleaseTestFilter
|
||||
displayName: TestFilter used for release type builds on non Windows agents
|
||||
type: string
|
||||
@@ -455,13 +455,13 @@ stages:
|
||||
projects: "tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj"
|
||||
testRunTitle: Integration Tests SQLite - $(Agent.OS)
|
||||
${{ if and(eq(variables['Agent.OS'],'Windows_NT'), or(variables.releaseTestFilter, parameters.forceReleaseTestFilter)) }}:
|
||||
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build'
|
||||
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.integrationReleaseTestFilter}}'
|
||||
${{ elseif eq(variables['Agent.OS'],'Windows_NT') }}:
|
||||
arguments: '--filter "$(testFilter) & ${{parameters.integrationNonReleaseTestFilter}}" --configuration $(buildConfiguration) --no-build'
|
||||
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.integrationNonReleaseTestFilter}}'
|
||||
${{ elseif or(variables.releaseTestFilter, parameters.forceReleaseTestFilter) }}:
|
||||
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build'
|
||||
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationReleaseTestFilter}}'
|
||||
${{ else }}:
|
||||
arguments: '--filter "$(testFilter) & ${{parameters.nonWindowsIntegrationNonReleaseTestFilter}}" --configuration $(buildConfiguration) --no-build'
|
||||
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationNonReleaseTestFilter}}'
|
||||
# Integration Tests (SQL Server)
|
||||
- job:
|
||||
timeoutInMinutes: 180
|
||||
@@ -569,13 +569,13 @@ stages:
|
||||
projects: "tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj"
|
||||
testRunTitle: Integration Tests SQL Server - $(Agent.OS)
|
||||
${{ if and(eq(variables['Agent.OS'],'Windows_NT'), or(variables.releaseTestFilter, parameters.forceReleaseTestFilter)) }}:
|
||||
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build'
|
||||
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.integrationReleaseTestFilter}}'
|
||||
${{ elseif eq(variables['Agent.OS'],'Windows_NT') }}:
|
||||
arguments: '--filter "$(testFilter) & ${{parameters.integrationNonReleaseTestFilter}}" --configuration $(buildConfiguration) --no-build'
|
||||
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.integrationNonReleaseTestFilter}}'
|
||||
${{ elseif or(variables.releaseTestFilter, parameters.forceReleaseTestFilter) }}:
|
||||
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build'
|
||||
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationReleaseTestFilter}}'
|
||||
${{ else }}:
|
||||
arguments: '--filter "$(testFilter) & ${{parameters.nonWindowsIntegrationNonReleaseTestFilter}}" --configuration $(buildConfiguration) --no-build'
|
||||
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationNonReleaseTestFilter}}'
|
||||
|
||||
# Stop SQL Server
|
||||
- pwsh: docker stop mssql
|
||||
@@ -905,10 +905,10 @@ stages:
|
||||
- job: WaitForApproval
|
||||
displayName: Wait for manual approval
|
||||
pool: server
|
||||
timeoutInMinutes: 4320 # 3 days
|
||||
steps:
|
||||
- task: ManualValidation@0
|
||||
displayName: Manual approval to push to NuGet
|
||||
timeoutInMinutes: 4320 # 3 days
|
||||
inputs:
|
||||
notifyUsers: ''
|
||||
instructions: 'Approve to push the NuGet release.'
|
||||
|
||||
@@ -4,11 +4,11 @@ pr: none
|
||||
trigger: none
|
||||
|
||||
schedules:
|
||||
- cron: '0 0 * * *'
|
||||
displayName: Daily 0AM build (main)
|
||||
- cron: '0 6 * * *'
|
||||
displayName: Daily 6AM build (v18/dev)
|
||||
branches:
|
||||
include:
|
||||
- main
|
||||
- v18/dev
|
||||
|
||||
parameters:
|
||||
- name: skipIntegrationTests
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
@@ -68,7 +69,7 @@ internal abstract class ConfigureUmbracoOpenApiOptionsBase : IConfigureNamedOpti
|
||||
});
|
||||
|
||||
options.ShouldInclude = ShouldInclude;
|
||||
options.CreateSchemaReferenceId = UmbracoSchemaIdGenerator.CreateSchemaReferenceId;
|
||||
options.CreateSchemaReferenceId = CreateSchemaReferenceId;
|
||||
|
||||
options.AddOperationTransformer<UmbracoOperationIdTransformer>();
|
||||
|
||||
@@ -79,6 +80,33 @@ internal abstract class ConfigureUmbracoOpenApiOptionsBase : IConfigureNamedOpti
|
||||
.AddDocumentTransformer<SortTagsAndPathsTransformer>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a schema reference ID for the given JSON type info.
|
||||
/// Returns null for types that should be inlined, the default schema ID for non-Umbraco types,
|
||||
/// or a generated schema ID for Umbraco types.
|
||||
/// </summary>
|
||||
/// <param name="jsonTypeInfo">The JSON type info to create a schema reference ID for.</param>
|
||||
/// <returns>The schema reference ID, or null if the type should be inlined.</returns>
|
||||
internal static string? CreateSchemaReferenceId(JsonTypeInfo jsonTypeInfo)
|
||||
{
|
||||
// Ensure that only types that would normally be included in the schema generation are given a schema reference ID.
|
||||
// Otherwise, we should return null to inline them.
|
||||
var defaultSchemaReferenceId = OpenApiOptions.CreateDefaultSchemaReferenceId(jsonTypeInfo);
|
||||
if (defaultSchemaReferenceId is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Type targetType = Nullable.GetUnderlyingType(jsonTypeInfo.Type) ?? jsonTypeInfo.Type;
|
||||
|
||||
if (targetType.Namespace?.StartsWith("Umbraco.Cms") is not true)
|
||||
{
|
||||
return defaultSchemaReferenceId;
|
||||
}
|
||||
|
||||
return UmbracoSchemaIdGenerator.Generate(targetType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified API description should be included in this OpenAPI document.
|
||||
/// </summary>
|
||||
|
||||
@@ -30,27 +30,6 @@ internal static class OpenApiSchemaServiceExtensions
|
||||
this IServiceCollection services,
|
||||
string documentName,
|
||||
string jsonOptionsName)
|
||||
=> services.ReplaceOpenApiSchemaService(
|
||||
documentName,
|
||||
sp => sp.GetRequiredService<IOptionsMonitor<JsonOptions>>().Get(jsonOptionsName));
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the internal Microsoft <c>OpenApiSchemaService</c> registration for the specified document so that schema
|
||||
/// generation uses the <see cref="JsonOptions"/> instance produced by the supplied factory. Use this overload when
|
||||
/// the options need to be resolved from the service provider, computed at the last moment, or built in a way that
|
||||
/// doesn't fit the named-options lookup.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <param name="documentName">The OpenAPI document key.</param>
|
||||
/// <param name="jsonOptionsFactory">Factory invoked when the schema service is first resolved. Receives the resolving <see cref="IServiceProvider"/> and returns the <see cref="JsonOptions"/> to use.</param>
|
||||
/// <returns>The same <see cref="IServiceCollection"/> for chaining.</returns>
|
||||
/// <remarks>
|
||||
/// Workaround for <see href="https://github.com/dotnet/aspnetcore/issues/66340">dotnet/aspnetcore#66340</see>.
|
||||
/// </remarks>
|
||||
public static IServiceCollection ReplaceOpenApiSchemaService(
|
||||
this IServiceCollection services,
|
||||
string documentName,
|
||||
Func<IServiceProvider, JsonOptions> jsonOptionsFactory)
|
||||
{
|
||||
ServiceDescriptor descriptor = services.FirstOrDefault(sd =>
|
||||
sd.ServiceType.FullName == OpenApiSchemaServiceFullName
|
||||
@@ -68,7 +47,7 @@ internal static class OpenApiSchemaServiceExtensions
|
||||
sp,
|
||||
descriptor.ServiceType,
|
||||
key,
|
||||
Options.Create(jsonOptionsFactory(sp))));
|
||||
Options.Create(sp.GetRequiredService<IOptionsMonitor<JsonOptions>>().Get(jsonOptionsName))));
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
+1
-15
@@ -23,26 +23,12 @@ public static class OpenApiServiceCollectionExtensions
|
||||
this IServiceCollection services,
|
||||
string documentName,
|
||||
string? documentTitle = null)
|
||||
=> services.AddOpenApiDocumentToUi(documentName, () => documentTitle);
|
||||
|
||||
/// <summary>
|
||||
/// Adds an OpenAPI document to the OpenAPI UI document selector dropdown, resolving the title lazily so
|
||||
/// callers (such as builder-pattern helpers) can defer it until SwaggerUI options are resolved.
|
||||
/// </summary>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/> instance.</param>
|
||||
/// <param name="documentName">The name/identifier of the OpenAPI document.</param>
|
||||
/// <param name="documentTitleFactory">Factory invoked when SwaggerUI options are resolved. Returning <c>null</c> falls back to <paramref name="documentName"/>.</param>
|
||||
/// <returns>The <see cref="IServiceCollection"/> instance.</returns>
|
||||
internal static IServiceCollection AddOpenApiDocumentToUi(
|
||||
this IServiceCollection services,
|
||||
string documentName,
|
||||
Func<string?> documentTitleFactory)
|
||||
{
|
||||
services.AddOptions<SwaggerUIOptions>()
|
||||
.Configure<IOptions<UmbracoOpenApiOptions>>((swaggerUiOptions, openApiOptions) =>
|
||||
{
|
||||
var openApiRoute = openApiOptions.Value.RouteTemplate.Replace("{documentName}", documentName).EnsureStartsWith("/");
|
||||
swaggerUiOptions.SwaggerEndpoint(openApiRoute, documentTitleFactory() ?? documentName);
|
||||
swaggerUiOptions.SwaggerEndpoint(openApiRoute, documentTitle ?? documentName);
|
||||
swaggerUiOptions.ConfigObject.Urls = swaggerUiOptions.ConfigObject.Urls.OrderBy(x => x.Name);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
using Microsoft.AspNetCore.Http.Json;
|
||||
using Microsoft.AspNetCore.OpenApi;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Api.Common.DependencyInjection;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Fluent builder for configuring a custom OpenAPI document.
|
||||
/// </summary>
|
||||
public sealed class BackOfficeOpenApiDocumentBuilder
|
||||
{
|
||||
private readonly List<Action<OpenApiOptions>> _configurations = [];
|
||||
|
||||
private string? _title;
|
||||
private string? _uiTitle;
|
||||
private bool _includedInUi = true;
|
||||
private Func<IServiceProvider, JsonOptions>? _httpJsonOptionsFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BackOfficeOpenApiDocumentBuilder"/> class.
|
||||
/// </summary>
|
||||
/// <param name="documentName">The name of the OpenAPI document being configured.</param>
|
||||
internal BackOfficeOpenApiDocumentBuilder(string documentName)
|
||||
=> DocumentName = documentName;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the OpenAPI document being configured.
|
||||
/// </summary>
|
||||
public string DocumentName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the document's <c>Info.Title</c>. Also used as the UI dropdown label unless overridden via
|
||||
/// <see cref="WithUiTitle"/>.
|
||||
/// </summary>
|
||||
/// <param name="title">The title to display.</param>
|
||||
/// <returns>The same builder for chaining.</returns>
|
||||
public BackOfficeOpenApiDocumentBuilder WithTitle(string title)
|
||||
{
|
||||
_title = title;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the UI dropdown label for this document.
|
||||
/// </summary>
|
||||
/// <param name="uiTitle">The label to display.</param>
|
||||
/// <returns>The same builder for chaining.</returns>
|
||||
public BackOfficeOpenApiDocumentBuilder WithUiTitle(string uiTitle)
|
||||
{
|
||||
_uiTitle = uiTitle;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Excludes this document from the UI dropdown.
|
||||
/// </summary>
|
||||
/// <returns>The same builder for chaining.</returns>
|
||||
public BackOfficeOpenApiDocumentBuilder ExcludeFromUi()
|
||||
{
|
||||
_includedInUi = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an <see cref="OpenApiOptions"/> configuration callback. Multiple calls compose.
|
||||
/// </summary>
|
||||
/// <param name="configure">Callback to configure the options.</param>
|
||||
/// <returns>The same builder for chaining.</returns>
|
||||
public BackOfficeOpenApiDocumentBuilder ConfigureOpenApiOptions(Action<OpenApiOptions> configure)
|
||||
{
|
||||
_configurations.Add(configure);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the named <see cref="JsonOptions">Microsoft.AspNetCore.Http.Json.JsonOptions</see> used when
|
||||
/// generating this document's schema. Use this to match the serialization conventions of the API
|
||||
/// endpoints the document describes.
|
||||
/// </summary>
|
||||
/// <param name="jsonOptionsName">The name of the registered HTTP <see cref="JsonOptions"/> to apply.</param>
|
||||
/// <returns>The same builder for chaining.</returns>
|
||||
public BackOfficeOpenApiDocumentBuilder WithJsonOptions(string jsonOptionsName)
|
||||
=> WithJsonOptions(sp => sp.GetRequiredService<IOptionsMonitor<JsonOptions>>().Get(jsonOptionsName));
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="JsonOptions">Microsoft.AspNetCore.Http.Json.JsonOptions</see> used when
|
||||
/// generating this document's schema. Use this to match the serialization conventions of the API
|
||||
/// endpoints the document describes.
|
||||
/// </summary>
|
||||
/// <param name="jsonOptions">The HTTP JSON options to apply.</param>
|
||||
/// <returns>The same builder for chaining.</returns>
|
||||
public BackOfficeOpenApiDocumentBuilder WithJsonOptions(JsonOptions jsonOptions)
|
||||
=> WithJsonOptions(_ => jsonOptions);
|
||||
|
||||
/// <summary>
|
||||
/// Sets a factory that produces the <see cref="JsonOptions">Microsoft.AspNetCore.Http.Json.JsonOptions</see>
|
||||
/// used when generating this document's schema. Use this to match the serialization conventions of the
|
||||
/// API endpoints the document describes.
|
||||
/// </summary>
|
||||
/// <param name="jsonOptionsFactory">Factory invoked when the schema service is first resolved.</param>
|
||||
/// <returns>The same builder for chaining.</returns>
|
||||
public BackOfficeOpenApiDocumentBuilder WithJsonOptions(Func<IServiceProvider, JsonOptions> jsonOptionsFactory)
|
||||
{
|
||||
_httpJsonOptionsFactory = jsonOptionsFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the accumulated configuration to the supplied <see cref="IUmbracoBuilder"/>'s service
|
||||
/// collection. Called by <c>AddBackOfficeOpenApiDocument</c> once the user-supplied callback returns.
|
||||
/// </summary>
|
||||
/// <param name="builder">The Umbraco builder to register services against.</param>
|
||||
internal void Build(IUmbracoBuilder builder)
|
||||
{
|
||||
builder.Services.AddOpenApi(
|
||||
DocumentName,
|
||||
options =>
|
||||
{
|
||||
options.ShouldInclude = apiDescription =>
|
||||
apiDescription.ActionDescriptor.HasMapToApiAttribute(DocumentName);
|
||||
|
||||
options.CreateSchemaReferenceId = UmbracoSchemaIdGenerator.CreateSchemaReferenceId;
|
||||
|
||||
if (_title is not null)
|
||||
{
|
||||
options.AddDocumentTransformer((document, _, _) =>
|
||||
{
|
||||
document.Info.Title = _title;
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
|
||||
// Generate operation IDs using Umbraco's naming conventions.
|
||||
options.AddOperationTransformer<UmbracoOperationIdTransformer>();
|
||||
|
||||
// Trim redundant JSON-equivalent MIME types (e.g. text/json, application/*+json, text/plain)
|
||||
// that ASP.NET Core adds alongside application/json.
|
||||
options.AddOperationTransformer<MimeTypesTransformer>();
|
||||
|
||||
// Mark non-nullable properties as required so generated SDKs reflect the C# nullability.
|
||||
options.AddSchemaTransformer<RequireNonNullablePropertiesSchemaTransformer>();
|
||||
|
||||
// Tag actions by group name and cleanup unused tags (caused by the tag changes).
|
||||
options
|
||||
.AddOperationTransformer<TagActionsByGroupNameTransformer>()
|
||||
.AddDocumentTransformer<TagActionsByGroupNameTransformer>()
|
||||
.AddDocumentTransformer<SortTagsAndPathsTransformer>();
|
||||
|
||||
foreach (Action<OpenApiOptions> configure in _configurations)
|
||||
{
|
||||
configure(options);
|
||||
}
|
||||
});
|
||||
|
||||
if (_includedInUi)
|
||||
{
|
||||
builder.Services.AddOpenApiDocumentToUi(DocumentName, _uiTitle ?? _title);
|
||||
}
|
||||
|
||||
if (_httpJsonOptionsFactory is not null)
|
||||
{
|
||||
builder.Services.ReplaceOpenApiSchemaService(DocumentName, _httpJsonOptionsFactory);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Net.Mime;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
using Microsoft.AspNetCore.OpenApi;
|
||||
using Microsoft.OpenApi;
|
||||
using Umbraco.Extensions;
|
||||
@@ -7,31 +7,11 @@ using Umbraco.Extensions;
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Trims redundant JSON-equivalent media types from OpenAPI operations.
|
||||
/// Removes unwanted MIME types from OpenAPI operations, keeping only the content types
|
||||
/// declared by <c>[Consumes]</c> for request bodies or <c>application/json</c> as the default.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// ASP.NET Core's content negotiation populates operations with several media types that all serialize to JSON
|
||||
/// (<c>text/json</c>, <c>application/*+json</c>, and <c>text/plain</c> alongside <c>application/json</c>).
|
||||
/// When <c>application/json</c> is present on a response or request body, this transformer strips those
|
||||
/// equivalents so OpenAPI consumers and generated SDKs aren't burdened with variants that produce identical
|
||||
/// payloads. Non-JSON media types (e.g. <c>application/xml</c>, <c>application/octet-stream</c>) are preserved.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Request bodies additionally honour <c>[Consumes]</c>: when the attribute is present, the request content is
|
||||
/// replaced entirely with the declared content types, taking precedence over the
|
||||
/// JSON-equivalent stripping above.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal class MimeTypesTransformer : IOpenApiOperationTransformer
|
||||
{
|
||||
private static readonly string[] _jsonEquivalentMimeTypes =
|
||||
[
|
||||
MediaTypeNames.Text.Plain,
|
||||
"application/*+json",
|
||||
"text/json"
|
||||
];
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task TransformAsync(
|
||||
OpenApiOperation operation,
|
||||
@@ -60,29 +40,29 @@ internal class MimeTypesTransformer : IOpenApiOperationTransformer
|
||||
}
|
||||
else
|
||||
{
|
||||
RemoveJsonEquivalentMimeTypes(requestContent);
|
||||
RemoveNonJsonMimeTypes(requestContent);
|
||||
}
|
||||
}
|
||||
|
||||
// For responses, drop JSON-equivalent media types when application/json is present.
|
||||
// For responses, always keep only application/json.
|
||||
foreach (IOpenApiResponse response in (operation.Responses ?? []).Values)
|
||||
{
|
||||
if (response is OpenApiResponse openApiResponse)
|
||||
{
|
||||
RemoveJsonEquivalentMimeTypes(openApiResponse.Content);
|
||||
RemoveNonJsonMimeTypes(openApiResponse.Content);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static void RemoveJsonEquivalentMimeTypes(IDictionary<string, OpenApiMediaType>? content)
|
||||
private static void RemoveNonJsonMimeTypes(IDictionary<string, OpenApiMediaType>? content)
|
||||
{
|
||||
if (content?.ContainsKey(MediaTypeNames.Application.Json) != true)
|
||||
if (content?.ContainsKey("application/json") != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
content.RemoveAll(r => _jsonEquivalentMimeTypes.Contains(r.Key, StringComparer.OrdinalIgnoreCase));
|
||||
content.RemoveAll(r => r.Key != "application/json");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="IUmbracoBuilder"/> to register custom OpenAPI documents.
|
||||
/// </summary>
|
||||
public static class UmbracoBuilderOpenApiExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers a custom OpenAPI document with Umbraco's defaults applied.
|
||||
/// </summary>
|
||||
/// <param name="builder">The Umbraco builder.</param>
|
||||
/// <param name="documentName">The document name. Matches the <c>[MapToApi]</c> value on controllers to include.</param>
|
||||
/// <param name="configure">Optional callback to customize the document.</param>
|
||||
/// <returns>The same <see cref="IUmbracoBuilder"/> for chaining.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The following defaults are applied to the document and can be customized or overridden via the
|
||||
/// <paramref name="configure"/> callback:
|
||||
/// </para>
|
||||
/// <list type="bullet">
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// Endpoints are filtered by <c>[MapToApi(documentName)]</c>; only matching endpoints appear in the document.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// Schema reference IDs are generated by <see cref="UmbracoSchemaIdGenerator.CreateSchemaReferenceId"/>, applying
|
||||
/// Umbraco naming conventions to types under the <c>Umbraco.Cms</c> namespace and falling back to the framework
|
||||
/// default for everything else. Register your own <c>CreateSchemaReferenceId</c> delegate via
|
||||
/// <see cref="BackOfficeOpenApiDocumentBuilder.ConfigureOpenApiOptions"/> to override.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// Operation IDs are generated by <see cref="UmbracoOperationIdTransformer"/>. Register your own
|
||||
/// <see cref="Microsoft.AspNetCore.OpenApi.IOpenApiOperationTransformer"/> via
|
||||
/// <see cref="BackOfficeOpenApiDocumentBuilder.ConfigureOpenApiOptions"/> to override.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// Operations are tagged by their controller's API group name, and the resulting tags and paths are sorted
|
||||
/// for stable, diffable document output.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// Redundant JSON-equivalent media types (such as <c>text/json</c>, <c>application/*+json</c>, and
|
||||
/// <c>text/plain</c>) are stripped from request and response content when <c>application/json</c> is present,
|
||||
/// so the document doesn't list spurious media types that ASP.NET Core adds by default.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// Non-nullable properties are marked as <c>required</c> in the schema so generated client SDKs reflect
|
||||
/// C# nullability. Override via <see cref="BackOfficeOpenApiDocumentBuilder.ConfigureOpenApiOptions"/>
|
||||
/// if your types don't follow this convention.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// The document is registered in the OpenAPI UI document selector dropdown. Call
|
||||
/// <see cref="BackOfficeOpenApiDocumentBuilder.ExcludeFromUi"/> to opt out.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public static IUmbracoBuilder AddBackOfficeOpenApiDocument(
|
||||
this IUmbracoBuilder builder,
|
||||
string documentName,
|
||||
Action<BackOfficeOpenApiDocumentBuilder>? configure = null)
|
||||
{
|
||||
var documentBuilder = new BackOfficeOpenApiDocumentBuilder(documentName);
|
||||
configure?.Invoke(documentBuilder);
|
||||
documentBuilder.Build(builder);
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -29,23 +29,16 @@ public class UmbracoOperationIdTransformer : IOpenApiOperationTransformer
|
||||
OpenApiOperationTransformerContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var operationId = GenerateOperationId(context);
|
||||
if (operationId is not null)
|
||||
{
|
||||
operation.OperationId = operationId;
|
||||
}
|
||||
|
||||
operation.OperationId = GenerateOperationId(context);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static string? GenerateOperationId(OpenApiOperationTransformerContext context)
|
||||
private static string GenerateOperationId(OpenApiOperationTransformerContext context)
|
||||
{
|
||||
ApiDescription apiDescription = context.Description;
|
||||
if (apiDescription.ActionDescriptor is not ControllerActionDescriptor controllerActionDescriptor)
|
||||
{
|
||||
// Minimal APIs and other non-MVC endpoints don't carry a ControllerActionDescriptor; leave their
|
||||
// operation ID untouched so the framework's default applies.
|
||||
return null;
|
||||
throw new ArgumentException($"This handler operates only on {nameof(ControllerActionDescriptor)}.");
|
||||
}
|
||||
|
||||
ApiVersion defaultVersion = context.ApplicationServices.GetRequiredService<IOptions<ApiVersioningOptions>>().Value.DefaultApiVersion;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.AspNetCore.OpenApi;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
@@ -31,32 +29,6 @@ public static class UmbracoSchemaIdGenerator
|
||||
return Regex.Replace(name, @"[^\w]", string.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a schema reference ID for the given JSON type info, applying Umbraco's naming conventions to
|
||||
/// types in the <c>Umbraco.Cms</c> namespace and falling back to the framework default for other types.
|
||||
/// </summary>
|
||||
/// <param name="jsonTypeInfo">The JSON type info to create a schema reference ID for.</param>
|
||||
/// <returns>The schema reference ID, or <c>null</c> if the type should be inlined.</returns>
|
||||
internal static string? CreateSchemaReferenceId(JsonTypeInfo jsonTypeInfo)
|
||||
{
|
||||
// Ensure that only types that would normally be included in the schema generation are given a schema reference ID.
|
||||
// Otherwise, we should return null to inline them.
|
||||
var defaultSchemaReferenceId = OpenApiOptions.CreateDefaultSchemaReferenceId(jsonTypeInfo);
|
||||
if (defaultSchemaReferenceId is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Type targetType = Nullable.GetUnderlyingType(jsonTypeInfo.Type) ?? jsonTypeInfo.Type;
|
||||
|
||||
if (targetType.Namespace?.StartsWith("Umbraco.Cms") is not true)
|
||||
{
|
||||
return defaultSchemaReferenceId;
|
||||
}
|
||||
|
||||
return Generate(targetType);
|
||||
}
|
||||
|
||||
private static string SanitizedTypeName(Type t) => t.Name
|
||||
// first grab the "non-generic" part of any generic type name (i.e. "PagedViewModel`1" becomes "PagedViewModel")
|
||||
.Split('`').First()
|
||||
|
||||
@@ -5,7 +5,6 @@ using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Umbraco.Cms.Api.Common.DependencyInjection;
|
||||
@@ -172,12 +171,6 @@ public static class UmbracoBuilderExtensions
|
||||
builder.Services.AddUnique<IDeliveryApiOutputCacheRequestFilter, DefaultDeliveryApiOutputCacheRequestFilter>();
|
||||
builder.Services.AddUnique<IDeliveryApiOutputCacheManager, DeliveryApiOutputCacheManager>();
|
||||
|
||||
// Signal that Umbraco has enabled output caching so the application builder registers
|
||||
// the output cache middleware. Gated via a marker rather than IOutputCacheStore so that
|
||||
// applications calling services.AddOutputCache(...) for their own purposes are not
|
||||
// affected by Umbraco's automatic middleware registration.
|
||||
builder.Services.TryAddSingleton<IUmbracoManagedOutputCacheMarker, UmbracoManagedOutputCacheMarker>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ using Microsoft.AspNetCore.OpenApi;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi;
|
||||
using Umbraco.Cms.Api.Common.OpenApi;
|
||||
using Umbraco.Cms.Api.Common.Configuration;
|
||||
using Umbraco.Cms.Api.Delivery.OpenApi.Extensions;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
@@ -122,52 +122,14 @@ public sealed class ContentTypeSchemaTransformer : IOpenApiSchemaTransformer, IO
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
foreach ((var schemaId, IOpenApiSchema componentsSchema) in document.Components.Schemas)
|
||||
foreach (IOpenApiSchema componentsSchema in document.Components.Schemas.Values)
|
||||
{
|
||||
ResolveSchemaReferences(document, componentsSchema);
|
||||
FixAutoBuiltDiscriminatorMapping(document, schemaId, componentsSchema);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Repairs broken discriminator mapping refs auto-built by the framework for polymorphic types.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The framework prefixes each ref with the base schema id, but the derived schemas are
|
||||
/// registered without that prefix. Stripping the prefix recovers the correct ref.
|
||||
/// </remarks>
|
||||
private static void FixAutoBuiltDiscriminatorMapping(OpenApiDocument document, string parentSchemaId, IOpenApiSchema schema)
|
||||
{
|
||||
if (schema is not OpenApiSchema concrete
|
||||
|| concrete.Discriminator?.Mapping is not { } mapping
|
||||
|| document.Components?.Schemas is not { } schemas)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ((var key, OpenApiSchemaReference currentRef) in mapping.ToList())
|
||||
{
|
||||
var targetId = currentRef.Reference.Id;
|
||||
if (string.IsNullOrEmpty(targetId) || schemas.ContainsKey(targetId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (targetId.StartsWith(parentSchemaId, StringComparison.Ordinal) is false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var stripped = targetId[parentSchemaId.Length..];
|
||||
if (schemas.ContainsKey(stripped))
|
||||
{
|
||||
mapping[key] = new OpenApiSchemaReference(stripped, document);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task TransformAsync(
|
||||
OpenApiSchema schema,
|
||||
@@ -533,7 +495,7 @@ public sealed class ContentTypeSchemaTransformer : IOpenApiSchemaTransformer, IO
|
||||
}
|
||||
|
||||
private static string? GetSchemaId(JsonTypeInfo type)
|
||||
=> UmbracoSchemaIdGenerator.CreateSchemaReferenceId(type);
|
||||
=> ConfigureUmbracoOpenApiOptionsBase.CreateSchemaReferenceId(type);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a temporary placeholder schema to break circular reference chains during schema generation.
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
using Microsoft.AspNetCore.OpenApi;
|
||||
using Umbraco.Cms.Api.Common.Configuration;
|
||||
using Umbraco.Cms.Api.Common.OpenApi;
|
||||
using Umbraco.Cms.Api.Management.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Management.OpenApi;
|
||||
using Umbraco.Cms.Api.Management.OpenApi.Transformers;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Configures the OpenAPI options for the Umbraco Management API.
|
||||
/// </summary>
|
||||
internal class ConfigureUmbracoManagementApiOpenApiOptions : ConfigureUmbracoOpenApiOptionsBase
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override string ApiName => ManagementApiConfiguration.ApiName;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string ApiTitle => ManagementApiConfiguration.ApiTitle;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string ApiVersion => "Latest";
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string ApiDescription =>
|
||||
"This shows all APIs available in this version of Umbraco - including all the legacy apis that are available for backward compatibility";
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void ConfigureOpenApi(OpenApiOptions options)
|
||||
{
|
||||
base.ConfigureOpenApi(options);
|
||||
|
||||
// Sets Security requirement on backoffice apis
|
||||
options.AddBackofficeSecurityRequirements();
|
||||
|
||||
options.AddSchemaTransformer<RequireNonNullablePropertiesSchemaTransformer>();
|
||||
options.AddSchemaTransformer<FixFileReturnTypesTransformer>();
|
||||
options.AddOperationTransformer<MimeTypesTransformer>();
|
||||
options.AddOperationTransformer<ResponseHeaderTransformer>();
|
||||
options.AddOperationTransformer<NotificationHeaderTransformer>();
|
||||
}
|
||||
}
|
||||
+3
-6
@@ -53,14 +53,11 @@ public class SearchDataTypeItemController : DatatypeItemControllerBase
|
||||
return Ok(new PagedModel<DataTypeItemResponseModel> { Total = searchResult.Total });
|
||||
}
|
||||
|
||||
Guid[] keys = searchResult.Items.Select(x => x.Key).ToArray();
|
||||
IEnumerable<IDataType> dataTypes = await _dataTypeService.GetAllAsync(keys);
|
||||
IEnumerable<IDataType> orderedDataTypes = OrderByRequestedIds(dataTypes, keys);
|
||||
|
||||
IEnumerable<IDataType> dataTypes = await _dataTypeService.GetAllAsync(searchResult.Items.Select(item => item.Key).ToArray());
|
||||
var result = new PagedModel<DataTypeItemResponseModel>
|
||||
{
|
||||
Items = _mapper.MapEnumerable<IDataType, DataTypeItemResponseModel>(orderedDataTypes),
|
||||
Total = searchResult.Total,
|
||||
Items = _mapper.MapEnumerable<IDataType, DataTypeItemResponseModel>(dataTypes),
|
||||
Total = searchResult.Total
|
||||
};
|
||||
|
||||
return Ok(result);
|
||||
|
||||
+2
-2
@@ -49,8 +49,8 @@ public class ByKeyElementFolderController : ElementFolderControllerBase
|
||||
{
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ElementContainerPermissionResource.WithKeys(ActionElementContainerBrowse.ActionLetter, id),
|
||||
AuthorizationPolicies.ElementFolderPermissionByResource);
|
||||
ElementPermissionResource.WithKeys(ActionElementBrowse.ActionLetter, id),
|
||||
AuthorizationPolicies.ElementPermissionByResource);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
|
||||
+2
-2
@@ -50,8 +50,8 @@ public class CreateElementFolderController : ElementFolderControllerBase
|
||||
{
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ElementContainerPermissionResource.WithKeys(ActionElementContainerNew.ActionLetter, createFolderRequestModel.Parent?.Id),
|
||||
AuthorizationPolicies.ElementFolderPermissionByResource);
|
||||
ElementPermissionResource.WithKeys(ActionElementNew.ActionLetter, createFolderRequestModel.Parent?.Id),
|
||||
AuthorizationPolicies.ElementPermissionByResource);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
|
||||
+1
-12
@@ -49,18 +49,7 @@ public class DeleteElementFolderController : ElementFolderControllerBase
|
||||
{
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ElementContainerPermissionResource.WithKeys(ActionElementContainerDelete.ActionLetter, id),
|
||||
AuthorizationPolicies.ElementFolderPermissionByResource);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
return Forbidden();
|
||||
}
|
||||
|
||||
// Also authorize deletion of all descendant elements.
|
||||
authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ElementPermissionResource.Branch(ActionElementDelete.ActionLetter, id),
|
||||
ElementPermissionResource.WithKeys(ActionElementDelete.ActionLetter, id),
|
||||
AuthorizationPolicies.ElementPermissionByResource);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
|
||||
+4
-4
@@ -60,8 +60,8 @@ public class MoveElementFolderController : ElementFolderControllerBase
|
||||
// Check Move permission on source folder
|
||||
AuthorizationResult sourceAuthorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ElementContainerPermissionResource.WithKeys(ActionElementContainerMove.ActionLetter, id),
|
||||
AuthorizationPolicies.ElementFolderPermissionByResource);
|
||||
ElementPermissionResource.WithKeys(ActionElementMove.ActionLetter, id),
|
||||
AuthorizationPolicies.ElementPermissionByResource);
|
||||
|
||||
if (!sourceAuthorizationResult.Succeeded)
|
||||
{
|
||||
@@ -71,8 +71,8 @@ public class MoveElementFolderController : ElementFolderControllerBase
|
||||
// Check Create permission on target (where we're moving to)
|
||||
AuthorizationResult targetAuthorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ElementContainerPermissionResource.WithKeys(ActionElementContainerNew.ActionLetter, moveFolderRequestModel.Target?.Id),
|
||||
AuthorizationPolicies.ElementFolderPermissionByResource);
|
||||
ElementPermissionResource.WithKeys(ActionElementNew.ActionLetter, moveFolderRequestModel.Target?.Id),
|
||||
AuthorizationPolicies.ElementPermissionByResource);
|
||||
|
||||
if (!targetAuthorizationResult.Succeeded)
|
||||
{
|
||||
|
||||
+2
-2
@@ -57,8 +57,8 @@ public class MoveToRecycleBinElementFolderController : ElementFolderControllerBa
|
||||
{
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ElementContainerPermissionResource.WithKeys(ActionElementContainerDelete.ActionLetter, id),
|
||||
AuthorizationPolicies.ElementFolderPermissionByResource);
|
||||
ElementPermissionResource.WithKeys(ActionElementDelete.ActionLetter, id),
|
||||
AuthorizationPolicies.ElementPermissionByResource);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
|
||||
+2
-2
@@ -51,8 +51,8 @@ public class UpdateElementFolderController : ElementFolderControllerBase
|
||||
{
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ElementContainerPermissionResource.WithKeys(ActionElementContainerUpdate.ActionLetter, id),
|
||||
AuthorizationPolicies.ElementFolderPermissionByResource);
|
||||
ElementPermissionResource.WithKeys(ActionElementUpdate.ActionLetter, id),
|
||||
AuthorizationPolicies.ElementPermissionByResource);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
|
||||
+1
-2
@@ -61,9 +61,8 @@ public class SearchElementItemController : ElementItemControllerBase
|
||||
.GetAll(UmbracoObjectTypes.Element, keys)
|
||||
.OfType<IElementEntitySlim>()
|
||||
.ToArray();
|
||||
List<IElementEntitySlim> orderedElements = OrderByRequestedIds(elements, keys);
|
||||
|
||||
ElementItemResponseModel[] items = await Task.WhenAll(orderedElements.Select(_elementPresentationFactory.CreateItemResponseModelAsync));
|
||||
ElementItemResponseModel[] items = await Task.WhenAll(elements.Select(_elementPresentationFactory.CreateItemResponseModelAsync));
|
||||
|
||||
return Ok(
|
||||
new PagedModel<ElementItemResponseModel>
|
||||
|
||||
+1
-12
@@ -63,18 +63,7 @@ public class DeleteElementFolderRecycleBinController : ElementRecycleBinControll
|
||||
{
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ElementContainerPermissionResource.RecycleBin(ActionElementContainerDelete.ActionLetter),
|
||||
AuthorizationPolicies.ElementFolderPermissionByResource);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
return Forbidden();
|
||||
}
|
||||
|
||||
// Also authorize deletion of all descendant elements.
|
||||
authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ElementPermissionResource.Branch(ActionElementDelete.ActionLetter, id),
|
||||
ElementPermissionResource.RecycleBin(ActionElementDelete.ActionLetter),
|
||||
AuthorizationPolicies.ElementPermissionByResource);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
|
||||
+2
-2
@@ -59,8 +59,8 @@ public class OriginalParentElementFolderRecycleBinController : ElementRecycleBin
|
||||
{
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ElementContainerPermissionResource.RecycleBin(ActionElementContainerBrowse.ActionLetter),
|
||||
AuthorizationPolicies.ElementFolderPermissionByResource);
|
||||
ElementPermissionResource.RecycleBin(ActionElementBrowse.ActionLetter),
|
||||
AuthorizationPolicies.ElementPermissionByResource);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
|
||||
+2
-2
@@ -64,8 +64,8 @@ public class RestoreElementFolderRecycleBinController : ElementRecycleBinControl
|
||||
{
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ElementContainerPermissionResource.RecycleBin(ActionElementContainerMove.ActionLetter),
|
||||
AuthorizationPolicies.ElementFolderPermissionByResource);
|
||||
ElementPermissionResource.RecycleBin(ActionElementMove.ActionLetter),
|
||||
AuthorizationPolicies.ElementPermissionByResource);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
|
||||
+3
-6
@@ -54,14 +54,11 @@ public class SearchMediaTypeItemController : MediaTypeItemControllerBase
|
||||
return Task.FromResult<IActionResult>(Ok(new PagedModel<MediaTypeItemResponseModel> { Total = searchResult.Total }));
|
||||
}
|
||||
|
||||
Guid[] keys = searchResult.Items.Select(item => item.Key).ToArray();
|
||||
IEnumerable<IMediaType> mediaTypes = _mediaTypeService.GetMany(keys.EmptyNull());
|
||||
IEnumerable<IMediaType> orderedMediaTypes = OrderByRequestedIds(mediaTypes, keys);
|
||||
|
||||
IEnumerable<IMediaType> mediaTypes = _mediaTypeService.GetMany(searchResult.Items.Select(item => item.Key).ToArray().EmptyNull());
|
||||
var result = new PagedModel<MediaTypeItemResponseModel>
|
||||
{
|
||||
Items = _mapper.MapEnumerable<IMediaType, MediaTypeItemResponseModel>(orderedMediaTypes),
|
||||
Total = searchResult.Total,
|
||||
Items = _mapper.MapEnumerable<IMediaType, MediaTypeItemResponseModel>(mediaTypes),
|
||||
Total = searchResult.Total
|
||||
};
|
||||
|
||||
return Task.FromResult<IActionResult>(Ok(result));
|
||||
|
||||
+3
-14
@@ -32,14 +32,6 @@ public class SearchMemberTypeItemController : MemberTypeItemControllerBase
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches for member type items matching the specified query, with support for pagination.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
|
||||
/// <param name="query">The search query used to filter member type items.</param>
|
||||
/// <param name="skip">The number of items to skip before starting to collect the result set (used for pagination).</param>
|
||||
/// <param name="take">The maximum number of items to return in the result set (used for pagination).</param>
|
||||
/// <returns>A task representing the asynchronous operation. The task result contains an <see cref="IActionResult"/> with a <see cref="PagedModel{MemberTypeItemResponseModel}"/> containing the search results.</returns>
|
||||
[HttpGet("search")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedModel<MemberTypeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
@@ -53,14 +45,11 @@ public class SearchMemberTypeItemController : MemberTypeItemControllerBase
|
||||
return Task.FromResult<IActionResult>(Ok(new PagedModel<MemberTypeItemResponseModel> { Total = searchResult.Total }));
|
||||
}
|
||||
|
||||
Guid[] keys = searchResult.Items.Select(item => item.Key).ToArray();
|
||||
IEnumerable<IMemberType> memberTypes = _memberTypeService.GetMany(keys);
|
||||
IEnumerable<IMemberType> orderedMemberTypes = OrderByRequestedIds(memberTypes, keys);
|
||||
|
||||
IEnumerable<IMemberType> memberTypes = _memberTypeService.GetMany(searchResult.Items.Select(item => item.Key).ToArray());
|
||||
var result = new PagedModel<MemberTypeItemResponseModel>
|
||||
{
|
||||
Items = _mapper.MapEnumerable<IMemberType, MemberTypeItemResponseModel>(orderedMemberTypes),
|
||||
Total = searchResult.Total,
|
||||
Items = _mapper.MapEnumerable<IMemberType, MemberTypeItemResponseModel>(memberTypes),
|
||||
Total = searchResult.Total
|
||||
};
|
||||
|
||||
return Task.FromResult<IActionResult>(Ok(result));
|
||||
|
||||
+45
-16
@@ -8,38 +8,67 @@ using Umbraco.Cms.Core.Security;
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.RedirectUrlManagement;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for setting the redirect URL tracking status. Retained for backwards compatibility only;
|
||||
/// the endpoint no longer modifies any configuration.
|
||||
/// Controller for setting the redirect URL tracking status.
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Obsolete("This controller is deprecated and no longer modifies the configuration. Set the Umbraco:CMS:WebRouting:DisableRedirectUrlTracking configuration key instead. Scheduled for removal in Umbraco 19.")]
|
||||
public class SetStatusRedirectUrlManagementController : RedirectUrlManagementControllerBase
|
||||
{
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
private readonly IConfigManipulator _configManipulator;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SetStatusRedirectUrlManagementController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="backOfficeSecurityAccessor">Ignored. Retained for binary compatibility.</param>
|
||||
/// <param name="configManipulator">Ignored. Retained for binary compatibility.</param>
|
||||
/// <param name="backOfficeSecurityAccessor">The back office security accessor.</param>
|
||||
/// <param name="configManipulator">The configuration manipulator.</param>
|
||||
public SetStatusRedirectUrlManagementController(
|
||||
#pragma warning disable IDE0060 // Remove unused parameter
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
|
||||
IConfigManipulator configManipulator)
|
||||
#pragma warning restore IDE0060 // Remove unused parameter
|
||||
{
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
_configManipulator = configManipulator;
|
||||
}
|
||||
|
||||
// TODO: Consider if we should even allow this, or only allow using the appsettings
|
||||
// We generally don't want to edit the appsettings from our code.
|
||||
// But maybe there is a valid use case for doing it on the fly.
|
||||
/// <summary>
|
||||
/// Deprecated. Returns an OK response without modifying any configuration. To toggle redirect URL tracking,
|
||||
/// set the <c>Umbraco:CMS:WebRouting:DisableRedirectUrlTracking</c> configuration key instead.
|
||||
/// Sets the redirect URL tracking status.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token for the HTTP request.</param>
|
||||
/// <param name="status">The redirect status (ignored).</param>
|
||||
/// <returns>An OK result.</returns>
|
||||
/// <param name="status">The redirect status to set.</param>
|
||||
/// <returns>An OK result if successful.</returns>
|
||||
[HttpPost("status")]
|
||||
[EndpointSummary("Deprecated. No longer changes the redirect URL tracking status.")]
|
||||
[EndpointDescription("This endpoint is deprecated and no longer modifies the configuration. To toggle redirect URL tracking, set the Umbraco:CMS:WebRouting:DisableRedirectUrlTracking configuration key instead.")]
|
||||
[EndpointSummary("Sets the redirect URL tracking status.")]
|
||||
[EndpointDescription("Updates the redirect URL tracking configuration according to the provided status.")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[Obsolete("This endpoint is deprecated and no longer modifies the configuration. Set the Umbraco:CMS:WebRouting:DisableRedirectUrlTracking configuration key instead. Scheduled for removal in Umbraco 19.")]
|
||||
public Task<IActionResult> SetStatus(CancellationToken cancellationToken, [FromQuery] RedirectStatus status)
|
||||
=> Task.FromResult<IActionResult>(Ok());
|
||||
public async Task<IActionResult> SetStatus(CancellationToken cancellationToken, [FromQuery] RedirectStatus status)
|
||||
{
|
||||
// TODO: uncomment this when auth is implemented.
|
||||
// var userIsAdmin = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.IsAdmin();
|
||||
// if (userIsAdmin is null or false)
|
||||
// {
|
||||
// return Unauthorized();
|
||||
// }
|
||||
|
||||
var enable = status switch
|
||||
{
|
||||
RedirectStatus.Enabled => true,
|
||||
RedirectStatus.Disabled => false,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(status), status, "Unknown redirect status")
|
||||
};
|
||||
|
||||
// For now I'm not gonna change this to limit breaking, but it's weird to have a "disabled" switch,
|
||||
// since you're essentially negating the boolean from the get go,
|
||||
// it's much easier to reason with enabled = false == disabled.
|
||||
await _configManipulator.SaveDisableRedirectUrlTrackingAsync(!enable);
|
||||
|
||||
// Taken from the existing implementation in RedirectUrlManagementController
|
||||
// TODO this is ridiculous, but we need to ensure the configuration is reloaded, before this request is ended.
|
||||
// otherwise we can read the old value in GetEnableState.
|
||||
// The value is equal to JsonConfigurationSource.ReloadDelay
|
||||
Thread.Sleep(250);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ public class ConfigurationServerController : ServerControllerBase
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IBackOfficeExternalLoginProviders _externalLoginProviders;
|
||||
private readonly IHostingEnvironment _hostingEnvironment;
|
||||
private readonly SignalRSettings _signalRSettings;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConfigurationServerController"/> class.
|
||||
@@ -37,38 +36,13 @@ public class ConfigurationServerController : ServerControllerBase
|
||||
/// <param name="globalSettings">The global settings options.</param>
|
||||
/// <param name="externalLoginProviders">The external login providers for back office.</param>
|
||||
/// <param name="hostingEnvironment">The hosting environment.</param>
|
||||
/// <param name="signalRSettings">The SignalR settings options.</param>
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public ConfigurationServerController(
|
||||
IOptions<SecuritySettings> securitySettings,
|
||||
IOptions<GlobalSettings> globalSettings,
|
||||
IBackOfficeExternalLoginProviders externalLoginProviders,
|
||||
IHostingEnvironment hostingEnvironment,
|
||||
IOptions<SignalRSettings> signalRSettings)
|
||||
public ConfigurationServerController(IOptions<SecuritySettings> securitySettings, IOptions<GlobalSettings> globalSettings, IBackOfficeExternalLoginProviders externalLoginProviders, IHostingEnvironment hostingEnvironment)
|
||||
{
|
||||
_securitySettings = securitySettings.Value;
|
||||
_globalSettings = globalSettings.Value;
|
||||
_externalLoginProviders = externalLoginProviders;
|
||||
_hostingEnvironment = hostingEnvironment;
|
||||
_signalRSettings = signalRSettings.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Umbraco.Cms.Api.Management.Controllers.Server.ConfigurationServerController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="securitySettings">The <see cref="SecuritySettings"/> options.</param>
|
||||
/// <param name="globalSettings">The <see cref="GlobalSettings"/> options.</param>
|
||||
/// <param name="externalLoginProviders">The external login providers used for back office authentication.</param>
|
||||
/// <param name="hostingEnvironment">The hosting environment.</param>
|
||||
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
|
||||
public ConfigurationServerController(IOptions<SecuritySettings> securitySettings, IOptions<GlobalSettings> globalSettings, IBackOfficeExternalLoginProviders externalLoginProviders, IHostingEnvironment hostingEnvironment)
|
||||
: this(
|
||||
securitySettings,
|
||||
globalSettings,
|
||||
externalLoginProviders,
|
||||
hostingEnvironment,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IOptions<SignalRSettings>>())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -104,10 +78,6 @@ public class ConfigurationServerController : ServerControllerBase
|
||||
VersionCheckPeriod = _globalSettings.VersionCheckPeriod,
|
||||
AllowLocalLogin = _externalLoginProviders.HasDenyLocalLogin() is false,
|
||||
UmbracoCssPath = _hostingEnvironment.ToAbsolute(_globalSettings.UmbracoCssPath),
|
||||
SignalR = new SignalRClientSettingsResponseModel
|
||||
{
|
||||
SkipNegotiation = _signalRSettings.ClientShouldSkipNegotiation,
|
||||
},
|
||||
};
|
||||
|
||||
return Task.FromResult<IActionResult>(Ok(responseModel));
|
||||
|
||||
+3
-14
@@ -32,14 +32,6 @@ public class SearchTemplateItemController : TemplateItemControllerBase
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches for template items matching the specified query, with support for pagination.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
|
||||
/// <param name="query">The search query used to filter template items.</param>
|
||||
/// <param name="skip">The number of items to skip before starting to collect the result set (used for pagination).</param>
|
||||
/// <param name="take">The maximum number of items to return in the result set (used for pagination).</param>
|
||||
/// <returns>A task representing the asynchronous operation. The task result contains an <see cref="IActionResult"/> with a <see cref="PagedModel{TemplateItemResponseModel}"/> containing the search results.</returns>
|
||||
[HttpGet("search")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedModel<TemplateItemResponseModel>), StatusCodes.Status200OK)]
|
||||
@@ -53,14 +45,11 @@ public class SearchTemplateItemController : TemplateItemControllerBase
|
||||
return Ok(new PagedModel<TemplateItemResponseModel> { Total = searchResult.Total });
|
||||
}
|
||||
|
||||
Guid[] keys = searchResult.Items.Select(x => x.Key).ToArray();
|
||||
IEnumerable<ITemplate> templates = await _templateService.GetAllAsync(keys);
|
||||
IEnumerable<ITemplate> orderedTemplates = OrderByRequestedIds(templates, keys);
|
||||
|
||||
IEnumerable<ITemplate> templates = await _templateService.GetAllAsync(searchResult.Items.Select(item => item.Key).ToArray());
|
||||
var result = new PagedModel<TemplateItemResponseModel>
|
||||
{
|
||||
Items = _mapper.MapEnumerable<ITemplate, TemplateItemResponseModel>(orderedTemplates),
|
||||
Total = searchResult.Total,
|
||||
Items = _mapper.MapEnumerable<ITemplate, TemplateItemResponseModel>(templates),
|
||||
Total = searchResult.Total
|
||||
};
|
||||
|
||||
return Ok(result);
|
||||
|
||||
-7
@@ -26,7 +26,6 @@ internal static class BackOfficeAuthPolicyBuilderExtensions
|
||||
builder.Services.AddSingleton<IAuthorizationHandler, DenyLocalLoginHandler>();
|
||||
builder.Services.AddSingleton<IAuthorizationHandler, DictionaryPermissionHandler>();
|
||||
builder.Services.AddSingleton<IAuthorizationHandler, ElementPermissionHandler>();
|
||||
builder.Services.AddSingleton<IAuthorizationHandler, ElementContainerPermissionHandler>();
|
||||
builder.Services.AddSingleton<IAuthorizationHandler, FeatureAuthorizeHandler>();
|
||||
builder.Services.AddSingleton<IAuthorizationHandler, MediaPermissionHandler>();
|
||||
builder.Services.AddSingleton<IAuthorizationHandler, UserGroupPermissionHandler>();
|
||||
@@ -151,12 +150,6 @@ internal static class BackOfficeAuthPolicyBuilderExtensions
|
||||
policy.Requirements.Add(new ElementPermissionRequirement());
|
||||
});
|
||||
|
||||
options.AddPolicy(AuthorizationPolicies.ElementFolderPermissionByResource, policy =>
|
||||
{
|
||||
policy.AuthenticationSchemes.Add(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme);
|
||||
policy.Requirements.Add(new ElementContainerPermissionRequirement());
|
||||
});
|
||||
|
||||
options.AddPolicy(AuthorizationPolicies.MediaPermissionByResource, policy =>
|
||||
{
|
||||
policy.AuthenticationSchemes.Add(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme);
|
||||
|
||||
@@ -5,7 +5,6 @@ using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Hosting;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Web.Common.Hosting;
|
||||
using Umbraco.Cms.Web.Common.Middleware;
|
||||
|
||||
namespace Umbraco.Extensions;
|
||||
|
||||
@@ -69,10 +68,6 @@ public static partial class UmbracoBuilderExtensions
|
||||
builder.Services.AddSingleton<IBackOfficeEnabledMarker, BackOfficeEnabledMarker>();
|
||||
|
||||
builder.Services.AddUnique<IBackOfficePathGenerator, UmbracoBackOfficePathGenerator>();
|
||||
// Registered here rather than in AddWebComponents because the middleware depends on
|
||||
// IBackOfficePathGenerator (registered just above). DI scope validation would otherwise
|
||||
// fail in Delivery-only/Website-only bootstraps that never call AddBackOffice().
|
||||
builder.Services.AddSingleton<UmbracoBackOfficeCacheHeadersMiddleware>();
|
||||
builder.Services.AddUnique<IPhysicalFileSystem>(factory =>
|
||||
{
|
||||
var path = "~/";
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Common.Configuration;
|
||||
using Umbraco.Cms.Api.Common.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Common.OpenApi;
|
||||
using Umbraco.Cms.Api.Management.Configuration;
|
||||
using Umbraco.Cms.Api.Management.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Management.Middleware;
|
||||
using Umbraco.Cms.Api.Management.OpenApi;
|
||||
using Umbraco.Cms.Api.Management.OpenApi.Transformers;
|
||||
using Umbraco.Cms.Api.Management.Routing;
|
||||
using Umbraco.Cms.Api.Management.Serialization;
|
||||
using Umbraco.Cms.Api.Management.Services;
|
||||
@@ -102,25 +100,10 @@ public static partial class UmbracoBuilderExtensions
|
||||
// Configures the JSON options for the Open API schema generation (based on the back-office MVC JSON options)
|
||||
builder.Services.ConfigureOptions<ConfigureUmbracoBackofficeHttpJsonOptions>();
|
||||
|
||||
builder.AddBackOfficeOpenApiDocument(
|
||||
builder.AddUmbracoOpenApiDocument<ConfigureUmbracoManagementApiOpenApiOptions>(
|
||||
ManagementApiConfiguration.ApiName,
|
||||
document => document
|
||||
.WithTitle(ManagementApiConfiguration.ApiTitle)
|
||||
.WithBackOfficeAuthentication()
|
||||
.WithJsonOptions(Constants.JsonOptionsNames.BackOffice)
|
||||
.ConfigureOpenApiOptions(options =>
|
||||
{
|
||||
options.AddDocumentTransformer((doc, _, _) =>
|
||||
{
|
||||
doc.Info.Version = "Latest";
|
||||
doc.Info.Description = "This shows all APIs available in this version of Umbraco - including all the legacy apis that are available for backward compatibility";
|
||||
doc.Servers?.Clear();
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
options.AddSchemaTransformer<FixFileReturnTypesTransformer>();
|
||||
options.AddOperationTransformer<ResponseHeaderTransformer>();
|
||||
options.AddOperationTransformer<NotificationHeaderTransformer>();
|
||||
}));
|
||||
ManagementApiConfiguration.ApiTitle,
|
||||
Constants.JsonOptionsNames.BackOffice);
|
||||
|
||||
services.Configure<UmbracoPipelineOptions>(options =>
|
||||
{
|
||||
|
||||
+305
-721
File diff suppressed because it is too large
Load Diff
@@ -1,18 +0,0 @@
|
||||
using Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Management-specific extension methods for <see cref="BackOfficeOpenApiDocumentBuilder"/>.
|
||||
/// </summary>
|
||||
public static class BackOfficeOpenApiDocumentBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds backoffice authentication requirements to the document.
|
||||
/// </summary>
|
||||
/// <param name="documentBuilder">The document builder.</param>
|
||||
/// <returns>The same builder for chaining.</returns>
|
||||
public static BackOfficeOpenApiDocumentBuilder WithBackOfficeAuthentication(
|
||||
this BackOfficeOpenApiDocumentBuilder documentBuilder)
|
||||
=> documentBuilder.ConfigureOpenApiOptions(options => options.AddBackofficeSecurityRequirements());
|
||||
}
|
||||
+2
-11
@@ -26,20 +26,11 @@ internal class BackOfficeSecurityRequirementsTransformer : IOpenApiOperationTran
|
||||
OpenApiOperationTransformerContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (context.Description.ActionDescriptor is not ControllerActionDescriptor description)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (description.MethodInfo.GetCustomAttributes(true).Any(x => x is AllowAnonymousAttribute) ||
|
||||
if (context.Description.ActionDescriptor is not ControllerActionDescriptor description ||
|
||||
description.MethodInfo.GetCustomAttributes(true).Any(x => x is AllowAnonymousAttribute) ||
|
||||
description.MethodInfo.DeclaringType?.GetCustomAttributes(true).Any(x => x is AllowAnonymousAttribute) ==
|
||||
true)
|
||||
{
|
||||
// Explicitly clear security on anonymous operations so they override the document-level
|
||||
// security requirement added below. Without this, OpenAPI consumers (including the
|
||||
// generated backoffice SDK) treat these endpoints as authenticated and attach a Bearer
|
||||
// token, which triggers a /token refresh before the user has logged in.
|
||||
operation.Security = [];
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Api.Management.Controllers.Security;
|
||||
using Umbraco.Cms.Api.Management.ServerEvents;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Web.Common.Routing;
|
||||
using Umbraco.Extensions;
|
||||
@@ -16,36 +12,26 @@ namespace Umbraco.Cms.Api.Management.Routing;
|
||||
/// <summary>
|
||||
/// Creates routes for the back office area.
|
||||
/// </summary>
|
||||
public sealed class BackOfficeAreaRoutes : SignalRRoutesBase, IAreaRoutes
|
||||
public sealed class BackOfficeAreaRoutes : IAreaRoutes
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BackOfficeAreaRoutes" /> class.
|
||||
/// </summary>
|
||||
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
|
||||
public BackOfficeAreaRoutes(IRuntimeState runtimeState)
|
||||
: this(
|
||||
runtimeState,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IOptions<SignalRSettings>>())
|
||||
{
|
||||
}
|
||||
private readonly IRuntimeState _runtimeState;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BackOfficeAreaRoutes" /> class.
|
||||
/// </summary>
|
||||
public BackOfficeAreaRoutes(IRuntimeState runtimeState, IOptions<SignalRSettings> signalRSettings)
|
||||
: base(runtimeState, signalRSettings)
|
||||
{
|
||||
}
|
||||
public BackOfficeAreaRoutes(IRuntimeState runtimeState)
|
||||
=> _runtimeState = runtimeState;
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public void CreateRoutes(IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
if (RuntimeState.Level is RuntimeLevel.Install or RuntimeLevel.Upgrade or RuntimeLevel.Upgrading or RuntimeLevel.Run)
|
||||
if (_runtimeState.Level is RuntimeLevel.Install or RuntimeLevel.Upgrade or RuntimeLevel.Upgrading or RuntimeLevel.Run)
|
||||
{
|
||||
MapMinimalBackOffice(endpoints);
|
||||
|
||||
endpoints.MapHub<BackofficeHub>(Constants.System.UmbracoPathSegment + Constants.Web.BackofficeSignalRHub, ConfigureHubEndpoint);
|
||||
endpoints.MapHub<ServerEventHub>(Constants.System.UmbracoPathSegment + Constants.Web.ServerEventSignalRHub, ConfigureHubEndpoint);
|
||||
endpoints.MapHub<BackofficeHub>(Constants.System.UmbracoPathSegment + Constants.Web.BackofficeSignalRHub);
|
||||
endpoints.MapHub<ServerEventHub>(Constants.System.UmbracoPathSegment + Constants.Web.ServerEventSignalRHub);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Api.Management.Preview;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Web.Common.Routing;
|
||||
|
||||
@@ -14,29 +10,16 @@ namespace Umbraco.Cms.Api.Management.Routing;
|
||||
/// <summary>
|
||||
/// Creates routes for the preview hub
|
||||
/// </summary>
|
||||
public sealed class PreviewRoutes : SignalRRoutesBase, IAreaRoutes
|
||||
public sealed class PreviewRoutes : IAreaRoutes
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PreviewRoutes"/> class, configuring preview routing based on the application's runtime state.
|
||||
/// </summary>
|
||||
/// <param name="runtimeState">An instance representing the current runtime state of the Umbraco application.</param>
|
||||
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
|
||||
public PreviewRoutes(IRuntimeState runtimeState)
|
||||
: this(
|
||||
runtimeState,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IOptions<SignalRSettings>>())
|
||||
{
|
||||
}
|
||||
private readonly IRuntimeState _runtimeState;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PreviewRoutes"/> class, configuring preview routing based on the application's runtime state.
|
||||
/// Initializes a new instance of the <see cref="Umbraco.Cms.Api.Management.Routing.PreviewRoutes"/> class, configuring preview routing based on the application's runtime state.
|
||||
/// </summary>
|
||||
/// <param name="runtimeState">An instance representing the current runtime state of the Umbraco application.</param>
|
||||
/// <param name="signalRSettings">The SignalR settings options.</param>
|
||||
public PreviewRoutes(IRuntimeState runtimeState, IOptions<SignalRSettings> signalRSettings)
|
||||
: base(runtimeState, signalRSettings)
|
||||
{
|
||||
}
|
||||
public PreviewRoutes(IRuntimeState runtimeState)
|
||||
=> _runtimeState = runtimeState;
|
||||
|
||||
/// <summary>
|
||||
/// Creates the preview routes on the specified endpoint route builder.
|
||||
@@ -44,9 +27,9 @@ public sealed class PreviewRoutes : SignalRRoutesBase, IAreaRoutes
|
||||
/// <param name="endpoints">The endpoint route builder to add routes to.</param>
|
||||
public void CreateRoutes(IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
if (RuntimeState.Level is RuntimeLevel.Install or RuntimeLevel.Upgrade or RuntimeLevel.Upgrading or RuntimeLevel.Run)
|
||||
if (_runtimeState.Level is RuntimeLevel.Install or RuntimeLevel.Upgrade or RuntimeLevel.Upgrading or RuntimeLevel.Run)
|
||||
{
|
||||
endpoints.MapHub<PreviewHub>(GetPreviewHubRoute(), ConfigureHubEndpoint);
|
||||
endpoints.MapHub<PreviewHub>(GetPreviewHubRoute());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,4 +41,3 @@ public sealed class PreviewRoutes : SignalRRoutesBase, IAreaRoutes
|
||||
/// </returns>
|
||||
public string GetPreviewHubRoute() => $"/{Constants.System.UmbracoPathSegment}/{nameof(PreviewHub)}";
|
||||
}
|
||||
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
using Microsoft.AspNetCore.Http.Connections;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Routing;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for route definitions that map SignalR hub endpoints,
|
||||
/// applying shared transport configuration from <see cref="SignalRSettings"/>.
|
||||
/// </summary>
|
||||
public abstract class SignalRRoutesBase
|
||||
{
|
||||
private readonly SignalRSettings _signalRSettings;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SignalRRoutesBase"/> class.
|
||||
/// </summary>
|
||||
/// <param name="runtimeState">The current runtime state of the Umbraco application.</param>
|
||||
/// <param name="signalRSettings">The SignalR settings options.</param>
|
||||
protected SignalRRoutesBase(IRuntimeState runtimeState, IOptions<SignalRSettings> signalRSettings)
|
||||
{
|
||||
RuntimeState = runtimeState;
|
||||
_signalRSettings = signalRSettings.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current runtime state of the Umbraco application.
|
||||
/// </summary>
|
||||
protected IRuntimeState RuntimeState { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Configures the transport options for a SignalR hub endpoint.
|
||||
/// When <see cref="SignalRSettings.ClientShouldSkipNegotiation"/> is enabled,
|
||||
/// restricts the endpoint to WebSocket transport only so clients can skip the negotiate round-trip.
|
||||
/// </summary>
|
||||
/// <param name="options">The hub endpoint dispatcher options to configure.</param>
|
||||
protected void ConfigureHubEndpoint(HttpConnectionDispatcherOptions options)
|
||||
{
|
||||
if (_signalRSettings.ClientShouldSkipNegotiation)
|
||||
{
|
||||
options.Transports = HttpTransportType.WebSockets;
|
||||
}
|
||||
}
|
||||
}
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Security.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Security.Authorization.Element;
|
||||
|
||||
/// <summary>
|
||||
/// Authorizes that the current user has the correct permission access to the element container item(s) specified in the request.
|
||||
/// </summary>
|
||||
public class ElementContainerPermissionHandler : MustSatisfyRequirementAuthorizationHandler<ElementContainerPermissionRequirement, ElementContainerPermissionResource>
|
||||
{
|
||||
private readonly IElementContainerPermissionAuthorizer _elementContainerPermissionAuthorizer;
|
||||
private readonly IAuthorizationHelper _authorizationHelper;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ElementContainerPermissionHandler" /> class.
|
||||
/// </summary>
|
||||
/// <param name="elementContainerPermissionAuthorizer">Authorizer for element container access.</param>
|
||||
/// <param name="authorizationHelper">The authorization helper.</param>
|
||||
public ElementContainerPermissionHandler(IElementContainerPermissionAuthorizer elementContainerPermissionAuthorizer, IAuthorizationHelper authorizationHelper)
|
||||
{
|
||||
_elementContainerPermissionAuthorizer = elementContainerPermissionAuthorizer;
|
||||
_authorizationHelper = authorizationHelper;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task<bool> IsAuthorized(
|
||||
AuthorizationHandlerContext context,
|
||||
ElementContainerPermissionRequirement requirement,
|
||||
ElementContainerPermissionResource resource)
|
||||
{
|
||||
var result = true;
|
||||
|
||||
IUser user = _authorizationHelper.GetUmbracoUser(context.User);
|
||||
if (resource.CheckRoot)
|
||||
{
|
||||
result &= await _elementContainerPermissionAuthorizer.IsDeniedAtRootLevelAsync(user, resource.PermissionsToCheck) is false;
|
||||
}
|
||||
|
||||
if (resource.CheckRecycleBin)
|
||||
{
|
||||
result &= await _elementContainerPermissionAuthorizer.IsDeniedAtRecycleBinLevelAsync(user, resource.PermissionsToCheck) is false;
|
||||
}
|
||||
|
||||
if (resource.ContainerKeys.Any())
|
||||
{
|
||||
result &= await _elementContainerPermissionAuthorizer.IsDeniedAsync(user, resource.ContainerKeys, resource.PermissionsToCheck) is false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Security.Authorization.Element;
|
||||
|
||||
/// <summary>
|
||||
/// Authorization requirement for the <see cref="ElementContainerPermissionHandler" />.
|
||||
/// </summary>
|
||||
public class ElementContainerPermissionRequirement : IAuthorizationRequirement
|
||||
{
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+1
-2
@@ -1,6 +1,5 @@
|
||||
using Umbraco.Cms.Core.Actions;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Entities;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
@@ -26,7 +25,7 @@ internal sealed class DocumentPermissionFilterService : PermissionFilterServiceB
|
||||
=> _contentPermissionService = contentPermissionService;
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override string BrowseActionLetter(IEntitySlim entity) => ActionBrowse.ActionLetter;
|
||||
protected override string BrowseActionLetter => ActionBrowse.ActionLetter;
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Task<IEnumerable<NodePermissions>> GetPermissionsAsync(IUser user, IEnumerable<Guid> entityKeys)
|
||||
|
||||
+1
-6
@@ -1,7 +1,5 @@
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Actions;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Entities;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
@@ -27,10 +25,7 @@ internal sealed class ElementPermissionFilterService : PermissionFilterServiceBa
|
||||
=> _elementPermissionService = elementPermissionService;
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override string BrowseActionLetter(IEntitySlim entity)
|
||||
=> entity.NodeObjectType == Constants.ObjectTypes.Element
|
||||
? ActionElementBrowse.ActionLetter
|
||||
: ActionElementContainerBrowse.ActionLetter;
|
||||
protected override string BrowseActionLetter => ActionElementBrowse.ActionLetter;
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Task<IEnumerable<NodePermissions>> GetPermissionsAsync(IUser user, IEnumerable<Guid> entityKeys)
|
||||
|
||||
+2
-2
@@ -22,7 +22,7 @@ internal abstract class PermissionFilterServiceBase
|
||||
/// <summary>
|
||||
/// Gets the browse action letter used to check permissions.
|
||||
/// </summary>
|
||||
protected abstract string BrowseActionLetter(IEntitySlim entity);
|
||||
protected abstract string BrowseActionLetter { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Filters entities based on the current user's browse permissions.
|
||||
@@ -106,5 +106,5 @@ internal abstract class PermissionFilterServiceBase
|
||||
|
||||
private bool HasBrowsePermission(IEntitySlim entity, Dictionary<Guid, NodePermissions> permissionsByNodeKey)
|
||||
=> permissionsByNodeKey.TryGetValue(entity.Key, out NodePermissions? nodePermissions)
|
||||
&& nodePermissions.Permissions.Contains(BrowseActionLetter(entity));
|
||||
&& nodePermissions.Permissions.Contains(BrowseActionLetter);
|
||||
}
|
||||
|
||||
@@ -21,9 +21,4 @@ public class ServerConfigurationResponseModel
|
||||
/// Gets or sets the relative or absolute path to the Umbraco CSS file used by the application.
|
||||
/// </summary>
|
||||
public string UmbracoCssPath { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the client-side SignalR settings.
|
||||
/// </summary>
|
||||
public SignalRClientSettingsResponseModel SignalR { get; set; } = new();
|
||||
}
|
||||
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
namespace Umbraco.Cms.Api.Management.ViewModels.Server;
|
||||
|
||||
/// <summary>
|
||||
/// Represents client-side SignalR settings returned by the server configuration endpoint.
|
||||
/// </summary>
|
||||
public class SignalRClientSettingsResponseModel
|
||||
{
|
||||
/// <summary>Gets or sets a value indicating whether the client should skip the SignalR negotiate round-trip.</summary>
|
||||
public bool SkipNegotiation { get; set; }
|
||||
}
|
||||
@@ -119,7 +119,8 @@ BackofficeProjectDirectory = ../Umbraco.Web.UI.Client/
|
||||
BackofficeAssetsPath = wwwroot/umbraco/backoffice
|
||||
```
|
||||
|
||||
**Login Build** (lines 102-148):
|
||||
**Login Build** (lines 94-148):
|
||||
|
||||
```
|
||||
LoginProjectDirectory = ../Umbraco.Web.UI.Login/
|
||||
LoginAssetsPath = wwwroot/umbraco/login
|
||||
@@ -127,18 +128,13 @@ LoginAssetsPath = wwwroot/umbraco/login
|
||||
|
||||
### Build Targets
|
||||
|
||||
| Target | Purpose |
|
||||
|--------|---------|
|
||||
| `BuildBackofficeStaticAssetsPreconditions` | Checks if Backoffice build needed (Visual Studio only) |
|
||||
| `RestoreBackoffice` | Runs `npm i` if package-lock changed |
|
||||
| `BuildBackoffice` | Runs `npm run build:for:cms` |
|
||||
| `DefineBackofficeAssets` | Registers Backoffice assets with StaticWebAssets system |
|
||||
| `CleanBackoffice` | Removes built Backoffice assets on `dotnet clean` |
|
||||
| `BuildLoginStaticAssetsPreconditions` | Checks if Login build needed (Visual Studio only) |
|
||||
| `RestoreLogin` | Runs `npm i` if Login's package-lock changed |
|
||||
| `BuildLogin` | Runs `npm run build` in Login. Depends on `RestoreBackoffice` because Login's `tsc` walks Client's `src/` via tsconfig path aliases and needs Client's `node_modules` populated for transitive `lit`/`rxjs`/UUI resolution |
|
||||
| `DefineLoginAssets` | Registers Login assets with StaticWebAssets system |
|
||||
| `CleanLogin` | Removes built Login assets on `dotnet clean` |
|
||||
| Target | Purpose |
|
||||
| -------------------------------- | -------------------------------------------- |
|
||||
| `BuildStaticAssetsPreconditions` | Checks if build needed (Visual Studio only) |
|
||||
| `RestoreBackoffice` | Runs `npm i` if package-lock changed |
|
||||
| `BuildBackoffice` | Runs `npm run build:for:cms` |
|
||||
| `DefineBackofficeAssets` | Registers assets with StaticWebAssets system |
|
||||
| `CleanBackoffice` | Removes built assets on `dotnet clean` |
|
||||
|
||||
### Build Conditions
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
<Exec Command="npm i --no-fund --no-audit" WorkingDirectory="$(LoginProjectDirectory)" />
|
||||
</Target>
|
||||
|
||||
<Target Name="BuildLogin" DependsOnTargets="RestoreLogin;RestoreBackoffice">
|
||||
<Target Name="BuildLogin" DependsOnTargets="RestoreLogin">
|
||||
<Message Importance="high" Text="Executing Login NPM build script..." />
|
||||
<Exec Command="npm run build" WorkingDirectory="$(LoginProjectDirectory)" />
|
||||
<ItemGroup>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
var backOfficeAssetsPath = BackOfficePathGenerator.BackOfficeAssetsPath;
|
||||
var loginLogoImageAlternative = Url.RouteUrl(BackOfficeGraphicsController.LoginLogoAlternativeRouteName, new {Version= "1"});
|
||||
}<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="@GlobalSettings.Value.DefaultUILanguage">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
@@ -61,7 +61,7 @@
|
||||
<p>Here are the <a href="https://www.enable-javascript.com/" target="_blank" rel="noopener" style="text-decoration: underline;">instructions how to enable JavaScript in your web browser</a>.</p>
|
||||
</div>
|
||||
</noscript>
|
||||
<umb-app lang="@GlobalSettings.Value.DefaultUILanguage" @(SecuritySettings.Value.KeepUserLoggedIn ? "keep-user-logged-in" : "")></umb-app>
|
||||
<umb-app @(SecuritySettings.Value.KeepUserLoggedIn ? "keep-user-logged-in" : "")></umb-app>
|
||||
|
||||
@if (isDebug)
|
||||
{
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="@GlobalSettings.Value.DefaultUILanguage">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<base href="@backOfficePath.EnsureEndsWith('/')" />
|
||||
@@ -83,7 +83,6 @@
|
||||
</noscript>
|
||||
|
||||
<umb-auth
|
||||
lang="@GlobalSettings.Value.DefaultUILanguage"
|
||||
return-url="@backOfficePath"
|
||||
logo-image="@loginLogoImage"
|
||||
logo-image-alternative="@loginLogoImageAlternative"
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 176 KiB After Width: | Height: | Size: 216 KiB |
@@ -16,16 +16,21 @@
|
||||
</ItemGroup>
|
||||
|
||||
<!--
|
||||
The Razor editor in modern Visual Studio and the C# extension for VS Code use the Razor source generator
|
||||
The Razor editor in VS2026 and the C# extension for VS Code uses the Razor source generator
|
||||
for IDE functionality. We need to add some things to make sure it works correctly, but we
|
||||
only do them for design time builds, so that we don't impact regular builds or CI.
|
||||
We also have an escape hatch in case it does cause issues, users can set EnableCohostEditorCompatibility=false
|
||||
We also have an escape hatch in case it does cause issues, users can set the appropriate property
|
||||
in their project file to disable this.
|
||||
|
||||
CompilerVisibleProperty is surfaced to generators via AnalyzerConfigOptionsProvider, not as a source-generator input file,
|
||||
so it doesn't enter the hintName-collision codepath that AdditionalFiles does. Keeping it at evaluation time is safe.
|
||||
-->
|
||||
<ItemGroup Condition="'$(DesignTimeBuild)' == 'true' and '$(EnableCohostEditorCompatibility)' != 'false'">
|
||||
<!--
|
||||
We have to make sure the source generator can see the .cshtml files, so make them AdditionalFiles.
|
||||
-->
|
||||
<AdditionalFiles Include="**\*.cshtml" />
|
||||
|
||||
<!--
|
||||
Make sure the source generator knows where the project is, so it can compute target paths.
|
||||
-->
|
||||
<CompilerVisibleProperty Include="MSBuildProjectDirectory" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -49,39 +49,4 @@
|
||||
<ContentWithTargetPath Include="@(_UmbracoFolderFiles)" Exclude="@(ContentWithTargetPath)" TargetPath="%(Identity)" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
<!--
|
||||
The Razor source generator needs .cshtml files in @(AdditionalFiles). The Razor SDK adds them
|
||||
via @(RazorGenerate), but only inside a target that runs during the build — so during cohost
|
||||
design-time builds they may not be present yet, which is what PR #21861 worked around.
|
||||
|
||||
Doing the include at evaluation time (as PR #21861 did) causes duplicates with the SDK during
|
||||
dotnet watch / hot reload design-time builds: the SDK adds the same .cshtml under a different
|
||||
item Identity (slash form / relative vs absolute) and the generator then sees two inputs that
|
||||
derive the same hintName, which crashes it with CS8785 (see issue #22773).
|
||||
|
||||
Run as a target before CoreCompile (hot-reload path) and CompileDesignTime (IDE design-time path)
|
||||
so the SDK's contribution is visible in both cases. Then add only the .cshtml files that are not already
|
||||
present. Both sides are normalized to %(FullPath) so items with different Identity forms still compare equal.
|
||||
|
||||
Set EnableCohostEditorCompatibility=false in a project to opt out entirely.
|
||||
-->
|
||||
<Target Name="_UmbracoEnsureRazorAdditionalFilesForCohostEditor"
|
||||
BeforeTargets="CoreCompile;CompileDesignTime"
|
||||
Condition="'$(DesignTimeBuild)' == 'true' and '$(EnableCohostEditorCompatibility)' != 'false'">
|
||||
<ItemGroup>
|
||||
<_UmbracoCshtmlCandidate Include="**\*.cshtml" />
|
||||
<_UmbracoCshtmlCandidateFull Include="@(_UmbracoCshtmlCandidate->'%(FullPath)')" />
|
||||
|
||||
<_UmbracoExistingAdditionalCshtmlFull
|
||||
Include="@(AdditionalFiles->'%(FullPath)')"
|
||||
Condition="'%(Extension)' == '.cshtml'" />
|
||||
|
||||
<_UmbracoCshtmlMissingFromAdditional
|
||||
Include="@(_UmbracoCshtmlCandidateFull)"
|
||||
Exclude="@(_UmbracoExistingAdditionalCshtmlFull)" />
|
||||
|
||||
<AdditionalFiles Include="@(_UmbracoCshtmlMissingFromAdditional)" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
</Project>
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
namespace Umbraco.Cms.Core.Actions;
|
||||
|
||||
/// <summary>
|
||||
/// This action is used as a security constraint that grants a user the ability to view element containers in a tree
|
||||
/// that has permissions applied to it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This action should not be invoked. It is used as the minimum required permission to view element containers in the element tree.
|
||||
/// By granting a user this permission, the user is able to see the element container in the tree but not edit it.
|
||||
/// </remarks>
|
||||
public class ActionElementContainerBrowse : IAction
|
||||
{
|
||||
/// <inheritdoc cref="IAction.ActionLetter" />
|
||||
public const string ActionLetter = "Umb.ElementContainer.Read";
|
||||
|
||||
/// <inheritdoc cref="IAction.ActionAlias" />
|
||||
public const string ActionAlias = "elementcontainerbrowse";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Letter => ActionLetter;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Alias => ActionAlias;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ShowInNotifier => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanBePermissionAssigned => true;
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
namespace Umbraco.Cms.Core.Actions;
|
||||
|
||||
/// <summary>
|
||||
/// This action is used as a security constraint that grants a user the ability to delete element containers.
|
||||
/// </summary>
|
||||
public class ActionElementContainerDelete : IAction
|
||||
{
|
||||
/// <inheritdoc cref="IAction.ActionLetter" />
|
||||
public const string ActionLetter = "Umb.ElementContainer.Delete";
|
||||
|
||||
/// <inheritdoc cref="IAction.ActionAlias" />
|
||||
public const string ActionAlias = "elementcontainerdelete";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Letter => ActionLetter;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Alias => ActionAlias;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ShowInNotifier => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanBePermissionAssigned => true;
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
namespace Umbraco.Cms.Core.Actions;
|
||||
|
||||
/// <summary>
|
||||
/// This action is used as a security constraint that grants a user the ability to move element containers.
|
||||
/// </summary>
|
||||
public class ActionElementContainerMove : IAction
|
||||
{
|
||||
/// <inheritdoc cref="IAction.ActionLetter" />
|
||||
public const string ActionLetter = "Umb.ElementContainer.Move";
|
||||
|
||||
/// <inheritdoc cref="IAction.ActionAlias" />
|
||||
public const string ActionAlias = "elementcontainermove";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Letter => ActionLetter;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Alias => ActionAlias;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ShowInNotifier => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanBePermissionAssigned => true;
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
namespace Umbraco.Cms.Core.Actions;
|
||||
|
||||
/// <summary>
|
||||
/// This action is used as a security constraint that grants a user the ability to create new element containers.
|
||||
/// </summary>
|
||||
public class ActionElementContainerNew : IAction
|
||||
{
|
||||
/// <inheritdoc cref="IAction.ActionLetter" />
|
||||
public const string ActionLetter = "Umb.ElementContainer.Create";
|
||||
|
||||
/// <inheritdoc cref="IAction.ActionAlias" />
|
||||
public const string ActionAlias = "elementcontainercreate";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Letter => ActionLetter;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Alias => ActionAlias;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ShowInNotifier => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanBePermissionAssigned => true;
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
namespace Umbraco.Cms.Core.Actions;
|
||||
|
||||
/// <summary>
|
||||
/// This action is used as a security constraint that grants a user the ability to update element containers.
|
||||
/// </summary>
|
||||
public class ActionElementContainerUpdate : IAction
|
||||
{
|
||||
/// <inheritdoc cref="IAction.ActionLetter" />
|
||||
public const string ActionLetter = "Umb.ElementContainer.Update";
|
||||
|
||||
/// <inheritdoc cref="IAction.ActionAlias" />
|
||||
public const string ActionAlias = "elementcontainerupdate";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Letter => ActionLetter;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Alias => ActionAlias;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ShowInNotifier => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanBePermissionAssigned => true;
|
||||
}
|
||||
@@ -306,8 +306,6 @@ public class MyEntityCacheRefresher : CacheRefresherBase<MyEntityCacheRefresher>
|
||||
- `Attempt.Succeed(value)` / `Attempt.Fail<T>()`
|
||||
- `Attempt<Content, ContentEditingOperationStatus>` - typed result with status
|
||||
|
||||
> Writing or reviewing a query with a `WHERE IN` on a runtime-sized collection? See "Avoiding the SQL Server 2100-parameter limit" in `/src/Umbraco.Infrastructure/CLAUDE.md` — that's where the full helper list (`Constants.Sql.MaxParameterCount`, `InGroupsOf`, NPoco's `FetchByGroups`) and the decision rules live.
|
||||
|
||||
### Configuration
|
||||
|
||||
Configuration models in `/Configuration/Models`:
|
||||
|
||||
@@ -26,21 +26,9 @@ public interface IRepositoryCacheVersionAccessor
|
||||
/// Notifies of a version change on a given cache key.
|
||||
/// </summary>
|
||||
/// <param name="cacheKey">Key of the changed version.</param>
|
||||
[Obsolete("Use version that takes newVersion, scheduled for removal in V19")]
|
||||
void VersionChanged(string cacheKey)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Notifies of a version change on a given cache key, providing the new version so internal caches
|
||||
/// can be updated in-place without a database round-trip.
|
||||
/// </summary>
|
||||
/// <param name="cacheKey">Key of the changed version.</param>
|
||||
/// <param name="newVersion">The new version GUID that was just written to the database.</param>
|
||||
void VersionChanged(string cacheKey, Guid newVersion)
|
||||
{
|
||||
VersionChanged(cacheKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Notifies the accessor that caches have been synchronized.
|
||||
/// </summary>
|
||||
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
|
||||
namespace Umbraco.Cms.Core.Cache;
|
||||
|
||||
/// <summary>
|
||||
/// Defines an asynchronous handler for a <typeparamref name="TNotification" /> that should be invoked when notifications are dispatched in a distributed cache scope (e.g. to trigger a distributed cache refresher).
|
||||
/// </summary>
|
||||
/// <typeparam name="TNotification">The type of the notification.</typeparam>
|
||||
public interface IDistributedCacheAsyncNotificationHandler<in TNotification> : INotificationAsyncHandler<TNotification>, IDistributedCacheNotificationHandler
|
||||
where TNotification : INotification
|
||||
{ }
|
||||
@@ -428,15 +428,15 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
|
||||
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshNode))
|
||||
{
|
||||
Guid key = payload.Key ?? _idKeyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).Result;
|
||||
_documentUrlService.UpdateUrlSegmentCacheAsync(key).GetAwaiter().GetResult();
|
||||
_documentUrlAliasService.UpdateAliasCacheAsync(key).GetAwaiter().GetResult();
|
||||
_documentUrlService.CreateOrUpdateUrlSegmentsAsync(key).GetAwaiter().GetResult();
|
||||
_documentUrlAliasService.CreateOrUpdateAliasesAsync(key).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch))
|
||||
{
|
||||
Guid key = payload.Key ?? _idKeyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).Result;
|
||||
_documentUrlService.UpdateUrlSegmentCacheWithDescendantsAsync(key).GetAwaiter().GetResult();
|
||||
_documentUrlAliasService.UpdateAliasCacheWithDescendantsAsync(key).GetAwaiter().GetResult();
|
||||
_documentUrlService.CreateOrUpdateUrlSegmentsWithDescendantsAsync(key).GetAwaiter().GetResult();
|
||||
_documentUrlAliasService.CreateOrUpdateAliasesWithDescendantsAsync(key).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -131,8 +131,8 @@ public sealed class ElementCacheRefresher : PayloadCacheRefresherBase<ElementCac
|
||||
// By INT Id
|
||||
isolatedCache.Clear(RepositoryCacheKeys.GetKey<IElement, int>(payload.Id));
|
||||
|
||||
// By GUID Key (GUID-keyed read repository uses a separate "uRepoGuid_" prefix)
|
||||
isolatedCache.Clear(RepositoryCacheKeys.GetGuidKey<IElement>(payload.Key));
|
||||
// By GUID Key
|
||||
isolatedCache.Clear(RepositoryCacheKeys.GetKey<IElement, Guid?>(payload.Key));
|
||||
|
||||
HandleMemoryCache(payload);
|
||||
HandlePublishStatusAsync(payload, CancellationToken.None).GetAwaiter().GetResult();
|
||||
|
||||
@@ -78,10 +78,8 @@ public sealed class UserCacheRefresher : PayloadCacheRefresherBase<UserCacheRefr
|
||||
userCache.Result?.Clear(RepositoryCacheKeys.GetKey<IUser, int>(p.Id));
|
||||
userCache.Result?.ClearByKey(CacheKeys.UserContentStartNodePathsPrefix + p.Key);
|
||||
userCache.Result?.ClearByKey(CacheKeys.UserMediaStartNodePathsPrefix + p.Key);
|
||||
userCache.Result?.ClearByKey(CacheKeys.UserElementStartNodePathsPrefix + p.Key);
|
||||
userCache.Result?.ClearByKey(CacheKeys.UserAllContentStartNodesPrefix + p.Key);
|
||||
userCache.Result?.ClearByKey(CacheKeys.UserAllMediaStartNodesPrefix + p.Key);
|
||||
userCache.Result?.ClearByKey(CacheKeys.UserAllElementStartNodesPrefix + p.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.Collections;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
@@ -15,7 +14,6 @@ internal class RepositoryCacheVersionService : IRepositoryCacheVersionService
|
||||
private readonly ILogger<RepositoryCacheVersionService> _logger;
|
||||
private readonly IRepositoryCacheVersionAccessor _repositoryCacheVersionAccessor;
|
||||
private readonly ConcurrentDictionary<string, Guid> _cacheVersions = new();
|
||||
private readonly ConcurrentDictionary<Guid, ConcurrentHashSet<string>> _writtenKeysByScope = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RepositoryCacheVersionService" /> class.
|
||||
@@ -46,6 +44,7 @@ internal class RepositoryCacheVersionService : IRepositoryCacheVersionService
|
||||
|
||||
var cacheKey = GetCacheKey<TEntity>();
|
||||
|
||||
// The cache version accessor will take a read lock if the version is not in request cache, so we don't need to take one here.
|
||||
RepositoryCacheVersion? databaseVersion = await _repositoryCacheVersionAccessor.GetAsync(cacheKey);
|
||||
|
||||
if (databaseVersion?.Version is null)
|
||||
@@ -85,23 +84,18 @@ internal class RepositoryCacheVersionService : IRepositoryCacheVersionService
|
||||
public async Task SetCacheUpdatedAsync<TEntity>()
|
||||
where TEntity : class
|
||||
{
|
||||
string cacheKey = GetCacheKey<TEntity>();
|
||||
|
||||
ConcurrentHashSet<string>? writtenKeys = GetOrRegisterScopeWrittenKeys();
|
||||
if (writtenKeys?.TryAdd(cacheKey) is false)
|
||||
{
|
||||
_logger.LogDebug("Cache version for {EntityType} already written in this scope, skipping", typeof(TEntity).Name);
|
||||
return;
|
||||
}
|
||||
|
||||
using ICoreScope scope = _scopeProvider.CreateCoreScope();
|
||||
|
||||
// We have to take a write lock to ensure the cache is not being read while we update the version.
|
||||
scope.WriteLock(Constants.Locks.CacheVersion);
|
||||
|
||||
var cacheKey = GetCacheKey<TEntity>();
|
||||
var newVersion = Guid.NewGuid();
|
||||
|
||||
_logger.LogDebug("Setting cache for {EntityType} to version {Version}", typeof(TEntity).Name, newVersion);
|
||||
await _repositoryCacheVersionRepository.SaveAsync(new RepositoryCacheVersion { Identifier = cacheKey, Version = newVersion.ToString() });
|
||||
_cacheVersions[cacheKey] = newVersion;
|
||||
_repositoryCacheVersionAccessor.VersionChanged(cacheKey, newVersion);
|
||||
_repositoryCacheVersionAccessor.VersionChanged(cacheKey);
|
||||
|
||||
scope.Complete();
|
||||
}
|
||||
@@ -110,6 +104,7 @@ internal class RepositoryCacheVersionService : IRepositoryCacheVersionService
|
||||
public async Task SetCachesSyncedAsync()
|
||||
{
|
||||
using ICoreScope scope = _scopeProvider.CreateCoreScope();
|
||||
scope.ReadLock(Constants.Locks.CacheVersion);
|
||||
|
||||
// We always sync all caches versions, so it's safe to assume all caches are synced at this point.
|
||||
IEnumerable<RepositoryCacheVersion> cacheVersions = await _repositoryCacheVersionRepository.GetAllAsync();
|
||||
@@ -136,22 +131,4 @@ internal class RepositoryCacheVersionService : IRepositoryCacheVersionService
|
||||
internal string GetCacheKey<TEntity>()
|
||||
where TEntity : class =>
|
||||
typeof(TEntity).FullName ?? typeof(TEntity).Name;
|
||||
|
||||
private ConcurrentHashSet<string>? GetOrRegisterScopeWrittenKeys()
|
||||
{
|
||||
IScopeContext? context = _scopeProvider.Context;
|
||||
if (context is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Guid contextId = context.InstanceId;
|
||||
ConcurrentHashSet<string> writtenKeys = _writtenKeysByScope.GetOrAdd(contextId, _ => new ConcurrentHashSet<string>());
|
||||
|
||||
context.Enlist(
|
||||
$"RepositoryCacheVersionService_{contextId}",
|
||||
completed => _writtenKeysByScope.TryRemove(contextId, out _));
|
||||
|
||||
return writtenKeys;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ public sealed class TypeLoader
|
||||
public ITypeFinder TypeFinder { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the set of assemblies to scan.
|
||||
/// Gets or sets the set of assemblies to scan.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
@@ -58,15 +58,19 @@ public sealed class TypeLoader
|
||||
/// assemblies
|
||||
/// for example.
|
||||
/// </para>
|
||||
/// <para>Marked as internal as used only for unit tests.</para>
|
||||
/// <para>This is for unit tests.</para>
|
||||
/// </remarks>
|
||||
internal IEnumerable<Assembly> AssembliesToScan => _assemblies ??= TypeFinder.AssembliesToScan;
|
||||
// internal for tests
|
||||
[Obsolete("Scheduled for removal in Umbraco 18.")]
|
||||
public IEnumerable<Assembly> AssembliesToScan => _assemblies ??= TypeFinder.AssembliesToScan;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type lists.
|
||||
/// </summary>
|
||||
/// <remarks>Marked as internal as used only for unit tests.</remarks>
|
||||
internal IEnumerable<TypeList> TypeLists => _types.Values;
|
||||
/// <remarks>For unit tests.</remarks>
|
||||
// internal for tests
|
||||
[Obsolete("Scheduled for removal in Umbraco 18.")]
|
||||
public IEnumerable<TypeList> TypeLists => _types.Values;
|
||||
|
||||
#region Get Assembly Attributes
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ public interface IConfigManipulator
|
||||
/// </summary>
|
||||
/// <param name="disable">The value to save.</param>
|
||||
/// <returns></returns>
|
||||
[Obsolete("This method is no longer used by Umbraco. Set the Umbraco:CMS:WebRouting:DisableRedirectUrlTracking configuration key instead. Scheduled for removal in Umbraco 19.")]
|
||||
Task SaveDisableRedirectUrlTrackingAsync(bool disable);
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace Umbraco.Cms.Core.Configuration.Models;
|
||||
/// Typed configuration options for back-office token cookie settings.
|
||||
/// </summary>
|
||||
[UmbracoOptions(Constants.Configuration.ConfigBackOfficeTokenCookie)]
|
||||
[Obsolete("This will be replaced with a different authentication scheme when the BFF project is complete. Scheduled for removal in Umbraco 19.")]
|
||||
[Obsolete("This will be replaced with a different authentication scheme. Scheduled for removal in Umbraco 18.")]
|
||||
public class BackOfficeTokenCookieSettings
|
||||
{
|
||||
private const string StaticSameSite = "Strict";
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace Umbraco.Cms.Core.Configuration.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Typed configuration options for SignalR settings.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// When <see cref="ClientShouldSkipNegotiation"/> is enabled, all hub endpoints are restricted
|
||||
/// to WebSocket transport and the client skips the negotiate round-trip. The setting is forwarded
|
||||
/// to the client via the <c>/umbraco/management/api/v1/server/configuration</c> endpoint.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Downstream packages (e.g. Umbraco Cloud) can configure these settings via
|
||||
/// <c>IConfigureOptions<SignalRSettings></c> or <c>appsettings.json</c>
|
||||
/// under <c>Umbraco:CMS:SignalR</c>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[UmbracoOptions(Constants.Configuration.ConfigSignalR)]
|
||||
public class SignalRSettings
|
||||
{
|
||||
internal const bool StaticClientShouldSkipNegotiation = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the client should skip the SignalR negotiate
|
||||
/// round-trip and connect directly via WebSockets.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When <c>true</c>, the server restricts all hub endpoints to the WebSocket transport only
|
||||
/// (via <c>HttpConnectionDispatcherOptions.Transports</c>) and the client is instructed to
|
||||
/// set <c>skipNegotiation = true</c> with <c>transport = WebSockets</c>. This eliminates the
|
||||
/// negotiate HTTP request that causes failures in load-balanced deployments without sticky sessions.
|
||||
/// This is safe for self-hosted SignalR but must <b>not</b> be used with Azure SignalR Service.
|
||||
/// </remarks>
|
||||
[DefaultValue(StaticClientShouldSkipNegotiation)]
|
||||
public bool ClientShouldSkipNegotiation { get; set; } = StaticClientShouldSkipNegotiation;
|
||||
}
|
||||
@@ -16,7 +16,6 @@ public class UnattendedSettings
|
||||
private const bool StaticInstallUnattended = false;
|
||||
private const bool StaticUpgradeUnattended = false;
|
||||
private const TelemetryLevel StaticTelemetryLevel = TelemetryLevel.Detailed;
|
||||
private const string StaticMigrationClaimTimeout = "02:00:00";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether unattended installs are enabled.
|
||||
@@ -46,17 +45,6 @@ public class UnattendedSettings
|
||||
/// </remarks>
|
||||
public bool PackageMigrationsUnattended { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum time a migration leadership claim is considered valid before
|
||||
/// another server may take over. Protects against a leader crashing mid-migration.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only relevant in load-balanced deployments with <see cref="UpgradeUnattended"/> enabled.
|
||||
/// Default is 2 hours, which should exceed the longest reasonable migration run time.
|
||||
/// </remarks>
|
||||
[DefaultValue(StaticMigrationClaimTimeout)]
|
||||
public TimeSpan MigrationClaimTimeout { get; set; } = TimeSpan.Parse(StaticMigrationClaimTimeout);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value to use for creating a user with a name for Unattended Installs
|
||||
/// </summary>
|
||||
|
||||
@@ -302,11 +302,6 @@ public static partial class Constants
|
||||
/// </summary>
|
||||
public const string ConfigWebsite = ConfigPrefix + "Website";
|
||||
|
||||
/// <summary>
|
||||
/// The configuration key for SignalR settings.
|
||||
/// </summary>
|
||||
public const string ConfigSignalR = ConfigPrefix + "SignalR";
|
||||
|
||||
/// <summary>
|
||||
/// Contains constants for named options used in configuration.
|
||||
/// </summary>
|
||||
|
||||
@@ -36,14 +36,6 @@ public static partial class Constants
|
||||
/// The key used to store the Umbraco pre-migrations upgrade plan state.
|
||||
/// </summary>
|
||||
public const string UmbracoUpgradePlanPremigrationsKey = KeyValuePrefix + UmbracoUpgradePlanPremigrationsName;
|
||||
|
||||
/// <summary>
|
||||
/// The key used to coordinate migration leadership across servers in a load-balanced
|
||||
/// environment. The value is either empty (no active leader) or
|
||||
/// <c>"{machineIdentifier}|{claimedAtUtc:O}"</c> when a server holds the claim,
|
||||
/// where <c>machineIdentifier</c> is the value returned by <see cref="Umbraco.Cms.Core.Factories.IMachineInfoFactory.GetMachineIdentifier"/>.
|
||||
/// </summary>
|
||||
public const string UpgradeLockKey = "Umbraco.Core.Upgrader.Lock";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
namespace Umbraco.Cms.Core.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Marker interface indicating that Umbraco itself has enabled ASP.NET Core output caching
|
||||
/// (via Website template caching or Delivery API caching configuration).
|
||||
/// Used to gate Umbraco's automatic registration of the output cache middleware so that
|
||||
/// applications calling <c>services.AddOutputCache(...)</c> for their own purposes do not
|
||||
/// inadvertently trigger a duplicate <c>UseOutputCache()</c> registration.
|
||||
/// </summary>
|
||||
public interface IUmbracoManagedOutputCacheMarker { }
|
||||
|
||||
/// <summary>
|
||||
/// Marker class implementation for <see cref="IUmbracoManagedOutputCacheMarker"/>.
|
||||
/// </summary>
|
||||
public sealed class UmbracoManagedOutputCacheMarker : IUmbracoManagedOutputCacheMarker { }
|
||||
@@ -101,8 +101,7 @@ public static partial class UmbracoBuilderExtensions
|
||||
.AddUmbracoOptions<SystemDateMigrationSettings>()
|
||||
.AddUmbracoOptions<DistributedJobSettings>()
|
||||
.AddUmbracoOptions<BackOfficeTokenCookieSettings>()
|
||||
.AddUmbracoOptions<WebsiteSettings>()
|
||||
.AddUmbracoOptions<SignalRSettings>();
|
||||
.AddUmbracoOptions<WebsiteSettings>();
|
||||
|
||||
// Configure connection string and ensure it's updated when the configuration changes
|
||||
builder.Services.AddSingleton<IConfigureOptions<ConnectionStrings>, ConfigureConnectionStrings>();
|
||||
|
||||
@@ -312,7 +312,6 @@ namespace Umbraco.Cms.Core.DependencyInjection
|
||||
Services.AddUnique<IContentPermissionService, ContentPermissionService>();
|
||||
Services.AddUnique<IDictionaryPermissionService, DictionaryPermissionService>();
|
||||
Services.AddUnique<IElementPermissionService, ElementPermissionService>();
|
||||
Services.AddUnique<IElementContainerPermissionService, ElementContainerPermissionService>();
|
||||
Services.AddUnique<IContentService, ContentService>();
|
||||
Services.AddUnique<IElementService, ElementService>();
|
||||
Services.AddUnique<IElementVersionService, ElementVersionService>();
|
||||
@@ -337,6 +336,7 @@ namespace Umbraco.Cms.Core.DependencyInjection
|
||||
Services.AddUnique<IMediaTypeService, MediaTypeService>();
|
||||
Services.AddUnique<IContentTypeEditingService, ContentTypeEditingService>();
|
||||
Services.AddUnique<IMediaTypeEditingService, MediaTypeEditingService>();
|
||||
Services.AddUnique<IFileService, FileService>();
|
||||
Services.AddUnique<ITemplateService, TemplateService>();
|
||||
Services.AddUnique<IScriptService, ScriptService>();
|
||||
Services.AddUnique<IStylesheetService, StylesheetService>();
|
||||
@@ -452,7 +452,6 @@ namespace Umbraco.Cms.Core.DependencyInjection
|
||||
Services.AddSingleton<IContentPermissionAuthorizer, ContentPermissionAuthorizer>();
|
||||
Services.AddSingleton<IDictionaryPermissionAuthorizer, DictionaryPermissionAuthorizer>();
|
||||
Services.AddSingleton<IElementPermissionAuthorizer, ElementPermissionAuthorizer>();
|
||||
Services.AddSingleton<IElementContainerPermissionAuthorizer, ElementContainerPermissionAuthorizer>();
|
||||
Services.AddSingleton<IFeatureAuthorizer, FeatureAuthorizer>();
|
||||
Services.AddSingleton<IMediaPermissionAuthorizer, MediaPermissionAuthorizer>();
|
||||
Services.AddSingleton<IUserGroupPermissionAuthorizer, UserGroupPermissionAuthorizer>();
|
||||
@@ -476,7 +475,6 @@ namespace Umbraco.Cms.Core.DependencyInjection
|
||||
Services.AddUnique<IDocumentUrlAliasService, DocumentUrlAliasService>();
|
||||
Services.AddNotificationAsyncHandler<UmbracoApplicationStartingNotification, DocumentUrlAliasServiceInitializerNotificationHandler>();
|
||||
Services.AddNotificationAsyncHandler<ContentTypeChangedNotification, DocumentUrlServiceContentTypeChangedNotificationHandler>();
|
||||
Services.AddNotificationAsyncHandler<ContentTreeChangeNotification, DocumentUrlServiceContentTreeChangeNotificationHandler>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,10 +13,11 @@ public class MoveEventInfo<TEntity> : MoveEventInfoBase<TEntity>
|
||||
/// <param name="originalPath">The original path of the entity.</param>
|
||||
/// <param name="newParentId">The identifier of the new parent.</param>
|
||||
/// <param name="newParentKey">The unique identifier of the new parent.</param>
|
||||
[Obsolete("Use the overload without the newParentId parameter instead. Scheduled for removal in v19.")]
|
||||
public MoveEventInfo(TEntity entity, string originalPath, int newParentId, Guid? newParentKey)
|
||||
: this(entity, originalPath, newParentKey)
|
||||
: base(entity, originalPath)
|
||||
{
|
||||
NewParentId = newParentId;
|
||||
NewParentKey = newParentKey;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -25,23 +26,15 @@ public class MoveEventInfo<TEntity> : MoveEventInfoBase<TEntity>
|
||||
/// <param name="entity">The entity being moved.</param>
|
||||
/// <param name="originalPath">The original path of the entity.</param>
|
||||
/// <param name="newParentId">The identifier of the new parent.</param>
|
||||
[Obsolete("Use the overload with the newParentKey parameter instead. Scheduled for removal in v19.")]
|
||||
public MoveEventInfo(TEntity entity, string originalPath, int newParentId)
|
||||
: this(entity, originalPath, null)
|
||||
public MoveEventInfo(TEntity entity, string originalPath, int newParentId) : this(entity, originalPath, newParentId, null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MoveEventInfo{TEntity}" /> class.
|
||||
/// Gets or sets the identifier of the new parent.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity being moved.</param>
|
||||
/// <param name="originalPath">The original path of the entity.</param>
|
||||
/// <param name="newParentKey">The unique identifier of the new parent.</param>
|
||||
public MoveEventInfo(TEntity entity, string originalPath, Guid? newParentKey)
|
||||
: base(entity, originalPath)
|
||||
{
|
||||
NewParentKey = newParentKey;
|
||||
}
|
||||
[Obsolete("Please use NewParentKey instead. Scheduled for removal in Umbraco 18.")]
|
||||
public int NewParentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the unique identifier of the new parent.
|
||||
@@ -64,7 +57,7 @@ public class MoveEventInfo<TEntity> : MoveEventInfoBase<TEntity>
|
||||
/// </summary>
|
||||
/// <param name="other">The other instance to compare.</param>
|
||||
/// <returns><c>true</c> if the instances are equal; otherwise, <c>false</c>.</returns>
|
||||
public bool Equals(MoveEventInfo<TEntity>? other) => NewParentKey == other?.NewParentKey && base.Equals(other);
|
||||
public bool Equals(MoveEventInfo<TEntity>? other) => NewParentId == other?.NewParentId && NewParentKey == other.NewParentKey && base.Equals(other);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int GetHashCode()
|
||||
@@ -74,7 +67,7 @@ public class MoveEventInfo<TEntity> : MoveEventInfoBase<TEntity>
|
||||
var hashCode = Entity is not null
|
||||
? EqualityComparer<TEntity>.Default.GetHashCode(Entity)
|
||||
: base.GetHashCode();
|
||||
hashCode = (hashCode * 397) ^ NewParentKey.GetHashCode();
|
||||
hashCode = (hashCode * 397) ^ NewParentId;
|
||||
hashCode = (hashCode * 397) ^ OriginalPath.GetHashCode();
|
||||
return hashCode;
|
||||
}
|
||||
|
||||
@@ -196,14 +196,14 @@ public static class PublishedContentExtensions
|
||||
/// Returns the current template Alias
|
||||
/// </summary>
|
||||
/// <returns>Empty string if none is set.</returns>
|
||||
public static string GetTemplateAlias(this IPublishedContent content, ITemplateService templateService)
|
||||
public static string GetTemplateAlias(this IPublishedContent content, IFileService fileService)
|
||||
{
|
||||
if (content.TemplateId.HasValue == false)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
ITemplate? template = templateService.GetAsync(content.TemplateId.Value).GetAwaiter().GetResult();
|
||||
ITemplate? template = fileService.GetTemplate(content.TemplateId.Value);
|
||||
return template?.Alias ?? string.Empty;
|
||||
}
|
||||
|
||||
@@ -253,15 +253,15 @@ public static class PublishedContentExtensions
|
||||
/// Determines whether a specific template is allowed for the content item by template alias.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="templateService">The template service.</param>
|
||||
/// <param name="fileService">The file service.</param>
|
||||
/// <param name="contentTypeService">The content type service.</param>
|
||||
/// <param name="disableAlternativeTemplates">Whether alternative templates are disabled.</param>
|
||||
/// <param name="validateAlternativeTemplates">Whether to validate alternative templates against allowed templates.</param>
|
||||
/// <param name="templateAlias">The template alias.</param>
|
||||
/// <returns><c>true</c> if the template is allowed; otherwise, <c>false</c>.</returns>
|
||||
public static bool IsAllowedTemplate(this IPublishedContent content, ITemplateService templateService, IContentTypeService contentTypeService, bool disableAlternativeTemplates, bool validateAlternativeTemplates, string templateAlias)
|
||||
public static bool IsAllowedTemplate(this IPublishedContent content, IFileService fileService, IContentTypeService contentTypeService, bool disableAlternativeTemplates, bool validateAlternativeTemplates, string templateAlias)
|
||||
{
|
||||
ITemplate? template = templateService.GetAsync(templateAlias).GetAwaiter().GetResult();
|
||||
ITemplate? template = fileService.GetTemplate(templateAlias);
|
||||
return template != null && content.IsAllowedTemplate(contentTypeService, disableAlternativeTemplates, validateAlternativeTemplates, template.Id);
|
||||
}
|
||||
|
||||
@@ -2141,9 +2141,9 @@ public static class PublishedContentExtensions
|
||||
// with a non-existing published node, will get cache misses and call the DB
|
||||
// making it a very slow operation.
|
||||
|
||||
// INavigationQueryService.TryGetChildrenKeys returns keys already ordered by SortOrder
|
||||
// and FilterAvailable preserves enumeration order, so no further OrderBy is needed.
|
||||
return publishedStatusFilteringService.FilterAvailable(childrenKeys, culture);
|
||||
return publishedStatusFilteringService
|
||||
.FilterAvailable(childrenKeys, culture)
|
||||
.OrderBy(x => x.SortOrder);
|
||||
}
|
||||
|
||||
private static IEnumerable<IPublishedContent> EnumerateDescendantsOrSelfInternal(
|
||||
|
||||
@@ -106,7 +106,6 @@ public static class UdiGetterExtensions
|
||||
return entity switch
|
||||
{
|
||||
IContent content => content.GetUdi(),
|
||||
IElement element => element.GetUdi(),
|
||||
IMedia media => media.GetUdi(),
|
||||
IMember member => member.GetUdi(),
|
||||
_ => throw new NotSupportedException($"Content base type {entity.GetType().FullName} is not supported."),
|
||||
@@ -129,20 +128,6 @@ public static class UdiGetterExtensions
|
||||
return new GuidUdi(entityType, entity.Key).EnsureClosed();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entity identifier of the entity.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity.</param>
|
||||
/// <returns>
|
||||
/// The entity identifier of the entity.
|
||||
/// </returns>
|
||||
public static GuidUdi GetUdi(this IElement entity)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
|
||||
return new GuidUdi(Constants.UdiEntityType.Element, entity.Key).EnsureClosed();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entity identifier of the entity.
|
||||
/// </summary>
|
||||
|
||||
@@ -106,7 +106,7 @@ public interface IHostingEnvironment
|
||||
/// content root are the same, however
|
||||
/// in netcore the web root is /www therefore this will Map to a physical path within www.
|
||||
/// </remarks>
|
||||
[Obsolete("Please use the MapPathWebRoot extension method on an instance of IWebHostEnvironment instead. Scheduled for removal in Umbraco 20.")]
|
||||
[Obsolete("Please use the MapPathWebRoot extension method on an instance of IWebHostEnvironment instead")]
|
||||
string MapPathWebRoot(string path);
|
||||
|
||||
/// <summary>
|
||||
@@ -118,7 +118,7 @@ public interface IHostingEnvironment
|
||||
/// in netcore the web root is /www therefore this will Map to a physical path within www.
|
||||
/// </remarks>
|
||||
[Obsolete(
|
||||
"Please use the MapPathContentRoot extension method on an instance of IHostEnvironment (or IWebHostEnvironment) instead. Scheduled for removal in Umbraco 20.")]
|
||||
"Please use the MapPathContentRoot extension method on an instance of IHostEnvironment (or IWebHostEnvironment) instead")]
|
||||
string MapPathContentRoot(string path);
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -36,14 +36,7 @@ internal sealed partial class ShadowFileSystem : IFileSystem
|
||||
/// <summary>
|
||||
/// Gets the dictionary of shadow nodes tracking file and directory changes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Uses <see cref="StringComparer.OrdinalIgnoreCase"/> so the shadow exposes case-insensitive
|
||||
/// path semantics (matching Windows file system behavior) while preserving the original case
|
||||
/// of paths. Preserving case is required for <see cref="Complete"/>: the stored key is also
|
||||
/// used to locate the shadow file via <c>_sfs.GetFullPath</c>, which on case-sensitive
|
||||
/// file systems (e.g. Linux) must match the case the file was actually written with.
|
||||
/// </remarks>
|
||||
private Dictionary<string, ShadowNode> Nodes => _nodes ??= new Dictionary<string, ShadowNode>(StringComparer.OrdinalIgnoreCase);
|
||||
private Dictionary<string, ShadowNode> Nodes => _nodes ??= new Dictionary<string, ShadowNode>();
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<string> GetDirectories(string path)
|
||||
@@ -73,7 +66,7 @@ internal sealed partial class ShadowFileSystem : IFileSystem
|
||||
var normPath = NormPath(path);
|
||||
if (recursive)
|
||||
{
|
||||
Nodes[normPath] = new ShadowNode(true, true, normPath);
|
||||
Nodes[normPath] = new ShadowNode(true, true);
|
||||
var remove = Nodes.Where(x => IsDescendant(normPath, x.Key)).ToList();
|
||||
foreach (KeyValuePair<string, ShadowNode> kvp in remove)
|
||||
{
|
||||
@@ -91,7 +84,7 @@ internal sealed partial class ShadowFileSystem : IFileSystem
|
||||
throw new InvalidOperationException("Directory is not empty.");
|
||||
}
|
||||
|
||||
Nodes[normPath] = new ShadowNode(true, true, normPath);
|
||||
Nodes[path] = new ShadowNode(true, true);
|
||||
var remove = Nodes.Where(x => IsChild(normPath, x.Key)).ToList();
|
||||
foreach (KeyValuePair<string, ShadowNode> kvp in remove)
|
||||
{
|
||||
@@ -138,7 +131,7 @@ internal sealed partial class ShadowFileSystem : IFileSystem
|
||||
|
||||
if (sd.IsDelete)
|
||||
{
|
||||
Nodes[dirPath] = new ShadowNode(false, true, dirPath);
|
||||
Nodes[dirPath] = new ShadowNode(false, true);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -153,13 +146,12 @@ internal sealed partial class ShadowFileSystem : IFileSystem
|
||||
throw new InvalidOperationException("Invalid path.");
|
||||
}
|
||||
|
||||
Nodes[dirPath] = new ShadowNode(false, true, dirPath);
|
||||
Nodes[dirPath] = new ShadowNode(false, true);
|
||||
}
|
||||
}
|
||||
|
||||
var canonicalPath = sf?.CanonicalPath ?? path;
|
||||
_sfs.AddFile(canonicalPath, stream, overrideIfExists);
|
||||
Nodes[normPath] = new ShadowNode(false, false, canonicalPath);
|
||||
_sfs.AddFile(path, stream, overrideIfExists);
|
||||
Nodes[normPath] = new ShadowNode(false, false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -186,7 +178,7 @@ internal sealed partial class ShadowFileSystem : IFileSystem
|
||||
{
|
||||
if (Nodes.TryGetValue(NormPath(path), out ShadowNode? sf))
|
||||
{
|
||||
return sf.IsDir || sf.IsDelete ? Stream.Null : _sfs.OpenFile(sf.CanonicalPath);
|
||||
return sf.IsDir || sf.IsDelete ? Stream.Null : _sfs.OpenFile(path);
|
||||
}
|
||||
|
||||
return Inner.OpenFile(path);
|
||||
@@ -200,8 +192,7 @@ internal sealed partial class ShadowFileSystem : IFileSystem
|
||||
return;
|
||||
}
|
||||
|
||||
var normPath = NormPath(path);
|
||||
Nodes[normPath] = new ShadowNode(true, false, normPath);
|
||||
Nodes[NormPath(path)] = new ShadowNode(true, false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -235,7 +226,7 @@ internal sealed partial class ShadowFileSystem : IFileSystem
|
||||
|
||||
if (sd.IsDelete)
|
||||
{
|
||||
Nodes[dirPath] = new ShadowNode(false, true, dirPath);
|
||||
Nodes[dirPath] = new ShadowNode(false, true);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -250,15 +241,13 @@ internal sealed partial class ShadowFileSystem : IFileSystem
|
||||
throw new InvalidOperationException("Invalid path.");
|
||||
}
|
||||
|
||||
Nodes[dirPath] = new ShadowNode(false, true, dirPath);
|
||||
Nodes[dirPath] = new ShadowNode(false, true);
|
||||
}
|
||||
}
|
||||
|
||||
var sourceCanonical = sf?.CanonicalPath ?? normSource;
|
||||
var targetCanonical = tf?.CanonicalPath ?? normTarget;
|
||||
_sfs.MoveFile(sourceCanonical, targetCanonical, overrideIfExists);
|
||||
Nodes[normSource] = new ShadowNode(true, false, sourceCanonical);
|
||||
Nodes[normTarget] = new ShadowNode(false, false, targetCanonical);
|
||||
_sfs.MoveFile(normSource, normTarget, overrideIfExists);
|
||||
Nodes[normSource] = new ShadowNode(true, false);
|
||||
Nodes[normTarget] = new ShadowNode(false, false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -280,7 +269,7 @@ internal sealed partial class ShadowFileSystem : IFileSystem
|
||||
{
|
||||
if (Nodes.TryGetValue(NormPath(path), out ShadowNode? sf))
|
||||
{
|
||||
return sf.IsDir || sf.IsDelete ? string.Empty : _sfs.GetFullPath(sf.CanonicalPath);
|
||||
return sf.IsDir || sf.IsDelete ? string.Empty : _sfs.GetFullPath(path);
|
||||
}
|
||||
|
||||
return Inner.GetFullPath(path);
|
||||
@@ -302,7 +291,7 @@ internal sealed partial class ShadowFileSystem : IFileSystem
|
||||
throw new InvalidOperationException("Invalid path.");
|
||||
}
|
||||
|
||||
return _sfs.GetLastModified(sf.CanonicalPath);
|
||||
return _sfs.GetLastModified(path);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -318,7 +307,7 @@ internal sealed partial class ShadowFileSystem : IFileSystem
|
||||
throw new InvalidOperationException("Invalid path.");
|
||||
}
|
||||
|
||||
return _sfs.GetCreated(sf.CanonicalPath);
|
||||
return _sfs.GetCreated(path);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -334,7 +323,7 @@ internal sealed partial class ShadowFileSystem : IFileSystem
|
||||
throw new InvalidOperationException("Invalid path.");
|
||||
}
|
||||
|
||||
return _sfs.GetSize(sf.CanonicalPath);
|
||||
return _sfs.GetSize(path);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -359,7 +348,7 @@ internal sealed partial class ShadowFileSystem : IFileSystem
|
||||
|
||||
if (sd.IsDelete)
|
||||
{
|
||||
Nodes[dirPath] = new ShadowNode(false, true, dirPath);
|
||||
Nodes[dirPath] = new ShadowNode(false, true);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -374,13 +363,12 @@ internal sealed partial class ShadowFileSystem : IFileSystem
|
||||
throw new InvalidOperationException("Invalid path.");
|
||||
}
|
||||
|
||||
Nodes[dirPath] = new ShadowNode(false, true, dirPath);
|
||||
Nodes[dirPath] = new ShadowNode(false, true);
|
||||
}
|
||||
}
|
||||
|
||||
var canonicalPath = sf?.CanonicalPath ?? path;
|
||||
_sfs.AddFile(canonicalPath, physicalPath, overrideIfExists, copy);
|
||||
Nodes[normPath] = new ShadowNode(false, false, canonicalPath);
|
||||
_sfs.AddFile(path, physicalPath, overrideIfExists, copy);
|
||||
Nodes[normPath] = new ShadowNode(false, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -405,11 +393,11 @@ internal sealed partial class ShadowFileSystem : IFileSystem
|
||||
{
|
||||
if (Inner.CanAddPhysical)
|
||||
{
|
||||
Inner.AddFile(kvp.Key, _sfs.GetFullPath(kvp.Value.CanonicalPath)); // overwrite, move
|
||||
Inner.AddFile(kvp.Key, _sfs.GetFullPath(kvp.Key)); // overwrite, move
|
||||
}
|
||||
else
|
||||
{
|
||||
using (Stream stream = _sfs.OpenFile(kvp.Value.CanonicalPath))
|
||||
using (Stream stream = _sfs.OpenFile(kvp.Key))
|
||||
{
|
||||
Inner.AddFile(kvp.Key, stream, true);
|
||||
}
|
||||
@@ -453,15 +441,11 @@ internal sealed partial class ShadowFileSystem : IFileSystem
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes a path's directory separators to forward slashes.
|
||||
/// Normalizes a path to lowercase with forward slashes.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to normalize.</param>
|
||||
/// <returns>The normalized path.</returns>
|
||||
/// <remarks>
|
||||
/// Case is preserved. Case-insensitive matching is handled by <see cref="Nodes"/>'s
|
||||
/// <see cref="StringComparer.OrdinalIgnoreCase"/> comparer.
|
||||
/// </remarks>
|
||||
private static string NormPath(string path) => path.Replace("\\", "/");
|
||||
private static string NormPath(string path) => path.ToLowerInvariant().Replace("\\", "/");
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the input path is a direct child of the specified path.
|
||||
@@ -472,7 +456,7 @@ internal sealed partial class ShadowFileSystem : IFileSystem
|
||||
/// <remarks>Values can be "" (root), "foo", "foo/bar"...</remarks>
|
||||
private static bool IsChild(string path, string input)
|
||||
{
|
||||
if (input.StartsWith(path, StringComparison.OrdinalIgnoreCase) == false || input.Length < path.Length + 2)
|
||||
if (input.StartsWith(path) == false || input.Length < path.Length + 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -482,7 +466,7 @@ internal sealed partial class ShadowFileSystem : IFileSystem
|
||||
return false;
|
||||
}
|
||||
|
||||
var pos = input.IndexOf('/', path.Length + 1);
|
||||
var pos = input.IndexOf("/", path.Length + 1, StringComparison.OrdinalIgnoreCase);
|
||||
return pos < 0;
|
||||
}
|
||||
|
||||
@@ -494,7 +478,7 @@ internal sealed partial class ShadowFileSystem : IFileSystem
|
||||
/// <returns><c>true</c> if input is a descendant of path; otherwise, <c>false</c>.</returns>
|
||||
private static bool IsDescendant(string path, string input)
|
||||
{
|
||||
if (input.StartsWith(path, StringComparison.OrdinalIgnoreCase) == false || input.Length < path.Length + 2)
|
||||
if (input.StartsWith(path) == false || input.Length < path.Length + 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -511,14 +495,12 @@ internal sealed partial class ShadowFileSystem : IFileSystem
|
||||
{
|
||||
foreach (var file in Inner.GetFiles(path))
|
||||
{
|
||||
var normFile = NormPath(file);
|
||||
Nodes[normFile] = new ShadowNode(true, false, normFile);
|
||||
Nodes[NormPath(file)] = new ShadowNode(true, false);
|
||||
}
|
||||
|
||||
foreach (var dir in Inner.GetDirectories(path))
|
||||
{
|
||||
var normDir = NormPath(dir);
|
||||
Nodes[normDir] = new ShadowNode(true, true, normDir);
|
||||
Nodes[NormPath(dir)] = new ShadowNode(true, true);
|
||||
if (recurse)
|
||||
{
|
||||
Delete(dir, true);
|
||||
@@ -630,12 +612,10 @@ internal sealed partial class ShadowFileSystem : IFileSystem
|
||||
/// </summary>
|
||||
/// <param name="isDelete">Whether this node represents a deletion.</param>
|
||||
/// <param name="isdir">Whether this node represents a directory.</param>
|
||||
/// <param name="canonicalPath">The original-case path tracked by this node.</param>
|
||||
public ShadowNode(bool isDelete, bool isdir, string canonicalPath)
|
||||
public ShadowNode(bool isDelete, bool isdir)
|
||||
{
|
||||
IsDelete = isDelete;
|
||||
IsDir = isdir;
|
||||
CanonicalPath = canonicalPath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -648,17 +628,6 @@ internal sealed partial class ShadowFileSystem : IFileSystem
|
||||
/// </summary>
|
||||
public bool IsDir { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the original-case path tracked by this node. For existing-file nodes this is
|
||||
/// the path used the first time the file was staged in the current shadow scope.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// All operations against the inner shadow file system (<c>_sfs</c>) must use this
|
||||
/// path so that re-staging the same logical path with a different case still reaches
|
||||
/// the same on-disk file on case-sensitive file systems (e.g. Linux).
|
||||
/// </remarks>
|
||||
public string CanonicalPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this node represents an existing item (not deleted).
|
||||
/// </summary>
|
||||
|
||||
@@ -64,8 +64,4 @@ public class BlockGridLayoutItem : BlockLayoutItemBase
|
||||
/// <inheritdoc />
|
||||
public override bool ReferencesSetting(Guid key)
|
||||
=> SettingsKey == key || Areas.Any(area => area.ContainsSetting(key));
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IEnumerable<IBlockLayoutItem> GetContainedLayouts()
|
||||
=> Areas.SelectMany(area => area.Items);
|
||||
}
|
||||
|
||||
@@ -5,18 +5,12 @@ namespace Umbraco.Cms.Core.Models.Blocks;
|
||||
/// </summary>
|
||||
public abstract class BlockLayoutItemBase : IBlockLayoutItem
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Guid Key { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid ContentKey { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid? SettingsKey { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsExternalContent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BlockLayoutItemBase" /> class.
|
||||
/// </summary>
|
||||
@@ -50,7 +44,4 @@ public abstract class BlockLayoutItemBase : IBlockLayoutItem
|
||||
/// <inheritdoc />
|
||||
public virtual bool ReferencesSetting(Guid key)
|
||||
=> SettingsKey == key;
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual IEnumerable<IBlockLayoutItem> GetContainedLayouts() => [];
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ public abstract class BlockValue
|
||||
/// <returns>
|
||||
/// <c>true</c> if the specified block layout alias is supported; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
[Obsolete("Scheduled for removal in Umbraco 19.")]
|
||||
[Obsolete("Scheduled for removal in Umbraco 18.")]
|
||||
public virtual bool SupportsBlockLayoutAlias(string alias) => alias.Equals(PropertyEditorAlias);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,18 +8,6 @@ namespace Umbraco.Cms.Core.Models.Blocks;
|
||||
/// </summary>
|
||||
public interface IBlockLayoutItem
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the layout item key.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The layout item key.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// Uniquely identifies a layout item. Previously the <see cref="ContentKey"/> could be used for this, but
|
||||
/// with reusable elements, the same <see cref="ContentKey"/> can appear multiple times in one layout.
|
||||
/// </remarks>
|
||||
public Guid Key { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the content key.
|
||||
/// </summary>
|
||||
@@ -36,11 +24,6 @@ public interface IBlockLayoutItem
|
||||
/// </value>
|
||||
public Guid? SettingsKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if the content source is local or originates from the element service.
|
||||
/// </summary>
|
||||
public bool IsExternalContent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this layout item references the specified content key.
|
||||
/// </summary>
|
||||
@@ -58,10 +41,4 @@ public interface IBlockLayoutItem
|
||||
/// <c>true</c> if this layout item references the specified settings key; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public bool ReferencesSetting(Guid key) => SettingsKey == key;
|
||||
|
||||
/// <summary>
|
||||
/// Returns any nested layouts for this layout (e.g. area layouts for the Block Grid).
|
||||
/// </summary>
|
||||
/// <returns>The nested layouts.</returns>
|
||||
public IEnumerable<IBlockLayoutItem> GetContainedLayouts();
|
||||
}
|
||||
|
||||
@@ -454,24 +454,7 @@ public static class ContentRepositoryExtensions
|
||||
/// Clears all publish culture information from the content item.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item to clear publish information from.</param>
|
||||
public static void ClearPublishInfos(this IPublishableContentBase content)
|
||||
{
|
||||
if (content.PublishCultureInfos is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Pass each published culture through ClearPublishInfo([culture]) to ensure correct change tracking.
|
||||
var cultures = content.PublishCultureInfos.Values.Select(c => c.Culture).ToArray();
|
||||
foreach (var culture in cultures)
|
||||
{
|
||||
content.ClearPublishInfo(culture);
|
||||
}
|
||||
|
||||
// Following #22799 the explicit calls to `ClearPublishInfo` for each culture cause the unpublish in all cultures.
|
||||
// `PublishCultureInfos` is set to null purely to retain previous behaviour at a property level.
|
||||
content.PublishCultureInfos = null;
|
||||
}
|
||||
public static void ClearPublishInfos(this IPublishableContentBase content) => content.PublishCultureInfos = null;
|
||||
|
||||
/// <summary>
|
||||
/// Returns false if the culture is already unpublished
|
||||
|
||||
@@ -8,25 +8,7 @@ namespace Umbraco.Cms.Core.Models.Navigation;
|
||||
/// </summary>
|
||||
public sealed class NavigationNode
|
||||
{
|
||||
private static readonly Comparison<(Guid Key, int SortOrder)> _sortBySortOrder =
|
||||
static (a, b) => a.SortOrder.CompareTo(b.SortOrder);
|
||||
|
||||
private readonly ConcurrentHashSet<Guid> _children;
|
||||
|
||||
/// <summary>
|
||||
/// Cached snapshot of <see cref="Children"/> ordered by each child's <c>SortOrder</c>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Built lazily by <see cref="GetOrderedChildren"/> on first access and invalidated
|
||||
/// (set to <c>null</c>) by <see cref="AddChild"/> / <see cref="RemoveChild"/> /
|
||||
/// <see cref="InvalidateOrderedChildren"/>. Reads are lock-free on the fast path; the
|
||||
/// build and invalidation paths take <see cref="_orderedChildrenLock"/> so concurrent
|
||||
/// first-access threads agree on a single canonical array and an in-flight build
|
||||
/// cannot finish after a concurrent invalidation has cleared it.
|
||||
/// </remarks>
|
||||
private Guid[]? _orderedChildren;
|
||||
|
||||
private readonly Lock _orderedChildrenLock = new();
|
||||
private ConcurrentHashSet<Guid> _children;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the unique key of this navigation node.
|
||||
@@ -71,17 +53,6 @@ public sealed class NavigationNode
|
||||
/// Updates the sort order of this node.
|
||||
/// </summary>
|
||||
/// <param name="newSortOrder">The new sort order value.</param>
|
||||
/// <remarks>
|
||||
/// The parent node's cached ordered-children list (if any) is now stale because it sorts
|
||||
/// by child <c>SortOrder</c>. Callers that hold a reference to the parent should call
|
||||
/// <see cref="InvalidateOrderedChildren"/> on it; <see cref="NavigationNode"/> does not
|
||||
/// hold a reference to its parent <see cref="NavigationNode"/> so cannot invalidate it
|
||||
/// itself.
|
||||
/// </remarks>
|
||||
// TODO (V19): Make internal. The contract requires the caller to invalidate the parent's
|
||||
// ordered-children cache (InvalidateOrderedChildren is internal, so external callers cannot
|
||||
// satisfy that contract and would silently observe stale ordering on subsequent reads).
|
||||
// Internal callers in ContentNavigationServiceBase already do the invalidation correctly.
|
||||
public void UpdateSortOrder(int newSortOrder) => SortOrder = newSortOrder;
|
||||
|
||||
/// <summary>
|
||||
@@ -103,8 +74,6 @@ public sealed class NavigationNode
|
||||
child.SortOrder = _children.Count;
|
||||
|
||||
_children.Add(childKey);
|
||||
|
||||
InvalidateOrderedChildren();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -122,91 +91,5 @@ public sealed class NavigationNode
|
||||
|
||||
_children.Remove(childKey);
|
||||
child.Parent = null;
|
||||
|
||||
InvalidateOrderedChildren();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns this node's children ordered by <c>SortOrder</c>.
|
||||
/// </summary>
|
||||
/// <param name="navigationStructure">The navigation structure dictionary containing all nodes; needed to look up each child's current <c>SortOrder</c>.</param>
|
||||
/// <returns>An immutable, sort-order-presorted snapshot of the children. The result is cached and reused across calls until the children set or a child's <c>SortOrder</c> is mutated.</returns>
|
||||
/// <remarks>
|
||||
/// Lock-free fast path: a non-null cached array is returned without acquiring the lock.
|
||||
/// If the cache is empty, <see cref="BuildOrderedChildren"/> is called under the lock to
|
||||
/// build (with double-checked re-read) and store the canonical array.
|
||||
/// </remarks>
|
||||
internal IReadOnlyList<Guid> GetOrderedChildren(ConcurrentDictionary<Guid, NavigationNode> navigationStructure)
|
||||
{
|
||||
// Volatile.Read provides the acquire fence that pairs with the release fence on the
|
||||
// lock-protected stores in BuildOrderedChildren / InvalidateOrderedChildren. On weak
|
||||
// memory architectures (e.g. ARM64) a plain read can observe writes out of order with
|
||||
// the lock release, so without this barrier a reader could in principle see a torn or
|
||||
// unpublished reference; on x86/x64 the TSO model already gives acquire semantics so
|
||||
// this compiles to a normal load. Matches the lock-free read idiom in System.Lazy<T>
|
||||
// and LazyInitializer.EnsureInitialized.
|
||||
Guid[]? cached = Volatile.Read(ref _orderedChildren);
|
||||
if (cached is not null)
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
|
||||
return BuildOrderedChildren(navigationStructure);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invalidates the cached ordered-children snapshot.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Called by <see cref="AddChild"/> and <see cref="RemoveChild"/> automatically. Must be
|
||||
/// called externally when a child's <c>SortOrder</c> changes (the parent's cache sorts by
|
||||
/// child <c>SortOrder</c> and so is stale after such an update).
|
||||
/// </remarks>
|
||||
internal void InvalidateOrderedChildren()
|
||||
{
|
||||
lock (_orderedChildrenLock)
|
||||
{
|
||||
_orderedChildren = null;
|
||||
}
|
||||
}
|
||||
|
||||
private Guid[] BuildOrderedChildren(ConcurrentDictionary<Guid, NavigationNode> navigationStructure)
|
||||
{
|
||||
lock (_orderedChildrenLock)
|
||||
{
|
||||
// Double-check under the lock — another thread may have built the cache while we
|
||||
// were waiting to acquire it.
|
||||
Guid[]? cached = _orderedChildren;
|
||||
if (cached is not null)
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
|
||||
if (_children.Count == 0)
|
||||
{
|
||||
_orderedChildren = [];
|
||||
return _orderedChildren;
|
||||
}
|
||||
|
||||
var sorted = new List<(Guid Key, int SortOrder)>(_children.Count);
|
||||
foreach (Guid childKey in _children)
|
||||
{
|
||||
if (navigationStructure.TryGetValue(childKey, out NavigationNode? childNode))
|
||||
{
|
||||
sorted.Add((childKey, childNode.SortOrder));
|
||||
}
|
||||
}
|
||||
|
||||
sorted.Sort(_sortBySortOrder);
|
||||
|
||||
var result = new Guid[sorted.Count];
|
||||
for (var i = 0; i < sorted.Count; i++)
|
||||
{
|
||||
result[i] = sorted[i].Key;
|
||||
}
|
||||
|
||||
_orderedChildren = result;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,10 +74,6 @@ public class PublishedValueFallback : IPublishedValueFallback
|
||||
}
|
||||
|
||||
break;
|
||||
case Fallback.Ancestors:
|
||||
// Ancestors fallback only applies at IPublishedContent level (tree-aware).
|
||||
// Skip silently here so chained fallbacks still work and direct element calls don't throw.
|
||||
continue;
|
||||
default:
|
||||
throw NotSupportedFallbackMethod(f, "property");
|
||||
}
|
||||
@@ -131,10 +127,6 @@ public class PublishedValueFallback : IPublishedValueFallback
|
||||
}
|
||||
|
||||
break;
|
||||
case Fallback.Ancestors:
|
||||
// Ancestors fallback only applies at IPublishedContent level (tree-aware).
|
||||
// Skip silently here so chained fallbacks still work and direct element calls don't throw.
|
||||
continue;
|
||||
default:
|
||||
throw NotSupportedFallbackMethod(f, "element");
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ using Umbraco.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Core.Notifications;
|
||||
/// <summary>
|
||||
/// A notification that is used to trigger the <see cref="Services.IScriptService"/> after a script has been deleted.
|
||||
/// A notification that is used to trigger the IFileService when the DeleteScript method is called in the API, after the script has been deleted.
|
||||
/// </summary>
|
||||
public class ScriptDeletedNotification : DeletedNotification<IScript>
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@ using Umbraco.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Core.Notifications;
|
||||
/// <summary>
|
||||
/// A notification that is used to trigger the <see cref="Services.IScriptService"/> when a script is being deleted.
|
||||
/// A notification that is used to trigger the IFileService when the DeleteScript method is called in the API.
|
||||
/// </summary>
|
||||
public class ScriptDeletingNotification : DeletingNotification<IScript>
|
||||
{
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace Umbraco.Cms.Core.Notifications;
|
||||
/// Notification that is published after a script file has been saved.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This notification is published by the <see cref="Services.IScriptService"/> after the script has been persisted.
|
||||
/// This notification is published by the <see cref="Services.IFileService"/> after the script has been persisted.
|
||||
/// It is not cancelable since the save operation has already completed.
|
||||
/// </remarks>
|
||||
public class ScriptSavedNotification : SavedNotification<IScript>
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Umbraco.Cms.Core.Notifications;
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This notification is cancelable, allowing handlers to prevent the save operation.
|
||||
/// The notification is published by the <see cref="Services.IScriptService"/> before the script is persisted.
|
||||
/// The notification is published by the <see cref="Services.IFileService"/> before the script is persisted.
|
||||
/// </remarks>
|
||||
public class ScriptSavingNotification : SavingNotification<IScript>
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@ using Umbraco.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Core.Notifications;
|
||||
/// <summary>
|
||||
/// A notification that is used to trigger the <see cref="Services.IStylesheetService"/> after a stylesheet has been deleted.
|
||||
/// A notification that is used to trigger the IFileService when the DeleteStylesheet method is called in the API, after the stylesheet has been deleted.
|
||||
/// </summary>
|
||||
public class StylesheetDeletedNotification : DeletedNotification<IStylesheet>
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@ using Umbraco.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Core.Notifications;
|
||||
/// <summary>
|
||||
/// A notification that is used to trigger the <see cref="Services.IStylesheetService"/> when a stylesheet is being deleted.
|
||||
/// A notification that is used to trigger the IFileService when the DeleteStylesheet method is called in the API.
|
||||
/// </summary>
|
||||
public class StylesheetDeletingNotification : DeletingNotification<IStylesheet>
|
||||
{
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace Umbraco.Cms.Core.Notifications;
|
||||
/// Notification that is published after a stylesheet has been saved.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This notification is published by the <see cref="Services.IStylesheetService"/> after the stylesheet has been persisted.
|
||||
/// This notification is published by the <see cref="Services.IFileService"/> after the stylesheet has been persisted.
|
||||
/// It is not cancelable since the save operation has already completed.
|
||||
/// </remarks>
|
||||
public class StylesheetSavedNotification : SavedNotification<IStylesheet>
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Umbraco.Cms.Core.Notifications;
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This notification is cancelable, allowing handlers to prevent the save operation.
|
||||
/// The notification is published by the <see cref="Services.IStylesheetService"/> before the stylesheet is persisted.
|
||||
/// The notification is published by the <see cref="Services.IFileService"/> before the stylesheet is persisted.
|
||||
/// </remarks>
|
||||
public class StylesheetSavingNotification : SavingNotification<IStylesheet>
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@ using Umbraco.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Core.Notifications;
|
||||
/// <summary>
|
||||
/// A notification that is used to trigger the <see cref="Services.ITemplateService"/> after a template has been deleted.
|
||||
/// A notification that is used to trigger the IFileService when the DeleteTemplate method is called in the API, after the template has been deleted.
|
||||
/// </summary>
|
||||
public class TemplateDeletedNotification : DeletedNotification<ITemplate>
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user