LLMS_REST_Users_Controller
Source Source
File: libraries/lifterlms-rest/includes/abstracts/class-llms-rest-users-controller.php
abstract class LLMS_REST_Users_Controller extends LLMS_Rest_Controller { /** * Resource ID or Name * * For example: 'student' or 'instructor'. * * @var string */ protected $resource_name; /** * Schema properties available for ordering the collection * * @var string[] */ protected $orderby_properties = array( 'id', 'email', 'name', 'registered_date', ); /** * Whether search is allowed * * @var boolean */ protected $is_searchable = true; /** * Schema properties to query search columns mapping * * @var array */ protected $search_columns_mapping = array( 'id' => 'ID', 'username' => 'user_login', 'email' => 'user_email', 'url' => 'user_url', 'name' => 'display_name', ); /** * Constructor. * * @since 1.0.0-beta.27 * * @return void */ public function __construct() { $this->meta = new WP_REST_User_Meta_Fields(); } /** * Determine if the current user has permissions to manage the role(s) present in a request * * @since 1.0.0-beta.1 * * @param WP_REST_Request $request Request object. * @return true|WP_Error */ protected function check_roles_permissions( $request ) { global $wp_roles; $schema = $this->get_item_schema(); $roles = array(); if ( ! empty( $request['roles'] ) ) { $roles = $request['roles']; } elseif ( ! empty( $schema['properties']['roles']['default'] ) ) { $roles = $schema['properties']['roles']['default']; } foreach ( $roles as $role ) { if ( ! isset( $wp_roles->role_objects[ $role ] ) ) { // Translators: %s = role key. return llms_rest_bad_request_error( sprintf( __( 'The role %s does not exist.', 'lifterlms' ), $role ) ); } $potential_role = $wp_roles->role_objects[ $role ]; /* * Don't let anyone with 'edit_users' (admins) edit their own role to something without it. * Multisite super admins can freely edit their blog roles -- they possess all caps. */ if ( ! ( is_multisite() && current_user_can( 'manage_sites' ) ) && get_current_user_id() === $request['id'] && ! $potential_role->has_cap( 'edit_users' ) ) { return llms_rest_authorization_required_error( __( 'You are not allowed to give users this role.', 'lifterlms' ) ); } // Include admin functions to get access to `get_editable_roles()`. require_once ABSPATH . 'wp-admin/includes/admin.php'; // The new role must be editable by the logged-in user. $editable_roles = get_editable_roles(); if ( empty( $editable_roles[ $role ] ) ) { return llms_rest_authorization_required_error( __( 'You are not allowed to give users this role.', 'lifterlms' ) ); } } return true; } /** * Insert the prepared data into the database * * @since 1.0.0-beta.1 * * @param array $prepared Prepared item data. * @param WP_REST_Request $request Request object. * @return obj Object Instance of object from `$this->get_object()`. */ protected function create_object( $prepared, $request ) { $object_id = wp_insert_user( $prepared ); if ( is_wp_error( $object_id ) ) { return $object_id; } return $this->update_additional_data( $object_id, $prepared, $request ); } /** * Delete the object * * Note: we do not return 404s when the resource to delete cannot be found. We assume it's already been deleted and respond with 204. * Errors returned by this method should be any error other than a 404! * * @since 1.0.0-beta.1 * * @param obj $object Instance of the object from `$this->get_object()`. * @param WP_REST_Request $request Request object. * @return true|WP_Error `true` when the object is removed, `WP_Error` on failure. */ protected function delete_object( $object, $request ) { $id = $object->get( 'id' ); $reassign = 0 === $request['reassign'] ? null : $request['reassign']; if ( ! empty( $reassign ) ) { if ( $reassign === $id || ! get_userdata( $reassign ) ) { return llms_rest_bad_request_error( __( 'Invalid user ID for reassignment.', 'lifterlms' ) ); } } // Include admin user functions to get access to `wp_delete_user()`. require_once ABSPATH . 'wp-admin/includes/user.php'; $result = wp_delete_user( $id, $reassign ); if ( ! $result ) { return llms_rest_server_error( __( 'The user could not be deleted.', 'lifterlms' ) ); } return true; } /** * Determine if the current user can view the object * * @since 1.0.0-beta.7 * * @param object $object Object. * @return bool */ protected function check_read_object_permissions( $object ) { return $this->check_read_item_permissions( $this->get_object_id( $object ) ); } /** * Retrieves the query params for the objects collection * * @since 1.0.0-beta.1 * * @return array Collection parameters. */ public function get_collection_params() { $params = parent::get_collection_params(); $params['roles'] = array( 'description' => __( 'Include only users keys matching matching a specific role. Accepts a single role or a comma separated list of roles.', 'lifterlms' ), 'type' => 'array', 'items' => array( 'type' => 'string', 'enum' => $this->get_enum_roles(), ), ); return $params; } /** * Retrieve arguments for deleting a resource * * @since 1.0.0-beta.1 * * @return array */ public function get_delete_item_args() { return array( 'reassign' => array( 'type' => 'integer', 'description' => __( 'Reassign the deleted user\'s posts and links to this user ID.', 'lifterlms' ), 'default' => 0, 'sanitize_callback' => 'absint', ), ); } /** * Retrieve an array of allowed user role values. * * @since 1.0.0-beta.1 * * @return string[] */ protected function get_enum_roles() { global $wp_roles; return array_keys( $wp_roles->roles ); } /** * Get the item schema base. * * @since 1.0.0-beta.27 * * @return array */ protected function get_item_schema_base() { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => $this->resource_name, 'type' => 'object', 'properties' => array( 'id' => array( 'description' => __( 'Unique identifier for the user.', 'lifterlms' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), 'username' => array( 'description' => __( 'Login name for the user.', 'lifterlms' ), 'type' => 'string', 'context' => array( 'edit' ), 'arg_options' => array( 'sanitize_callback' => array( $this, 'sanitize_username' ), ), ), 'name' => array( 'description' => __( 'Display name for the user.', 'lifterlms' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'first_name' => array( 'description' => __( 'First name for the user.', 'lifterlms' ), 'type' => 'string', 'context' => array( 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'last_name' => array( 'description' => __( 'Last name for the user.', 'lifterlms' ), 'type' => 'string', 'context' => array( 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'email' => array( 'description' => __( 'The email address for the user.', 'lifterlms' ), 'type' => 'string', 'format' => 'email', 'context' => array( 'edit' ), 'required' => true, ), 'url' => array( 'description' => __( 'URL of the user.', 'lifterlms' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view', 'edit' ), ), 'description' => array( 'description' => __( 'Description of the user.', 'lifterlms' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), 'nickname' => array( 'description' => __( 'The nickname for the user.', 'lifterlms' ), 'type' => 'string', 'context' => array( 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'registered_date' => array( 'description' => __( 'Registration date for the user.', 'lifterlms' ), 'type' => 'string', 'format' => 'date-time', 'context' => array( 'edit' ), 'readonly' => true, ), 'roles' => array( 'description' => __( 'Roles assigned to the user.', 'lifterlms' ), 'type' => 'array', 'items' => array( 'type' => 'string', 'enum' => $this->get_enum_roles(), ), 'context' => array( 'edit' ), 'default' => array( 'student' ), ), 'password' => array( 'description' => __( 'Password for the user (never included).', 'lifterlms' ), 'type' => 'string', 'context' => array(), // Password is never displayed. 'arg_options' => array( 'sanitize_callback' => array( $this, 'sanitize_password' ), ), ), 'billing_address_1' => array( 'description' => __( 'User address line 1.', 'lifterlms' ), 'type' => 'string', 'context' => array( 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'billing_address_2' => array( 'description' => __( 'User address line 2.', 'lifterlms' ), 'type' => 'string', 'context' => array( 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'billing_city' => array( 'description' => __( 'User address city name.', 'lifterlms' ), 'type' => 'string', 'context' => array( 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'billing_state' => array( 'description' => __( 'User address ISO code for the state, province, or district.', 'lifterlms' ), 'type' => 'string', 'context' => array( 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'billing_postcode' => array( 'description' => __( 'User address postal code.', 'lifterlms' ), 'type' => 'string', 'context' => array( 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), 'billing_country' => array( 'description' => __( 'User address ISO code for the country.', 'lifterlms' ), 'type' => 'string', 'context' => array( 'edit' ), 'arg_options' => array( 'sanitize_callback' => 'sanitize_text_field', ), ), ), ); if ( get_option( 'show_avatars' ) ) { $avatar_properties = array(); foreach ( rest_get_avatar_sizes() as $size ) { $avatar_properties[ $size ] = array( // Translators: %d = avatar image size in pixels. 'description' => sprintf( __( 'Avatar URL with image size of %d pixels.', 'lifterlms' ), $size ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view', 'edit' ), ); } $schema['properties']['avatar_urls'] = array( 'description' => __( 'Avatar URLs for the user.', 'lifterlms' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), 'readonly' => true, 'properties' => $avatar_properties, ); } return $schema; } /** * Retrieve a query object based on arguments from a `get_items()` (collection) request * * @since 1.0.0-beta.1 * @since 1.0.0-beta.12 Parse `search` and `search_columns` args. * * @param array $prepared Array of collection arguments. * @param WP_REST_Request $request Request object. * @return WP_User_Query */ protected function get_objects_query( $prepared, $request ) { if ( 'id' === $prepared['orderby'] ) { $prepared['orderby'] = 'ID'; } elseif ( 'registered_date' === $prepared['orderby'] ) { $prepared['orderby'] = 'registered'; } $args = array( 'paged' => $prepared['page'], 'number' => $prepared['per_page'], 'order' => strtoupper( $prepared['order'] ), 'orderby' => $prepared['orderby'], ); if ( ! empty( $prepared['roles'] ) ) { $args['role__in'] = $prepared['roles']; } if ( ! empty( $prepared['include'] ) ) { $args['include'] = $prepared['include']; } if ( ! empty( $prepared['exclude'] ) ) { $args['exclude'] = $prepared['exclude']; } if ( ! empty( $prepared['search'] ) ) { $args['search'] = $prepared['search']; } if ( ! empty( $prepared['search_columns'] ) ) { $args['search_columns'] = $prepared['search_columns']; } return new WP_User_Query( $args ); } /** * Retrieve an array of objects from the result of `$this->get_objects_query()` * * @since 1.0.0-beta.1 * * @param obj $query Objects query result. * @return WP_User[] */ protected function get_objects_from_query( $query ) { return $query->get_results(); } /** * Retrieve pagination information from an objects query. * * @since 1.0.0-beta.1 * * @param WP_User_Query $query Objects query result returned by {@see LLMS_REST_Users_Controller::get_objects_query()}. * @param array $prepared Array of collection arguments. * @param WP_REST_Request $request Request object. * @return array { * Array of pagination information. * * @type int $current_page Current page number. * @type int $total_results Total number of results. * @type int $total_pages Total number of results pages. * } */ protected function get_pagination_data_from_query( $query, $prepared, $request ) { $current_page = absint( $prepared['page'] ); $total_results = $query->get_total(); $total_pages = absint( ceil( $total_results / $prepared['per_page'] ) ); return compact( 'current_page', 'total_results', 'total_pages' ); } /** * Map request keys to database keys for insertion * * Array keys are the request fields (as defined in the schema) and * array values are the database fields. * * @since 1.0.0-beta.1 * @since 1.0.0-beta.11 Correctly map request's `billing_postcode` param to `billing_zip` meta. * * @return array */ protected function map_schema_to_database() { $map = parent::map_schema_to_database(); $map['username'] = 'user_login'; $map['password'] = 'user_pass'; $map['name'] = 'display_name'; $map['email'] = 'user_email'; $map['url'] = 'user_url'; $map['registered_date'] = 'user_registered'; $map['billing_postcode'] = 'billing_zip'; // Not inserted/read via database calls. unset( $map['roles'], $map['avatar_urls'] ); return $map; } /** * Prepare request arguments for a database insert/update * * @since 1.0.0-beta.1 * * @param WP_Rest_Request $request Request object. * @return array */ protected function prepare_item_for_database( $request ) { $prepared = parent::prepare_item_for_database( $request ); // If we're creating a new item, maybe add some defaults. if ( empty( $prepared['id'] ) ) { // Pass an explicit false to `wp_insert_user()`. $prepared['role'] = false; if ( empty( $prepared['user_pass'] ) ) { $prepared['user_pass'] = wp_generate_password( 22 ); } if ( empty( $prepared['user_login'] ) ) { $prepared['user_login'] = LLMS_Person_Handler::generate_username( $prepared['user_email'] ); } } return $prepared; } /** * Prepare an object for response * * @since 1.0.0-beta.1 * @since 1.0.0-beta.14 Only add remapped keys to the response when the schema key is present in the expected response fields array. * * @param LLMS_Abstract_User_Data $object User object. * @param WP_REST_Request $request Request object. * @return array */ protected function prepare_object_for_response( $object, $request ) { $prepared = array(); $map = array_flip( $this->map_schema_to_database() ); $fields = $this->get_fields_for_response( $request ); // Write Only. unset( $map['user_pass'] ); foreach ( $map as $db_key => $schema_key ) { if ( in_array( $schema_key, $fields, true ) ) { $prepared[ $schema_key ] = $object->get( $db_key ); } } if ( in_array( 'roles', $fields, true ) ) { $prepared['roles'] = $object->get_user()->roles; } if ( in_array( 'avatar_urls', $fields, true ) ) { $prepared['avatar_urls'] = rest_get_avatar_urls( $object->get( 'user_email' ) ); } return $prepared; } /** * Validate a username is valid and allowed * * @since 1.0.0-beta.1 * * @param string $value User-submitted username. * @param WP_REST_Request $request Request object. * @param string $param Parameter name. * @return WP_Error|string Sanitized username if valid or error object. */ public function sanitize_password( $value, $request, $param ) { $password = (string) $value; if ( false !== strpos( $password, '\\' ) ) { return llms_rest_bad_request_error( __( 'Passwords cannot contain the "\\" character.', 'lifterlms' ) ); } // @todo: Should validate against password strength too, maybe? return $password; } /** * Validate a username is valid and allowed * * @since 1.0.0-beta.1 * * @param string $value User-submitted username. * @param WP_REST_Request $request Request object. * @param string $param Parameter name. * @return WP_Error|string Sanitized username if valid or error object. */ public function sanitize_username( $value, $request, $param ) { $username = (string) $value; if ( ! validate_username( $username ) ) { return llms_rest_bad_request_error( __( 'Username contains invalid characters.', 'lifterlms' ) ); } /** * Filter defined in WP Core. * * @link https://developer.wordpress.org/reference/hooks/illegal_user_logins/ * * @param array $illegal_logins Array of banned usernames. */ $illegal_logins = (array) apply_filters( 'illegal_user_logins', array() ); if ( in_array( strtolower( $username ), array_map( 'strtolower', $illegal_logins ), true ) ) { return llms_rest_bad_request_error( __( 'Username is not allowed.', 'lifterlms' ) ); } return $username; } /** * Updates additional information not handled by WP Core insert/update user functions * * @since 1.0.0-beta.1 * @since 1.0.0-beta.10 Fixed setting roles instead of appending them. * @since 1.0.0-beta.11 Made sure to set user's meta with the correct db key. * * @param int $object_id WP User id. * @param array $prepared Prepared item data. * @param WP_REST_Request $request Request object. * @return LLMS_Abstract_User_Data|WP_error */ protected function update_additional_data( $object_id, $prepared, $request ) { $object = $this->get_object( $object_id ); if ( is_wp_error( $object ) ) { return $object; } $metas = array( 'billing_address_1', 'billing_address_2', 'billing_city', 'billing_state', 'billing_postcode', 'billing_country', ); $map = $this->map_schema_to_database(); foreach ( $metas as $meta ) { if ( ! empty( $map[ $meta ] ) && ! empty( $prepared[ $map[ $meta ] ] ) ) { $object->set( $map[ $meta ], $prepared[ $map[ $meta ] ] ); } } if ( ! empty( $request['roles'] ) ) { $user = $object->get_user(); $user->set_role( '' ); foreach ( $request['roles'] as $role ) { $user->add_role( $role ); } } return $object; } /** * Update item * * @since 1.0.0-beta.1 * * @param WP_REST_Request $request Request object. * @return WP_REST_Response|WP_Error Response object or `WP_Error` on failure. */ public function update_item( $request ) { $object = $this->get_object( $request['id'] ); if ( is_wp_error( $object ) ) { return $object; } // Ensure we're not trying to update the email to an email that already exists. $owner_id = email_exists( $request['email'] ); if ( $owner_id && $owner_id !== $request['id'] ) { return llms_rest_bad_request_error( __( 'Invalid email address.', 'lifterlms' ) ); } // Cannot change a username. if ( ! empty( $request['username'] ) && $request['username'] !== $object->get( 'user_login' ) ) { return llms_rest_bad_request_error( __( 'Username is not editable.', 'lifterlms' ) ); } return parent::update_item( $request ); } /** * Update the object in the database with prepared data * * @since 1.0.0-beta.1 * * @param array $prepared Prepared item data. * @param WP_REST_Request $request Request object. * @return obj Object Instance of object from `$this->get_object()`. */ protected function update_object( $prepared, $request ) { $prepared['ID'] = $prepared['id']; $object_id = wp_update_user( $prepared );
Expand full source code Collapse full source code View on GitHub
Methods Methods
- check_read_object_permissions — Determine if the current user can view the object
- check_roles_permissions — Determine if the current user has permissions to manage the role(s) present in a request
- create_object — Insert the prepared data into the database
- delete_object — Delete the object
- get_collection_params — Retrieves the query params for the objects collection
- get_delete_item_args — Retrieve arguments for deleting a resource
- get_enum_roles — Retrieve an array of allowed user role values
- get_item_schema — Get the item schema
- get_objects_from_query — Retrieve an array of objects from the result of `$this->get_objects_query()`
- get_objects_query — Retrieve a query object based on arguments from a `get_items()` (collection) request
- get_pagination_data_from_query — Retrieve pagination information from an objects query.
- map_schema_to_database — Map request keys to database keys for insertion
- prepare_item_for_database — Prepare request arguments for a database insert/update
- prepare_object_for_response — Prepare an object for response
- sanitize_password — Validate a username is valid and allowed
- sanitize_username — Validate a username is valid and allowed
- update_additional_data — Updates additional information not handled by WP Core insert/update user functions
- update_item — Update item
- update_object — Update the object in the database with prepared data
Changelog Changelog
Version | Description |
---|---|
1.0.0-beta.7 | Added check_read_object_permissions() method override. |
1.0.0-beta.14 | Only add remapped keys to the response when the schema key is present in the expected response fields array. |
1.0.0-beta.12 | Add search and search_columns collection filtering. |
1.0.0-beta.11 | Correctly map request's billing_postcode param to billing_zip meta. |
1.0.0-beta.10 | Fixed setting roles instead of appending them when updating user. |
1.0.0-beta.1 | Introduced. |