Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions abilities-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,8 @@
* Then the public access functions that users can use to interact with the abilities.
*/
require_once WP_ABILITIES_API_DIR . 'src/abilities-api.php';

/**
* Initialize REST API controllers.
*/
require_once WP_ABILITIES_API_DIR . 'src/rest/class-wp-rest-abilities-init.php';
1 change: 1 addition & 0 deletions phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@
<element value="WP_Ability" />
<element value="WP_Abilities" />
<element value="WP_ABILITIES_API" />
<element value="WP_REST_Abilities" />
</property>
</properties>
</rule>
Expand Down
11 changes: 11 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,14 @@ parameters:
analyseAndScan:
- node_modules (?)

# Ignore specific errors
ignoreErrors:
# WP_REST_Request is not actually a generic class in WordPress core.
# PHPStan's WordPress stubs appear to define it as generic for type checking,
# but WordPress itself doesn't use generics. This seems to be an incompatibility
# between static analysis tools and WordPress's actual implementation.
-
message: '#has parameter \$request with generic class WP_REST_Request but does not specify its types#'
paths:
- src/rest/*.php

43 changes: 43 additions & 0 deletions src/rest/class-wp-rest-abilities-init.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php
/**
* REST API initialization for Abilities API.
*
* @package abilities-api
* @since 0.1.0
*/

declare( strict_types = 1 );

/**
* REST API: WP_REST_Abilities_Init class
*
* @package WordPress
* @subpackage REST_API
* @since 0.1.0
*/

/**
* Handles initialization of Abilities REST API endpoints.
*
* @since 0.1.0
*/
class WP_REST_Abilities_Init {

/**
* Registers the REST API routes for abilities.
*
* @since 0.1.0
*/

Check failure on line 30 in src/rest/class-wp-rest-abilities-init.php

View workflow job for this annotation

GitHub Actions / Run PHPCS coding standards checks

Missing @return tag in function comment
public static function register_routes(): void {
require_once __DIR__ . '/class-wp-rest-abilities-run-controller.php';
require_once __DIR__ . '/class-wp-rest-abilities-list-controller.php';

$run_controller = new WP_REST_Abilities_Run_Controller();
$run_controller->register_routes();

$list_controller = new WP_REST_Abilities_List_Controller();
$list_controller->register_routes();
}
}

add_action( 'rest_api_init', array( 'WP_REST_Abilities_Init', 'register_routes' ) );
315 changes: 315 additions & 0 deletions src/rest/class-wp-rest-abilities-list-controller.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,315 @@
<?php
/**
* REST API list controller for Abilities API.
*
* @package abilities-api
* @since 0.1.0
*/

declare( strict_types = 1 );

/**
* REST API: WP_REST_Abilities_List_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 0.1.0
*/

