| @@ -105,11 +105,11 @@ | ||
| 105 | 105 | * |
| 106 | 106 | * @return string |
| 107 | 107 | */ |
| 108 | 108 | public static function sanitize_wysiwyg( $input ): string { |
| 109 | - $input = wp_kses_post( $input ); | |
| 109 | + $input = wp_specialchars_decode( stripslashes( $input ), ENT_COMPAT ); | |
| 110 | 110 | |
| 111 | - return wp_specialchars_decode( stripslashes( $input ), ENT_COMPAT ); | |
| 111 | + return wp_kses_post( $input ); | |
| 112 | 112 | } |
| 113 | 113 | |
| 114 | 114 | /** |
| 115 | 115 | * Return all taxonomies list to use in JS components |
| @@ -272,8 +272,110 @@ | ||
| 272 | 272 | } |
| 273 | 273 | } |
| 274 | 274 | |
| 275 | 275 | /** |
| 276 | + * Returns editable roles and ensures the core helper is loaded. | |
| 277 | + * | |
| 278 | + * @return array | |
| 279 | + */ | |
| 280 | + public static function get_editable_roles_safe(): array { | |
| 281 | + if ( ! function_exists( 'get_editable_roles' ) ) { | |
| 282 | + require_once ABSPATH . 'wp-admin/includes/user.php'; | |
| 283 | + } | |
| 284 | + | |
| 285 | + if ( ! function_exists( 'get_editable_roles' ) ) { | |
| 286 | + return array(); | |
| 287 | + } | |
| 288 | + | |
| 289 | + return get_editable_roles(); | |
| 290 | + } | |
| 291 | + | |
| 292 | + /** | |
| 293 | + * Returns all registered roles without filtering by the current user's capabilities. | |
| 294 | + * | |
| 295 | + * @return array | |
| 296 | + */ | |
| 297 | + public static function get_registered_roles_safe(): array { | |
| 298 | + $wp_roles = wp_roles(); | |
| 299 | + | |
| 300 | + if ( ! is_a( $wp_roles, \WP_Roles::class ) ) { | |
| 301 | + return array(); | |
| 302 | + } | |
| 303 | + | |
| 304 | + return $wp_roles->roles ?? array(); | |
| 305 | + } | |
| 306 | + | |
| 307 | + /** | |
| 308 | + * Returns auto-detected low-privilege roles that can be used as | |
| 309 | + * a starting point for self-service role transitions. | |
| 310 | + * | |
| 311 | + * @return string[] | |
| 312 | + */ | |
| 313 | + public static function get_default_self_promotable_roles(): array { | |
| 314 | + $editable_roles = self::get_registered_roles_safe(); | |
| 315 | + | |
| 316 | + if ( empty( $editable_roles ) ) { | |
| 317 | + return array(); | |
| 318 | + } | |
| 319 | + | |
| 320 | + $roles = array(); | |
| 321 | + | |
| 322 | + foreach ( array_keys( $editable_roles ) as $role ) { | |
| 323 | + if ( self::is_safe_self_promotable_role( $role ) ) { | |
| 324 | + $roles[] = $role; | |
| 325 | + } | |
| 326 | + } | |
| 327 | + | |
| 328 | + return array_values( array_unique( $roles ) ); | |
| 329 | + } | |
| 330 | + | |
| 331 | + public static function is_safe_self_promotable_role( string $role ): bool { | |
| 332 | + if ( 'administrator' === $role ) { | |
| 333 | + return false; | |
| 334 | + } | |
| 335 | + | |
| 336 | + $wp_roles = wp_roles(); | |
| 337 | + | |
| 338 | + if ( ! isset( $wp_roles->roles[ $role ] ) ) { | |
| 339 | + return false; | |
| 340 | + } | |
| 341 | + | |
| 342 | + $caps = array_filter( $wp_roles->roles[ $role ]['capabilities'] ?? array() ); | |
| 343 | + | |
| 344 | + $dangerous_caps = array( | |
| 345 | + 'activate_plugins', | |
| 346 | + 'create_users', | |
| 347 | + 'delete_users', | |
| 348 | + 'edit_users', | |
| 349 | + 'list_users', | |
| 350 | + 'promote_users', | |
| 351 | + 'remove_users', | |
| 352 | + 'manage_options', | |
| 353 | + 'edit_theme_options', | |
| 354 | + 'switch_themes', | |
| 355 | + 'install_plugins', | |
| 356 | + 'update_plugins', | |
| 357 | + 'delete_plugins', | |
| 358 | + 'install_themes', | |
| 359 | + 'update_themes', | |
| 360 | + 'delete_themes', | |
| 361 | + 'unfiltered_html', | |
| 362 | + 'edit_others_posts', | |
| 363 | + 'delete_others_posts', | |
| 364 | + 'publish_posts', | |
| 365 | + 'manage_categories', | |
| 366 | + ); | |
| 367 | + | |
| 368 | + foreach ( $dangerous_caps as $cap ) { | |
| 369 | + if ( ! empty( $caps[ $cap ] ) ) { | |
| 370 | + return false; | |
| 371 | + } | |
| 372 | + } | |
| 373 | + | |
| 374 | + return true; | |
| 375 | + } | |
| 376 | + | |
| 377 | + /** | |
| 276 | 378 | * Prepare passed array for using in JS options |
| 277 | 379 | * |
| 278 | 380 | * @param array $array |
| 279 | 381 | * @param null $value_key |
| @@ -438,8 +540,15 @@ | ||
| 438 | 540 | |
| 439 | 541 | $result = array(); |
| 440 | 542 | |
| 441 | 543 | foreach ( $source as $key => $value ) { |
| 544 | + $key = is_string( $key ) ? self::sanitize_array_key( $key ) : $key; | |
| 545 | + | |
| 546 | + // When sanitized keys collide, keep the first value. | |
| 547 | + if ( array_key_exists( $key, $result ) ) { | |
| 548 | + continue; | |
| 549 | + } | |
| 550 | + | |
| 442 | 551 | $result[ $key ] = self::sanitize_recursive( $value ); |
| 443 | 552 | } |
| 444 | 553 | |
| 445 | 554 | return $result; |
| @@ -444,8 +553,20 @@ | ||
| 444 | 553 | |
| 445 | 554 | return $result; |
| 446 | 555 | } |
| 447 | 556 | |
| 557 | + /** | |
| 558 | + * Strips tags from an array key. Keys without tags stay unchanged, | |
| 559 | + * preserving whitespace and percent-encoded sequences in field names. | |
| 560 | + * | |
| 561 | + * @param string $key | |
| 562 | + * | |
| 563 | + * @return string | |
| 564 | + */ | |
| 565 | + public static function sanitize_array_key( string $key ): string { | |
| 566 | + return false === strpos( $key, '<' ) ? $key : wp_strip_all_tags( $key, false ); | |
| 567 | + } | |
| 568 | + | |
| 448 | 569 | public static function maybe_recursive_sanitize( $source = null ) { |
| 449 | 570 | return self::sanitize_recursive( $source ); |
| 450 | 571 | } |
| 451 | 572 | |
| @@ -492,8 +613,14 @@ | ||
| 492 | 613 | } |
| 493 | 614 | |
| 494 | 615 | $result = array(); |
| 495 | 616 | foreach ( $source as $key => $value ) { |
| 617 | + $key = is_string( $key ) ? self::sanitize_array_key( $key ) : $key; | |
| 618 | + | |
| 619 | + if ( array_key_exists( $key, $result ) ) { | |
| 620 | + continue; | |
| 621 | + } | |
| 622 | + | |
| 496 | 623 | $result[ $key ] = self::sanitize_recursive( $value ); |
| 497 | 624 | } |
| 498 | 625 | |
| 499 | 626 | return $result; |
| @@ -706,15 +833,17 @@ | ||
| 706 | 833 | return false; |
| 707 | 834 | } |
| 708 | 835 | |
| 709 | 836 | public static function get_array_of_user_roles( $settings ) { |
| 710 | - $user_roles = $settings ?? array(); | |
| 711 | - if ( ! empty( $user_roles ) ) { | |
| 712 | - if ( is_string( $user_roles ) ) { | |
| 713 | - $user_roles = array( $user_roles ); | |
| 714 | - } | |
| 837 | + if ( is_string( $settings ) ) { | |
| 838 | + return '' === $settings ? array() : array( $settings ); | |
| 715 | 839 | } |
| 716 | - return $user_roles; | |
| 840 | + | |
| 841 | + if ( ! is_array( $settings ) ) { | |
| 842 | + return array(); | |
| 843 | + } | |
| 844 | + | |
| 845 | + return $settings; | |
| 717 | 846 | } |
| 718 | 847 | |
| 719 | 848 | public static function get_main_user_role_by_priority( $roles ): string { |
| 720 | 849 | if ( is_string( $roles ) ) { |