Skip to content

Commit 157b7d1

Browse files
committed
Merge branch 'v15/dev' into contrib
2 parents 9db62a2 + f3658bf commit 157b7d1

File tree

56 files changed

+2035
-493
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+2035
-493
lines changed

src/Umbraco.Cms.Api.Management/Controllers/Tree/UserStartNodeTreeControllerBase.cs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,21 @@ protected override IEntitySlim[] GetPagedRootEntities(int skip, int take, out lo
4242

4343
protected override IEntitySlim[] GetPagedChildEntities(Guid parentKey, int skip, int take, out long totalItems)
4444
{
45-
IEntitySlim[] children = base.GetPagedChildEntities(parentKey, skip, take, out totalItems);
46-
return UserHasRootAccess() || IgnoreUserStartNodes()
47-
? children
48-
// Keeping the correct totalItems amount from GetPagedChildEntities
49-
: CalculateAccessMap(() => _userStartNodeEntitiesService.ChildUserAccessEntities(children, UserStartNodePaths), out _);
45+
if (UserHasRootAccess() || IgnoreUserStartNodes())
46+
{
47+
return base.GetPagedChildEntities(parentKey, skip, take, out totalItems);
48+
}
49+
50+
IEnumerable<UserAccessEntity> userAccessEntities = _userStartNodeEntitiesService.ChildUserAccessEntities(
51+
ItemObjectType,
52+
UserStartNodePaths,
53+
parentKey,
54+
skip,
55+
take,
56+
ItemOrdering,
57+
out totalItems);
58+
59+
return CalculateAccessMap(() => userAccessEntities, out _);
5060
}
5161

5262
protected override TItem[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities)

src/Umbraco.Cms.Api.Management/Controllers/User/ConfigurationUserController.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
1-
using Asp.Versioning;
2-
using Microsoft.AspNetCore.Authorization;
1+
using Asp.Versioning;
32
using Microsoft.AspNetCore.Http;
43
using Microsoft.AspNetCore.Mvc;
54
using Umbraco.Cms.Api.Management.Factories;
65
using Umbraco.Cms.Api.Management.ViewModels.User;
7-
using Umbraco.Cms.Web.Common.Authorization;
86

97
namespace Umbraco.Cms.Api.Management.Controllers.User;
108

119
[ApiVersion("1.0")]
12-
[Authorize(Policy = AuthorizationPolicies.RequireAdminAccess)]
1310
public class ConfigurationUserController : UserControllerBase
1411
{
1512
private readonly IUserPresentationFactory _userPresentationFactory;

src/Umbraco.Cms.Api.Management/Services/Entities/IUserStartNodeEntitiesService.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,41 @@ public interface IUserStartNodeEntitiesService
1616
/// <remarks>
1717
/// The returned entities may include entities that outside of the user start node scope, but are needed to
1818
/// for browsing to the actual user start nodes. These entities will be marked as "no access" entities.
19+
///
20+
/// This method does not support pagination, because it must load all entities explicitly in order to calculate
21+
/// the correct result, given that user start nodes can be descendants of root nodes. Consumers need to apply
22+
/// pagination to the result if applicable.
1923
/// </remarks>
2024
IEnumerable<UserAccessEntity> RootUserAccessEntities(UmbracoObjectTypes umbracoObjectType, int[] userStartNodeIds);
2125

26+
/// <summary>
27+
/// Calculates the applicable child entities for a given object type for users without root access.
28+
/// </summary>
29+
/// <param name="umbracoObjectType">The object type.</param>
30+
/// <param name="userStartNodePaths">The calculated start node paths for the user.</param>
31+
/// <param name="parentKey">The key of the parent.</param>
32+
/// <param name="skip">The number of applicable children to skip.</param>
33+
/// <param name="take">The number of applicable children to take.</param>
34+
/// <param name="ordering">The ordering to apply when fetching and paginating the children.</param>
35+
/// <param name="totalItems">The total number of applicable children available.</param>
36+
/// <returns>A list of child entities applicable for the user.</returns>
37+
/// <remarks>
38+
/// The returned entities may include entities that outside of the user start node scope, but are needed to
39+
/// for browsing to the actual user start nodes. These entities will be marked as "no access" entities.
40+
/// </remarks>
41+
IEnumerable<UserAccessEntity> ChildUserAccessEntities(
42+
UmbracoObjectTypes umbracoObjectType,
43+
string[] userStartNodePaths,
44+
Guid parentKey,
45+
int skip,
46+
int take,
47+
Ordering ordering,
48+
out long totalItems)
49+
{
50+
totalItems = 0;
51+
return [];
52+
}
53+
2254
/// <summary>
2355
/// Calculates the applicable child entities from a list of candidate child entities for users without root access.
2456
/// </summary>

src/Umbraco.Cms.Api.Management/Services/Entities/UserStartNodeEntitiesService.cs

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,37 @@
1-
using Umbraco.Cms.Core;
1+
using Microsoft.Extensions.DependencyInjection;
2+
using Umbraco.Cms.Core;
23
using Umbraco.Cms.Core.Models;
34
using Umbraco.Cms.Core.Models.Entities;
45
using Umbraco.Cms.Core.Services;
56
using Umbraco.Cms.Api.Management.Models.Entities;
7+
using Umbraco.Cms.Core.DependencyInjection;
8+
using Umbraco.Cms.Core.Persistence.Querying;
9+
using Umbraco.Cms.Core.Scoping;
610
using Umbraco.Extensions;
711

812
namespace Umbraco.Cms.Api.Management.Services.Entities;
913

1014
public class UserStartNodeEntitiesService : IUserStartNodeEntitiesService
1115
{
1216
private readonly IEntityService _entityService;
17+
private readonly ICoreScopeProvider _scopeProvider;
18+
private readonly IIdKeyMap _idKeyMap;
1319

14-
public UserStartNodeEntitiesService(IEntityService entityService) => _entityService = entityService;
20+
[Obsolete("Please use the non-obsolete constructor. Scheduled for removal in V17.")]
21+
public UserStartNodeEntitiesService(IEntityService entityService)
22+
: this(
23+
entityService,
24+
StaticServiceProvider.Instance.GetRequiredService<ICoreScopeProvider>(),
25+
StaticServiceProvider.Instance.GetRequiredService<IIdKeyMap>())
26+
{
27+
}
28+
29+
public UserStartNodeEntitiesService(IEntityService entityService, ICoreScopeProvider scopeProvider, IIdKeyMap idKeyMap)
30+
{
31+
_entityService = entityService;
32+
_scopeProvider = scopeProvider;
33+
_idKeyMap = idKeyMap;
34+
}
1535

1636
/// <inheritdoc />
1737
public IEnumerable<UserAccessEntity> RootUserAccessEntities(UmbracoObjectTypes umbracoObjectType, int[] userStartNodeIds)
@@ -43,6 +63,54 @@ public IEnumerable<UserAccessEntity> RootUserAccessEntities(UmbracoObjectTypes u
4363
.ToArray();
4464
}
4565

66+
public IEnumerable<UserAccessEntity> ChildUserAccessEntities(UmbracoObjectTypes umbracoObjectType, string[] userStartNodePaths, Guid parentKey, int skip, int take, Ordering ordering, out long totalItems)
67+
{
68+
Attempt<int> parentIdAttempt = _idKeyMap.GetIdForKey(parentKey, umbracoObjectType);
69+
if (parentIdAttempt.Success is false)
70+
{
71+
totalItems = 0;
72+
return [];
73+
}
74+
75+
var parentId = parentIdAttempt.Result;
76+
IEntitySlim? parent = _entityService.Get(parentId);
77+
if (parent is null)
78+
{
79+
totalItems = 0;
80+
return [];
81+
}
82+
83+
IEntitySlim[] children;
84+
if (userStartNodePaths.Any(path => $"{parent.Path},".StartsWith($"{path},")))
85+
{
86+
// the requested parent is one of the user start nodes (or a descendant of one), all children are by definition allowed
87+
children = _entityService.GetPagedChildren(parentKey, umbracoObjectType, skip, take, out totalItems, ordering: ordering).ToArray();
88+
return ChildUserAccessEntities(children, userStartNodePaths);
89+
}
90+
91+
// if one or more of the user start nodes are descendants of the requested parent, find the "next child IDs" in those user start node paths
92+
// - e.g. given the user start node path "-1,2,3,4,5", if the requested parent ID is 3, the "next child ID" is 4.
93+
var userStartNodePathIds = userStartNodePaths.Select(path => path.Split(Constants.CharArrays.Comma).Select(int.Parse).ToArray()).ToArray();
94+
var allowedChildIds = userStartNodePathIds
95+
.Where(ids => ids.Contains(parentId))
96+
// given the previous checks, the parent ID can never be the last in the user start node path, so this is safe
97+
.Select(ids => ids[ids.IndexOf(parentId) + 1])
98+
.Distinct()
99+
.ToArray();
100+
101+
totalItems = allowedChildIds.Length;
102+
if (allowedChildIds.Length == 0)
103+
{
104+
// the requested parent is outside the scope of any user start nodes
105+
return [];
106+
}
107+
108+
// even though we know the IDs of the allowed child entities to fetch, we still use a Query to yield correctly sorted children
109+
IQuery<IUmbracoEntity> query = _scopeProvider.CreateQuery<IUmbracoEntity>().Where(x => allowedChildIds.Contains(x.Id));
110+
children = _entityService.GetPagedChildren(parentKey, umbracoObjectType, skip, take, out totalItems, query, ordering).ToArray();
111+
return ChildUserAccessEntities(children, userStartNodePaths);
112+
}
113+
46114
/// <inheritdoc />
47115
public IEnumerable<UserAccessEntity> ChildUserAccessEntities(IEnumerable<IEntitySlim> candidateChildren, string[] userStartNodePaths)
48116
// child entities for users without root access should include:
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace Umbraco.Cms.Core.Models;
2+
3+
/// <summary>
4+
/// Definition of relation directions used as a filter when requesting if a given item has relations.
5+
/// </summary>
6+
[Flags]
7+
public enum RelationDirectionFilter
8+
{
9+
Parent = 1,
10+
Child = 2,
11+
Any = Parent | Child
12+
}

src/Umbraco.Core/Services/ContentEditingServiceBase.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using Microsoft.Extensions.Logging;
1+
using Microsoft.Extensions.Logging;
22
using Microsoft.Extensions.Options;
33
using Umbraco.Cms.Core.Configuration.Models;
44
using Umbraco.Cms.Core.Models;
@@ -198,7 +198,7 @@ private async Task<Attempt<ContentValidationResult, ContentEditingOperationStatu
198198
return await Task.FromResult(Attempt.FailWithStatus<TContent?, ContentEditingOperationStatus>(status, content));
199199
}
200200

201-
if (disabledWhenReferenced && _relationService.IsRelated(content.Id))
201+
if (disabledWhenReferenced && _relationService.IsRelated(content.Id, RelationDirectionFilter.Child))
202202
{
203203
return Attempt.FailWithStatus<TContent?, ContentEditingOperationStatus>(referenceFailStatus, content);
204204
}

src/Umbraco.Core/Services/ContentPublishingService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ public async Task<Attempt<ContentPublishingOperationStatus>> UnpublishAsync(Guid
311311
return Attempt.Fail(ContentPublishingOperationStatus.ContentNotFound);
312312
}
313313

314-
if (_contentSettings.DisableUnpublishWhenReferenced && _relationService.IsRelated(content.Id))
314+
if (_contentSettings.DisableUnpublishWhenReferenced && _relationService.IsRelated(content.Id, RelationDirectionFilter.Child))
315315
{
316316
scope.Complete();
317317
return Attempt<ContentPublishingOperationStatus>.Fail(ContentPublishingOperationStatus.CannotUnpublishWhenReferenced);

src/Umbraco.Core/Services/ContentService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2767,7 +2767,7 @@ public OperationResult EmptyRecycleBin(int userId = Constants.Security.SuperUser
27672767
{
27682768
foreach (IContent content in contents)
27692769
{
2770-
if (_contentSettings.DisableDeleteWhenReferenced && _relationService.IsRelated(content.Id))
2770+
if (_contentSettings.DisableDeleteWhenReferenced && _relationService.IsRelated(content.Id, RelationDirectionFilter.Child))
27712771
{
27722772
continue;
27732773
}

src/Umbraco.Core/Services/IRelationService.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,8 +297,20 @@ public interface IRelationService : IService
297297
/// </summary>
298298
/// <param name="id">Id of an object to check relations for</param>
299299
/// <returns>Returns <c>True</c> if any relations exists with the given Id, otherwise <c>False</c></returns>
300+
[Obsolete("Please use the overload taking a RelationDirectionFilter parameter. Scheduled for removal in Umbraco 17.")]
300301
bool IsRelated(int id);
301302

303+
/// <summary>
304+
/// Checks whether any relations exists for the passed in Id and direction.
305+
/// </summary>
306+
/// <param name="id">Id of an object to check relations for</param>
307+
/// <param name="directionFilter">Indicates whether to check for relations as parent, child or in either direction.</param>
308+
/// <returns>Returns <c>True</c> if any relations exists with the given Id, otherwise <c>False</c></returns>
309+
bool IsRelated(int id, RelationDirectionFilter directionFilter)
310+
#pragma warning disable CS0618 // Type or member is obsolete
311+
=> IsRelated(id);
312+
#pragma warning restore CS0618 // Type or member is obsolete
313+
302314
/// <summary>
303315
/// Checks whether two items are related
304316
/// </summary>

0 commit comments

Comments
 (0)