PluginProbe
Groups – Memberships and Access Control / 4.7.1
Groups – Memberships and Access Control v4.7.1
4.7.1 4.7.0 4.6.0 4.5.0 4.4.0 4.3.0 trunk 1.0.0-beta-1 1.0.0-beta-2 1.0.0-beta-3 1.0.0-beta-3b 1.0.0-beta-3c 1.0.0-beta-3d 1.1.4 1.1.5 1.10.0 1.10.1 1.10.2 1.10.3 1.11.0 1.11.1 1.11.2 1.11.3 1.12.0 1.13.0 All 131 releases
← All changes | lib/wp/class-groups-wordpress.php +203 -28 1.11.14.7.1 View file →
@@ -26,12 +26,44 @@
26 26 /**
27 27 * WordPress capabilities integration.
28 28 */
29 29 class Groups_WordPress {
30 -
30 +
31 31 /**
32 + * Cache group
33 + *
34 + * @var string
35 + */
36 + const CACHE_GROUP = 'groups';
37 +
38 + /**
39 + * Cache key prefix
40 + *
41 + * @var string
42 + */
43 + const HAS_CAP = 'has_cap';
44 +
45 + /**
46 + * Filter priority: groups_user_can
47 + *
48 + * @var integer
49 + *
50 + * @since 2.11.0
51 + */
52 + const GROUPS_USER_CAN_FILTER_PRIORITY = 10;
53 +
54 + /**
55 + * Filter priority: user_has_cap
56 + *
57 + * @var int
58 + *
59 + * @since 2.11.0
60 + */
61 + const USER_HAS_CAP_FILTER_PRIORITY = PHP_INT_MAX;
62 +
63 + /**
32 64 * Hook into actions to extend user capabilities.
33 - *
65 + *
34 66 * @todo We might want to keep up with new capabilities when added, so
35 67 * that others don't have to add these explicitly to Groups when they
36 68 * add them to WordPress. Currently there's no hook for when a capability
37 69 * is added and checking this in any other way is too costly.
@@ -36,61 +68,203 @@
36 68 * add them to WordPress. Currently there's no hook for when a capability
37 69 * is added and checking this in any other way is too costly.
38 70 */
39 71 public static function init() {
40 - // args: string $result, Groups_User $groups_user, string $capability
41 - add_filter( 'groups_user_can', array( __CLASS__, 'groups_user_can' ), 10, 3 );
42 - add_filter( 'user_has_cap', array( __CLASS__, 'user_has_cap' ), 10, 3 );
72 + // args: boolean $result, Groups_User $groups_user, string $capability
73 + add_filter( 'groups_user_can', array( __CLASS__, 'groups_user_can' ), self::GROUPS_USER_CAN_FILTER_PRIORITY, 5 );
74 + add_filter( 'user_has_cap', array( __CLASS__, 'user_has_cap' ), self::USER_HAS_CAP_FILTER_PRIORITY, 4 );
43 75 }
44 -
76 +
45 77 /**
78 + * Whether the given user has the capability.
79 + *
80 + * Calls Groups_User::user_can() with user_has_cap and groups_user_can filters temporarily removed.
81 + *
82 + * @since 3.1.0
83 + *
84 + * @param int|WP_User $user user ID or object
85 + * @param string $capability capability name
86 + * @param mixed ...$args optional parameters, typically an object ID
87 + *
88 + * @return boolean
89 + */
90 + private static function unfiltered_user_can( $user, $capability, ...$args ) {
91 + // We want to check without the capabilities granted to the user via groups.
92 + // Groups_User::user_can() relies on user_can() or WP_User->has_cap() in the absense of the former,
93 + // which will cause our user_has_cap filter to act so we have to remove it temporarily.
94 + // To avoid potential internal conflicts or cicular dependencies, we also remove the groups_user_can filter which
95 + // relies on this method.
96 + $result = false;
97 + $filter_user_has_cap = remove_filter( 'user_has_cap', array( __CLASS__, 'user_has_cap' ), self::USER_HAS_CAP_FILTER_PRIORITY ); // @since 3.1.0
98 + $filter_groups_user_can = remove_filter( 'groups_user_can', array( __CLASS__, 'groups_user_can' ), self::GROUPS_USER_CAN_FILTER_PRIORITY ); // @since 3.1.0
99 + $result = Groups_User::user_can( $user, $capability, ...$args );
100 + if ( $filter_user_has_cap ) {
101 + add_filter( 'user_has_cap', array( __CLASS__, 'user_has_cap' ), self::USER_HAS_CAP_FILTER_PRIORITY, 4 );
102 + }
103 + if ( $filter_groups_user_can ) {
104 + add_filter( 'groups_user_can', array( __CLASS__, 'groups_user_can' ), self::GROUPS_USER_CAN_FILTER_PRIORITY, 5 );
105 + }
106 + return $result;
107 + }
108 +
109 + /**
46 110 * Extends Groups user capability with its WP_User capability.
47 - *
48 - * @param string $result
111 + *
112 + * @param boolean $result
49 113 * @param Groups_User $groups_user
50 114 * @param string $capability
115 + * @param mixed $object
116 + * @param mixed $args
117 + *
118 + * @return boolean
51 119 */
52 - public static function groups_user_can( $result, $groups_user, $capability ) {
120 + public static function groups_user_can( $result, $groups_user, $capability, $object, $args ) {
121 + // The intention here is to complement capabilities from groups with those of the user.
122 + // Our user_has_cap filter extends user capabilities with those from groups, i.e. the reverse complementary.
123 + // Thus, our user_has_cap filter should not be involved here. Also, we want to avoid any potential circular
124 + // dependency which could arise from having our groups_user_can fired again while we attend to that action here.
125 + // To avoid any potential hickups, we also remove the filter while we handle it.
126 + $filter_groups_user_can = remove_filter( 'groups_user_can', array( __CLASS__, 'groups_user_can' ), self::GROUPS_USER_CAN_FILTER_PRIORITY ); // @since 3.1.0
53 127 if ( !$result ) {
54 128 // Check if the capability exists, otherwise this will
55 129 // produce a deprecation warning "Usage of user levels by plugins
56 130 // and themes is deprecated", not because we actually use a
57 131 // deprecated user level, but because it doesn't exist.
58 - if ( Groups_Capability::read_by_capability( $capability ) ) {
59 - $result = user_can( $groups_user->user->ID, $capability );
132 + if (
133 + !is_numeric( $capability ) ||
134 + Groups_Capability::read_by_capability( $capability )
135 + ) {
136 + if ( $groups_user instanceof Groups_User ) {
137 + $user_id = $groups_user->get_user_id();
138 + if ( $user_id !== null ) {
139 +
140 + // reduce to remnant args; we have $args[0] as $capability, $args[1] as $user_id and $args[2] as $object and pass them along explicitly
141 + if ( is_array( $args ) ) {
142 + array_shift( $args ); // $capability <-> user_has_cap filter $args[0] -> requested capability
143 + array_shift( $args ); // $user_id <-> user_has_cap filter $args[1] -> concerned user ID
144 + array_shift( $args ); // $object <-> user_has_cap filter $args[2] -> typically object ID
145 + } else if ( $args instanceof Traversable ) {
146 + $pass_args = array();
147 + $i = 0;
148 + foreach ( $args as $arg ) {
149 + if ( $i > 2 ) {
150 + $pass_args[] = $arg;
151 + }
152 + }
153 + $args = $pass_args;
154 + }
155 +
156 + if ( $object === null ) {
157 + $result = self::unfiltered_user_can( $user_id, $capability );
158 + } else {
159 + // @since 3.0.0
160 + $object_id = null;
161 + if ( is_numeric( $object ) ) {
162 + $object_id = Groups_Utility::id( $object );
163 + if ( $object_id === false ) {
164 + $object_id = null;
165 + }
166 + } else if ( is_object( $object ) && method_exists( $object, 'get_id' ) ) {
167 + $object_id = $object->get_id();
168 + }
169 + // @since 4.0.0 make sure that $args can be unpacked
170 + if ( !( is_array( $args ) || is_object( $args ) && $args instanceof Traversable ) ) {
171 + $args = array();
172 + }
173 + // PHP >= 8.1.0 named arguments can be used after unpacking ...$args
174 + // With prior PHP versions, the order in which values appear in an $args array
175 + // determines the order of the parameters passed.
176 + if ( $object_id !== null ) {
177 + $result = self::unfiltered_user_can( $user_id, $capability, $object_id, ...$args );
178 + } else {
179 + $result = self::unfiltered_user_can( $user_id, $capability, $object, ...$args );
180 + }
181 + }
182 + }
183 + }
60 184 }
61 185 }
186 + if ( $filter_groups_user_can ) {
187 + add_filter( 'groups_user_can', array( __CLASS__, 'groups_user_can' ), self::GROUPS_USER_CAN_FILTER_PRIORITY, 5 ); // @since 3.1.0
188 + }
62 189 return $result;
63 190 }
64 -
191 +
65 192 /**
66 193 * Extend user capabilities with Groups user capabilities.
67 - *
68 - * @param array $allcaps the capabilities the user has
69 - * @param array $caps the requested capabilities
70 - * @param array $args capability context which can provide the requested capability as $args[0], the user ID as $args[1] and the related object's ID as $args[2]
194 + *
195 + * Hooked on the user_has_cap filter. See WP_User::has_cap().
196 + *
197 + * @see user_can()
198 + * @see WP_User::has_cap()
199 + *
200 + * @param array $allcaps capability names mapped to boolean values representing whether the user has the capability
201 + * @param array $caps required primitive capabilities for the requested capability
202 + * @param array $args the requested capability in $args[0], the user ID in $args[1] and optionally the related object's ID in $args[2] and potentially further parameters in $args[3] and so on ...
203 + * @param WP_User $user the user object
204 + *
205 + * @return array
71 206 */
72 - public static function user_has_cap( $allcaps, $caps, $args ) {
73 - $user_id = isset( $args[1] ) ? $args[1] : null;
74 - $groups_user = new Groups_User( $user_id );
207 + public static function user_has_cap( $allcaps, $caps, $args, $user ) {
75 208 if ( is_array( $caps ) ) {
76 - // we need to deactivate this because invoking $groups_user->can()
77 - // would trigger this same function and we would end up
78 - // in an infinite loop
79 - remove_filter( 'user_has_cap', array( __CLASS__, 'user_has_cap' ), 10, 3 );
80 - foreach( $caps as $cap ) {
81 - if ( $groups_user->can( $cap ) ) {
82 - $allcaps[$cap] = true;
209 + $user_id = 0;
210 + if ( isset( $user->ID ) ) {
211 + $user_id = intval( $user->ID );
212 + } else if ( isset( $args[1] ) ) {
213 + $user_id = intval( $args[1] );
214 + }
215 + $hash = md5( json_encode( $caps ) . json_encode( $args ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode
216 + $cached = Groups_Cache::get( self::HAS_CAP . '_' . $user_id . '_' . $hash, self::CACHE_GROUP );
217 +
218 + if ( $cached !== null ) {
219 + $_allcaps = $cached->get_value();
220 + unset( $cached );
221 + // Capabilities that were added after our value was cached must be added and
222 + // those entries that provide different values must be adopted. This is necessary
223 + // because other filters which hook into user_has_cap might have added or modified
224 + // things after we had already cached the values.
225 + foreach ( $allcaps as $cap => $value ) {
226 + if (
227 + !key_exists( $cap, $_allcaps ) ||
228 + $_allcaps[$cap] !== $value
229 + ) {
230 + $_allcaps[$cap] = $value;
231 + }
83 232 }
233 + $allcaps = $_allcaps;
234 + } else {
235 + $requested_cap = $args[0] ?? ''; // will always be supplied, just in case
236 + $object_id = $args[2] ?? null;
237 + $groups_user = new Groups_User( $user_id );
238 + // we need to deactivate this because invoking $groups_user->can()
239 + // would trigger this same function and we would end up
240 + // in an infinite loop
241 + remove_filter( 'user_has_cap', array( __CLASS__, 'user_has_cap' ), self::USER_HAS_CAP_FILTER_PRIORITY );
242 + foreach ( $caps as $cap ) {
243 + // Check for known primitive capability, if user has it. This requires no parameters from $args.
244 + if (
245 + $cap === $requested_cap && $object_id === null || // COND A
246 + $cap !== $requested_cap // COND B
247 + ) {
248 + if ( Groups_Capability::read_by_capability( $cap ) ) {
249 + if ( $groups_user->can( $cap ) ) {
250 + $allcaps[$cap] = true;
251 + }
252 + }
253 + }
254 + }
255 + add_filter( 'user_has_cap', array( __CLASS__, 'user_has_cap' ), self::USER_HAS_CAP_FILTER_PRIORITY, 4 );
256 + Groups_Cache::set( self::HAS_CAP . '_' . $user_id . '_' . $hash, $allcaps, self::CACHE_GROUP );
84 257 }
85 - add_filter( 'user_has_cap', array( __CLASS__, 'user_has_cap' ), 10, 3 );
86 258 }
259 + $allcaps = apply_filters( 'groups_user_has_cap', $allcaps, $caps, $args, $user );
87 260 return $allcaps;
88 261 }
89 -
262 +
90 263 /**
91 264 * Adds WordPress capabilities to Groups capabilities.
92 265 * Must be called explicitly.
266 + *
93 267 * @see Groups_Controller::activate()
94 268 */
95 269 public static function activate() {
96 270 self::refresh_capabilities();
@@ -97,8 +271,9 @@
97 271 }
98 272
99 273 /**
100 274 * Refreshes Groups capabilities based on WordPress capabilities.
275 + *
101 276 * @return int number of capabilities added
102 277 */
103 278 public static function refresh_capabilities() {
104 279 global $wp_roles;