| 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 |
|