diff --git a/.github/contributing-backoffice.md b/.github/contributing-backoffice.md
index 21993dd92f..76b8246fff 100644
--- a/.github/contributing-backoffice.md
+++ b/.github/contributing-backoffice.md
@@ -114,7 +114,7 @@ To declare the Published Cache Status Dashboard as a new manifest, we need to ad
},
conditions: [
{
- alias: 'Umb.Condition.SectionAlias',
+ alias: UMB_SECTION_ALIAS_CONDITION_ALIAS,
match: 'Umb.Section.Settings',
},
],
diff --git a/.github/workflows/azure-backoffice.yml b/.github/workflows/azure-backoffice.yml
index b22b1d36ee..4c40540065 100644
--- a/.github/workflows/azure-backoffice.yml
+++ b/.github/workflows/azure-backoffice.yml
@@ -20,7 +20,7 @@ on:
jobs:
build_and_deploy_job:
- if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed' && (contains(github.event.pull_request.labels.*.name, 'preview/backoffice')))
+ if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed' && contains(github.event.pull_request.labels.*.name, 'preview/backoffice') && github.repository == github.event.pull_request.head.repo.full_name)
runs-on: ubuntu-latest
name: Build and Deploy Job
steps:
@@ -44,7 +44,7 @@ jobs:
###### End of Repository/Build Configurations ######
close_pull_request_job:
- if: github.event_name == 'pull_request' && github.event.action == 'closed'
+ if: github.event_name == 'pull_request' && github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'preview/backoffice') && github.repository == github.event.pull_request.head.repo.full_name
runs-on: ubuntu-latest
name: Close Pull Request Job
steps:
diff --git a/.github/workflows/azure-storybook.yml b/.github/workflows/azure-storybook.yml
index 0da6fddfbe..586cd10d22 100644
--- a/.github/workflows/azure-storybook.yml
+++ b/.github/workflows/azure-storybook.yml
@@ -23,7 +23,7 @@ env:
jobs:
build_and_deploy_job:
- if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed' && (contains(github.event.pull_request.labels.*.name, 'preview/storybook')))
+ if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed' && contains(github.event.pull_request.labels.*.name, 'preview/storybook') && github.repository == github.event.pull_request.head.repo.full_name)
runs-on: ubuntu-latest
name: Build and Deploy Job
steps:
@@ -45,7 +45,7 @@ jobs:
###### End of Repository/Build Configurations ######
close_pull_request_job:
- if: github.event_name == 'pull_request' && github.event.action == 'closed'
+ if: github.event_name == 'pull_request' && github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'preview/storybook') && github.repository == github.event.pull_request.head.repo.full_name
runs-on: ubuntu-latest
name: Close Pull Request Job
steps:
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 5f4eb90345..ef4677989e 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -62,6 +62,7 @@
"cwd": "${workspaceFolder}/src/Umbraco.Web.UI",
"stopAtEntry": false,
"requireExactSource": false,
+ "postDebugTask": "kill-umbraco-web-ui",
// Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
"serverReadyAction": {
"action": "openExternally",
@@ -96,6 +97,7 @@
"stopAtEntry": false,
"requireExactSource": false,
"checkForDevCert": true,
+ "postDebugTask": "kill-umbraco-web-ui",
"env": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_URLS": "https://localhost:44339",
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index f143ad431c..ed79d86c9a 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -1,76 +1,87 @@
{
- "version": "2.0.0",
- "tasks": [
- {
- "label": "Build",
- "detail": "Builds the client and SLN",
- "promptOnClose": true,
- "group": "build",
- "dependsOn": [
- "Client Build",
- "Dotnet build"
- ],
- "problemMatcher": []
- },
- {
- "label": "Client Install",
- "detail": "install npm for Umbraco.Web.UI.Client",
- "promptOnClose": true,
- "type": "npm",
- "script": "install",
- "path": "src/Umbraco.Web.UI.Client/",
- "problemMatcher": []
- },
- {
- "label": "Client Build",
- "detail": "runs npm run build for Umbraco.Web.UI.Client",
- "promptOnClose": true,
- "group": "build",
- "type": "npm",
- "script": "build:for:cms",
- "path": "src/Umbraco.Web.UI.Client/",
- "problemMatcher": []
- },
- {
- "label": "Client Watch",
- "detail": "runs npm run dev for Umbraco.Web.UI.Client",
- "promptOnClose": true,
- "group": "build",
- "type": "npm",
- "script": "dev",
- "path": "src/Umbraco.Web.UI.Client/",
- "problemMatcher": []
- },
- {
- "label": "Dotnet build",
- "detail": "Dotnet build of SLN",
- "promptOnClose": true,
- "group": "build",
- "command": "dotnet",
- "type": "process",
- "args": [
- "build",
- "${workspaceFolder}/umbraco.sln",
- "/property:GenerateFullPaths=true",
- "/consoleloggerparameters:NoSummary"
- ],
- "problemMatcher": "$msCompile"
- },
- {
- "label": "Dotnet watch",
- "detail": "Dotnet run and watch of Web.UI",
- "promptOnClose": true,
- "command": "dotnet",
- "type": "process",
- "args": [
- "watch",
- "run",
- "--project",
- "${workspaceFolder}/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj",
- "/property:GenerateFullPaths=true",
- "/consoleloggerparameters:NoSummary"
- ],
- "problemMatcher": "$msCompile"
- }
- ]
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "Build",
+ "detail": "Builds the client and SLN",
+ "promptOnClose": true,
+ "group": "build",
+ "dependsOn": ["Client Build", "Dotnet build"],
+ "problemMatcher": []
+ },
+ {
+ "label": "Client Install",
+ "detail": "install npm for Umbraco.Web.UI.Client",
+ "promptOnClose": true,
+ "type": "npm",
+ "script": "install",
+ "path": "src/Umbraco.Web.UI.Client/",
+ "problemMatcher": []
+ },
+ {
+ "label": "Client Build",
+ "detail": "runs npm run build for Umbraco.Web.UI.Client",
+ "promptOnClose": true,
+ "group": "build",
+ "type": "npm",
+ "script": "build:for:cms",
+ "path": "src/Umbraco.Web.UI.Client/",
+ "problemMatcher": []
+ },
+ {
+ "label": "Client Watch",
+ "detail": "runs npm run dev for Umbraco.Web.UI.Client",
+ "promptOnClose": true,
+ "group": "build",
+ "type": "npm",
+ "script": "dev",
+ "path": "src/Umbraco.Web.UI.Client/",
+ "problemMatcher": []
+ },
+ {
+ "label": "Dotnet build",
+ "detail": "Dotnet build of SLN",
+ "promptOnClose": true,
+ "group": "build",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "build",
+ "${workspaceFolder}/umbraco.sln",
+ "/property:GenerateFullPaths=true",
+ "/consoleloggerparameters:NoSummary"
+ ],
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "Dotnet watch",
+ "detail": "Dotnet run and watch of Web.UI",
+ "promptOnClose": true,
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "watch",
+ "run",
+ "--project",
+ "${workspaceFolder}/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj",
+ "/property:GenerateFullPaths=true",
+ "/consoleloggerparameters:NoSummary"
+ ],
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "kill-umbraco-web-ui",
+ "type": "shell",
+ "problemMatcher": [],
+ "osx": {
+ "command": "pkill -f Umbraco.Web.UI"
+ },
+ "linux": {
+ "command": "pkill -f Umbraco.Web.UI"
+ },
+ "windows": {
+ "command": "taskkill /IM Umbraco.Web.UI.exe /F"
+ }
+ }
+ ]
}
diff --git a/Directory.Build.props b/Directory.Build.props
index 4dda16251e..7a38d8f922 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -40,8 +40,8 @@
false
- false
- 15.0.0
+ true
+ 16.0.0
true
true
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DataType/Tree/SiblingsDataTypeTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DataType/Tree/SiblingsDataTypeTreeController.cs
new file mode 100644
index 0000000000..253d32e3e8
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Management/Controllers/DataType/Tree/SiblingsDataTypeTreeController.cs
@@ -0,0 +1,19 @@
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Umbraco.Cms.Api.Management.ViewModels.Tree;
+using Umbraco.Cms.Core.Services;
+
+namespace Umbraco.Cms.Api.Management.Controllers.DataType.Tree;
+
+public class SiblingsDataTypeTreeController : DataTypeTreeControllerBase
+{
+ public SiblingsDataTypeTreeController(IEntityService entityService, IDataTypeService dataTypeService)
+ : base(entityService, dataTypeService)
+ {
+ }
+
+ [HttpGet("siblings")]
+ [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)]
+ public Task>> Siblings(CancellationToken cancellationToken, Guid target, int before, int after)
+ => GetSiblings(target, before, after);
+}
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/MoveDocumentController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/MoveDocumentController.cs
index 1141b34b5c..cdaafa904a 100644
--- a/src/Umbraco.Cms.Api.Management/Controllers/Document/MoveDocumentController.cs
+++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/MoveDocumentController.cs
@@ -53,16 +53,6 @@ public class MoveDocumentController : DocumentControllerBase
return Forbidden();
}
- AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
- User,
- ContentPermissionResource.WithKeys(ActionMove.ActionLetter, new[] { moveDocumentRequestModel.Target?.Id, id }),
- AuthorizationPolicies.ContentPermissionByResource);
-
- if (!authorizationResult.Succeeded)
- {
- return Forbidden();
- }
-
Attempt result = await _contentEditingService.MoveAsync(
id,
moveDocumentRequestModel.Target?.Id,
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/Tree/SiblingsDocumentTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/Tree/SiblingsDocumentTreeController.cs
new file mode 100644
index 0000000000..3fec79bf36
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/Tree/SiblingsDocumentTreeController.cs
@@ -0,0 +1,40 @@
+using Asp.Versioning;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Umbraco.Cms.Api.Management.Factories;
+using Umbraco.Cms.Api.Management.Services.Entities;
+using Umbraco.Cms.Api.Management.ViewModels.Tree;
+using Umbraco.Cms.Core.Cache;
+using Umbraco.Cms.Core.Security;
+using Umbraco.Cms.Core.Services;
+
+namespace Umbraco.Cms.Api.Management.Controllers.Document.Tree;
+
+[ApiVersion("1.0")]
+public class SiblingsDocumentTreeController : DocumentTreeControllerBase
+{
+ public SiblingsDocumentTreeController(
+ IEntityService entityService,
+ IUserStartNodeEntitiesService userStartNodeEntitiesService,
+ IDataTypeService dataTypeService,
+ IPublicAccessService publicAccessService,
+ AppCaches appCaches,
+ IBackOfficeSecurityAccessor backofficeSecurityAccessor,
+ IDocumentPresentationFactory documentPresentationFactory)
+ : base(
+ entityService,
+ userStartNodeEntitiesService,
+ dataTypeService,
+ publicAccessService,
+ appCaches,
+ backofficeSecurityAccessor,
+ documentPresentationFactory)
+ {
+ }
+
+ [HttpGet("siblings")]
+ [MapToApiVersion("1.0")]
+ [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)]
+ public Task>> Siblings(CancellationToken cancellationToken, Guid target, int before, int after)
+ => GetSiblings(target, before, after);
+}
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Tree/SiblingsDocumentBlueprintTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Tree/SiblingsDocumentBlueprintTreeController.cs
new file mode 100644
index 0000000000..ac5578155f
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Tree/SiblingsDocumentBlueprintTreeController.cs
@@ -0,0 +1,24 @@
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Umbraco.Cms.Api.Management.Factories;
+using Umbraco.Cms.Api.Management.ViewModels.Tree;
+using Umbraco.Cms.Core.Services;
+
+namespace Umbraco.Cms.Api.Management.Controllers.DocumentBlueprint.Tree;
+
+public class SiblingsDocumentBlueprintTreeController : DocumentBlueprintTreeControllerBase
+{
+ public SiblingsDocumentBlueprintTreeController(IEntityService entityService, IDocumentPresentationFactory documentPresentationFactory)
+ : base(entityService, documentPresentationFactory)
+ {
+ }
+
+ [HttpGet("siblings")]
+ [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)]
+ public Task>> Siblings(
+ CancellationToken cancellationToken,
+ Guid target,
+ int before,
+ int after) =>
+ GetSiblings(target, before, after);
+}
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Tree/SiblingsDocumentTypeTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Tree/SiblingsDocumentTypeTreeController.cs
new file mode 100644
index 0000000000..7bb9c26358
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Tree/SiblingsDocumentTypeTreeController.cs
@@ -0,0 +1,23 @@
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Umbraco.Cms.Api.Management.ViewModels.Tree;
+using Umbraco.Cms.Core.Services;
+
+namespace Umbraco.Cms.Api.Management.Controllers.DocumentType.Tree;
+
+public class SiblingsDocumentTypeTreeController : DocumentTypeTreeControllerBase
+{
+ public SiblingsDocumentTypeTreeController(IEntityService entityService, IContentTypeService contentTypeService)
+ : base(entityService, contentTypeService)
+ {
+ }
+
+ [HttpGet("siblings")]
+ [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)]
+ public Task>> Siblings(
+ CancellationToken cancellationToken,
+ Guid target,
+ int before,
+ int after) =>
+ GetSiblings(target, before, after);
+}
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Media/Tree/SiblingsMediaTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Media/Tree/SiblingsMediaTreeController.cs
new file mode 100644
index 0000000000..f5708fa638
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Management/Controllers/Media/Tree/SiblingsMediaTreeController.cs
@@ -0,0 +1,29 @@
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Umbraco.Cms.Api.Management.Factories;
+using Umbraco.Cms.Api.Management.Services.Entities;
+using Umbraco.Cms.Api.Management.ViewModels.Tree;
+using Umbraco.Cms.Core.Cache;
+using Umbraco.Cms.Core.Security;
+using Umbraco.Cms.Core.Services;
+
+namespace Umbraco.Cms.Api.Management.Controllers.Media.Tree;
+
+public class SiblingsMediaTreeController : MediaTreeControllerBase
+{
+ public SiblingsMediaTreeController(
+ IEntityService entityService,
+ IUserStartNodeEntitiesService userStartNodeEntitiesService,
+ IDataTypeService dataTypeService,
+ AppCaches appCaches,
+ IBackOfficeSecurityAccessor backofficeSecurityAccessor,
+ IMediaPresentationFactory mediaPresentationFactory)
+ : base(entityService, userStartNodeEntitiesService, dataTypeService, appCaches, backofficeSecurityAccessor, mediaPresentationFactory)
+ {
+ }
+
+ [HttpGet("siblings")]
+ [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)]
+ public Task>> Siblings(CancellationToken cancellationToken, Guid target, int before, int after)
+ => GetSiblings(target, before, after);
+}
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Tree/SiblingsMediaTypeTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Tree/SiblingsMediaTypeTreeController.cs
new file mode 100644
index 0000000000..1482788b57
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Tree/SiblingsMediaTypeTreeController.cs
@@ -0,0 +1,19 @@
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Umbraco.Cms.Api.Management.ViewModels.Tree;
+using Umbraco.Cms.Core.Services;
+
+namespace Umbraco.Cms.Api.Management.Controllers.MediaType.Tree;
+
+public class SiblingsMediaTypeTreeController : MediaTypeTreeControllerBase
+{
+ public SiblingsMediaTypeTreeController(IEntityService entityService, IMediaTypeService mediaTypeService)
+ : base(entityService, mediaTypeService)
+ {
+ }
+
+ [HttpGet("siblings")]
+ [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)]
+ public Task>> Siblings(CancellationToken cancellationToken, Guid target, int before, int after)
+ => GetSiblings(target, before, after);
+}
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Package/AllMigrationStatusPackageController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Package/AllMigrationStatusPackageController.cs
index a5fbf0e007..0bb2f830dc 100644
--- a/src/Umbraco.Cms.Api.Management/Controllers/Package/AllMigrationStatusPackageController.cs
+++ b/src/Umbraco.Cms.Api.Management/Controllers/Package/AllMigrationStatusPackageController.cs
@@ -1,8 +1,11 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
+using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.Package;
+using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Packaging;
using Umbraco.Cms.Core.Services;
@@ -14,12 +17,25 @@ namespace Umbraco.Cms.Api.Management.Controllers.Package;
public class AllMigrationStatusPackageController : PackageControllerBase
{
private readonly IPackagingService _packagingService;
- private readonly IUmbracoMapper _umbracoMapper;
+ private readonly IPackagePresentationFactory _packagePresentationFactory;
+ [Obsolete("Please use the non-obsolete constructor. Scheduled for removal in V18.")]
public AllMigrationStatusPackageController(IPackagingService packagingService, IUmbracoMapper umbracoMapper)
+ : this(packagingService, StaticServiceProvider.Instance.GetRequiredService())
+ {
+ }
+
+ [Obsolete("Please use the non-obsolete constructor. Scheduled for removal in V18.")]
+ public AllMigrationStatusPackageController(IPackagingService packagingService, IUmbracoMapper umbracoMapper, IPackagePresentationFactory packagePresentationFactory)
+ : this(packagingService, packagePresentationFactory)
+ {
+ }
+
+ [ActivatorUtilitiesConstructor]
+ public AllMigrationStatusPackageController(IPackagingService packagingService, IPackagePresentationFactory packagePresentationFactory)
{
_packagingService = packagingService;
- _umbracoMapper = umbracoMapper;
+ _packagePresentationFactory = packagePresentationFactory;
}
///
@@ -38,11 +54,7 @@ public class AllMigrationStatusPackageController : PackageControllerBase
{
PagedModel migrationPlans = await _packagingService.GetInstalledPackagesFromMigrationPlansAsync(skip, take);
- var viewModel = new PagedViewModel
- {
- Total = migrationPlans.Total,
- Items = _umbracoMapper.MapEnumerable(migrationPlans.Items)
- };
+ PagedViewModel viewModel = _packagePresentationFactory.CreatePackageMigrationStatusResponseModel(migrationPlans);
return Ok(viewModel);
}
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PublishedCache/RebuildPublishedCacheController.cs b/src/Umbraco.Cms.Api.Management/Controllers/PublishedCache/RebuildPublishedCacheController.cs
index 392c9338db..8f9cd490eb 100644
--- a/src/Umbraco.Cms.Api.Management/Controllers/PublishedCache/RebuildPublishedCacheController.cs
+++ b/src/Umbraco.Cms.Api.Management/Controllers/PublishedCache/RebuildPublishedCacheController.cs
@@ -15,7 +15,7 @@ public class RebuildPublishedCacheController : PublishedCacheControllerBase
[HttpPost("rebuild")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public async Task Rebuild(CancellationToken cancellationToken)
+ public Task Rebuild(CancellationToken cancellationToken)
{
if (_databaseCacheRebuilder.IsRebuilding())
{
@@ -27,10 +27,10 @@ public class RebuildPublishedCacheController : PublishedCacheControllerBase
Type = "Error",
};
- return await Task.FromResult(Conflict(problemDetails));
+ return Task.FromResult(Conflict(problemDetails));
}
_databaseCacheRebuilder.Rebuild(true);
- return await Task.FromResult(Ok());
+ return Task.FromResult(Ok());
}
}
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Template/Tree/SiblingsTemplateTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Template/Tree/SiblingsTemplateTreeController.cs
new file mode 100644
index 0000000000..ed27092ecb
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Management/Controllers/Template/Tree/SiblingsTemplateTreeController.cs
@@ -0,0 +1,23 @@
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Umbraco.Cms.Api.Management.ViewModels.Tree;
+using Umbraco.Cms.Core.Services;
+
+namespace Umbraco.Cms.Api.Management.Controllers.Template.Tree;
+
+public class SiblingsTemplateTreeController : TemplateTreeControllerBase
+{
+ public SiblingsTemplateTreeController(IEntityService entityService)
+ : base(entityService)
+ {
+ }
+
+ [HttpGet("siblings")]
+ [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)]
+ public Task>> Siblings(
+ CancellationToken cancellationToken,
+ Guid target,
+ int before,
+ int after) =>
+ GetSiblings(target, before, after);
+}
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Tree/EntityTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Tree/EntityTreeControllerBase.cs
index bf86b852ed..13bbe9bc2b 100644
--- a/src/Umbraco.Cms.Api.Management/Controllers/Tree/EntityTreeControllerBase.cs
+++ b/src/Umbraco.Cms.Api.Management/Controllers/Tree/EntityTreeControllerBase.cs
@@ -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,6 +44,23 @@ public abstract class EntityTreeControllerBase : ManagementApiControllerB
return Task.FromResult>>(Ok(result));
}
+ protected Task>> GetSiblings(Guid target, int before, int after)
+ {
+ IEntitySlim[] siblings = EntityService.GetSiblings(target, ItemObjectType, before, after, ItemOrdering).ToArray();
+ if (siblings.Length == 0)
+ {
+ return Task.FromResult>>(NotFound());
+ }
+
+ IEntitySlim? entity = siblings.FirstOrDefault();
+ Guid? parentKey = entity?.ParentId > 0
+ ? EntityService.GetKey(entity.ParentId, ItemObjectType).Result
+ : Constants.System.RootKey;
+
+ TItem[] treeItemsViewModels = MapTreeItemViewModels(parentKey, siblings);
+ return Task.FromResult>>(Ok(treeItemsViewModels));
+ }
+
protected virtual async Task>> GetAncestors(Guid descendantKey, bool includeSelf = true)
{
IEntitySlim[] ancestorEntities = await GetAncestorEntitiesAsync(descendantKey, includeSelf);
diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/ApplicationBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/ApplicationBuilderExtensions.cs
index 871a98c6d3..44efaf7be8 100644
--- a/src/Umbraco.Cms.Api.Management/DependencyInjection/ApplicationBuilderExtensions.cs
+++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/ApplicationBuilderExtensions.cs
@@ -43,7 +43,7 @@ internal static class ApplicationBuilderExtensions
var response = new ProblemDetails
{
- Title = exception.Message,
+ Title = isDebug ? exception.Message : "Server Error",
Detail = isDebug ? exception.StackTrace : null,
Status = statusCode ?? StatusCodes.Status500InternalServerError,
Instance = isDebug ? exception.GetType().Name : null,
diff --git a/src/Umbraco.Cms.Api.Management/Factories/IPackagePresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/IPackagePresentationFactory.cs
index d05617536a..f055416bb7 100644
--- a/src/Umbraco.Cms.Api.Management/Factories/IPackagePresentationFactory.cs
+++ b/src/Umbraco.Cms.Api.Management/Factories/IPackagePresentationFactory.cs
@@ -1,4 +1,6 @@
+using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.ViewModels.Package;
+using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Packaging;
namespace Umbraco.Cms.Api.Management.Factories;
@@ -8,4 +10,6 @@ public interface IPackagePresentationFactory
PackageDefinition CreatePackageDefinition(CreatePackageRequestModel createPackageRequestModel);
PackageConfigurationResponseModel CreateConfigurationResponseModel();
+
+ PagedViewModel CreatePackageMigrationStatusResponseModel(PagedModel installedPackages) => new();
}
diff --git a/src/Umbraco.Cms.Api.Management/Factories/IUserPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/IUserPresentationFactory.cs
index ac33b042eb..9ae171b02e 100644
--- a/src/Umbraco.Cms.Api.Management/Factories/IUserPresentationFactory.cs
+++ b/src/Umbraco.Cms.Api.Management/Factories/IUserPresentationFactory.cs
@@ -6,25 +6,58 @@ using Umbraco.Cms.Core.Models.Membership;
namespace Umbraco.Cms.Api.Management.Factories;
+///
+/// Defines factory methods for the creation of user presentation models.
+///
public interface IUserPresentationFactory
{
+ ///
+ /// Creates a response model for the provided user.
+ ///
UserResponseModel CreateResponseModel(IUser user);
+ ///
+ /// Creates a create model for a user based on the provided request model.
+ ///
Task CreateCreationModelAsync(CreateUserRequestModel requestModel);
+ ///
+ /// Creates an invite model for a user based on the provided request model.
+ ///
Task CreateInviteModelAsync(InviteUserRequestModel requestModel);
+ ///
+ /// Creates an update model for an existing user based on the provided request model.
+ ///
Task CreateUpdateModelAsync(Guid existingUserKey, UpdateUserRequestModel updateModel);
+ ///
+ /// Creates a response model for the current user based on the provided user.
+ ///
Task CreateCurrentUserResponseModelAsync(IUser user);
+ ///
+ /// Creates an resend invite model for a user based on the provided request model.
+ ///
Task CreateResendInviteModelAsync(ResendInviteUserRequestModel requestModel);
+ ///
+ /// Creates a user configuration model that contains the necessary data for user management operations.
+ ///
Task CreateUserConfigurationModelAsync();
+ ///
+ /// Creates a current user configuration model that contains the necessary data for the current user's management operations.
+ ///
Task CreateCurrentUserConfigurationModelAsync();
+ ///
+ /// Creates a user item response model for the provided user.
+ ///
UserItemResponseModel CreateItemResponseModel(IUser user);
+ ///
+ /// Creates a calculated user start nodes response model based on the provided user.
+ ///
Task CreateCalculatedUserStartNodesResponseModelAsync(IUser user);
}
diff --git a/src/Umbraco.Cms.Api.Management/Factories/PackagePresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/PackagePresentationFactory.cs
index ded31a63f2..960b2fdbcd 100644
--- a/src/Umbraco.Cms.Api.Management/Factories/PackagePresentationFactory.cs
+++ b/src/Umbraco.Cms.Api.Management/Factories/PackagePresentationFactory.cs
@@ -1,10 +1,12 @@
using System.Collections.Specialized;
using System.Web;
using Microsoft.Extensions.Options;
+using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.ViewModels.Package;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Mapping;
+using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Packaging;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
@@ -42,6 +44,25 @@ internal class PackagePresentationFactory : IPackagePresentationFactory
MarketplaceUrl = GetMarketplaceUrl(),
};
+ public PagedViewModel CreatePackageMigrationStatusResponseModel(PagedModel installedPackages)
+ {
+ InstalledPackage[] installedPackagesAsArray = installedPackages.Items as InstalledPackage[] ?? installedPackages.Items.ToArray();
+
+ return new PagedViewModel
+ {
+ Total = installedPackages.Total,
+ Items = installedPackagesAsArray
+ .GroupBy(package => package.PackageName)
+ .Select(packages => packages.OrderByDescending(package => package.HasPendingMigrations).First())
+ .Select(package => new PackageMigrationStatusResponseModel
+ {
+ PackageName = package.PackageName ?? string.Empty,
+ HasPendingMigrations = package.HasPendingMigrations,
+ })
+ .ToArray(),
+ };
+ }
+
private string GetMarketplaceUrl()
{
var uriBuilder = new UriBuilder(Constants.Marketplace.Url);
diff --git a/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs
index 4d7ed3c853..80c7b17e78 100644
--- a/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs
+++ b/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs
@@ -1,5 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
+using Umbraco.Cms.Api.Management.Mapping.Permissions;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Api.Management.Security;
using Umbraco.Cms.Api.Management.ViewModels;
@@ -22,6 +23,9 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Factories;
+///
+/// Factory for creating user presentation models, implementing .
+///
public class UserPresentationFactory : IUserPresentationFactory
{
private readonly IEntityService _entityService;
@@ -34,9 +38,11 @@ public class UserPresentationFactory : IUserPresentationFactory
private readonly IPasswordConfigurationPresentationFactory _passwordConfigurationPresentationFactory;
private readonly IBackOfficeExternalLoginProviders _externalLoginProviders;
private readonly SecuritySettings _securitySettings;
- private readonly IUserService _userService;
- private readonly IContentService _contentService;
+ private readonly Dictionary _permissionPresentationMappersByType;
+ ///
+ /// Initializes a new instance of the class.
+ ///
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 17.")]
public UserPresentationFactory(
IEntityService entityService,
@@ -50,7 +56,7 @@ public class UserPresentationFactory : IUserPresentationFactory
IOptionsSnapshot securitySettings,
IBackOfficeExternalLoginProviders externalLoginProviders)
: this(
- entityService,
+ entityService,
appCaches,
mediaFileManager,
imageUrlGenerator,
@@ -61,10 +67,15 @@ public class UserPresentationFactory : IUserPresentationFactory
securitySettings,
externalLoginProviders,
StaticServiceProvider.Instance.GetRequiredService(),
- StaticServiceProvider.Instance.GetRequiredService())
+ StaticServiceProvider.Instance.GetRequiredService(),
+ StaticServiceProvider.Instance.GetRequiredService>())
{
}
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 17.")]
public UserPresentationFactory(
IEntityService entityService,
AppCaches appCaches,
@@ -78,6 +89,44 @@ public class UserPresentationFactory : IUserPresentationFactory
IBackOfficeExternalLoginProviders externalLoginProviders,
IUserService userService,
IContentService contentService)
+ : this(
+ entityService,
+ appCaches,
+ mediaFileManager,
+ imageUrlGenerator,
+ userGroupPresentationFactory,
+ absoluteUrlBuilder,
+ emailSender,
+ passwordConfigurationPresentationFactory,
+ securitySettings,
+ externalLoginProviders,
+ userService,
+ contentService,
+ StaticServiceProvider.Instance.GetRequiredService>())
+ {
+ }
+
+ // TODO (V17): Remove the unused userService and contentService parameters from this constructor.
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public UserPresentationFactory(
+ IEntityService entityService,
+ AppCaches appCaches,
+ MediaFileManager mediaFileManager,
+ IImageUrlGenerator imageUrlGenerator,
+ IUserGroupPresentationFactory userGroupPresentationFactory,
+ IAbsoluteUrlBuilder absoluteUrlBuilder,
+ IEmailSender emailSender,
+ IPasswordConfigurationPresentationFactory passwordConfigurationPresentationFactory,
+ IOptionsSnapshot securitySettings,
+ IBackOfficeExternalLoginProviders externalLoginProviders,
+#pragma warning disable IDE0060 // Remove unused parameter - need to keep these until the next major to avoid breaking changes and/or ambiguous constructor errors
+ IUserService userService,
+ IContentService contentService,
+#pragma warning restore IDE0060 // Remove unused parameter
+ IEnumerable permissionPresentationMappers)
{
_entityService = entityService;
_appCaches = appCaches;
@@ -89,10 +138,10 @@ public class UserPresentationFactory : IUserPresentationFactory
_externalLoginProviders = externalLoginProviders;
_securitySettings = securitySettings.Value;
_absoluteUrlBuilder = absoluteUrlBuilder;
- _userService = userService;
- _contentService = contentService;
+ _permissionPresentationMappersByType = permissionPresentationMappers.ToDictionary(x => x.PresentationModelToHandle);
}
+ ///
public UserResponseModel CreateResponseModel(IUser user)
{
var responseModel = new UserResponseModel
@@ -123,6 +172,7 @@ public class UserPresentationFactory : IUserPresentationFactory
return responseModel;
}
+ ///
public UserItemResponseModel CreateItemResponseModel(IUser user) =>
new()
{
@@ -130,9 +180,10 @@ public class UserPresentationFactory : IUserPresentationFactory
Name = user.Name ?? user.Username,
AvatarUrls = user.GetUserAvatarUrls(_appCaches.RuntimeCache, _mediaFileManager, _imageUrlGenerator)
.Select(url => _absoluteUrlBuilder.ToAbsoluteUrl(url).ToString()),
- Kind = user.Kind
+ Kind = user.Kind,
};
+ ///
public Task CreateCreationModelAsync(CreateUserRequestModel requestModel)
{
var createModel = new UserCreateModel
@@ -142,12 +193,13 @@ public class UserPresentationFactory : IUserPresentationFactory
Name = requestModel.Name,
UserName = requestModel.UserName,
UserGroupKeys = requestModel.UserGroupIds.Select(x => x.Id).ToHashSet(),
- Kind = requestModel.Kind
+ Kind = requestModel.Kind,
};
return Task.FromResult(createModel);
}
+ ///
public Task CreateInviteModelAsync(InviteUserRequestModel requestModel)
{
var inviteModel = new UserInviteModel
@@ -162,6 +214,7 @@ public class UserPresentationFactory : IUserPresentationFactory
return Task.FromResult(inviteModel);
}
+ ///
public Task CreateResendInviteModelAsync(ResendInviteUserRequestModel requestModel)
{
var inviteModel = new UserResendInviteModel
@@ -173,6 +226,7 @@ public class UserPresentationFactory : IUserPresentationFactory
return Task.FromResult(inviteModel);
}
+ ///
public Task CreateCurrentUserConfigurationModelAsync()
{
var model = new CurrentUserConfigurationResponseModel
@@ -188,6 +242,7 @@ public class UserPresentationFactory : IUserPresentationFactory
return Task.FromResult(model);
}
+ ///
public Task CreateUserConfigurationModelAsync() =>
Task.FromResult(new UserConfigurationResponseModel
{
@@ -201,6 +256,7 @@ public class UserPresentationFactory : IUserPresentationFactory
AllowTwoFactor = _externalLoginProviders.HasDenyLocalLogin() is false,
});
+ ///
public Task CreateUpdateModelAsync(Guid existingUserKey, UpdateUserRequestModel updateModel)
{
var model = new UserUpdateModel
@@ -214,24 +270,24 @@ public class UserPresentationFactory : IUserPresentationFactory
HasContentRootAccess = updateModel.HasDocumentRootAccess,
MediaStartNodeKeys = updateModel.MediaStartNodeIds.Select(x => x.Id).ToHashSet(),
HasMediaRootAccess = updateModel.HasMediaRootAccess,
+ UserGroupKeys = updateModel.UserGroupIds.Select(x => x.Id).ToHashSet()
};
- model.UserGroupKeys = updateModel.UserGroupIds.Select(x => x.Id).ToHashSet();
-
return Task.FromResult(model);
}
+ ///
public async Task CreateCurrentUserResponseModelAsync(IUser user)
{
- var presentationUser = CreateResponseModel(user);
- var presentationGroups = await _userGroupPresentationFactory.CreateMultipleAsync(user.Groups);
+ UserResponseModel presentationUser = CreateResponseModel(user);
+ IEnumerable presentationGroups = await _userGroupPresentationFactory.CreateMultipleAsync(user.Groups);
var languages = presentationGroups.SelectMany(x => x.Languages).Distinct().ToArray();
var mediaStartNodeIds = user.CalculateMediaStartNodeIds(_entityService, _appCaches);
- var mediaStartNodeKeys = GetKeysFromIds(mediaStartNodeIds, UmbracoObjectTypes.Media);
+ ISet mediaStartNodeKeys = GetKeysFromIds(mediaStartNodeIds, UmbracoObjectTypes.Media);
var contentStartNodeIds = user.CalculateContentStartNodeIds(_entityService, _appCaches);
- var documentStartNodeKeys = GetKeysFromIds(contentStartNodeIds, UmbracoObjectTypes.Document);
+ ISet documentStartNodeKeys = GetKeysFromIds(contentStartNodeIds, UmbracoObjectTypes.Document);
- var permissions = GetAggregatedGranularPermissions(user, presentationGroups);
+ HashSet permissions = GetAggregatedGranularPermissions(user, presentationGroups);
var fallbackPermissions = presentationGroups.SelectMany(x => x.FallbackPermissions).ToHashSet();
var hasAccessToAllLanguages = presentationGroups.Any(x => x.HasAccessToAllLanguages);
@@ -263,70 +319,56 @@ public class UserPresentationFactory : IUserPresentationFactory
private HashSet GetAggregatedGranularPermissions(IUser user, IEnumerable presentationGroups)
{
- var aggregatedPermissions = new HashSet();
-
var permissions = presentationGroups.SelectMany(x => x.Permissions).ToHashSet();
+ return GetAggregatedGranularPermissions(user, permissions);
+ }
- AggregateAndAddDocumentPermissions(user, aggregatedPermissions, permissions);
+ private HashSet GetAggregatedGranularPermissions(IUser user, HashSet permissions)
+ {
+ // The raw permission data consists of several permissions for each entity (e.g. document), as permissions are assigned to user groups
+ // and a user may be part of multiple groups. We want to aggregate this server-side so we return one set of aggregate permissions per
+ // entity that the client will use.
+ // We need to handle here not just permissions known to core (e.g. document and document property value permissions), but also custom
+ // permissions defined by packages or implemetors.
+ IEnumerable<(Type, IEnumerable)> permissionModelsByType = permissions
+ .GroupBy(x => x.GetType())
+ .Select(x => (x.Key, x.Select(y => y)));
- AggregateAndAddDocumentPropertyValuePermissions(aggregatedPermissions, permissions);
+ var aggregatedPermissions = new HashSet();
+ foreach ((Type Type, IEnumerable Models) permissionModelByType in permissionModelsByType)
+ {
+ if (_permissionPresentationMappersByType.TryGetValue(permissionModelByType.Type, out IPermissionPresentationMapper? mapper))
+ {
+
+ IEnumerable aggregatedModels = mapper.AggregatePresentationModels(user, permissionModelByType.Models);
+ foreach (IPermissionPresentationModel aggregatedModel in aggregatedModels)
+ {
+ aggregatedPermissions.Add(aggregatedModel);
+ }
+ }
+ else
+ {
+ IEnumerable<(string Context, ISet Verbs)> groupedModels = permissionModelByType.Models
+ .Where(x => x is UnknownTypePermissionPresentationModel)
+ .Cast()
+ .GroupBy(x => x.Context)
+ .Select(x => (x.Key, (ISet)x.SelectMany(y => y.Verbs).Distinct().ToHashSet()));
+
+ foreach ((string context, ISet verbs) in groupedModels)
+ {
+ aggregatedPermissions.Add(new UnknownTypePermissionPresentationModel
+ {
+ Context = context,
+ Verbs = verbs
+ });
+ }
+ }
+ }
return aggregatedPermissions;
}
- private void AggregateAndAddDocumentPermissions(IUser user, HashSet aggregatedPermissions, HashSet permissions)
- {
- // The raw permission data consists of several permissions for each document. We want to aggregate this server-side so
- // we return one set of aggregate permissions per document that the client will use.
-
- // Get the unique document keys that have granular permissions.
- IEnumerable documentKeysWithGranularPermissions = permissions
- .Where(x => x is DocumentPermissionPresentationModel)
- .Cast()
- .Select(x => x.Document.Id)
- .Distinct();
-
- foreach (Guid documentKey in documentKeysWithGranularPermissions)
- {
- // Retrieve the path of the document.
- var path = _contentService.GetById(documentKey)?.Path;
- if (string.IsNullOrEmpty(path))
- {
- continue;
- }
-
- // With the path we can call the same logic as used server-side for authorizing access to resources.
- EntityPermissionSet permissionsForPath = _userService.GetPermissionsForPath(user, path);
- aggregatedPermissions.Add(new DocumentPermissionPresentationModel
- {
- Document = new ReferenceByIdModel(documentKey),
- Verbs = permissionsForPath.GetAllPermissions()
- });
- }
- }
-
- private static void AggregateAndAddDocumentPropertyValuePermissions(HashSet aggregatedPermissions, HashSet permissions)
- {
- // We also have permissions for document type/property type combinations.
- // These don't have an ancestor relationship that we need to take into account, but should be aggregated
- // and included in the set.
- IEnumerable<((Guid DocumentTypeId, Guid PropertyTypeId) Key, ISet Verbs)> documentTypePropertyTypeKeysWithGranularPermissions = permissions
- .Where(x => x is DocumentPropertyValuePermissionPresentationModel)
- .Cast()
- .GroupBy(x => (x.DocumentType.Id, x.PropertyType.Id))
- .Select(x => (x.Key, (ISet)x.SelectMany(y => y.Verbs).Distinct().ToHashSet()));
-
- foreach (((Guid DocumentTypeId, Guid PropertyTypeId) Key, ISet Verbs) documentTypePropertyTypeKey in documentTypePropertyTypeKeysWithGranularPermissions)
- {
- aggregatedPermissions.Add(new DocumentPropertyValuePermissionPresentationModel
- {
- DocumentType = new ReferenceByIdModel(documentTypePropertyTypeKey.Key.DocumentTypeId),
- PropertyType = new ReferenceByIdModel(documentTypePropertyTypeKey.Key.PropertyTypeId),
- Verbs = documentTypePropertyTypeKey.Verbs
- });
- }
- }
-
+ ///
public Task CreateCalculatedUserStartNodesResponseModelAsync(IUser user)
{
var mediaStartNodeIds = user.CalculateMediaStartNodeIds(_entityService, _appCaches);
@@ -357,6 +399,6 @@ public class UserPresentationFactory : IUserPresentationFactory
: new HashSet(models);
}
- private bool HasRootAccess(IEnumerable? startNodeIds)
+ private static bool HasRootAccess(IEnumerable? startNodeIds)
=> startNodeIds?.Contains(Constants.System.Root) is true;
}
diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Package/PackageViewModelMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Package/PackageViewModelMapDefinition.cs
index 4ab05a910d..758580b464 100644
--- a/src/Umbraco.Cms.Api.Management/Mapping/Package/PackageViewModelMapDefinition.cs
+++ b/src/Umbraco.Cms.Api.Management/Mapping/Package/PackageViewModelMapDefinition.cs
@@ -58,6 +58,7 @@ public class PackageViewModelMapDefinition : IMapDefinition
}
// Umbraco.Code.MapAll
+ [Obsolete("Please use the IPackagePresentationFactory instead. Scheduled for removal in V18.")]
private static void Map(InstalledPackage source, PackageMigrationStatusResponseModel target, MapperContext context)
{
if (source.PackageName is not null)
diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Permissions/DocumentPermissionMapper.cs b/src/Umbraco.Cms.Api.Management/Mapping/Permissions/DocumentPermissionMapper.cs
index 55c66221fc..abab7a7fab 100644
--- a/src/Umbraco.Cms.Api.Management/Mapping/Permissions/DocumentPermissionMapper.cs
+++ b/src/Umbraco.Cms.Api.Management/Mapping/Permissions/DocumentPermissionMapper.cs
@@ -1,19 +1,50 @@
+using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.ViewModels;
using Umbraco.Cms.Api.Management.ViewModels.UserGroup.Permissions;
+using Umbraco.Cms.Core.DependencyInjection;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Models.Membership.Permissions;
+using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
using Umbraco.Cms.Infrastructure.Persistence.Mappers;
namespace Umbraco.Cms.Api.Management.Mapping.Permissions;
+
///
-/// Mapping required for mapping all the way from viewmodel to database and back.
+/// Implements for document permissions.
///
///
/// This mapping maps all the way from management api to database in one file intentionally, so it is very clear what it takes, if we wanna add permissions to media or other types in the future.
///
public class DocumentPermissionMapper : IPermissionPresentationMapper, IPermissionMapper
{
+ private readonly Lazy _entityService;
+ private readonly Lazy _userService;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 17.")]
+ public DocumentPermissionMapper()
+ : this(
+ StaticServiceProvider.Instance.GetRequiredService>(),
+ StaticServiceProvider.Instance.GetRequiredService>())
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public DocumentPermissionMapper(Lazy entityService, Lazy userService)
+ {
+ _entityService = entityService;
+ _userService = userService;
+ }
+
+ ///
public string Context => DocumentGranularPermission.ContextType;
+
public IGranularPermission MapFromDto(UserGroup2GranularPermissionDto dto) =>
new DocumentGranularPermission()
{
@@ -21,8 +52,10 @@ public class DocumentPermissionMapper : IPermissionPresentationMapper, IPermissi
Permission = dto.Permission,
};
+ ///
public Type PresentationModelToHandle => typeof(DocumentPermissionPresentationModel);
+ ///
public IEnumerable MapManyAsync(IEnumerable granularPermissions)
{
IEnumerable> keyGroups = granularPermissions.GroupBy(x => x.Key);
@@ -40,6 +73,7 @@ public class DocumentPermissionMapper : IPermissionPresentationMapper, IPermissi
}
}
+ ///
public IEnumerable MapToGranularPermissions(IPermissionPresentationModel permissionViewModel)
{
if (permissionViewModel is not DocumentPermissionPresentationModel documentPermissionPresentationModel)
@@ -47,7 +81,7 @@ public class DocumentPermissionMapper : IPermissionPresentationMapper, IPermissi
yield break;
}
- if(documentPermissionPresentationModel.Verbs.Any() is false || (documentPermissionPresentationModel.Verbs.Count == 1 && documentPermissionPresentationModel.Verbs.Contains(string.Empty)))
+ if (documentPermissionPresentationModel.Verbs.Any() is false || (documentPermissionPresentationModel.Verbs.Count == 1 && documentPermissionPresentationModel.Verbs.Contains(string.Empty)))
{
yield return new DocumentGranularPermission
{
@@ -56,12 +90,14 @@ public class DocumentPermissionMapper : IPermissionPresentationMapper, IPermissi
};
yield break;
}
+
foreach (var verb in documentPermissionPresentationModel.Verbs)
{
if (string.IsNullOrEmpty(verb))
{
continue;
}
+
yield return new DocumentGranularPermission
{
Key = documentPermissionPresentationModel.Document.Id,
@@ -69,4 +105,37 @@ public class DocumentPermissionMapper : IPermissionPresentationMapper, IPermissi
};
}
}
+
+ ///
+ public IEnumerable AggregatePresentationModels(IUser user, IEnumerable models)
+ {
+ // Get the unique document keys that have granular permissions.
+ Guid[] documentKeysWithGranularPermissions = models
+ .Cast()
+ .Select(x => x.Document.Id)
+ .Distinct()
+ .ToArray();
+
+ // Batch retrieve all documents by their keys.
+ var documents = _entityService.Value.GetAll(documentKeysWithGranularPermissions)
+ .ToDictionary(doc => doc.Key, doc => doc.Path);
+
+ // Iterate through each document key that has granular permissions.
+ foreach (Guid documentKey in documentKeysWithGranularPermissions)
+ {
+ // Retrieve the path from the pre-fetched documents.
+ if (!documents.TryGetValue(documentKey, out var path) || string.IsNullOrEmpty(path))
+ {
+ continue;
+ }
+
+ // With the path we can call the same logic as used server-side for authorizing access to resources.
+ EntityPermissionSet permissionsForPath = _userService.Value.GetPermissionsForPath(user, path);
+ yield return new DocumentPermissionPresentationModel
+ {
+ Document = new ReferenceByIdModel(documentKey),
+ Verbs = permissionsForPath.GetAllPermissions(),
+ };
+ }
+ }
}
diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Permissions/DocumentPropertyValuePermissionMapper.cs b/src/Umbraco.Cms.Api.Management/Mapping/Permissions/DocumentPropertyValuePermissionMapper.cs
index ed0964abda..2de518f0ba 100644
--- a/src/Umbraco.Cms.Api.Management/Mapping/Permissions/DocumentPropertyValuePermissionMapper.cs
+++ b/src/Umbraco.Cms.Api.Management/Mapping/Permissions/DocumentPropertyValuePermissionMapper.cs
@@ -1,5 +1,6 @@
-using Umbraco.Cms.Api.Management.ViewModels;
+using Umbraco.Cms.Api.Management.ViewModels;
using Umbraco.Cms.Api.Management.ViewModels.UserGroup.Permissions;
+using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Models.Membership.Permissions;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
using Umbraco.Cms.Infrastructure.Persistence.Mappers;
@@ -7,10 +8,18 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Mapping.Permissions;
+///
+/// Implements for document property value permissions.
+///
public class DocumentPropertyValuePermissionMapper : IPermissionPresentationMapper, IPermissionMapper
{
+ ///
public string Context => DocumentPropertyValueGranularPermission.ContextType;
+ ///
+ public Type PresentationModelToHandle => typeof(DocumentPropertyValuePermissionPresentationModel);
+
+ ///
public IGranularPermission MapFromDto(UserGroup2GranularPermissionDto dto) =>
new DocumentPropertyValueGranularPermission()
{
@@ -18,8 +27,7 @@ public class DocumentPropertyValuePermissionMapper : IPermissionPresentationMapp
Permission = dto.Permission,
};
- public Type PresentationModelToHandle => typeof(DocumentPropertyValuePermissionPresentationModel);
-
+ ///
public IEnumerable MapManyAsync(IEnumerable granularPermissions)
{
var intermediate = granularPermissions.Where(p => p.Key.HasValue).Select(p =>
@@ -50,6 +58,7 @@ public class DocumentPropertyValuePermissionMapper : IPermissionPresentationMapp
}
}
+ ///
public IEnumerable MapToGranularPermissions(IPermissionPresentationModel permissionViewModel)
{
if (permissionViewModel is not DocumentPropertyValuePermissionPresentationModel documentTypePermissionPresentationModel)
@@ -66,4 +75,23 @@ public class DocumentPropertyValuePermissionMapper : IPermissionPresentationMapp
};
}
}
+
+ ///
+ public IEnumerable AggregatePresentationModels(IUser user, IEnumerable models)
+ {
+ IEnumerable<((Guid DocumentTypeId, Guid PropertyTypeId) Key, ISet Verbs)> groupedModels = models
+ .Cast()
+ .GroupBy(x => (x.DocumentType.Id, x.PropertyType.Id))
+ .Select(x => (x.Key, (ISet)x.SelectMany(y => y.Verbs).Distinct().ToHashSet()));
+
+ foreach (((Guid DocumentTypeId, Guid PropertyTypeId) key, ISet verbs) in groupedModels)
+ {
+ yield return new DocumentPropertyValuePermissionPresentationModel
+ {
+ DocumentType = new ReferenceByIdModel(key.DocumentTypeId),
+ PropertyType = new ReferenceByIdModel(key.PropertyTypeId),
+ Verbs = verbs
+ };
+ }
+ }
}
diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Permissions/IPermissionPresentationMapper.cs b/src/Umbraco.Cms.Api.Management/Mapping/Permissions/IPermissionPresentationMapper.cs
index 8ba261ce6e..8860f187dc 100644
--- a/src/Umbraco.Cms.Api.Management/Mapping/Permissions/IPermissionPresentationMapper.cs
+++ b/src/Umbraco.Cms.Api.Management/Mapping/Permissions/IPermissionPresentationMapper.cs
@@ -1,15 +1,36 @@
using Umbraco.Cms.Api.Management.ViewModels.UserGroup.Permissions;
+using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Models.Membership.Permissions;
namespace Umbraco.Cms.Api.Management.Mapping.Permissions;
+///
+/// Defines methods for mapping and aggregating granular permissions to presentation models.
+///
public interface IPermissionPresentationMapper
{
+ ///
+ /// Gets the context type for the permissions being handled by this mapper.
+ ///
string Context { get; }
+ ///
+ /// Gets the type of the presentation model that this mapper handles.
+ ///
Type PresentationModelToHandle { get; }
+ ///
+ /// Maps a granular permission entity to a granular permission model.
+ ///
IEnumerable MapManyAsync(IEnumerable granularPermissions);
+ ///
+ /// Maps a granular permission to a granular permission model.
+ ///
IEnumerable MapToGranularPermissions(IPermissionPresentationModel permissionViewModel);
+
+ ///
+ /// Aggregates multiple permission presentation models into a collection containing only one item per entity with aggregated permissions.
+ ///
+ IEnumerable AggregatePresentationModels(IUser user, IEnumerable models) => [];
}
diff --git a/src/Umbraco.Core/CompatibilitySuppressions.xml b/src/Umbraco.Core/CompatibilitySuppressions.xml
new file mode 100644
index 0000000000..3cd59acc38
--- /dev/null
+++ b/src/Umbraco.Core/CompatibilitySuppressions.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ CP0002
+ M:Umbraco.Cms.Core.Configuration.Models.ContentSettings.get_Error404Collection
+ lib/net9.0/Umbraco.Core.dll
+ lib/net9.0/Umbraco.Core.dll
+ true
+
+
\ No newline at end of file
diff --git a/src/Umbraco.Core/Composing/TypeFinderConfig.cs b/src/Umbraco.Core/Composing/TypeFinderConfig.cs
index 2fd9283500..efba61ccb4 100644
--- a/src/Umbraco.Core/Composing/TypeFinderConfig.cs
+++ b/src/Umbraco.Core/Composing/TypeFinderConfig.cs
@@ -25,8 +25,8 @@ public class TypeFinderConfig : ITypeFinderConfig
var s = _settings.AssembliesAcceptingLoadExceptions;
return _assembliesAcceptingLoadExceptions = string.IsNullOrWhiteSpace(s)
- ? Array.Empty()
- : s.Split(',').Select(x => x.Trim()).ToArray();
+ ? []
+ : s.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
}
}
}
diff --git a/src/Umbraco.Core/Configuration/Models/ContentSettings.cs b/src/Umbraco.Core/Configuration/Models/ContentSettings.cs
index 3e89369f00..6df9b429e9 100644
--- a/src/Umbraco.Core/Configuration/Models/ContentSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/ContentSettings.cs
@@ -50,7 +50,7 @@ public class ContentSettings
///
/// Gets or sets a value for the collection of error pages.
///
- public ISet Error404Collection { get; set; } = new HashSet();
+ public IEnumerable Error404Collection { get; set; } = [];
///
/// Gets or sets a value for the preview badge mark-up.
diff --git a/src/Umbraco.Core/Configuration/Models/NuCacheSerializerType.cs b/src/Umbraco.Core/Configuration/Models/NuCacheSerializerType.cs
index 0506ddb98b..a012f47aa3 100644
--- a/src/Umbraco.Core/Configuration/Models/NuCacheSerializerType.cs
+++ b/src/Umbraco.Core/Configuration/Models/NuCacheSerializerType.cs
@@ -4,10 +4,23 @@
namespace Umbraco.Cms.Core.Configuration.Models;
///
-/// The serializer type that nucache uses to persist documents in the database.
+/// The serializer type that the published content cache uses to persist documents in the database.
///
public enum NuCacheSerializerType
{
- MessagePack = 1, // Default
+ ///
+ /// The default serializer type, which uses MessagePack for serialization.
+ ///
+ MessagePack = 1,
+
+ ///
+ /// The legacy JSON serializer type, which uses JSON for serialization.
+ ///
+ ///
+ /// This option was provided for backward compatibility for the Umbraco cache implementation used from Umbraco 8 to 14 (NuCache).
+ /// It is no longer supported with the cache implementation from Umbraco 15 based on .NET's Hybrid cache.
+ /// Use the faster and more compact instead.
+ /// The option is kept available only for a more readable format suitable for testing purposes.
+ ///
JSON = 2,
}
diff --git a/src/Umbraco.Core/Extensions/DateTimeExtensions.cs b/src/Umbraco.Core/Extensions/DateTimeExtensions.cs
index 00191e5a76..9fadb6217a 100644
--- a/src/Umbraco.Core/Extensions/DateTimeExtensions.cs
+++ b/src/Umbraco.Core/Extensions/DateTimeExtensions.cs
@@ -5,6 +5,9 @@ using System.Globalization;
namespace Umbraco.Extensions;
+///
+/// Provides Extensions for .
+///
public static class DateTimeExtensions
{
///
@@ -18,6 +21,7 @@ public static class DateTimeExtensions
Hour,
Minute,
Second,
+ Millisecond,
}
///
@@ -35,32 +39,15 @@ public static class DateTimeExtensions
/// The level to truncate the date to.
/// The truncated date.
public static DateTime TruncateTo(this DateTime dt, DateTruncate truncateTo)
- {
- if (truncateTo == DateTruncate.Year)
+ => truncateTo switch
{
- return new DateTime(dt.Year, 1, 1, 0, 0, 0, dt.Kind);
- }
-
- if (truncateTo == DateTruncate.Month)
- {
- return new DateTime(dt.Year, dt.Month, 1, 0, 0, 0, dt.Kind);
- }
-
- if (truncateTo == DateTruncate.Day)
- {
- return new DateTime(dt.Year, dt.Month, dt.Day, 0, 0, 0, dt.Kind);
- }
-
- if (truncateTo == DateTruncate.Hour)
- {
- return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, 0, 0, dt.Kind);
- }
-
- if (truncateTo == DateTruncate.Minute)
- {
- return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, 0, dt.Kind);
- }
-
- return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second, dt.Kind);
- }
+ DateTruncate.Year => new DateTime(dt.Year, 1, 1, 0, 0, 0, dt.Kind),
+ DateTruncate.Month => new DateTime(dt.Year, dt.Month, 1, 0, 0, 0, dt.Kind),
+ DateTruncate.Day => new DateTime(dt.Year, dt.Month, dt.Day, 0, 0, 0, dt.Kind),
+ DateTruncate.Hour => new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, 0, 0, dt.Kind),
+ DateTruncate.Minute => new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, 0, dt.Kind),
+ DateTruncate.Second => new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second, dt.Kind),
+ DateTruncate.Millisecond => new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second, dt.Millisecond, dt.Kind),
+ _ => throw new ArgumentOutOfRangeException(nameof(truncateTo), truncateTo, "Invalid truncation level"),
+ };
}
diff --git a/src/Umbraco.Core/Extensions/StringExtensions.cs b/src/Umbraco.Core/Extensions/StringExtensions.cs
index dd5a41bafa..c1269aac60 100644
--- a/src/Umbraco.Core/Extensions/StringExtensions.cs
+++ b/src/Umbraco.Core/Extensions/StringExtensions.cs
@@ -509,8 +509,7 @@ public static class StringExtensions
var convertToHex = input.ConvertToHex();
var hexLength = convertToHex.Length < 32 ? convertToHex.Length : 32;
var hex = convertToHex[..hexLength].PadLeft(32, '0');
- Guid output = Guid.Empty;
- return Guid.TryParse(hex, out output) ? output : Guid.Empty;
+ return Guid.TryParse(hex, out Guid output) ? output : Guid.Empty;
}
///
diff --git a/src/Umbraco.Core/Factories/UserSettingsFactory.cs b/src/Umbraco.Core/Factories/UserSettingsFactory.cs
index c006fe0043..99da578eaa 100644
--- a/src/Umbraco.Core/Factories/UserSettingsFactory.cs
+++ b/src/Umbraco.Core/Factories/UserSettingsFactory.cs
@@ -36,7 +36,6 @@ public class UserSettingsFactory : IUserSettingsFactory
private IEnumerable CreateConsentLevelModels() =>
Enum.GetValues()
- .ToList()
.Select(level => new ConsentLevelModel
{
Level = level,
diff --git a/src/Umbraco.Core/Logging/LoggingConfiguration.cs b/src/Umbraco.Core/Logging/LoggingConfiguration.cs
index b6d6893482..d21d56dfea 100644
--- a/src/Umbraco.Core/Logging/LoggingConfiguration.cs
+++ b/src/Umbraco.Core/Logging/LoggingConfiguration.cs
@@ -58,8 +58,7 @@ public class LoggingConfiguration : ILoggingConfiguration
public string LogFileNameFormat { get; }
///
- public string[] GetLogFileNameFormatArguments() => _logFileNameFormatArguments.Split(',', StringSplitOptions.RemoveEmptyEntries)
- .Select(x => x.Trim())
+ public string[] GetLogFileNameFormatArguments() => _logFileNameFormatArguments.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(GetValue)
.ToArray();
diff --git a/src/Umbraco.Core/Models/Blocks/BlockEditorDataConverter.cs b/src/Umbraco.Core/Models/Blocks/BlockEditorDataConverter.cs
index b3bc74e54c..c408d40100 100644
--- a/src/Umbraco.Core/Models/Blocks/BlockEditorDataConverter.cs
+++ b/src/Umbraco.Core/Models/Blocks/BlockEditorDataConverter.cs
@@ -71,12 +71,12 @@ public abstract class BlockEditorDataConverter
// this method is only meant to have any effect when migrating block editor values
// from the original format to the new, variant enabled format
- private void AmendExpose(TValue value)
- => value.Expose = value.ContentData.Select(cd => new BlockItemVariation(cd.Key, null, null)).ToList();
+ private static void AmendExpose(TValue value)
+ => value.Expose = value.ContentData.ConvertAll(cd => new BlockItemVariation(cd.Key, null, null));
// this method is only meant to have any effect when migrating block editor values
// from the original format to the new, variant enabled format
- private bool ConvertOriginalBlockFormat(List blockItemDatas)
+ private static bool ConvertOriginalBlockFormat(List blockItemDatas)
{
var converted = false;
foreach (BlockItemData blockItemData in blockItemDatas)
diff --git a/src/Umbraco.Core/Notifications/ContentSavedBlueprintNotification.cs b/src/Umbraco.Core/Notifications/ContentSavedBlueprintNotification.cs
index 9219b89f23..897065a532 100644
--- a/src/Umbraco.Core/Notifications/ContentSavedBlueprintNotification.cs
+++ b/src/Umbraco.Core/Notifications/ContentSavedBlueprintNotification.cs
@@ -5,6 +5,7 @@ using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Notifications;
+
///
/// A notification that is used to trigger the IContentService when the SavedBlueprint method is called in the API.
///
@@ -14,8 +15,21 @@ public sealed class ContentSavedBlueprintNotification : ObjectNotification
/// Getting the saved blueprint object.
///
public IContent SavedBlueprint => Target;
+
+ ///
+ /// Getting the saved blueprint object.
+ ///
+ public IContent? CreatedFromContent { get; }
+
}
diff --git a/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs
index 477f3e5c50..cdf05ca5aa 100644
--- a/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs
@@ -19,6 +19,17 @@ public interface IEntityRepository : IRepository
IEnumerable GetAll(Guid objectType, params Guid[] keys);
+ ///
+ /// Gets sibling entities of a specified target entity, within a given range before and after the target, ordered as specified.
+ ///
+ /// The object type key of the entities.
+ /// The key of the target entity whose siblings are to be retrieved.
+ /// The number of siblings to retrieve before the target entity.
+ /// The number of siblings to retrieve after the target entity.
+ /// The ordering to apply to the siblings.
+ /// Enumerable of sibling entities.
+ IEnumerable GetSiblings(Guid objectType, Guid targetKey, int before, int after, Ordering ordering) => [];
+
///
/// Gets entities for a query
///
diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs
index 96e84bfb83..727c28f077 100644
--- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs
+++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs
@@ -115,7 +115,7 @@ public class MultiNodeTreePickerValueConverter : PropertyValueConverterBase, IDe
udi,
ref objectType,
UmbracoObjectTypes.Document,
- id => _contentCache.GetById(guidUdi.Guid));
+ id => _contentCache.GetById(preview, guidUdi.Guid));
break;
case Constants.UdiEntityType.Media:
multiNodeTreePickerItem = GetPublishedContent(
@@ -205,7 +205,7 @@ public class MultiNodeTreePickerValueConverter : PropertyValueConverterBase, IDe
{
Constants.UdiEntityType.Document => entityTypeUdis.Select(udi =>
{
- IPublishedContent? content = _contentCache.GetById(udi.Guid);
+ IPublishedContent? content = _contentCache.GetById(preview, udi.Guid);
return content != null
? _apiContentBuilder.Build(content)
: null;
diff --git a/src/Umbraco.Core/Routing/DomainUtilities.cs b/src/Umbraco.Core/Routing/DomainUtilities.cs
index 2d9be885a3..03ddd438d3 100644
--- a/src/Umbraco.Core/Routing/DomainUtilities.cs
+++ b/src/Umbraco.Core/Routing/DomainUtilities.cs
@@ -256,14 +256,14 @@ namespace Umbraco.Cms.Core.Routing
// if a culture is specified, then try to get domains for that culture
// (else cultureDomains will be null)
// do NOT specify a default culture, else it would pick those domains
- IReadOnlyCollection? cultureDomains = SelectByCulture(domainsAndUris, culture, defaultCulture: null);
+ IReadOnlyList? cultureDomains = SelectByCulture(domainsAndUris, culture, defaultCulture: null);
IReadOnlyCollection considerForBaseDomains = domainsAndUris;
if (cultureDomains != null)
{
if (cultureDomains.Count == 1)
{
// only 1, return
- return cultureDomains.First();
+ return cultureDomains[0];
}
// else restrict to those domains, for base lookup
@@ -272,11 +272,11 @@ namespace Umbraco.Cms.Core.Routing
// look for domains that would be the base of the uri
// we need to order so example.com/foo matches before example.com/
- IReadOnlyCollection baseDomains = SelectByBase(considerForBaseDomains.OrderByDescending(d => d.Uri.ToString()).ToList(), uri, culture);
+ List baseDomains = SelectByBase(considerForBaseDomains.OrderByDescending(d => d.Uri.ToString()).ToArray(), uri, culture);
if (baseDomains.Count > 0)
{
// found, return
- return baseDomains.First();
+ return baseDomains[0];
}
// if nothing works, then try to run the filter to select a domain
@@ -296,7 +296,7 @@ namespace Umbraco.Cms.Core.Routing
private static bool MatchesCulture(DomainAndUri domain, string? culture)
=> culture == null || domain.Culture.InvariantEquals(culture);
- private static IReadOnlyCollection SelectByBase(IReadOnlyCollection domainsAndUris, Uri uri, string? culture)
+ private static List SelectByBase(DomainAndUri[] domainsAndUris, Uri uri, string? culture)
{
// look for domains that would be the base of the uri
// ie current is www.example.com/foo/bar, look for domain www.example.com
@@ -314,7 +314,7 @@ namespace Umbraco.Cms.Core.Routing
return baseDomains;
}
- private static IReadOnlyCollection? SelectByCulture(IReadOnlyCollection domainsAndUris, string? culture, string? defaultCulture)
+ private static List? SelectByCulture(DomainAndUri[] domainsAndUris, string? culture, string? defaultCulture)
{
// we try our best to match cultures, but may end with a bogus domain
if (culture is not null)
@@ -434,13 +434,18 @@ namespace Umbraco.Cms.Core.Routing
private static Domain? FindDomainInPath(IEnumerable? domains, string path, int? rootNodeId, bool isWildcard)
{
+ if (domains is null)
+ {
+ return null;
+ }
+
var stopNodeId = rootNodeId ?? -1;
return path.Split(Constants.CharArrays.Comma)
.Reverse()
.Select(s => int.Parse(s, CultureInfo.InvariantCulture))
.TakeWhile(id => id != stopNodeId)
- .Select(id => domains?.FirstOrDefault(d => d.ContentId == id && d.IsWildcard == isWildcard))
+ .Select(id => domains.FirstOrDefault(d => d.ContentId == id && d.IsWildcard == isWildcard))
.FirstOrDefault(domain => domain is not null);
}
diff --git a/src/Umbraco.Core/Services/ContentBlueprintEditingService.cs b/src/Umbraco.Core/Services/ContentBlueprintEditingService.cs
index 7af5171bc9..d2e68ad327 100644
--- a/src/Umbraco.Core/Services/ContentBlueprintEditingService.cs
+++ b/src/Umbraco.Core/Services/ContentBlueprintEditingService.cs
@@ -45,11 +45,13 @@ internal sealed class ContentBlueprintEditingService
return Task.FromResult(null);
}
+ IContent scaffold = blueprint.DeepCloneWithResetIdentities();
+
using ICoreScope scope = CoreScopeProvider.CreateCoreScope();
- scope.Notifications.Publish(new ContentScaffoldedNotification(blueprint, blueprint, Constants.System.Root, new EventMessages()));
+ scope.Notifications.Publish(new ContentScaffoldedNotification(blueprint, scaffold, Constants.System.Root, new EventMessages()));
scope.Complete();
- return Task.FromResult(blueprint);
+ return Task.FromResult(scaffold);
}
public async Task?, ContentEditingOperationStatus>> GetPagedByContentTypeAsync(Guid contentTypeKey, int skip, int take)
@@ -112,7 +114,7 @@ internal sealed class ContentBlueprintEditingService
// Create Blueprint
var currentUserId = await GetUserIdAsync(userKey);
- IContent blueprint = ContentService.CreateContentFromBlueprint(content, name, currentUserId);
+ IContent blueprint = ContentService.CreateBlueprintFromContent(content, name, currentUserId);
if (key.HasValue)
{
@@ -120,7 +122,7 @@ internal sealed class ContentBlueprintEditingService
}
// Save blueprint
- await SaveAsync(blueprint, userKey);
+ await SaveAsync(blueprint, userKey, content);
return Attempt.SucceedWithStatus(ContentEditingOperationStatus.Success, new ContentCreateResult { Content = blueprint });
}
@@ -238,10 +240,10 @@ internal sealed class ContentBlueprintEditingService
protected override OperationResult? Delete(IContent content, int userId) => throw new NotImplementedException();
- private async Task SaveAsync(IContent blueprint, Guid userKey)
+ private async Task SaveAsync(IContent blueprint, Guid userKey, IContent? createdFromContent = null)
{
var currentUserId = await GetUserIdAsync(userKey);
- ContentService.SaveBlueprint(blueprint, currentUserId);
+ ContentService.SaveBlueprint(blueprint, createdFromContent, currentUserId);
}
private bool ValidateUniqueName(string name, IContent content)
diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs
index f246b9c3f0..0c3b61a1c3 100644
--- a/src/Umbraco.Core/Services/ContentService.cs
+++ b/src/Umbraco.Core/Services/ContentService.cs
@@ -3611,6 +3611,9 @@ public class ContentService : RepositoryService, IContentService
}
public void SaveBlueprint(IContent content, int userId = Constants.Security.SuperUserId)
+ => SaveBlueprint(content, null, userId);
+
+ public void SaveBlueprint(IContent content, IContent? createdFromContent, int userId = Constants.Security.SuperUserId)
{
EventMessages evtMsgs = EventMessagesFactory.Get();
@@ -3631,7 +3634,7 @@ public class ContentService : RepositoryService, IContentService
Audit(AuditType.Save, userId, content.Id, $"Saved content template: {content.Name}");
- scope.Notifications.Publish(new ContentSavedBlueprintNotification(content, evtMsgs));
+ scope.Notifications.Publish(new ContentSavedBlueprintNotification(content, createdFromContent, evtMsgs));
scope.Notifications.Publish(new ContentTreeChangeNotification(content, TreeChangeTypes.RefreshNode, evtMsgs));
scope.Complete();
@@ -3654,12 +3657,12 @@ public class ContentService : RepositoryService, IContentService
private static readonly string?[] ArrayOfOneNullString = { null };
- public IContent CreateContentFromBlueprint(IContent blueprint, string name, int userId = Constants.Security.SuperUserId)
+ public IContent CreateBlueprintFromContent(
+ IContent blueprint,
+ string name,
+ int userId = Constants.Security.SuperUserId)
{
- if (blueprint == null)
- {
- throw new ArgumentNullException(nameof(blueprint));
- }
+ ArgumentNullException.ThrowIfNull(blueprint);
IContentType contentType = GetContentType(blueprint.ContentType.Alias);
var content = new Content(name, -1, contentType);
@@ -3672,15 +3675,13 @@ public class ContentService : RepositoryService, IContentService
if (blueprint.CultureInfos?.Count > 0)
{
cultures = blueprint.CultureInfos.Values.Select(x => x.Culture);
- using (ICoreScope scope = ScopeProvider.CreateCoreScope())
+ using ICoreScope scope = ScopeProvider.CreateCoreScope();
+ if (blueprint.CultureInfos.TryGetValue(_languageRepository.GetDefaultIsoCode(), out ContentCultureInfos defaultCulture))
{
- if (blueprint.CultureInfos.TryGetValue(_languageRepository.GetDefaultIsoCode(), out ContentCultureInfos defaultCulture))
- {
- defaultCulture.Name = name;
- }
-
- scope.Complete();
+ defaultCulture.Name = name;
}
+
+ scope.Complete();
}
DateTime now = DateTime.Now;
@@ -3701,6 +3702,11 @@ public class ContentService : RepositoryService, IContentService
return content;
}
+ ///
+ [Obsolete("Use IContentBlueprintEditingService.GetScaffoldedAsync() instead. Scheduled for removal in V18.")]
+ public IContent CreateContentFromBlueprint(IContent blueprint, string name, int userId = Constants.Security.SuperUserId)
+ => CreateBlueprintFromContent(blueprint, name, userId);
+
public IEnumerable GetBlueprintsForContentTypes(params int[] contentTypeId)
{
using (ScopeProvider.CreateCoreScope(autoComplete: true))
diff --git a/src/Umbraco.Core/Services/ContentTypeEditing/ContentTypeEditingServiceBase.cs b/src/Umbraco.Core/Services/ContentTypeEditing/ContentTypeEditingServiceBase.cs
index fdf160ef4f..680d78d5ea 100644
--- a/src/Umbraco.Core/Services/ContentTypeEditing/ContentTypeEditingServiceBase.cs
+++ b/src/Umbraco.Core/Services/ContentTypeEditing/ContentTypeEditingServiceBase.cs
@@ -50,7 +50,7 @@ internal abstract class ContentTypeEditingServiceBase currentCompositeKeys.Contains(ct.Key)).Select(ct => ct.Alias).ToArray()
- : Array.Empty();
+ : [];
ContentTypeAvailableCompositionsResults availableCompositions = _contentTypeService.GetAvailableCompositeContentTypes(
contentType,
@@ -142,9 +142,9 @@ internal abstract class ContentTypeEditingServiceBase(ContentTypeOperationStatus.Success, contentType);
}
- protected virtual async Task AdditionalCreateValidationAsync(
+ protected virtual Task AdditionalCreateValidationAsync(
ContentTypeEditingModelBase model)
- => await Task.FromResult(ContentTypeOperationStatus.Success);
+ => Task.FromResult(ContentTypeOperationStatus.Success);
#region Sanitization
@@ -346,7 +346,7 @@ internal abstract class ContentTypeEditingServiceBase model, IContentTypeComposition[] allContentTypeCompositions)
+ private static ContentTypeOperationStatus ValidateProperties(ContentTypeEditingModelBase model, IContentTypeComposition[] allContentTypeCompositions)
{
// grab all content types used for composition and/or inheritance
Guid[] allCompositionKeys = KeysForCompositionTypes(model, CompositionType.Composition, CompositionType.Inheritance);
@@ -365,7 +365,7 @@ internal abstract class ContentTypeEditingServiceBase model, IContentTypeComposition[] allContentTypeCompositions)
+ private static ContentTypeOperationStatus ValidateContainers(ContentTypeEditingModelBase model, IContentTypeComposition[] allContentTypeCompositions)
{
if (model.Containers.Any(container => Enum.TryParse(container.Type, out _) is false))
{
@@ -420,7 +420,7 @@ internal abstract class ContentTypeEditingServiceBase model,
IContentTypeComposition[] allContentTypeCompositions)
@@ -573,7 +573,7 @@ internal abstract class ContentTypeEditingServiceBase model,
IContentTypeComposition[] allContentTypeCompositions)
@@ -658,7 +658,7 @@ internal abstract class ContentTypeEditingServiceBase model,
IContentTypeComposition[] allContentTypeCompositions)
@@ -677,7 +677,7 @@ internal abstract class ContentTypeEditingServiceBase model)
+ private static Guid[] GetDataTypeKeys(ContentTypeEditingModelBase model)
=> model.Properties.Select(property => property.DataTypeKey).Distinct().ToArray();
private async Task GetDataTypesAsync(ContentTypeEditingModelBase model)
@@ -685,7 +685,7 @@ internal abstract class ContentTypeEditingServiceBase();
+ : [];
}
private int? GetParentId(ContentTypeEditingModelBase model, Guid? containerKey)
@@ -711,7 +711,7 @@ internal abstract class ContentTypeEditingServiceBase model, params CompositionType[] compositionTypes)
+ private static Guid[] KeysForCompositionTypes(ContentTypeEditingModelBase model, params CompositionType[] compositionTypes)
=> model.Compositions
.Where(c => compositionTypes.Contains(c.CompositionType))
.Select(c => c.Key)
diff --git a/src/Umbraco.Core/Services/ContentTypeEditing/ElementSwitchValidator.cs b/src/Umbraco.Core/Services/ContentTypeEditing/ElementSwitchValidator.cs
index bf02fa6759..4d41e78b1e 100644
--- a/src/Umbraco.Core/Services/ContentTypeEditing/ElementSwitchValidator.cs
+++ b/src/Umbraco.Core/Services/ContentTypeEditing/ElementSwitchValidator.cs
@@ -20,27 +20,27 @@ public class ElementSwitchValidator : IElementSwitchValidator
_dataTypeService = dataTypeService;
}
- public async Task AncestorsAreAlignedAsync(IContentType contentType)
+ public Task AncestorsAreAlignedAsync(IContentType contentType)
{
// this call does not return the system roots
var ancestorIds = contentType.AncestorIds();
if (ancestorIds.Length == 0)
{
// if there are no ancestors, validation passes
- return true;
+ return Task.FromResult(true);
}
// if there are any ancestors where IsElement is different from the contentType, the validation fails
- return await Task.FromResult(_contentTypeService.GetMany(ancestorIds)
+ return Task.FromResult(_contentTypeService.GetMany(ancestorIds)
.Any(ancestor => ancestor.IsElement != contentType.IsElement) is false);
}
- public async Task DescendantsAreAlignedAsync(IContentType contentType)
+ public Task DescendantsAreAlignedAsync(IContentType contentType)
{
IEnumerable descendants = _contentTypeService.GetDescendants(contentType.Id, false);
// if there are any descendants where IsElement is different from the contentType, the validation fails
- return await Task.FromResult(descendants.Any(descendant => descendant.IsElement != contentType.IsElement) is false);
+ return Task.FromResult(descendants.Any(descendant => descendant.IsElement != contentType.IsElement) is false);
}
public async Task ElementToDocumentNotUsedInBlockStructuresAsync(IContentTypeBase contentType)
@@ -59,8 +59,8 @@ public class ElementSwitchValidator : IElementSwitchValidator
.ConfiguredElementTypeKeys().Contains(contentType.Key)) is false;
}
- public async Task DocumentToElementHasNoContentAsync(IContentTypeBase contentType) =>
+ public Task DocumentToElementHasNoContentAsync(IContentTypeBase contentType) =>
// if any content for the content type exists, the validation fails.
- await Task.FromResult(_contentTypeService.HasContentNodes(contentType.Id) is false);
+ Task.FromResult(_contentTypeService.HasContentNodes(contentType.Id) is false);
}
diff --git a/src/Umbraco.Core/Services/DataTypeService.cs b/src/Umbraco.Core/Services/DataTypeService.cs
index 83effe7400..8b1f7f953c 100644
--- a/src/Umbraco.Core/Services/DataTypeService.cs
+++ b/src/Umbraco.Core/Services/DataTypeService.cs
@@ -355,13 +355,13 @@ namespace Umbraco.Cms.Core.Services.Implement
}
///
- public async Task> GetByEditorAliasAsync(string[] propertyEditorAlias)
+ public Task> GetByEditorAliasAsync(string[] propertyEditorAlias)
{
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
IQuery query = Query().Where(x => propertyEditorAlias.Contains(x.EditorAlias));
IEnumerable dataTypes = _dataTypeRepository.Get(query).ToArray();
ConvertMissingEditorsOfDataTypesToLabels(dataTypes);
- return await Task.FromResult(dataTypes);
+ return Task.FromResult(dataTypes);
}
///
@@ -396,7 +396,7 @@ namespace Umbraco.Cms.Core.Services.Implement
return;
}
- ConvertMissingEditorsOfDataTypesToLabels(new[] { dataType });
+ ConvertMissingEditorsOfDataTypesToLabels([dataType]);
}
private void ConvertMissingEditorsOfDataTypesToLabels(IEnumerable dataTypes)
@@ -763,7 +763,7 @@ namespace Umbraco.Cms.Core.Services.Implement
var totalItems = combinedUsages.Count;
// Create the page of items.
- IList<(string PropertyAlias, Udi Udi)> pagedUsages = combinedUsages
+ List<(string PropertyAlias, Udi Udi)> pagedUsages = combinedUsages
.OrderBy(x => x.Udi.EntityType) // Document types first, then media types, then member types.
.ThenBy(x => x.PropertyAlias)
.Skip(skip)
@@ -772,7 +772,7 @@ namespace Umbraco.Cms.Core.Services.Implement
// Get the content types for the UDIs referenced in the page of items to construct the response from.
// They could be document, media or member types.
- IList contentTypes = GetReferencedContentTypes(pagedUsages);
+ List contentTypes = GetReferencedContentTypes(pagedUsages);
IEnumerable relations = pagedUsages
.Select(x =>
@@ -807,7 +807,7 @@ namespace Umbraco.Cms.Core.Services.Implement
return Task.FromResult(pagedModel);
}
- private IList GetReferencedContentTypes(IList<(string PropertyAlias, Udi Udi)> pagedUsages)
+ private List GetReferencedContentTypes(List<(string PropertyAlias, Udi Udi)> pagedUsages)
{
IEnumerable documentTypes = GetContentTypes(
pagedUsages,
@@ -845,10 +845,10 @@ namespace Umbraco.Cms.Core.Services.Implement
{
IConfigurationEditor? configurationEditor = dataType.Editor?.GetConfigurationEditor();
return configurationEditor == null
- ? new[]
- {
+ ?
+ [
new ValidationResult($"Data type with editor alias {dataType.EditorAlias} does not have a configuration editor")
- }
+ ]
: configurationEditor.Validate(dataType.ConfigurationData);
}
diff --git a/src/Umbraco.Core/Services/DocumentUrlService.cs b/src/Umbraco.Core/Services/DocumentUrlService.cs
index c73b2c41e6..2b60d736cc 100644
--- a/src/Umbraco.Core/Services/DocumentUrlService.cs
+++ b/src/Umbraco.Core/Services/DocumentUrlService.cs
@@ -44,26 +44,51 @@ public class DocumentUrlService : IDocumentUrlService
///
/// Model used to cache a single published document along with all it's URL segments.
///
- private class PublishedDocumentUrlSegments
+ /// Internal for the purpose of unit and benchmark testing.
+ internal class PublishedDocumentUrlSegments
{
+ ///
+ /// Gets or sets the document key.
+ ///
public required Guid DocumentKey { get; set; }
+ ///
+ /// Gets or sets the language Id.
+ ///
public required int LanguageId { get; set; }
+ ///
+ /// Gets or sets the collection of for the document, language and state.
+ ///
public required IList UrlSegments { get; set; }
+ ///
+ /// Gets or sets a value indicating whether the document is a draft version or not.
+ ///
public required bool IsDraft { get; set; }
+ ///
+ /// Model used to represent a URL segment for a document in the cache.
+ ///
public class UrlSegment
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
public UrlSegment(string segment, bool isPrimary)
{
Segment = segment;
IsPrimary = isPrimary;
}
+ ///
+ /// Gets the URL segment string.
+ ///
public string Segment { get; }
+ ///
+ /// Gets a value indicating whether this URL segment is the primary one for the document, language and state.
+ ///
public bool IsPrimary { get; }
}
}
@@ -116,14 +141,18 @@ public class DocumentUrlService : IDocumentUrlService
using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
if (ShouldRebuildUrls())
{
- _logger.LogInformation("Rebuilding all URLs.");
+ _logger.LogInformation("Rebuilding all document URLs.");
await RebuildAllUrlsAsync();
}
+ _logger.LogInformation("Caching document URLs.");
+
IEnumerable publishedDocumentUrlSegments = _documentUrlRepository.GetAll();
IEnumerable languages = await _languageService.GetAllAsync();
var languageIdToIsoCode = languages.ToDictionary(x => x.Id, x => x.IsoCode);
+
+ int numberOfCachedUrls = 0;
foreach (PublishedDocumentUrlSegments publishedDocumentUrlSegment in ConvertToCacheModel(publishedDocumentUrlSegments))
{
if (cancellationToken.IsCancellationRequested)
@@ -134,9 +163,12 @@ public class DocumentUrlService : IDocumentUrlService
if (languageIdToIsoCode.TryGetValue(publishedDocumentUrlSegment.LanguageId, out var isoCode))
{
UpdateCache(_coreScopeProvider.Context!, publishedDocumentUrlSegment, isoCode);
+ numberOfCachedUrls++;
}
}
+ _logger.LogInformation("Cached {NumberOfUrls} document URLs.", numberOfCachedUrls);
+
_isInitialized = true;
scope.Complete();
}
@@ -168,45 +200,40 @@ public class DocumentUrlService : IDocumentUrlService
scope.Complete();
}
- private static IEnumerable ConvertToCacheModel(IEnumerable publishedDocumentUrlSegments)
+ ///
+ /// Converts a collection of to a collection of for caching purposes.
+ ///
+ /// The collection of retrieved from the database on startup.
+ /// The collection of cache models.
+ /// Internal for the purpose of unit and benchmark testing.
+ internal static IEnumerable ConvertToCacheModel(IEnumerable publishedDocumentUrlSegments)
{
- var cacheModels = new List();
+ var cacheModels = new Dictionary<(Guid DocumentKey, int LanguageId, bool IsDraft), PublishedDocumentUrlSegments>();
+
foreach (PublishedDocumentUrlSegment model in publishedDocumentUrlSegments)
{
- PublishedDocumentUrlSegments? existingCacheModel = GetModelFromCache(cacheModels, model);
- if (existingCacheModel is null)
+ (Guid DocumentKey, int LanguageId, bool IsDraft) key = (model.DocumentKey, model.LanguageId, model.IsDraft);
+
+ if (!cacheModels.TryGetValue(key, out PublishedDocumentUrlSegments? existingCacheModel))
{
- cacheModels.Add(new PublishedDocumentUrlSegments
+ cacheModels[key] = new PublishedDocumentUrlSegments
{
DocumentKey = model.DocumentKey,
LanguageId = model.LanguageId,
UrlSegments = [new PublishedDocumentUrlSegments.UrlSegment(model.UrlSegment, model.IsPrimary)],
IsDraft = model.IsDraft,
- });
+ };
}
else
{
- existingCacheModel.UrlSegments = GetUpdatedUrlSegments(existingCacheModel.UrlSegments, model.UrlSegment, model.IsPrimary);
+ if (existingCacheModel.UrlSegments.Any(x => x.Segment == model.UrlSegment) is false)
+ {
+ existingCacheModel.UrlSegments.Add(new PublishedDocumentUrlSegments.UrlSegment(model.UrlSegment, model.IsPrimary));
+ }
}
}
- return cacheModels;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static PublishedDocumentUrlSegments? GetModelFromCache(List cacheModels, PublishedDocumentUrlSegment model)
- => cacheModels
- .SingleOrDefault(x => x.DocumentKey == model.DocumentKey && x.LanguageId == model.LanguageId && x.IsDraft == model.IsDraft);
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static IList GetUpdatedUrlSegments(IList urlSegments, string segment, bool isPrimary)
- {
- if (urlSegments.FirstOrDefault(x => x.Segment == segment) is null)
- {
- urlSegments.Add(new PublishedDocumentUrlSegments.UrlSegment(segment, isPrimary));
- }
-
- return urlSegments;
+ return cacheModels.Values;
}
private void RemoveFromCache(IScopeContext scopeContext, Guid documentKey, string isoCode, bool isDraft)
diff --git a/src/Umbraco.Core/Services/EntityService.cs b/src/Umbraco.Core/Services/EntityService.cs
index f2fe2eefd8..0a284bdebf 100644
--- a/src/Umbraco.Core/Services/EntityService.cs
+++ b/src/Umbraco.Core/Services/EntityService.cs
@@ -318,6 +318,39 @@ public class EntityService : RepositoryService, IEntityService
return children;
}
+ ///
+ public IEnumerable GetSiblings(
+ Guid key,
+ UmbracoObjectTypes objectType,
+ int before,
+ int after,
+ Ordering? ordering = null)
+ {
+ if (before < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(before), "The 'before' parameter must be greater than or equal to 0.");
+ }
+
+ if (after < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(after), "The 'after' parameter must be greater than or equal to 0.");
+ }
+
+ ordering ??= new Ordering("sortOrder");
+
+ using ICoreScope scope = ScopeProvider.CreateCoreScope();
+
+ IEnumerable siblings = _entityRepository.GetSiblings(
+ objectType.GetGuid(),
+ key,
+ before,
+ after,
+ ordering);
+
+ scope.Complete();
+ return siblings;
+ }
+
///
public virtual IEnumerable GetDescendants(int id)
{
diff --git a/src/Umbraco.Core/Services/EntityTypeContainerService.cs b/src/Umbraco.Core/Services/EntityTypeContainerService.cs
index 4a403e0c1f..575ff11f2e 100644
--- a/src/Umbraco.Core/Services/EntityTypeContainerService.cs
+++ b/src/Umbraco.Core/Services/EntityTypeContainerService.cs
@@ -52,18 +52,18 @@ internal abstract class EntityTypeContainerService
- public async Task> GetAsync(string name, int level)
+ public Task> GetAsync(string name, int level)
{
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
ReadLock(scope);
- return await Task.FromResult(_entityContainerRepository.Get(name, level));
+ return Task.FromResult(_entityContainerRepository.Get(name, level));
}
///
- public async Task> GetAllAsync()
+ public Task> GetAllAsync()
{
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
ReadLock(scope);
- return await Task.FromResult(_entityContainerRepository.GetMany());
+ return Task.FromResult(_entityContainerRepository.GetMany());
}
///
diff --git a/src/Umbraco.Core/Services/ICacheInstructionService.cs b/src/Umbraco.Core/Services/ICacheInstructionService.cs
index 25b52c09e3..94ccea42b4 100644
--- a/src/Umbraco.Core/Services/ICacheInstructionService.cs
+++ b/src/Umbraco.Core/Services/ICacheInstructionService.cs
@@ -34,14 +34,37 @@ public interface ICacheInstructionService
void DeliverInstructionsInBatches(IEnumerable instructions, string localIdentity);
///
- /// Processes and then prunes pending database cache instructions.
+ /// Processes pending database cache instructions.
+ ///
+ /// Cache refreshers.
+ /// Cancellation token.
+ /// Local identity of the executing AppDomain.
+ /// Id of the latest processed instruction.
+ /// The processing result.
+ ProcessInstructionsResult ProcessInstructions(
+ CacheRefresherCollection cacheRefreshers,
+ CancellationToken cancellationToken,
+ string localIdentity,
+ int lastId) =>
+ ProcessInstructions(
+ cacheRefreshers,
+ ServerRole.Unknown,
+ cancellationToken,
+ localIdentity,
+ lastPruned: DateTime.UtcNow,
+ lastId);
+
+ ///
+ /// Processes pending database cache instructions.
///
/// Cache refreshers.
/// Server role.
/// Cancellation token.
/// Local identity of the executing AppDomain.
/// Date of last prune operation.
- /// Id of the latest processed instruction
+ /// Id of the latest processed instruction.
+ /// The processing result.
+ [Obsolete("Use the non-obsolete overload. Scheduled for removal in V17.")]
ProcessInstructionsResult ProcessInstructions(
CacheRefresherCollection cacheRefreshers,
ServerRole serverRole,
diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs
index ea14e6771a..a7bde2dc46 100644
--- a/src/Umbraco.Core/Services/IContentService.cs
+++ b/src/Umbraco.Core/Services/IContentService.cs
@@ -47,16 +47,35 @@ public interface IContentService : IContentServiceBase
///
/// Saves a blueprint.
///
+ [Obsolete("Please use the method taking all parameters. Scheduled for removal in Umbraco 18.")]
void SaveBlueprint(IContent content, int userId = Constants.Security.SuperUserId);
+ ///
+ /// Saves a blueprint.
+ ///
+ void SaveBlueprint(IContent content, IContent? createdFromContent, int userId = Constants.Security.SuperUserId)
+#pragma warning disable CS0618 // Type or member is obsolete
+ => SaveBlueprint(content, userId);
+#pragma warning restore CS0618 // Type or member is obsolete
+
///
/// Deletes a blueprint.
///
void DeleteBlueprint(IContent content, int userId = Constants.Security.SuperUserId);
///
- /// Creates a new content item from a blueprint.
+ /// Creates a blueprint from a content item.
///
+ // TODO: Remove the default implementation when CreateContentFromBlueprint is removed.
+ IContent CreateBlueprintFromContent(IContent blueprint, string name, int userId = Constants.Security.SuperUserId)
+ => throw new NotImplementedException();
+
+ ///
+ /// (Deprecated) Creates a new content item from a blueprint.
+ ///
+ /// If creating content from a blueprint, use
+ /// instead. If creating a blueprint from content use instead.
+ [Obsolete("Use IContentBlueprintEditingService.GetScaffoldedAsync() instead. Scheduled for removal in V18.")]
IContent CreateContentFromBlueprint(IContent blueprint, string name, int userId = Constants.Security.SuperUserId);
///
diff --git a/src/Umbraco.Core/Services/IEntityService.cs b/src/Umbraco.Core/Services/IEntityService.cs
index 964ec9f502..6652062ac0 100644
--- a/src/Umbraco.Core/Services/IEntityService.cs
+++ b/src/Umbraco.Core/Services/IEntityService.cs
@@ -170,6 +170,22 @@ public interface IEntityService
/// The object type of the parent.
IEntitySlim? GetParent(int id, UmbracoObjectTypes objectType);
+ ///
+ /// Gets sibling entities of a specified target entity, within a given range before and after the target, ordered as specified.
+ ///
+ /// The key of the target entity whose siblings are to be retrieved.
+ /// The object type key of the entities.
+ /// The number of siblings to retrieve before the target entity. Needs to be greater or equal to 0.
+ /// The number of siblings to retrieve after the target entity. Needs to be greater or equal to 0.
+ /// The ordering to apply to the siblings.
+ /// Enumerable of sibling entities.
+ IEnumerable GetSiblings(
+ Guid key,
+ UmbracoObjectTypes objectType,
+ int before,
+ int after,
+ Ordering? ordering = null) => [];
+
///
/// Gets the children of an entity.
///
diff --git a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs
index d7562a76b9..bb849c61e7 100644
--- a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs
+++ b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs
@@ -17,8 +17,8 @@ internal abstract class ContentNavigationServiceBase> _contentTypeAliasToKeyMap;
private ConcurrentDictionary _navigationStructure = new();
private ConcurrentDictionary _recycleBinNavigationStructure = new();
- private IList _roots = new List();
- private IList _recycleBinRoots = new List();
+ private HashSet _roots = [];
+ private HashSet _recycleBinRoots = [];
protected ContentNavigationServiceBase(ICoreScopeProvider coreScopeProvider, INavigationRepository navigationRepository, TContentTypeService typeService)
{
@@ -321,7 +321,7 @@ internal abstract class ContentNavigationServiceBase structure, Guid childKey, out Guid? parentKey)
+ private static bool TryGetParentKeyFromStructure(ConcurrentDictionary structure, Guid childKey, out Guid? parentKey)
{
if (structure.TryGetValue(childKey, out NavigationNode? childNode))
{
@@ -335,25 +335,32 @@ internal abstract class ContentNavigationServiceBase input,
+ HashSet input,
out IEnumerable rootKeys,
Guid? contentTypeKey = null)
{
- // Apply contentTypeKey filter
- IEnumerable filteredKeys = contentTypeKey.HasValue
- ? input.Where(key => _navigationStructure[key].ContentTypeKey == contentTypeKey.Value)
- : input;
+ var keysWithSortOrder = new List<(Guid Key, int SortOrder)>(input.Count);
+ foreach (Guid key in input)
+ {
+ NavigationNode navigationNode = _navigationStructure[key];
+
+ // Apply contentTypeKey filter
+ if (contentTypeKey.HasValue && navigationNode.ContentTypeKey != contentTypeKey.Value)
+ {
+ continue;
+ }
+
+ keysWithSortOrder.Add((key, navigationNode.SortOrder));
+ }
- // TODO can we make this more efficient?
// Sort by SortOrder
- rootKeys = filteredKeys
- .OrderBy(key => _navigationStructure[key].SortOrder)
- .ToList();
+ keysWithSortOrder.Sort((a, b) => a.SortOrder.CompareTo(b.SortOrder));
+ rootKeys = keysWithSortOrder.ConvertAll(keyWithSortOrder => keyWithSortOrder.Key);
return true;
}
- private bool TryGetChildrenKeysFromStructure(
+ private static bool TryGetChildrenKeysFromStructure(
ConcurrentDictionary structure,
Guid parentKey,
out IEnumerable childrenKeys,
@@ -367,12 +374,12 @@ internal abstract class ContentNavigationServiceBase structure,
Guid parentKey,
out IEnumerable descendantsKeys,
@@ -393,7 +400,7 @@ internal abstract class ContentNavigationServiceBase structure,
Guid childKey,
out IEnumerable ancestorsKeys,
@@ -421,7 +428,7 @@ internal abstract class ContentNavigationServiceBase structure,
Guid key,
out IEnumerable siblingsKeys,
@@ -463,14 +470,14 @@ internal abstract class ContentNavigationServiceBase structure,
NavigationNode node,
List descendants,
Guid? contentTypeKey = null)
{
// Get all children regardless of contentType
- var childrenKeys = GetOrderedChildren(node, structure).ToList();
+ IReadOnlyList childrenKeys = GetOrderedChildren(node, structure);
foreach (Guid childKey in childrenKeys)
{
// Apply contentTypeKey filter
@@ -487,7 +494,7 @@ internal abstract class ContentNavigationServiceBase structure, Guid key, out NavigationNode? nodeToRemove)
+ private static bool TryRemoveNodeFromParentInStructure(ConcurrentDictionary structure, Guid key, out NavigationNode? nodeToRemove)
{
if (structure.TryGetValue(key, out nodeToRemove) is false)
{
@@ -507,7 +514,7 @@ internal abstract class ContentNavigationServiceBase childrenKeys = GetOrderedChildren(node, _navigationStructure);
foreach (Guid childKey in childrenKeys)
{
@@ -530,7 +537,7 @@ internal abstract class ContentNavigationServiceBase childrenKeys = GetOrderedChildren(node, _recycleBinNavigationStructure);
foreach (Guid childKey in childrenKeys)
{
if (_recycleBinNavigationStructure.TryGetValue(childKey, out NavigationNode? childNode) is false)
@@ -551,7 +558,7 @@ internal abstract class ContentNavigationServiceBase childrenKeys = GetOrderedChildren(node, _recycleBinNavigationStructure);
foreach (Guid childKey in childrenKeys)
{
@@ -570,24 +577,35 @@ internal abstract class ContentNavigationServiceBase GetOrderedChildren(
+ private static IReadOnlyList GetOrderedChildren(
NavigationNode node,
ConcurrentDictionary structure,
Guid? contentTypeKey = null)
{
- IEnumerable children = node
- .Children
- .Where(structure.ContainsKey);
-
- // Apply contentTypeKey filter
- if (contentTypeKey.HasValue)
+ if (node.Children.Count < 1)
{
- children = children.Where(childKey => structure[childKey].ContentTypeKey == contentTypeKey.Value);
+ return [];
}
- return children
- .OrderBy(childKey => structure[childKey].SortOrder)
- .ToList();
+ var childrenWithSortOrder = new List<(Guid ChildNodeKey, int SortOrder)>(node.Children.Count);
+ foreach (Guid childNodeKey in node.Children)
+ {
+ if (!structure.TryGetValue(childNodeKey, out NavigationNode? childNode))
+ {
+ continue;
+ }
+
+ // Apply contentTypeKey filter
+ if (contentTypeKey.HasValue && childNode.ContentTypeKey != contentTypeKey.Value)
+ {
+ continue;
+ }
+
+ childrenWithSortOrder.Add((childNodeKey, childNode.SortOrder));
+ }
+
+ childrenWithSortOrder.Sort((a, b) => a.SortOrder.CompareTo(b.SortOrder));
+ return childrenWithSortOrder.ConvertAll(childWithSortOrder => childWithSortOrder.ChildNodeKey);
}
private bool TryGetContentTypeKey(string contentTypeAlias, out Guid? contentTypeKey)
@@ -613,7 +631,7 @@ internal abstract class ContentNavigationServiceBase nodesStructure, IList roots, IEnumerable entities)
+ private static void BuildNavigationDictionary(ConcurrentDictionary nodesStructure, HashSet roots, IEnumerable entities)
{
var entityList = entities.ToList();
var idToKeyMap = entityList.ToDictionary(x => x.Id, x => x.Key);
diff --git a/src/Umbraco.Core/Services/ProcessInstructionsResult.cs b/src/Umbraco.Core/Services/ProcessInstructionsResult.cs
index 39751dad61..e109d1ed10 100644
--- a/src/Umbraco.Core/Services/ProcessInstructionsResult.cs
+++ b/src/Umbraco.Core/Services/ProcessInstructionsResult.cs
@@ -14,11 +14,13 @@ public class ProcessInstructionsResult
public int LastId { get; private set; }
+ [Obsolete("Instruction pruning has been moved to a separate background job. Scheduled for removal in V18.")]
public bool InstructionsWerePruned { get; private set; }
public static ProcessInstructionsResult AsCompleted(int numberOfInstructionsProcessed, int lastId) =>
new() { NumberOfInstructionsProcessed = numberOfInstructionsProcessed, LastId = lastId };
+ [Obsolete("Instruction pruning has been moved to a separate background job. Scheduled for removal in V18.")]
public static ProcessInstructionsResult AsCompletedAndPruned(int numberOfInstructionsProcessed, int lastId) =>
new()
{
diff --git a/src/Umbraco.Core/Services/Querying/ContentQueryService.cs b/src/Umbraco.Core/Services/Querying/ContentQueryService.cs
index 241236f550..22c4eb40df 100644
--- a/src/Umbraco.Core/Services/Querying/ContentQueryService.cs
+++ b/src/Umbraco.Core/Services/Querying/ContentQueryService.cs
@@ -23,23 +23,23 @@ public class ContentQueryService : IContentQueryService
_coreScopeProvider = coreScopeProvider;
}
- public async Task> GetWithSchedulesAsync(Guid id)
+ public Task> GetWithSchedulesAsync(Guid id)
{
using ICoreScope scope = _coreScopeProvider.CreateCoreScope(autoComplete: true);
- IContent? content = await Task.FromResult(_contentService.GetById(id));
+ IContent? content = _contentService.GetById(id);
if (content == null)
{
- return Attempt.Fail(ContentQueryOperationStatus
- .ContentNotFound);
+ return Task.FromResult(Attempt.Fail(ContentQueryOperationStatus
+ .ContentNotFound));
}
ContentScheduleCollection schedules = _contentService.GetContentScheduleByContentId(id);
- return Attempt