Compare commits
108
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31f0ad4490 | ||
|
|
b6307fb359 | ||
|
|
d37b5a2edb | ||
|
|
dc459bca67 | ||
|
|
2a7478e7f4 | ||
|
|
7b5a087ea5 | ||
|
|
fefb9ca7aa | ||
|
|
8f584e1a49 | ||
|
|
de02456cf8 | ||
|
|
f47a456515 | ||
|
|
234907aedb | ||
|
|
e3ff3ea3b9 | ||
|
|
6050bdd40f | ||
|
|
8c23cb2ffd | ||
|
|
faba205025 | ||
|
|
35c940364c | ||
|
|
f4ff1da043 | ||
|
|
30aea87e28 | ||
|
|
93229fd457 | ||
|
|
4f29bcd8d6 | ||
|
|
80dddc430c | ||
|
|
5a33a55f21 | ||
|
|
4276e903d7 | ||
|
|
c5098f6df3 | ||
|
|
aa0d636503 | ||
|
|
a0e1908d2b | ||
|
|
3e57bb8128 | ||
|
|
0094c19780 | ||
|
|
ed162feddf | ||
|
|
28e9607910 | ||
|
|
b8d02067fe | ||
|
|
3fd311bce8 | ||
|
|
55b77157ec | ||
|
|
906cbb50d8 | ||
|
|
93dff1a7a3 | ||
|
|
962db17628 | ||
|
|
4df4ee9c31 | ||
|
|
c37fab8e4e | ||
|
|
bfc8053c11 | ||
|
|
50b2c8e03b | ||
|
|
2564d04941 | ||
|
|
ca3091e92e | ||
|
|
036225a76d | ||
|
|
307f3be505 | ||
|
|
b5fb17682c | ||
|
|
83882a94bd | ||
|
|
0c5156c7b3 | ||
|
|
c859b2883d | ||
|
|
2aa49a3004 | ||
|
|
54a1364a88 | ||
|
|
603788a039 | ||
|
|
c8454684d0 | ||
|
|
848df27482 | ||
|
|
6ed4075eeb | ||
|
|
0da1fa116e | ||
|
|
51cec0e41f | ||
|
|
b563561d2a | ||
|
|
c003751466 | ||
|
|
caa01b5122 | ||
|
|
b14a7ca3e3 | ||
|
|
0803ab8bda | ||
|
|
ac86d97c4e | ||
|
|
cbde6af9c0 | ||
|
|
da8a532080 | ||
|
|
cf7623e5bd | ||
|
|
8a47072b9b | ||
|
|
6726e6b208 | ||
|
|
27209c2295 | ||
|
|
6c40dc0d25 | ||
|
|
fe06cd28bf | ||
|
|
4083ceb63a | ||
|
|
69e7151079 | ||
|
|
74c71f977c | ||
|
|
f12da6e4cb | ||
|
|
c19cc92690 | ||
|
|
396ebdd48d | ||
|
|
e91c2e659d | ||
|
|
5bb265b3a4 | ||
|
|
107fb48002 | ||
|
|
7b449658d3 | ||
|
|
add5bd31c9 | ||
|
|
72447b040a | ||
|
|
b13544083f | ||
|
|
70d1728843 | ||
|
|
c08e39064f | ||
|
|
dfb7507ae2 | ||
|
|
d749af8974 | ||
|
|
14fe6f4888 | ||
|
|
0d4f24300a | ||
|
|
ebb6590bad | ||
|
|
7b85109606 | ||
|
|
0e60602746 | ||
|
|
e08a60e74d | ||
|
|
4c7abc41b2 | ||
|
|
45ae5aaac0 | ||
|
|
30d9288f12 | ||
|
|
e27b661b3f | ||
|
|
75e4b2b18d | ||
|
|
8642b9e615 | ||
|
|
638e181334 | ||
|
|
254400bb58 | ||
|
|
3bcdcc56c8 | ||
|
|
dd6ae8bd25 | ||
|
|
aa32035830 | ||
|
|
911d2fe67b | ||
|
|
81685bf0e2 | ||
|
|
9fb018276a | ||
|
|
2a604c8719 |
+1
-1
@@ -102,7 +102,7 @@ dotnet_style_predefined_type_for_locals_parameters_members = true:warning
|
||||
dotnet_style_predefined_type_for_member_access = true:warning
|
||||
# Modifier preferences
|
||||
# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#normalize-modifiers
|
||||
dotnet_style_require_accessibility_modifiers = always:warning
|
||||
dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning
|
||||
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:warning
|
||||
visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:warning
|
||||
dotnet_style_readonly_field = true:warning
|
||||
|
||||
@@ -30,7 +30,7 @@ This guide describes each step to make your first contribution:
|
||||
|
||||
Create a new branch based on `main` and name it after the issue you're fixing. For example: `v15/bugfix/18132-rte-tinymce-onchange-value-check`.
|
||||
|
||||
Please follow this format for branches: `v{major}/{feature|bugfix|task}/{issue}-{description}`.
|
||||
Please follow this format for branches: `v{major}/{feature|bugfix|task|qa|improvement}/{issue}-{description}`.
|
||||
|
||||
This is a development branch for the particular issue you're working on, in this case, a bug-fix for issue number `18132` that affects Umbraco v.15.
|
||||
|
||||
|
||||
+4
-2
@@ -95,6 +95,10 @@ tools/docfx/
|
||||
/tests/Umbraco.Tests.Integration/[Uu]mbraco/[Ll]ogs/
|
||||
/tests/Umbraco.Tests.Integration/Views/
|
||||
/tests/Umbraco.Tests.UnitTests/[Uu]mbraco/[Dd]ata/TEMP/
|
||||
/BenchmarkDotNet.Artifacts/
|
||||
playwright-report
|
||||
trace.zip
|
||||
/tests/Umbraco.Tests.AcceptanceTest/results
|
||||
|
||||
# Ignore auto-generated schema
|
||||
/src/Umbraco.Cms.Targets/tasks/
|
||||
@@ -107,5 +111,3 @@ tools/docfx/
|
||||
/tests/Umbraco.Tests.Integration/appsettings-schema.*.json
|
||||
/tests/Umbraco.Tests.Integration/umbraco-package-schema.json
|
||||
/src/Umbraco.Cms/appsettings-schema.json
|
||||
playwright-report
|
||||
trace.zip
|
||||
|
||||
Vendored
+1
@@ -3,6 +3,7 @@
|
||||
"backoffice",
|
||||
"pickable",
|
||||
"Pickable",
|
||||
"Umbraco",
|
||||
"unprovide",
|
||||
"Unproviding"
|
||||
],
|
||||
|
||||
@@ -142,8 +142,33 @@ Web.UI → Web.Common → Infrastructure → Core
|
||||
### Branching Strategy
|
||||
|
||||
- **Main branch**: `main` (protected)
|
||||
- **Branch naming**:
|
||||
- See `.github/CONTRIBUTING.md` for full guidelines
|
||||
- **Branch naming convention**: `v<version>/<type>/<description>`
|
||||
|
||||
**Format**: `v{major-version}/{type}/{kebab-case-description}`
|
||||
|
||||
**Version**: Read from `version.json` in the repository root. Use the major version number (e.g., `v17` for version 17.x.x).
|
||||
|
||||
**Types**:
|
||||
| Type | Use Case |
|
||||
|------|----------|
|
||||
| `feature` | New feature being introduced to the product |
|
||||
| `bugfix` | Fix to an existing issue with the product |
|
||||
| `qa` | Adding or updating unit, integration, or end-to-end tests |
|
||||
| `improvement` | Update to something that already exists but isn't broken (UI finessing, refactoring) |
|
||||
| `task` | Update that doesn't directly impact product behavior (dependency updates, build pipeline) |
|
||||
|
||||
**Description**: A short, kebab-case description (a few words). This should be prefixed with the GitHub issue number if the update is related to resolving a tracked issue.
|
||||
|
||||
**Examples**:
|
||||
```
|
||||
v17/bugfix/12345-correct-display-of-pending-migrations
|
||||
v17/feature/add-webhook-support
|
||||
v17/improvement/optimize-content-cache
|
||||
v17/qa/add-media-service-tests
|
||||
v17/task/update-ef-core-dependency
|
||||
```
|
||||
|
||||
See `.github/CONTRIBUTING.md` for full guidelines.
|
||||
|
||||
### Pull Request Process
|
||||
|
||||
|
||||
+29
-20
@@ -2,6 +2,7 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
|
||||
</PropertyGroup>
|
||||
<!-- Global packages (private, build-time packages for all projects) -->
|
||||
<ItemGroup>
|
||||
@@ -12,27 +13,27 @@
|
||||
</ItemGroup>
|
||||
<!-- Microsoft packages -->
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="10.1.0" />
|
||||
<PackageVersion Include="System.Linq.Async" Version="7.0.0" />
|
||||
</ItemGroup>
|
||||
<!-- Umbraco packages -->
|
||||
@@ -50,6 +51,7 @@
|
||||
<PackageVersion Include="JsonPatch.Net" Version="3.3.0" />
|
||||
<PackageVersion Include="K4os.Compression.LZ4" Version="1.3.8" />
|
||||
<PackageVersion Include="MailKit" Version="4.14.1" />
|
||||
<PackageVersion Include="Markdig" Version="0.44.0" />
|
||||
<PackageVersion Include="Markdown" Version="2.2.1" />
|
||||
<PackageVersion Include="MessagePack" Version="3.1.4" />
|
||||
<PackageVersion Include="MiniProfiler.AspNetCore.Mvc" Version="4.5.4" />
|
||||
@@ -78,5 +80,12 @@
|
||||
</ItemGroup>
|
||||
<!-- Transitive pinned versions (only required because our direct dependencies have vulnerable versions of transitive dependencies) -->
|
||||
<ItemGroup>
|
||||
<!-- Dazinator.Extensions.FileProviders references vulnerable versions of the following: -->
|
||||
<!-- TODO (V18): Remove these pinned dependencies when the Dazinator.Extensions.FileProviders dependency is removed. -->
|
||||
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
|
||||
<PackageVersion Include="System.Private.Uri" Version="4.3.2" />
|
||||
<!-- Markdown references vulnerable version of the following: -->
|
||||
<!-- TODO (V19): Remove these pinned dependencies when the Markdown dependency is removed. -->
|
||||
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -196,6 +196,14 @@ Copyright: 2013-2024 .NET Foundation and Contributors
|
||||
|
||||
---
|
||||
|
||||
Markdig: A fast, powerful, CommonMark compliant, extensible Markdown processor for .NET
|
||||
|
||||
URL: https://github.com/xoofx/markdig
|
||||
License: BSD-2-Clause license
|
||||
Copyright: 2018+, Alexandre Mutel. All rights reserved.
|
||||
|
||||
---
|
||||
|
||||
Markdown: A library for parsing and compiling Markdown
|
||||
|
||||
URL: https://github.com/hey-red/Markdown
|
||||
|
||||
@@ -215,7 +215,7 @@ stages:
|
||||
inputs:
|
||||
targetType: inline
|
||||
script: |
|
||||
choco install docfx --version=2.59.4 -y
|
||||
dotnet tool install -g docfx
|
||||
if ($lastexitcode -ne 0){
|
||||
throw ("Error installing DocFX")
|
||||
}
|
||||
|
||||
@@ -28,6 +28,12 @@ parameters:
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
# Can we slow our tests down when running on SQLite? That way we might be able to avoid DB locks
|
||||
- name: skipSqliteAcceptanceTests
|
||||
displayName: Skip SQLite acceptance tests
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
variables:
|
||||
nodeVersion: 20
|
||||
solution: umbraco.sln
|
||||
@@ -342,7 +348,7 @@ stages:
|
||||
- job:
|
||||
displayName: E2E Tests (SQLite)
|
||||
timeoutInMinutes: 180
|
||||
condition: ${{ eq(parameters.skipDefaultConfigAcceptanceTests, false) }}
|
||||
condition: ${{ and(eq(parameters.skipDefaultConfigAcceptanceTests, false), eq(parameters.skipSqliteAcceptanceTests, false)) }}
|
||||
variables:
|
||||
# Connection string
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=Umbraco;Mode=Memory;Cache=Shared;Foreign Keys=True;Pooling=True
|
||||
@@ -581,6 +587,23 @@ stages:
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
|
||||
additionalEnvironmentVariables: false
|
||||
# ContentSettingConfig
|
||||
WindowsContentSettingsConfig:
|
||||
vmImage: "windows-latest"
|
||||
testFolder: "ContentSettingConfig"
|
||||
port: ''
|
||||
testCommand: "npx playwright test --project=contentSettingConfig"
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
|
||||
additionalEnvironmentVariables: false
|
||||
LinuxContentSettingsConfig:
|
||||
vmImage: "ubuntu-latest"
|
||||
testFolder: "ContentSettingConfig"
|
||||
port: ''
|
||||
testCommand: "npx playwright test --project=contentSettingConfig"
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
|
||||
additionalEnvironmentVariables: false
|
||||
# SMTP
|
||||
LinuxSMTP:
|
||||
vmImage: "ubuntu-latest"
|
||||
@@ -693,7 +716,7 @@ stages:
|
||||
AZUREB2CTESTUSEREMAIL: $(AZUREB2CTESTUSEREMAIL)
|
||||
AZUREB2CTESTUSERPASSWORD: $(AZUREB2CTESTUSERPASSWORD)
|
||||
DatabaseType: ${{ variables.DatabaseType }}
|
||||
|
||||
|
||||
# Stop SMTP4dev container
|
||||
- bash: |
|
||||
echo "Stopping SMTP4dev container..."
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Umbraco.Cms.Api.Common.OpenApi;
|
||||
using Umbraco.Extensions;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Configuration;
|
||||
|
||||
@@ -16,15 +13,31 @@ public class ConfigureUmbracoSwaggerGenOptions : IConfigureOptions<SwaggerGenOpt
|
||||
private readonly IOperationIdSelector _operationIdSelector;
|
||||
private readonly ISchemaIdSelector _schemaIdSelector;
|
||||
private readonly ISubTypesSelector _subTypesSelector;
|
||||
private readonly IDocumentInclusionSelector _documentInclusionSelector;
|
||||
|
||||
public ConfigureUmbracoSwaggerGenOptions(
|
||||
IOperationIdSelector operationIdSelector,
|
||||
ISchemaIdSelector schemaIdSelector,
|
||||
ISubTypesSelector subTypesSelector)
|
||||
ISubTypesSelector subTypesSelector,
|
||||
IDocumentInclusionSelector documentInclusionSelector)
|
||||
{
|
||||
_operationIdSelector = operationIdSelector;
|
||||
_schemaIdSelector = schemaIdSelector;
|
||||
_subTypesSelector = subTypesSelector;
|
||||
_documentInclusionSelector = documentInclusionSelector;
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
|
||||
public ConfigureUmbracoSwaggerGenOptions(
|
||||
IOperationIdSelector operationIdSelector,
|
||||
ISchemaIdSelector schemaIdSelector,
|
||||
ISubTypesSelector subTypesSelector)
|
||||
: this(
|
||||
operationIdSelector,
|
||||
schemaIdSelector,
|
||||
subTypesSelector,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IDocumentInclusionSelector>())
|
||||
{
|
||||
}
|
||||
|
||||
public void Configure(SwaggerGenOptions swaggerGenOptions)
|
||||
@@ -39,19 +52,11 @@ public class ConfigureUmbracoSwaggerGenOptions : IConfigureOptions<SwaggerGenOpt
|
||||
});
|
||||
|
||||
swaggerGenOptions.CustomOperationIds(description => _operationIdSelector.OperationId(description));
|
||||
swaggerGenOptions.DocInclusionPredicate((name, api) =>
|
||||
{
|
||||
if (api.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor
|
||||
&& controllerActionDescriptor.HasMapToApiAttribute(name))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
ApiVersionMetadata apiVersionMetadata = api.ActionDescriptor.GetApiVersionMetadata();
|
||||
return apiVersionMetadata.Name == name
|
||||
|| (string.IsNullOrEmpty(apiVersionMetadata.Name) && name == DefaultApiConfiguration.ApiName);
|
||||
});
|
||||
swaggerGenOptions.TagActionsBy(api => new[] { api.GroupName });
|
||||
swaggerGenOptions.DocInclusionPredicate(_documentInclusionSelector.Include);
|
||||
swaggerGenOptions.TagActionsBy(api =>
|
||||
api.GroupName is null
|
||||
? []
|
||||
: new[] { api.GroupName });
|
||||
swaggerGenOptions.OrderActionsBy(ActionOrderBy);
|
||||
swaggerGenOptions.SchemaFilter<EnumSchemaFilter>();
|
||||
swaggerGenOptions.CustomSchemaIds(_schemaIdSelector.SchemaId);
|
||||
|
||||
@@ -25,6 +25,7 @@ public static class UmbracoBuilderApiExtensions
|
||||
builder.Services.AddSingleton<ISchemaIdHandler, SchemaIdHandler>();
|
||||
builder.Services.AddSingleton<ISubTypesSelector, SubTypesSelector>();
|
||||
builder.Services.AddSingleton<ISubTypesHandler, SubTypesHandler>();
|
||||
builder.Services.AddSingleton<IDocumentInclusionSelector, DocumentInclusionSelector>();
|
||||
builder.Services.Configure<UmbracoPipelineOptions>(options => options.AddFilter(new SwaggerRouteTemplatePipelineFilter("UmbracoApiCommon")));
|
||||
|
||||
return builder;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Umbraco.Cms.Api.Common.Configuration;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether an API description should be included in a specific documentation set based on the document name
|
||||
/// and API metadata.
|
||||
/// </summary>
|
||||
public class DocumentInclusionSelector : IDocumentInclusionSelector
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public bool Include(string documentName, ApiDescription apiDescription)
|
||||
{
|
||||
if (apiDescription.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor
|
||||
&& controllerActionDescriptor.HasMapToApiAttribute(documentName))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
ApiVersionMetadata apiVersionMetadata = apiDescription.ActionDescriptor.GetApiVersionMetadata();
|
||||
return apiVersionMetadata.Name == documentName
|
||||
|| (string.IsNullOrEmpty(apiVersionMetadata.Name) && documentName == DefaultApiConfiguration.ApiName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a method that determines whether a given API description should be included in a specific documentation
|
||||
/// document.
|
||||
/// </summary>
|
||||
public interface IDocumentInclusionSelector
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether the specified API description should be included in the generated documentation for the given
|
||||
/// document name.
|
||||
/// </summary>
|
||||
/// <param name="documentName">The name of the documentation document being generated.</param>
|
||||
/// <param name="apiDescription">The API description to evaluate for inclusion.</param>
|
||||
/// <returns>true if the API description should be included in the documentation; otherwise, false.</returns>
|
||||
bool Include(string documentName, ApiDescription apiDescription);
|
||||
}
|
||||
@@ -99,8 +99,8 @@ public class ByRouteContentApiController : ContentApiItemControllerBase
|
||||
|
||||
private IActionResult RedirectTo(IApiContentRoute redirectRoute)
|
||||
{
|
||||
Response.Headers.Add("Location-Start-Item-Path", redirectRoute.StartItem.Path);
|
||||
Response.Headers.Add("Location-Start-Item-Id", redirectRoute.StartItem.Id.ToString("D"));
|
||||
Response.Headers.Append("Location-Start-Item-Path", redirectRoute.StartItem.Path);
|
||||
Response.Headers.Append("Location-Start-Item-Id", redirectRoute.StartItem.Id.ToString("D"));
|
||||
return RedirectPermanent(redirectRoute.Path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,9 @@
|
||||
<PropertyGroup>
|
||||
<!--
|
||||
TODO: Fix and remove overrides:
|
||||
[ASP0019] use IHeaderDictionary.Append or the indexer to append or set headers
|
||||
[CS0618/CS0612] update obsolete references
|
||||
-->
|
||||
<WarningsNotAsErrors>$(WarningsNotAsErrors),ASP0019,CS0618,CS0612</WarningsNotAsErrors>
|
||||
<WarningsNotAsErrors>$(WarningsNotAsErrors),CS0618,CS0612</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
+1
@@ -32,6 +32,7 @@ public class ExecuteActionHealthCheckController : HealthCheckControllerBase
|
||||
/// <summary>
|
||||
/// Executes a given action from a HealthCheck.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="action">The action to be executed.</param>
|
||||
/// <returns>The result of a health check after the health check action is performed.</returns>
|
||||
[HttpPost("execute-action")]
|
||||
|
||||
+1
@@ -25,6 +25,7 @@ public class AllHealthCheckGroupController : HealthCheckGroupControllerBase
|
||||
/// <summary>
|
||||
/// Gets a paginated grouped list of all names the health checks are grouped by.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="skip">The amount of items to skip.</param>
|
||||
/// <param name="take">The amount of items to take.</param>
|
||||
/// <returns>The paged result of health checks group names.</returns>
|
||||
|
||||
+1
@@ -25,6 +25,7 @@ public class ByNameHealthCheckGroupController : HealthCheckGroupControllerBase
|
||||
/// <summary>
|
||||
/// Gets a health check group with all its health checks by a group name.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="name">The name of the group.</param>
|
||||
/// <returns>The health check group or not found result.</returns>
|
||||
[HttpGet("{name}")]
|
||||
|
||||
+1
@@ -18,6 +18,7 @@ public class CheckHealthCheckGroupController : HealthCheckGroupControllerBase
|
||||
/// <summary>
|
||||
/// Check all health checks in the group with a given group name.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="name">The name of the group.</param>
|
||||
/// <remarks>The health check result(s) will be included as part of the health checks.</remarks>
|
||||
/// <returns>The health check group or not found result.</returns>
|
||||
|
||||
@@ -24,8 +24,9 @@ public class DetailsIndexerController : IndexerControllerBase
|
||||
/// <summary>
|
||||
/// Check if the index has been rebuilt
|
||||
/// </summary>
|
||||
/// <param name="indexName"></param>
|
||||
/// <returns></returns>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="indexName">The name of the index.</param>
|
||||
/// <returns>The index details.</returns>
|
||||
/// <remarks>
|
||||
/// This is kind of rudimentary since there's no way we can know that the index has rebuilt, we
|
||||
/// have a listener for the index op complete so we'll just check if that id is no longer there in the runtime cache
|
||||
|
||||
@@ -28,8 +28,9 @@ public class RebuildIndexerController : IndexerControllerBase
|
||||
/// <summary>
|
||||
/// Rebuilds the index.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="indexName">The name of the index to rebuild.</param>
|
||||
/// <returns></returns>
|
||||
/// <returns>The result of the rebuild operation.</returns>
|
||||
[HttpPost("{indexName}/rebuild")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
|
||||
@@ -28,6 +28,7 @@ public class AllLogViewerController : LogViewerControllerBase
|
||||
/// <summary>
|
||||
/// Gets a paginated list of all logs for a specific date range.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="skip">The amount of items to skip.</param>
|
||||
/// <param name="take">The amount of items to take.</param>
|
||||
/// <param name="orderDirection">
|
||||
|
||||
+1
@@ -27,6 +27,7 @@ public class AllMessageTemplateLogViewerController : LogViewerControllerBase
|
||||
/// <summary>
|
||||
/// Gets a paginated list of all log message templates for a specific date range.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="skip">The amount of items to skip.</param>
|
||||
/// <param name="take">The amount of items to take.</param>
|
||||
/// <param name="startDate">The start date for the date range (can be null).</param>
|
||||
|
||||
+1
@@ -24,6 +24,7 @@ public class AllSinkLevelLogViewerController : LogViewerControllerBase
|
||||
/// <summary>
|
||||
/// Gets a paginated list of all loggers' levels.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="skip">The amount of items to skip.</param>
|
||||
/// <param name="take">The amount of items to take.</param>
|
||||
/// <returns>The paged result of the configured loggers and their level.</returns>
|
||||
|
||||
+1
@@ -25,6 +25,7 @@ public class LogLevelCountLogViewerController : LogViewerControllerBase
|
||||
/// <summary>
|
||||
/// Gets the count for each log level from the logs for a specific date range.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="startDate">The start date for the date range (can be null).</param>
|
||||
/// <param name="endDate">The end date for the date range (can be null).</param>
|
||||
/// <returns>The log level counts from the (filtered) logs.</returns>
|
||||
|
||||
+1
@@ -24,6 +24,7 @@ public class AllSavedSearchLogViewerController : SavedSearchLogViewerControllerB
|
||||
/// <summary>
|
||||
/// Gets a paginated list of all saved log searches.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="skip">The amount of items to skip.</param>
|
||||
/// <param name="take">The amount of items to take.</param>
|
||||
/// <returns>The paged result of the saved log searches.</returns>
|
||||
|
||||
+1
@@ -23,6 +23,7 @@ public class ByNameSavedSearchLogViewerController : SavedSearchLogViewerControll
|
||||
/// <summary>
|
||||
/// Gets a saved log search by name.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="name">The name of the saved log search.</param>
|
||||
/// <returns>The saved log search or not found result.</returns>
|
||||
[HttpGet("{name}")]
|
||||
|
||||
+1
@@ -19,6 +19,7 @@ public class CreateSavedSearchLogViewerController : SavedSearchLogViewerControll
|
||||
/// <summary>
|
||||
/// Creates a saved log search.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="savedSearch">The log search to be saved.</param>
|
||||
/// <returns>The location of the saved log search after the creation.</returns>
|
||||
[HttpPost]
|
||||
|
||||
+1
@@ -18,6 +18,7 @@ public class DeleteSavedSearchLogViewerController : SavedSearchLogViewerControll
|
||||
/// <summary>
|
||||
/// Deletes a saved log search with a given name.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="name">The name of the saved log search.</param>
|
||||
/// <returns>The result of the deletion.</returns>
|
||||
[HttpDelete("{name}")]
|
||||
|
||||
+1
@@ -17,6 +17,7 @@ public class ValidateLogFileSizeLogViewerController : LogViewerControllerBase
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not you are able to view logs for a specified date range.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="startDate">The start date for the date range (can be null).</param>
|
||||
/// <param name="endDate">The end date for the date range (can be null).</param>
|
||||
/// <returns>The boolean result.</returns>
|
||||
|
||||
@@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Media.Item;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Entities;
|
||||
@@ -18,16 +19,32 @@ public class SearchMediaItemController : MediaItemControllerBase
|
||||
private readonly IIndexedEntitySearchService _indexedEntitySearchService;
|
||||
private readonly IMediaPresentationFactory _mediaPresentationFactory;
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
private readonly IMediaTypeService _mediaTypeService;
|
||||
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public SearchMediaItemController(
|
||||
IIndexedEntitySearchService indexedEntitySearchService,
|
||||
IMediaPresentationFactory mediaPresentationFactory,
|
||||
IDataTypeService dataTypeService)
|
||||
IDataTypeService dataTypeService,
|
||||
IMediaTypeService mediaTypeService)
|
||||
{
|
||||
_indexedEntitySearchService = indexedEntitySearchService;
|
||||
_mediaPresentationFactory = mediaPresentationFactory;
|
||||
_dataTypeService = dataTypeService;
|
||||
_mediaTypeService = mediaTypeService;
|
||||
}
|
||||
|
||||
[Obsolete("Use the non-obsolete constructor instead, will be removed in Umbraco 18.")]
|
||||
public SearchMediaItemController(
|
||||
IIndexedEntitySearchService indexedEntitySearchService,
|
||||
IMediaPresentationFactory mediaPresentationFactory,
|
||||
IDataTypeService dataTypeService)
|
||||
: this(
|
||||
indexedEntitySearchService,
|
||||
mediaPresentationFactory,
|
||||
dataTypeService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IMediaTypeService>())
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Use the non-obsolete constructor instead, will be removed in Umbraco 18.")]
|
||||
@@ -77,6 +94,17 @@ public class SearchMediaItemController : MediaItemControllerBase
|
||||
[FromQuery] IEnumerable<Guid>? allowedMediaTypes = null,
|
||||
Guid? dataTypeId = null)
|
||||
{
|
||||
// We always want to include folders in the search results (aligns with behaviour in Umbraco 13, and allows folders
|
||||
// to be selected to find the selectable items inside).
|
||||
if (allowedMediaTypes is not null)
|
||||
{
|
||||
IMediaType? folderMediaType = _mediaTypeService.Get(Constants.Conventions.MediaTypes.Folder);
|
||||
if (folderMediaType is not null && allowedMediaTypes.Contains(folderMediaType.Key) is false)
|
||||
{
|
||||
allowedMediaTypes = [..allowedMediaTypes, folderMediaType.Key];
|
||||
}
|
||||
}
|
||||
|
||||
var ignoreUserStartNodes = await IgnoreUserStartNodes(dataTypeId);
|
||||
PagedModel<IEntitySlim> searchResult = await _indexedEntitySearchService.SearchAsync(
|
||||
UmbracoObjectTypes.Media,
|
||||
|
||||
+1
@@ -41,6 +41,7 @@ public class AllMigrationStatusPackageController : PackageControllerBase
|
||||
/// <summary>
|
||||
/// Gets a paginated list of the migration status of each installed package.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="skip">The amount of items to skip.</param>
|
||||
/// <param name="take">The amount of items to take.</param>
|
||||
/// <returns>The paged result of the installed packages migration status.</returns>
|
||||
|
||||
+1
@@ -25,6 +25,7 @@ public class AllCreatedPackageController : CreatedPackageControllerBase
|
||||
/// <summary>
|
||||
/// Gets a paginated list of all created packages.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="skip">The amount of items to skip.</param>
|
||||
/// <param name="take">The amount of items to take.</param>
|
||||
/// <returns>The paged result of the created packages.</returns>
|
||||
|
||||
+1
@@ -23,6 +23,7 @@ public class ByKeyCreatedPackageController : CreatedPackageControllerBase
|
||||
/// <summary>
|
||||
/// Gets a package by id.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="id">The id of the package.</param>
|
||||
/// <returns>The package or not found result.</returns>
|
||||
[HttpGet("{id:guid}")]
|
||||
|
||||
+1
@@ -31,6 +31,7 @@ public class CreateCreatedPackageController : CreatedPackageControllerBase
|
||||
/// <summary>
|
||||
/// Creates a package.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="createPackageRequestModel">The model containing the data for a new package.</param>
|
||||
/// <returns>The created package.</returns>
|
||||
[HttpPost]
|
||||
|
||||
+1
@@ -24,6 +24,7 @@ public class DeleteCreatedPackageController : CreatedPackageControllerBase
|
||||
/// <summary>
|
||||
/// Deletes a package with a given id.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="id">The id of the package.</param>
|
||||
/// <returns>The result of the deletion.</returns>
|
||||
[HttpDelete("{id:guid}")]
|
||||
|
||||
+1
@@ -21,6 +21,7 @@ public class DownloadCreatedPackageController : CreatedPackageControllerBase
|
||||
/// <summary>
|
||||
/// Downloads a package XML or ZIP file.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="id">The id of the package.</param>
|
||||
/// <returns>The XML or ZIP file of the package or not found result.</returns>
|
||||
[HttpGet("{id:guid}/download")]
|
||||
|
||||
+1
@@ -31,6 +31,7 @@ public class UpdateCreatedPackageController : CreatedPackageControllerBase
|
||||
/// <summary>
|
||||
/// Updates a package.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="id">The id of the package.</param>
|
||||
/// <param name="updatePackageRequestModel">The model containing the data for updating a package.</param>
|
||||
/// <returns>The created package.</returns>
|
||||
|
||||
@@ -18,6 +18,7 @@ public class RunMigrationPackageController : PackageControllerBase
|
||||
/// <summary>
|
||||
/// Runs all migration plans for a package with a given name if any are pending.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="name">The name of the package.</param>
|
||||
/// <returns>The result of running the package migrations.</returns>
|
||||
[HttpPost("{name}/run-migration")]
|
||||
|
||||
+17
-2
@@ -1,4 +1,4 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core.Configuration;
|
||||
using Umbraco.Cms.Core.Models.RedirectUrlManagement;
|
||||
@@ -6,12 +6,20 @@ using Umbraco.Cms.Core.Security;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.RedirectUrlManagement;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for setting the redirect URL tracking status.
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
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">The back office security accessor.</param>
|
||||
/// <param name="configManipulator">The configuration manipulator.</param>
|
||||
public SetStatusRedirectUrlManagementController(
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
|
||||
IConfigManipulator configManipulator)
|
||||
@@ -23,6 +31,12 @@ public class SetStatusRedirectUrlManagementController : RedirectUrlManagementCon
|
||||
// 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>
|
||||
/// Sets the redirect URL tracking status.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token for the HTTP request.</param>
|
||||
/// <param name="status">The redirect status to set.</param>
|
||||
/// <returns>An OK result if successful.</returns>
|
||||
[HttpPost("status")]
|
||||
[MapToApiVersion("1.0")]
|
||||
public async Task<IActionResult> SetStatus(CancellationToken cancellationToken, [FromQuery] RedirectStatus status)
|
||||
@@ -37,7 +51,8 @@ public class SetStatusRedirectUrlManagementController : RedirectUrlManagementCon
|
||||
var enable = status switch
|
||||
{
|
||||
RedirectStatus.Enabled => true,
|
||||
RedirectStatus.Disabled => false
|
||||
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,
|
||||
|
||||
+2
-1
@@ -1,4 +1,4 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
@@ -33,6 +33,7 @@ public class FilterUserFilterController : UserFilterControllerBase
|
||||
/// <summary>
|
||||
/// Query users
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="skip">Amount to skip.</param>
|
||||
/// <param name="take">Amount to take.</param>
|
||||
/// <param name="orderBy">Property to order by.</param>
|
||||
|
||||
+15
-5
@@ -60,15 +60,25 @@ internal static class BackOfficeAuthPolicyBuilderExtensions
|
||||
AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessContentOrMedia, Constants.Applications.Content, Constants.Applications.Media);
|
||||
AddAllowedApplicationsPolicy(
|
||||
AuthorizationPolicies.SectionAccessForContentTree,
|
||||
Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Users,
|
||||
Constants.Applications.Settings, Constants.Applications.Packages, Constants.Applications.Members);
|
||||
Constants.Applications.Content,
|
||||
Constants.Applications.Media,
|
||||
Constants.Applications.Users,
|
||||
Constants.Applications.Settings,
|
||||
Constants.Applications.Packages,
|
||||
Constants.Applications.Members);
|
||||
AddAllowedApplicationsPolicy(
|
||||
AuthorizationPolicies.SectionAccessForMediaTree,
|
||||
Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Users,
|
||||
Constants.Applications.Settings, Constants.Applications.Packages, Constants.Applications.Members);
|
||||
Constants.Applications.Content,
|
||||
Constants.Applications.Media,
|
||||
Constants.Applications.Users,
|
||||
Constants.Applications.Settings,
|
||||
Constants.Applications.Packages,
|
||||
Constants.Applications.Members);
|
||||
AddAllowedApplicationsPolicy(
|
||||
AuthorizationPolicies.SectionAccessForMemberTree,
|
||||
Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members);
|
||||
Constants.Applications.Content,
|
||||
Constants.Applications.Media,
|
||||
Constants.Applications.Members);
|
||||
AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessMedia, Constants.Applications.Media);
|
||||
AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessMembers, Constants.Applications.Members);
|
||||
AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessPackages, Constants.Applications.Packages);
|
||||
|
||||
@@ -54,6 +54,7 @@ public class UserGroupPresentationFactory : IUserGroupPresentationFactory
|
||||
{
|
||||
Id = userGroup.Key,
|
||||
Name = userGroup.Name ?? string.Empty,
|
||||
Description = userGroup.Description ?? string.Empty,
|
||||
Alias = userGroup.Alias,
|
||||
DocumentStartNode = ReferenceByIdModel.ReferenceOrNull(contentStartNodeKey),
|
||||
DocumentRootAccess = contentRootAccess,
|
||||
@@ -87,6 +88,7 @@ public class UserGroupPresentationFactory : IUserGroupPresentationFactory
|
||||
{
|
||||
Id = userGroup.Key,
|
||||
Name = userGroup.Name ?? string.Empty,
|
||||
Description = userGroup.Description ?? string.Empty,
|
||||
Alias = userGroup.Alias,
|
||||
DocumentStartNode = ReferenceByIdModel.ReferenceOrNull(contentStartNodeKey),
|
||||
MediaStartNode = ReferenceByIdModel.ReferenceOrNull(mediaStartNodeKey),
|
||||
@@ -132,6 +134,7 @@ public class UserGroupPresentationFactory : IUserGroupPresentationFactory
|
||||
{
|
||||
Name = CleanUserGroupNameOrAliasForXss(requestModel.Name),
|
||||
Alias = CleanUserGroupNameOrAliasForXss(requestModel.Alias),
|
||||
Description = requestModel.Description,
|
||||
Icon = requestModel.Icon,
|
||||
HasAccessToAllLanguages = requestModel.HasAccessToAllLanguages,
|
||||
Permissions = requestModel.FallbackPermissions,
|
||||
@@ -197,9 +200,10 @@ public class UserGroupPresentationFactory : IUserGroupPresentationFactory
|
||||
|
||||
current.Name = CleanUserGroupNameOrAliasForXss(request.Name);
|
||||
current.Alias = CleanUserGroupNameOrAliasForXss(request.Alias);
|
||||
current.Description = request.Description;
|
||||
current.Icon = request.Icon;
|
||||
current.HasAccessToAllLanguages = request.HasAccessToAllLanguages;
|
||||
|
||||
|
||||
current.Permissions = request.FallbackPermissions;
|
||||
current.GranularPermissions = await _permissionPresentationFactory.CreatePermissionSetsAsync(request.Permissions);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core;
|
||||
@@ -19,7 +19,18 @@ public class PostUnattendedInstallNotificationHandler : INotificationAsyncHandle
|
||||
private readonly IUserService _userService;
|
||||
private readonly IMetricsConsentService _metricsConsentService;
|
||||
|
||||
public PostUnattendedInstallNotificationHandler(IOptions<UnattendedSettings> unattendedSettings, IUserService userService, IServiceScopeFactory serviceScopeFactory, IMetricsConsentService metricsConsentService)
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PostUnattendedInstallNotificationHandler" /> class.
|
||||
/// </summary>
|
||||
/// <param name="unattendedSettings">The unattended settings.</param>
|
||||
/// <param name="userService">The user service.</param>
|
||||
/// <param name="serviceScopeFactory">The service scope factory.</param>
|
||||
/// <param name="metricsConsentService">The metrics consent service.</param>
|
||||
public PostUnattendedInstallNotificationHandler(
|
||||
IOptions<UnattendedSettings> unattendedSettings,
|
||||
IUserService userService,
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
IMetricsConsentService metricsConsentService)
|
||||
{
|
||||
_unattendedSettings = unattendedSettings;
|
||||
_userService = userService;
|
||||
@@ -95,7 +106,9 @@ public class PostUnattendedInstallNotificationHandler : INotificationAsyncHandle
|
||||
}
|
||||
|
||||
IdentityResult resetResult =
|
||||
await backOfficeUserManager.ChangePasswordWithResetAsync(membershipUser.Id, resetToken,
|
||||
await backOfficeUserManager.ChangePasswordWithResetAsync(
|
||||
membershipUser.Id,
|
||||
resetToken,
|
||||
unattendedPassword!.Trim());
|
||||
if (!resetResult.Succeeded)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Extensions;
|
||||
@@ -30,7 +30,9 @@ public class UnhandledExceptionLoggerMiddleware : IMiddleware
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Unhandled controller exception occurred for request '{RequestUrl}'",
|
||||
_logger.LogError(
|
||||
e,
|
||||
"Unhandled controller exception occurred for request '{RequestUrl}'",
|
||||
context.Request.GetEncodedPathAndQuery());
|
||||
// Throw the error again, just in case it gets handled
|
||||
throw;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.OpenApi;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Umbraco.Cms.Api.Management.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Management.ViewModels;
|
||||
using Umbraco.Cms.Core;
|
||||
|
||||
@@ -10,6 +11,12 @@ internal sealed class NotificationHeaderFilter : IOperationFilter
|
||||
{
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
// Only apply to the Umbraco CMS Management API.
|
||||
if (context.DocumentName != ManagementApiConfiguration.ApiName)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.ApiDescription.HttpMethod == HttpMethod.Get.Method)
|
||||
{
|
||||
return;
|
||||
|
||||
+3
-1
@@ -52,7 +52,9 @@ public abstract class MustSatisfyRequirementAuthorizationHandler<T, TResource> :
|
||||
where T : IAuthorizationRequirement
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, T requirement,
|
||||
protected override async Task HandleRequirementAsync(
|
||||
AuthorizationHandlerContext context,
|
||||
T requirement,
|
||||
TResource resource)
|
||||
{
|
||||
var isAuth = await IsAuthorized(context, requirement, resource);
|
||||
|
||||
@@ -7,5 +7,13 @@ namespace Umbraco.Cms.Api.Management.Security;
|
||||
/// </summary>
|
||||
public class ExternalLoginSignInResult : SignInResult
|
||||
{
|
||||
public static ExternalLoginSignInResult NotAllowed { get; } = new() { Succeeded = false };
|
||||
/// <summary>
|
||||
/// Gets a <see cref="ExternalLoginSignInResult" /> that represents a sign-in attempt that failed because
|
||||
/// the user is not allowed to sign-in.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A <see cref="ExternalLoginSignInResult" /> that represents a sign-in attempt that failed because
|
||||
/// the user is not allowed to sign-in.
|
||||
/// </value>
|
||||
public static new ExternalLoginSignInResult NotAllowed { get; } = new() { Succeeded = false };
|
||||
}
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Entities;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Models.ServerEvents;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.ServerEvents;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.ServerEvents;
|
||||
|
||||
/// <summary>
|
||||
/// Handles server event notifications for various entity types by routing create, update, delete, and recycle bin
|
||||
/// events to connected clients or systems.
|
||||
/// </summary>
|
||||
internal sealed class ServerEventSender :
|
||||
INotificationAsyncHandler<ContentSavedNotification>,
|
||||
INotificationAsyncHandler<ContentSavedBlueprintNotification>,
|
||||
@@ -57,8 +63,16 @@ internal sealed class ServerEventSender :
|
||||
INotificationAsyncHandler<MediaMovedToRecycleBinNotification>
|
||||
{
|
||||
private readonly IServerEventRouter _serverEventRouter;
|
||||
private readonly IIdKeyMap _idKeyMap;
|
||||
|
||||
public ServerEventSender(IServerEventRouter serverEventRouter) => _serverEventRouter = serverEventRouter;
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ServerEventSender"/> class.
|
||||
/// </summary>
|
||||
public ServerEventSender(IServerEventRouter serverEventRouter, IIdKeyMap idKeyMap)
|
||||
{
|
||||
_serverEventRouter = serverEventRouter;
|
||||
_idKeyMap = idKeyMap;
|
||||
}
|
||||
|
||||
private async Task NotifySavedAsync<T>(SavedNotification<T> notification, string source)
|
||||
where T : IEntity
|
||||
@@ -113,66 +127,115 @@ internal sealed class ServerEventSender :
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(ContentSavedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.Document);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(ContentSavedBlueprintNotification notification, CancellationToken cancellationToken) =>
|
||||
await RouteCreatedOrUpdatedEvent(Constants.ServerEvents.EventSource.DocumentBlueprint, notification.SavedBlueprint);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(ContentTypeSavedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.DocumentType);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(MediaSavedNotification notification, CancellationToken cancellationToken)
|
||||
=> await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.Media);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(MediaTypeSavedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.MediaType);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(MemberSavedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.Member);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(MemberTypeSavedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.MemberType);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(MemberGroupSavedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.MemberGroup);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(DataTypeSavedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.DataType);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(LanguageSavedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.Language);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(ScriptSavedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.Script);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(StylesheetSavedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.Stylesheet);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(TemplateSavedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.Template);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(DictionaryItemSavedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.DictionaryItem);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(DomainSavedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.Domain);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(PartialViewSavedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.PartialView);
|
||||
|
||||
public async Task HandleAsync(PublicAccessEntrySavedNotification notification, CancellationToken cancellationToken) =>
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(PublicAccessEntrySavedNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.PublicAccessEntry);
|
||||
|
||||
// For public access entries, we also need to notify affected content items, so any client-side
|
||||
// cache for the document can be invalidated.
|
||||
await RouteDocumentUpdatedEventForPublicAccessModification(notification.SavedEntities);
|
||||
}
|
||||
|
||||
private async Task RouteDocumentUpdatedEventForPublicAccessModification(IEnumerable<PublicAccessEntry> entities)
|
||||
{
|
||||
foreach (PublicAccessEntry entity in entities)
|
||||
{
|
||||
Attempt<Guid> getKeyAttempt = _idKeyMap.GetKeyForId(
|
||||
entity.ProtectedNodeId,
|
||||
UmbracoObjectTypes.Document);
|
||||
if (getKeyAttempt.Success is false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var eventModel = new ServerEvent
|
||||
{
|
||||
EventType = Constants.ServerEvents.EventType.Updated,
|
||||
Key = getKeyAttempt.Result,
|
||||
EventSource = Constants.ServerEvents.EventSource.Document,
|
||||
};
|
||||
await _serverEventRouter.RouteEventAsync(eventModel);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(RelationSavedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.Relation);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(RelationTypeSavedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.RelationType);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(UserGroupSavedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.UserGroup);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(UserSavedNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
// We still need to notify of saved entities like any other event source.
|
||||
@@ -192,12 +255,15 @@ internal sealed class ServerEventSender :
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(WebhookSavedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.Webhook);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(ContentDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.Document);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(ContentDeletedBlueprintNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (Core.Models.IContent entity in notification.DeletedBlueprints)
|
||||
@@ -206,69 +272,97 @@ internal sealed class ServerEventSender :
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(ContentTypeDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.DocumentType);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(MediaDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.Media);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(MediaTypeDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.MediaType);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(MemberDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.Member);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(MemberTypeDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.MemberType);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(MemberGroupDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.MemberGroup);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(DataTypeDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.DataType);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(LanguageDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.Language);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(ScriptDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.Script);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(StylesheetDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.Stylesheet);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(TemplateDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.Template);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(DictionaryItemDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.DictionaryItem);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(DomainDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.Domain);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(PartialViewDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.PartialView);
|
||||
|
||||
public async Task HandleAsync(PublicAccessEntryDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(PublicAccessEntryDeletedNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.PublicAccessEntry);
|
||||
|
||||
// For public access entries, we also need to notify affected content items, so any client-side
|
||||
// cache for the document can be invalidated.
|
||||
await RouteDocumentUpdatedEventForPublicAccessModification(notification.DeletedEntities);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(RelationDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.Relation);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(RelationTypeDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.RelationType);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(UserGroupDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.UserGroup);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(UserDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.User);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(WebhookDeletedNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.Webhook);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(ContentMovedToRecycleBinNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyTrashedAsync(notification, Constants.ServerEvents.EventSource.Document);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(MediaMovedToRecycleBinNotification notification, CancellationToken cancellationToken) =>
|
||||
await NotifyTrashedAsync(notification, Constants.ServerEvents.EventSource.Media);
|
||||
}
|
||||
|
||||
@@ -11,12 +11,21 @@ using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Services.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Provides functionality for retrieving user start node entities with access information.
|
||||
/// </summary>
|
||||
public class UserStartNodeEntitiesService : IUserStartNodeEntitiesService
|
||||
{
|
||||
private readonly IEntityService _entityService;
|
||||
private readonly ICoreScopeProvider _scopeProvider;
|
||||
private readonly IIdKeyMap _idKeyMap;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserStartNodeEntitiesService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="entityService">The entity service.</param>
|
||||
/// <param name="scopeProvider">The core scope provider.</param>
|
||||
/// <param name="idKeyMap">The ID to key mapping service.</param>
|
||||
public UserStartNodeEntitiesService(IEntityService entityService, ICoreScopeProvider scopeProvider, IIdKeyMap idKeyMap)
|
||||
{
|
||||
_entityService = entityService;
|
||||
@@ -150,8 +159,7 @@ public class UserStartNodeEntitiesService : IUserStartNodeEntitiesService
|
||||
int after,
|
||||
Ordering ordering,
|
||||
out long totalBefore,
|
||||
out long totalAfter
|
||||
)
|
||||
out long totalAfter)
|
||||
{
|
||||
Attempt<int> targetIdAttempt = _idKeyMap.GetIdForKey(targetKey, umbracoObjectType);
|
||||
if (targetIdAttempt.Success is false)
|
||||
|
||||
@@ -2,12 +2,19 @@
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.ViewModels.MediaType;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request model for creating a media type.
|
||||
/// </summary>
|
||||
public class CreateMediaTypeRequestModel
|
||||
: CreateContentTypeWithParentRequestModelBase<CreateMediaTypePropertyTypeRequestModel, CreateMediaTypePropertyTypeContainerRequestModel>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the allowed media types that can be created under this media type.
|
||||
/// </summary>
|
||||
public IEnumerable<MediaTypeSort> AllowedMediaTypes { get; set; } = Enumerable.Empty<MediaTypeSort>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the compositions for this media type.
|
||||
/// </summary>
|
||||
public IEnumerable<MediaTypeComposition> Compositions { get; set; } = Enumerable.Empty<MediaTypeComposition>();
|
||||
|
||||
public ReferenceByIdModel? Collection { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Umbraco.Cms.Api.Management.ViewModels.UserGroup.Permissions;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.UserGroup.Permissions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.ViewModels.UserGroup;
|
||||
|
||||
@@ -22,6 +22,11 @@ public class UserGroupBase
|
||||
/// </summary>
|
||||
public required string Alias { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The description of the user group
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Icon for the user group
|
||||
/// </summary>
|
||||
|
||||
+2
@@ -7,6 +7,8 @@ using Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
#pragma warning disable ASPDEPR003 // MvcRazorRuntimeCompilationOptions is obsolete - intentional use for development-mode runtime compilation
|
||||
|
||||
namespace Umbraco.Cms.DevelopmentMode.Backoffice.InMemoryAuto;
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Exceptions;
|
||||
|
||||
#pragma warning disable ASPDEPR003 // MvcRazorRuntimeCompilationOptions is obsolete - intentional use for development-mode runtime compilation
|
||||
|
||||
namespace Umbraco.Cms.DevelopmentMode.Backoffice.InMemoryAuto;
|
||||
|
||||
internal sealed class UmbracoViewCompilerProvider : IViewCompilerProvider
|
||||
|
||||
@@ -6,6 +6,10 @@ using Size = System.Drawing.Size;
|
||||
|
||||
namespace Umbraco.Cms.Imaging.ImageSharp.Media;
|
||||
|
||||
/// <summary>
|
||||
/// Extracts image dimensions using ImageSharp, with support for EXIF orientation.
|
||||
/// </summary>
|
||||
/// <seealso cref="IImageDimensionExtractor" />
|
||||
public sealed class ImageSharpDimensionExtractor : IImageDimensionExtractor
|
||||
{
|
||||
private readonly Configuration _configuration;
|
||||
|
||||
@@ -29,7 +29,7 @@ public sealed class ImageSharpImageUrlGenerator : IImageUrlGenerator
|
||||
/// </summary>
|
||||
/// <param name="configuration">The ImageSharp configuration.</param>
|
||||
/// <param name="requestAuthorizationUtilities">Contains helpers that allow authorization of image requests.</param>
|
||||
/// <param name="options"></param>
|
||||
/// <param name="options">The ImageSharp middleware options.</param>
|
||||
public ImageSharpImageUrlGenerator(
|
||||
Configuration configuration,
|
||||
RequestAuthorizationUtilities? requestAuthorizationUtilities,
|
||||
|
||||
@@ -14,11 +14,16 @@ using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Imaging.ImageSharp;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="IUmbracoBuilder" /> to add ImageSharp image processing.
|
||||
/// </summary>
|
||||
public static class UmbracoBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds Image Sharp with Umbraco settings
|
||||
/// Adds ImageSharp image processing with Umbraco settings.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IUmbracoBuilder" />.</param>
|
||||
/// <returns>The <see cref="IServiceCollection" />.</returns>
|
||||
public static IServiceCollection AddUmbracoImageSharp(this IUmbracoBuilder builder)
|
||||
{
|
||||
// Add default ImageSharp configuration and service implementations
|
||||
|
||||
@@ -5,6 +5,10 @@ using Size = System.Drawing.Size;
|
||||
|
||||
namespace Umbraco.Cms.Imaging.ImageSharp.Media;
|
||||
|
||||
/// <summary>
|
||||
/// Extracts image dimensions using ImageSharp, with support for EXIF orientation.
|
||||
/// </summary>
|
||||
/// <seealso cref="IImageDimensionExtractor" />
|
||||
public sealed class ImageSharpDimensionExtractor : IImageDimensionExtractor
|
||||
{
|
||||
private readonly Configuration _configuration;
|
||||
|
||||
@@ -14,11 +14,16 @@ using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Imaging.ImageSharp;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="IUmbracoBuilder" /> to add ImageSharp 2.x image processing.
|
||||
/// </summary>
|
||||
public static class UmbracoBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds Image Sharp with Umbraco settings
|
||||
/// Adds ImageSharp with Umbraco settings.
|
||||
/// </summary>
|
||||
/// <param name="builder">The Umbraco builder.</param>
|
||||
/// <returns>The <see cref="IServiceCollection" />.</returns>
|
||||
public static IServiceCollection AddUmbracoImageSharp(this IUmbracoBuilder builder)
|
||||
{
|
||||
// Add default ImageSharp configuration and service implementations
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace Umbraco.Cms.Persistence.EFCore.Scoping;
|
||||
|
||||
internal interface IAmbientEFCoreScopeStack<TDbContext> : IEFCoreScopeAccessor<TDbContext> where TDbContext : DbContext
|
||||
{
|
||||
public IEfCoreScope<TDbContext>? AmbientScope { get; }
|
||||
new IEfCoreScope<TDbContext>? AmbientScope { get; }
|
||||
|
||||
IEfCoreScope<TDbContext> Pop();
|
||||
|
||||
|
||||
@@ -13,7 +13,10 @@ public interface IEfCoreScope<TDbContext> : ICoreScope
|
||||
/// <returns></returns>
|
||||
Task<T> ExecuteWithContextAsync<T>(Func<TDbContext, Task<T>> method);
|
||||
|
||||
public IScopeContext? ScopeContext { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the scope context.
|
||||
/// </summary>
|
||||
IScopeContext? ScopeContext { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Executes the given function on the database.
|
||||
@@ -26,5 +29,5 @@ public interface IEfCoreScope<TDbContext> : ICoreScope
|
||||
/// <summary>
|
||||
/// Gets the scope notification publisher
|
||||
/// </summary>
|
||||
IScopedNotificationPublisher Notifications { get; }
|
||||
new IScopedNotificationPublisher Notifications { get; }
|
||||
}
|
||||
|
||||
@@ -11,6 +11,17 @@ using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Cache;
|
||||
|
||||
/// <summary>
|
||||
/// Provides cache refresh functionality for content items, ensuring that content-related caches are updated or
|
||||
/// invalidated in response to content changes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The ContentCacheRefresher coordinates cache invalidation for content, including memory caches, URL
|
||||
/// caches, navigation structures, and domain assignments. It responds to content change notifications and ensures that
|
||||
/// all relevant caches reflect the current state of published and unpublished content. This refresher is used
|
||||
/// internally to maintain cache consistency after content operations such as publish, unpublish, move,
|
||||
/// or delete.
|
||||
/// </remarks>
|
||||
public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCacheRefresherNotification,
|
||||
ContentCacheRefresher.JsonPayload>
|
||||
{
|
||||
@@ -25,6 +36,9 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
|
||||
private readonly IPublishStatusManagementService _publishStatusManagementService;
|
||||
private readonly IIdKeyMap _idKeyMap;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ContentCacheRefresher"/> class.
|
||||
/// </summary>
|
||||
public ContentCacheRefresher(
|
||||
AppCaches appCaches,
|
||||
IJsonSerializer serializer,
|
||||
@@ -60,11 +74,15 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
|
||||
|
||||
#region Indirect
|
||||
|
||||
/// <summary>
|
||||
/// Clears cached content and public access data from the provided application caches.
|
||||
/// </summary>
|
||||
/// <param name="appCaches">The application caches instance from which to clear content and public access entries.</param>
|
||||
public static void RefreshContentTypes(AppCaches appCaches)
|
||||
{
|
||||
// we could try to have a mechanism to notify the PublishedCachesService
|
||||
// We could try to have a mechanism to notify the PublishedCachesService
|
||||
// and figure out whether published items were modified or not... keep it
|
||||
// simple for now, just clear the whole thing
|
||||
// simple for now, just clear the whole thing.
|
||||
appCaches.ClearPartialViewCache();
|
||||
|
||||
appCaches.IsolatedCaches.ClearCache<PublicAccessEntry>();
|
||||
@@ -75,16 +93,22 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
|
||||
|
||||
#region Define
|
||||
|
||||
/// <summary>
|
||||
/// Represents a unique identifier for the cache refresher.
|
||||
/// </summary>
|
||||
public static readonly Guid UniqueId = Guid.Parse("900A4FBE-DF3C-41E6-BB77-BE896CD158EA");
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Guid RefresherUniqueId => UniqueId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string Name => "ContentCacheRefresher";
|
||||
|
||||
#endregion
|
||||
|
||||
#region Refresher
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void RefreshInternal(JsonPayload[] payloads)
|
||||
{
|
||||
AppCaches.RuntimeCache.ClearOfType<PublicAccessEntry>();
|
||||
@@ -121,13 +145,15 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
|
||||
base.RefreshInternal(payloads);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Refresh(JsonPayload[] payloads)
|
||||
{
|
||||
var idsRemoved = new HashSet<int>();
|
||||
|
||||
foreach (JsonPayload payload in payloads)
|
||||
{
|
||||
// if the item is not a blueprint and is being completely removed, we need to refresh the domains cache if any domain was assigned to the content
|
||||
// If the item is not a blueprint and is being completely removed, we need to refresh the domains cache if any domain was assigned to the content.
|
||||
// So track the IDs that have been removed.
|
||||
if (payload.Blueprint is false && payload.ChangeTypes.HasTypesAny(TreeChangeTypes.Remove))
|
||||
{
|
||||
idsRemoved.Add(payload.Id);
|
||||
@@ -138,43 +164,18 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
|
||||
|
||||
HandleNavigation(payload);
|
||||
HandlePublishedAsync(payload, CancellationToken.None).GetAwaiter().GetResult();
|
||||
if (payload.Id != default)
|
||||
{
|
||||
_idKeyMap.ClearCache(payload.Id);
|
||||
}
|
||||
if (payload.Key.HasValue)
|
||||
{
|
||||
_idKeyMap.ClearCache(payload.Key.Value);
|
||||
}
|
||||
|
||||
HandleIdKeyMap(payload);
|
||||
}
|
||||
|
||||
// Clear partial view cache when published content changes
|
||||
// Clear partial view cache when published content changes.
|
||||
if (ShouldClearPartialViewCache(payloads))
|
||||
{
|
||||
AppCaches.ClearPartialViewCache();
|
||||
}
|
||||
|
||||
if (idsRemoved.Count > 0)
|
||||
{
|
||||
var assignedDomains = _domainService.GetAll(true)
|
||||
?.Where(x => x.RootContentId.HasValue && idsRemoved.Contains(x.RootContentId.Value)).ToList();
|
||||
|
||||
if (assignedDomains?.Count > 0)
|
||||
{
|
||||
// TODO: this is duplicating the logic in DomainCacheRefresher BUT we cannot inject that into this because it it not registered explicitly in the container,
|
||||
// and we cannot inject the CacheRefresherCollection since that would be a circular reference, so what is the best way to call directly in to the
|
||||
// DomainCacheRefresher?
|
||||
ClearAllIsolatedCacheByEntityType<IDomain>();
|
||||
|
||||
// note: must do what's above FIRST else the repositories still have the old cached
|
||||
// content and when the PublishedCachesService is notified of changes it does not see
|
||||
// the new content...
|
||||
// notify
|
||||
_domainCacheService.Refresh(assignedDomains
|
||||
.Select(x => new DomainCacheRefresher.JsonPayload(x.Id, DomainChangeTypes.Remove)).ToArray());
|
||||
}
|
||||
}
|
||||
// Clear the domain cache if any domain is assigned to removed content.
|
||||
HandleDomainCache(idsRemoved);
|
||||
|
||||
base.Refresh(payloads);
|
||||
}
|
||||
@@ -251,18 +252,51 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsBranchUnpublished(JsonPayload payload)
|
||||
{
|
||||
private static bool IsBranchUnpublished(JsonPayload payload) =>
|
||||
|
||||
// If unpublished cultures has one or more values, but published cultures does not, this means that the branch is unpublished entirely
|
||||
// And therefore should no longer be resolve-able from the cache, so we need to remove it instead.
|
||||
// Otherwise, some culture is still published, so it should be resolve-able from cache, and published cultures should instead be used.
|
||||
return payload.UnpublishedCultures is not null && payload.UnpublishedCultures.Length != 0 &&
|
||||
payload.UnpublishedCultures is not null && payload.UnpublishedCultures.Length != 0 &&
|
||||
(payload.PublishedCultures is null || payload.PublishedCultures.Length == 0);
|
||||
|
||||
private void HandleRouting(JsonPayload payload)
|
||||
{
|
||||
if (payload.ChangeTypes.HasType(TreeChangeTypes.Remove))
|
||||
{
|
||||
Guid key = payload.Key ?? _idKeyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).Result;
|
||||
|
||||
// Note that we need to clear the navigation service as the last thing.
|
||||
if (_documentNavigationQueryService.TryGetDescendantsKeysOrSelfKeys(key, out IEnumerable<Guid>? descendantsOrSelfKeys))
|
||||
{
|
||||
_documentUrlService.DeleteUrlsFromCacheAsync(descendantsOrSelfKeys).GetAwaiter().GetResult();
|
||||
}
|
||||
else if (_documentNavigationQueryService.TryGetDescendantsKeysOrSelfKeysInBin(key, out IEnumerable<Guid>? descendantsOrSelfKeysInBin))
|
||||
{
|
||||
_documentUrlService.DeleteUrlsFromCacheAsync(descendantsOrSelfKeysInBin).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
||||
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll))
|
||||
{
|
||||
_documentUrlService.InitAsync(false, CancellationToken.None).GetAwaiter().GetResult(); // TODO: make async
|
||||
}
|
||||
|
||||
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshNode))
|
||||
{
|
||||
Guid key = payload.Key ?? _idKeyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).Result;
|
||||
_documentUrlService.CreateOrUpdateUrlSegmentsAsync(key).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch))
|
||||
{
|
||||
Guid key = payload.Key ?? _idKeyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).Result;
|
||||
_documentUrlService.CreateOrUpdateUrlSegmentsWithDescendantsAsync(key).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleNavigation(JsonPayload payload)
|
||||
{
|
||||
|
||||
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll))
|
||||
{
|
||||
_documentNavigationManagementService.RebuildAsync().GetAwaiter().GetResult();
|
||||
@@ -389,50 +423,68 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
|
||||
await _publishStatusManagementService.AddOrUpdateStatusWithDescendantsAsync(payload.Key.Value, cancellationToken);
|
||||
}
|
||||
}
|
||||
private void HandleRouting(JsonPayload payload)
|
||||
|
||||
private void HandleIdKeyMap(JsonPayload payload)
|
||||
{
|
||||
if (payload.ChangeTypes.HasType(TreeChangeTypes.Remove))
|
||||
// We only need to flush the ID/Key map when content is deleted.
|
||||
if (payload.ChangeTypes.HasTypesAny(TreeChangeTypes.Remove) is false)
|
||||
{
|
||||
var key = payload.Key ?? _idKeyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).Result;
|
||||
|
||||
//Note the we need to clear the navigation service as the last thing
|
||||
if (_documentNavigationQueryService.TryGetDescendantsKeysOrSelfKeys(key, out var descendantsOrSelfKeys))
|
||||
{
|
||||
_documentUrlService.DeleteUrlsFromCacheAsync(descendantsOrSelfKeys).GetAwaiter().GetResult();
|
||||
}
|
||||
else if (_documentNavigationQueryService.TryGetDescendantsKeysOrSelfKeysInBin(key, out var descendantsOrSelfKeysInBin))
|
||||
{
|
||||
_documentUrlService.DeleteUrlsFromCacheAsync(descendantsOrSelfKeysInBin).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
}
|
||||
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll))
|
||||
{
|
||||
_documentUrlService.InitAsync(false, CancellationToken.None).GetAwaiter().GetResult(); //TODO make async
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshNode))
|
||||
if (payload.Id != default)
|
||||
{
|
||||
var key = payload.Key ?? _idKeyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).Result;
|
||||
_documentUrlService.CreateOrUpdateUrlSegmentsAsync(key).GetAwaiter().GetResult();
|
||||
_idKeyMap.ClearCache(payload.Id);
|
||||
}
|
||||
|
||||
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch))
|
||||
if (payload.Key.HasValue)
|
||||
{
|
||||
var key = payload.Key ?? _idKeyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).Result;
|
||||
_documentUrlService.CreateOrUpdateUrlSegmentsWithDescendantsAsync(key).GetAwaiter().GetResult();
|
||||
_idKeyMap.ClearCache(payload.Key.Value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// these events should never trigger
|
||||
// everything should be PAYLOAD/JSON
|
||||
private void HandleDomainCache(HashSet<int> idsRemoved)
|
||||
{
|
||||
if (idsRemoved.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
var assignedDomains = _domainService.GetAll(true)
|
||||
.Where(x => x.RootContentId.HasValue && idsRemoved.Contains(x.RootContentId.Value))
|
||||
.ToList();
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
if (assignedDomains.Count <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: this is duplicating the logic in DomainCacheRefresher BUT we cannot inject that into this because it it not registered explicitly in the container,
|
||||
// and we cannot inject the CacheRefresherCollection since that would be a circular reference, so what is the best way to call directly in to the
|
||||
// DomainCacheRefresher?
|
||||
ClearAllIsolatedCacheByEntityType<IDomain>();
|
||||
|
||||
// note: must do what's above FIRST else the repositories still have the old cached
|
||||
// content and when the PublishedCachesService is notified of changes it does not see
|
||||
// the new content...
|
||||
// notify
|
||||
_domainCacheService.Refresh(assignedDomains
|
||||
.Select(x => new DomainCacheRefresher.JsonPayload(x.Id, DomainChangeTypes.Remove)).ToArray());
|
||||
}
|
||||
|
||||
// These events should never trigger. Everything should be PAYLOAD/JSON.
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void RefreshAll() => throw new NotSupportedException();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Refresh(int id) => throw new NotSupportedException();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Refresh(Guid id) => throw new NotSupportedException();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Remove(int id) => throw new NotSupportedException();
|
||||
|
||||
#endregion
|
||||
@@ -440,19 +492,40 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
|
||||
#region Json
|
||||
|
||||
// TODO (V14): Change into a record
|
||||
/// <summary>
|
||||
/// Represents a JSON-serializable payload containing information about a content or tree change event, including
|
||||
/// identifiers, change types, and culture-specific publishing details.
|
||||
/// </summary>
|
||||
public class JsonPayload
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Gets the unique integer identifier for the entity.
|
||||
/// </summary>
|
||||
public int Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the unique GUID key associated with the entity, or null if no key is assigned.
|
||||
/// </summary>
|
||||
public Guid? Key { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the types of changes that have occurred in the tree.
|
||||
/// </summary>
|
||||
public TreeChangeTypes ChangeTypes { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the content represents a document blueprint.
|
||||
/// </summary>
|
||||
public bool Blueprint { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of culture codes in which the content is published.
|
||||
/// </summary>
|
||||
public string[]? PublishedCultures { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of culture codes for which the content has been unpublished.
|
||||
/// </summary>
|
||||
public string[]? UnpublishedCultures { get; init; }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
@@ -18,6 +20,7 @@ public sealed class DataTypeCacheRefresher : PayloadCacheRefresherBase<DataTypeC
|
||||
private readonly IPublishedContentTypeCache _publishedContentTypeCache;
|
||||
private readonly IDocumentCacheService _documentCacheService;
|
||||
private readonly IMediaCacheService _mediaCacheService;
|
||||
private readonly IContentTypeCommonRepository _contentTypeCommonRepository;
|
||||
|
||||
public DataTypeCacheRefresher(
|
||||
AppCaches appCaches,
|
||||
@@ -29,7 +32,8 @@ public sealed class DataTypeCacheRefresher : PayloadCacheRefresherBase<DataTypeC
|
||||
IPublishedContentTypeFactory publishedContentTypeFactory,
|
||||
IPublishedContentTypeCache publishedContentTypeCache,
|
||||
IDocumentCacheService documentCacheService,
|
||||
IMediaCacheService mediaCacheService)
|
||||
IMediaCacheService mediaCacheService,
|
||||
IContentTypeCommonRepository contentTypeCommonRepository)
|
||||
: base(appCaches, serializer, eventAggregator, factory)
|
||||
{
|
||||
_idKeyMap = idKeyMap;
|
||||
@@ -38,6 +42,34 @@ public sealed class DataTypeCacheRefresher : PayloadCacheRefresherBase<DataTypeC
|
||||
_publishedContentTypeCache = publishedContentTypeCache;
|
||||
_documentCacheService = documentCacheService;
|
||||
_mediaCacheService = mediaCacheService;
|
||||
_contentTypeCommonRepository = contentTypeCommonRepository;
|
||||
}
|
||||
|
||||
[Obsolete("Use the non-obsolete constructor instead. Scheduled for removal in V18.")]
|
||||
public DataTypeCacheRefresher(
|
||||
AppCaches appCaches,
|
||||
IJsonSerializer serializer,
|
||||
IIdKeyMap idKeyMap,
|
||||
IEventAggregator eventAggregator,
|
||||
ICacheRefresherNotificationFactory factory,
|
||||
IPublishedModelFactory publishedModelFactory,
|
||||
IPublishedContentTypeFactory publishedContentTypeFactory,
|
||||
IPublishedContentTypeCache publishedContentTypeCache,
|
||||
IDocumentCacheService documentCacheService,
|
||||
IMediaCacheService mediaCacheService)
|
||||
: this(
|
||||
appCaches,
|
||||
serializer,
|
||||
idKeyMap,
|
||||
eventAggregator,
|
||||
factory,
|
||||
publishedModelFactory,
|
||||
publishedContentTypeFactory,
|
||||
publishedContentTypeCache,
|
||||
documentCacheService,
|
||||
mediaCacheService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IContentTypeCommonRepository>())
|
||||
{
|
||||
}
|
||||
|
||||
#region Json
|
||||
@@ -84,6 +116,9 @@ public sealed class DataTypeCacheRefresher : PayloadCacheRefresherBase<DataTypeC
|
||||
ClearAllIsolatedCacheByEntityType<IMember>();
|
||||
ClearAllIsolatedCacheByEntityType<IMemberType>();
|
||||
|
||||
// Also clear the 5 minute runtime cache held in ContentTypeCommonRepository.
|
||||
_contentTypeCommonRepository.ClearCache();
|
||||
|
||||
Attempt<IAppPolicyCache?> dataTypeCache = AppCaches.IsolatedCaches.Get<IDataType>();
|
||||
|
||||
foreach (JsonPayload payload in payloads)
|
||||
|
||||
@@ -29,7 +29,7 @@ public class TypeFinder : ITypeFinder
|
||||
"DataAnnotationsExtensions,", "DataAnnotationsExtensions.", "Dynamic,", "Examine,", "Examine.",
|
||||
"HtmlAgilityPack,", "HtmlAgilityPack.", "HtmlDiff,", "ICSharpCode.", "Iesi.Collections,", // used by NHibernate
|
||||
"JetBrains.Annotations,", "LightInject.", // DI
|
||||
"LightInject,", "Lucene.", "Markdown,", "Microsoft.", "MiniProfiler,", "Moq,", "MySql.", "NHibernate,",
|
||||
"LightInject,", "Lucene.", "Markdig,", "Markdown,", "Microsoft.", "MiniProfiler,", "Moq,", "MySql.", "NHibernate,",
|
||||
"NHibernate.", "Newtonsoft.", "NPoco,", "NuGet.", "RouteDebugger,", "Semver.", "Serilog.", "Serilog,",
|
||||
"ServiceStack.", "SqlCE4Umbraco,", "Superpower,", // used by Serilog
|
||||
"System.", "TidyNet,", "TidyNet.", "WebDriver,", "itextsharp,", "mscorlib,", "NUnit,", "NUnit.", "NUnit3.",
|
||||
@@ -115,7 +115,10 @@ public class TypeFinder : ITypeFinder
|
||||
{
|
||||
IEnumerable<Assembly> assemblyList = assemblies ?? AssembliesToScan;
|
||||
|
||||
return GetClassesWithBaseType(assignTypeFrom, assemblyList, onlyConcreteClasses,
|
||||
return GetClassesWithBaseType(
|
||||
assignTypeFrom,
|
||||
assemblyList,
|
||||
onlyConcreteClasses,
|
||||
|
||||
// the additional filter will ensure that any found types also have the attribute applied.
|
||||
t => t.GetCustomAttributes(attributeType, false).Any());
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Collections;
|
||||
using Umbraco.Cms.Core.Logging;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Composing;
|
||||
@@ -20,13 +17,21 @@ namespace Umbraco.Cms.Core.Composing;
|
||||
/// </remarks>
|
||||
public sealed class TypeLoader
|
||||
{
|
||||
private readonly Lock _locko = new();
|
||||
private readonly Lock _typesLock = new();
|
||||
private readonly ILogger<TypeLoader> _logger;
|
||||
|
||||
private readonly Dictionary<CompositeTypeTypeKey, TypeList> _types = new();
|
||||
|
||||
private IEnumerable<Assembly>? _assemblies;
|
||||
|
||||
private bool IsDebugEnabled => _logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TypeLoader"/> class.
|
||||
/// </summary>
|
||||
/// <param name="typeFinder">The type finder used to discover types.</param>
|
||||
/// <param name="logger">The logger instance.</param>
|
||||
/// <param name="assembliesToScan">Optional set of assemblies to scan.</param>
|
||||
public TypeLoader(
|
||||
ITypeFinder typeFinder,
|
||||
ILogger<TypeLoader> logger,
|
||||
@@ -105,10 +110,7 @@ public sealed class TypeLoader
|
||||
/// <remarks>Caching is disabled when using specific assemblies.</remarks>
|
||||
public IEnumerable<Type> GetTypes<T>(bool cache = true, IEnumerable<Assembly>? specificAssemblies = null)
|
||||
{
|
||||
if (_logger == null)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot get types from a test/blank type loader.");
|
||||
}
|
||||
EnsureInitialized();
|
||||
|
||||
// do not cache anything from specific assemblies
|
||||
cache &= specificAssemblies == null;
|
||||
@@ -116,14 +118,11 @@ public sealed class TypeLoader
|
||||
// if not IDiscoverable, directly get types
|
||||
if (!typeof(IDiscoverable).IsAssignableFrom(typeof(T)))
|
||||
{
|
||||
// warn
|
||||
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Running a full, " + (cache ? string.Empty : "non-") +
|
||||
"cached, scan for non-discoverable type {TypeName} (slow).",
|
||||
LogDebugIf(
|
||||
true,
|
||||
"Running a full, {CacheStatus}cached, scan for non-discoverable type {TypeName} (slow).",
|
||||
CacheStatus(cache),
|
||||
typeof(T).FullName);
|
||||
}
|
||||
|
||||
return GetTypesInternal(
|
||||
typeof(T),
|
||||
@@ -134,23 +133,12 @@ public sealed class TypeLoader
|
||||
}
|
||||
|
||||
// get IDiscoverable and always cache
|
||||
IEnumerable<Type> discovered = GetTypesInternal(
|
||||
typeof(IDiscoverable),
|
||||
null,
|
||||
() => TypeFinder.FindClassesOfType<IDiscoverable>(AssembliesToScan),
|
||||
"scanning assemblies",
|
||||
true);
|
||||
IEnumerable<Type> discovered = GetDiscoverableTypes();
|
||||
|
||||
// warn
|
||||
if (!cache)
|
||||
{
|
||||
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Running a non-cached, filter for discoverable type {TypeName} (slowish).",
|
||||
typeof(T).FullName);
|
||||
}
|
||||
}
|
||||
LogDebugIf(
|
||||
!cache,
|
||||
"Running a non-cached, filter for discoverable type {TypeName} (slowish).",
|
||||
typeof(T).FullName);
|
||||
|
||||
// filter the cached discovered types (and maybe cache the result)
|
||||
return GetTypesInternal(
|
||||
@@ -175,10 +163,7 @@ public sealed class TypeLoader
|
||||
IEnumerable<Assembly>? specificAssemblies = null)
|
||||
where TAttribute : Attribute
|
||||
{
|
||||
if (_logger == null)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot get types from a test/blank type loader.");
|
||||
}
|
||||
EnsureInitialized();
|
||||
|
||||
// do not cache anything from specific assemblies
|
||||
cache &= specificAssemblies == null;
|
||||
@@ -186,14 +171,12 @@ public sealed class TypeLoader
|
||||
// if not IDiscoverable, directly get types
|
||||
if (!typeof(IDiscoverable).IsAssignableFrom(typeof(T)))
|
||||
{
|
||||
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Running a full, " + (cache ? string.Empty : "non-") +
|
||||
"cached, scan for non-discoverable type {TypeName} / attribute {AttributeName} (slow).",
|
||||
LogDebugIf(
|
||||
true,
|
||||
"Running a full, {CacheStatus}cached, scan for non-discoverable type {TypeName} / attribute {AttributeName} (slow).",
|
||||
CacheStatus(cache),
|
||||
typeof(T).FullName,
|
||||
typeof(TAttribute).FullName);
|
||||
}
|
||||
|
||||
return GetTypesInternal(
|
||||
typeof(T),
|
||||
@@ -204,24 +187,13 @@ public sealed class TypeLoader
|
||||
}
|
||||
|
||||
// get IDiscoverable and always cache
|
||||
IEnumerable<Type> discovered = GetTypesInternal(
|
||||
typeof(IDiscoverable),
|
||||
null,
|
||||
() => TypeFinder.FindClassesOfType<IDiscoverable>(AssembliesToScan),
|
||||
"scanning assemblies",
|
||||
true);
|
||||
IEnumerable<Type> discovered = GetDiscoverableTypes();
|
||||
|
||||
// warn
|
||||
if (!cache)
|
||||
{
|
||||
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Running a non-cached, filter for discoverable type {TypeName} / attribute {AttributeName} (slowish).",
|
||||
typeof(T).FullName,
|
||||
typeof(TAttribute).FullName);
|
||||
}
|
||||
}
|
||||
LogDebugIf(
|
||||
!cache,
|
||||
"Running a non-cached, filter for discoverable type {TypeName} / attribute {AttributeName} (slowish).",
|
||||
typeof(T).FullName,
|
||||
typeof(TAttribute).FullName);
|
||||
|
||||
// filter the cached discovered types (and maybe cache the result)
|
||||
return GetTypesInternal(
|
||||
@@ -247,23 +219,15 @@ public sealed class TypeLoader
|
||||
IEnumerable<Assembly>? specificAssemblies = null)
|
||||
where TAttribute : Attribute
|
||||
{
|
||||
if (_logger == null)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot get types from a test/blank type loader.");
|
||||
}
|
||||
EnsureInitialized();
|
||||
|
||||
// do not cache anything from specific assemblies
|
||||
cache &= specificAssemblies == null;
|
||||
|
||||
if (!cache)
|
||||
{
|
||||
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Running a full, non-cached, scan for types / attribute {AttributeName} (slow).",
|
||||
typeof(TAttribute).FullName);
|
||||
}
|
||||
}
|
||||
LogDebugIf(
|
||||
!cache,
|
||||
"Running a full, non-cached, scan for types / attribute {AttributeName} (slow).",
|
||||
typeof(TAttribute).FullName);
|
||||
|
||||
return GetTypesInternal(
|
||||
typeof(object),
|
||||
@@ -280,6 +244,38 @@ public sealed class TypeLoader
|
||||
return s;
|
||||
}
|
||||
|
||||
private void EnsureInitialized()
|
||||
{
|
||||
if (_logger == null)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot get types from a test/blank type loader.");
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<Type> GetDiscoverableTypes() =>
|
||||
GetTypesInternal(
|
||||
typeof(IDiscoverable),
|
||||
null,
|
||||
() => TypeFinder.FindClassesOfType<IDiscoverable>(AssembliesToScan),
|
||||
"scanning assemblies",
|
||||
true);
|
||||
|
||||
/// <summary>
|
||||
/// Logs a debug message if the specified condition is true and debug logging is enabled.
|
||||
/// </summary>
|
||||
/// <param name="condition">The condition that must be true to log.</param>
|
||||
/// <param name="message">The log message template.</param>
|
||||
/// <param name="args">The message arguments.</param>
|
||||
private void LogDebugIf(bool condition, string message, params object?[] args)
|
||||
{
|
||||
if (condition && IsDebugEnabled)
|
||||
{
|
||||
_logger.LogDebug(message, args);
|
||||
}
|
||||
}
|
||||
|
||||
private string CacheStatus(bool cache) => cache ? string.Empty : "non-";
|
||||
|
||||
private IEnumerable<Type> GetTypesInternal(
|
||||
Type baseType,
|
||||
Type? attributeType,
|
||||
@@ -291,7 +287,7 @@ public sealed class TypeLoader
|
||||
// lock at a time, and we don't have non-upgradeable readers, and quite probably the type
|
||||
// loader is mostly not going to be used in any kind of massively multi-threaded scenario - so,
|
||||
// a plain lock is enough
|
||||
lock (_locko)
|
||||
lock (_typesLock)
|
||||
{
|
||||
return GetTypesInternalLocked(baseType, attributeType, finder, action, cache);
|
||||
}
|
||||
@@ -305,34 +301,21 @@ public sealed class TypeLoader
|
||||
bool cache)
|
||||
{
|
||||
// check if the TypeList already exists, if so return it, if not we'll create it
|
||||
Type tobject = typeof(object); // CompositeTypeTypeKey does not support null values
|
||||
var listKey = new CompositeTypeTypeKey(baseType ?? tobject, attributeType ?? tobject);
|
||||
TypeList? typeList = null;
|
||||
Type objectType = typeof(object); // CompositeTypeTypeKey does not support null values
|
||||
var listKey = new CompositeTypeTypeKey(baseType ?? objectType, attributeType ?? objectType);
|
||||
|
||||
if (cache)
|
||||
// need to put some logging here to try to figure out why this is happening: http://issues.umbraco.org/issue/U4-3505
|
||||
if (cache && _types.TryGetValue(listKey, out TypeList? cachedList))
|
||||
{
|
||||
_types.TryGetValue(listKey, out typeList); // else null
|
||||
LogDebugIf(true, "Getting {TypeName}: found a cached type list.", GetName(baseType, attributeType));
|
||||
return cachedList.Types;
|
||||
}
|
||||
|
||||
// if caching and found, return
|
||||
if (typeList != null)
|
||||
{
|
||||
// need to put some logging here to try to figure out why this is happening: http://issues.umbraco.org/issue/U4-3505
|
||||
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Getting {TypeName}: found a cached type list.", GetName(baseType, attributeType));
|
||||
}
|
||||
return typeList.Types;
|
||||
}
|
||||
|
||||
// else proceed,
|
||||
typeList = new TypeList(baseType, attributeType);
|
||||
// else proceed
|
||||
var typeList = new TypeList(baseType, attributeType);
|
||||
|
||||
// either we had to scan, or we could not get the types from the cache file - scan now
|
||||
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Getting {TypeName}: " + action + ".", GetName(baseType, attributeType));
|
||||
}
|
||||
LogDebugIf(true, "Getting {TypeName}: " + action + ".", GetName(baseType, attributeType));
|
||||
|
||||
foreach (Type t in finder())
|
||||
{
|
||||
@@ -343,17 +326,11 @@ public sealed class TypeLoader
|
||||
if (cache)
|
||||
{
|
||||
var added = _types.TryAdd(listKey, typeList);
|
||||
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Got {TypeName}, caching ({CacheType}).", GetName(baseType, attributeType), added.ToString().ToLowerInvariant());
|
||||
}
|
||||
LogDebugIf(true, "Got {TypeName}, caching ({CacheType}).", GetName(baseType, attributeType), added.ToString().ToLowerInvariant());
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Got {TypeName}.", GetName(baseType, attributeType));
|
||||
}
|
||||
LogDebugIf(true, "Got {TypeName}.", GetName(baseType, attributeType));
|
||||
}
|
||||
|
||||
return typeList.Types;
|
||||
@@ -371,14 +348,25 @@ public sealed class TypeLoader
|
||||
{
|
||||
private readonly HashSet<Type> _types = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TypeList"/> class.
|
||||
/// </summary>
|
||||
/// <param name="baseType">The base type to filter by.</param>
|
||||
/// <param name="attributeType">The attribute type to filter by.</param>
|
||||
public TypeList(Type? baseType, Type? attributeType)
|
||||
{
|
||||
BaseType = baseType;
|
||||
AttributeType = attributeType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the base type used for filtering.
|
||||
/// </summary>
|
||||
public Type? BaseType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the attribute type used for filtering.
|
||||
/// </summary>
|
||||
public Type? AttributeType { get; }
|
||||
|
||||
/// <summary>
|
||||
@@ -402,59 +390,5 @@ public sealed class TypeLoader
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the error that occurs when a type was not found in the cache type list with the specified
|
||||
/// TypeResolutionKind.
|
||||
/// </summary>
|
||||
/// <seealso cref="System.Exception" />
|
||||
[Serializable]
|
||||
internal sealed class CachedTypeNotFoundInFileException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CachedTypeNotFoundInFileException" /> class.
|
||||
/// </summary>
|
||||
public CachedTypeNotFoundInFileException()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CachedTypeNotFoundInFileException" /> class.
|
||||
/// </summary>
|
||||
/// <param name="message">The message that describes the error.</param>
|
||||
public CachedTypeNotFoundInFileException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CachedTypeNotFoundInFileException" /> class.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message that explains the reason for the exception.</param>
|
||||
/// <param name="innerException">
|
||||
/// The exception that is the cause of the current exception, or a null reference (
|
||||
/// <see langword="Nothing" /> in Visual Basic) if no inner exception is specified.
|
||||
/// </param>
|
||||
public CachedTypeNotFoundInFileException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CachedTypeNotFoundInFileException" /> class.
|
||||
/// </summary>
|
||||
/// <param name="info">
|
||||
/// The <see cref="T:System.Runtime.Serialization.SerializationInfo" /> that holds the serialized object
|
||||
/// data about the exception being thrown.
|
||||
/// </param>
|
||||
/// <param name="context">
|
||||
/// The <see cref="T:System.Runtime.Serialization.StreamingContext" /> that contains contextual
|
||||
/// information about the source or destination.
|
||||
/// </param>
|
||||
protected CachedTypeNotFoundInFileException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -10,9 +10,12 @@ namespace Umbraco.Cms.Core.Configuration.Models;
|
||||
/// </summary>
|
||||
public class ContentImagingSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// The default set of accepted image file extensions.
|
||||
/// </summary>
|
||||
internal const string StaticImageFileTypes = "jpeg,jpg,gif,bmp,png,tiff,tif,webp";
|
||||
|
||||
private static readonly ISet<ImagingAutoFillUploadField> DefaultImagingAutoFillUploadField = new HashSet<ImagingAutoFillUploadField>
|
||||
private static readonly ISet<ImagingAutoFillUploadField> _defaultImagingAutoFillUploadField = new HashSet<ImagingAutoFillUploadField>
|
||||
{
|
||||
new()
|
||||
{
|
||||
@@ -31,8 +34,7 @@ public class ContentImagingSettings
|
||||
public ISet<string> ImageFileTypes { get; set; } = new HashSet<string>(StaticImageFileTypes.Split(Constants.CharArrays.Comma));
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value for the imaging autofill following media file upload fields.
|
||||
/// Gets or sets the collection of media property mappings that are automatically populated with image metadata after a media file is uploaded.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
public ISet<ImagingAutoFillUploadField> AutoFillImageProperties { get; set; } = DefaultImagingAutoFillUploadField;
|
||||
public ISet<ImagingAutoFillUploadField> AutoFillImageProperties { get; set; } = _defaultImagingAutoFillUploadField;
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ public class AuthorizationException : Exception
|
||||
/// The <see cref="T:System.Runtime.Serialization.StreamingContext" /> that contains contextual
|
||||
/// information about the source or destination.
|
||||
/// </param>
|
||||
[Obsolete("Constructors taking a signature of SerializationInfo info, StreamingContext context are deprecated and not used within Umbraco. Scheduled for removal in Umbraco 19.", DiagnosticId = "SYSLIB0051")]
|
||||
protected AuthorizationException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context)
|
||||
{
|
||||
|
||||
@@ -23,7 +23,7 @@ public class BootFailedException : Exception
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Exception" /> class with a specified error message.
|
||||
/// Initializes a new instance of the <see cref="BootFailedException" /> class with a specified error message.
|
||||
/// </summary>
|
||||
/// <param name="message">The message that describes the error.</param>
|
||||
public BootFailedException(string message)
|
||||
@@ -32,7 +32,7 @@ public class BootFailedException : Exception
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Exception" /> class with a specified error message
|
||||
/// Initializes a new instance of the <see cref="BootFailedException" /> class with a specified error message
|
||||
/// and a reference to the inner exception which is the cause of this exception.
|
||||
/// </summary>
|
||||
/// <param name="message">The message that describes the error.</param>
|
||||
@@ -53,6 +53,7 @@ public class BootFailedException : Exception
|
||||
/// The <see cref="T:System.Runtime.Serialization.StreamingContext" /> that contains contextual
|
||||
/// information about the source or destination.
|
||||
/// </param>
|
||||
[Obsolete("Constructors taking a signature of SerializationInfo info, StreamingContext context are deprecated and not used within Umbraco. Scheduled for removal in Umbraco 19.", DiagnosticId = "SYSLIB0051")]
|
||||
protected BootFailedException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context)
|
||||
{
|
||||
@@ -61,9 +62,8 @@ public class BootFailedException : Exception
|
||||
/// <summary>
|
||||
/// Rethrows a captured <see cref="BootFailedException" />.
|
||||
/// </summary>
|
||||
/// <param name="bootFailedException">The boot failed exception.</param>
|
||||
/// <exception cref="BootFailedException">
|
||||
/// </exception>
|
||||
/// <param name="bootFailedException">The boot failed exception to rethrow, or null to throw with a default message.</param>
|
||||
/// <exception cref="BootFailedException">Always thrown with the original exception details or a default message.</exception>
|
||||
/// <remarks>
|
||||
/// The exception can be null, in which case a default message is used.
|
||||
/// </remarks>
|
||||
|
||||
@@ -40,6 +40,7 @@ public class ConfigurationException : Exception
|
||||
/// The <see cref="T:System.Runtime.Serialization.StreamingContext" /> that contains contextual
|
||||
/// information about the source or destination.
|
||||
/// </param>
|
||||
[Obsolete("Constructors taking a signature of SerializationInfo info, StreamingContext context are deprecated and not used within Umbraco. Scheduled for removal in Umbraco 19.", DiagnosticId = "SYSLIB0051")]
|
||||
protected ConfigurationException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context)
|
||||
{
|
||||
|
||||
@@ -50,6 +50,7 @@ public class PanicException : Exception
|
||||
/// The <see cref="T:System.Runtime.Serialization.StreamingContext" /> that contains contextual
|
||||
/// information about the source or destination.
|
||||
/// </param>
|
||||
[Obsolete("Constructors taking a signature of SerializationInfo info, StreamingContext context are deprecated and not used within Umbraco. Scheduled for removal in Umbraco 19.", DiagnosticId = "SYSLIB0051")]
|
||||
protected PanicException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context)
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace Umbraco.Cms.Core.Exceptions;
|
||||
/// <summary>
|
||||
/// An exception that is thrown if an unattended installation occurs.
|
||||
/// </summary>
|
||||
/// <seealso cref="System.Exception" />
|
||||
[Serializable]
|
||||
public class UnattendedInstallException : Exception
|
||||
{
|
||||
@@ -46,6 +47,7 @@ public class UnattendedInstallException : Exception
|
||||
/// The <see cref="T:System.Runtime.Serialization.StreamingContext" /> that contains contextual
|
||||
/// information about the source or destination.
|
||||
/// </param>
|
||||
[Obsolete("Constructors taking a signature of SerializationInfo info, StreamingContext context are deprecated and not used within Umbraco. Scheduled for removal in Umbraco 19.", DiagnosticId = "SYSLIB0051")]
|
||||
protected UnattendedInstallException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context)
|
||||
{
|
||||
|
||||
@@ -15,6 +15,10 @@ using Umbraco.Cms.Core.Services.Navigation;
|
||||
|
||||
namespace Umbraco.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for <see cref="IPublishedContent"/> to navigate content trees,
|
||||
/// access properties, and retrieve related content such as ancestors, descendants, siblings, and children.
|
||||
/// </summary>
|
||||
public static class PublishedContentExtensions
|
||||
{
|
||||
#region Name
|
||||
@@ -267,9 +271,26 @@ public static class PublishedContentExtensions
|
||||
return template?.Alias ?? string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a specific template is allowed for the content item.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="contentTypeService">The content type service.</param>
|
||||
/// <param name="webRoutingSettings">The web routing settings.</param>
|
||||
/// <param name="templateId">The template identifier.</param>
|
||||
/// <returns><c>true</c> if the template is allowed; otherwise, <c>false</c>.</returns>
|
||||
public static bool IsAllowedTemplate(this IPublishedContent content, IContentTypeService contentTypeService, WebRoutingSettings webRoutingSettings, int templateId) =>
|
||||
content.IsAllowedTemplate(contentTypeService, webRoutingSettings.DisableAlternativeTemplates, webRoutingSettings.ValidateAlternativeTemplates, templateId);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a specific template is allowed for the content item.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</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="templateId">The template identifier.</param>
|
||||
/// <returns><c>true</c> if the template is allowed; otherwise, <c>false</c>.</returns>
|
||||
public static bool IsAllowedTemplate(this IPublishedContent content, IContentTypeService contentTypeService, bool disableAlternativeTemplates, bool validateAlternativeTemplates, int templateId)
|
||||
{
|
||||
if (disableAlternativeTemplates)
|
||||
@@ -292,6 +313,16 @@ public static class PublishedContentExtensions
|
||||
return publishedContentContentType.IsAllowedTemplate(templateId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a specific template is allowed for the content item by template alias.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</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, IFileService fileService, IContentTypeService contentTypeService, bool disableAlternativeTemplates, bool validateAlternativeTemplates, string templateAlias)
|
||||
{
|
||||
ITemplate? template = fileService.GetTemplate(templateAlias);
|
||||
@@ -443,8 +474,20 @@ public static class PublishedContentExtensions
|
||||
|
||||
#region IsSomething: equality
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this content item is equal to another content item by comparing their IDs.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="other">The other content item to compare.</param>
|
||||
/// <returns><c>true</c> if both content items have the same ID; otherwise, <c>false</c>.</returns>
|
||||
public static bool IsEqual(this IPublishedContent content, IPublishedContent other) => content.Id == other.Id;
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this content item is not equal to another content item by comparing their IDs.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="other">The other content item to compare.</param>
|
||||
/// <returns><c>true</c> if the content items have different IDs; otherwise, <c>false</c>.</returns>
|
||||
public static bool IsNotEqual(this IPublishedContent content, IPublishedContent other) =>
|
||||
content.IsEqual(other) == false;
|
||||
|
||||
@@ -452,15 +495,39 @@ public static class PublishedContentExtensions
|
||||
|
||||
#region IsSomething: ancestors and descendants
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this content item is a descendant of another content item.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="other">The potential ancestor content item.</param>
|
||||
/// <returns><c>true</c> if this content is a descendant of the other; otherwise, <c>false</c>.</returns>
|
||||
public static bool IsDescendant(this IPublishedContent content, IPublishedContent other) =>
|
||||
other.Level < content.Level && content.Path.InvariantStartsWith(other.Path.EnsureEndsWith(','));
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this content item is a descendant of or the same as another content item.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="other">The potential ancestor or same content item.</param>
|
||||
/// <returns><c>true</c> if this content is a descendant of or equal to the other; otherwise, <c>false</c>.</returns>
|
||||
public static bool IsDescendantOrSelf(this IPublishedContent content, IPublishedContent other) =>
|
||||
content.Path.InvariantEquals(other.Path) || content.IsDescendant(other);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this content item is an ancestor of another content item.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="other">The potential descendant content item.</param>
|
||||
/// <returns><c>true</c> if this content is an ancestor of the other; otherwise, <c>false</c>.</returns>
|
||||
public static bool IsAncestor(this IPublishedContent content, IPublishedContent other) =>
|
||||
content.Level < other.Level && other.Path.InvariantStartsWith(content.Path.EnsureEndsWith(','));
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this content item is an ancestor of or the same as another content item.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="other">The potential descendant or same content item.</param>
|
||||
/// <returns><c>true</c> if this content is an ancestor of or equal to the other; otherwise, <c>false</c>.</returns>
|
||||
public static bool IsAncestorOrSelf(this IPublishedContent content, IPublishedContent other) =>
|
||||
other.Path.InvariantEquals(content.Path) || content.IsAncestor(other);
|
||||
|
||||
@@ -571,7 +638,8 @@ public static class PublishedContentExtensions
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The content type.</typeparam>
|
||||
/// <param name="content">The content.</param>
|
||||
/// <param name="publishedStatusFilteringService"></param>
|
||||
/// <param name="navigationQueryService">The query service for the in-memory navigation structure.</param>
|
||||
/// <param name="publishedStatusFilteringService">The service for filtering published content by status.</param>
|
||||
/// <param name="maxLevel">The level.</param>
|
||||
/// <returns>
|
||||
/// The ancestors of the content, at a level lesser or equal to the specified level, and of the specified
|
||||
@@ -871,6 +939,15 @@ public static class PublishedContentExtensions
|
||||
where T : class, IPublishedContent
|
||||
=> content.AncestorsOrSelf<T>(navigationQueryService, publishedStatusFilteringService, maxLevel).FirstOrDefault();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ancestors or self of the content item, optionally filtered by a predicate.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="navigationQueryService">The navigation query service.</param>
|
||||
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
|
||||
/// <param name="orSelf">Whether to include the content item itself.</param>
|
||||
/// <param name="func">An optional predicate to filter the ancestors.</param>
|
||||
/// <returns>An enumerable of ancestors or self matching the criteria.</returns>
|
||||
public static IEnumerable<IPublishedContent> AncestorsOrSelf(
|
||||
this IPublishedContent content,
|
||||
INavigationQueryService navigationQueryService,
|
||||
@@ -984,6 +1061,15 @@ public static class PublishedContentExtensions
|
||||
// - every node occurs before all of its children and descendants.
|
||||
// - the relative order of siblings is the order in which they occur in the children property of their parent node.
|
||||
// - children and descendants occur before following siblings.
|
||||
|
||||
/// <summary>
|
||||
/// Gets all descendants of the content item.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="navigationQueryService">The navigation query service.</param>
|
||||
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
|
||||
/// <param name="culture">The culture for variant content.</param>
|
||||
/// <returns>An enumerable of all descendant content items.</returns>
|
||||
public static IEnumerable<IPublishedContent> Descendants(
|
||||
this IPublishedContent content,
|
||||
INavigationQueryService navigationQueryService,
|
||||
@@ -991,6 +1077,15 @@ public static class PublishedContentExtensions
|
||||
string? culture = null)
|
||||
=> content.DescendantsOrSelf(navigationQueryService, publishedStatusFilteringService, false, null, culture);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all descendants of the content item at or above a specified level.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="navigationQueryService">The navigation query service.</param>
|
||||
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
|
||||
/// <param name="level">The minimum level of descendants to return.</param>
|
||||
/// <param name="culture">The culture for variant content.</param>
|
||||
/// <returns>An enumerable of descendant content items at or above the specified level.</returns>
|
||||
public static IEnumerable<IPublishedContent> Descendants(
|
||||
this IPublishedContent content,
|
||||
INavigationQueryService navigationQueryService,
|
||||
@@ -1004,6 +1099,15 @@ public static class PublishedContentExtensions
|
||||
p => p.Level >= level,
|
||||
culture);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all descendants of the content item of a specific content type.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="navigationQueryService">The navigation query service.</param>
|
||||
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
|
||||
/// <param name="contentTypeAlias">The content type alias to filter by.</param>
|
||||
/// <param name="culture">The culture for variant content.</param>
|
||||
/// <returns>An enumerable of descendant content items of the specified type.</returns>
|
||||
public static IEnumerable<IPublishedContent> DescendantsOfType(
|
||||
this IPublishedContent content,
|
||||
INavigationQueryService navigationQueryService,
|
||||
@@ -1017,6 +1121,15 @@ public static class PublishedContentExtensions
|
||||
false,
|
||||
contentTypeAlias);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all descendants of the content item of a specific type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of content to return.</typeparam>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="navigationQueryService">The navigation query service.</param>
|
||||
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
|
||||
/// <param name="culture">The culture for variant content.</param>
|
||||
/// <returns>An enumerable of descendant content items of the specified type.</returns>
|
||||
public static IEnumerable<T> Descendants<T>(
|
||||
this IPublishedContent content,
|
||||
INavigationQueryService navigationQueryService,
|
||||
@@ -1025,6 +1138,16 @@ public static class PublishedContentExtensions
|
||||
where T : class, IPublishedContent
|
||||
=> content.Descendants(navigationQueryService, publishedStatusFilteringService, culture).OfType<T>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets all descendants of the content item of a specific type at or above a specified level.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of content to return.</typeparam>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="navigationQueryService">The navigation query service.</param>
|
||||
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
|
||||
/// <param name="level">The minimum level of descendants to return.</param>
|
||||
/// <param name="culture">The culture for variant content.</param>
|
||||
/// <returns>An enumerable of descendant content items of the specified type at or above the level.</returns>
|
||||
public static IEnumerable<T> Descendants<T>(
|
||||
this IPublishedContent content,
|
||||
INavigationQueryService navigationQueryService,
|
||||
@@ -1034,6 +1157,14 @@ public static class PublishedContentExtensions
|
||||
where T : class, IPublishedContent
|
||||
=> content.Descendants(navigationQueryService, publishedStatusFilteringService, level, culture).OfType<T>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets all descendants of the content item including itself.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="navigationQueryService">The navigation query service.</param>
|
||||
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
|
||||
/// <param name="culture">The culture for variant content.</param>
|
||||
/// <returns>An enumerable of the content item and all its descendants.</returns>
|
||||
public static IEnumerable<IPublishedContent> DescendantsOrSelf(
|
||||
this IPublishedContent content,
|
||||
INavigationQueryService navigationQueryService,
|
||||
@@ -1041,6 +1172,15 @@ public static class PublishedContentExtensions
|
||||
string? culture = null)
|
||||
=> content.DescendantsOrSelf(navigationQueryService, publishedStatusFilteringService, true, null, culture);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all descendants of the content item including itself at or above a specified level.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="navigationQueryService">The navigation query service.</param>
|
||||
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
|
||||
/// <param name="level">The minimum level to return.</param>
|
||||
/// <param name="culture">The culture for variant content.</param>
|
||||
/// <returns>An enumerable of the content item and all its descendants at or above the level.</returns>
|
||||
public static IEnumerable<IPublishedContent> DescendantsOrSelf(
|
||||
this IPublishedContent content,
|
||||
INavigationQueryService navigationQueryService,
|
||||
@@ -1049,6 +1189,15 @@ public static class PublishedContentExtensions
|
||||
string? culture = null)
|
||||
=> content.DescendantsOrSelf(navigationQueryService, publishedStatusFilteringService, true, p => p.Level >= level, culture);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all descendants of the content item including itself of a specific content type.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="navigationQueryService">The navigation query service.</param>
|
||||
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
|
||||
/// <param name="contentTypeAlias">The content type alias to filter by.</param>
|
||||
/// <param name="culture">The culture for variant content.</param>
|
||||
/// <returns>An enumerable of the content item and its descendants of the specified type.</returns>
|
||||
public static IEnumerable<IPublishedContent> DescendantsOrSelfOfType(
|
||||
this IPublishedContent content,
|
||||
INavigationQueryService navigationQueryService,
|
||||
@@ -1062,6 +1211,15 @@ public static class PublishedContentExtensions
|
||||
true,
|
||||
contentTypeAlias);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all descendants of the content item including itself of a specific type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of content to return.</typeparam>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="navigationQueryService">The navigation query service.</param>
|
||||
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
|
||||
/// <param name="culture">The culture for variant content.</param>
|
||||
/// <returns>An enumerable of the content item and its descendants of the specified type.</returns>
|
||||
public static IEnumerable<T> DescendantsOrSelf<T>(
|
||||
this IPublishedContent content,
|
||||
INavigationQueryService navigationQueryService,
|
||||
@@ -1070,6 +1228,16 @@ public static class PublishedContentExtensions
|
||||
where T : class, IPublishedContent
|
||||
=> content.DescendantsOrSelf(navigationQueryService, publishedStatusFilteringService, culture).OfType<T>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets all descendants of the content item including itself of a specific type at or above a specified level.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of content to return.</typeparam>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="navigationQueryService">The navigation query service.</param>
|
||||
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
|
||||
/// <param name="level">The minimum level to return.</param>
|
||||
/// <param name="culture">The culture for variant content.</param>
|
||||
/// <returns>An enumerable of the content item and its descendants of the specified type at or above the level.</returns>
|
||||
public static IEnumerable<T> DescendantsOrSelf<T>(
|
||||
this IPublishedContent content,
|
||||
INavigationQueryService navigationQueryService,
|
||||
@@ -1079,6 +1247,14 @@ public static class PublishedContentExtensions
|
||||
where T : class, IPublishedContent
|
||||
=> content.DescendantsOrSelf(navigationQueryService, publishedStatusFilteringService, level, culture).OfType<T>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the first descendant of the content item.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="navigationQueryService">The navigation query service.</param>
|
||||
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
|
||||
/// <param name="culture">The culture for variant content.</param>
|
||||
/// <returns>The first descendant, or null if none exists.</returns>
|
||||
public static IPublishedContent? Descendant(
|
||||
this IPublishedContent content,
|
||||
INavigationQueryService navigationQueryService,
|
||||
@@ -1086,6 +1262,15 @@ public static class PublishedContentExtensions
|
||||
string? culture = null)
|
||||
=> content.Children(navigationQueryService, publishedStatusFilteringService, culture)?.FirstOrDefault();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the first descendant of the content item at a specified level.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="navigationQueryService">The navigation query service.</param>
|
||||
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
|
||||
/// <param name="level">The level to find a descendant at.</param>
|
||||
/// <param name="culture">The culture for variant content.</param>
|
||||
/// <returns>The first descendant at the specified level, or null if none exists.</returns>
|
||||
public static IPublishedContent? Descendant(
|
||||
this IPublishedContent content,
|
||||
INavigationQueryService navigationQueryService,
|
||||
@@ -1096,6 +1281,15 @@ public static class PublishedContentExtensions
|
||||
.EnumerateDescendants(navigationQueryService, publishedStatusFilteringService, false, culture)
|
||||
.FirstOrDefault(x => x.Level == level);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the first descendant of the content item of a specific content type.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="navigationQueryService">The navigation query service.</param>
|
||||
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
|
||||
/// <param name="contentTypeAlias">The content type alias to filter by.</param>
|
||||
/// <param name="culture">The culture for variant content.</param>
|
||||
/// <returns>The first descendant of the specified type, or null if none exists.</returns>
|
||||
public static IPublishedContent? DescendantOfType(
|
||||
this IPublishedContent content,
|
||||
INavigationQueryService navigationQueryService,
|
||||
@@ -1111,6 +1305,15 @@ public static class PublishedContentExtensions
|
||||
contentTypeAlias)
|
||||
.FirstOrDefault();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the first descendant of the content item of a specific type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of content to return.</typeparam>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="navigationQueryService">The navigation query service.</param>
|
||||
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
|
||||
/// <param name="culture">The culture for variant content.</param>
|
||||
/// <returns>The first descendant of the specified type, or null if none exists.</returns>
|
||||
public static T? Descendant<T>(
|
||||
this IPublishedContent content,
|
||||
INavigationQueryService navigationQueryService,
|
||||
@@ -1121,6 +1324,16 @@ public static class PublishedContentExtensions
|
||||
.EnumerateDescendants(navigationQueryService, publishedStatusFilteringService, false, culture)
|
||||
.FirstOrDefault(x => x is T) as T;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the first descendant of the content item of a specific type at a specified level.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of content to return.</typeparam>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="navigationQueryService">The navigation query service.</param>
|
||||
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
|
||||
/// <param name="level">The level to find a descendant at.</param>
|
||||
/// <param name="culture">The culture for variant content.</param>
|
||||
/// <returns>The first descendant of the specified type at the level, or null if none exists.</returns>
|
||||
public static T? Descendant<T>(
|
||||
this IPublishedContent content,
|
||||
INavigationQueryService navigationQueryService,
|
||||
@@ -1130,6 +1343,14 @@ public static class PublishedContentExtensions
|
||||
where T : class, IPublishedContent
|
||||
=> content.Descendant(navigationQueryService, publishedStatusFilteringService, level, culture) as T;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the first descendant or self of the content item.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="navigationQueryService">The navigation query service.</param>
|
||||
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
|
||||
/// <param name="culture">The culture for variant content.</param>
|
||||
/// <returns>The first descendant, or the content item itself if no descendants exist.</returns>
|
||||
public static IPublishedContent DescendantOrSelf(
|
||||
this IPublishedContent content,
|
||||
INavigationQueryService navigationQueryService,
|
||||
@@ -1143,6 +1364,15 @@ public static class PublishedContentExtensions
|
||||
.FirstOrDefault() ??
|
||||
content;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the first descendant or self of the content item at a specified level.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="navigationQueryService">The navigation query service.</param>
|
||||
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
|
||||
/// <param name="level">The level to find a descendant at.</param>
|
||||
/// <param name="culture">The culture for variant content.</param>
|
||||
/// <returns>The first descendant or self at the specified level, or null if none exists.</returns>
|
||||
public static IPublishedContent? DescendantOrSelf(
|
||||
this IPublishedContent content,
|
||||
INavigationQueryService navigationQueryService,
|
||||
@@ -1153,6 +1383,15 @@ public static class PublishedContentExtensions
|
||||
.EnumerateDescendants(navigationQueryService, publishedStatusFilteringService, true, culture)
|
||||
.FirstOrDefault(x => x.Level == level);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the first descendant or self of the content item of a specific content type.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="navigationQueryService">The navigation query service.</param>
|
||||
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
|
||||
/// <param name="contentTypeAlias">The content type alias to filter by.</param>
|
||||
/// <param name="culture">The culture for variant content.</param>
|
||||
/// <returns>The first descendant or self of the specified type, or null if none exists.</returns>
|
||||
public static IPublishedContent? DescendantOrSelfOfType(
|
||||
this IPublishedContent content,
|
||||
INavigationQueryService navigationQueryService,
|
||||
@@ -1168,6 +1407,15 @@ public static class PublishedContentExtensions
|
||||
contentTypeAlias)
|
||||
.FirstOrDefault();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the first descendant or self of the content item of a specific type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of content to return.</typeparam>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="navigationQueryService">The navigation query service.</param>
|
||||
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
|
||||
/// <param name="culture">The culture for variant content.</param>
|
||||
/// <returns>The first descendant or self of the specified type, or null if none exists.</returns>
|
||||
public static T? DescendantOrSelf<T>(
|
||||
this IPublishedContent content,
|
||||
INavigationQueryService navigationQueryService,
|
||||
@@ -1178,6 +1426,16 @@ public static class PublishedContentExtensions
|
||||
.EnumerateDescendants(navigationQueryService, publishedStatusFilteringService, true, culture)
|
||||
.FirstOrDefault(x => x is T) as T;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the first descendant or self of the content item of a specific type at a specified level.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of content to return.</typeparam>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="navigationQueryService">The navigation query service.</param>
|
||||
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
|
||||
/// <param name="level">The level to find a descendant at.</param>
|
||||
/// <param name="culture">The culture for variant content.</param>
|
||||
/// <returns>The first descendant or self of the specified type at the level, or null if none exists.</returns>
|
||||
public static T? DescendantOrSelf<T>(
|
||||
this IPublishedContent content,
|
||||
INavigationQueryService navigationQueryService,
|
||||
@@ -1225,13 +1483,12 @@ public static class PublishedContentExtensions
|
||||
/// Gets the children of the content item.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="navigationQueryService"></param>
|
||||
/// <param name="publishedStatusFilteringService"></param>
|
||||
/// <param name="navigationQueryService">The navigation query service.</param>
|
||||
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
|
||||
/// <param name="culture">
|
||||
/// The specific culture to get the URL children for. Default is null which will use the current culture in
|
||||
/// <see cref="VariationContext" />
|
||||
/// </param>
|
||||
/// <param name="publishedCache"></param>
|
||||
/// <remarks>
|
||||
/// <para>Gets children that are available for the specified culture.</para>
|
||||
/// <para>Children are sorted by their sortOrder.</para>
|
||||
@@ -1260,14 +1517,13 @@ public static class PublishedContentExtensions
|
||||
/// Gets the children of the content, filtered by a predicate.
|
||||
/// </summary>
|
||||
/// <param name="content">The content.</param>
|
||||
/// <param name="navigationQueryService"></param>
|
||||
/// <param name="publishedStatusFilteringService"></param>
|
||||
/// <param name="navigationQueryService">The navigation query service.</param>
|
||||
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
|
||||
/// <param name="predicate">The predicate.</param>
|
||||
/// <param name="culture">
|
||||
/// The specific culture to filter for. If null is used the current culture is used. (Default is
|
||||
/// null)
|
||||
/// </param>
|
||||
/// <param name="publishedCache"></param>
|
||||
/// <returns>The children of the content, filtered by the predicate.</returns>
|
||||
/// <remarks>
|
||||
/// <para>Children are sorted by their sortOrder.</para>
|
||||
@@ -1307,14 +1563,12 @@ public static class PublishedContentExtensions
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The content type.</typeparam>
|
||||
/// <param name="content">The content.</param>
|
||||
/// <param name="variationContextAccessor">The accessor for the VariationContext</param>
|
||||
/// <param name="navigationQueryService"></param>
|
||||
/// <param name="publishStatusQueryService"></param>
|
||||
/// <param name="navigationQueryService">The query service for the in-memory navigation structure.</param>
|
||||
/// <param name="publishedStatusFilteringService">The service for filtering published content by status.</param>
|
||||
/// <param name="culture">
|
||||
/// The specific culture to filter for. If null is used the current culture is used. (Default is
|
||||
/// null)
|
||||
/// </param>
|
||||
/// <param name="publishedCache"></param>
|
||||
/// <returns>The children of content, of the given content type.</returns>
|
||||
/// <remarks>
|
||||
/// <para>Children are sorted by their sortOrder.</para>
|
||||
@@ -1327,6 +1581,14 @@ public static class PublishedContentExtensions
|
||||
where T : class, IPublishedContent
|
||||
=> content.Children(navigationQueryService, publishedStatusFilteringService, culture).OfType<T>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the first child of the content item.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="navigationQueryService">The navigation query service.</param>
|
||||
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
|
||||
/// <param name="culture">The culture for variant content.</param>
|
||||
/// <returns>The first child, or null if no children exist.</returns>
|
||||
public static IPublishedContent? FirstChild(
|
||||
this IPublishedContent content,
|
||||
INavigationQueryService navigationQueryService,
|
||||
@@ -1349,6 +1611,15 @@ public static class PublishedContentExtensions
|
||||
.ChildrenOfType(navigationQueryService, publishedStatusFilteringService, contentTypeAlias, culture)
|
||||
.FirstOrDefault();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the first child of the content item that matches a predicate.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="navigationQueryService">The navigation query service.</param>
|
||||
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
|
||||
/// <param name="predicate">The predicate to filter children.</param>
|
||||
/// <param name="culture">The culture for variant content.</param>
|
||||
/// <returns>The first child matching the predicate, or null if none exists.</returns>
|
||||
public static IPublishedContent? FirstChild(
|
||||
this IPublishedContent content,
|
||||
INavigationQueryService navigationQueryService,
|
||||
@@ -1359,6 +1630,15 @@ public static class PublishedContentExtensions
|
||||
.Children(navigationQueryService, publishedStatusFilteringService, predicate, culture)
|
||||
.FirstOrDefault();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the first child of the content item with a specific unique identifier.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="navigationQueryService">The navigation query service.</param>
|
||||
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
|
||||
/// <param name="uniqueId">The unique identifier of the child to find.</param>
|
||||
/// <param name="culture">The culture for variant content.</param>
|
||||
/// <returns>The first child with the specified unique identifier, or null if none exists.</returns>
|
||||
public static IPublishedContent? FirstChild(
|
||||
this IPublishedContent content,
|
||||
INavigationQueryService navigationQueryService,
|
||||
@@ -1369,6 +1649,15 @@ public static class PublishedContentExtensions
|
||||
.Children(navigationQueryService, publishedStatusFilteringService, x => x.Key == uniqueId, culture)
|
||||
.FirstOrDefault();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the first child of the content item of a specific type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of content to return.</typeparam>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="navigationQueryService">The navigation query service.</param>
|
||||
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
|
||||
/// <param name="culture">The culture for variant content.</param>
|
||||
/// <returns>The first child of the specified type, or null if none exists.</returns>
|
||||
public static T? FirstChild<T>(
|
||||
this IPublishedContent content,
|
||||
INavigationQueryService navigationQueryService,
|
||||
@@ -1379,6 +1668,16 @@ public static class PublishedContentExtensions
|
||||
.Children<T>(navigationQueryService, publishedStatusFilteringService, culture)
|
||||
.FirstOrDefault();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the first child of the content item of a specific type that matches a predicate.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of content to return.</typeparam>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="navigationQueryService">The navigation query service.</param>
|
||||
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
|
||||
/// <param name="predicate">The predicate to filter children.</param>
|
||||
/// <param name="culture">The culture for variant content.</param>
|
||||
/// <returns>The first child of the specified type matching the predicate, or null if none exists.</returns>
|
||||
public static T? FirstChild<T>(
|
||||
this IPublishedContent content,
|
||||
INavigationQueryService navigationQueryService,
|
||||
@@ -1579,12 +1878,24 @@ public static class PublishedContentExtensions
|
||||
|
||||
#region Writer and creator
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the user who created the content item.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="userService">The user service.</param>
|
||||
/// <returns>The name of the creator, or an empty string if not found.</returns>
|
||||
public static string GetCreatorName(this IPublishedContent content, IUserService userService)
|
||||
{
|
||||
IProfile? user = userService.GetProfileById(content.CreatorId);
|
||||
return user?.Name ?? string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the user who last updated the content item.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="userService">The user service.</param>
|
||||
/// <returns>The name of the writer, or an empty string if not found.</returns>
|
||||
public static string GetWriterName(this IPublishedContent content, IUserService userService)
|
||||
{
|
||||
IProfile? user = userService.GetProfileById(content.WriterId);
|
||||
@@ -1642,70 +1953,190 @@ public static class PublishedContentExtensions
|
||||
|
||||
#endregion
|
||||
|
||||
#region Convenience overloads (using service locator)
|
||||
|
||||
/// <summary>
|
||||
/// Gets an ancestor of the content item up to a maximum level.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="maxLevel">The maximum level to traverse.</param>
|
||||
/// <returns>The ancestor at or below the maximum level, or null if not found.</returns>
|
||||
public static IPublishedContent? Ancestor(this IPublishedContent content, int maxLevel)
|
||||
=> content.Ancestor(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), maxLevel);
|
||||
|
||||
/// <summary>
|
||||
/// Gets an ancestor of the content item of a specific content type.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="contentTypeAlias">The content type alias to find.</param>
|
||||
/// <returns>The first ancestor of the specified type, or null if not found.</returns>
|
||||
public static IPublishedContent? Ancestor(this IPublishedContent content, string contentTypeAlias)
|
||||
=> content.Ancestor(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias);
|
||||
|
||||
/// <summary>
|
||||
/// Gets an ancestor of the content item of a specific type up to a maximum level.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of content to return.</typeparam>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="maxLevel">The maximum level to traverse.</param>
|
||||
/// <returns>The ancestor of the specified type at or below the maximum level, or null if not found.</returns>
|
||||
public static T? Ancestor<T>(this IPublishedContent content, int maxLevel)
|
||||
where T : class, IPublishedContent
|
||||
=> content.Ancestor<T>(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), maxLevel);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ancestors of the content item up to a maximum level.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="maxLevel">The maximum level to traverse.</param>
|
||||
/// <returns>An enumerable of ancestors at or below the maximum level.</returns>
|
||||
public static IEnumerable<IPublishedContent> Ancestors(this IPublishedContent content, int maxLevel)
|
||||
=> content.Ancestors(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), maxLevel);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ancestors of the content item of a specific content type.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="contentTypeAlias">The content type alias to filter by.</param>
|
||||
/// <returns>An enumerable of ancestors of the specified type.</returns>
|
||||
public static IEnumerable<IPublishedContent> Ancestors(this IPublishedContent content, string contentTypeAlias)
|
||||
=> content.Ancestors(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ancestors of the content item of a specific type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of content to return.</typeparam>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <returns>An enumerable of ancestors of the specified type.</returns>
|
||||
public static IEnumerable<T> Ancestors<T>(this IPublishedContent content)
|
||||
where T : class, IPublishedContent
|
||||
=> content.Ancestors<T>(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content));
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ancestors of the content item of a specific type up to a maximum level.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of content to return.</typeparam>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="maxLevel">The maximum level to traverse.</param>
|
||||
/// <returns>An enumerable of ancestors of the specified type at or below the maximum level.</returns>
|
||||
public static IEnumerable<T> Ancestors<T>(this IPublishedContent content, int maxLevel)
|
||||
where T : class, IPublishedContent
|
||||
=> content.Ancestors<T>(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), maxLevel);
|
||||
|
||||
/// <summary>
|
||||
/// Gets an ancestor or self of the content item at or below a maximum level.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="maxLevel">The maximum level to traverse.</param>
|
||||
/// <returns>The content item or an ancestor at or below the maximum level.</returns>
|
||||
public static IPublishedContent AncestorOrSelf(this IPublishedContent content, int maxLevel)
|
||||
=> content.AncestorOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), maxLevel);
|
||||
|
||||
/// <summary>
|
||||
/// Gets an ancestor or self of the content item of a specific content type.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="contentTypeAlias">The content type alias to find.</param>
|
||||
/// <returns>The content item or an ancestor of the specified type.</returns>
|
||||
public static IPublishedContent AncestorOrSelf(this IPublishedContent content, string contentTypeAlias)
|
||||
=> content.AncestorOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias);
|
||||
|
||||
/// <summary>
|
||||
/// Gets an ancestor or self of the content item of a specific type up to a maximum level.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of content to return.</typeparam>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="maxLevel">The maximum level to traverse.</param>
|
||||
/// <returns>The content item or an ancestor of the specified type at or below the maximum level, or null if not found.</returns>
|
||||
public static T? AncestorOrSelf<T>(this IPublishedContent content, int maxLevel)
|
||||
where T : class, IPublishedContent
|
||||
=> content.AncestorOrSelf<T>(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), maxLevel);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ancestors or self of the content item up to a maximum level.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="maxLevel">The maximum level to traverse.</param>
|
||||
/// <returns>An enumerable of the content item and its ancestors at or below the maximum level.</returns>
|
||||
public static IEnumerable<IPublishedContent> AncestorsOrSelf(this IPublishedContent content, int maxLevel)
|
||||
=> content.AncestorsOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), maxLevel);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ancestors or self of the content item of a specific content type.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="contentTypeAlias">The content type alias to filter by.</param>
|
||||
/// <returns>An enumerable of the content item and its ancestors of the specified type.</returns>
|
||||
public static IEnumerable<IPublishedContent> AncestorsOrSelf(this IPublishedContent content, string contentTypeAlias)
|
||||
=> content.Ancestors(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ancestors or self of the content item of a specific type up to a maximum level.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of content to return.</typeparam>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="maxLevel">The maximum level to traverse.</param>
|
||||
/// <returns>An enumerable of the content item and its ancestors of the specified type at or below the maximum level.</returns>
|
||||
public static IEnumerable<T> AncestorsOrSelf<T>(this IPublishedContent content, int maxLevel)
|
||||
where T : class, IPublishedContent
|
||||
=> content.AncestorsOrSelf<T>(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), maxLevel);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ancestors or self of the content item, optionally filtered by a predicate.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="orSelf">Whether to include the content item itself.</param>
|
||||
/// <param name="func">An optional predicate to filter the ancestors.</param>
|
||||
/// <returns>An enumerable of the content item and/or its ancestors matching the criteria.</returns>
|
||||
public static IEnumerable<IPublishedContent> AncestorsOrSelf(this IPublishedContent content, bool orSelf, Func<IPublishedContent, bool>? func)
|
||||
=> content.AncestorsOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), orSelf, func);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the breadcrumbs (ancestors and self, top to bottom) for the content item.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="andSelf">Whether to include the content item itself. Default is true.</param>
|
||||
/// <returns>An enumerable of the breadcrumb trail from root to the content item.</returns>
|
||||
public static IEnumerable<IPublishedContent> Breadcrumbs(
|
||||
this IPublishedContent content,
|
||||
bool andSelf = true) =>
|
||||
content.Breadcrumbs(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), andSelf);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the breadcrumbs (ancestors and self, top to bottom) for the content item at or above a minimum level.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="minLevel">The minimum level to include in the breadcrumbs.</param>
|
||||
/// <param name="andSelf">Whether to include the content item itself. Default is true.</param>
|
||||
/// <returns>An enumerable of the breadcrumb trail from the minimum level to the content item.</returns>
|
||||
public static IEnumerable<IPublishedContent> Breadcrumbs(
|
||||
this IPublishedContent content,
|
||||
int minLevel,
|
||||
bool andSelf = true) =>
|
||||
content.Breadcrumbs(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), minLevel, andSelf);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the breadcrumbs (ancestors and self, top to bottom) for the content item of a specific type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of content to include in the breadcrumbs.</typeparam>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="andSelf">Whether to include the content item itself. Default is true.</param>
|
||||
/// <returns>An enumerable of the breadcrumb trail of the specified type.</returns>
|
||||
public static IEnumerable<IPublishedContent> Breadcrumbs<T>(
|
||||
this IPublishedContent content,
|
||||
bool andSelf = true)
|
||||
where T : class, IPublishedContent=>
|
||||
where T : class, IPublishedContent =>
|
||||
content.Breadcrumbs<T>(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), andSelf);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all descendants or self of a collection of content items of a specific content type.
|
||||
/// </summary>
|
||||
/// <param name="parentNodes">The collection of parent content items.</param>
|
||||
/// <param name="navigationQueryService">The navigation query service.</param>
|
||||
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
|
||||
/// <param name="docTypeAlias">The content type alias to filter by.</param>
|
||||
/// <param name="culture">The culture for variant content.</param>
|
||||
/// <returns>An enumerable of all content items and their descendants of the specified type.</returns>
|
||||
public static IEnumerable<IPublishedContent> DescendantsOrSelfOfType(
|
||||
this IEnumerable<IPublishedContent> parentNodes,
|
||||
INavigationQueryService navigationQueryService,
|
||||
@@ -1718,6 +2149,15 @@ public static class PublishedContentExtensions
|
||||
docTypeAlias,
|
||||
culture));
|
||||
|
||||
/// <summary>
|
||||
/// Gets all descendants or self of a collection of content items of a specific type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of content to return.</typeparam>
|
||||
/// <param name="parentNodes">The collection of parent content items.</param>
|
||||
/// <param name="navigationQueryService">The navigation query service.</param>
|
||||
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
|
||||
/// <param name="culture">The culture for variant content.</param>
|
||||
/// <returns>An enumerable of all content items and their descendants of the specified type.</returns>
|
||||
public static IEnumerable<T> DescendantsOrSelf<T>(
|
||||
this IEnumerable<IPublishedContent> parentNodes,
|
||||
INavigationQueryService navigationQueryService,
|
||||
@@ -1729,6 +2169,8 @@ public static class PublishedContentExtensions
|
||||
publishedStatusFilteringService,
|
||||
culture));
|
||||
|
||||
#endregion
|
||||
|
||||
private static INavigationQueryService GetNavigationQueryService(IPublishedContent content)
|
||||
{
|
||||
switch (content.ItemType)
|
||||
|
||||
@@ -758,13 +758,7 @@ public static class StringExtensions
|
||||
/// <returns>The hashed string</returns>
|
||||
private static string GenerateHash(this string str, string? hashType)
|
||||
{
|
||||
HashAlgorithm? hasher = null;
|
||||
|
||||
// create an instance of the correct hashing provider based on the type passed in
|
||||
if (hashType is not null)
|
||||
{
|
||||
hasher = HashAlgorithm.Create(hashType);
|
||||
}
|
||||
HashAlgorithm? hasher = CreateHashAlgorithm(hashType);
|
||||
|
||||
if (hasher == null)
|
||||
{
|
||||
@@ -794,6 +788,33 @@ public static class StringExtensions
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a hash algorithm instance by name.
|
||||
/// </summary>
|
||||
/// <param name="algorithmName">The algorithm name (e.g., "SHA1", "SHA256", "MD5").</param>
|
||||
/// <returns>A HashAlgorithm instance, or null if the algorithm is not recognized.</returns>
|
||||
private static HashAlgorithm? CreateHashAlgorithm(string? algorithmName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(algorithmName))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return algorithmName.ToUpperInvariant() switch
|
||||
{
|
||||
"SHA1" or "SHA-1" or "SYSTEM.SECURITY.CRYPTOGRAPHY.SHA1" => SHA1.Create(),
|
||||
"SHA256" or "SHA-256" or "SYSTEM.SECURITY.CRYPTOGRAPHY.SHA256" => SHA256.Create(),
|
||||
"SHA384" or "SHA-384" or "SYSTEM.SECURITY.CRYPTOGRAPHY.SHA384" => SHA384.Create(),
|
||||
"SHA512" or "SHA-512" or "SYSTEM.SECURITY.CRYPTOGRAPHY.SHA512" => SHA512.Create(),
|
||||
"MD5" or "SYSTEM.SECURITY.CRYPTOGRAPHY.MD5" => MD5.Create(),
|
||||
"HMACSHA1" or "SYSTEM.SECURITY.CRYPTOGRAPHY.HMACSHA1" => new HMACSHA1(),
|
||||
"HMACSHA256" or "SYSTEM.SECURITY.CRYPTOGRAPHY.HMACSHA256" => new HMACSHA256(),
|
||||
"HMACSHA384" or "SYSTEM.SECURITY.CRYPTOGRAPHY.HMACSHA384" => new HMACSHA384(),
|
||||
"HMACSHA512" or "SYSTEM.SECURITY.CRYPTOGRAPHY.HMACSHA512" => new HMACSHA512(),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encodes a string so that it is 'safe' for URLs, files, etc..
|
||||
/// </summary>
|
||||
|
||||
@@ -16,16 +16,39 @@ public class HashGenerator : DisposableObjectSlim
|
||||
private readonly MemoryStream _ms = new();
|
||||
private StreamWriter _writer;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HashGenerator"/> class.
|
||||
/// </summary>
|
||||
public HashGenerator() => _writer = new StreamWriter(_ms, Encoding.Unicode, 1024, true);
|
||||
|
||||
/// <summary>
|
||||
/// Adds an integer value to the hash computation.
|
||||
/// </summary>
|
||||
/// <param name="i">The integer value to add.</param>
|
||||
public void AddInt(int i) => _writer.Write(i);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a long value to the hash computation.
|
||||
/// </summary>
|
||||
/// <param name="i">The long value to add.</param>
|
||||
public void AddLong(long i) => _writer.Write(i);
|
||||
|
||||
/// <summary>
|
||||
/// Adds an object's string representation to the hash computation.
|
||||
/// </summary>
|
||||
/// <param name="o">The object to add.</param>
|
||||
public void AddObject(object o) => _writer.Write(o);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a DateTime value to the hash computation using its ticks.
|
||||
/// </summary>
|
||||
/// <param name="d">The DateTime value to add.</param>
|
||||
public void AddDateTime(DateTime d) => _writer.Write(d.Ticks);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a string to the hash computation.
|
||||
/// </summary>
|
||||
/// <param name="s">The string to add. If null, nothing is added.</param>
|
||||
public void AddString(string s)
|
||||
{
|
||||
if (s != null)
|
||||
@@ -34,6 +57,14 @@ public class HashGenerator : DisposableObjectSlim
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a string to the hash computation in a case-insensitive manner.
|
||||
/// </summary>
|
||||
/// <param name="s">The string to add. If null, nothing is added.</param>
|
||||
/// <remarks>
|
||||
/// The string is converted to uppercase before being added to ensure
|
||||
/// case-insensitive hash generation.
|
||||
/// </remarks>
|
||||
public void AddCaseInsensitiveString(string s)
|
||||
{
|
||||
// I've tried to no allocate a new string with this which can be done if we use the CompareInfo.GetSortKey method which will create a new
|
||||
@@ -48,6 +79,14 @@ public class HashGenerator : DisposableObjectSlim
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a file system item (file or directory) to the hash computation.
|
||||
/// </summary>
|
||||
/// <param name="f">The file system item to add.</param>
|
||||
/// <remarks>
|
||||
/// For files, adds the full name, creation time, last write time, and length.
|
||||
/// For directories, recursively adds all files and subdirectories.
|
||||
/// </remarks>
|
||||
public void AddFileSystemItem(FileSystemInfo f)
|
||||
{
|
||||
// if it doesn't exist, don't proceed.
|
||||
@@ -80,14 +119,27 @@ public class HashGenerator : DisposableObjectSlim
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a file to the hash computation.
|
||||
/// </summary>
|
||||
/// <param name="f">The file to add.</param>
|
||||
public void AddFile(FileInfo f) => AddFileSystemItem(f);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a folder and its contents to the hash computation.
|
||||
/// </summary>
|
||||
/// <param name="d">The directory to add.</param>
|
||||
public void AddFolder(DirectoryInfo d) => AddFileSystemItem(d);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the generated hash output of all added objects
|
||||
/// Returns the generated hash of all added objects.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <returns>A hexadecimal string representation of the computed hash.</returns>
|
||||
/// <remarks>
|
||||
/// Uses SHA1 when FIPS compliance is required, otherwise uses MD5.
|
||||
/// This method can be called multiple times; subsequent calls will include
|
||||
/// any objects added after the previous call.
|
||||
/// </remarks>
|
||||
public string GenerateHash()
|
||||
{
|
||||
// flush,close,dispose the writer,then create a new one since it's possible to keep adding after GenerateHash is called.
|
||||
@@ -96,10 +148,8 @@ public class HashGenerator : DisposableObjectSlim
|
||||
_writer.Dispose();
|
||||
_writer = new StreamWriter(_ms, Encoding.UTF8, 1024, true);
|
||||
|
||||
var hashType = CryptoConfig.AllowOnlyFipsAlgorithms ? "SHA1" : "MD5";
|
||||
|
||||
// create an instance of the correct hashing provider based on the type passed in
|
||||
HashAlgorithm hasher = HashAlgorithm.Create(hashType) ?? throw new InvalidOperationException("No hashing type found by name " + hashType);
|
||||
// Use SHA1 for FIPS compliance, otherwise MD5
|
||||
HashAlgorithm hasher = CryptoConfig.AllowOnlyFipsAlgorithms ? SHA1.Create() : MD5.Create();
|
||||
|
||||
using (hasher)
|
||||
{
|
||||
@@ -123,6 +173,7 @@ public class HashGenerator : DisposableObjectSlim
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void DisposeResources()
|
||||
{
|
||||
_writer.Close();
|
||||
|
||||
@@ -44,7 +44,7 @@ public class LogProfiler : IProfiler
|
||||
private readonly Action<long> _callback;
|
||||
private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
|
||||
|
||||
protected internal LightDisposableTimer(Action<long> callback)
|
||||
internal LightDisposableTimer(Action<long> callback)
|
||||
{
|
||||
_callback = callback ?? throw new ArgumentNullException(nameof(callback));
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ public class BlockGridModel : BlockModelCollection<BlockGridItem>
|
||||
/// <value>
|
||||
/// The empty <see cref="BlockGridModel" />.
|
||||
/// </value>
|
||||
public static BlockGridModel Empty { get; } = new BlockGridModel();
|
||||
public static new BlockGridModel Empty { get; } = new BlockGridModel();
|
||||
|
||||
/// <summary>
|
||||
/// Prevents a default instance of the <see cref="BlockGridModel" /> class from being created.
|
||||
|
||||
@@ -34,5 +34,5 @@ public class BlockListModel : BlockModelCollection<BlockListItem>
|
||||
/// <value>
|
||||
/// The empty <see cref="BlockListModel" />.
|
||||
/// </value>
|
||||
public static BlockListModel Empty { get; } = new();
|
||||
public static new BlockListModel Empty { get; } = new();
|
||||
}
|
||||
|
||||
@@ -34,5 +34,5 @@ public class RichTextBlockModel : BlockModelCollection<RichTextBlockItem>
|
||||
/// <value>
|
||||
/// The empty <see cref="RichTextBlockModel" />.
|
||||
/// </value>
|
||||
public static RichTextBlockModel Empty { get; } = new();
|
||||
public static new RichTextBlockModel Empty { get; } = new();
|
||||
}
|
||||
|
||||
@@ -145,7 +145,9 @@ public sealed class CultureImpact
|
||||
/// <param name="savingCultures"></param>
|
||||
/// <param name="defaultCulture"></param>
|
||||
/// <returns></returns>
|
||||
public static string? GetCultureForInvariantErrors(IContent? content, string?[] savingCultures,
|
||||
public static string? GetCultureForInvariantErrors(
|
||||
IContent? content,
|
||||
string?[] savingCultures,
|
||||
string? defaultCulture)
|
||||
{
|
||||
if (content == null)
|
||||
|
||||
@@ -50,11 +50,11 @@ public interface IContentType : IContentTypeComposition
|
||||
bool RemoveTemplate(ITemplate template);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a deep clone of the current entity with its identity/alias and it's property identities reset
|
||||
/// Creates a deep clone of the current entity with its identity/alias and it's property identities reset.
|
||||
/// </summary>
|
||||
/// <param name="newAlias"></param>
|
||||
/// <returns></returns>
|
||||
IContentType DeepCloneWithResetIdentities(string newAlias);
|
||||
/// <param name="newAlias">The new alias for the cloned content type.</param>
|
||||
/// <returns>A deep clone of the current <see cref="IContentType"/> with reset identities.</returns>
|
||||
new IContentType DeepCloneWithResetIdentities(string newAlias);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the history cleanup configuration.
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
namespace Umbraco.Cms.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a ContentType, which Media is based on
|
||||
/// Defines a ContentType, which Media is based on.
|
||||
/// </summary>
|
||||
public interface IMediaType : IContentTypeComposition
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a deep clone of the current entity with its identity/alias and it's property identities reset
|
||||
/// Creates a deep clone of the current entity with its identity/alias and it's property identities reset.
|
||||
/// </summary>
|
||||
/// <param name="newAlias"></param>
|
||||
/// <returns></returns>
|
||||
IMediaType DeepCloneWithResetIdentities(string newAlias);
|
||||
/// <param name="newAlias">The new alias for the cloned media type.</param>
|
||||
/// <returns>A deep clone of the current <see cref="IMediaType"/> with reset identities.</returns>
|
||||
public new IMediaType DeepCloneWithResetIdentities(string newAlias);
|
||||
}
|
||||
|
||||
@@ -513,8 +513,8 @@ public class Member : ContentBase, IMember
|
||||
{
|
||||
StaticApplicationLogging.Logger.LogWarning(
|
||||
"Trying to access the '{PropertyName}' property on '{MemberType}' " +
|
||||
"but the {PropertyAlias} property does not exist on the member type so a default value is returned. " +
|
||||
"Ensure that you have a property type with alias: {PropertyAlias} configured on your member type in order to use the '{PropertyName}' property on the model correctly.",
|
||||
"but the property type with alias '{PropertyAlias}' does not exist on the member type. " +
|
||||
"A default value is returned. Ensure this property type is configured on your member type.",
|
||||
logPropertyName,
|
||||
typeof(Member),
|
||||
logPropertyAlias);
|
||||
|
||||
@@ -9,6 +9,11 @@ public interface IReadOnlyUserGroup
|
||||
{
|
||||
string? Name { get; }
|
||||
|
||||
string Alias { get; }
|
||||
|
||||
// TODO (V18): Remove the default implementations.
|
||||
string? Description { get { return null; } }
|
||||
|
||||
string? Icon { get; }
|
||||
|
||||
int Id { get; }
|
||||
@@ -19,11 +24,6 @@ public interface IReadOnlyUserGroup
|
||||
|
||||
int? StartMediaId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The alias
|
||||
/// </summary>
|
||||
string Alias { get; }
|
||||
|
||||
// This is set to return true as default to avoid breaking changes.
|
||||
bool HasAccessToAllLanguages => true;
|
||||
|
||||
|
||||
@@ -4,12 +4,24 @@ using Umbraco.Cms.Core.Models.Membership.Permissions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Models.Membership;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a user group in Umbraco.
|
||||
/// </summary>
|
||||
public interface IUserGroup : IEntity, IRememberBeingDirty
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the alias of the user group.
|
||||
/// </summary>
|
||||
string Alias { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the starting content node ID for this user group.
|
||||
/// </summary>
|
||||
int? StartContentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the starting media node ID for this user group.
|
||||
/// </summary>
|
||||
int? StartMediaId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -22,6 +34,18 @@ public interface IUserGroup : IEntity, IRememberBeingDirty
|
||||
/// </summary>
|
||||
string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the description of the user group.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// TODO (V18): Remove the default implementations.
|
||||
/// </remarks>
|
||||
string? Description
|
||||
{
|
||||
get => null;
|
||||
set { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If this property is true it will give the group access to all languages
|
||||
/// </summary>
|
||||
@@ -40,26 +64,57 @@ public interface IUserGroup : IEntity, IRememberBeingDirty
|
||||
/// </remarks>
|
||||
ISet<string> Permissions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the granular permissions for this user group.
|
||||
/// </summary>
|
||||
ISet<IGranularPermission> GranularPermissions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of section aliases that this user group has access to.
|
||||
/// </summary>
|
||||
IEnumerable<string> AllowedSections { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Removes access to a section for this user group.
|
||||
/// </summary>
|
||||
/// <param name="sectionAlias">The alias of the section to remove.</param>
|
||||
void RemoveAllowedSection(string sectionAlias);
|
||||
|
||||
/// <summary>
|
||||
/// Adds access to a section for this user group.
|
||||
/// </summary>
|
||||
/// <param name="sectionAlias">The alias of the section to add.</param>
|
||||
void AddAllowedSection(string sectionAlias);
|
||||
|
||||
/// <summary>
|
||||
/// Removes access to all sections for this user group.
|
||||
/// </summary>
|
||||
void ClearAllowedSections();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of language IDs that this user group has access to.
|
||||
/// </summary>
|
||||
IEnumerable<int> AllowedLanguages => Enumerable.Empty<int>();
|
||||
|
||||
/// <summary>
|
||||
/// Removes access to a language for this user group.
|
||||
/// </summary>
|
||||
/// <param name="languageId">The ID of the language to remove.</param>
|
||||
void RemoveAllowedLanguage(int languageId)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds access to a language for this user group.
|
||||
/// </summary>
|
||||
/// <param name="languageId">The ID of the language to add.</param>
|
||||
void AddAllowedLanguage(int languageId)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes access to all languages for this user group.
|
||||
/// </summary>
|
||||
void ClearAllowedLanguages()
|
||||
{
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace Umbraco.Cms.Core.Models.Membership;
|
||||
|
||||
public class ReadOnlyUserGroup : IReadOnlyUserGroup, IEquatable<ReadOnlyUserGroup>
|
||||
{
|
||||
[Obsolete("Please use the constructor that includes all parameters. Scheduled for removal in Umbraco 19.")]
|
||||
public ReadOnlyUserGroup(
|
||||
int id,
|
||||
Guid key,
|
||||
@@ -17,11 +18,43 @@ public class ReadOnlyUserGroup : IReadOnlyUserGroup, IEquatable<ReadOnlyUserGrou
|
||||
ISet<string> permissions,
|
||||
ISet<IGranularPermission> granularPermissions,
|
||||
bool hasAccessToAllLanguages)
|
||||
: this(
|
||||
id,
|
||||
key,
|
||||
name,
|
||||
null,
|
||||
icon,
|
||||
startContentId,
|
||||
startMediaId,
|
||||
alias,
|
||||
allowedLanguages,
|
||||
allowedSections,
|
||||
permissions,
|
||||
granularPermissions,
|
||||
hasAccessToAllLanguages)
|
||||
{
|
||||
}
|
||||
|
||||
public ReadOnlyUserGroup(
|
||||
int id,
|
||||
Guid key,
|
||||
string? name,
|
||||
string? description,
|
||||
string? icon,
|
||||
int? startContentId,
|
||||
int? startMediaId,
|
||||
string? alias,
|
||||
IEnumerable<int> allowedLanguages,
|
||||
IEnumerable<string> allowedSections,
|
||||
ISet<string> permissions,
|
||||
ISet<IGranularPermission> granularPermissions,
|
||||
bool hasAccessToAllLanguages)
|
||||
{
|
||||
Name = name ?? string.Empty;
|
||||
Icon = icon;
|
||||
Id = id;
|
||||
Key = key;
|
||||
Name = name ?? string.Empty;
|
||||
Description = description;
|
||||
Icon = icon;
|
||||
Alias = alias ?? string.Empty;
|
||||
AllowedLanguages = allowedLanguages.ToArray();
|
||||
AllowedSections = allowedSections.ToArray();
|
||||
@@ -38,23 +71,10 @@ public class ReadOnlyUserGroup : IReadOnlyUserGroup, IEquatable<ReadOnlyUserGrou
|
||||
|
||||
public Guid Key { get; }
|
||||
|
||||
public bool Equals(ReadOnlyUserGroup? other)
|
||||
{
|
||||
if (ReferenceEquals(null, other))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(this, other))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return string.Equals(Alias, other.Alias);
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public string? Description { get; }
|
||||
|
||||
public string? Icon { get; }
|
||||
|
||||
public int? StartContentId { get; }
|
||||
@@ -77,7 +97,7 @@ public class ReadOnlyUserGroup : IReadOnlyUserGroup, IEquatable<ReadOnlyUserGrou
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (ReferenceEquals(null, obj))
|
||||
if (obj is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -95,6 +115,21 @@ public class ReadOnlyUserGroup : IReadOnlyUserGroup, IEquatable<ReadOnlyUserGrou
|
||||
return Equals((ReadOnlyUserGroup)obj);
|
||||
}
|
||||
|
||||
public bool Equals(ReadOnlyUserGroup? other)
|
||||
{
|
||||
if (other is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(this, other))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return string.Equals(Alias, other.Alias);
|
||||
}
|
||||
|
||||
public override int GetHashCode() => Alias?.GetHashCode() ?? base.GetHashCode();
|
||||
|
||||
public static bool operator !=(ReadOnlyUserGroup left, ReadOnlyUserGroup right) => !Equals(left, right);
|
||||
|
||||
@@ -30,6 +30,7 @@ public class UserGroup : EntityBase, IUserGroup, IReadOnlyUserGroup
|
||||
private string _alias;
|
||||
private string? _icon;
|
||||
private string _name;
|
||||
private string? _description;
|
||||
private bool _hasAccessToAllLanguages;
|
||||
private ISet<string> _permissions;
|
||||
private ISet<IGranularPermission> _granularPermissions;
|
||||
@@ -53,13 +54,13 @@ public class UserGroup : EntityBase, IUserGroup, IReadOnlyUserGroup
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor to create an existing user group
|
||||
/// Constructor to create an existing user group.
|
||||
/// </summary>
|
||||
/// <param name="userCount"></param>
|
||||
/// <param name="alias"></param>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="icon"></param>
|
||||
/// <param name="shortStringHelper"></param>
|
||||
/// <param name="shortStringHelper">The short string helper.</param>
|
||||
/// <param name="userCount">The user count.</param>
|
||||
/// <param name="alias">The alias.</param>
|
||||
/// <param name="name">The name.</param>
|
||||
/// <param name="icon">The icon.</param>
|
||||
public UserGroup(
|
||||
IShortStringHelper shortStringHelper,
|
||||
int userCount,
|
||||
@@ -100,7 +101,8 @@ public class UserGroup : EntityBase, IUserGroup, IReadOnlyUserGroup
|
||||
{
|
||||
get => _alias;
|
||||
set => SetPropertyValueAndDetectChanges(
|
||||
value.ToCleanString(_shortStringHelper, CleanStringType.Alias | CleanStringType.UmbracoCase), ref _alias!,
|
||||
value.ToCleanString(_shortStringHelper, CleanStringType.Alias | CleanStringType.UmbracoCase),
|
||||
ref _alias!,
|
||||
nameof(Alias));
|
||||
}
|
||||
|
||||
@@ -111,6 +113,13 @@ public class UserGroup : EntityBase, IUserGroup, IReadOnlyUserGroup
|
||||
set => SetPropertyValueAndDetectChanges(value, ref _name!, nameof(Name));
|
||||
}
|
||||
|
||||
[DataMember]
|
||||
public string? Description
|
||||
{
|
||||
get => _description;
|
||||
set => SetPropertyValueAndDetectChanges(value, ref _description!, nameof(Description));
|
||||
}
|
||||
|
||||
[DataMember]
|
||||
public bool HasAccessToAllLanguages
|
||||
{
|
||||
|
||||
@@ -18,6 +18,7 @@ public static class UserGroupExtensions
|
||||
group.Id,
|
||||
group.Key,
|
||||
group.Name,
|
||||
group.Description,
|
||||
group.Icon,
|
||||
group.StartContentId,
|
||||
group.StartMediaId,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Umbraco.Cms.Core.Persistence.Repositories;
|
||||
|
||||
@@ -8,33 +8,25 @@ namespace Umbraco.Cms.Core.Persistence.Repositories;
|
||||
public static class RepositoryCacheKeys
|
||||
{
|
||||
/// <summary>
|
||||
/// A cache for the keys we don't keep allocating strings.
|
||||
/// A thread-safe cache for the keys so we don't keep allocating strings.
|
||||
/// </summary>
|
||||
private static readonly Dictionary<Type, string> Keys = new();
|
||||
private static readonly ConcurrentDictionary<Type, string> _keys = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the repository cache key for the provided type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The entity type to get the cache key for.</typeparam>
|
||||
/// <returns>A cache key string in the format "uRepo_{TypeName}_".</returns>
|
||||
public static string GetKey<T>()
|
||||
{
|
||||
Type type = typeof(T);
|
||||
|
||||
// The following code is a micro-optimization to avoid an unnecessary lookup in the Keys dictionary, when writing the newly created key.
|
||||
// Previously, the code was:
|
||||
// return Keys.TryGetValue(type, out var key)
|
||||
// ? key
|
||||
// : Keys[type] = "uRepo_" + type.Name + "_";
|
||||
|
||||
// Look up the existing value or get a reference to the newly created default value.
|
||||
ref string? key = ref CollectionsMarshal.GetValueRefOrAddDefault(Keys, type, out _);
|
||||
|
||||
// As we have the reference, we can just assign it if null, without the expensive write back to the dictionary.
|
||||
return key ??= "uRepo_" + type.Name + "_";
|
||||
}
|
||||
=> _keys.GetOrAdd(typeof(T), static type => "uRepo_" + type.Name + "_");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the repository cache key for the provided type and Id.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The entity type to get the cache key for.</typeparam>
|
||||
/// <typeparam name="TId">The type of the entity identifier.</typeparam>
|
||||
/// <param name="id">The entity identifier.</param>
|
||||
/// <returns>A cache key string in the format "uRepo_{TypeName}_{Id}", or an empty string if the id is the default value.</returns>
|
||||
public static string GetKey<T, TId>(TId? id)
|
||||
{
|
||||
if (EqualityComparer<TId?>.Default.Equals(id, default))
|
||||
|
||||
@@ -9,8 +9,6 @@ namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
[DataContract]
|
||||
public class ConfigurationField
|
||||
{
|
||||
private readonly string? _view;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConfigurationField" /> class.
|
||||
/// </summary>
|
||||
@@ -22,6 +20,7 @@ public class ConfigurationField
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConfigurationField" /> class.
|
||||
/// </summary>
|
||||
/// <param name="validators">The validators for this configuration field.</param>
|
||||
public ConfigurationField(params IValueValidator[] validators)
|
||||
: this(validators.ToList())
|
||||
{
|
||||
|
||||
@@ -8,8 +8,14 @@ namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
/// </summary>
|
||||
public class DefaultPropertyIndexValueFactory : IPropertyIndexValueFactory
|
||||
{
|
||||
public IEnumerable<IndexValue> GetIndexValues(IProperty property, string? culture, string? segment, bool published,
|
||||
IEnumerable<string> availableCultures, IDictionary<Guid, IContentType> contentTypeDictionary)
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<IndexValue> GetIndexValues(
|
||||
IProperty property,
|
||||
string? culture,
|
||||
string? segment,
|
||||
bool published,
|
||||
IEnumerable<string> availableCultures,
|
||||
IDictionary<Guid, IContentType> contentTypeDictionary)
|
||||
=>
|
||||
[
|
||||
new IndexValue
|
||||
|
||||
@@ -66,6 +66,17 @@ public interface IDataEditor : IDiscoverable
|
||||
/// <returns>The result of the merge operation.</returns>
|
||||
object? MergePartialPropertyValueForCulture(object? sourceValue, object? targetValue, string? culture) => sourceValue;
|
||||
|
||||
object? MergeVariantInvariantPropertyValue(object? sourceValue, object? targetValue,
|
||||
bool canUpdateInvariantData, HashSet<string> allowedCultures) => sourceValue;
|
||||
/// <summary>
|
||||
/// Merges variant and invariant property values.
|
||||
/// </summary>
|
||||
/// <param name="sourceValue">The source property value.</param>
|
||||
/// <param name="targetValue">The target property value.</param>
|
||||
/// <param name="canUpdateInvariantData">A value indicating whether invariant data can be updated.</param>
|
||||
/// <param name="allowedCultures">The set of allowed cultures.</param>
|
||||
/// <returns>The result of the merge operation.</returns>
|
||||
object? MergeVariantInvariantPropertyValue(
|
||||
object? sourceValue,
|
||||
object? targetValue,
|
||||
bool canUpdateInvariantData,
|
||||
HashSet<string> allowedCultures) => sourceValue;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,12 @@ namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
public class NoopPropertyIndexValueFactory : IPropertyIndexValueFactory
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<IndexValue> GetIndexValues(IProperty property, string? culture, string? segment, bool published,
|
||||
IEnumerable<string> availableCultures, IDictionary<Guid, IContentType> contentTypeDictionary)
|
||||
public IEnumerable<IndexValue> GetIndexValues(
|
||||
IProperty property,
|
||||
string? culture,
|
||||
string? segment,
|
||||
bool published,
|
||||
IEnumerable<string> availableCultures,
|
||||
IDictionary<Guid, IContentType> contentTypeDictionary)
|
||||
=> [];
|
||||
}
|
||||
|
||||
@@ -72,7 +72,11 @@ public class TextOnlyValueEditor : DataValueEditor
|
||||
_localizedTextService = localizedTextService;
|
||||
}
|
||||
|
||||
public IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration,
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<ValidationResult> Validate(
|
||||
object? value,
|
||||
string? valueType,
|
||||
object? dataTypeConfiguration,
|
||||
PropertyValidationContext validationContext)
|
||||
{
|
||||
int? maxCharacters = dataTypeConfiguration switch
|
||||
|
||||
@@ -4,18 +4,21 @@ using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.PublishedCache;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a property of a published element with caching support for property value conversions.
|
||||
/// </summary>
|
||||
internal sealed class PublishedElementPropertyBase : PublishedPropertyBase
|
||||
{
|
||||
protected readonly IPublishedElement Element;
|
||||
private readonly IPublishedElement _element;
|
||||
|
||||
// define constant - determines whether to use cache when previewing
|
||||
// to store eg routes, property converted values, anything - caching
|
||||
// means faster execution, but uses memory - not sure if we want it
|
||||
// so making it configurable.
|
||||
private readonly Lock _locko = new();
|
||||
private readonly Lock _cacheLock = new();
|
||||
private readonly object? _sourceValue;
|
||||
protected readonly bool IsMember;
|
||||
protected readonly bool IsPreviewing;
|
||||
private readonly bool _isMember;
|
||||
private readonly bool _isPreviewing;
|
||||
private readonly VariationContext _variationContext;
|
||||
private readonly ICacheManager? _cacheManager;
|
||||
private CacheValues? _cacheValues;
|
||||
@@ -24,6 +27,16 @@ internal sealed class PublishedElementPropertyBase : PublishedPropertyBase
|
||||
private object? _interValue;
|
||||
private string? _valuesCacheKey;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PublishedElementPropertyBase"/> class.
|
||||
/// </summary>
|
||||
/// <param name="propertyType">The published property type.</param>
|
||||
/// <param name="element">The published element that owns this property.</param>
|
||||
/// <param name="previewing">Whether this is a preview request.</param>
|
||||
/// <param name="referenceCacheLevel">The reference cache level.</param>
|
||||
/// <param name="variationContext">The variation context for culture and segment.</param>
|
||||
/// <param name="cacheManager">The cache manager.</param>
|
||||
/// <param name="sourceValue">The source value of the property.</param>
|
||||
public PublishedElementPropertyBase(
|
||||
IPublishedPropertyType propertyType,
|
||||
IPublishedElement element,
|
||||
@@ -35,11 +48,11 @@ internal sealed class PublishedElementPropertyBase : PublishedPropertyBase
|
||||
: base(propertyType, referenceCacheLevel)
|
||||
{
|
||||
_sourceValue = sourceValue;
|
||||
Element = element;
|
||||
IsPreviewing = previewing;
|
||||
_element = element;
|
||||
_isPreviewing = previewing;
|
||||
_variationContext = variationContext;
|
||||
_cacheManager = cacheManager;
|
||||
IsMember = propertyType.ContentType?.ItemType == PublishedItemType.Member;
|
||||
_isMember = propertyType.ContentType?.ItemType == PublishedItemType.Member;
|
||||
}
|
||||
|
||||
// used to cache the CacheValues of this property
|
||||
@@ -47,9 +60,11 @@ internal sealed class PublishedElementPropertyBase : PublishedPropertyBase
|
||||
private string ValuesCacheKey => _valuesCacheKey ??= PropertyCacheValuesKey();
|
||||
|
||||
private string PropertyCacheValuesKey() =>
|
||||
$"PublishedSnapshot.Property.CacheValues[{(IsPreviewing ? "D:" : "P:")}{Element.Key}:{Alias}:{_variationContext.Culture.IfNullOrWhiteSpace("inv")}+{_variationContext.Segment.IfNullOrWhiteSpace("inv")}]";
|
||||
$"PublishedSnapshot.Property.CacheValues[{(_isPreviewing ? "D:" : "P:")}{_element.Key}:{Alias}:{_variationContext.Culture.IfNullOrWhiteSpace("inv")}+{_variationContext.Segment.IfNullOrWhiteSpace("inv")}]";
|
||||
|
||||
// ReSharper restore InconsistentlySynchronizedField
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool HasValue(string? culture = null, string? segment = null)
|
||||
{
|
||||
var hasValue = PropertyType.IsValue(_sourceValue, PropertyValueLevel.Source);
|
||||
@@ -60,7 +75,7 @@ internal sealed class PublishedElementPropertyBase : PublishedPropertyBase
|
||||
|
||||
GetCacheLevels(out PropertyCacheLevel cacheLevel, out PropertyCacheLevel referenceCacheLevel);
|
||||
|
||||
lock (_locko)
|
||||
lock (_cacheLock)
|
||||
{
|
||||
var value = GetInterValue();
|
||||
hasValue = PropertyType.IsValue(value, PropertyValueLevel.Inter);
|
||||
@@ -73,7 +88,7 @@ internal sealed class PublishedElementPropertyBase : PublishedPropertyBase
|
||||
if (!cacheValues.ObjectInitialized)
|
||||
{
|
||||
cacheValues.ObjectValue =
|
||||
PropertyType.ConvertInterToObject(Element, referenceCacheLevel, value, IsPreviewing);
|
||||
PropertyType.ConvertInterToObject(_element, referenceCacheLevel, value, _isPreviewing);
|
||||
cacheValues.ObjectInitialized = true;
|
||||
}
|
||||
|
||||
@@ -82,6 +97,7 @@ internal sealed class PublishedElementPropertyBase : PublishedPropertyBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? GetSourceValue(string? culture = null, string? segment = null) => _sourceValue;
|
||||
|
||||
private void GetCacheLevels(out PropertyCacheLevel cacheLevel, out PropertyCacheLevel referenceCacheLevel)
|
||||
@@ -151,16 +167,17 @@ internal sealed class PublishedElementPropertyBase : PublishedPropertyBase
|
||||
return _interValue;
|
||||
}
|
||||
|
||||
_interValue = PropertyType.ConvertSourceToInter(Element, _sourceValue, IsPreviewing);
|
||||
_interValue = PropertyType.ConvertSourceToInter(_element, _sourceValue, _isPreviewing);
|
||||
_interInitialized = true;
|
||||
return _interValue;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? GetValue(string? culture = null, string? segment = null)
|
||||
{
|
||||
GetCacheLevels(out PropertyCacheLevel cacheLevel, out PropertyCacheLevel referenceCacheLevel);
|
||||
|
||||
lock (_locko)
|
||||
lock (_cacheLock)
|
||||
{
|
||||
CacheValues cacheValues = GetCacheValues(cacheLevel);
|
||||
if (cacheValues.ObjectInitialized)
|
||||
@@ -169,12 +186,13 @@ internal sealed class PublishedElementPropertyBase : PublishedPropertyBase
|
||||
}
|
||||
|
||||
cacheValues.ObjectValue =
|
||||
PropertyType.ConvertInterToObject(Element, referenceCacheLevel, GetInterValue(), IsPreviewing);
|
||||
PropertyType.ConvertInterToObject(_element, referenceCacheLevel, GetInterValue(), _isPreviewing);
|
||||
cacheValues.ObjectInitialized = true;
|
||||
return cacheValues.ObjectValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? GetDeliveryApiValue(bool expanding, string? culture = null, string? segment = null)
|
||||
{
|
||||
PropertyCacheLevel cacheLevel, referenceCacheLevel;
|
||||
@@ -187,11 +205,11 @@ internal sealed class PublishedElementPropertyBase : PublishedPropertyBase
|
||||
GetDeliveryApiCacheLevels(out cacheLevel, out referenceCacheLevel);
|
||||
}
|
||||
|
||||
lock (_locko)
|
||||
lock (_cacheLock)
|
||||
{
|
||||
CacheValues cacheValues = GetCacheValues(cacheLevel);
|
||||
|
||||
object? GetDeliveryApiObject() => PropertyType.ConvertInterToDeliveryApiObject(Element, referenceCacheLevel, GetInterValue(), IsPreviewing, expanding);
|
||||
object? GetDeliveryApiObject() => PropertyType.ConvertInterToDeliveryApiObject(_element, referenceCacheLevel, GetInterValue(), _isPreviewing, expanding);
|
||||
return expanding
|
||||
? GetDeliveryApiExpandedObject(cacheValues, GetDeliveryApiObject)
|
||||
: GetDeliveryApiDefaultObject(cacheValues, GetDeliveryApiObject);
|
||||
@@ -220,15 +238,15 @@ internal sealed class PublishedElementPropertyBase : PublishedPropertyBase
|
||||
return cacheValues.DeliveryApiExpandedObjectValue;
|
||||
}
|
||||
|
||||
protected class CacheValues
|
||||
private class CacheValues
|
||||
{
|
||||
public bool ObjectInitialized;
|
||||
public object? ObjectValue;
|
||||
public bool XPathInitialized;
|
||||
public object? XPathValue;
|
||||
public bool DeliveryApiDefaultObjectInitialized;
|
||||
public object? DeliveryApiDefaultObjectValue;
|
||||
public bool DeliveryApiExpandedObjectInitialized;
|
||||
public object? DeliveryApiExpandedObjectValue;
|
||||
public bool ObjectInitialized { get; set; }
|
||||
public object? ObjectValue { get; set; }
|
||||
public bool XPathInitialized { get; set; }
|
||||
public object? XPathValue { get; set; }
|
||||
public bool DeliveryApiDefaultObjectInitialized { get; set; }
|
||||
public object? DeliveryApiDefaultObjectValue { get; set; }
|
||||
public bool DeliveryApiExpandedObjectInitialized { get; set; }
|
||||
public object? DeliveryApiExpandedObjectValue { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ namespace Umbraco.Cms.Core.Routing;
|
||||
public class DefaultUrlProvider : IUrlProvider
|
||||
{
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private readonly ILocalizedTextService? _localizedTextService;
|
||||
private readonly ILogger<DefaultUrlProvider> _logger;
|
||||
private readonly ISiteDomainMapper _siteDomainMapper;
|
||||
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
|
||||
@@ -30,6 +29,18 @@ public class DefaultUrlProvider : IUrlProvider
|
||||
private readonly UriUtility _uriUtility;
|
||||
private RequestHandlerSettings _requestSettings;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DefaultUrlProvider"/> class.
|
||||
/// </summary>
|
||||
/// <param name="requestSettings">The request handler settings.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="siteDomainMapper">The site domain mapper.</param>
|
||||
/// <param name="umbracoContextAccessor">The Umbraco context accessor.</param>
|
||||
/// <param name="uriUtility">The URI utility.</param>
|
||||
/// <param name="localizationService">The localization service.</param>
|
||||
/// <param name="navigationQueryService">The document navigation query service.</param>
|
||||
/// <param name="publishedContentStatusFilteringService">The published content status filtering service.</param>
|
||||
/// <param name="publishedUrlProvider">The published URL provider.</param>
|
||||
public DefaultUrlProvider(
|
||||
IOptionsMonitor<RequestHandlerSettings> requestSettings,
|
||||
ILogger<DefaultUrlProvider> logger,
|
||||
@@ -54,6 +65,17 @@ public class DefaultUrlProvider : IUrlProvider
|
||||
requestSettings.OnChange(x => _requestSettings = x);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DefaultUrlProvider"/> class.
|
||||
/// </summary>
|
||||
/// <param name="requestSettings">The request handler settings.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="siteDomainMapper">The site domain mapper.</param>
|
||||
/// <param name="umbracoContextAccessor">The Umbraco context accessor.</param>
|
||||
/// <param name="uriUtility">The URI utility.</param>
|
||||
/// <param name="localizationService">The localization service.</param>
|
||||
/// <param name="navigationQueryService">The document navigation query service.</param>
|
||||
/// <param name="publishedContentStatusFilteringService">The published content status filtering service.</param>
|
||||
[Obsolete("Use the other constructor - Scheduled for removal in V18")]
|
||||
public DefaultUrlProvider(
|
||||
IOptionsMonitor<RequestHandlerSettings> requestSettings,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Globalization;
|
||||
using System.Globalization;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
@@ -7,7 +7,19 @@ using Umbraco.Cms.Core.Web;
|
||||
|
||||
namespace Umbraco.Cms.Core.Routing;
|
||||
|
||||
// TODO: Kill this, but we need to port all of it's functionality
|
||||
/// <summary>
|
||||
/// Represents a legacy published content request.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This class is deprecated and scheduled for removal. Its functionality is being
|
||||
/// migrated to the newer <see cref="IPublishedRequest"/> implementations.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// TODO: Kill this, but we need to port all of its functionality.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Obsolete("This class is deprecated and will be removed in a future version. Use IPublishedRequest instead. Scheduled for removal in Umbraco 19.")]
|
||||
public class PublishedRequestOld // : IPublishedRequest
|
||||
{
|
||||
private readonly IPublishedRouter _publishedRouter;
|
||||
@@ -15,13 +27,14 @@ public class PublishedRequestOld // : IPublishedRequest
|
||||
private CultureInfo? _culture;
|
||||
private DomainAndUri? _domain;
|
||||
private bool _is404;
|
||||
private IPublishedContent? _publishedContent;
|
||||
|
||||
private bool _readonly; // after prepared
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PublishedRequest" /> class.
|
||||
/// Initializes a new instance of the <see cref="PublishedRequestOld" /> class.
|
||||
/// </summary>
|
||||
/// <param name="publishedRouter">The published router.</param>
|
||||
/// <param name="umbracoContext">The Umbraco context.</param>
|
||||
/// <param name="webRoutingSettings">The web routing settings.</param>
|
||||
/// <param name="uri">The optional URI for this request. If not provided, uses the cleaned Umbraco URL from the context.</param>
|
||||
public PublishedRequestOld(IPublishedRouter publishedRouter, IUmbracoContext umbracoContext, IOptions<WebRoutingSettings> webRoutingSettings, Uri? uri = null)
|
||||
{
|
||||
UmbracoContext = umbracoContext ?? throw new ArgumentNullException(nameof(umbracoContext));
|
||||
@@ -41,6 +54,9 @@ public class PublishedRequestOld // : IPublishedRequest
|
||||
/// <remarks>The cleaned up Uri has no virtual directory, no trailing slash, no .aspx extension, etc.</remarks>
|
||||
public Uri Uri { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether caching should be disabled for this request.
|
||||
/// </summary>
|
||||
public bool CacheabilityNoCache { get; set; }
|
||||
|
||||
///// <summary>
|
||||
@@ -102,13 +118,15 @@ public class PublishedRequestOld // : IPublishedRequest
|
||||
}
|
||||
}
|
||||
|
||||
// utility for ensuring it is ok to set some properties
|
||||
/// <summary>
|
||||
/// Ensures the request is in a writeable state.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is a no-op. The readonly check was removed because the backing field
|
||||
/// was never set to true, making the check unnecessary. Retained for API compatibility.
|
||||
/// </remarks>
|
||||
public void EnsureWriteable()
|
||||
{
|
||||
if (_readonly)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot modify a PublishedRequest once it is read-only.");
|
||||
}
|
||||
}
|
||||
|
||||
// #region Events
|
||||
@@ -226,18 +244,19 @@ public class PublishedRequestOld // : IPublishedRequest
|
||||
/// <summary>
|
||||
/// Gets value indicating whether the current published content is the initial one.
|
||||
/// </summary>
|
||||
public bool IsInitialPublishedContent =>
|
||||
InitialPublishedContent != null && InitialPublishedContent == _publishedContent;
|
||||
/// <remarks>Always returns false as _publishedContent field was removed (never assigned).</remarks>
|
||||
public bool IsInitialPublishedContent => false;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the current PublishedContent is the initial one.
|
||||
/// </summary>
|
||||
/// <remarks>Note: _publishedContent field was removed (never assigned), so this sets InitialPublishedContent to null.</remarks>
|
||||
public void SetIsInitialPublishedContent()
|
||||
{
|
||||
EnsureWriteable();
|
||||
|
||||
// note: it can very well be null if the initial content was not found
|
||||
InitialPublishedContent = _publishedContent;
|
||||
InitialPublishedContent = null;
|
||||
IsInternalRedirectPublishedContent = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,9 @@ public class ContentPermissionResource : IPermissionResource
|
||||
/// <param name="contentKey">The key of the content or null if root.</param>
|
||||
/// <param name="cultures">The cultures to validate</param>
|
||||
/// <returns>An instance of <see cref="ContentPermissionResource" />.</returns>
|
||||
public static ContentPermissionResource WithKeys(string permissionToCheck, Guid? contentKey,
|
||||
public static ContentPermissionResource WithKeys(
|
||||
string permissionToCheck,
|
||||
Guid? contentKey,
|
||||
IEnumerable<string> cultures) =>
|
||||
contentKey is null
|
||||
? Root(permissionToCheck, cultures)
|
||||
@@ -61,7 +63,9 @@ public class ContentPermissionResource : IPermissionResource
|
||||
/// <param name="contentKey">The key of the content.</param>
|
||||
/// <param name="cultures">The required culture access</param>
|
||||
/// <returns>An instance of <see cref="ContentPermissionResource" />.</returns>
|
||||
public static ContentPermissionResource WithKeys(string permissionToCheck, Guid contentKey,
|
||||
public static ContentPermissionResource WithKeys(
|
||||
string permissionToCheck,
|
||||
Guid contentKey,
|
||||
IEnumerable<string> cultures) => WithKeys(permissionToCheck, contentKey.Yield(), cultures);
|
||||
|
||||
/// <summary>
|
||||
@@ -80,7 +84,9 @@ public class ContentPermissionResource : IPermissionResource
|
||||
/// <param name="contentKeys">The keys of the contents.</param>
|
||||
/// <param name="cultures">The required culture access</param>
|
||||
/// <returns>An instance of <see cref="ContentPermissionResource" />.</returns>
|
||||
public static ContentPermissionResource WithKeys(string permissionToCheck, IEnumerable<Guid> contentKeys,
|
||||
public static ContentPermissionResource WithKeys(
|
||||
string permissionToCheck,
|
||||
IEnumerable<Guid> contentKeys,
|
||||
IEnumerable<string> cultures) =>
|
||||
new(
|
||||
contentKeys,
|
||||
@@ -114,7 +120,12 @@ public class ContentPermissionResource : IPermissionResource
|
||||
/// <param name="cultures">The cultures to validate</param>
|
||||
/// <returns>An instance of <see cref="ContentPermissionResource" />.</returns>
|
||||
public static ContentPermissionResource Root(string permissionToCheck, IEnumerable<string> cultures) =>
|
||||
new(Enumerable.Empty<Guid>(), new HashSet<string> { permissionToCheck }, true, false, null,
|
||||
new(
|
||||
Enumerable.Empty<Guid>(),
|
||||
new HashSet<string> { permissionToCheck },
|
||||
true,
|
||||
false,
|
||||
null,
|
||||
new HashSet<string>(cultures));
|
||||
|
||||
/// <summary>
|
||||
@@ -176,7 +187,9 @@ public class ContentPermissionResource : IPermissionResource
|
||||
/// <param name="parentKeyForBranch">The parent key of the branch.</param>
|
||||
/// <param name="culturesToCheck">The required cultures</param>
|
||||
/// <returns>An instance of <see cref="ContentPermissionResource" />.</returns>
|
||||
public static ContentPermissionResource Branch(string permissionToCheck, Guid parentKeyForBranch,
|
||||
public static ContentPermissionResource Branch(
|
||||
string permissionToCheck,
|
||||
Guid parentKeyForBranch,
|
||||
IEnumerable<string> culturesToCheck) =>
|
||||
new(
|
||||
Enumerable.Empty<Guid>(),
|
||||
@@ -189,7 +202,8 @@ public class ContentPermissionResource : IPermissionResource
|
||||
private ContentPermissionResource(
|
||||
IEnumerable<Guid> contentKeys,
|
||||
ISet<string> permissionsToCheck,
|
||||
bool checkRoot, bool checkRecycleBin,
|
||||
bool checkRoot,
|
||||
bool checkRecycleBin,
|
||||
Guid? parentKeyForBranch,
|
||||
ISet<string>? culturesToCheck)
|
||||
{
|
||||
|
||||
@@ -12,17 +12,25 @@ namespace Umbraco.Cms.Core.Security;
|
||||
/// </remarks>
|
||||
public class LegacyPasswordSecurity
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a cryptographically secure random salt.
|
||||
/// </summary>
|
||||
/// <returns>A base64-encoded salt string.</returns>
|
||||
public static string GenerateSalt()
|
||||
{
|
||||
var numArray = new byte[16];
|
||||
using (var rng = new RNGCryptoServiceProvider())
|
||||
{
|
||||
rng.GetBytes(numArray);
|
||||
return Convert.ToBase64String(numArray);
|
||||
}
|
||||
RandomNumberGenerator.Fill(numArray);
|
||||
return Convert.ToBase64String(numArray);
|
||||
}
|
||||
|
||||
// Used for tests
|
||||
/// <summary>
|
||||
/// Formats a password for storage by combining salt and hashed password.
|
||||
/// </summary>
|
||||
/// <param name="algorithmType">The hashing algorithm type.</param>
|
||||
/// <param name="hashedPassword">The hashed password.</param>
|
||||
/// <param name="salt">The salt used for hashing.</param>
|
||||
/// <returns>The formatted password string for storage.</returns>
|
||||
/// <remarks>Used for tests.</remarks>
|
||||
internal string FormatPasswordForStorage(string algorithmType, string hashedPassword, string salt)
|
||||
{
|
||||
if (!SupportHashAlgorithm(algorithmType))
|
||||
@@ -34,12 +42,12 @@ public class LegacyPasswordSecurity
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies if the password matches the expected hash+salt of the stored password string
|
||||
/// Verifies if the password matches the expected hash+salt of the stored password string.
|
||||
/// </summary>
|
||||
/// <param name="algorithm">The hashing algorithm for the stored password.</param>
|
||||
/// <param name="password">The password.</param>
|
||||
/// <param name="password">The password to verify.</param>
|
||||
/// <param name="dbPassword">The value of the password stored in a data store.</param>
|
||||
/// <returns></returns>
|
||||
/// <returns><c>true</c> if the password matches; otherwise, <c>false</c>.</returns>
|
||||
public bool VerifyPassword(string algorithm, string password, string dbPassword)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dbPassword))
|
||||
@@ -66,8 +74,11 @@ public class LegacyPasswordSecurity
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify a legacy hashed password (HMACSHA1)
|
||||
/// Verifies a legacy hashed password that was created using HMACSHA1.
|
||||
/// </summary>
|
||||
/// <param name="password">The password to verify.</param>
|
||||
/// <param name="dbPassword">The stored hashed password from the database.</param>
|
||||
/// <returns><c>true</c> if the password matches; otherwise, <c>false</c>.</returns>
|
||||
public bool VerifyLegacyHashedPassword(string password, string dbPassword)
|
||||
{
|
||||
using var hashAlgorithm = new HMACSHA1
|
||||
@@ -82,8 +93,12 @@ public class LegacyPasswordSecurity
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new password hash and a new salt
|
||||
/// Creates a new password hash with a newly generated salt.
|
||||
/// </summary>
|
||||
/// <param name="algorithm">The hashing algorithm to use.</param>
|
||||
/// <param name="newPassword">The password to hash.</param>
|
||||
/// <param name="salt">Returns the generated salt.</param>
|
||||
/// <returns>The hashed password.</returns>
|
||||
internal string HashNewPassword(string algorithm, string newPassword, out string salt)
|
||||
{
|
||||
salt = GenerateSalt();
|
||||
@@ -91,12 +106,12 @@ public class LegacyPasswordSecurity
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses out the hashed password and the salt from the stored password string value
|
||||
/// Parses out the hashed password and the salt from the stored password string value.
|
||||
/// </summary>
|
||||
/// <param name="algorithm">The hashing algorithm for the stored password.</param>
|
||||
/// <param name="storedString"></param>
|
||||
/// <param name="salt">returns the salt</param>
|
||||
/// <returns></returns>
|
||||
/// <param name="storedString">The stored password string containing salt and hash.</param>
|
||||
/// <param name="salt">Returns the extracted salt.</param>
|
||||
/// <returns>The hashed password portion of the stored string.</returns>
|
||||
public string ParseStoredHashPassword(string algorithm, string storedString, out string salt)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(storedString))
|
||||
@@ -114,6 +129,11 @@ public class LegacyPasswordSecurity
|
||||
return storedString.Substring(saltLen.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified hash algorithm is supported.
|
||||
/// </summary>
|
||||
/// <param name="algorithm">The algorithm name to check.</param>
|
||||
/// <returns><c>true</c> if the algorithm is supported; otherwise, <c>false</c>.</returns>
|
||||
public bool SupportHashAlgorithm(string algorithm)
|
||||
{
|
||||
// This is for the v6-v8 hashing algorithm
|
||||
@@ -132,12 +152,12 @@ public class LegacyPasswordSecurity
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hashes a password with a given salt
|
||||
/// Hashes a password with a given salt.
|
||||
/// </summary>
|
||||
/// <param name="algorithmType">The hashing algorithm for the password.</param>
|
||||
/// <param name="pass"></param>
|
||||
/// <param name="salt"></param>
|
||||
/// <returns></returns>
|
||||
/// <param name="pass">The password to hash.</param>
|
||||
/// <param name="salt">The salt to use for hashing.</param>
|
||||
/// <returns>The hashed password as a base64-encoded string.</returns>
|
||||
private string HashPassword(string algorithmType, string pass, string salt)
|
||||
{
|
||||
if (!SupportHashAlgorithm(algorithmType))
|
||||
@@ -195,10 +215,11 @@ public class LegacyPasswordSecurity
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return the hash algorithm to use based on the provided <paramref name="algorithm"/>.
|
||||
/// Returns the hash algorithm instance based on the provided algorithm name.
|
||||
/// </summary>
|
||||
/// <param name="algorithm">The hashing algorithm name.</param>
|
||||
/// <returns></returns>
|
||||
/// <param name="algorithm">The hashing algorithm name (e.g., "HMACSHA256", "SHA1", "MD5").</param>
|
||||
/// <returns>A <see cref="HashAlgorithm"/> instance for the specified algorithm.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown when the algorithm is not specified or not recognized.</exception>
|
||||
private HashAlgorithm GetHashAlgorithm(string algorithm)
|
||||
{
|
||||
if (algorithm.IsNullOrWhiteSpace())
|
||||
@@ -206,12 +227,16 @@ public class LegacyPasswordSecurity
|
||||
throw new InvalidOperationException("No hash algorithm type specified");
|
||||
}
|
||||
|
||||
var alg = HashAlgorithm.Create(algorithm);
|
||||
if (alg == null)
|
||||
return algorithm.ToUpperInvariant() switch
|
||||
{
|
||||
throw new InvalidOperationException($"The hash algorithm specified {algorithm} cannot be resolved");
|
||||
}
|
||||
|
||||
return alg;
|
||||
"HMACSHA256" => new HMACSHA256(),
|
||||
"HMACSHA1" => new HMACSHA1(),
|
||||
"SHA1" => SHA1.Create(),
|
||||
"SHA256" => SHA256.Create(),
|
||||
"SHA384" => SHA384.Create(),
|
||||
"SHA512" => SHA512.Create(),
|
||||
"MD5" => MD5.Create(),
|
||||
_ => throw new InvalidOperationException($"The hash algorithm specified {algorithm} cannot be resolved")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,10 +98,7 @@ public class PasswordGenerator
|
||||
var data = new byte[length];
|
||||
var chArray = new char[length];
|
||||
var num1 = 0;
|
||||
using (var rng = new RNGCryptoServiceProvider())
|
||||
{
|
||||
rng.GetBytes(data);
|
||||
}
|
||||
RandomNumberGenerator.Fill(data);
|
||||
|
||||
for (var index = 0; index < length; ++index)
|
||||
{
|
||||
|
||||
@@ -292,7 +292,9 @@ internal sealed class ContentEditingService
|
||||
public async Task<Attempt<IContent?, ContentEditingOperationStatus>> CopyAsync(Guid key, Guid? parentKey, bool relateToOriginal, bool includeDescendants, Guid userKey)
|
||||
=> await HandleCopyAsync(key, parentKey, relateToOriginal, includeDescendants, userKey);
|
||||
|
||||
public async Task<ContentEditingOperationStatus> SortAsync(Guid? parentKey, IEnumerable<SortingModel> sortingModels,
|
||||
public async Task<ContentEditingOperationStatus> SortAsync(
|
||||
Guid? parentKey,
|
||||
IEnumerable<SortingModel> sortingModels,
|
||||
Guid userKey)
|
||||
=> await HandleSortAsync(parentKey, sortingModels, userKey);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user