/**
* Core controller used to access abilities via the REST API.
*
* @since 0.1.0
*
* @see WP_REST_Controller
*/
class WP_REST_Abilities_List_Controller extends WP_REST_Controller {

/**
* Default number of items per page for pagination.
*
* @since 0.1.0
* @var int
*/
public const DEFAULT_PER_PAGE = 50;

/**
* REST API namespace.
*
* @since 0.1.0
* @var string
*/
protected $namespace = 'wp/v2';

/**
* REST API base route.
*
* @since 0.1.0
* @var string
*/
protected $rest_base = 'abilities';

/**
* Registers the routes for abilities.
*
* @since 0.1.0
*
* @see register_rest_route()
*/
public function register_routes(): void {
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_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);

register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<name>[a-zA-Z0-9\-\/]+)',
array(
'args' => array(
'name' => array(
'description' => __( 'Unique identifier for the ability.', 'abilities-api' ),
'type' => 'string',
'pattern' => '^[a-zA-Z0-9\-\/]+$',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_permissions_check' ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}

/**
* Retrieves all abilities.
*
* @since 0.1.0
*
* @param \WP_REST_Request $request Full details about the request.
* @return \WP_REST_Response Response object on success.
*/
public function get_items( \WP_REST_Request $request ): \WP_REST_Response {
// TODO: Add HEAD method support for performance optimization.
// Should return early with empty body but include X-WP-Total and X-WP-TotalPages headers.
// See: https://github.com/WordPress/wordpress-develop/blob/trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php#L316-L318

$abilities = wp_get_abilities();

// Handle pagination with explicit defaults.
$page = isset( $request['page'] ) ? $request['page'] : 1;
$per_page = isset( $request['per_page'] ) ? $request['per_page'] : self::DEFAULT_PER_PAGE;
$offset = ( $page - 1 ) * $per_page;

$total_abilities = count( $abilities );
$max_pages = ceil( $total_abilities / $per_page );

$abilities = array_slice( $abilities, $offset, $per_page );

$data = array();
foreach ( $abilities as $ability ) {
$item = $this->prepare_item_for_response( $ability, $request );
$data[] = $this->prepare_response_for_collection( $item );
}

$response = rest_ensure_response( $data );

$response->header( 'X-WP-Total', (string) $total_abilities );
$response->header( 'X-WP-TotalPages', (string) $max_pages );

$request_params = $request->get_query_params();
$base = add_query_arg( urlencode_deep( $request_params ), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) );

if ( $page > 1 ) {
$prev_page = $page - 1;
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->add_link( 'prev', $prev_link );
}

if ( $page < $max_pages ) {
$next_page = $page + 1;
$next_link = add_query_arg( 'page', $next_page, $base );
$response->add_link( 'next', $next_link );
}

return $response;
}

/**
* Retrieves a specific ability.
*
* @since 0.1.0
*
* @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( \WP_REST_Request $request ) {
$ability = wp_get_ability( $request['name'] );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason not to use WP_REST_Request->get_param() (/ ->get_params() elsewhere )?

We lose the rest_request_parameter_order filter by relying on \ArrayAccess

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see both forms used in REST API controllers included in WP core, sometimes even in the same handler 😅

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gziolo I think that's a sign of tech debt, not an endorsement of pattern 😅

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can update to use get_param() 👍🏻

Will you take care of it?


if ( ! $ability ) {
return new \WP_Error(
'rest_ability_not_found',
__( 'Ability not found.', 'abilities-api' ),
array( 'status' => 404 )
);
}

$data = $this->prepare_item_for_response( $ability, $request );
return rest_ensure_response( $data );
}

/**
* Checks if a given request has access to read abilities.
*
* @since 0.1.0
*
* @param \WP_REST_Request $request Full details about the request.
* @return boolean True if the request has read access.
*/
public function get_permissions_check( \WP_REST_Request $request ): bool {
return current_user_can( 'read' );
}

/**
* Prepares an ability for response.
*
* @since 0.1.0
*
* @param \WP_Ability $ability The ability object.
* @param \WP_REST_Request $request Request object.
* @return \WP_REST_Response Response object.
*/
public function prepare_item_for_response( \WP_Ability $ability, \WP_REST_Request $request ): \WP_REST_Response {
$data = array(
'name' => $ability->get_name(),
'label' => $ability->get_label(),
'description' => $ability->get_description(),
'input_schema' => $ability->get_input_schema(),
'output_schema' => $ability->get_output_schema(),
'meta' => $ability->get_meta(),
);

$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );

$response = rest_ensure_response( $data );

$fields = $this->get_fields_for_response( $request );
if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
$links = array(
'self' => array(
'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $ability->get_name() ) ),
),
'collection' => array(
'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
),
);

$links['run'] = array(
'href' => rest_url( sprintf( '%s/%s/%s/run', $this->namespace, $this->rest_base, $ability->get_name() ) ),
);

$response->add_links( $links );
}

return $response;
}

/**
* Retrieves the ability's schema, conforming to JSON Schema.
*
* @since 0.1.0
*
* @return array<string, mixed> Item schema data.
*/
public function get_item_schema(): array {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'ability',
'type' => 'object',
'properties' => array(
'name' => array(
'description' => __( 'Unique identifier for the ability.', 'abilities-api' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'label' => array(
'description' => __( 'Display label for the ability.', 'abilities-api' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'description' => array(
'description' => __( 'Description of the ability.', 'abilities-api' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'input_schema' => array(
'description' => __( 'JSON Schema for the ability input.', 'abilities-api' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'output_schema' => array(
'description' => __( 'JSON Schema for the ability output.', 'abilities-api' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'meta' => array(
'description' => __( 'Meta information about the ability.', 'abilities-api' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
'required' => array( 'name', 'label', 'description' ),
);

return $this->add_additional_fields_schema( $schema );
}

/**
* Retrieves the query params for collections.
*
* @since 0.1.0
*
* @return array<string, mixed> Collection parameters.
*/
public function get_collection_params(): array {
return array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
'page' => array(
'description' => __( 'Current page of the collection.', 'abilities-api' ),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
),
'per_page' => array(
'description' => __( 'Maximum number of items to be returned in result set.', 'abilities-api' ),
'type' => 'integer',
'default' => self::DEFAULT_PER_PAGE,
'minimum' => 1,
'maximum' => 100,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
),
);
}
}
Loading
Loading