Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
4 changes: 3 additions & 1 deletion includes/abilities-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
* @package WordPress
* @subpackage Abilities API
* @since 0.1.0
*
* phpcs:disable WordPress.NamingConventions.PrefixAllGlobals
*/

declare( strict_types = 1 );
Expand Down Expand Up @@ -34,7 +36,7 @@
* input_schema?: array<string,mixed>,
* output_schema?: array<string,mixed>,
* execute_callback?: callable( array<string,mixed> $input): (mixed|\WP_Error),
* permission_callback?: callable( ?array<string,mixed> $input ): bool,
* permission_callback?: callable( array<string,mixed> $input ): (bool|\WP_Error),
* meta?: array<string,mixed>,
* ability_class?: class-string<\WP_Ability>,
* ...<string, mixed>
Expand Down
2 changes: 1 addition & 1 deletion includes/abilities-api/class-wp-abilities-registry.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ final class WP_Abilities_Registry {
* input_schema?: array<string,mixed>,
* output_schema?: array<string,mixed>,
* execute_callback?: callable( array<string,mixed> $input): (mixed|\WP_Error),
* permission_callback?: ?callable( ?array<string,mixed> $input ): bool,
* permission_callback?: ?callable( array<string,mixed> $input ): (bool|\WP_Error),
* meta?: array<string,mixed>,
* ability_class?: class-string<\WP_Ability>,
* ...<string, mixed>
Expand Down
69 changes: 59 additions & 10 deletions includes/abilities-api/class-wp-ability.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,15 @@ class WP_Ability {
* The ability execute callback.
*
* @since 0.1.0
* @var callable
* @var callable( array<string,mixed> $input): (mixed|\WP_Error)
*/
protected $execute_callback;

/**
* The optional ability permission callback.
*
* @since 0.1.0
* @var ?callable
* @var ?callable( array<string,mixed> $input ): (bool|\WP_Error)
*/
protected $permission_callback = null;

Expand Down Expand Up @@ -107,7 +107,7 @@ class WP_Ability {
* input_schema?: array<string,mixed>,
* output_schema?: array<string,mixed>,
* execute_callback: callable( array<string,mixed> $input): (mixed|\WP_Error),
* permission_callback?: ?callable( ?array<string,mixed> $input ): bool,
* permission_callback?: ?callable( array<string,mixed> $input ): (bool|\WP_Error),
* meta?: array<string,mixed>,
* ...<string, mixed>,
* } $properties
Expand Down Expand Up @@ -177,7 +177,16 @@ public function get_description(): string {
* @return array<string,mixed> The input schema for the ability.
*/
public function get_input_schema(): array {
return $this->input_schema;
/**
* Filters the input schema for a specific ability.
*
* @since 0.1.0
*
* @param array<string,mixed> $input_schema The input schema.
* @param string $ability_name The ability name.
* @return array<string,mixed> The filtered input schema.
*/
return (array) apply_filters( 'ability_input_schema', $this->input_schema, $this->name );
}

/**
Expand All @@ -188,7 +197,16 @@ public function get_input_schema(): array {
* @return array<string,mixed> The output schema for the ability.
*/
public function get_output_schema(): array {
return $this->output_schema;
/**
* Filters the output schema for a specific ability.
*
* @since 0.1.0
*
* @param array<string,mixed> $output_schema The output schema.
* @param string $ability_name The ability name.
* @return array<string,mixed> The filtered output schema.
*/
return (array) apply_filters( 'ability_output_schema', $this->output_schema, $this->name );
}

/**
Expand Down Expand Up @@ -240,19 +258,34 @@ protected function validate_input( array $input = array() ) {
* @since 0.1.0
*
* @param array<string,mixed> $input Optional. The input data for permission checking.
* @return true|\WP_Error Whether the ability has the necessary permission.
* @return bool|\WP_Error Whether the ability has the necessary permission.
*/
public function has_permission( array $input = array() ) {
$is_valid = $this->validate_input( $input );
if ( is_wp_error( $is_valid ) ) {
return $is_valid;
}

if ( ! is_callable( $this->permission_callback ) ) {
return true;
$permission_result = true;
if ( is_callable( $this->permission_callback ) ) {
$permission_result = call_user_func( $this->permission_callback, $input );
}

return call_user_func( $this->permission_callback, $input );
/**
* Filters the permission result for a specific ability.
*
* Allows plugins to override or short-circuit the permission check for an ability.
* The filter receives the current permission result (bool), the
* ability name, and the input provided for the permission check.
*
* @since 0.1.0
*
* @param bool|\WP_Error $permission_result The current permission result.
* @param string $ability_name The ability name.
* @param array<string,mixed> $input The input for the ability.
* @return bool|\WP_Error The filtered permission result.
*/
return apply_filters( 'ability_permission_result', $permission_result, $this->name, $input );
}

/**
Expand All @@ -272,7 +305,23 @@ protected function do_execute( array $input ) {
);
}

return call_user_func( $this->execute_callback, $input );
$result = call_user_func( $this->execute_callback, $input );

/**
* Filters the raw result returned by the ability execute callback.
*
* Allows plugins to modify or short-circuit the execute callback result
* before output validation occurs. The filter receives the raw result,
* the ability name and the input provided to the ability.
*
* @since 0.1.0
*
* @param mixed|\WP_Error $result The raw result from the execute callback (can be any type or \WP_Error).
* @param string $ability_name The ability name.
* @param array<string,mixed> $input The input passed to the ability.
* @return mixed|\WP_Error The filtered result.
*/
return apply_filters( 'ability_execute_result', $result, $this->name, $input );
}

/**
Expand Down
5 changes: 3 additions & 2 deletions phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
</rule>
<rule ref="WordPress-Extra">
<exclude name="WordPress.WP.I18n.MissingArgDomain" />
<exclude name="WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound" />

<!-- Needed to typehint, see: https://github.com/WordPress/WordPress-Coding-Standards/issues/403 -->
<exclude name="Generic.Commenting.DocComment.MissingShort" />
</rule>
Expand Down Expand Up @@ -196,10 +196,11 @@
<properties>
<property name="prefixes" type="array">
<element value="abilities_api_" /><!-- Hook prefix -->
<element value="ability_" /><!-- Hook prefix -->
<element value="WP_Ability" />
<element value="WP_Abilities" />
<element value="WP_REST_Abilities" />
<element value="WP_ABILITIES_API" /> <!-- Constant -->
<element value="WP_ABILITIES_API" /><!-- Constant -->
</property>
</properties>
</rule>
Expand Down
1 change: 0 additions & 1 deletion tests/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
*
* @package abilities-api
*
* phpcs:disable WordPress.NamingConventions.PrefixAllGlobals
* phpcs:disable WordPressVIPMinimum.Files.IncludingFile.UsingVariable
*/

Expand Down
2 changes: 2 additions & 0 deletions tests/unit/abilities-api/wpAbilitiesRegistry.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<?php declare( strict_types=1 );

/**
* Tests for the abilities registry functionality.
*
* @covers WP_Abilities_Registry
*
* @group abilities-api
Expand Down
194 changes: 194 additions & 0 deletions tests/unit/abilities-api/wpAbilityFilters.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
<?php declare( strict_types=1 );

/**
* Tests for filter integration on WP_Ability.
*
* @covers WP_Ability
*
* @group abilities-api
*/
class Tests_Abilities_API_WpAbility_Filters extends WP_UnitTestCase {

public static $ability_name = 'test/filter-ability';
public static $ability_properties = array();

/**
* Set up each test method.
*/
public function set_up(): void {
parent::set_up();

self::$ability_properties = array(
'label' => 'Filter ability',
'description' => 'Ability used to test filters.',
'input_schema' => array(
'type' => 'object',
'properties' => array(
'value' => array(
'type' => 'string',
),
),
'additionalProperties' => false,
),
'output_schema' => array(
'type' => 'string',
),
'execute_callback' => static function ( array $input ) {
return $input['value'] ?? '';
},
'permission_callback' => static function (): bool {
return true;
},
);
}

/**
* Input schema filter should be applied and receive the ability name.
*
* @covers WP_Ability::get_input_schema
*/
public function test_input_schema_filter_applied(): void {
$ability = new WP_Ability( self::$ability_name, self::$ability_properties );

$filter_cb = static function ( $schema, $ability_name ) {
if ( 'test/filter-ability' !== $ability_name ) {
return $schema;
}

$schema['properties']['extra'] = array( 'type' => 'number' );
return $schema;
};

add_filter( 'ability_input_schema', $filter_cb, 10, 2 );

$filtered = $ability->get_input_schema();

remove_filter( 'ability_input_schema', $filter_cb );

$this->assertArrayHasKey( 'extra', $filtered['properties'] );
$this->assertSame( array( 'type' => 'number' ), $filtered['properties']['extra'] );
}

/**
* Output schema getter should cast non-array filter returns to array.
*
* @covers WP_Ability::get_output_schema
*/
public function test_output_schema_filter_non_array_returns_empty(): void {
$ability = new WP_Ability( self::$ability_name, self::$ability_properties );

$filter_cb = static function ( $schema ) {
return 'not an array';
};

add_filter( 'ability_output_schema', $filter_cb );

$output = $ability->get_output_schema();

remove_filter( 'ability_output_schema', $filter_cb );

$this->assertIsArray( $output );
$this->assertSame( array( 'not an array' ), $output );
}

/**
* The ability_permission_result filter can override permission to false.
*
* @covers WP_Ability::has_permission
*/
public function test_permission_filter_can_override_false(): void {
$ability = new WP_Ability( self::$ability_name, self::$ability_properties );

$filter_cb = static function ( $permission, $ability_name ) {
if ( 'test/filter-ability' !== $ability_name ) {
return $permission;
}

return false;
};

add_filter( 'ability_permission_result', $filter_cb, 10, 2 );

$result = $ability->has_permission();

remove_filter( 'ability_permission_result', $filter_cb );

$this->assertFalse( $result );
}

/**
* The ability_permission_result filter can return a WP_Error and it should be propagated.
*
* @covers WP_Ability::has_permission
*/
public function test_permission_filter_can_return_wp_error(): void {
$ability = new WP_Ability( self::$ability_name, self::$ability_properties );

$filter_cb = static function ( $permission, $ability_name ) {
if ( 'test/filter-ability' !== $ability_name ) {
return $permission;
}

return new \WP_Error( 'test_error', 'Denied by filter' );
};

add_filter( 'ability_permission_result', $filter_cb, 10, 2 );

$result = $ability->has_permission();

remove_filter( 'ability_permission_result', $filter_cb );

$this->assertTrue( is_wp_error( $result ) );
}

/**
* The ability_execute_result filter should be applied to the result returned by execute().
*
* @covers WP_Ability::execute
*/
public function test_execute_result_filter_can_modify_result(): void {
$ability = new WP_Ability( self::$ability_name, self::$ability_properties );

$filter_cb = static function ( $result, $ability_name ) {
if ( 'test/filter-ability' !== $ability_name ) {
return $result;
}

return 'modified-' . $result;
};

add_filter( 'ability_execute_result', $filter_cb, 10, 2 );

$output = $ability->execute( array( 'value' => 'ok' ) );

remove_filter( 'ability_execute_result', $filter_cb );

$this->assertSame( 'modified-ok', $output );
}

/**
* The ability_execute_result filter can replace the execute() result with a WP_Error.
*
* @covers WP_Ability::execute
*/
public function test_execute_result_filter_can_return_wp_error(): void {
$ability = new WP_Ability( self::$ability_name, self::$ability_properties );

$filter_cb = static function ( $result, $ability_name ) {
if ( 'test/filter-ability' !== $ability_name ) {
return $result;
}

return new \WP_Error( 'filtered_error', 'Filtered out' );
};

add_filter( 'ability_execute_result', $filter_cb, 10, 2 );

$output = $ability->execute( array( 'value' => 'ok' ) );

remove_filter( 'ability_execute_result', $filter_cb );

$this->assertTrue( is_wp_error( $output ) );
$this->assertSame( 'filtered_error', $output->get_error_code() );
}
}
2 changes: 2 additions & 0 deletions tests/unit/abilities-api/wpRegisterAbility.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ protected function do_execute( array $input ) {
}

/**
* Tests for registering, unregistering and retrieving abilities.
*
* @covers wp_register_ability
* @covers wp_unregister_ability
* @covers wp_get_ability
Expand Down
Loading
Loading