PluginProbe
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder / trunk
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder vtrunk
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 1.1.0 1.1.1 1.1.2 1.2.0 1.2.1 All 78 releases
ablocks / includes / permissions.php

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

405 lines 12.9 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 * @return string[]
185 */
186 public static function all_slugs() {
187 static $slugs = null;
188 if ( null === $slugs ) {
189 $slugs = [];
190 foreach ( self::catalogue() as $group ) {
191 $slugs = array_merge( $slugs, array_keys( $group['permissions'] ) );
192 }
193 }
194 return $slugs;
195 }
196
197 public static function is_valid_slug( $slug ) {
198 return in_array( $slug, self::all_slugs(), true );
199 }
200
201 /**
202 * Capabilities that can never be granted by this module, only taken away.
203 *
204 * The add-ons screen installs plugins (Ajax\Dashboard::install_plugin), and a
205 * permission system that can hand out plugin installation can hand out
206 * everything. So this permission gates the screen for people who already
207 * hold the WordPress capability, and does nothing for anyone else.
208 *
209 * @return array Slug => the WordPress capability the user must already hold.
210 */
211 public static function requires_native_cap() {
212 return [
213 'ablocks_manage_addons' => 'install_plugins',
214 ];
215 }
216
217 /**
218 * Everything the block editor itself is gated on.
219 *
220 * The set a role that can edit posts holds by default, and the building
221 * block Pro's presets start from.
222 *
223 * @return string[]
224 */
225 public static function editing_capabilities() {
226 return [
227 'ablocks_use_editor',
228 'ablocks_edit_style',
229 'ablocks_edit_advanced',
230 'ablocks_edit_custom_css',
231 'ablocks_copy_paste_style',
232 ];
233 }
234
235 /**
236 * Sort + filter a grants list so two equivalent lists compare equal.
237 *
238 * @param array $grants
239 *
240 * @return string[]
241 */
242 public static function normalize( array $grants ) {
243 $grants = array_values( array_unique( array_filter( $grants, [ __CLASS__, 'is_valid_slug' ] ) ) );
244 sort( $grants );
245 return $grants;
246 }
247
248 /**
249 * What a role gets when nothing has configured it.
250 *
251 * Installing or updating aBlocks must not change what anybody can do. Any
252 * role that can edit posts could already use every aBlocks block and
253 * control, and no role but administrator could reach an aBlocks screen. That
254 * is exactly what this returns.
255 *
256 * @param string $role_slug
257 *
258 * @return string[]
259 */
260 public static function default_grants_for_role( $role_slug ) {
261 $role = get_role( $role_slug );
262
263 $grants = ( $role && ! empty( $role->capabilities['edit_posts'] ) )
264 ? self::editing_capabilities()
265 : [];
266
267 return apply_filters( 'ablocks/permissions/default_role_grants', $grants, $role_slug );
268 }
269
270 /**
271 * The capabilities a role holds.
272 *
273 * The filter is where aBlocks Pro returns the map a site configured on the
274 * Roles & Permissions screen. It may return an empty array — "this role gets
275 * nothing" has to be expressible — so a filtered value replaces the default
276 * rather than adding to it.
277 *
278 * @param string $role_slug
279 *
280 * @return string[]
281 */
282 public static function get_role_grants( $role_slug ) {
283 $grants = apply_filters(
284 'ablocks/permissions/role_grants',
285 self::default_grants_for_role( $role_slug ),
286 $role_slug
287 );
288
289 return self::normalize( (array) $grants );
290 }
291
292 /**
293 * Every capability a user effectively holds.
294 *
295 * Deliberately reads roles and meta directly and never calls user_can().
296 * Permissions\Caps calls this from inside the `user_has_cap` filter, so a
297 * capability check in here would recurse.
298 *
299 * @param WP_User|int|null $user
300 *
301 * @return string[]
302 */
303 public static function for_user( $user = null ) {
304 $user = self::resolve_user( $user );
305
306 if ( ! $user || ! $user->exists() ) {
307 return [];
308 }
309
310 if ( self::is_real_admin( $user ) ) {
311 return self::all_slugs();
312 }
313
314 $grants = [];
315 foreach ( (array) $user->roles as $role_slug ) {
316 $grants = array_merge( $grants, self::get_role_grants( $role_slug ) );
317 }
318
319 // Where aBlocks Pro applies a per-user override. It replaces the union of
320 // the user's roles rather than adding to it, because an override has to be
321 // able to take something away as well as give it.
322 $grants = apply_filters( 'ablocks/permissions/user_grants', self::normalize( $grants ), $user );
323 $grants = self::normalize( (array) $grants );
324
325 // Permissions that can only ever restrict. Holding one without the
326 // underlying WordPress capability means nothing. Applied after the filter
327 // so an override cannot route around it either.
328 foreach ( self::requires_native_cap() as $slug => $native ) {
329 if ( in_array( $slug, $grants, true ) && empty( $user->allcaps[ $native ] ) ) {
330 $grants = array_values( array_diff( $grants, [ $slug ] ) );
331 }
332 }
333
334 return $grants;
335 }
336
337 /**
338 * Whether a user holds a capability.
339 *
340 * Everywhere except inside the `user_has_cap` filter itself, prefer plain
341 * current_user_can( 'ablocks_…' ) — Caps makes that work.
342 *
343 * @param string $slug
344 * @param WP_User|int|null $user
345 *
346 * @return bool
347 */
348 public static function user_can( $slug, $user = null ) {
349 $grants = self::for_user( $user );
350 $derived = self::derived_capabilities();
351
352 if ( isset( $derived[ $slug ] ) ) {
353 return ! empty( array_intersect( $derived[ $slug ], $grants ) );
354 }
355
356 return in_array( $slug, $grants, true );
357 }
358
359 /**
360 * A real administrator, as opposed to somebody this module elevated.
361 *
362 * Reads the raw capability array rather than user_can(), because the
363 * `user_has_cap` filter can change what user_can() returns and anything that
364 * decides who may edit the permission map has to be immune to the thing it
365 * configures. Every permission-management endpoint gates on this.
366 *
367 * @param WP_User|int|null $user
368 *
369 * @return bool
370 */
371 public static function is_real_admin( $user = null ) {
372 $user = self::resolve_user( $user );
373
374 if ( ! $user || ! $user->exists() ) {
375 return false;
376 }
377
378 if ( is_multisite() && is_super_admin( $user->ID ) ) {
379 return true;
380 }
381
382 return ! empty( $user->allcaps['manage_options'] );
383 }
384
385 /**
386 * @param WP_User|int|null $user
387 *
388 * @return WP_User|null
389 */
390 private static function resolve_user( $user = null ) {
391 if ( $user instanceof WP_User ) {
392 return $user;
393 }
394
395 if ( is_numeric( $user ) && $user > 0 ) {
396 $resolved = get_user_by( 'id', (int) $user );
397 return $resolved ?: null;
398 }
399
400 $current = wp_get_current_user();
401
402 return $current instanceof WP_User ? $current : null;
403 }
404 }
405