Compare commits
110
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4eae48e794 | ||
|
|
d32348d6b2 | ||
|
|
e142a9c0d1 | ||
|
|
cef7bb9357 | ||
|
|
d895032e22 | ||
|
|
5a22e50120 | ||
|
|
9cb5a12143 | ||
|
|
e60acfe437 | ||
|
|
c436c4288b | ||
|
|
229f1e0ce0 | ||
|
|
56569af0f9 | ||
|
|
fc3d8c8781 | ||
|
|
44af43e091 | ||
|
|
94b3e767c7 | ||
|
|
db4a85fcb8 | ||
|
|
13bb25a8b8 | ||
|
|
58cc7691a3 | ||
|
|
937f4b8122 | ||
|
|
cee441da49 | ||
|
|
aa269e317b | ||
|
|
2266529895 | ||
|
|
4efe8f59b8 | ||
|
|
417576b275 | ||
|
|
2289493384 | ||
|
|
2def046ea3 | ||
|
|
b3d5744d34 | ||
|
|
5f1ecbae94 | ||
|
|
ba8e1ae5dd | ||
|
|
e7da631ec2 | ||
|
|
d23c2acd63 | ||
|
|
a0aff9d10c | ||
|
|
8a94383262 | ||
|
|
63ed1eec41 | ||
|
|
eb986d9de9 | ||
|
|
e675894dd5 | ||
|
|
3f34c8f433 | ||
|
|
44aa5dcf1c | ||
|
|
3a4e0b0654 | ||
|
|
e5d1c67c36 | ||
|
|
046a3d9aad | ||
|
|
ad0854be4d | ||
|
|
81dd568e03 | ||
|
|
aad253fb11 | ||
|
|
e3da1792cd | ||
|
|
36b5f63b4f | ||
|
|
6c40a6f1e1 | ||
|
|
c0847920f4 | ||
|
|
89a8bd3721 | ||
|
|
9414201c2e | ||
|
|
39b395c05d | ||
|
|
20e47d5edd | ||
|
|
71057b5f0b | ||
|
|
0c22d512e2 | ||
|
|
240e155d91 | ||
|
|
50eeb76c5a | ||
|
|
fcba10aecf | ||
|
|
20254f0bbc | ||
|
|
f5ff2bbf59 | ||
|
|
c31861945c | ||
|
|
af8742651c | ||
|
|
4bf2fbf1ba | ||
|
|
7c9c7337b9 | ||
|
|
a630febf67 | ||
|
|
170becdd5b | ||
|
|
39a5362de8 | ||
|
|
de64c53777 | ||
|
|
d9e7e9e8a8 | ||
|
|
a1d1c5492a | ||
|
|
7199ed0310 | ||
|
|
615fcddcf7 | ||
|
|
8d6096636d | ||
|
|
4483f47668 | ||
|
|
ce7ce10d64 | ||
|
|
965408df57 | ||
|
|
4a37f06677 | ||
|
|
300f507efc | ||
|
|
345afd243e | ||
|
|
133796f2dd | ||
|
|
34989307db | ||
|
|
fb9a9b38a8 | ||
|
|
66b2e3b421 | ||
|
|
bc3e3d789d | ||
|
|
75c7d00b53 | ||
|
|
7e82c258ee | ||
|
|
a2cc6a0a87 | ||
|
|
0b42085353 | ||
|
|
5c57d03d6d | ||
|
|
12141d2d61 | ||
|
|
a5612107a6 | ||
|
|
c20b30d625 | ||
|
|
be8e0f319d | ||
|
|
0507d1a0c8 | ||
|
|
73c995c8ec | ||
|
|
5e68bf7c21 | ||
|
|
6a5b1a76cc | ||
|
|
b722c0d72d | ||
|
|
ca1476f7c7 | ||
|
|
d7a32a31a1 | ||
|
|
df9b387175 | ||
|
|
8cc6508b22 | ||
|
|
a0406b1406 | ||
|
|
93d61d0316 | ||
|
|
55cc415c30 | ||
|
|
1ec9641c9d | ||
|
|
5c05fd2877 | ||
|
|
6fe39b071f | ||
|
|
7458143bb8 | ||
|
|
2a83e405a7 | ||
|
|
7f88013689 | ||
|
|
0bcc26a811 |
+174
-4
@@ -513,9 +513,9 @@ stages:
|
||||
UMBRACO__CMS__WEBROUTING__UMBRACOAPPLICATIONURL: https://localhost:44331/
|
||||
ASPNETCORE_URLS: https://localhost:44331
|
||||
jobs:
|
||||
# E2E Tests
|
||||
# E2E Smoke Tests
|
||||
- job:
|
||||
displayName: E2E Tests (SQLite)
|
||||
displayName: E2E Smoke Tests (SQLite)
|
||||
# currently disabled due to DB locks randomly occuring.
|
||||
condition: eq(${{parameters.sqliteAcceptanceTests}}, True)
|
||||
variables:
|
||||
@@ -678,7 +678,7 @@ stages:
|
||||
testRunTitle: "$(Agent.JobName)"
|
||||
|
||||
- job:
|
||||
displayName: E2E Tests (SQL Server)
|
||||
displayName: E2E Smoke Tests (SQL Server)
|
||||
variables:
|
||||
# Connection string
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True
|
||||
@@ -862,6 +862,176 @@ stages:
|
||||
searchFolder: "tests/Umbraco.Tests.AcceptanceTest/results"
|
||||
testRunTitle: "$(Agent.JobName)"
|
||||
|
||||
- job:
|
||||
displayName: E2E Release Tests (SQL Server)
|
||||
variables:
|
||||
# Connection string
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
|
||||
condition: eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True')
|
||||
strategy:
|
||||
matrix:
|
||||
WindowsPart1Of3:
|
||||
vmImage: "windows-latest"
|
||||
testCommand: "npm run releaseTest -- --shard=1/3"
|
||||
WindowsPart2Of3:
|
||||
vmImage: "windows-latest"
|
||||
testCommand: "npm run releaseTest -- --shard=2/3"
|
||||
WindowsPart3Of3:
|
||||
vmImage: "windows-latest"
|
||||
testCommand: "npm run releaseTest -- --shard=3/3"
|
||||
pool:
|
||||
vmImage: $(vmImage)
|
||||
steps:
|
||||
# Setup test environment
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download NuGet artifacts
|
||||
inputs:
|
||||
artifact: nupkg
|
||||
path: $(Agent.BuildDirectory)/app/nupkg
|
||||
|
||||
- task: NodeTool@0
|
||||
displayName: Use Node.js $(nodeVersion)
|
||||
inputs:
|
||||
versionSpec: $(nodeVersion)
|
||||
|
||||
- task: UseDotNet@2
|
||||
displayName: Use .NET SDK from global.json
|
||||
inputs:
|
||||
useGlobalJson: true
|
||||
|
||||
- pwsh: |
|
||||
"UMBRACO_USER_LOGIN=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL)
|
||||
UMBRACO_USER_PASSWORD=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
|
||||
URL=$(ASPNETCORE_URLS)
|
||||
STORAGE_STAGE_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth/user.json
|
||||
CONSOLE_ERRORS_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/console-errors.json" | Out-File .env
|
||||
displayName: Generate .env
|
||||
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
|
||||
|
||||
# Cache and restore NPM packages
|
||||
- task: Cache@2
|
||||
displayName: Cache NPM packages
|
||||
inputs:
|
||||
key: 'npm_e2e | "$(Agent.OS)" | $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/package-lock.json'
|
||||
restoreKeys: |
|
||||
npm_e2e | "$(Agent.OS)"
|
||||
npm_e2e
|
||||
path: $(npm_config_cache)
|
||||
|
||||
- script: npm ci --no-fund --no-audit --prefer-offline
|
||||
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
|
||||
displayName: Restore NPM packages
|
||||
|
||||
# Build application
|
||||
- pwsh: |
|
||||
$cmsVersion = "$(Build.BuildNumber)" -replace "\+",".g"
|
||||
dotnet new nugetconfig
|
||||
dotnet nuget add source ./nupkg --name Local
|
||||
dotnet new install Umbraco.Templates::$cmsVersion
|
||||
dotnet new umbraco --name UmbracoProject --version $cmsVersion --exclude-gitignore --no-restore --no-update-check
|
||||
dotnet restore UmbracoProject
|
||||
cp $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/*.cs UmbracoProject
|
||||
dotnet build UmbracoProject --configuration $(buildConfiguration) --no-restore
|
||||
dotnet dev-certs https
|
||||
displayName: Build application
|
||||
workingDirectory: $(Agent.BuildDirectory)/app
|
||||
|
||||
# Start SQL Server
|
||||
- powershell: docker run --name mssql -d -p 1433:1433 -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=$(SA_PASSWORD)" mcr.microsoft.com/mssql/server:2022-latest
|
||||
displayName: Start SQL Server Docker image (Linux)
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
|
||||
|
||||
- pwsh: SqlLocalDB start MSSQLLocalDB
|
||||
displayName: Start SQL Server LocalDB (Windows)
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
|
||||
|
||||
# Run application
|
||||
- bash: |
|
||||
nohup dotnet run --project UmbracoProject --configuration $(buildConfiguration) --no-build --no-launch-profile > $(Build.ArtifactStagingDirectory)/playwright.log 2>&1 &
|
||||
echo "##vso[task.setvariable variable=AcceptanceTestProcessId]$!"
|
||||
displayName: Run application (Linux)
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
|
||||
workingDirectory: $(Agent.BuildDirectory)/app
|
||||
|
||||
- pwsh: |
|
||||
$process = Start-Process dotnet "run --project UmbracoProject --configuration $(buildConfiguration) --no-build --no-launch-profile 2>&1" -PassThru -NoNewWindow -RedirectStandardOutput $(Build.ArtifactStagingDirectory)/playwright.log
|
||||
Write-Host "##vso[task.setvariable variable=AcceptanceTestProcessId]$($process.Id)"
|
||||
displayName: Run application (Windows)
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
|
||||
workingDirectory: $(Agent.BuildDirectory)/app
|
||||
|
||||
# Wait for application to start responding to requests
|
||||
- pwsh: npx wait-on -v --interval 1000 --timeout 120000 $(ASPNETCORE_URLS)
|
||||
displayName: Wait for application
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
|
||||
# Install Playwright and dependencies
|
||||
- pwsh: npx playwright install chromium
|
||||
displayName: Install Playwright only with Chromium browser
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
|
||||
# Test
|
||||
- pwsh: $(testCommand)
|
||||
displayName: Run Playwright tests
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
env:
|
||||
CI: true
|
||||
CommitId: $(Build.SourceVersion)
|
||||
AgentOs: $(Agent.OS)
|
||||
|
||||
# Stop application
|
||||
- bash: kill -15 $(AcceptanceTestProcessId)
|
||||
displayName: Stop application (Linux)
|
||||
condition: and(ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Linux'))
|
||||
|
||||
- pwsh: Stop-Process -Id $(AcceptanceTestProcessId)
|
||||
displayName: Stop application (Windows)
|
||||
condition: and(ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Windows_NT'))
|
||||
|
||||
# Stop SQL Server
|
||||
- pwsh: docker stop mssql
|
||||
displayName: Stop SQL Server Docker image (Linux)
|
||||
condition: eq(variables['Agent.OS'], 'Linux')
|
||||
|
||||
- pwsh: SqlLocalDB stop MSSQLLocalDB
|
||||
displayName: Stop SQL Server LocalDB (Windows)
|
||||
condition: eq(variables['Agent.OS'], 'Windows_NT')
|
||||
|
||||
# Copy artifacts
|
||||
- pwsh: |
|
||||
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/results/*) {
|
||||
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results/* $(Build.ArtifactStagingDirectory) -Recurse
|
||||
}
|
||||
displayName: Copy Playwright results
|
||||
condition: succeededOrFailed()
|
||||
|
||||
# Copy console error log
|
||||
- pwsh: |
|
||||
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/console-errors.json) {
|
||||
Copy-Item tests/Umbraco.Tests.AcceptanceTest/console-errors.json $(Build.ArtifactStagingDirectory)
|
||||
}
|
||||
displayName: Copy console error log
|
||||
condition: succeededOrFailed()
|
||||
|
||||
# Publish test artifacts
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: Publish test artifacts
|
||||
condition: succeededOrFailed()
|
||||
inputs:
|
||||
targetPath: $(Build.ArtifactStagingDirectory)
|
||||
artifact: "Acceptance Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)"
|
||||
|
||||
# Publish test results
|
||||
- task: PublishTestResults@2
|
||||
displayName: "Publish test results"
|
||||
condition: succeededOrFailed()
|
||||
inputs:
|
||||
testResultsFormat: 'JUnit'
|
||||
testResultsFiles: '*.xml'
|
||||
searchFolder: "tests/Umbraco.Tests.AcceptanceTest/results"
|
||||
testRunTitle: "$(Agent.JobName)"
|
||||
|
||||
###############################################
|
||||
## Release
|
||||
###############################################
|
||||
@@ -1067,4 +1237,4 @@ stages:
|
||||
storage: umbracoapidocs
|
||||
ContainerName: "$web"
|
||||
BlobPrefix: v$(umbracoMajorVersion)/ui-api
|
||||
CleanTargetBeforeCopy: true
|
||||
CleanTargetBeforeCopy: true
|
||||
@@ -9,11 +9,10 @@ schedules:
|
||||
branches:
|
||||
include:
|
||||
- v15/dev
|
||||
- release/16.0
|
||||
- main
|
||||
|
||||
parameters:
|
||||
# Skipped due to DB locks
|
||||
# Skipped due to DB locks
|
||||
- name: sqliteAcceptanceTests
|
||||
displayName: Run SQLite Acceptance Tests
|
||||
type: boolean
|
||||
@@ -484,3 +483,51 @@ stages:
|
||||
testResultsFiles: '*.xml'
|
||||
searchFolder: "tests/Umbraco.Tests.AcceptanceTest/results"
|
||||
testRunTitle: "$(Agent.JobName)"
|
||||
|
||||
- stage: NotifySlackBot
|
||||
displayName: Notify Slack on Failure
|
||||
dependsOn: E2E
|
||||
# This stage will only run if the E2E tests fail or succeed with issues
|
||||
condition: or(
|
||||
eq(dependencies.E2E.result, 'failed'),
|
||||
eq(dependencies.E2E.result, 'succeededWithIssues'))
|
||||
jobs:
|
||||
- job: PostToSlack
|
||||
displayName: Send Slack Notification
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
# We send a payload to the Slack webhook URL, which will post a message to a specific channel
|
||||
- bash: |
|
||||
PROJECT_NAME_ENCODED=$(echo -n "$SYSTEM_TEAMPROJECT" | jq -s -R -r @uri)
|
||||
PIPELINE_URL="${SYSTEM_TEAMFOUNDATIONCOLLECTIONURI}${PROJECT_NAME_ENCODED}/_build/results?buildId=${BUILD_BUILDID}&view=ms.vss-test-web.build-test-results-tab"
|
||||
|
||||
PAYLOAD="{
|
||||
\"attachments\": [
|
||||
{
|
||||
\"color\": \"#ff0000\",
|
||||
\"pretext\": \"Nightly E2E pipeline *${BUILD_DEFINITIONNAME}* (#${BUILD_BUILDNUMBER}) failed!\",
|
||||
\"title\": \"View Failed E2E Test Results\",
|
||||
\"title_link\": \"$PIPELINE_URL\",
|
||||
\"fields\": [
|
||||
{
|
||||
\"title\": \"Pipeline\",
|
||||
\"value\": \"${BUILD_DEFINITIONNAME}\",
|
||||
\"short\": true
|
||||
},
|
||||
{
|
||||
\"title\": \"Build ID\",
|
||||
\"value\": \"${BUILD_BUILDID}\",
|
||||
\"short\": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}"
|
||||
|
||||
echo "Sending Slack message to: $PIPELINE_URL"
|
||||
curl -X POST -H 'Content-type: application/json' \
|
||||
--data "$PAYLOAD" \
|
||||
"$SLACK_WEBHOOK_URL"
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: $(E2ESLACKWEBHOOKURL)
|
||||
|
||||
@@ -4,7 +4,7 @@ using Umbraco.Cms.Core.Configuration.Models;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Configuration;
|
||||
|
||||
internal class ConfigureOpenIddict : IConfigureOptions<OpenIddictServerAspNetCoreOptions>
|
||||
internal sealed class ConfigureOpenIddict : IConfigureOptions<OpenIddictServerAspNetCoreOptions>
|
||||
{
|
||||
private readonly IOptions<GlobalSettings> _globalSettings;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Json;
|
||||
|
||||
internal class NamedSystemTextJsonInputFormatter : SystemTextJsonInputFormatter
|
||||
internal sealed class NamedSystemTextJsonInputFormatter : SystemTextJsonInputFormatter
|
||||
{
|
||||
private readonly string _jsonOptionsName;
|
||||
|
||||
|
||||
@@ -3,8 +3,7 @@ using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Json;
|
||||
|
||||
|
||||
internal class NamedSystemTextJsonOutputFormatter : SystemTextJsonOutputFormatter
|
||||
internal sealed class NamedSystemTextJsonOutputFormatter : SystemTextJsonOutputFormatter
|
||||
{
|
||||
private readonly string _jsonOptionsName;
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
|
||||
public class SubsetViewModel<T>
|
||||
{
|
||||
[Required]
|
||||
public long TotalBefore { get; set; }
|
||||
|
||||
[Required]
|
||||
public long TotalAfter { get; set; }
|
||||
|
||||
[Required]
|
||||
public IEnumerable<T> Items { get; set; } = Enumerable.Empty<T>();
|
||||
|
||||
public static SubsetViewModel<T> Empty() => new();
|
||||
}
|
||||
@@ -23,7 +23,12 @@ internal sealed class DeliveryApiOutputCachePolicy : IOutputCachePolicy
|
||||
.RequestServices
|
||||
.GetRequiredService<IRequestPreviewService>();
|
||||
|
||||
context.EnableOutputCaching = requestPreviewService.IsPreview() is false;
|
||||
IApiAccessService apiAccessService = context
|
||||
.HttpContext
|
||||
.RequestServices
|
||||
.GetRequiredService<IApiAccessService>();
|
||||
|
||||
context.EnableOutputCaching = requestPreviewService.IsPreview() is false && apiAccessService.HasPublicAccess();
|
||||
context.ResponseExpirationTimeSpan = _duration;
|
||||
context.CacheVaryByRules.HeaderNames = _varyByHeaderNames;
|
||||
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ public class ConfigureUmbracoMemberAuthenticationDeliveryApiSwaggerGenOptions :
|
||||
options.OperationFilter<DeliveryApiSecurityFilter>();
|
||||
}
|
||||
|
||||
private class DeliveryApiSecurityFilter : SwaggerFilterBase<ContentApiControllerBase>, IOperationFilter, IDocumentFilter
|
||||
private sealed class DeliveryApiSecurityFilter : SwaggerFilterBase<ContentApiControllerBase>, IOperationFilter, IDocumentFilter
|
||||
{
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
|
||||
@@ -13,7 +13,7 @@ internal sealed class ContextualizeFromAcceptHeadersAttribute : TypeFilterAttrib
|
||||
{
|
||||
}
|
||||
|
||||
private class LocalizeFromAcceptLanguageHeaderAttributeFilter : IActionFilter
|
||||
private sealed class LocalizeFromAcceptLanguageHeaderAttributeFilter : IActionFilter
|
||||
{
|
||||
private readonly IRequestCultureService _requestCultureService;
|
||||
private readonly IRequestSegmmentService _requestSegmentService;
|
||||
|
||||
@@ -11,7 +11,7 @@ internal sealed class DeliveryApiAccessAttribute : TypeFilterAttribute
|
||||
{
|
||||
}
|
||||
|
||||
private class DeliveryApiAccessFilter : IActionFilter
|
||||
private sealed class DeliveryApiAccessFilter : IActionFilter
|
||||
{
|
||||
private readonly IApiAccessService _apiAccessService;
|
||||
private readonly IRequestPreviewService _requestPreviewService;
|
||||
|
||||
@@ -11,7 +11,7 @@ internal sealed class DeliveryApiMediaAccessAttribute : TypeFilterAttribute
|
||||
{
|
||||
}
|
||||
|
||||
private class DeliveryApiMediaAccessFilter : IActionFilter
|
||||
private sealed class DeliveryApiMediaAccessFilter : IActionFilter
|
||||
{
|
||||
private readonly IApiAccessService _apiAccessService;
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ internal sealed class ValidateStartItemAttribute : TypeFilterAttribute
|
||||
{
|
||||
}
|
||||
|
||||
private class ValidateStartItemFilter : IActionFilter
|
||||
private sealed class ValidateStartItemFilter : IActionFilter
|
||||
{
|
||||
private readonly IRequestStartItemProviderAccessor _requestStartItemProviderAccessor;
|
||||
|
||||
|
||||
+35
-7
@@ -7,6 +7,7 @@ using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Sync;
|
||||
using Umbraco.Cms.Infrastructure.Security;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Handlers;
|
||||
@@ -18,18 +19,24 @@ internal sealed class InitializeMemberApplicationNotificationHandler : INotifica
|
||||
private readonly DeliveryApiSettings _deliveryApiSettings;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
private readonly IMemberClientCredentialsManager _memberClientCredentialsManager;
|
||||
private readonly IServerRoleAccessor _serverRoleAccessor;
|
||||
|
||||
private static readonly SemaphoreSlim _locker = new(1);
|
||||
private static bool _isInitialized = false;
|
||||
|
||||
public InitializeMemberApplicationNotificationHandler(
|
||||
IRuntimeState runtimeState,
|
||||
IOptions<DeliveryApiSettings> deliveryApiSettings,
|
||||
ILogger<InitializeMemberApplicationNotificationHandler> logger,
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
IMemberClientCredentialsManager memberClientCredentialsManager)
|
||||
IMemberClientCredentialsManager memberClientCredentialsManager,
|
||||
IServerRoleAccessor serverRoleAccessor)
|
||||
{
|
||||
_runtimeState = runtimeState;
|
||||
_logger = logger;
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_memberClientCredentialsManager = memberClientCredentialsManager;
|
||||
_serverRoleAccessor = serverRoleAccessor;
|
||||
_deliveryApiSettings = deliveryApiSettings.Value;
|
||||
}
|
||||
|
||||
@@ -40,13 +47,34 @@ internal sealed class InitializeMemberApplicationNotificationHandler : INotifica
|
||||
return;
|
||||
}
|
||||
|
||||
// we cannot inject the IMemberApplicationManager because it ultimately takes a dependency on the DbContext ... and during
|
||||
// install that is not allowed (no connection string means no DbContext)
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
IMemberApplicationManager memberApplicationManager = scope.ServiceProvider.GetRequiredService<IMemberApplicationManager>();
|
||||
if (_serverRoleAccessor.CurrentServerRole is ServerRole.Subscriber)
|
||||
{
|
||||
// subscriber instances should not alter the member application
|
||||
return;
|
||||
}
|
||||
|
||||
await HandleMemberApplication(memberApplicationManager, cancellationToken);
|
||||
await HandleMemberClientCredentialsApplication(memberApplicationManager, cancellationToken);
|
||||
try
|
||||
{
|
||||
await _locker.WaitAsync(cancellationToken);
|
||||
if (_isInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// we cannot inject the IMemberApplicationManager because it ultimately takes a dependency on the DbContext ... and during
|
||||
// install that is not allowed (no connection string means no DbContext)
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
IMemberApplicationManager memberApplicationManager = scope.ServiceProvider.GetRequiredService<IMemberApplicationManager>();
|
||||
|
||||
await HandleMemberApplication(memberApplicationManager, cancellationToken);
|
||||
await HandleMemberClientCredentialsApplication(memberApplicationManager, cancellationToken);
|
||||
|
||||
_isInitialized = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_locker.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleMemberApplication(IMemberApplicationManager memberApplicationManager, CancellationToken cancellationToken)
|
||||
|
||||
@@ -124,7 +124,7 @@ internal sealed class RequestContextOutputExpansionStrategyV2 : IOutputExpansion
|
||||
private object? GetPropertyValue(IPublishedProperty property)
|
||||
=> _propertyRenderer.GetPropertyValue(property, _expandProperties.Peek() is not null);
|
||||
|
||||
private class Node
|
||||
private sealed class Node
|
||||
{
|
||||
public string Key { get; private set; } = string.Empty;
|
||||
|
||||
|
||||
+7
-3
@@ -1,5 +1,6 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
@@ -13,7 +14,10 @@ public class SiblingsDataTypeTreeController : DataTypeTreeControllerBase
|
||||
}
|
||||
|
||||
[HttpGet("siblings")]
|
||||
[ProducesResponseType(typeof(IEnumerable<DataTypeTreeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public Task<ActionResult<IEnumerable<DataTypeTreeItemResponseModel>>> Siblings(CancellationToken cancellationToken, Guid target, int before, int after)
|
||||
=> GetSiblings(target, before, after);
|
||||
[ProducesResponseType(typeof(SubsetViewModel<DataTypeTreeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<SubsetViewModel<DataTypeTreeItemResponseModel>>> Siblings(CancellationToken cancellationToken, Guid target, int before, int after, bool foldersOnly = false)
|
||||
{
|
||||
RenderFoldersOnly(foldersOnly);
|
||||
return await GetSiblings(target, before, after);
|
||||
}
|
||||
}
|
||||
|
||||
+60
-3
@@ -1,11 +1,14 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Document.Item;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Entities;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Document.Item;
|
||||
|
||||
@@ -14,13 +17,52 @@ public class SearchDocumentItemController : DocumentItemControllerBase
|
||||
{
|
||||
private readonly IIndexedEntitySearchService _indexedEntitySearchService;
|
||||
private readonly IDocumentPresentationFactory _documentPresentationFactory;
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
|
||||
public SearchDocumentItemController(IIndexedEntitySearchService indexedEntitySearchService, IDocumentPresentationFactory documentPresentationFactory)
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public SearchDocumentItemController(
|
||||
IIndexedEntitySearchService indexedEntitySearchService,
|
||||
IDocumentPresentationFactory documentPresentationFactory,
|
||||
IDataTypeService dataTypeService)
|
||||
{
|
||||
_indexedEntitySearchService = indexedEntitySearchService;
|
||||
_documentPresentationFactory = documentPresentationFactory;
|
||||
_dataTypeService = dataTypeService;
|
||||
}
|
||||
|
||||
[Obsolete("Use the non-obsolete constructor instead, will be removed in v18")]
|
||||
public SearchDocumentItemController(
|
||||
IIndexedEntitySearchService indexedEntitySearchService,
|
||||
IDocumentPresentationFactory documentPresentationFactory)
|
||||
: this(
|
||||
indexedEntitySearchService,
|
||||
documentPresentationFactory,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IDataTypeService>())
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the overload taking all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public async Task<IActionResult> SearchWithTrashed(
|
||||
CancellationToken cancellationToken,
|
||||
string query,
|
||||
bool? trashed = null,
|
||||
string? culture = null,
|
||||
int skip = 0,
|
||||
int take = 100,
|
||||
Guid? parentId = null,
|
||||
[FromQuery] IEnumerable<Guid>? allowedDocumentTypes = null)
|
||||
=> await SearchWithTrashed(
|
||||
cancellationToken,
|
||||
query,
|
||||
trashed,
|
||||
culture,
|
||||
skip,
|
||||
take,
|
||||
parentId,
|
||||
allowedDocumentTypes,
|
||||
null);
|
||||
|
||||
[HttpGet("search")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedModel<DocumentItemResponseModel>), StatusCodes.Status200OK)]
|
||||
@@ -32,9 +74,21 @@ public class SearchDocumentItemController : DocumentItemControllerBase
|
||||
int skip = 0,
|
||||
int take = 100,
|
||||
Guid? parentId = null,
|
||||
[FromQuery] IEnumerable<Guid>? allowedDocumentTypes = null)
|
||||
[FromQuery] IEnumerable<Guid>? allowedDocumentTypes = null,
|
||||
Guid? dataTypeId = null)
|
||||
{
|
||||
PagedModel<IEntitySlim> searchResult = await _indexedEntitySearchService.SearchAsync(UmbracoObjectTypes.Document, query, parentId, allowedDocumentTypes, trashed, culture, skip, take);
|
||||
var ignoreUserStartNodes = await IgnoreUserStartNodes(dataTypeId);
|
||||
PagedModel<IEntitySlim> searchResult = await _indexedEntitySearchService.SearchAsync(
|
||||
UmbracoObjectTypes.Document,
|
||||
query,
|
||||
parentId,
|
||||
allowedDocumentTypes,
|
||||
trashed,
|
||||
culture,
|
||||
skip,
|
||||
take,
|
||||
ignoreUserStartNodes);
|
||||
|
||||
var result = new PagedModel<DocumentItemResponseModel>
|
||||
{
|
||||
Items = searchResult.Items.OfType<IDocumentEntitySlim>().Select(_documentPresentationFactory.CreateItemResponseModel),
|
||||
@@ -43,4 +97,7 @@ public class SearchDocumentItemController : DocumentItemControllerBase
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
private async Task<bool> IgnoreUserStartNodes(Guid? dataTypeKey) =>
|
||||
dataTypeKey is not null && await _dataTypeService.IsDataTypeIgnoringUserStartNodesAsync(dataTypeKey.Value);
|
||||
}
|
||||
|
||||
+6
-5
@@ -58,11 +58,12 @@ public class PublishDocumentWithDescendantsController : DocumentControllerBase
|
||||
true);
|
||||
|
||||
return attempt.Success && attempt.Result.AcceptedTaskId.HasValue
|
||||
? Ok(new PublishWithDescendantsResultModel
|
||||
{
|
||||
TaskId = attempt.Result.AcceptedTaskId.Value,
|
||||
IsComplete = false
|
||||
})
|
||||
? Ok(
|
||||
new PublishWithDescendantsResultModel
|
||||
{
|
||||
TaskId = attempt.Result.AcceptedTaskId.Value,
|
||||
IsComplete = false,
|
||||
})
|
||||
: DocumentPublishingOperationStatusResult(attempt.Status, failedBranchItems: attempt.Result.FailedItems);
|
||||
}
|
||||
|
||||
|
||||
+13
-11
@@ -49,21 +49,23 @@ public class PublishDocumentWithDescendantsResultController : DocumentController
|
||||
var isPublishing = await _contentPublishingService.IsPublishingBranchAsync(taskId);
|
||||
if (isPublishing)
|
||||
{
|
||||
return Ok(new PublishWithDescendantsResultModel
|
||||
{
|
||||
TaskId = taskId,
|
||||
IsComplete = false
|
||||
});
|
||||
};
|
||||
return Ok(
|
||||
new PublishWithDescendantsResultModel
|
||||
{
|
||||
TaskId = taskId,
|
||||
IsComplete = false,
|
||||
});
|
||||
}
|
||||
|
||||
// If completed, get the result and return the status.
|
||||
Attempt<ContentPublishingBranchResult, ContentPublishingOperationStatus> attempt = await _contentPublishingService.GetPublishBranchResultAsync(taskId);
|
||||
return attempt.Success
|
||||
? Ok(new PublishWithDescendantsResultModel
|
||||
{
|
||||
TaskId = taskId,
|
||||
IsComplete = true
|
||||
})
|
||||
? Ok(
|
||||
new PublishWithDescendantsResultModel
|
||||
{
|
||||
TaskId = taskId,
|
||||
IsComplete = true,
|
||||
})
|
||||
: DocumentPublishingOperationStatusResult(attempt.Status, failedBranchItems: attempt.Result.FailedItems);
|
||||
}
|
||||
}
|
||||
|
||||
+6
-1
@@ -36,7 +36,12 @@ public class ChildrenDocumentTreeController : DocumentTreeControllerBase
|
||||
[HttpGet("children")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<DocumentTreeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedViewModel<DocumentTreeItemResponseModel>>> Children(CancellationToken cancellationToken, Guid parentId, int skip = 0, int take = 100, Guid? dataTypeId = null)
|
||||
public async Task<ActionResult<PagedViewModel<DocumentTreeItemResponseModel>>> Children(
|
||||
CancellationToken cancellationToken,
|
||||
Guid parentId,
|
||||
int skip = 0,
|
||||
int take = 100,
|
||||
Guid? dataTypeId = null)
|
||||
{
|
||||
IgnoreUserStartNodesForDataType(dataTypeId);
|
||||
return await GetChildren(parentId, skip, take);
|
||||
|
||||
+7
-3
@@ -1,6 +1,7 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.Services.Entities;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
@@ -34,7 +35,10 @@ public class SiblingsDocumentTreeController : DocumentTreeControllerBase
|
||||
|
||||
[HttpGet("siblings")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(IEnumerable<DocumentTreeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public Task<ActionResult<IEnumerable<DocumentTreeItemResponseModel>>> Siblings(CancellationToken cancellationToken, Guid target, int before, int after)
|
||||
=> GetSiblings(target, before, after);
|
||||
[ProducesResponseType(typeof(SubsetViewModel<DocumentTreeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<SubsetViewModel<DocumentTreeItemResponseModel>>> Siblings(CancellationToken cancellationToken, Guid target, int before, int after, Guid? dataTypeId = null)
|
||||
{
|
||||
IgnoreUserStartNodesForDataType(dataTypeId);
|
||||
return await GetSiblings(target, before, after);
|
||||
}
|
||||
}
|
||||
|
||||
+9
-4
@@ -1,5 +1,6 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
@@ -14,11 +15,15 @@ public class SiblingsDocumentBlueprintTreeController : DocumentBlueprintTreeCont
|
||||
}
|
||||
|
||||
[HttpGet("siblings")]
|
||||
[ProducesResponseType(typeof(IEnumerable<DocumentBlueprintTreeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public Task<ActionResult<IEnumerable<DocumentBlueprintTreeItemResponseModel>>> Siblings(
|
||||
[ProducesResponseType(typeof(SubsetViewModel<DocumentBlueprintTreeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<SubsetViewModel<DocumentBlueprintTreeItemResponseModel>>> Siblings(
|
||||
CancellationToken cancellationToken,
|
||||
Guid target,
|
||||
int before,
|
||||
int after) =>
|
||||
GetSiblings(target, before, after);
|
||||
int after,
|
||||
bool foldersOnly = false)
|
||||
{
|
||||
RenderFoldersOnly(foldersOnly);
|
||||
return await GetSiblings(target, before, after);
|
||||
}
|
||||
}
|
||||
|
||||
+9
-4
@@ -1,5 +1,6 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
@@ -13,11 +14,15 @@ public class SiblingsDocumentTypeTreeController : DocumentTypeTreeControllerBase
|
||||
}
|
||||
|
||||
[HttpGet("siblings")]
|
||||
[ProducesResponseType(typeof(IEnumerable<DocumentTypeTreeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public Task<ActionResult<IEnumerable<DocumentTypeTreeItemResponseModel>>> Siblings(
|
||||
[ProducesResponseType(typeof(SubsetViewModel<DocumentTypeTreeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<SubsetViewModel<DocumentTypeTreeItemResponseModel>>> Siblings(
|
||||
CancellationToken cancellationToken,
|
||||
Guid target,
|
||||
int before,
|
||||
int after) =>
|
||||
GetSiblings(target, before, after);
|
||||
int after,
|
||||
bool foldersOnly = false)
|
||||
{
|
||||
RenderFoldersOnly(foldersOnly);
|
||||
return await GetSiblings(target, before, after);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
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.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Entities;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Media.Item;
|
||||
|
||||
@@ -14,13 +17,52 @@ public class SearchMediaItemController : MediaItemControllerBase
|
||||
{
|
||||
private readonly IIndexedEntitySearchService _indexedEntitySearchService;
|
||||
private readonly IMediaPresentationFactory _mediaPresentationFactory;
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
|
||||
public SearchMediaItemController(IIndexedEntitySearchService indexedEntitySearchService, IMediaPresentationFactory mediaPresentationFactory)
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public SearchMediaItemController(
|
||||
IIndexedEntitySearchService indexedEntitySearchService,
|
||||
IMediaPresentationFactory mediaPresentationFactory,
|
||||
IDataTypeService dataTypeService)
|
||||
{
|
||||
_indexedEntitySearchService = indexedEntitySearchService;
|
||||
_mediaPresentationFactory = mediaPresentationFactory;
|
||||
_dataTypeService = dataTypeService;
|
||||
}
|
||||
|
||||
[Obsolete("Use the non-obsolete constructor instead, will be removed in Umbraco 18.")]
|
||||
public SearchMediaItemController(
|
||||
IIndexedEntitySearchService indexedEntitySearchService,
|
||||
IMediaPresentationFactory mediaPresentationFactory)
|
||||
: this(
|
||||
indexedEntitySearchService,
|
||||
mediaPresentationFactory,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IDataTypeService>())
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the overload taking all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public async Task<IActionResult> SearchFromParentWithAllowedTypes(
|
||||
CancellationToken cancellationToken,
|
||||
string query,
|
||||
bool? trashed = null,
|
||||
string? culture = null,
|
||||
int skip = 0,
|
||||
int take = 100,
|
||||
Guid? parentId = null,
|
||||
[FromQuery] IEnumerable<Guid>? allowedMediaTypes = null)
|
||||
=> await SearchFromParentWithAllowedTypes(
|
||||
cancellationToken,
|
||||
query,
|
||||
trashed,
|
||||
culture,
|
||||
skip,
|
||||
take,
|
||||
parentId,
|
||||
allowedMediaTypes,
|
||||
null);
|
||||
|
||||
[HttpGet("search")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedModel<MediaItemResponseModel>), StatusCodes.Status200OK)]
|
||||
@@ -32,9 +74,20 @@ public class SearchMediaItemController : MediaItemControllerBase
|
||||
int skip = 0,
|
||||
int take = 100,
|
||||
Guid? parentId = null,
|
||||
[FromQuery]IEnumerable<Guid>? allowedMediaTypes = null)
|
||||
[FromQuery] IEnumerable<Guid>? allowedMediaTypes = null,
|
||||
Guid? dataTypeId = null)
|
||||
{
|
||||
PagedModel<IEntitySlim> searchResult = await _indexedEntitySearchService.SearchAsync(UmbracoObjectTypes.Media, query, parentId, allowedMediaTypes, trashed, culture, skip, take);
|
||||
var ignoreUserStartNodes = await IgnoreUserStartNodes(dataTypeId);
|
||||
PagedModel<IEntitySlim> searchResult = await _indexedEntitySearchService.SearchAsync(
|
||||
UmbracoObjectTypes.Media,
|
||||
query,
|
||||
parentId,
|
||||
allowedMediaTypes,
|
||||
trashed,
|
||||
culture,
|
||||
skip,
|
||||
take,
|
||||
ignoreUserStartNodes);
|
||||
var result = new PagedModel<MediaItemResponseModel>
|
||||
{
|
||||
Items = searchResult.Items.OfType<IMediaEntitySlim>().Select(_mediaPresentationFactory.CreateItemResponseModel),
|
||||
@@ -43,4 +96,7 @@ public class SearchMediaItemController : MediaItemControllerBase
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
private async Task<bool> IgnoreUserStartNodes(Guid? dataTypeKey) =>
|
||||
dataTypeKey is not null && await _dataTypeService.IsDataTypeIgnoringUserStartNodesAsync(dataTypeKey.Value);
|
||||
}
|
||||
|
||||
+7
-3
@@ -1,5 +1,6 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.Services.Entities;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
@@ -23,7 +24,10 @@ public class SiblingsMediaTreeController : MediaTreeControllerBase
|
||||
}
|
||||
|
||||
[HttpGet("siblings")]
|
||||
[ProducesResponseType(typeof(IEnumerable<MediaTreeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public Task<ActionResult<IEnumerable<MediaTreeItemResponseModel>>> Siblings(CancellationToken cancellationToken, Guid target, int before, int after)
|
||||
=> GetSiblings(target, before, after);
|
||||
[ProducesResponseType(typeof(SubsetViewModel<MediaTreeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<SubsetViewModel<MediaTreeItemResponseModel>>> Siblings(CancellationToken cancellationToken, Guid target, int before, int after, Guid? dataTypeId = null)
|
||||
{
|
||||
IgnoreUserStartNodesForDataType(dataTypeId);
|
||||
return await GetSiblings(target, before, after);
|
||||
}
|
||||
}
|
||||
|
||||
+12
-3
@@ -1,5 +1,6 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
@@ -13,7 +14,15 @@ public class SiblingsMediaTypeTreeController : MediaTypeTreeControllerBase
|
||||
}
|
||||
|
||||
[HttpGet("siblings")]
|
||||
[ProducesResponseType(typeof(IEnumerable<MediaTypeTreeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public Task<ActionResult<IEnumerable<MediaTypeTreeItemResponseModel>>> Siblings(CancellationToken cancellationToken, Guid target, int before, int after)
|
||||
=> GetSiblings(target, before, after);
|
||||
[ProducesResponseType(typeof(SubsetViewModel<MediaTypeTreeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<SubsetViewModel<MediaTypeTreeItemResponseModel>>> Siblings(
|
||||
CancellationToken cancellationToken,
|
||||
Guid target,
|
||||
int before,
|
||||
int after,
|
||||
bool foldersOnly = false)
|
||||
{
|
||||
RenderFoldersOnly(foldersOnly);
|
||||
return await GetSiblings(target, before, after);
|
||||
}
|
||||
}
|
||||
|
||||
+7
-6
@@ -1,6 +1,8 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.PublishedCache;
|
||||
@@ -15,9 +17,10 @@ public class RebuildPublishedCacheController : PublishedCacheControllerBase
|
||||
[HttpPost("rebuild")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public Task<IActionResult> Rebuild(CancellationToken cancellationToken)
|
||||
public async Task<IActionResult> Rebuild(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_databaseCacheRebuilder.IsRebuilding())
|
||||
Attempt<DatabaseCacheRebuildResult> attempt = await _databaseCacheRebuilder.RebuildAsync(true);
|
||||
if (attempt is { Success: false, Result: DatabaseCacheRebuildResult.AlreadyRunning })
|
||||
{
|
||||
var problemDetails = new ProblemDetails
|
||||
{
|
||||
@@ -26,11 +29,9 @@ public class RebuildPublishedCacheController : PublishedCacheControllerBase
|
||||
Status = StatusCodes.Status400BadRequest,
|
||||
Type = "Error",
|
||||
};
|
||||
|
||||
return Task.FromResult<IActionResult>(Conflict(problemDetails));
|
||||
return Conflict(problemDetails);
|
||||
}
|
||||
|
||||
_databaseCacheRebuilder.Rebuild(true);
|
||||
return Task.FromResult<IActionResult>(Ok());
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
|
||||
+7
-6
@@ -16,12 +16,13 @@ public class RebuildPublishedCacheStatusController : PublishedCacheControllerBas
|
||||
[HttpGet("rebuild/status")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(RebuildStatusModel), StatusCodes.Status200OK)]
|
||||
public Task<IActionResult> Status(CancellationToken cancellationToken)
|
||||
public async Task<IActionResult> Status(CancellationToken cancellationToken)
|
||||
{
|
||||
var isRebuilding = _databaseCacheRebuilder.IsRebuilding();
|
||||
return Task.FromResult((IActionResult)Ok(new RebuildStatusModel
|
||||
{
|
||||
IsRebuilding = isRebuilding
|
||||
}));
|
||||
var isRebuilding = await _databaseCacheRebuilder.IsRebuildingAsync();
|
||||
return Ok(
|
||||
new RebuildStatusModel
|
||||
{
|
||||
IsRebuilding = isRebuilding,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+4
-3
@@ -1,5 +1,6 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
@@ -13,11 +14,11 @@ public class SiblingsTemplateTreeController : TemplateTreeControllerBase
|
||||
}
|
||||
|
||||
[HttpGet("siblings")]
|
||||
[ProducesResponseType(typeof(IEnumerable<NamedEntityTreeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public Task<ActionResult<IEnumerable<NamedEntityTreeItemResponseModel>>> Siblings(
|
||||
[ProducesResponseType(typeof(SubsetViewModel<NamedEntityTreeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<SubsetViewModel<NamedEntityTreeItemResponseModel>>> Siblings(
|
||||
CancellationToken cancellationToken,
|
||||
Guid target,
|
||||
int before,
|
||||
int after) =>
|
||||
GetSiblings(target, before, after);
|
||||
await GetSiblings(target, before, after);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.ViewModels;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
@@ -44,12 +44,12 @@ public abstract class EntityTreeControllerBase<TItem> : ManagementApiControllerB
|
||||
return Task.FromResult<ActionResult<PagedViewModel<TItem>>>(Ok(result));
|
||||
}
|
||||
|
||||
protected Task<ActionResult<IEnumerable<TItem>>> GetSiblings(Guid target, int before, int after)
|
||||
protected Task<ActionResult<SubsetViewModel<TItem>>> GetSiblings(Guid target, int before, int after)
|
||||
{
|
||||
IEntitySlim[] siblings = EntityService.GetSiblings(target, ItemObjectType, before, after, ItemOrdering).ToArray();
|
||||
IEntitySlim[] siblings = GetSiblingEntities(target, before, after, out var totalBefore, out var totalAfter);
|
||||
if (siblings.Length == 0)
|
||||
{
|
||||
return Task.FromResult<ActionResult<IEnumerable<TItem>>>(NotFound());
|
||||
return Task.FromResult<ActionResult<SubsetViewModel<TItem>>>(NotFound());
|
||||
}
|
||||
|
||||
IEntitySlim? entity = siblings.FirstOrDefault();
|
||||
@@ -57,8 +57,11 @@ public abstract class EntityTreeControllerBase<TItem> : ManagementApiControllerB
|
||||
? EntityService.GetKey(entity.ParentId, ItemObjectType).Result
|
||||
: Constants.System.RootKey;
|
||||
|
||||
TItem[] treeItemsViewModels = MapTreeItemViewModels(parentKey, siblings);
|
||||
return Task.FromResult<ActionResult<IEnumerable<TItem>>>(Ok(treeItemsViewModels));
|
||||
TItem[] treeItemViewModels = MapTreeItemViewModels(parentKey, siblings);
|
||||
|
||||
SubsetViewModel<TItem> result = SubsetViewModel(treeItemViewModels, totalBefore, totalAfter);
|
||||
|
||||
return Task.FromResult<ActionResult<SubsetViewModel<TItem>>>(Ok(result));
|
||||
}
|
||||
|
||||
protected virtual async Task<ActionResult<IEnumerable<TItem>>> GetAncestors(Guid descendantKey, bool includeSelf = true)
|
||||
@@ -110,7 +113,8 @@ public abstract class EntityTreeControllerBase<TItem> : ManagementApiControllerB
|
||||
.ToArray();
|
||||
|
||||
protected virtual IEntitySlim[] GetPagedChildEntities(Guid parentKey, int skip, int take, out long totalItems) =>
|
||||
EntityService.GetPagedChildren(
|
||||
EntityService
|
||||
.GetPagedChildren(
|
||||
parentKey,
|
||||
ItemObjectType,
|
||||
skip,
|
||||
@@ -119,6 +123,18 @@ public abstract class EntityTreeControllerBase<TItem> : ManagementApiControllerB
|
||||
ordering: ItemOrdering)
|
||||
.ToArray();
|
||||
|
||||
protected virtual IEntitySlim[] GetSiblingEntities(Guid target, int before, int after, out long totalBefore, out long totalAfter) =>
|
||||
EntityService
|
||||
.GetSiblings(
|
||||
target,
|
||||
[ItemObjectType],
|
||||
before,
|
||||
after,
|
||||
out totalBefore,
|
||||
out totalAfter,
|
||||
ordering: ItemOrdering)
|
||||
.ToArray();
|
||||
|
||||
protected virtual TItem[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities)
|
||||
=> entities.Select(entity => MapTreeItemViewModel(parentKey, entity)).ToArray();
|
||||
|
||||
@@ -141,4 +157,7 @@ public abstract class EntityTreeControllerBase<TItem> : ManagementApiControllerB
|
||||
|
||||
protected PagedViewModel<TItem> PagedViewModel(IEnumerable<TItem> treeItemViewModels, long totalItems)
|
||||
=> new() { Total = totalItems, Items = treeItemViewModels };
|
||||
|
||||
protected SubsetViewModel<TItem> SubsetViewModel(IEnumerable<TItem> treeItemViewModels, long totalBefore, long totalAfter)
|
||||
=> new() { TotalBefore = totalBefore, TotalAfter = totalAfter, Items = treeItemViewModels };
|
||||
}
|
||||
|
||||
@@ -51,6 +51,24 @@ public abstract class FolderTreeControllerBase<TItem> : NamedEntityTreeControlle
|
||||
take,
|
||||
out totalItems);
|
||||
|
||||
protected override IEntitySlim[] GetSiblingEntities(Guid target, int before, int after, out long totalBefore, out long totalAfter)
|
||||
{
|
||||
totalBefore = 0;
|
||||
totalAfter = 0;
|
||||
|
||||
UmbracoObjectTypes[] siblingObjectTypes = GetObjectTypes();
|
||||
|
||||
return EntityService.GetSiblings(
|
||||
target,
|
||||
siblingObjectTypes,
|
||||
before,
|
||||
after,
|
||||
out totalBefore,
|
||||
out totalAfter,
|
||||
ordering: ItemOrdering)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
protected override TItem MapTreeItemViewModel(Guid? parentKey, IEntitySlim entity)
|
||||
{
|
||||
TItem viewModel = base.MapTreeItemViewModel(parentKey, entity);
|
||||
@@ -93,19 +111,19 @@ public abstract class FolderTreeControllerBase<TItem> : NamedEntityTreeControlle
|
||||
{
|
||||
totalItems = 0;
|
||||
|
||||
UmbracoObjectTypes[] childObjectTypes = _foldersOnly ? [FolderObjectType] : [FolderObjectType, ItemObjectType];
|
||||
UmbracoObjectTypes[] childObjectTypes = GetObjectTypes();
|
||||
|
||||
IEntitySlim[] itemEntities = EntityService.GetPagedChildren(
|
||||
parentKey,
|
||||
[FolderObjectType, ItemObjectType],
|
||||
childObjectTypes,
|
||||
skip,
|
||||
take,
|
||||
false,
|
||||
out totalItems,
|
||||
ordering: ItemOrdering)
|
||||
.ToArray();
|
||||
|
||||
return itemEntities;
|
||||
return EntityService.GetPagedChildren(
|
||||
parentKey,
|
||||
[FolderObjectType, ItemObjectType],
|
||||
childObjectTypes,
|
||||
skip,
|
||||
take,
|
||||
false,
|
||||
out totalItems,
|
||||
ordering: ItemOrdering)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private UmbracoObjectTypes[] GetObjectTypes() => _foldersOnly ? [FolderObjectType] : [FolderObjectType, ItemObjectType];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Umbraco.Cms.Api.Management.Models.Entities;
|
||||
using Umbraco.Cms.Api.Management.Models.Entities;
|
||||
using Umbraco.Cms.Api.Management.Services.Entities;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
using Umbraco.Cms.Core;
|
||||
@@ -59,6 +59,26 @@ public abstract class UserStartNodeTreeControllerBase<TItem> : EntityTreeControl
|
||||
return CalculateAccessMap(() => userAccessEntities, out _);
|
||||
}
|
||||
|
||||
protected override IEntitySlim[] GetSiblingEntities(Guid target, int before, int after, out long totalBefore, out long totalAfter)
|
||||
{
|
||||
if (UserHasRootAccess() || IgnoreUserStartNodes())
|
||||
{
|
||||
return base.GetSiblingEntities(target, before, after, out totalBefore, out totalAfter);
|
||||
}
|
||||
|
||||
IEnumerable<UserAccessEntity> userAccessEntities = _userStartNodeEntitiesService.SiblingUserAccessEntities(
|
||||
ItemObjectType,
|
||||
UserStartNodePaths,
|
||||
target,
|
||||
before,
|
||||
after,
|
||||
ItemOrdering,
|
||||
out totalBefore,
|
||||
out totalAfter);
|
||||
|
||||
return CalculateAccessMap(() => userAccessEntities, out _);
|
||||
}
|
||||
|
||||
protected override TItem[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities)
|
||||
{
|
||||
if (UserHasRootAccess() || IgnoreUserStartNodes())
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@ public static class BackOfficeAuthBuilderExtensions
|
||||
}
|
||||
}
|
||||
|
||||
internal class BackofficePipelineFilter : UmbracoPipelineFilter
|
||||
internal sealed class BackofficePipelineFilter : UmbracoPipelineFilter
|
||||
{
|
||||
public BackofficePipelineFilter(string name)
|
||||
: base(name)
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace Umbraco.Cms.Api.Management.DependencyInjection;
|
||||
|
||||
internal static class ManagementApiConfiguration
|
||||
{
|
||||
internal const string ApiSecurityName = "Backoffice User";
|
||||
internal const string ApiSecurityName = "Backoffice-User";
|
||||
internal const string ApiTitle = "Umbraco Management API";
|
||||
|
||||
internal const string ApiName = "management";
|
||||
|
||||
@@ -13,7 +13,7 @@ using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Factories;
|
||||
|
||||
internal class PackagePresentationFactory : IPackagePresentationFactory
|
||||
internal sealed class PackagePresentationFactory : IPackagePresentationFactory
|
||||
{
|
||||
private readonly IUmbracoMapper _umbracoMapper;
|
||||
private readonly IRuntimeState _runtimeState;
|
||||
|
||||
@@ -8,7 +8,7 @@ using Umbraco.Cms.Core.Webhooks;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Factories;
|
||||
|
||||
internal class WebhookPresentationFactory : IWebhookPresentationFactory
|
||||
internal sealed class WebhookPresentationFactory : IWebhookPresentationFactory
|
||||
{
|
||||
private readonly WebhookEventCollection _webhookEventCollection;
|
||||
private readonly IHostingEnvironment _hostingEnvironment;
|
||||
|
||||
@@ -21,7 +21,7 @@ public sealed class AppendEventMessagesAttribute : TypeFilterAttribute
|
||||
{
|
||||
}
|
||||
|
||||
private class AppendEventMessagesFilter : IActionFilter
|
||||
private sealed class AppendEventMessagesFilter : IActionFilter
|
||||
{
|
||||
private readonly IEventMessagesFactory _eventMessagesFactory;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
|
||||
+2
-2
@@ -4,14 +4,14 @@ using Umbraco.Cms.Core.Configuration.Models;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Filters;
|
||||
|
||||
internal class UserPasswordEnsureMinimumResponseTimeAttribute : TypeFilterAttribute
|
||||
internal sealed class UserPasswordEnsureMinimumResponseTimeAttribute : TypeFilterAttribute
|
||||
{
|
||||
public UserPasswordEnsureMinimumResponseTimeAttribute()
|
||||
: base(typeof(UserPasswordEnsureMinimumResponseTimeFilter))
|
||||
{
|
||||
}
|
||||
|
||||
private class UserPasswordEnsureMinimumResponseTimeFilter : EnsureMinimumResponseTimeFilter
|
||||
private sealed class UserPasswordEnsureMinimumResponseTimeFilter : EnsureMinimumResponseTimeFilter
|
||||
{
|
||||
public UserPasswordEnsureMinimumResponseTimeFilter(IOptions<UserPasswordConfigurationSettings> options)
|
||||
: base(options.Value.MinimumResponseTime)
|
||||
|
||||
@@ -45,7 +45,7 @@ public static class SectionMapper
|
||||
return name;
|
||||
}
|
||||
|
||||
private class SectionMapping
|
||||
private sealed class SectionMapping
|
||||
{
|
||||
public required string Alias { get; init; }
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -2,7 +2,7 @@ using Umbraco.Cms.Api.Management.DependencyInjection;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.OpenApi;
|
||||
|
||||
internal class BackOfficeSecurityRequirementsOperationFilter : BackOfficeSecurityRequirementsOperationFilterBase
|
||||
internal sealed class BackOfficeSecurityRequirementsOperationFilter : BackOfficeSecurityRequirementsOperationFilterBase
|
||||
{
|
||||
protected override string ApiName => ManagementApiConfiguration.ApiName;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ using Umbraco.Cms.Core;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.OpenApi;
|
||||
|
||||
internal class NotificationHeaderFilter : IOperationFilter
|
||||
internal sealed class NotificationHeaderFilter : IOperationFilter
|
||||
{
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@ using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.OpenApi;
|
||||
|
||||
internal class ResponseHeaderOperationFilter : IOperationFilter
|
||||
internal sealed class ResponseHeaderOperationFilter : IOperationFilter
|
||||
{
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
@@ -29,7 +29,7 @@ internal class ResponseHeaderOperationFilter : IOperationFilter
|
||||
}
|
||||
}
|
||||
|
||||
private void SetHeader(OpenApiResponse value, string headerName, string description, string type, string? format = null)
|
||||
private static void SetHeader(OpenApiResponse value, string headerName, string description, string type, string? format = null)
|
||||
{
|
||||
|
||||
if (value.Headers is null)
|
||||
|
||||
@@ -61,7 +61,7 @@ public class BackOfficeAuthenticationBuilder : AuthenticationBuilder
|
||||
// TODO: We could override and throw NotImplementedException for other methods?
|
||||
|
||||
// Ensures that the sign in scheme is always the Umbraco back office external type
|
||||
internal class EnsureBackOfficeScheme<TOptions> : IPostConfigureOptions<TOptions>
|
||||
internal sealed class EnsureBackOfficeScheme<TOptions> : IPostConfigureOptions<TOptions>
|
||||
where TOptions : RemoteAuthenticationOptions
|
||||
{
|
||||
public void PostConfigure(string? name, TOptions options)
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Umbraco.Cms.Api.Management.Security;
|
||||
/// <summary>
|
||||
/// Custom secure format that ensures the Identity in the ticket is verified <see cref="ClaimsIdentity" />
|
||||
/// </summary>
|
||||
internal class BackOfficeSecureDataFormat : ISecureDataFormat<AuthenticationTicket>
|
||||
internal sealed class BackOfficeSecureDataFormat : ISecureDataFormat<AuthenticationTicket>
|
||||
{
|
||||
private readonly TimeSpan _loginTimeout;
|
||||
private readonly ISecureDataFormat<AuthenticationTicket> _ticketDataFormat;
|
||||
|
||||
@@ -7,7 +7,7 @@ internal sealed class UserConnectionManager : IUserConnectionManager
|
||||
{
|
||||
// We use a normal dictionary instead of ConcurrentDictionary, since we need to lock the set anyways.
|
||||
private readonly Dictionary<Guid, HashSet<string>> _connections = new();
|
||||
private readonly object _lock = new();
|
||||
private readonly Lock _lock = new();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ISet<string> GetConnections(Guid userKey)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Entities;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Api.Management.Models.Entities;
|
||||
@@ -64,6 +64,37 @@ public interface IUserStartNodeEntitiesService
|
||||
/// </remarks>
|
||||
IEnumerable<UserAccessEntity> ChildUserAccessEntities(IEnumerable<IEntitySlim> candidateChildren, string[] userStartNodePaths);
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the applicable sibling entities for a given object type for users without root access.
|
||||
/// </summary>
|
||||
/// <param name="umbracoObjectType">The object type.</param>
|
||||
/// <param name="userStartNodePaths">The calculated start node paths for the user.</param>
|
||||
/// <param name="targetKey">The key of the target.</param>
|
||||
/// <param name="before">The number of applicable siblings to retrieve before the target.</param>
|
||||
/// <param name="after">The number of applicable siblings to retrieve after the target.</param>
|
||||
/// <param name="ordering">The ordering to apply when fetching and paginating the children.</param>
|
||||
/// <param name="totalBefore">Outputs the total number of siblings before the target entity.</param>
|
||||
/// <param name="totalAfter">Outputs the total number of siblings after the target entity.</param>
|
||||
/// <returns>A list of sibling entities applicable for the user.</returns>
|
||||
/// <remarks>
|
||||
/// The returned entities may include entities that outside of the user start node scope, but are needed to
|
||||
/// for browsing to the actual user start nodes. These entities will be marked as "no access" entities.
|
||||
/// </remarks>
|
||||
IEnumerable<UserAccessEntity> SiblingUserAccessEntities(
|
||||
UmbracoObjectTypes umbracoObjectType,
|
||||
string[] userStartNodePaths,
|
||||
Guid targetKey,
|
||||
int before,
|
||||
int after,
|
||||
Ordering ordering,
|
||||
out long totalBefore,
|
||||
out long totalAfter)
|
||||
{
|
||||
totalBefore = 0;
|
||||
totalAfter = 0;
|
||||
return [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the access level of a collection of entities for users without root access.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Entities;
|
||||
@@ -36,17 +36,17 @@ public class UserStartNodeEntitiesService : IUserStartNodeEntitiesService
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<UserAccessEntity> RootUserAccessEntities(UmbracoObjectTypes umbracoObjectType, int[] userStartNodeIds)
|
||||
{
|
||||
// root entities for users without root access should include:
|
||||
// Root entities for users without root access should include:
|
||||
// - the start nodes that are actual root entities (level == 1)
|
||||
// - the root level ancestors to the rest of the start nodes (required for browsing to the actual start nodes - will be marked as "no access")
|
||||
IEntitySlim[] userStartEntities = userStartNodeIds.Any()
|
||||
? _entityService.GetAll(umbracoObjectType, userStartNodeIds).ToArray()
|
||||
: Array.Empty<IEntitySlim>();
|
||||
|
||||
// find the start nodes that are at root level (level == 1)
|
||||
// Find the start nodes that are at root level (level == 1).
|
||||
IEntitySlim[] allowedTopmostEntities = userStartEntities.Where(entity => entity.Level == 1).ToArray();
|
||||
|
||||
// find the root level ancestors of the rest of the start nodes, and add those as well
|
||||
// Find the root level ancestors of the rest of the start nodes, and add those as well.
|
||||
var nonAllowedTopmostEntityIds = userStartEntities.Except(allowedTopmostEntities)
|
||||
.Select(entity => int.TryParse(entity.Path.Split(Constants.CharArrays.Comma).Skip(1).FirstOrDefault(), out var id) ? id : 0)
|
||||
.Where(id => id > 0)
|
||||
@@ -63,7 +63,15 @@ public class UserStartNodeEntitiesService : IUserStartNodeEntitiesService
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public IEnumerable<UserAccessEntity> ChildUserAccessEntities(UmbracoObjectTypes umbracoObjectType, string[] userStartNodePaths, Guid parentKey, int skip, int take, Ordering ordering, out long totalItems)
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<UserAccessEntity> ChildUserAccessEntities(
|
||||
UmbracoObjectTypes umbracoObjectType,
|
||||
string[] userStartNodePaths,
|
||||
Guid parentKey,
|
||||
int skip,
|
||||
int take,
|
||||
Ordering ordering,
|
||||
out long totalItems)
|
||||
{
|
||||
Attempt<int> parentIdAttempt = _idKeyMap.GetIdForKey(parentKey, umbracoObjectType);
|
||||
if (parentIdAttempt.Success is false)
|
||||
@@ -83,40 +91,46 @@ public class UserStartNodeEntitiesService : IUserStartNodeEntitiesService
|
||||
IEntitySlim[] children;
|
||||
if (userStartNodePaths.Any(path => $"{parent.Path},".StartsWith($"{path},")))
|
||||
{
|
||||
// the requested parent is one of the user start nodes (or a descendant of one), all children are by definition allowed
|
||||
// The requested parent is one of the user start nodes (or a descendant of one), all children are by definition allowed.
|
||||
children = _entityService.GetPagedChildren(parentKey, umbracoObjectType, skip, take, out totalItems, ordering: ordering).ToArray();
|
||||
return ChildUserAccessEntities(children, userStartNodePaths);
|
||||
}
|
||||
|
||||
// if one or more of the user start nodes are descendants of the requested parent, find the "next child IDs" in those user start node paths
|
||||
// - e.g. given the user start node path "-1,2,3,4,5", if the requested parent ID is 3, the "next child ID" is 4.
|
||||
var userStartNodePathIds = userStartNodePaths.Select(path => path.Split(Constants.CharArrays.Comma).Select(int.Parse).ToArray()).ToArray();
|
||||
var allowedChildIds = userStartNodePathIds
|
||||
.Where(ids => ids.Contains(parentId))
|
||||
// given the previous checks, the parent ID can never be the last in the user start node path, so this is safe
|
||||
.Select(ids => ids[ids.IndexOf(parentId) + 1])
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
int[] allowedChildIds = GetAllowedIds(userStartNodePaths, parentId);
|
||||
|
||||
totalItems = allowedChildIds.Length;
|
||||
if (allowedChildIds.Length == 0)
|
||||
{
|
||||
// the requested parent is outside the scope of any user start nodes
|
||||
// The requested parent is outside the scope of any user start nodes.
|
||||
return [];
|
||||
}
|
||||
|
||||
// even though we know the IDs of the allowed child entities to fetch, we still use a Query to yield correctly sorted children
|
||||
// Even though we know the IDs of the allowed child entities to fetch, we still use a Query to yield correctly sorted children.
|
||||
IQuery<IUmbracoEntity> query = _scopeProvider.CreateQuery<IUmbracoEntity>().Where(x => allowedChildIds.Contains(x.Id));
|
||||
children = _entityService.GetPagedChildren(parentKey, umbracoObjectType, skip, take, out totalItems, query, ordering).ToArray();
|
||||
return ChildUserAccessEntities(children, userStartNodePaths);
|
||||
}
|
||||
|
||||
private static int[] GetAllowedIds(string[] userStartNodePaths, int parentId)
|
||||
{
|
||||
// If one or more of the user start nodes are descendants of the requested parent, find the "next child IDs" in those user start node paths
|
||||
// that are the final entries in the path.
|
||||
// E.g. given the user start node path "-1,2,3,4,5", if the requested parent ID is 3, the "next child ID" is 4.
|
||||
var userStartNodePathIds = userStartNodePaths.Select(path => path.Split(Constants.CharArrays.Comma).Select(int.Parse).ToArray()).ToArray();
|
||||
return userStartNodePathIds
|
||||
.Where(ids => ids.Contains(parentId))
|
||||
.Select(ids => ids[ids.IndexOf(parentId) + 1]) // Given the previous checks, the parent ID can never be the last in the user start node path, so this is safe
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<UserAccessEntity> ChildUserAccessEntities(IEnumerable<IEntitySlim> candidateChildren, string[] userStartNodePaths)
|
||||
// child entities for users without root access should include:
|
||||
|
||||
// Child or sibling entities for users without root access should include:
|
||||
// - children that are descendant-or-self of a user start node
|
||||
// - children that are ancestors of a user start node (required for browsing to the actual start nodes - will be marked as "no access")
|
||||
// all other candidate children should be discarded
|
||||
// All other candidate children should be discarded.
|
||||
=> candidateChildren.Select(child =>
|
||||
{
|
||||
// is descendant-or-self of a start node?
|
||||
@@ -134,9 +148,72 @@ public class UserStartNodeEntitiesService : IUserStartNodeEntitiesService
|
||||
return null;
|
||||
}).WhereNotNull().ToArray();
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<UserAccessEntity> SiblingUserAccessEntities(
|
||||
UmbracoObjectTypes umbracoObjectType,
|
||||
string[] userStartNodePaths,
|
||||
Guid targetKey,
|
||||
int before,
|
||||
int after,
|
||||
Ordering ordering,
|
||||
out long totalBefore,
|
||||
out long totalAfter
|
||||
)
|
||||
{
|
||||
Attempt<int> targetIdAttempt = _idKeyMap.GetIdForKey(targetKey, umbracoObjectType);
|
||||
if (targetIdAttempt.Success is false)
|
||||
{
|
||||
totalBefore = 0;
|
||||
totalAfter = 0;
|
||||
return [];
|
||||
}
|
||||
|
||||
var targetId = targetIdAttempt.Result;
|
||||
IEntitySlim? target = _entityService.Get(targetId);
|
||||
if (target is null)
|
||||
{
|
||||
totalBefore = 0;
|
||||
totalAfter = 0;
|
||||
return [];
|
||||
}
|
||||
|
||||
IEntitySlim[] siblings;
|
||||
|
||||
IEntitySlim? targetParent = _entityService.Get(target.ParentId);
|
||||
if (targetParent is null) // Even if the parent is the root, we still expect to get a value here.
|
||||
{
|
||||
totalBefore = 0;
|
||||
totalAfter = 0;
|
||||
return [];
|
||||
}
|
||||
|
||||
if (userStartNodePaths.Any(path => $"{targetParent?.Path},".StartsWith($"{path},")))
|
||||
{
|
||||
// The requested parent of the target is one of the user start nodes (or a descendant of one), all siblings are by definition allowed.
|
||||
siblings = _entityService.GetSiblings(targetKey, [umbracoObjectType], before, after, out totalBefore, out totalAfter, ordering: ordering).ToArray();
|
||||
return ChildUserAccessEntities(siblings, userStartNodePaths);
|
||||
}
|
||||
|
||||
int[] allowedSiblingIds = GetAllowedIds(userStartNodePaths, targetParent.Id);
|
||||
|
||||
if (allowedSiblingIds.Length == 0)
|
||||
{
|
||||
// The requested target is outside the scope of any user start nodes.
|
||||
totalBefore = 0;
|
||||
totalAfter = 0;
|
||||
return [];
|
||||
}
|
||||
|
||||
// Even though we know the IDs of the allowed sibling entities to fetch, we still use a Query to yield correctly sorted children.
|
||||
IQuery<IUmbracoEntity> query = _scopeProvider.CreateQuery<IUmbracoEntity>().Where(x => allowedSiblingIds.Contains(x.Id));
|
||||
siblings = _entityService.GetSiblings(targetKey, [umbracoObjectType], before, after, out totalBefore, out totalAfter, query, ordering).ToArray();
|
||||
return ChildUserAccessEntities(siblings, userStartNodePaths);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<UserAccessEntity> UserAccessEntities(IEnumerable<IEntitySlim> entities, string[] userStartNodePaths)
|
||||
// entities for users without root access should include:
|
||||
|
||||
// Entities for users without root access should include:
|
||||
// - entities that are descendant-or-self of a user start node as regular entities
|
||||
// - all other entities as "no access" entities
|
||||
=> entities.Select(entity => new UserAccessEntity(entity, IsDescendantOrSelf(entity, userStartNodePaths))).ToArray();
|
||||
|
||||
@@ -19,7 +19,7 @@ public class UmbracoEFCoreComposer : IComposer
|
||||
builder.AddNotificationAsyncHandler<DatabaseSchemaAndDataCreatedNotification, EFCoreCreateTablesNotificationHandler>();
|
||||
builder.AddNotificationAsyncHandler<UnattendedInstallNotification, EFCoreCreateTablesNotificationHandler>();
|
||||
|
||||
builder.Services.AddUmbracoDbContext<UmbracoDbContext>((options) =>
|
||||
builder.Services.AddUmbracoDbContext<UmbracoDbContext>((provider, options, connectionString, providerName) =>
|
||||
{
|
||||
// Register the entity sets needed by OpenIddict.
|
||||
options.UseOpenIddict();
|
||||
|
||||
+53
-14
@@ -17,32 +17,50 @@ public static class UmbracoEFCoreServiceCollectionExtensions
|
||||
/// <summary>
|
||||
/// Adds a EFCore DbContext with all the services needed to integrate with Umbraco scopes.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="services"></param>
|
||||
/// <param name="optionsAction"></param>
|
||||
/// <returns></returns>
|
||||
public static IServiceCollection AddUmbracoDbContext<T>(this IServiceCollection services, Action<DbContextOptionsBuilder>? optionsAction = null)
|
||||
[Obsolete("Please use the method overload that takes all parameters for the optionsAction. Scheduled for removal in Umbraco 18.")]
|
||||
public static IServiceCollection AddUmbracoDbContext<T>(
|
||||
this IServiceCollection services,
|
||||
Action<DbContextOptionsBuilder>? optionsAction = null)
|
||||
where T : DbContext
|
||||
=> AddUmbracoDbContext<T>(services, (sp, optionsBuilder, connectionString, providerName) => optionsAction?.Invoke(optionsBuilder));
|
||||
|
||||
/// <summary>
|
||||
/// Adds a EFCore DbContext with all the services needed to integrate with Umbraco scopes.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddUmbracoDbContext<T>(
|
||||
this IServiceCollection services,
|
||||
Action<DbContextOptionsBuilder, string?, string?, IServiceProvider?>? optionsAction = null)
|
||||
where T : DbContext
|
||||
{
|
||||
return AddUmbracoDbContext<T>(services, (IServiceProvider _, DbContextOptionsBuilder options) =>
|
||||
return AddUmbracoDbContext<T>(services, (IServiceProvider provider, DbContextOptionsBuilder optionsBuilder, string? providerName, string? connectionString) =>
|
||||
{
|
||||
optionsAction?.Invoke(options);
|
||||
ConnectionStrings connectionStrings = GetConnectionStringAndProviderName(provider);
|
||||
optionsAction?.Invoke(optionsBuilder, connectionStrings.ConnectionString, connectionStrings.ProviderName, provider);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a EFCore DbContext with all the services needed to integrate with Umbraco scopes.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="services"></param>
|
||||
/// <param name="optionsAction"></param>
|
||||
/// <returns></returns>
|
||||
public static IServiceCollection AddUmbracoDbContext<T>(this IServiceCollection services, Action<IServiceProvider, DbContextOptionsBuilder>? optionsAction = null)
|
||||
[Obsolete("Please use the method overload that takes all parameters for the optionsAction. Scheduled for removal in Umbraco 18.")]
|
||||
public static IServiceCollection AddUmbracoDbContext<T>(
|
||||
this IServiceCollection services,
|
||||
Action<IServiceProvider, DbContextOptionsBuilder>? optionsAction = null)
|
||||
where T : DbContext
|
||||
=> AddUmbracoDbContext<T>(services, (sp, optionsBuilder, connectionString, providerName) => optionsAction?.Invoke(sp, optionsBuilder));
|
||||
|
||||
/// <summary>
|
||||
/// Adds a EFCore DbContext with all the services needed to integrate with Umbraco scopes.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddUmbracoDbContext<T>(
|
||||
this IServiceCollection services,
|
||||
Action<IServiceProvider, DbContextOptionsBuilder, string?, string?>? optionsAction = null)
|
||||
where T : DbContext
|
||||
{
|
||||
optionsAction ??= (sp, options) => { };
|
||||
optionsAction ??= (sp, optionsBuilder, connectionString, providerName) => { };
|
||||
|
||||
services.AddPooledDbContextFactory<T>(optionsAction);
|
||||
|
||||
services.AddPooledDbContextFactory<T>((provider, optionsBuilder) => SetupDbContext(optionsAction, provider, optionsBuilder));
|
||||
services.AddTransient(services => services.GetRequiredService<IDbContextFactory<T>>().CreateDbContext());
|
||||
|
||||
services.AddUnique<IAmbientEFCoreScopeStack<T>, AmbientEFCoreScopeStack<T>>();
|
||||
@@ -110,4 +128,25 @@ public static class UmbracoEFCoreServiceCollectionExtensions
|
||||
|
||||
builder.UseDatabaseProvider(connectionStrings.ProviderName, connectionStrings.ConnectionString);
|
||||
}
|
||||
|
||||
private static void SetupDbContext(Action<IServiceProvider, DbContextOptionsBuilder, string?, string?>? optionsAction, IServiceProvider provider, DbContextOptionsBuilder builder)
|
||||
{
|
||||
ConnectionStrings connectionStrings = GetConnectionStringAndProviderName(provider);
|
||||
|
||||
optionsAction?.Invoke(provider, builder, connectionStrings.ConnectionString, connectionStrings.ProviderName);
|
||||
}
|
||||
|
||||
private static ConnectionStrings GetConnectionStringAndProviderName(IServiceProvider serviceProvider)
|
||||
{
|
||||
ConnectionStrings connectionStrings = serviceProvider.GetRequiredService<IOptionsMonitor<ConnectionStrings>>().CurrentValue;
|
||||
|
||||
// Replace data directory
|
||||
string? dataDirectory = AppDomain.CurrentDomain.GetData(Constants.System.DataDirectoryName)?.ToString();
|
||||
if (string.IsNullOrEmpty(dataDirectory) is false)
|
||||
{
|
||||
connectionStrings.ConnectionString = connectionStrings.ConnectionString?.Replace(Constants.System.DataDirectoryPlaceholder, dataDirectory);
|
||||
}
|
||||
|
||||
return connectionStrings;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,10 +127,16 @@ internal class EFCoreScope<TDbContext> : CoreScope, IEfCoreScope<TDbContext>
|
||||
|
||||
Locks.ClearLocks(InstanceId);
|
||||
|
||||
if (ParentScope is null)
|
||||
// Since we can nest EFCoreScopes in other scopes derived from CoreScope, we should check whether our ParentScope OR the base ParentScope exists.
|
||||
// Only if neither do do we take responsibility for ensuring the locks are cleared.
|
||||
// Eventually the highest parent will clear the locks.
|
||||
// Further, these locks are a reference to the locks of the highest parent anyway (see the constructor of CoreScope).
|
||||
#pragma warning disable SA1100 // Do not prefix calls with base unless local implementation exists (justification: provides additional clarify here that this is defined on the base class).
|
||||
if (ParentScope is null && base.HasParentScope is false)
|
||||
{
|
||||
Locks.EnsureLocksCleared(InstanceId);
|
||||
}
|
||||
#pragma warning restore SA1100 // Do not prefix calls with base unless local implementation exists
|
||||
|
||||
_efCoreScopeProvider.PopAmbientScope();
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Configuration;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -17,14 +18,14 @@ namespace Umbraco.Cms.Persistence.EFCore;
|
||||
/// and insure the 'src/Umbraco.Web.UI/appsettings.json' have a connection string set with the right provider.
|
||||
///
|
||||
/// Create a migration for each provider.
|
||||
/// <code>dotnet ef migrations add %Name% -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.SqlServer -c UmbracoDbContext -- --provider SqlServer</code>
|
||||
/// <code>dotnet ef migrations add %Name% -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.SqlServer -c UmbracoDbContext</code>
|
||||
///
|
||||
/// <code>dotnet ef migrations add %Name% -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.Sqlite -c UmbracoDbContext -- --provider Sqlite</code>
|
||||
/// <code>dotnet ef migrations add %Name% -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.Sqlite -c UmbracoDbContext</code>
|
||||
///
|
||||
/// Remove the last migration for each provider.
|
||||
/// <code>dotnet ef migrations remove -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.SqlServer -- --provider SqlServer</code>
|
||||
/// <code>dotnet ef migrations remove -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.SqlServer</code>
|
||||
///
|
||||
/// <code>dotnet ef migrations remove -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.Sqlite -- --provider Sqlite</code>
|
||||
/// <code>dotnet ef migrations remove -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.Sqlite</code>
|
||||
///
|
||||
/// To find documentation about this way of working with the context see
|
||||
/// https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/providers?tabs=dotnet-core-cli#using-one-context-type
|
||||
@@ -37,28 +38,35 @@ public class UmbracoDbContext : DbContext
|
||||
/// <param name="options"></param>
|
||||
public UmbracoDbContext(DbContextOptions<UmbracoDbContext> options)
|
||||
: base(ConfigureOptions(options))
|
||||
{
|
||||
|
||||
}
|
||||
{ }
|
||||
|
||||
private static DbContextOptions<UmbracoDbContext> ConfigureOptions(DbContextOptions<UmbracoDbContext> options)
|
||||
{
|
||||
IOptionsMonitor<ConnectionStrings> connectionStringsOptionsMonitor = StaticServiceProvider.Instance.GetRequiredService<IOptionsMonitor<ConnectionStrings>>();
|
||||
|
||||
ConnectionStrings connectionStrings = connectionStringsOptionsMonitor.CurrentValue;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(connectionStrings.ConnectionString))
|
||||
var extensions = options.Extensions.FirstOrDefault() as Microsoft.EntityFrameworkCore.Infrastructure.CoreOptionsExtension;
|
||||
IServiceProvider? serviceProvider = extensions?.ApplicationServiceProvider;
|
||||
serviceProvider ??= StaticServiceProvider.Instance;
|
||||
if (serviceProvider == null)
|
||||
{
|
||||
ILogger<UmbracoDbContext> logger = StaticServiceProvider.Instance.GetRequiredService<ILogger<UmbracoDbContext>>();
|
||||
logger.LogCritical("No connection string was found, cannot setup Umbraco EF Core context");
|
||||
// If the service provider is null, we cannot resolve the connection string or migration provider.
|
||||
throw new InvalidOperationException("The service provider is not configured. Ensure that UmbracoDbContext is registered correctly.");
|
||||
}
|
||||
|
||||
IOptionsMonitor<ConnectionStrings>? connectionStringsOptionsMonitor = serviceProvider?.GetRequiredService<IOptionsMonitor<ConnectionStrings>>();
|
||||
|
||||
ConnectionStrings? connectionStrings = connectionStringsOptionsMonitor?.CurrentValue;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(connectionStrings?.ConnectionString))
|
||||
{
|
||||
ILogger<UmbracoDbContext>? logger = serviceProvider?.GetRequiredService<ILogger<UmbracoDbContext>>();
|
||||
logger?.LogCritical("No connection string was found, cannot setup Umbraco EF Core context");
|
||||
|
||||
// we're throwing an exception here to make it abundantly clear that one should never utilize (or have a
|
||||
// dependency on) the DbContext before the connection string has been initialized by the installer.
|
||||
throw new InvalidOperationException("No connection string was found, cannot setup Umbraco EF Core context");
|
||||
}
|
||||
|
||||
IEnumerable<IMigrationProviderSetup> migrationProviders = StaticServiceProvider.Instance.GetServices<IMigrationProviderSetup>();
|
||||
IMigrationProviderSetup? migrationProvider = migrationProviders.FirstOrDefault(x => x.ProviderName.CompareProviderNames(connectionStrings.ProviderName));
|
||||
IEnumerable<IMigrationProviderSetup>? migrationProviders = serviceProvider?.GetServices<IMigrationProviderSetup>();
|
||||
IMigrationProviderSetup? migrationProvider = migrationProviders?.FirstOrDefault(x => x.ProviderName.CompareProviderNames(connectionStrings.ProviderName));
|
||||
|
||||
if (migrationProvider == null && connectionStrings.ProviderName != null)
|
||||
{
|
||||
|
||||
@@ -162,6 +162,8 @@ public class SqliteSyntaxProvider : SqlSyntaxProviderBase<SqliteSyntaxProvider>
|
||||
|
||||
public override string ConvertDateToOrderableString => "{0}";
|
||||
|
||||
public override string ConvertUniqueIdentifierToString => "{0}";
|
||||
|
||||
public override string RenameTable => "ALTER TABLE {0} RENAME TO {1}";
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Umbraco.Cms.Core;
|
||||
|
||||
/// <summary>
|
||||
@@ -9,6 +11,7 @@ namespace Umbraco.Cms.Core;
|
||||
public struct Attempt<TResult, TStatus>
|
||||
{
|
||||
// private - use Succeed() or Fail() methods to create attempts
|
||||
[JsonConstructor]
|
||||
private Attempt(bool success, TResult result, TStatus status, Exception? exception)
|
||||
{
|
||||
Success = success;
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ namespace Umbraco.Cms.Core.Cache.PartialViewCacheInvalidators;
|
||||
/// The default implementation is added in Umbraco.Web.Website, but we need this to ensure we have a service
|
||||
/// registered for this interface even in headless setups).
|
||||
/// </remarks>
|
||||
internal class NoopMemberPartialViewCacheInvalidator : IMemberPartialViewCacheInvalidator
|
||||
internal sealed class NoopMemberPartialViewCacheInvalidator : IMemberPartialViewCacheInvalidator
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public void ClearPartialViewCacheItems(IEnumerable<int> memberIds)
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Umbraco.Cms.Core.Collections;
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
[Serializable]
|
||||
public class ConcurrentHashSet<T> : ICollection<T>
|
||||
public class ConcurrentHashSet<T> : ICollection<T>, ISet<T>
|
||||
{
|
||||
private readonly HashSet<T> _innerSet = new();
|
||||
private readonly ReaderWriterLockSlim _instanceLocker = new(LockRecursionPolicy.NoRecursion);
|
||||
@@ -274,4 +274,180 @@ public class ConcurrentHashSet<T> : ICollection<T>
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
public void ExceptWith(IEnumerable<T> other)
|
||||
{
|
||||
try
|
||||
{
|
||||
_instanceLocker.EnterWriteLock();
|
||||
_innerSet.ExceptWith(other);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_instanceLocker.IsWriteLockHeld)
|
||||
{
|
||||
_instanceLocker.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void IntersectWith(IEnumerable<T> other)
|
||||
{
|
||||
try
|
||||
{
|
||||
_instanceLocker.EnterWriteLock();
|
||||
_innerSet.IntersectWith(other);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_instanceLocker.IsWriteLockHeld)
|
||||
{
|
||||
_instanceLocker.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsProperSubsetOf(IEnumerable<T> other)
|
||||
{
|
||||
try
|
||||
{
|
||||
_instanceLocker.EnterReadLock();
|
||||
return _innerSet.IsProperSubsetOf(other);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_instanceLocker.IsReadLockHeld)
|
||||
{
|
||||
_instanceLocker.ExitReadLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsProperSupersetOf(IEnumerable<T> other)
|
||||
{
|
||||
try
|
||||
{
|
||||
_instanceLocker.EnterReadLock();
|
||||
return _innerSet.IsProperSupersetOf(other);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_instanceLocker.IsReadLockHeld)
|
||||
{
|
||||
_instanceLocker.ExitReadLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsSubsetOf(IEnumerable<T> other)
|
||||
{
|
||||
try
|
||||
{
|
||||
_instanceLocker.EnterReadLock();
|
||||
return _innerSet.IsSubsetOf(other);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_instanceLocker.IsReadLockHeld)
|
||||
{
|
||||
_instanceLocker.ExitReadLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsSupersetOf(IEnumerable<T> other)
|
||||
{
|
||||
try
|
||||
{
|
||||
_instanceLocker.EnterReadLock();
|
||||
return _innerSet.IsSupersetOf(other);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_instanceLocker.IsReadLockHeld)
|
||||
{
|
||||
_instanceLocker.ExitReadLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool Overlaps(IEnumerable<T> other)
|
||||
{
|
||||
try
|
||||
{
|
||||
_instanceLocker.EnterReadLock();
|
||||
return _innerSet.Overlaps(other);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_instanceLocker.IsReadLockHeld)
|
||||
{
|
||||
_instanceLocker.ExitReadLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool SetEquals(IEnumerable<T> other)
|
||||
{
|
||||
try
|
||||
{
|
||||
_instanceLocker.EnterReadLock();
|
||||
return _innerSet.SetEquals(other);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_instanceLocker.IsReadLockHeld)
|
||||
{
|
||||
_instanceLocker.ExitReadLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SymmetricExceptWith(IEnumerable<T> other)
|
||||
{
|
||||
try
|
||||
{
|
||||
_instanceLocker.EnterWriteLock();
|
||||
_innerSet.IntersectWith(other);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_instanceLocker.IsWriteLockHeld)
|
||||
{
|
||||
_instanceLocker.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void UnionWith(IEnumerable<T> other)
|
||||
{
|
||||
try
|
||||
{
|
||||
_instanceLocker.EnterWriteLock();
|
||||
_innerSet.UnionWith(other);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_instanceLocker.IsWriteLockHeld)
|
||||
{
|
||||
_instanceLocker.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool ISet<T>.Add(T item)
|
||||
{
|
||||
try
|
||||
{
|
||||
_instanceLocker.EnterWriteLock();
|
||||
return _innerSet.Add(item);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_instanceLocker.IsWriteLockHeld)
|
||||
{
|
||||
_instanceLocker.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Umbraco.Cms.Core.Composing;
|
||||
/// <summary>
|
||||
/// Handles the composers.
|
||||
/// </summary>
|
||||
internal class ComposerGraph
|
||||
internal sealed class ComposerGraph
|
||||
{
|
||||
private readonly IUmbracoBuilder _builder;
|
||||
private readonly IEnumerable<Type> _composerTypes;
|
||||
@@ -415,7 +415,7 @@ internal class ComposerGraph
|
||||
}
|
||||
}
|
||||
|
||||
private class EnableInfo
|
||||
private sealed class EnableInfo
|
||||
{
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Umbraco.Cms.Core.Composing;
|
||||
/// borrowed and modified from here
|
||||
/// https://github.com/dotnet/aspnetcore-tooling/blob/master/src/Razor/src/Microsoft.NET.Sdk.Razor/FindAssembliesWithReferencesTo.cs
|
||||
/// </remarkes>
|
||||
internal class FindAssembliesWithReferencesTo
|
||||
internal sealed class FindAssembliesWithReferencesTo
|
||||
{
|
||||
private readonly bool _includeTargets;
|
||||
private readonly ILogger<FindAssembliesWithReferencesTo> _logger;
|
||||
|
||||
@@ -408,7 +408,7 @@ public sealed class TypeLoader
|
||||
/// </summary>
|
||||
/// <seealso cref="System.Exception" />
|
||||
[Serializable]
|
||||
internal class CachedTypeNotFoundInFileException : Exception
|
||||
internal sealed class CachedTypeNotFoundInFileException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CachedTypeNotFoundInFileException" /> class.
|
||||
|
||||
@@ -2,7 +2,7 @@ using System.Reflection;
|
||||
|
||||
namespace Umbraco.Cms.Core.Configuration;
|
||||
|
||||
internal class EntryAssemblyMetadata : IEntryAssemblyMetadata
|
||||
internal sealed class EntryAssemblyMetadata : IEntryAssemblyMetadata
|
||||
{
|
||||
public EntryAssemblyMetadata()
|
||||
{
|
||||
|
||||
@@ -37,7 +37,7 @@ public class ContentErrorPage : ValidatableEntryBase
|
||||
[Required]
|
||||
public string Culture { get; set; } = null!;
|
||||
|
||||
internal override bool IsValid() =>
|
||||
internal sealed override bool IsValid() =>
|
||||
base.IsValid() &&
|
||||
(HasContentId ^ HasContentKey);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace Umbraco.Cms.Core.Configuration.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Typed configuration options for long-running operations cleanup settings.
|
||||
/// </summary>
|
||||
public class LongRunningOperationsCleanupSettings
|
||||
{
|
||||
private const string StaticPeriod = "00:02:00";
|
||||
private const string StaticMaxAge = "01:00:00";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value for the period in which long-running operations are cleaned up.
|
||||
/// </summary>
|
||||
[DefaultValue(StaticPeriod)]
|
||||
public TimeSpan Period { get; set; } = TimeSpan.Parse(StaticPeriod);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum time a long-running operation entry can exist, without being updated, before it is considered for cleanup.
|
||||
/// </summary>
|
||||
[DefaultValue(StaticMaxAge)]
|
||||
public TimeSpan MaxEntryAge { get; set; } = TimeSpan.Parse(StaticMaxAge);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace Umbraco.Cms.Core.Configuration.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Typed configuration options for long-running operations settings.
|
||||
/// </summary>
|
||||
[UmbracoOptions(Constants.Configuration.ConfigLongRunningOperations)]
|
||||
public class LongRunningOperationsSettings
|
||||
{
|
||||
private const string StaticExpirationTime = "00:05:00";
|
||||
private const string StaticTimeBetweenStatusChecks = "00:00:10";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the cleanup settings for long-running operations.
|
||||
/// </summary>
|
||||
public LongRunningOperationsCleanupSettings Cleanup { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the time after which a long-running operation is considered expired/stale, if not updated.
|
||||
/// </summary>
|
||||
[DefaultValue(StaticExpirationTime)]
|
||||
public TimeSpan ExpirationTime { get; set; } = TimeSpan.Parse(StaticExpirationTime);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the time between status checks for long-running operations.
|
||||
/// </summary>
|
||||
[DefaultValue(StaticTimeBetweenStatusChecks)]
|
||||
public TimeSpan TimeBetweenStatusChecks { get; set; } = TimeSpan.Parse(StaticTimeBetweenStatusChecks);
|
||||
}
|
||||
@@ -21,6 +21,7 @@ public class WebRoutingSettings
|
||||
internal const bool StaticDisableFindContentByIdentifierPath = false;
|
||||
internal const bool StaticDisableRedirectUrlTracking = false;
|
||||
internal const string StaticUrlProviderMode = "Auto";
|
||||
internal const bool StaticUseStrictDomainMatching = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to check if any routed endpoints match a front-end request before
|
||||
@@ -60,8 +61,12 @@ public class WebRoutingSettings
|
||||
[DefaultValue(StaticValidateAlternativeTemplates)]
|
||||
public bool ValidateAlternativeTemplates { get; set; } = StaticValidateAlternativeTemplates;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the content finder by a path of the content key (<see cref="Routing.ContentFinderByKeyPath" />) is disabled.
|
||||
/// </summary>
|
||||
[DefaultValue(StaticDisableFindContentByIdentifierPath)]
|
||||
public bool DisableFindContentByIdentifierPath { get; set; } = StaticDisableFindContentByIdentifierPath;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether redirect URL tracking is disabled.
|
||||
/// </summary>
|
||||
@@ -78,4 +83,15 @@ public class WebRoutingSettings
|
||||
/// Gets or sets a value for the Umbraco application URL.
|
||||
/// </summary>
|
||||
public string UmbracoApplicationUrl { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether strict domain matching is used when finding content to match the request.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>This setting is used within Umbraco's routing process based on content finders, specifically <see cref="Routing.ContentFinderByUrlNew" />.</para>
|
||||
/// <para>If set to the default value of <see langword="false"/>, requests that don't match a configured domain will be routed to the first root node.</para>
|
||||
/// <para>If set to <see langword="true"/>, requests that don't match a configured domain will not be routed.</para>
|
||||
/// </remarks>
|
||||
[DefaultValue(StaticUseStrictDomainMatching)]
|
||||
public bool UseStrictDomainMatching { get; set; } = StaticUseStrictDomainMatching;
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ public static partial class Constants
|
||||
public const string ConfigExamine = ConfigPrefix + "Examine";
|
||||
public const string ConfigIndexing = ConfigPrefix + "Indexing";
|
||||
public const string ConfigLogging = ConfigPrefix + "Logging";
|
||||
public const string ConfigLongRunningOperations = ConfigPrefix + "LongRunningOperations";
|
||||
public const string ConfigMemberPassword = ConfigPrefix + "Security:MemberPassword";
|
||||
public const string ConfigModelsBuilder = ConfigPrefix + "ModelsBuilder";
|
||||
public const string ConfigModelsMode = ConfigModelsBuilder + ":ModelsMode";
|
||||
|
||||
@@ -7,8 +7,6 @@ namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public sealed class ApiContentBuilder : ApiContentBuilderBase<IApiContent>, IApiContentBuilder
|
||||
{
|
||||
private readonly IVariationContextAccessor _variationContextAccessor;
|
||||
|
||||
[Obsolete("Please use the constructor that takes an IVariationContextAccessor instead. Scheduled for removal in V17.")]
|
||||
public ApiContentBuilder(
|
||||
IApiContentNameProvider apiContentNameProvider,
|
||||
@@ -27,9 +25,10 @@ public sealed class ApiContentBuilder : ApiContentBuilderBase<IApiContent>, IApi
|
||||
IApiContentRouteBuilder apiContentRouteBuilder,
|
||||
IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor,
|
||||
IVariationContextAccessor variationContextAccessor)
|
||||
: base(apiContentNameProvider, apiContentRouteBuilder, outputExpansionStrategyAccessor)
|
||||
=> _variationContextAccessor = variationContextAccessor;
|
||||
: base(apiContentNameProvider, apiContentRouteBuilder, outputExpansionStrategyAccessor, variationContextAccessor)
|
||||
{
|
||||
}
|
||||
|
||||
protected override IApiContent Create(IPublishedContent content, string name, IApiContentRoute route, IDictionary<string, object?> properties)
|
||||
=> new ApiContent(content.Key, name, content.ContentType.Alias, content.CreateDate, content.CultureDate(_variationContextAccessor), route, properties);
|
||||
=> new ApiContent(content.Key, name, content.ContentType.Alias, content.CreateDate, content.CultureDate(VariationContextAccessor), route, properties);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
@@ -7,26 +10,63 @@ public abstract class ApiContentBuilderBase<T>
|
||||
where T : IApiContent
|
||||
{
|
||||
private readonly IApiContentNameProvider _apiContentNameProvider;
|
||||
private readonly IApiContentRouteBuilder _apiContentRouteBuilder;
|
||||
private readonly IOutputExpansionStrategyAccessor _outputExpansionStrategyAccessor;
|
||||
|
||||
protected ApiContentBuilderBase(IApiContentNameProvider apiContentNameProvider, IApiContentRouteBuilder apiContentRouteBuilder, IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor)
|
||||
[Obsolete("Please use the constructor that takes all parameters. Scheduled for removal in Umbraco 17.")]
|
||||
protected ApiContentBuilderBase(
|
||||
IApiContentNameProvider apiContentNameProvider,
|
||||
IApiContentRouteBuilder apiContentRouteBuilder,
|
||||
IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor)
|
||||
: this(
|
||||
apiContentNameProvider,
|
||||
apiContentRouteBuilder,
|
||||
outputExpansionStrategyAccessor,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IVariationContextAccessor>())
|
||||
{
|
||||
}
|
||||
|
||||
protected ApiContentBuilderBase(
|
||||
IApiContentNameProvider apiContentNameProvider,
|
||||
IApiContentRouteBuilder apiContentRouteBuilder,
|
||||
IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor,
|
||||
IVariationContextAccessor variationContextAccessor)
|
||||
{
|
||||
_apiContentNameProvider = apiContentNameProvider;
|
||||
_apiContentRouteBuilder = apiContentRouteBuilder;
|
||||
ApiContentRouteBuilder = apiContentRouteBuilder;
|
||||
_outputExpansionStrategyAccessor = outputExpansionStrategyAccessor;
|
||||
VariationContextAccessor = variationContextAccessor;
|
||||
}
|
||||
|
||||
protected IApiContentRouteBuilder ApiContentRouteBuilder { get; }
|
||||
|
||||
protected IVariationContextAccessor VariationContextAccessor { get; }
|
||||
|
||||
protected abstract T Create(IPublishedContent content, string name, IApiContentRoute route, IDictionary<string, object?> properties);
|
||||
|
||||
public virtual T? Build(IPublishedContent content)
|
||||
{
|
||||
IApiContentRoute? route = _apiContentRouteBuilder.Build(content);
|
||||
IApiContentRoute? route = ApiContentRouteBuilder.Build(content);
|
||||
if (route is null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
// If a segment is requested and no segmented properties have any values, we consider the segment as not created or non-existing and return null.
|
||||
// This aligns the behaviour of the API when it comes to "Accept-Segment" and "Accept-Language" requests, so 404 is returned for both when
|
||||
// the segment or language is not created or does not exist.
|
||||
// It also aligns with what we show in the backoffice for whether a segment is "Published" or "Not created".
|
||||
// Requested languages that aren't created or don't exist will already have exited early in the route builder.
|
||||
var segment = VariationContextAccessor.VariationContext?.Segment;
|
||||
if (segment.IsNullOrWhiteSpace() is false
|
||||
&& content.ContentType.VariesBySegment()
|
||||
&& content
|
||||
.Properties
|
||||
.Where(p => p.PropertyType.VariesBySegment())
|
||||
.All(p => p.HasValue(VariationContextAccessor.VariationContext?.Culture, segment) is false))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
IDictionary<string, object?> properties =
|
||||
_outputExpansionStrategyAccessor.TryGetValue(out IOutputExpansionStrategy? outputExpansionStrategy)
|
||||
? outputExpansionStrategy.MapContentProperties(content)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Extensions;
|
||||
@@ -7,9 +7,6 @@ namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public class ApiContentResponseBuilder : ApiContentBuilderBase<IApiContentResponse>, IApiContentResponseBuilder
|
||||
{
|
||||
private readonly IApiContentRouteBuilder _apiContentRouteBuilder;
|
||||
private readonly IVariationContextAccessor _variationContextAccessor;
|
||||
|
||||
[Obsolete("Please use the constructor that takes an IVariationContextAccessor instead. Scheduled for removal in V17.")]
|
||||
public ApiContentResponseBuilder(
|
||||
IApiContentNameProvider apiContentNameProvider,
|
||||
@@ -28,16 +25,14 @@ public class ApiContentResponseBuilder : ApiContentBuilderBase<IApiContentRespon
|
||||
IApiContentRouteBuilder apiContentRouteBuilder,
|
||||
IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor,
|
||||
IVariationContextAccessor variationContextAccessor)
|
||||
: base(apiContentNameProvider, apiContentRouteBuilder, outputExpansionStrategyAccessor)
|
||||
: base(apiContentNameProvider, apiContentRouteBuilder, outputExpansionStrategyAccessor, variationContextAccessor)
|
||||
{
|
||||
_apiContentRouteBuilder = apiContentRouteBuilder;
|
||||
_variationContextAccessor = variationContextAccessor;
|
||||
}
|
||||
|
||||
protected override IApiContentResponse Create(IPublishedContent content, string name, IApiContentRoute route, IDictionary<string, object?> properties)
|
||||
{
|
||||
IDictionary<string, IApiContentRoute> cultures = GetCultures(content);
|
||||
return new ApiContentResponse(content.Key, name, content.ContentType.Alias, content.CreateDate, content.CultureDate(_variationContextAccessor), route, properties, cultures);
|
||||
return new ApiContentResponse(content.Key, name, content.ContentType.Alias, content.CreateDate, content.CultureDate(VariationContextAccessor), route, properties, cultures);
|
||||
}
|
||||
|
||||
protected virtual IDictionary<string, IApiContentRoute> GetCultures(IPublishedContent content)
|
||||
@@ -52,7 +47,7 @@ public class ApiContentResponseBuilder : ApiContentBuilderBase<IApiContentRespon
|
||||
continue;
|
||||
}
|
||||
|
||||
IApiContentRoute? cultureRoute = _apiContentRouteBuilder.Build(content, publishedCultureInfo.Culture);
|
||||
IApiContentRoute? cultureRoute = ApiContentRouteBuilder.Build(content, publishedCultureInfo.Culture);
|
||||
if (cultureRoute == null)
|
||||
{
|
||||
// content is un-routable in this culture
|
||||
|
||||
@@ -69,6 +69,7 @@ public static partial class UmbracoBuilderExtensions
|
||||
.AddUmbracoOptions<ImagingSettings>()
|
||||
.AddUmbracoOptions<IndexingSettings>()
|
||||
.AddUmbracoOptions<LoggingSettings>()
|
||||
.AddUmbracoOptions<LongRunningOperationsSettings>()
|
||||
.AddUmbracoOptions<MemberPasswordConfigurationSettings>()
|
||||
.AddUmbracoOptions<NuCacheSettings>()
|
||||
.AddUmbracoOptions<RequestHandlerSettings>()
|
||||
|
||||
@@ -34,6 +34,7 @@ using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.ContentTypeEditing;
|
||||
using Umbraco.Cms.Core.HostedServices;
|
||||
using Umbraco.Cms.Core.Preview;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.PublishedCache.Internal;
|
||||
@@ -341,6 +342,7 @@ namespace Umbraco.Cms.Core.DependencyInjection
|
||||
Services.AddUnique<ILocalizedTextService>(factory => new LocalizedTextService(
|
||||
factory.GetRequiredService<Lazy<LocalizedTextServiceFileSources>>(),
|
||||
factory.GetRequiredService<ILogger<LocalizedTextService>>()));
|
||||
Services.AddUnique<ILongRunningOperationService, LongRunningOperationService>();
|
||||
|
||||
Services.AddUnique<IEntityXmlSerializer, EntityXmlSerializer>();
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace Umbraco.Cms.Core.Diagnostics;
|
||||
|
||||
internal class NoopMarchal : IMarchal
|
||||
internal sealed class NoopMarchal : IMarchal
|
||||
{
|
||||
public IntPtr GetExceptionPointers() => IntPtr.Zero;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace Umbraco.Cms.Core.Dictionary;
|
||||
/// fast
|
||||
/// (even though there is caching involved, if there's lots of dictionary items the caching is not great)
|
||||
/// </remarks>
|
||||
internal class DefaultCultureDictionary : ICultureDictionary
|
||||
internal sealed class DefaultCultureDictionary : ICultureDictionary
|
||||
{
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private readonly IAppCache _requestCache;
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace Umbraco.Cms.Core;
|
||||
/// <summary>
|
||||
/// Currently just used to get the machine name for use with file names
|
||||
/// </summary>
|
||||
internal class EnvironmentHelper
|
||||
internal static class EnvironmentHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the machine name that is safe to use in file paths.
|
||||
|
||||
@@ -102,7 +102,7 @@ public partial class EventAggregator : IEventAggregator
|
||||
}
|
||||
}
|
||||
|
||||
private void PublishCore<TNotification>(IEnumerable<Action<IEnumerable<TNotification>>> allHandlers, IEnumerable<TNotification> notifications)
|
||||
private static void PublishCore<TNotification>(IEnumerable<Action<IEnumerable<TNotification>>> allHandlers, IEnumerable<TNotification> notifications)
|
||||
{
|
||||
foreach (Action<IEnumerable<TNotification>> handler in allHandlers)
|
||||
{
|
||||
@@ -110,7 +110,7 @@ public partial class EventAggregator : IEventAggregator
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PublishCoreAsync<TNotification>(IEnumerable<Func<IEnumerable<TNotification>, CancellationToken, Task>> allHandlers, IEnumerable<TNotification> notifications, CancellationToken cancellationToken)
|
||||
private static async Task PublishCoreAsync<TNotification>(IEnumerable<Func<IEnumerable<TNotification>, CancellationToken, Task>> allHandlers, IEnumerable<TNotification> notifications, CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (Func<IEnumerable<TNotification>, CancellationToken, Task> handler in allHandlers)
|
||||
{
|
||||
@@ -192,7 +192,7 @@ internal abstract class NotificationAsyncHandlerWrapper
|
||||
where TNotificationHandler : INotificationHandler;
|
||||
}
|
||||
|
||||
internal class NotificationAsyncHandlerWrapperImpl<TNotificationType> : NotificationAsyncHandlerWrapper
|
||||
internal sealed class NotificationAsyncHandlerWrapperImpl<TNotificationType> : NotificationAsyncHandlerWrapper
|
||||
where TNotificationType : INotification
|
||||
{
|
||||
/// <remarks>
|
||||
@@ -260,7 +260,7 @@ internal class NotificationAsyncHandlerWrapperImpl<TNotificationType> : Notifica
|
||||
}
|
||||
}
|
||||
|
||||
internal class NotificationHandlerWrapperImpl<TNotificationType> : NotificationHandlerWrapper
|
||||
internal sealed class NotificationHandlerWrapperImpl<TNotificationType> : NotificationHandlerWrapper
|
||||
where TNotificationType : INotification
|
||||
{
|
||||
/// <remarks>
|
||||
|
||||
@@ -169,7 +169,7 @@ public class EventNameExtractor
|
||||
return words[0].EndsWith("ing") == false;
|
||||
}
|
||||
|
||||
private class EventInfoArgs
|
||||
private sealed class EventInfoArgs
|
||||
{
|
||||
public EventInfoArgs(EventInfo eventInfo, Type[] genericArgs)
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Umbraco.Cms.Core.Events;
|
||||
/// This means that events will be raised during the scope transaction,
|
||||
/// whatever happens, and the transaction could roll back in the end.
|
||||
/// </remarks>
|
||||
internal class PassThroughEventDispatcher : IEventDispatcher
|
||||
internal sealed class PassThroughEventDispatcher : IEventDispatcher
|
||||
{
|
||||
public bool DispatchCancelable(EventHandler? eventHandler, object sender, CancellableEventArgs args, string? eventName = null)
|
||||
{
|
||||
|
||||
@@ -414,7 +414,7 @@ public abstract class QueuingEventDispatcherBase : IEventDispatcher
|
||||
}
|
||||
}
|
||||
|
||||
private class EventDefinitionInfos
|
||||
private sealed class EventDefinitionInfos
|
||||
{
|
||||
public IEventDefinition? EventDefinition { get; set; }
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ public class ScopedNotificationPublisher<TNotificationHandler> : IScopedNotifica
|
||||
protected virtual void PublishScopedNotifications(IList<INotification> notifications)
|
||||
=> _eventAggregator.Publish<INotification, TNotificationHandler>(notifications);
|
||||
|
||||
private class Suppressor : IDisposable
|
||||
private sealed class Suppressor : IDisposable
|
||||
{
|
||||
private readonly ScopedNotificationPublisher<TNotificationHandler> _scopedNotificationPublisher;
|
||||
private bool _disposedValue;
|
||||
@@ -129,7 +129,7 @@ public class ScopedNotificationPublisher<TNotificationHandler> : IScopedNotifica
|
||||
|
||||
public void Dispose() => Dispose(true);
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposedValue)
|
||||
{
|
||||
|
||||
@@ -5,7 +5,6 @@ using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
@@ -96,7 +96,7 @@ public class WarnDocumentTypeElementSwitchNotificationHandler :
|
||||
}
|
||||
}
|
||||
|
||||
private class DocumentTypeElementSwitchInformation
|
||||
private sealed class DocumentTypeElementSwitchInformation
|
||||
{
|
||||
public bool WasElement { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace Umbraco.Cms.Core.Hosting;
|
||||
|
||||
internal class NoopApplicationShutdownRegistry : IApplicationShutdownRegistry
|
||||
internal sealed class NoopApplicationShutdownRegistry : IApplicationShutdownRegistry
|
||||
{
|
||||
public void RegisterObject(IRegisteredObject registeredObject)
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@ using System.Text.RegularExpressions;
|
||||
|
||||
namespace Umbraco.Cms.Core.IO;
|
||||
|
||||
internal class ShadowFileSystem : IFileSystem
|
||||
internal sealed class ShadowFileSystem : IFileSystem
|
||||
{
|
||||
private readonly IFileSystem _sfs;
|
||||
|
||||
@@ -392,7 +392,7 @@ internal class ShadowFileSystem : IFileSystem
|
||||
}
|
||||
|
||||
// copied from System.Web.Util.Wildcard internal
|
||||
internal class WildcardExpression
|
||||
internal sealed class WildcardExpression
|
||||
{
|
||||
private static readonly Regex MetaRegex = new("[\\+\\{\\\\\\[\\|\\(\\)\\.\\^\\$]");
|
||||
private static readonly Regex QuestRegex = new("\\?");
|
||||
@@ -454,7 +454,7 @@ internal class ShadowFileSystem : IFileSystem
|
||||
}
|
||||
}
|
||||
|
||||
private class ShadowNode
|
||||
private sealed class ShadowNode
|
||||
{
|
||||
public ShadowNode(bool isDelete, bool isdir)
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
namespace Umbraco.Cms.Core.IO;
|
||||
|
||||
// shadow filesystems is definitively ... too convoluted
|
||||
internal class ShadowFileSystems : ICompletable
|
||||
internal sealed class ShadowFileSystems : ICompletable
|
||||
{
|
||||
private readonly FileSystems _fileSystems;
|
||||
private bool _completed;
|
||||
|
||||
@@ -5,7 +5,7 @@ using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.IO;
|
||||
|
||||
internal class ShadowWrapper : IFileSystem, IFileProviderFactory
|
||||
internal sealed class ShadowWrapper : IFileSystem, IFileProviderFactory
|
||||
{
|
||||
private const string ShadowFsPath = "ShadowFs";
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ public class LogProfiler : IProfiler
|
||||
public bool IsEnabled => _logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug);
|
||||
|
||||
// a lightweight disposable timer
|
||||
private class LightDisposableTimer : DisposableObjectSlim
|
||||
private sealed class LightDisposableTimer : DisposableObjectSlim
|
||||
{
|
||||
private readonly Action<long> _callback;
|
||||
private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
|
||||
|
||||
@@ -17,7 +17,7 @@ public class NoopProfiler : IProfiler
|
||||
/// <inheritdoc/>
|
||||
public bool IsEnabled => false;
|
||||
|
||||
private class VoidDisposable : DisposableObjectSlim
|
||||
private sealed class VoidDisposable : DisposableObjectSlim
|
||||
{
|
||||
protected override void DisposeResources()
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@ using Umbraco.Cms.Core.Models.Email;
|
||||
|
||||
namespace Umbraco.Cms.Core.Mail;
|
||||
|
||||
internal class NotImplementedEmailSender : IEmailSender
|
||||
internal sealed class NotImplementedEmailSender : IEmailSender
|
||||
{
|
||||
public Task SendAsync(EmailMessage message, string emailType)
|
||||
=> throw new NotImplementedException(
|
||||
|
||||
@@ -3,7 +3,7 @@ namespace Umbraco.Cms.Core.Mail;
|
||||
/// <summary>
|
||||
/// An <see cref="ISmsSender" /> that throws <see cref="NotImplementedException" />
|
||||
/// </summary>
|
||||
internal class NotImplementedSmsSender : ISmsSender
|
||||
internal sealed class NotImplementedSmsSender : ISmsSender
|
||||
{
|
||||
public Task SendSmsAsync(string number, string message)
|
||||
=> throw new NotImplementedException(
|
||||
|
||||
@@ -8,19 +8,39 @@ public class CacheSettings
|
||||
{
|
||||
internal const int StaticDocumentBreadthFirstSeedCount = 100;
|
||||
internal const int StaticMediaBreadthFirstSeedCount = 100;
|
||||
internal const int StaticDocumentSeedBatchSize = 100;
|
||||
internal const int StaticMediaSeedBatchSize = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value for the collection of content type ids to always have in the cache.
|
||||
/// Gets or sets a value for the collection of content type ids to always have in the cache.
|
||||
/// </summary>
|
||||
public List<Guid> ContentTypeKeys { get; set; } =
|
||||
new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value for the document breadth first seed count.
|
||||
/// </summary>
|
||||
[DefaultValue(StaticDocumentBreadthFirstSeedCount)]
|
||||
public int DocumentBreadthFirstSeedCount { get; set; } = StaticDocumentBreadthFirstSeedCount;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value for the media breadth first seed count.
|
||||
/// </summary>
|
||||
[DefaultValue(StaticMediaBreadthFirstSeedCount)]
|
||||
public int MediaBreadthFirstSeedCount { get; set; } = StaticDocumentBreadthFirstSeedCount;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value for the document seed batch size.
|
||||
/// </summary>
|
||||
[DefaultValue(StaticDocumentSeedBatchSize)]
|
||||
public int DocumentSeedBatchSize { get; set; } = StaticDocumentSeedBatchSize;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value for the media seed batch size.
|
||||
/// </summary>
|
||||
[DefaultValue(StaticMediaSeedBatchSize)]
|
||||
public int MediaSeedBatchSize { get; set; } = StaticMediaSeedBatchSize;
|
||||
|
||||
public CacheEntry Entry { get; set; } = new CacheEntry();
|
||||
|
||||
public class CacheEntry
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Umbraco.Cms.Core.Models.ContentPublishing;
|
||||
|
||||
internal sealed class ContentPublishingBranchInternalResult
|
||||
{
|
||||
public Guid? ContentKey { get; init; }
|
||||
|
||||
public IContent? Content { get; init; }
|
||||
|
||||
public IEnumerable<ContentPublishingBranchItemResult> SucceededItems { get; set; } = [];
|
||||
|
||||
public IEnumerable<ContentPublishingBranchItemResult> FailedItems { get; set; } = [];
|
||||
|
||||
public Guid? AcceptedTaskId { get; init; }
|
||||
}
|
||||
@@ -17,6 +17,7 @@ public class DataType : TreeEntityBase, IDataType
|
||||
private IDictionary<string, object> _configurationData;
|
||||
private ValueStorageType _databaseType;
|
||||
private IDataEditor? _editor;
|
||||
private string? _editorUiAlias;
|
||||
private bool _hasConfigurationObject;
|
||||
|
||||
/// <summary>
|
||||
@@ -60,7 +61,11 @@ public class DataType : TreeEntityBase, IDataType
|
||||
|
||||
/// <inheritdoc />
|
||||
[DataMember]
|
||||
public string? EditorUiAlias { get; set; }
|
||||
public string? EditorUiAlias
|
||||
{
|
||||
get => _editorUiAlias;
|
||||
set => SetPropertyValueAndDetectChanges(value, ref _editorUiAlias, nameof(EditorUiAlias));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[DataMember]
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Umbraco.Cms.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the result of a database cache rebuild operation.
|
||||
/// </summary>
|
||||
public enum DatabaseCacheRebuildResult
|
||||
{
|
||||
/// <summary>
|
||||
/// The cache rebuild operation was either successful or enqueued successfully.
|
||||
/// </summary>
|
||||
Success,
|
||||
|
||||
/// <summary>
|
||||
/// A cache rebuild operation is already in progress.
|
||||
/// </summary>
|
||||
AlreadyRunning,
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace Umbraco.Cms.Core.Models;
|
||||
|
||||
internal class DefaultPayloadModel
|
||||
internal sealed class DefaultPayloadModel
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace Umbraco.Cms.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a long-running operation.
|
||||
/// </summary>
|
||||
public class LongRunningOperation
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the unique identifier for the long-running operation.
|
||||
/// </summary>
|
||||
public required Guid Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the long-running operation.
|
||||
/// </summary>
|
||||
public required string Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the status of the long-running operation.
|
||||
/// </summary>
|
||||
public required LongRunningOperationStatus Status { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Umbraco.Cms.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a long-running operation.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The type of the result of the long-running operation.</typeparam>
|
||||
public class LongRunningOperation<TResult> : LongRunningOperation
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the result of the long-running operation.
|
||||
/// </summary>
|
||||
public TResult? Result { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
namespace Umbraco.Cms.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the status of a long-running operation.
|
||||
/// </summary>
|
||||
public enum LongRunningOperationStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// The operation has finished successfully.
|
||||
/// </summary>
|
||||
Success,
|
||||
|
||||
/// <summary>
|
||||
/// The operation has failed.
|
||||
/// </summary>
|
||||
Failed,
|
||||
|
||||
/// <summary>
|
||||
/// The operation has been queued.
|
||||
/// </summary>
|
||||
Enqueued,
|
||||
|
||||
/// <summary>
|
||||
/// The operation is currently running.
|
||||
/// </summary>
|
||||
Running,
|
||||
|
||||
/// <summary>
|
||||
/// The operation wasn't updated within the expected time frame and is considered stale.
|
||||
/// </summary>
|
||||
Stale,
|
||||
}
|
||||
@@ -426,7 +426,7 @@ public class User : EntityBase, IUser, IProfile
|
||||
/// <summary>
|
||||
/// Internal class used to wrap the user in a profile
|
||||
/// </summary>
|
||||
private class WrappedUserProfile : IProfile
|
||||
private sealed class WrappedUserProfile : IProfile
|
||||
{
|
||||
private readonly IUser _user;
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Umbraco.Cms.Core.Collections;
|
||||
|
||||
namespace Umbraco.Cms.Core.Models.Navigation;
|
||||
|
||||
public sealed class NavigationNode
|
||||
{
|
||||
private HashSet<Guid> _children;
|
||||
private ConcurrentHashSet<Guid> _children;
|
||||
|
||||
public Guid Key { get; private set; }
|
||||
|
||||
@@ -21,7 +22,7 @@ public sealed class NavigationNode
|
||||
Key = key;
|
||||
ContentTypeKey = contentTypeKey;
|
||||
SortOrder = sortOrder;
|
||||
_children = new HashSet<Guid>();
|
||||
_children = new ConcurrentHashSet<Guid>();
|
||||
}
|
||||
|
||||
public void UpdateSortOrder(int newSortOrder) => SortOrder = newSortOrder;
|
||||
|
||||
@@ -373,7 +373,7 @@ public class ModelType : Type
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
internal class ModelTypeArrayType : Type
|
||||
internal sealed class ModelTypeArrayType : Type
|
||||
{
|
||||
private readonly Type _elementType;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ using System.Globalization;
|
||||
|
||||
namespace Umbraco.Cms.Core.Models.PublishedContent;
|
||||
|
||||
internal class PublishedContentTypeConverter : TypeConverter
|
||||
internal sealed class PublishedContentTypeConverter : TypeConverter
|
||||
{
|
||||
private static readonly Type[] ConvertingTypes = { typeof(int) };
|
||||
|
||||
|
||||
@@ -151,7 +151,7 @@ public class PublishedModelFactory : IPublishedModelFactory
|
||||
public Type MapModelType(Type type)
|
||||
=> ModelType.Map(type, _modelTypeMap);
|
||||
|
||||
private class ModelInfo
|
||||
private sealed class ModelInfo
|
||||
{
|
||||
public Type? ParameterType { get; set; }
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user