From 75e9e80f3293f41d72102e86771aa6cba7544978 Mon Sep 17 00:00:00 2001 From: Corey McKrill <916023+coreymckrill@users.noreply.github.com> Date: Tue, 29 Mar 2022 13:17:35 -0700 Subject: [PATCH] Add a REST API endpoint for wporg users (#184) Allows API access to public wporg user data, given a user's `slug`. Fixes #182 --- mu-plugins/loader.php | 1 + .../class-wporg-rest-users-controller.php | 217 ++++++++++++++++++ mu-plugins/rest-api/index.php | 41 ++++ 3 files changed, 259 insertions(+) create mode 100644 mu-plugins/rest-api/endpoints/class-wporg-rest-users-controller.php create mode 100644 mu-plugins/rest-api/index.php diff --git a/mu-plugins/loader.php b/mu-plugins/loader.php index 8bb6820c6..602ed373d 100644 --- a/mu-plugins/loader.php +++ b/mu-plugins/loader.php @@ -5,5 +5,6 @@ require_once __DIR__ . '/blocks/global-header-footer/blocks.php'; require_once __DIR__ . '/global-fonts/index.php'; +require_once __DIR__ . '/rest-api/index.php'; require_once __DIR__ . '/skip-to/skip-to.php'; require_once __DIR__ . '/stream-tweaks/index.php'; diff --git a/mu-plugins/rest-api/endpoints/class-wporg-rest-users-controller.php b/mu-plugins/rest-api/endpoints/class-wporg-rest-users-controller.php new file mode 100644 index 000000000..d8e70b1b3 --- /dev/null +++ b/mu-plugins/rest-api/endpoints/class-wporg-rest-users-controller.php @@ -0,0 +1,217 @@ +namespace = 'wporg/v1'; + } + + /** + * Registers the routes for users. + * + * At this time, this endpoint is exclusively read-only. Other routes from the parent class have been omitted. + * + * @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_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\w-]+)', + array( + 'args' => array( + 'slug' => array( + 'description' => __( 'A unique alphanumeric identifier for the user.', 'wporg' ), + 'type' => 'string', + ), + ), + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Permissions check for getting all users. + * + * @param \WP_REST_Request $request Full details about the request. + * + * @return true|\WP_Error True if the request has read access, otherwise WP_Error object. + */ + public function get_items_permissions_check( $request ) { + $check = parent::get_items_permissions_check( $request ); + if ( is_wp_error( $check ) ) { + return $check; + } + + if ( empty( $request['slug'] ) ) { + return new \WP_Error( + 'rest_invalid request', + __( 'You must use the slug parameter for requests to this endpoint.', 'wporg' ), + array( 'status' => 400 ) + ); + } + + return true; + } + + /** + * Get the user, if the slug is valid. + * + * @param string $slug Supplied slug. + * + * @return \WP_User|\WP_Error True if slug is valid, WP_Error otherwise. + */ + protected function get_user_by_slug( $slug ) { + $error = new \WP_Error( + 'rest_user_invalid_slug', + __( 'Invalid user slug.', 'wporg' ), + array( 'status' => 404 ) + ); + + if ( mb_strlen( $slug ) > 50 || ! sanitize_title( $slug ) ) { + return $error; + } + + $user = get_user_by( 'slug', $slug ); + if ( empty( $user ) || ! $user->exists() ) { + return $error; + } + + return $user; + } + + /** + * Checks if a given request has access to read a user. + * + * Modified from the parent method to use slug instead of id and remove capability checks that necessitate + * blog membership. + * + * @param \WP_REST_Request $request Full details about the request. + * + * @return true|\WP_Error True if the request has read access for the item, otherwise WP_Error object. + */ + public function get_item_permissions_check( $request ) { + $user = $this->get_user_by_slug( $request['slug'] ); + if ( is_wp_error( $user ) ) { + return $user; + } + + return true; + } + + /** + * Retrieves a single user. + * + * Modified from the parent method to use slug instead of id. + * + * @param \WP_REST_Request $request Full details about the request. + * + * @return \WP_REST_Response|\WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_item( $request ) { + $user = $this->get_user_by_slug( $request['slug'] ); + if ( is_wp_error( $user ) ) { + return $user; + } + + $user = $this->prepare_item_for_response( $user, $request ); + $response = rest_ensure_response( $user ); + + return $response; + } + + /** + * Prepares links for the user request. + * + * @param \WP_User $user User object. + * + * @return array Links for the given user. + */ + protected function prepare_links( $user ) { + $links = parent::prepare_links( $user ); + + // Prepending / is to avoid replacing the 1 in wporg/v1 if the user ID is 1 :D + $links['self']['href'] = str_replace( '/' . $user->ID, '/' . $user->user_nicename, $links['self']['href'] ); + + $links['collection']['href'] = add_query_arg( + 'slug', + $user->user_nicename, + $links['collection']['href'] + ); + + return $links; + } + + /** + * Retrieves the query params for collections. + * + * @return array Collection parameters. + */ + public function get_collection_params() { + $allowed_params = array( + 'context' => '', + 'page' => '', + 'per_page' => '', + 'order' => '', + 'orderby' => '', + 'slug' => '', + ); + + $query_params = array_intersect_key( parent::get_collection_params(), $allowed_params ); + + if ( isset( $query_params['orderby']['enum'] ) ) { + $allowed_orderby = array( 'id', 'name', 'slug', 'include_slugs' ); + $query_params['orderby']['enum'] = array_intersect( $query_params['orderby']['enum'], $allowed_orderby ); + } + + return $query_params; + } + + /** + * Retrieves the magical context param. + * + * Prevents usage of contexts (such as edit) that potentially reveal users' sensitive account information. + * + * @param array $args Optional. Additional arguments for context parameter. Default empty array. + * + * @return array Context parameter details. + */ + public function get_context_param( $args = array() ) { + $context = parent::get_context_param( $args ); + $allowed_contexts = array( 'view', 'embed' ); + + $context['enum'] = array_intersect( $context['enum'], $allowed_contexts ); + + return $context; + } +} diff --git a/mu-plugins/rest-api/index.php b/mu-plugins/rest-api/index.php new file mode 100644 index 000000000..6cbdf8838 --- /dev/null +++ b/mu-plugins/rest-api/index.php @@ -0,0 +1,41 @@ +register_routes(); +} + +/** + * Tweak the user query to allow for getting users who aren't blog members. + * + * @param array $prepared_args + * @param \WP_REST_Request $request + * + * @return array + */ +function modify_user_query_parameters( $prepared_args, $request ) { + // Only for this specific endpoint. + if ( '/wporg/v1' !== substr( $request->get_route(), 0, 9 ) ) { + return $prepared_args; + } + + $prepared_args['blog_id'] = 0; // Avoid check for blog membership. + unset( $prepared_args['has_published_posts'] ); // Avoid another check for blog membership. + + return $prepared_args; +}