PluginProbe
The Innovative Form Builder – IvyForms / 1.5
The Innovative Form Builder – IvyForms v1.5
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 / Common / Helpers / McpHelpers / McpPermissionsHelper.php

McpPermissionsHelper.php in The Innovative Form Builder – IvyForms 1.5, at backend/src/Common/Helpers/McpHelpers/McpPermissionsHelper.php

332 lines 9.8 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 * @licence See COPYING.md for license details.
6 */
7
8 namespace IvyForms\Common\Helpers\McpHelpers;
9
10 use IvyForms\Services\Permissions\PermissionCatalog;
11 use WP_Error;
12
13 /**
14 * Shared helpers for the IvyForms MCP permission-management abilities.
15 *
16 * Reads and writes go through the internal IvyForms REST API
17 * ({@see McpAbilitiesHelper::restRequest()}), reusing the same controllers as the
18 * Permissions settings tab:
19 * - GET /permissions/access-rules → {@see \IvyForms\Controllers\Permissions\GetAccessRulesController}
20 * - GET /permissions/meta → {@see \IvyForms\Controllers\Permissions\GetPermissionsMetaController}
21 * - POST /permissions/access-rules → {@see \IvyForms\Controllers\Permissions\UpdateAccessRulesController}
22 *
23 * Only the grant/revoke MERGE (read current rules → union/diff → resubmit the full set)
24 * lives here, mirroring how the admin UI edits the rule set.
25 */
26 class McpPermissionsHelper
27 {
28 private const ACCESS_RULES_ROUTE = '/permissions/access-rules';
29 private const META_ROUTE = '/permissions/meta';
30
31 /**
32 * Current stored access rules (hydrated rows, as returned to the admin UI).
33 *
34 * Returns null when the read fails, so callers can distinguish "no rules"
35 * from "could not read rules" and avoid persisting a partial set.
36 *
37 * @return list<array<string, mixed>>|null
38 */
39 public static function currentRules(): ?array
40 {
41 $response = McpAbilitiesHelper::restRequest('GET', self::ACCESS_RULES_ROUTE);
42
43 return self::rulesFromResponse($response);
44 }
45
46 /**
47 * Available permission keys (with labels), assignable roles, and forms.
48 *
49 * @return array<string, mixed>
50 */
51 public static function permissionMetadata(): array
52 {
53 $response = McpAbilitiesHelper::restRequest('GET', self::META_ROUTE);
54 if (!is_array($response)) {
55 return [];
56 }
57 $payload = $response['data'] ?? $response;
58
59 return is_array($payload) ? $payload : [];
60 }
61
62 /**
63 * Grant permission keys to a role (creating or extending its rule).
64 *
65 * @param list<string> $permissions
66 * @param list<int>|null $formIds
67 * @return array<string, mixed>|\WP_Error
68 */
69 public static function grantRole(string $roleSlug, array $permissions, ?array $formIds = null)
70 {
71 $roleSlug = strtolower(trim($roleSlug));
72
73 return self::grant(
74 'role',
75 $roleSlug,
76 $permissions,
77 $formIds,
78 sprintf(
79 /* translators: %s: WordPress role slug, e.g. editor */
80 __('Granted permissions to role "%s".', 'ivyforms'),
81 $roleSlug
82 )
83 );
84 }
85
86 /**
87 * Revoke permission keys from a role.
88 *
89 * @param list<string> $permissions
90 * @param list<int>|null $formIds
91 * @return array<string, mixed>|\WP_Error
92 */
93 public static function revokeRole(string $roleSlug, array $permissions, ?array $formIds = null)
94 {
95 $roleSlug = strtolower(trim($roleSlug));
96
97 return self::revoke(
98 'role',
99 $roleSlug,
100 $permissions,
101 $formIds,
102 sprintf(
103 /* translators: %s: WordPress role slug, e.g. editor */
104 __('Revoked permissions from role "%s".', 'ivyforms'),
105 $roleSlug
106 )
107 );
108 }
109
110 /**
111 * Grant permission keys to a user (creating or extending their rule).
112 *
113 * @param list<string> $permissions
114 * @param list<int>|null $formIds
115 * @return array<string, mixed>|\WP_Error
116 */
117 public static function grantUser(int $userId, array $permissions, ?array $formIds = null)
118 {
119 if ($userId <= 0) {
120 return self::failure(
121 'invalid_user_id',
122 __('A valid userId is required.', 'ivyforms')
123 );
124 }
125
126 return self::grant(
127 'user',
128 $userId,
129 $permissions,
130 $formIds,
131 sprintf(
132 /* translators: %d: WordPress user ID */
133 __('Granted permissions to user #%d.', 'ivyforms'),
134 $userId
135 )
136 );
137 }
138
139 /**
140 * Revoke permission keys from a user.
141 *
142 * @param list<string> $permissions
143 * @param list<int>|null $formIds
144 * @return array<string, mixed>|\WP_Error
145 */
146 public static function revokeUser(int $userId, array $permissions, ?array $formIds = null)
147 {
148 if ($userId <= 0) {
149 return self::failure(
150 'invalid_user_id',
151 __('A valid userId is required.', 'ivyforms')
152 );
153 }
154
155 return self::revoke(
156 'user',
157 $userId,
158 $permissions,
159 $formIds,
160 sprintf(
161 /* translators: %d: WordPress user ID */
162 __('Revoked permissions from user #%d.', 'ivyforms'),
163 $userId
164 )
165 );
166 }
167
168 /**
169 * @param 'role'|'user' $type
170 * @param string|int $principal
171 * @param list<string> $permissions
172 * @param list<int>|null $formIds
173 * @return array<string, mixed>|\WP_Error
174 */
175 private static function grant(string $type, $principal, array $permissions, ?array $formIds, string $message)
176 {
177 $clean = PermissionCatalog::sanitize($permissions);
178 if ($clean === []) {
179 return self::failure(
180 'no_valid_permission_keys',
181 __('No valid permission keys provided.', 'ivyforms')
182 );
183 }
184
185 $current = self::currentRules();
186 if ($current === null) {
187 return self::failure(
188 'access_rules_read_failed',
189 __('Could not read the current access rules; no changes were made.', 'ivyforms'),
190 500
191 );
192 }
193
194 $rules = McpAccessRulesMerger::upsertGrant($current, $type, $principal, $clean, $formIds);
195
196 return self::persist($rules, $message);
197 }
198
199 /**
200 * @param 'role'|'user' $type
201 * @param string|int $principal
202 * @param list<string> $permissions
203 * @param list<int>|null $formIds
204 * @return array<string, mixed>|\WP_Error
205 */
206 private static function revoke(string $type, $principal, array $permissions, ?array $formIds, string $message)
207 {
208 $clean = PermissionCatalog::sanitize($permissions);
209 if ($clean === []) {
210 return self::failure(
211 'no_valid_permission_keys',
212 __('No valid permission keys provided.', 'ivyforms')
213 );
214 }
215
216 $current = self::currentRules();
217 if ($current === null) {
218 return self::failure(
219 'access_rules_read_failed',
220 __('Could not read the current access rules; no changes were made.', 'ivyforms'),
221 500
222 );
223 }
224
225 $rules = McpAccessRulesMerger::applyRevoke($current, $type, $principal, $clean, $formIds);
226
227 return self::persist($rules, $message);
228 }
229
230 /**
231 * Resolve a user id from an id, login, or email.
232 */
233 public static function resolveUserId(int $userId, string $userLogin = '', string $userEmail = ''): int
234 {
235 if ($userId > 0) {
236 return $userId;
237 }
238 if ($userLogin !== '') {
239 $user = get_user_by('login', $userLogin);
240 if ($user) {
241 return (int) $user->ID;
242 }
243 }
244 if ($userEmail !== '') {
245 $user = get_user_by('email', $userEmail);
246 if ($user) {
247 return (int) $user->ID;
248 }
249 }
250
251 return 0;
252 }
253
254 /**
255 * Persist the full rule set through the access-rules REST controller (validate + save + sync).
256 *
257 * @param list<array<string, mixed>> $rules
258 * @return array<string, mixed>|\WP_Error
259 */
260 private static function persist(array $rules, string $successMessage)
261 {
262 $response = McpAbilitiesHelper::restRequest(
263 'POST',
264 self::ACCESS_RULES_ROUTE,
265 ['rules' => array_values($rules)]
266 );
267
268 $saved = self::rulesFromResponse($response);
269 if ($saved === null) {
270 return self::failure(
271 'access_rules_update_failed',
272 self::errorMessage($response),
273 self::errorStatus($response)
274 );
275 }
276
277 return [
278 'message' => $successMessage,
279 'rules' => $saved,
280 ];
281 }
282
283 /**
284 * Extract the rules array from a (success-wrapped) REST response, or null when absent.
285 *
286 * @param mixed $response
287 * @return list<array<string, mixed>>|null
288 */
289 private static function rulesFromResponse($response): ?array
290 {
291 if (!is_array($response)) {
292 return null;
293 }
294 $rules = $response['data']['rules'] ?? $response['rules'] ?? null;
295
296 return is_array($rules) ? array_values($rules) : null;
297 }
298
299 /**
300 * @param mixed $response
301 */
302 private static function errorMessage($response): string
303 {
304 $message = is_array($response)
305 ? ($response['message'] ?? $response['data']['message'] ?? '')
306 : '';
307
308 return is_string($message) && $message !== ''
309 ? $message
310 : __('Failed to update access rules.', 'ivyforms');
311 }
312
313 /**
314 * @param mixed $response
315 */
316 private static function errorStatus($response): int
317 {
318 if (!is_array($response)) {
319 return 500;
320 }
321
322 $status = (int) ($response['data']['status'] ?? 0);
323
324 return $status >= 400 && $status < 600 ? $status : 500;
325 }
326
327 private static function failure(string $code, string $message, int $status = 400): WP_Error
328 {
329 return new WP_Error($code, $message, ['status' => $status]);
330 }
331 }
332