PluginProbe
The Innovative Form Builder – IvyForms / 1.5.1
The Innovative Form Builder – IvyForms v1.5.1
1.5.1 1.5 1.4.1 1.4 trunk 0.1.2 0.2 0.2.1 0.3 0.3.1 0.4 0.5 0.6 0.6.1 0.6.1-backup 0.6.1.1 0.7 0.8 0.8.1 0.8.2 0.9 0.9.1 1.0 1.1 1.1.1 All 27 releases
ivyforms / backend / src / Services / Permissions / AccessRulesRoleCapabilitySynchronizer.php

AccessRulesRoleCapabilitySynchronizer.php in The Innovative Form Builder – IvyForms 1.5.1, at backend/src/Services/Permissions/AccessRulesRoleCapabilitySynchronizer.php

288 lines 8.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * @copyright © Melograno Venture Studio. All rights reserved.
5 */
6
7 namespace IvyForms\Services\Permissions;
8
9 use WP_User;
10
11 /**
12 * Maps validated access rules to WordPress capabilities (ivyforms_*).
13 *
14 * Only rules with global form scope (form_ids null) mirror ivyforms_* onto roles or users.
15 * Form-scoped grants must not become user_can() caps, or PermissionResolver::userCan() would
16 * short-circuit and allow actions outside the allowed forms. Scoped access stays option-based
17 * only. ivyforms_access_plugin is still granted when any delegation exists for that principal
18 * so the IvyForms parent admin menu can register.
19 *
20 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
21 */
22 final class AccessRulesRoleCapabilitySynchronizer
23 {
24 /**
25 * @param list<array<string,mixed>> $rules
26 * @param list<int> $previousUserIds User ids that had type "user" rules before the last save (for cleanup).
27 */
28 public static function syncFromRules(array $rules, array $previousUserIds = []): void
29 {
30 $roleSync = self::aggregateRoleSyncStateBySlug($rules);
31 self::stripIvyformsCapsFromNonAdministratorRoles();
32 self::applyCapsToRoles($roleSync);
33
34 $userSync = self::aggregateUserSyncStateByUserId($rules);
35 self::syncUserCaps($userSync, $previousUserIds);
36 }
37
38 /**
39 * @param list<array<string,mixed>> $rules
40 * @return list<int>
41 */
42 public static function extractUserIdsFromUserRules(array $rules): array
43 {
44 $ids = [];
45 foreach ($rules as $rule) {
46 if (!is_array($rule) || ($rule['type'] ?? '') !== 'user') {
47 continue;
48 }
49 $uid = (int) ($rule['user_id'] ?? 0);
50 if ($uid > 0) {
51 $ids[$uid] = true;
52 }
53 }
54
55 return array_map('intval', array_keys($ids));
56 }
57
58 /**
59 * @param mixed[] $rule
60 */
61 private static function ruleHasGlobalFormScope(array $rule): bool
62 {
63 return !array_key_exists('form_ids', $rule) || $rule['form_ids'] === null;
64 }
65
66 /**
67 * @param array<string,mixed> $rule
68 * @param 'role'|'user' $principalType
69 * @param array<string|int, array{global_caps: array<string, true>, needs_plugin_cap: bool}> $accumulated
70 */
71 private static function accumulateDelegationRule(array $rule, string $principalType, array &$accumulated): void
72 {
73 if (($rule['type'] ?? '') !== $principalType) {
74 return;
75 }
76 $principalKey = self::principalKeyForDelegationRule($rule, $principalType);
77 if ($principalKey === null) {
78 return;
79 }
80 $permissions = $rule['permissions'] ?? [];
81 if (!is_array($permissions)) {
82 return;
83 }
84 $sanitized = PermissionCatalog::sanitize($permissions);
85 if ($sanitized === []) {
86 return;
87 }
88 if (!isset($accumulated[$principalKey])) {
89 $accumulated[$principalKey] = [
90 'global_caps' => [],
91 'needs_plugin_cap' => false,
92 ];
93 }
94 $accumulated[$principalKey]['needs_plugin_cap'] = true;
95 if (!self::ruleHasGlobalFormScope($rule)) {
96 return;
97 }
98 foreach ($sanitized as $logical) {
99 $accumulated[$principalKey]['global_caps'][PermissionCatalog::wpCapability($logical)] = true;
100 }
101 }
102
103 /**
104 * @param array<string,mixed> $rule
105 * @param 'role'|'user' $principalType
106 *
107 * @return string|int|null
108 */
109 private static function principalKeyForDelegationRule(array $rule, string $principalType)
110 {
111 if ($principalType === 'role') {
112 $slug = strtolower(trim((string) ($rule['role_slug'] ?? '')));
113 if ($slug === '' || $slug === 'administrator') {
114 return null;
115 }
116
117 return $slug;
118 }
119 $userId = (int) ($rule['user_id'] ?? 0);
120
121 return $userId > 0 ? $userId : null;
122 }
123
124 /**
125 * @param list<array<string,mixed>> $rules
126 * @return array<string, array{global_caps: array<string, true>, needs_plugin_cap: bool}>
127 */
128 private static function aggregateRoleSyncStateBySlug(array $rules): array
129 {
130 $byRole = [];
131 foreach ($rules as $rule) {
132 if (!is_array($rule)) {
133 continue;
134 }
135 self::accumulateDelegationRule($rule, 'role', $byRole);
136 }
137
138 return $byRole;
139 }
140
141 /**
142 * @param list<array<string,mixed>> $rules
143 * @return array<int, array{global_caps: array<string, true>, needs_plugin_cap: bool}>
144 */
145 private static function aggregateUserSyncStateByUserId(array $rules): array
146 {
147 $byUser = [];
148 foreach ($rules as $rule) {
149 if (!is_array($rule)) {
150 continue;
151 }
152 self::accumulateDelegationRule($rule, 'user', $byUser);
153 }
154
155 $normalized = [];
156 foreach ($byUser as $uid => $state) {
157 $normalized[(int) $uid] = $state;
158 }
159
160 return $normalized;
161 }
162
163 /**
164 * @param array<int, array{global_caps: array<string, true>, needs_plugin_cap: bool}> $userSyncById
165 * @param list<int> $previousUserIds
166 */
167 private static function syncUserCaps(array $userSyncById, array $previousUserIds): void
168 {
169 $newUserIds = array_map('intval', array_keys($userSyncById));
170 $previousUserIds = array_map('intval', $previousUserIds);
171 $affected = array_values(array_unique(array_merge($previousUserIds, $newUserIds)));
172
173 foreach ($affected as $userId) {
174 $userId = (int) $userId;
175 if ($userId <= 0) {
176 continue;
177 }
178 clean_user_cache($userId);
179 $user = new WP_User($userId);
180 if ((int) $user->ID <= 0) {
181 continue;
182 }
183 if (self::userShouldSkipUserCapSynchronization($user)) {
184 continue;
185 }
186 if (!in_array($userId, $newUserIds, true)) {
187 self::stripIvyformsCapsFromUser($user);
188 clean_user_cache($userId);
189 continue;
190 }
191 $state = $userSyncById[$userId];
192 self::applyCapsToUser($user, $state['global_caps'], $state['needs_plugin_cap']);
193 clean_user_cache($userId);
194 }
195 }
196
197 private static function userShouldSkipUserCapSynchronization(WP_User $user): bool
198 {
199 return IvyFormsAccess::userHasImplicitElevatedAccess($user)
200 || user_can($user, IvyFormsAccess::CAP_MANAGE_ALL);
201 }
202
203 private static function stripIvyformsCapsFromUser(WP_User $user): void
204 {
205 foreach (self::capsToStripFromRoles() as $cap) {
206 $user->remove_cap($cap);
207 }
208 }
209
210 /**
211 * @param array<string, true> $globalCaps ivyforms_* from rules with form_ids null only
212 */
213 private static function applyCapsToUser(WP_User $user, array $globalCaps, bool $needsPluginCap): void
214 {
215 foreach (PermissionCatalog::keys() as $key) {
216 $cap = PermissionCatalog::wpCapability($key);
217 if (isset($globalCaps[$cap])) {
218 $user->add_cap($cap);
219 continue;
220 }
221 $user->remove_cap($cap);
222 }
223 if ($needsPluginCap) {
224 $user->add_cap(IvyFormsAccess::CAP_ACCESS_PLUGIN);
225
226 return;
227 }
228 $user->remove_cap(IvyFormsAccess::CAP_ACCESS_PLUGIN);
229 }
230
231 private static function stripIvyformsCapsFromNonAdministratorRoles(): void
232 {
233 $wpRoles = wp_roles();
234 if (!$wpRoles instanceof \WP_Roles) {
235 return;
236 }
237
238 $stripCaps = self::capsToStripFromRoles();
239
240 foreach (array_keys($wpRoles->roles) as $slug) {
241 if ($slug === 'administrator') {
242 continue;
243 }
244 $role = get_role($slug);
245 if ($role === null) {
246 continue;
247 }
248 foreach ($stripCaps as $cap) {
249 $role->remove_cap($cap);
250 }
251 }
252 }
253
254 /**
255 * @return list<string>
256 */
257 private static function capsToStripFromRoles(): array
258 {
259 $caps = [];
260 foreach (PermissionCatalog::keys() as $key) {
261 $caps[] = PermissionCatalog::wpCapability($key);
262 }
263 $caps[] = IvyFormsAccess::CAP_ACCESS_PLUGIN;
264
265 return $caps;
266 }
267
268 /**
269 * @param array<string, array{global_caps: array<string, true>, needs_plugin_cap: bool}> $roleSyncBySlug
270 */
271 private static function applyCapsToRoles(array $roleSyncBySlug): void
272 {
273 foreach ($roleSyncBySlug as $slug => $state) {
274 $role = get_role($slug);
275 if ($role === null) {
276 continue;
277 }
278 $globalCaps = $state['global_caps'];
279 foreach (array_keys($globalCaps) as $cap) {
280 $role->add_cap($cap);
281 }
282 if ($state['needs_plugin_cap']) {
283 $role->add_cap(IvyFormsAccess::CAP_ACCESS_PLUGIN);
284 }
285 }
286 }
287 }
288