PluginProbe
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder / 2.16.0
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder v2.16.0
2.16.0 2.15.0 2.14.0 2.13.0 2.13.1 2.12.0 2.11.1 2.11.0 2.10.0 2.9.0 2.7.4 2.7.5 2.7.6 2.7.7 2.8.0 2.8.1 2.9.1 trunk 1.0 1.0-beta1 1.0-beta2 1.0-beta3 1.0.1 1.0.2 1.0.3 All 83 releases
ablocks / includes / permissions.php

permissions.php in aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder 2.16.0, at includes/permissions.php

427 lines 13.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * The aBlocks capability layer.
4 *
5 * Permission slugs ARE WordPress capability strings, so every call site in the
6 * plugin is a plain current_user_can( 'ablocks_…' ). Nothing is ever written to
7 * the database with add_cap(); Permissions\Caps grants them for the duration of
8 * a request through the `user_has_cap` filter, so revoking is immediate and
9 * deactivating aBlocks leaves a user with exactly the caps they started with.
10 *
11 * This half — the vocabulary and the bridge — is all aBlocks itself needs. On
12 * its own it hands out one fixed arrangement: an administrator holds
13 * everything, a role that can edit posts gets the editing capabilities, and
14 * nobody else reaches an aBlocks screen. That is exactly what the plugin did
15 * before capabilities had names, so installing or updating changes nothing.
16 *
17 * Configuring it — per role, per user, with presets and a screen to do it on —
18 * is aBlocks Pro. Pro supplies the stored map through
19 * `ablocks/permissions/role_grants` and `ablocks/permissions/user_grants`; see
20 * docs/ROLE-PERMISSIONS.md.
21 *
22 * @package ABlocks
23 */
24
25 namespace ABlocks;
26
27 if ( ! defined( 'ABSPATH' ) ) {
28 exit;
29 }
30
31 use WP_User;
32
33 class Permissions {
34
35 /**
36 * Reaching any aBlocks admin screen. Derived — anyone holding at least one
37 * other capability holds this one too, so the top-level menu can be gated
38 * without enumerating every permission at the call site.
39 */
40 const ACCESS = 'ablocks_access';
41
42 /**
43 * Saving anything on the Settings screen. Derived from the three groups the
44 * settings blob is partitioned into — the endpoint accepts the request and
45 * SettingsGuard decides, key by key, which groups the user may actually
46 * change.
47 */
48 const SAVE_SETTINGS = 'ablocks_save_settings';
49
50 public static function init() {
51 Permissions\Caps::init();
52 }
53
54 /**
55 * Capabilities that unlock an aBlocks admin screen.
56 *
57 * Editing capabilities are absent on purpose: someone who may style blocks
58 * has no reason to see the aBlocks menu, and showing them a top-level menu
59 * whose every child is hidden is worse than showing nothing.
60 *
61 * @return string[]
62 */
63 public static function screen_capabilities() {
64 return [
65 'ablocks_manage_settings',
66 'ablocks_manage_global_styles',
67 'ablocks_manage_performance',
68 'ablocks_manage_forms',
69 'ablocks_view_submissions',
70 'ablocks_run_scanner',
71 'ablocks_manage_addons',
72 'ablocks_manage_theme_builder',
73 'ablocks_import_templates',
74 ];
75 }
76
77 /**
78 * Capabilities computed from other capabilities.
79 *
80 * Not in the catalogue and not configurable — they exist so a call site can
81 * ask one question instead of three.
82 *
83 * @return array Derived capability => the capabilities that imply it.
84 */
85 public static function derived_capabilities() {
86 return [
87 self::ACCESS => self::screen_capabilities(),
88 self::SAVE_SETTINGS => [
89 'ablocks_manage_settings',
90 'ablocks_manage_global_styles',
91 'ablocks_manage_performance',
92 ],
93 ];
94 }
95
96 /**
97 * Every capability this module hands out, grouped for the admin screen.
98 *
99 * @return array
100 */
101 public static function catalogue() {
102 return apply_filters('ablocks/permissions/catalogue', [
103 'editing' => [
104 'label' => __( 'Editing', 'ablocks' ),
105 'permissions' => [
106 'ablocks_use_editor' => [
107 'label' => __( 'Use aBlocks blocks', 'ablocks' ),
108 'description' => __( 'Insert and edit aBlocks blocks in the post editor.', 'ablocks' ),
109 ],
110 'ablocks_edit_style' => [
111 'label' => __( 'Change styling', 'ablocks' ),
112 'description' => __( 'Colors, typography, backgrounds and borders. Without this a user gets content controls only.', 'ablocks' ),
113 ],
114 'ablocks_edit_advanced' => [
115 'label' => __( 'Advanced settings', 'ablocks' ),
116 'description' => __( 'Spacing, position, responsive visibility and animation.', 'ablocks' ),
117 ],
118 'ablocks_edit_custom_css' => [
119 'label' => __( 'Custom CSS & attributes', 'ablocks' ),
120 'description' => __( 'Separate from advanced settings on purpose — this one injects code into the page.', 'ablocks' ),
121 ],
122 'ablocks_copy_paste_style' => [
123 'label' => __( 'Copy & paste styles', 'ablocks' ),
124 'description' => __( 'Copy styles between blocks, and paste styles from Figma.', 'ablocks' ),
125 ],
126 ],
127 ],
128 'design' => [
129 'label' => __( 'Site design', 'ablocks' ),
130 'permissions' => [
131 'ablocks_manage_global_styles' => [
132 'label' => __( 'Manage the design system', 'ablocks' ),
133 'description' => __( 'Global colors, typography presets and container defaults — these affect every page.', 'ablocks' ),
134 ],
135 'ablocks_manage_theme_builder' => [
136 'label' => __( 'Theme Builder', 'ablocks' ),
137 'description' => __( 'Build and assign headers, footers and other site-wide layouts.', 'ablocks' ),
138 ],
139 'ablocks_access_site_editor' => [
140 'label' => __( 'WordPress Site Editor', 'ablocks' ),
141 'description' => __( 'Templates, template parts, menus and widgets. Grants the WordPress edit_theme_options capability.', 'ablocks' ),
142 ],
143 'ablocks_import_templates' => [
144 'label' => __( 'Import templates & demos', 'ablocks' ),
145 'description' => __( 'Template kits and demo import create posts and media in bulk.', 'ablocks' ),
146 ],
147 ],
148 ],
149 'admin' => [
150 'label' => __( 'aBlocks screens', 'ablocks' ),
151 'permissions' => [
152 'ablocks_manage_settings' => [
153 'label' => __( 'Settings', 'ablocks' ),
154 'description' => __( 'Editor options, page setup, visibility and integrations.', 'ablocks' ),
155 ],
156 'ablocks_manage_performance' => [
157 'label' => __( 'Performance suite', 'ablocks' ),
158 'description' => __( 'Caching, asset generation and image tools. A wrong toggle here affects every visitor.', 'ablocks' ),
159 ],
160 'ablocks_manage_forms' => [
161 'label' => __( 'Form Builder', 'ablocks' ),
162 'description' => __( 'Build forms and edit their settings.', 'ablocks' ),
163 ],
164 'ablocks_view_submissions' => [
165 'label' => __( 'Form submissions', 'ablocks' ),
166 'description' => __( 'Read and export what visitors submitted. This is personal data — grant it deliberately.', 'ablocks' ),
167 ],
168 'ablocks_run_scanner' => [
169 'label' => __( 'Site Scanner', 'ablocks' ),
170 'description' => __( 'Run the site scan and read its report.', 'ablocks' ),
171 ],
172 'ablocks_manage_addons' => [
173 'label' => __( 'Add-ons', 'ablocks' ),
174 'description' => __( 'Turn add-ons on and off. Only ever restricts — this screen installs plugins, so it always requires the WordPress plugin-installation capability as well.', 'ablocks' ),
175 ],
176 ],
177 ],
178 ]);
179 }
180
181 /**
182 * Flat list of every capability slug.
183 *
184 * Deliberately does NOT walk catalogue(): that one calls __() on every
185 * label, and the capability bridge fires on `user_has_cap` which can run
186 * before the `init` action. Translating that early triggers WordPress 6.7's
187 * `_load_textdomain_just_in_time was called incorrectly` warning. Slugs are
188 * identifiers, not UI copy — they live here, labels live in catalogue().
189 *
190 * @return string[]
191 */
192 public static function all_slugs() {
193 static $slugs = null;
194 if ( null === $slugs ) {
195 $slugs = apply_filters( 'ablocks/permissions/slugs', [
196 // editing
197 'ablocks_use_editor',
198 'ablocks_edit_style',
199 'ablocks_edit_advanced',
200 'ablocks_edit_custom_css',
201 'ablocks_copy_paste_style',
202 // design
203 'ablocks_manage_global_styles',
204 'ablocks_manage_theme_builder',
205 'ablocks_access_site_editor',
206 'ablocks_import_templates',
207 // admin screens
208 'ablocks_manage_settings',
209 'ablocks_manage_performance',
210 'ablocks_manage_forms',
211 'ablocks_view_submissions',
212 'ablocks_run_scanner',
213 'ablocks_manage_addons',
214 ] );
215 }
216 return $slugs;
217 }
218
219 public static function is_valid_slug( $slug ) {
220 return in_array( $slug, self::all_slugs(), true );
221 }
222
223 /**
224 * Capabilities that can never be granted by this module, only taken away.
225 *
226 * The add-ons screen installs plugins (Ajax\Dashboard::install_plugin), and a
227 * permission system that can hand out plugin installation can hand out
228 * everything. So this permission gates the screen for people who already
229 * hold the WordPress capability, and does nothing for anyone else.
230 *
231 * @return array Slug => the WordPress capability the user must already hold.
232 */
233 public static function requires_native_cap() {
234 return [
235 'ablocks_manage_addons' => 'install_plugins',
236 ];
237 }
238
239 /**
240 * Everything the block editor itself is gated on.
241 *
242 * The set a role that can edit posts holds by default, and the building
243 * block Pro's presets start from.
244 *
245 * @return string[]
246 */
247 public static function editing_capabilities() {
248 return [
249 'ablocks_use_editor',
250 'ablocks_edit_style',
251 'ablocks_edit_advanced',
252 'ablocks_edit_custom_css',
253 'ablocks_copy_paste_style',
254 ];
255 }
256
257 /**
258 * Sort + filter a grants list so two equivalent lists compare equal.
259 *
260 * @param array $grants
261 *
262 * @return string[]
263 */
264 public static function normalize( array $grants ) {
265 $grants = array_values( array_unique( array_filter( $grants, [ __CLASS__, 'is_valid_slug' ] ) ) );
266 sort( $grants );
267 return $grants;
268 }
269
270 /**
271 * What a role gets when nothing has configured it.
272 *
273 * Installing or updating aBlocks must not change what anybody can do. Any
274 * role that can edit posts could already use every aBlocks block and
275 * control, and no role but administrator could reach an aBlocks screen. That
276 * is exactly what this returns.
277 *
278 * @param string $role_slug
279 *
280 * @return string[]
281 */
282 public static function default_grants_for_role( $role_slug ) {
283 $role = get_role( $role_slug );
284
285 $grants = ( $role && ! empty( $role->capabilities['edit_posts'] ) )
286 ? self::editing_capabilities()
287 : [];
288
289 return apply_filters( 'ablocks/permissions/default_role_grants', $grants, $role_slug );
290 }
291
292 /**
293 * The capabilities a role holds.
294 *
295 * The filter is where aBlocks Pro returns the map a site configured on the
296 * Roles & Permissions screen. It may return an empty array — "this role gets
297 * nothing" has to be expressible — so a filtered value replaces the default
298 * rather than adding to it.
299 *
300 * @param string $role_slug
301 *
302 * @return string[]
303 */
304 public static function get_role_grants( $role_slug ) {
305 $grants = apply_filters(
306 'ablocks/permissions/role_grants',
307 self::default_grants_for_role( $role_slug ),
308 $role_slug
309 );
310
311 return self::normalize( (array) $grants );
312 }
313
314 /**
315 * Every capability a user effectively holds.
316 *
317 * Deliberately reads roles and meta directly and never calls user_can().
318 * Permissions\Caps calls this from inside the `user_has_cap` filter, so a
319 * capability check in here would recurse.
320 *
321 * @param WP_User|int|null $user
322 *
323 * @return string[]
324 */
325 public static function for_user( $user = null ) {
326 $user = self::resolve_user( $user );
327
328 if ( ! $user || ! $user->exists() ) {
329 return [];
330 }
331
332 if ( self::is_real_admin( $user ) ) {
333 return self::all_slugs();
334 }
335
336 $grants = [];
337 foreach ( (array) $user->roles as $role_slug ) {
338 $grants = array_merge( $grants, self::get_role_grants( $role_slug ) );
339 }
340
341 // Where aBlocks Pro applies a per-user override. It replaces the union of
342 // the user's roles rather than adding to it, because an override has to be
343 // able to take something away as well as give it.
344 $grants = apply_filters( 'ablocks/permissions/user_grants', self::normalize( $grants ), $user );
345 $grants = self::normalize( (array) $grants );
346
347 // Permissions that can only ever restrict. Holding one without the
348 // underlying WordPress capability means nothing. Applied after the filter
349 // so an override cannot route around it either.
350 foreach ( self::requires_native_cap() as $slug => $native ) {
351 if ( in_array( $slug, $grants, true ) && empty( $user->allcaps[ $native ] ) ) {
352 $grants = array_values( array_diff( $grants, [ $slug ] ) );
353 }
354 }
355
356 return $grants;
357 }
358
359 /**
360 * Whether a user holds a capability.
361 *
362 * Everywhere except inside the `user_has_cap` filter itself, prefer plain
363 * current_user_can( 'ablocks_…' ) — Caps makes that work.
364 *
365 * @param string $slug
366 * @param WP_User|int|null $user
367 *
368 * @return bool
369 */
370 public static function user_can( $slug, $user = null ) {
371 $grants = self::for_user( $user );
372 $derived = self::derived_capabilities();
373
374 if ( isset( $derived[ $slug ] ) ) {
375 return ! empty( array_intersect( $derived[ $slug ], $grants ) );
376 }
377
378 return in_array( $slug, $grants, true );
379 }
380
381 /**
382 * A real administrator, as opposed to somebody this module elevated.
383 *
384 * Reads the raw capability array rather than user_can(), because the
385 * `user_has_cap` filter can change what user_can() returns and anything that
386 * decides who may edit the permission map has to be immune to the thing it
387 * configures. Every permission-management endpoint gates on this.
388 *
389 * @param WP_User|int|null $user
390 *
391 * @return bool
392 */
393 public static function is_real_admin( $user = null ) {
394 $user = self::resolve_user( $user );
395
396 if ( ! $user || ! $user->exists() ) {
397 return false;
398 }
399
400 if ( is_multisite() && is_super_admin( $user->ID ) ) {
401 return true;
402 }
403
404 return ! empty( $user->allcaps['manage_options'] );
405 }
406
407 /**
408 * @param WP_User|int|null $user
409 *
410 * @return WP_User|null
411 */
412 private static function resolve_user( $user = null ) {
413 if ( $user instanceof WP_User ) {
414 return $user;
415 }
416
417 if ( is_numeric( $user ) && $user > 0 ) {
418 $resolved = get_user_by( 'id', (int) $user );
419 return $resolved ?: null;
420 }
421
422 $current = wp_get_current_user();
423
424 return $current instanceof WP_User ? $current : null;
425 }
426 }
427