Skip to content

Commit

Permalink
Add a REST API endpoint for wporg users (#184)
Browse files Browse the repository at this point in the history
Allows API access to public wporg user data, given a user's `slug`.

Fixes #182
  • Loading branch information
coreymckrill authored Mar 29, 2022
1 parent 6e176c6 commit 75e9e80
Show file tree
Hide file tree
Showing 3 changed files with 259 additions and 0 deletions.
1 change: 1 addition & 0 deletions mu-plugins/loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
217 changes: 217 additions & 0 deletions mu-plugins/rest-api/endpoints/class-wporg-rest-users-controller.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
<?php

namespace WordPressdotorg\MU_Plugins\REST_API;

/**
* Users_Controller
*/
class Users_Controller extends \WP_REST_Users_Controller {
/**
* Constructor.
*/
public function __construct() {
parent::__construct();

$this->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<slug>[\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;
}
}
41 changes: 41 additions & 0 deletions mu-plugins/rest-api/index.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

namespace WordPressdotorg\MU_Plugins\REST_API;

/**
* Actions and filters.
*/
add_action( 'rest_api_init', __NAMESPACE__ . '\initialize_rest_endpoints' );
add_filter( 'rest_user_query', __NAMESPACE__ . '\modify_user_query_parameters', 10, 2 );

/**
* Turn on API endpoints.
*
* @return void
*/
function initialize_rest_endpoints() {
require_once __DIR__ . '/endpoints/class-wporg-rest-users-controller.php';

$users_controller = new Users_Controller();
$users_controller->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;
}

0 comments on commit 75e9e80

Please sign in to comment.