|
11 | 11 | add_action( 'pre_get_posts', __NAMESPACE__ . '\scope_learn_content_list_to_author' ); |
12 | 12 | add_filter( 'user_has_cap', __NAMESPACE__ . '\set_caps_for_internal_notes' ); |
13 | 13 | add_filter( 'map_meta_cap', __NAMESPACE__ . '\map_meta_caps', 20, 4 ); // Needs to fire after meta caps in wporg-internal-notes. |
| 14 | +add_filter( 'editable_roles', __NAMESPACE__ . '\restrict_editable_roles' ); |
| 15 | +add_action( 'load-user-new.php', __NAMESPACE__ . '\restrict_invited_user_role' ); |
14 | 16 | add_action( 'init', __NAMESPACE__ . '\add_or_update_lesson_plan_editor_role' ); |
15 | 17 | add_action( 'init', __NAMESPACE__ . '\add_or_update_workshop_reviewer_role' ); |
16 | 18 |
|
@@ -170,6 +172,70 @@ function map_meta_caps( $required_caps, $current_cap, $user_id, $args ) { |
170 | 172 | return $required_caps; |
171 | 173 | } |
172 | 174 |
|
| 175 | +/** |
| 176 | + * Limit role assignment to capabilities the current user already holds. |
| 177 | + * |
| 178 | + * Compare primitive grants, including dynamically assigned capabilities. Checking mapped |
| 179 | + * capabilities would exclude roles with grants such as `unfiltered_html` that multisite |
| 180 | + * restricts independently of the role. Multisite super admins inherently hold all capabilities. |
| 181 | + * |
| 182 | + * @param array[] $roles Array of arrays containing role information. |
| 183 | + * |
| 184 | + * @return array[] |
| 185 | + */ |
| 186 | +function restrict_editable_roles( $roles ) { |
| 187 | + $user = wp_get_current_user(); |
| 188 | + $is_super_admin = is_multisite() && is_super_admin( $user->ID ); |
| 189 | + |
| 190 | + foreach ( $roles as $slug => $role ) { |
| 191 | + foreach ( array_keys( array_filter( $role['capabilities'] ) ) as $capability ) { |
| 192 | + if ( 'do_not_allow' === $capability ) { |
| 193 | + unset( $roles[ $slug ] ); |
| 194 | + break; |
| 195 | + } |
| 196 | + |
| 197 | + if ( $is_super_admin || 'exist' === $capability ) { |
| 198 | + continue; |
| 199 | + } |
| 200 | + |
| 201 | + /** This filter is documented in wp-includes/class-wp-user.php */ |
| 202 | + $user_caps = apply_filters( |
| 203 | + 'user_has_cap', |
| 204 | + $user->allcaps, |
| 205 | + array( $capability ), |
| 206 | + array( $capability, $user->ID ), |
| 207 | + $user |
| 208 | + ); |
| 209 | + |
| 210 | + if ( empty( $user_caps[ $capability ] ) ) { |
| 211 | + unset( $roles[ $slug ] ); |
| 212 | + break; |
| 213 | + } |
| 214 | + } |
| 215 | + } |
| 216 | + |
| 217 | + return $roles; |
| 218 | +} |
| 219 | + |
| 220 | +/** |
| 221 | + * Enforce editable roles before multisite stores an existing-user invitation. |
| 222 | + * |
| 223 | + * Core's confirmation flow stores the requested role without validating it against editable roles. |
| 224 | + * |
| 225 | + * @return void |
| 226 | + */ |
| 227 | +function restrict_invited_user_role() { |
| 228 | + if ( ! is_multisite() || ! isset( $_REQUEST['action'] ) || 'adduser' !== $_REQUEST['action'] ) { |
| 229 | + return; |
| 230 | + } |
| 231 | + |
| 232 | + check_admin_referer( 'add-user', '_wpnonce_add-user' ); |
| 233 | + |
| 234 | + // Validate the exact value core persists; sanitizing it could validate a different role. |
| 235 | + $role = isset( $_REQUEST['role'] ) && is_string( $_REQUEST['role'] ) ? $_REQUEST['role'] : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized |
| 236 | + wp_ensure_editable_role( $role ); |
| 237 | +} |
| 238 | + |
173 | 239 | /** |
174 | 240 | * Add the Lesson Plan Editor role if it doesn't exist yet, or ensure it has the correct capabilities. |
175 | 241 | * |
@@ -287,6 +353,7 @@ function add_or_update_workshop_reviewer_role() { |
287 | 353 | * |
288 | 354 | * The Workshop Reviewer should have all the same caps as the Editor role, with the addition of `promote_users` |
289 | 355 | * (normally reserved for the Admin role), so that they can add workshop presenters as new users on the site. |
| 356 | + * `restrict_editable_roles` limits role assignment to capabilities the current user already holds. |
290 | 357 | * |
291 | 358 | * This also gives them the cap to manage internal notes on workshop posts. (See `set_caps_for_internal_notes` above.) |
292 | 359 | * |
|
0 commit comments