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 / caps.php

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

180 lines 5.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * The capability bridge.
4 *
5 * Turns the stored permission map into capabilities WordPress can actually
6 * check, for the length of one request. Nothing here calls add_cap(): revoking
7 * a permission takes effect on the next page load, and deactivating aBlocks
8 * leaves every user with exactly the capabilities they had before.
9 *
10 * @package ABlocks
11 */
12
13 namespace ABlocks\Permissions;
14
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit;
17 }
18
19 use ABlocks\Permissions;
20 use WP_User;
21
22 class Caps {
23
24 /**
25 * Capabilities WordPress itself gates on, granted alongside one of ours.
26 *
27 * Only edit_theme_options, and only for the Site Editor permission. This is
28 * the one place the module reaches outside its own namespace, so the list
29 * stays short and obvious: a broad grant here is how a permission system
30 * turns into a privilege-escalation bug.
31 *
32 * @return array
33 */
34 public static function native_bridge() {
35 return apply_filters('ablocks/permissions/native_bridge', [
36 'ablocks_access_site_editor' => [ 'edit_theme_options' ],
37 ]);
38 }
39
40 /**
41 * Per-user cache of the capabilities we add. Cleared when the map changes.
42 *
43 * @var array
44 */
45 private static $cache = [];
46
47 /**
48 * Reentrancy guard. Everything Permissions::for_user() touches reads roles
49 * and meta directly, but a filter added by another plugin might not, and a
50 * capability check inside a capability filter would recurse forever.
51 *
52 * @var bool
53 */
54 private static $resolving = false;
55
56 public static function init() {
57 $self = new self();
58
59 add_filter( 'user_has_cap', [ $self, 'grant_capabilities' ], 10, 4 );
60 add_filter( 'map_meta_cap', [ $self, 'guard_theme_builder_templates' ], 10, 4 );
61
62 // The map changing has to invalidate the request-local cache, or a save
63 // followed by a check in the same request answers with the old map.
64 add_action( 'ablocks/permissions/changed', [ __CLASS__, 'flush_cache' ] );
65 }
66
67 public static function flush_cache() {
68 self::$cache = [];
69 }
70
71 /**
72 * @param array $allcaps
73 * @param array $caps
74 * @param array $args
75 * @param WP_User $user
76 *
77 * @return array
78 */
79 public function grant_capabilities( $allcaps, $caps, $args, $user ) {
80 if ( self::$resolving || ! $user instanceof WP_User || ! $user->ID ) {
81 return $allcaps;
82 }
83
84 // Administrators are NOT short-circuited here. They hold every aBlocks
85 // capability, but holding manage_options does not by itself answer
86 // current_user_can( 'ablocks_access' ) — these are our own capability
87 // strings, and nothing in WordPress knows an administrator should pass
88 // them. Skipping the merge for administrators hid the entire aBlocks
89 // menu from the very people who own the site. The per-user cache below
90 // keeps the repeated cost down instead.
91 $granted = self::granted_for( $user );
92
93 if ( empty( $granted ) ) {
94 return $allcaps;
95 }
96
97 // Union, never overwrite: a capability WordPress already answered false
98 // for stays false unless this module explicitly grants it.
99 return $allcaps + $granted;
100 }
101
102 /**
103 * The capability array to merge in for a user.
104 *
105 * @param WP_User $user
106 *
107 * @return array
108 */
109 private static function granted_for( WP_User $user ) {
110 if ( isset( self::$cache[ $user->ID ] ) ) {
111 return self::$cache[ $user->ID ];
112 }
113
114 self::$resolving = true;
115 $grants = Permissions::for_user( $user );
116 self::$resolving = false;
117
118 $granted = [];
119
120 foreach ( $grants as $slug ) {
121 $granted[ $slug ] = true;
122 }
123
124 foreach ( Permissions::derived_capabilities() as $derived => $implied_by ) {
125 if ( array_intersect( $implied_by, $grants ) ) {
126 $granted[ $derived ] = true;
127 }
128 }
129
130 $bridge = self::native_bridge();
131 foreach ( $bridge as $slug => $native_caps ) {
132 if ( empty( $granted[ $slug ] ) ) {
133 continue;
134 }
135 foreach ( (array) $native_caps as $native_cap ) {
136 $granted[ $native_cap ] = true;
137 }
138 }
139
140 self::$cache[ $user->ID ] = $granted;
141
142 return $granted;
143 }
144
145 /**
146 * Keep Theme Builder templates behind the Theme Builder permission.
147 *
148 * The ablocks_tb post type is registered with capability_type => post, so
149 * anybody who can edit posts can edit a site's header and footer. That was
150 * true before this module existed; now that Theme Builder is a permission,
151 * it should mean something. Denying here rather than changing the post
152 * type's capability_type keeps existing sites working with no migration and
153 * no capabilities written to the database.
154 *
155 * @param array $caps
156 * @param string $cap
157 * @param int $user_id
158 * @param array $args
159 *
160 * @return array
161 */
162 public function guard_theme_builder_templates( $caps, $cap, $user_id, $args ) {
163 static $guarded = [ 'edit_post', 'delete_post', 'publish_post' ];
164
165 if ( ! in_array( $cap, $guarded, true ) || empty( $args[0] ) ) {
166 return $caps;
167 }
168
169 if ( 'ablocks_tb' !== get_post_type( $args[0] ) ) {
170 return $caps;
171 }
172
173 if ( Permissions::is_real_admin( $user_id ) || Permissions::user_can( 'ablocks_manage_theme_builder', $user_id ) ) {
174 return $caps;
175 }
176
177 return [ 'do_not_allow' ];
178 }
179 }
180