array ( * 'name' => 'rolename', * 'capabilities' => array() * ) * ) * * @since 2.0.0 */ #[AllowDynamicProperties] class WP_Roles { /** * List of roles and capabilities. * * @since 2.0.0 * @var array[] */ public $roles; /** * List of the role objects. * * @since 2.0.0 * @var WP_Role[] */ public $role_objects = array(); /** * List of role names. * * @since 2.0.0 * @var string[] */ public $role_names = array(); /** * Option name for storing role list. * * @since 2.0.0 * @var string */ public $role_key; /** * Whether to use the database for retrieval and storage. * * @since 2.1.0 * @var bool */ public $use_db = true; /** * The site ID the roles are initialized for. * * @since 4.9.0 * @var int */ protected $site_id = 0; /** * Constructor. * * @since 2.0.0 * @since 4.9.0 The `$site_id` argument was added. * * @global array $wp_user_roles Used to set the 'roles' property value. * * @param int $site_id Site ID to initialize roles for. Default is the current site. */ public function __construct( $site_id = null ) { global $wp_user_roles; $this->use_db = empty( $wp_user_roles ); $this->for_site( $site_id ); } /** * Makes private/protected methods readable for backward compatibility. * * @since 4.0.0 * * @param string $name Method to call. * @param array $arguments Arguments to pass when calling. * @return mixed|false Return value of the callback, false otherwise. */ public function __call( $name, $arguments ) { if ( '_init' === $name ) { return $this->_init( ...$arguments ); } return false; } /** * Sets up the object properties. * * The role key is set to the current prefix for the $wpdb object with * 'user_roles' appended. If the $wp_user_roles global is set, then it will * be used and the role option will not be updated or used. * * @since 2.1.0 * @deprecated 4.9.0 Use WP_Roles::for_site() */ protected function _init() { _deprecated_function( __METHOD__, '4.9.0', 'WP_Roles::for_site()' ); $this->for_site(); } /** * Reinitializes the object. * * Recreates the role objects. This is typically called only by switch_to_blog() * after switching wpdb to a new site ID. * * @since 3.5.0 * @deprecated 4.7.0 Use WP_Roles::for_site() */ public function reinit() { _deprecated_function( __METHOD__, '4.7.0', 'WP_Roles::for_site()' ); $this->for_site(); } /** * Adds a role name with capabilities to the list. * * Updates the list of roles, if the role doesn't already exist. * * The capabilities are defined in the following format: `array( 'read' => true )`. * To explicitly deny the role a capability, set the value for that capability to false. * * @since 2.0.0 * * @param string $role Role name. * @param string $display_name Role display name. * @param bool[] $capabilities Optional. List of capabilities keyed by the capability name, * e.g. `array( 'edit_posts' => true, 'delete_posts' => false )`. * Default empty array. * @return WP_Role|void WP_Role object, if the role is added. */ public function add_role( $role, $display_name, $capabilities = array() ) { if ( empty( $role ) || isset( $this->roles[ $role ] ) ) { return; } $this->roles[ $role ] = array( 'name' => $display_name, 'capabilities' => $capabilities, ); if ( $this->use_db ) { update_option( $this->role_key, $this->roles, true ); } $this->role_objects[ $role ] = new WP_Role( $role, $capabilities ); $this->role_names[ $role ] = $display_name; return $this->role_objects[ $role ]; } /** * Removes a role by name. * * @since 2.0.0 * * @param string $role Role name. */ public function remove_role( $role ) { if ( ! isset( $this->role_objects[ $role ] ) ) { return; } unset( $this->role_objects[ $role ] ); unset( $this->role_names[ $role ] ); unset( $this->roles[ $role ] ); if ( $this->use_db ) { update_option( $this->role_key, $this->roles ); } if ( get_option( 'default_role' ) === $role ) { update_option( 'default_role', 'subscriber' ); } } /** * Adds a capability to role. * * @since 2.0.0 * * @param string $role Role name. * @param string $cap Capability name. * @param bool $grant Optional. Whether role is capable of performing capability. * Default true. */ public function add_cap( $role, $cap, $grant = true ) { if ( ! isset( $this->roles[ $role ] ) ) { return; } $this->roles[ $role ]['capabilities'][ $cap ] = $grant; if ( $this->use_db ) { update_option( $this->role_key, $this->roles ); } } /** * Removes a capability from role. * * @since 2.0.0 * * @param string $role Role name. * @param string $cap Capability name. */ public function remove_cap( $role, $cap ) { if ( ! isset( $this->roles[ $role ] ) ) { return; } unset( $this->roles[ $role ]['capabilities'][ $cap ] ); if ( $this->use_db ) { update_option( $this->role_key, $this->roles ); } } /** * Retrieves a role object by name. * * @since 2.0.0 * * @param string $role Role name. * @return WP_Role|null WP_Role object if found, null if the role does not exist. */ public function get_role( $role ) { if ( isset( $this->role_objects[ $role ] ) ) { return $this->role_objects[ $role ]; } else { return null; } } /** * Retrieves a list of role names. * * @since 2.0.0 * * @return string[] List of role names. */ public function get_names() { return $this->role_names; } /** * Determines whether a role name is currently in the list of available roles. * * @since 2.0.0 * * @param string $role Role name to look up. * @return bool */ public function is_role( $role ) { return isset( $this->role_names[ $role ] ); } /** * Initializes all of the available roles. * * @since 4.9.0 */ public function init_roles() { if ( empty( $this->roles ) ) { return; } $this->role_objects = array(); $this->role_names = array(); foreach ( array_keys( $this->roles ) as $role ) { $this->role_objects[ $role ] = new WP_Role( $role, $this->roles[ $role ]['capabilities'] ); $this->role_names[ $role ] = $this->roles[ $role ]['name']; } /** * Fires after the roles have been initialized, allowing plugins to add their own roles. * * @since 4.7.0 * * @param WP_Roles $wp_roles A reference to the WP_Roles object. */ do_action( 'wp_roles_init', $this ); } /** * Sets the site to operate on. Defaults to the current site. * * @since 4.9.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int $site_id Site ID to initialize roles for. Default is the current site. */ public function for_site( $site_id = null ) { global $wpdb; if ( ! empty( $site_id ) ) { $this->site_id = absint( $site_id ); } else { $this->site_id = get_current_blog_id(); } $this->role_key = $wpdb->get_blog_prefix( $this->site_id ) . 'user_roles'; if ( ! empty( $this->roles ) && ! $this->use_db ) { return; } $this->roles = $this->get_roles_data(); $this->init_roles(); } /** * Gets the ID of the site for which roles are currently initialized. * * @since 4.9.0 * * @return int Site ID. */ public function get_site_id() { return $this->site_id; } /** * Gets the available roles data. * * @since 4.9.0 * * @global array $wp_user_roles Used to set the 'roles' property value. * * @return array Roles array. */ protected function get_roles_data() { global $wp_user_roles; if ( ! empty( $wp_user_roles ) ) { return $wp_user_roles; } if ( is_multisite() && get_current_blog_id() !== $this->site_id ) { remove_action( 'switch_blog', 'wp_switch_roles_and_user', 1 ); $roles = get_blog_option( $this->site_id, $this->role_key, array() ); add_action( 'switch_blog', 'wp_switch_roles_and_user', 1, 2 ); return $roles; } return get_option( $this->role_key, array() ); } } namespace = 'wp/v2'; $this->rest_base = 'settings'; } /** * Registers the routes for the site's settings. * * @since 4.7.0 * * @see register_rest_route() */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'args' => array(), 'permission_callback' => array( $this, 'get_item_permissions_check' ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_item' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), 'permission_callback' => array( $this, 'get_item_permissions_check' ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); } /** * Checks if a given request has access to read and manage settings. * * @since 4.7.0 * * @param WP_REST_Request $request Full details about the request. * @return bool True if the request has read access for the item, otherwise false. */ public function get_item_permissions_check( $request ) { return current_user_can( 'manage_options' ); } /** * Retrieves the settings. * * @since 4.7.0 * * @param WP_REST_Request $request Full details about the request. * @return array|WP_Error Array on success, or WP_Error object on failure. */ public function get_item( $request ) { $options = $this->get_registered_options(); $response = array(); foreach ( $options as $name => $args ) { /** * Filters the value of a setting recognized by the REST API. * * Allow hijacking the setting value and overriding the built-in behavior by returning a * non-null value. The returned value will be presented as the setting value instead. * * @since 4.7.0 * * @param mixed $result Value to use for the requested setting. Can be a scalar * matching the registered schema for the setting, or null to * follow the default get_option() behavior. * @param string $name Setting name (as shown in REST API responses). * @param array $args Arguments passed to register_setting() for this setting. */ $response[ $name ] = apply_filters( 'rest_pre_get_setting', null, $name, $args ); if ( is_null( $response[ $name ] ) ) { // Default to a null value as "null" in the response means "not set". $response[ $name ] = get_option( $args['option_name'], $args['schema']['default'] ); } /* * Because get_option() is lossy, we have to * cast values to the type they are registered with. */ $response[ $name ] = $this->prepare_value( $response[ $name ], $args['schema'] ); } return $response; } /** * Prepares a value for output based off a schema array. * * @since 4.7.0 * * @param mixed $value Value to prepare. * @param array $schema Schema to match. * @return mixed The prepared value. */ protected function prepare_value( $value, $schema ) { /* * If the value is not valid by the schema, set the value to null. * Null values are specifically non-destructive, so this will not cause * overwriting the current invalid value to null. */ if ( is_wp_error( rest_validate_value_from_schema( $value, $schema ) ) ) { return null; } return rest_sanitize_value_from_schema( $value, $schema ); } /** * Updates settings for the settings object. * * @since 4.7.0 * * @param WP_REST_Request $request Full details about the request. * @return array|WP_Error Array on success, or error object on failure. */ public function update_item( $request ) { $options = $this->get_registered_options(); $params = $request->get_params(); foreach ( $options as $name => $args ) { if ( ! array_key_exists( $name, $params ) ) { continue; } /** * Filters whether to preempt a setting value update via the REST API. * * Allows hijacking the setting update logic and overriding the built-in behavior by * returning true. * * @since 4.7.0 * * @param bool $result Whether to override the default behavior for updating the * value of a setting. * @param string $name Setting name (as shown in REST API responses). * @param mixed $value Updated setting value. * @param array $args Arguments passed to register_setting() for this setting. */ $updated = apply_filters( 'rest_pre_update_setting', false, $name, $request[ $name ], $args ); if ( $updated ) { continue; } /* * A null value for an option would have the same effect as * deleting the option from the database, and relying on the * default value. */ if ( is_null( $request[ $name ] ) ) { /* * A null value is returned in the response for any option * that has a non-scalar value. * * To protect clients from accidentally including the null * values from a response object in a request, we do not allow * options with values that don't pass validation to be updated to null. * Without this added protection a client could mistakenly * delete all options that have invalid values from the * database. */ if ( is_wp_error( rest_validate_value_from_schema( get_option( $args['option_name'], false ), $args['schema'] ) ) ) { return new WP_Error( 'rest_invalid_stored_value', /* translators: %s: Property name. */ sprintf( __( 'The %s property has an invalid stored value, and cannot be updated to null.' ), $name ), array( 'status' => 500 ) ); } delete_option( $args['option_name'] ); } else { update_option( $args['option_name'], $request[ $name ] ); } } return $this->get_item( $request ); } /** * Retrieves all of the registered options for the Settings API. * * @since 4.7.0 * * @return array Array of registered options. */ protected function get_registered_options() { $rest_options = array(); foreach ( get_registered_settings() as $name => $args ) { if ( empty( $args['show_in_rest'] ) ) { continue; } $rest_args = array(); if ( is_array( $args['show_in_rest'] ) ) { $rest_args = $args['show_in_rest']; } $defaults = array( 'name' => ! empty( $rest_args['name'] ) ? $rest_args['name'] : $name, 'schema' => array(), ); $rest_args = array_merge( $defaults, $rest_args ); $default_schema = array( 'type' => empty( $args['type'] ) ? null : $args['type'], 'title' => empty( $args['label'] ) ? '' : $args['label'], 'description' => empty( $args['description'] ) ? '' : $args['description'], 'default' => isset( $args['default'] ) ? $args['default'] : null, ); $rest_args['schema'] = array_merge( $default_schema, $rest_args['schema'] ); $rest_args['option_name'] = $name; // Skip over settings that don't have a defined type in the schema. if ( empty( $rest_args['schema']['type'] ) ) { continue; } /* * Allow the supported types for settings, as we don't want invalid types * to be updated with arbitrary values that we can't do decent sanitizing for. */ if ( ! in_array( $rest_args['schema']['type'], array( 'number', 'integer', 'string', 'boolean', 'array', 'object' ), true ) ) { continue; } $rest_args['schema'] = rest_default_additional_properties_to_false( $rest_args['schema'] ); $rest_options[ $rest_args['name'] ] = $rest_args; } return $rest_options; } /** * Retrieves the site setting schema, conforming to JSON Schema. * * @since 4.7.0 * * @return array Item schema data. */ public function get_item_schema() { if ( $this->schema ) { return $this->add_additional_fields_schema( $this->schema ); } $options = $this->get_registered_options(); $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'settings', 'type' => 'object', 'properties' => array(), ); foreach ( $options as $option_name => $option ) { $schema['properties'][ $option_name ] = $option['schema']; $schema['properties'][ $option_name ]['arg_options'] = array( 'sanitize_callback' => array( $this, 'sanitize_callback' ), ); } $this->schema = $schema; return $this->add_additional_fields_schema( $this->schema ); } /** * Custom sanitize callback used for all options to allow the use of 'null'. * * By default, the schema of settings will throw an error if a value is set to * `null` as it's not a valid value for something like "type => string". We * provide a wrapper sanitizer to allow the use of `null`. * * @since 4.7.0 * * @param mixed $value The value for the setting. * @param WP_REST_Request $request The request object. * @param string $param The parameter name. * @return mixed|WP_Error */ public function sanitize_callback( $value, $request, $param ) { if ( is_null( $value ) ) { return $value; } return rest_parse_request_arg( $value, $request, $param ); } /** * Recursively add additionalProperties = false to all objects in a schema * if no additionalProperties setting is specified. * * This is needed to restrict properties of objects in settings values to only * registered items, as the REST API will allow additional properties by * default. * * @since 4.9.0 * @deprecated 6.1.0 Use {@see rest_default_additional_properties_to_false()} instead. * * @param array $schema The schema array. * @return array */ protected function set_additional_properties_to_false( $schema ) { _deprecated_function( __METHOD__, '6.1.0', 'rest_default_additional_properties_to_false()' ); return rest_default_additional_properties_to_false( $schema ); } }