-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Expand file tree
/
Copy pathUserStartNodeEntitiesService.cs
More file actions
216 lines (187 loc) · 9.5 KB
/
UserStartNodeEntitiesService.cs
File metadata and controls
216 lines (187 loc) · 9.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Api.Management.Models.Entities;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Persistence.Querying;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Services.Entities;
public class UserStartNodeEntitiesService : IUserStartNodeEntitiesService
{
private readonly IEntityService _entityService;
private readonly ICoreScopeProvider _scopeProvider;
private readonly IIdKeyMap _idKeyMap;
public UserStartNodeEntitiesService(IEntityService entityService, ICoreScopeProvider scopeProvider, IIdKeyMap idKeyMap)
{
_entityService = entityService;
_scopeProvider = scopeProvider;
_idKeyMap = idKeyMap;
}
/// <inheritdoc />
public IEnumerable<UserAccessEntity> RootUserAccessEntities(UmbracoObjectTypes umbracoObjectType, int[] userStartNodeIds)
{
// Root entities for users without root access should include:
// - the start nodes that are actual root entities (level == 1)
// - the root level ancestors to the rest of the start nodes (required for browsing to the actual start nodes - will be marked as "no access")
IEntitySlim[] userStartEntities = userStartNodeIds.Any()
? _entityService.GetAll(umbracoObjectType, userStartNodeIds).ToArray()
: Array.Empty<IEntitySlim>();
// Find the start nodes that are at root level (level == 1).
IEntitySlim[] allowedTopmostEntities = userStartEntities.Where(entity => entity.Level == 1).ToArray();
// Find the root level ancestors of the rest of the start nodes, and add those as well.
var nonAllowedTopmostEntityIds = userStartEntities.Except(allowedTopmostEntities)
.Select(entity => int.TryParse(entity.Path.Split(Constants.CharArrays.Comma).Skip(1).FirstOrDefault(), out var id) ? id : 0)
.Where(id => id > 0)
.ToArray();
IEntitySlim[] nonAllowedTopmostEntities = nonAllowedTopmostEntityIds.Any()
? _entityService.GetAll(umbracoObjectType, nonAllowedTopmostEntityIds).ToArray()
: Array.Empty<IEntitySlim>();
return allowedTopmostEntities
.Select(entity => new UserAccessEntity(entity, true))
.Union(
nonAllowedTopmostEntities
.Select(entity => new UserAccessEntity(entity, false)))
.ToArray();
}
/// <inheritdoc/>
public IEnumerable<UserAccessEntity> ChildUserAccessEntities(
UmbracoObjectTypes umbracoObjectType,
string[] userStartNodePaths,
Guid parentKey,
int skip,
int take,
Ordering ordering,
out long totalItems)
{
Attempt<int> parentIdAttempt = _idKeyMap.GetIdForKey(parentKey, umbracoObjectType);
if (parentIdAttempt.Success is false)
{
totalItems = 0;
return [];
}
var parentId = parentIdAttempt.Result;
IEntitySlim? parent = _entityService.Get(parentId);
if (parent is null)
{
totalItems = 0;
return [];
}
IEntitySlim[] children;
if (userStartNodePaths.Any(path => $"{parent.Path},".StartsWith($"{path},")))
{
// The requested parent is one of the user start nodes (or a descendant of one), all children are by definition allowed.
children = _entityService.GetPagedChildren(parentKey, umbracoObjectType, skip, take, out totalItems, ordering: ordering).ToArray();
return ChildUserAccessEntities(children, userStartNodePaths);
}
// Need to use a List here because the expression tree cannot convert an array when used in Contains.
// See ExpressionTests.Sql_In().
List<int> allowedChildIds = GetAllowedIds(userStartNodePaths, parentId);
totalItems = allowedChildIds.Count;
if (allowedChildIds.Count == 0)
{
// The requested parent is outside the scope of any user start nodes.
return [];
}
// Even though we know the IDs of the allowed child entities to fetch, we still use a Query to yield correctly sorted children.
IQuery<IUmbracoEntity> query = _scopeProvider.CreateQuery<IUmbracoEntity>().Where(x => allowedChildIds.Contains(x.Id));
children = _entityService.GetPagedChildren(parentKey, umbracoObjectType, skip, take, out totalItems, query, ordering).ToArray();
return ChildUserAccessEntities(children, userStartNodePaths);
}
private static List<int> GetAllowedIds(string[] userStartNodePaths, int parentId)
{
// If one or more of the user start nodes are descendants of the requested parent, find the "next child IDs" in those user start node paths
// that are the final entries in the path.
// E.g. given the user start node path "-1,2,3,4,5", if the requested parent ID is 3, the "next child ID" is 4.
var userStartNodePathIds = userStartNodePaths.Select(path => path.Split(Constants.CharArrays.Comma).Select(int.Parse).ToArray()).ToArray();
return userStartNodePathIds
.Where(ids => ids.Contains(parentId))
.Select(ids => ids[ids.IndexOf(parentId) + 1]) // Given the previous checks, the parent ID can never be the last in the user start node path, so this is safe
.Distinct()
.ToList();
}
/// <inheritdoc />
public IEnumerable<UserAccessEntity> ChildUserAccessEntities(IEnumerable<IEntitySlim> candidateChildren, string[] userStartNodePaths)
// Child or sibling entities for users without root access should include:
// - children that are descendant-or-self of a user start node
// - children that are ancestors of a user start node (required for browsing to the actual start nodes - will be marked as "no access")
// All other candidate children should be discarded.
=> candidateChildren.Select(child =>
{
// is descendant-or-self of a start node?
if (IsDescendantOrSelf(child, userStartNodePaths))
{
return new UserAccessEntity(child, true);
}
// is ancestor of a start node?
if (userStartNodePaths.Any(path => path.StartsWith(child.Path)))
{
return new UserAccessEntity(child, false);
}
return null;
}).WhereNotNull().ToArray();
/// <inheritdoc />
public IEnumerable<UserAccessEntity> SiblingUserAccessEntities(
UmbracoObjectTypes umbracoObjectType,
string[] userStartNodePaths,
Guid targetKey,
int before,
int after,
Ordering ordering,
out long totalBefore,
out long totalAfter
)
{
Attempt<int> targetIdAttempt = _idKeyMap.GetIdForKey(targetKey, umbracoObjectType);
if (targetIdAttempt.Success is false)
{
totalBefore = 0;
totalAfter = 0;
return [];
}
var targetId = targetIdAttempt.Result;
IEntitySlim? target = _entityService.Get(targetId);
if (target is null)
{
totalBefore = 0;
totalAfter = 0;
return [];
}
IEntitySlim[] siblings;
IEntitySlim? targetParent = _entityService.Get(target.ParentId);
if (targetParent is null) // Even if the parent is the root, we still expect to get a value here.
{
totalBefore = 0;
totalAfter = 0;
return [];
}
if (userStartNodePaths.Any(path => $"{targetParent?.Path},".StartsWith($"{path},")))
{
// The requested parent of the target is one of the user start nodes (or a descendant of one), all siblings are by definition allowed.
siblings = _entityService.GetSiblings(targetKey, [umbracoObjectType], before, after, out totalBefore, out totalAfter, ordering: ordering).ToArray();
return ChildUserAccessEntities(siblings, userStartNodePaths);
}
List<int> allowedSiblingIds = GetAllowedIds(userStartNodePaths, targetParent.Id);
if (allowedSiblingIds.Count == 0)
{
// The requested target is outside the scope of any user start nodes.
totalBefore = 0;
totalAfter = 0;
return [];
}
// Even though we know the IDs of the allowed sibling entities to fetch, we still use a Query to yield correctly sorted children.
IQuery<IUmbracoEntity> query = _scopeProvider.CreateQuery<IUmbracoEntity>().Where(x => allowedSiblingIds.Contains(x.Id));
siblings = _entityService.GetSiblings(targetKey, [umbracoObjectType], before, after, out totalBefore, out totalAfter, query, ordering).ToArray();
return ChildUserAccessEntities(siblings, userStartNodePaths);
}
/// <inheritdoc />
public IEnumerable<UserAccessEntity> UserAccessEntities(IEnumerable<IEntitySlim> entities, string[] userStartNodePaths)
// Entities for users without root access should include:
// - entities that are descendant-or-self of a user start node as regular entities
// - all other entities as "no access" entities
=> entities.Select(entity => new UserAccessEntity(entity, IsDescendantOrSelf(entity, userStartNodePaths))).ToArray();
private static bool IsDescendantOrSelf(IEntitySlim child, string[] userStartNodePaths)
=> userStartNodePaths.Any(path => child.Path.StartsWith(path));
}