From 6e685d29937a3c4df766c24d3182bd6f1609980f Mon Sep 17 00:00:00 2001 From: "Rolly G. Bueno Jr." <16076280+rollybueno@users.noreply.github.com> Date: Fri, 29 Aug 2025 15:23:00 +0800 Subject: [PATCH 1/6] Documentation: Include specific nvm version needed (#33) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Documentation: Include specific nvm version needed * Update CONTRIBUTING.md --------- Co-authored-by: Greg Ziółkowski --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c453e18..c20dfb5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,8 +49,8 @@ You can use Docker and the `wp-env` tool to set up a local development environme 2. Change into the project folder and install the development dependencies: ```bash - ## If you're using NVM, make sure to use the correct Node.js version: - nvm use + ## If you're using nvm, make sure to use the correct Node.js version: + nvm install && nvm use ## Then install the NPM dependencies: npm install From 50ae03a705f04c3148a462d15976cb6e613444bf Mon Sep 17 00:00:00 2001 From: Shail Mehta Date: Sat, 30 Aug 2025 01:47:52 +0530 Subject: [PATCH 2/6] Updated Inline Documentation Order (#49) --- .../class-wp-abilities-registry.php | 16 ++++++++-------- includes/abilities-api/class-wp-ability.php | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/includes/abilities-api/class-wp-abilities-registry.php b/includes/abilities-api/class-wp-abilities-registry.php index 107e28f..e36decd 100644 --- a/includes/abilities-api/class-wp-abilities-registry.php +++ b/includes/abilities-api/class-wp-abilities-registry.php @@ -39,10 +39,10 @@ final class WP_Abilities_Registry { * * Do not use this method directly. Instead, use the `wp_register_ability()` function. * - * @see wp_register_ability() - * * @since 0.1.0 * + * @see wp_register_ability() + * * @param string $name The name of the ability. The name must be a string containing a namespace * prefix, i.e. `my-plugin/my-ability`. It can only contain lowercase * alphanumeric characters, dashes and the forward slash. @@ -175,10 +175,10 @@ public function register( string $name, array $properties = array() ): ?WP_Abili * * Do not use this method directly. Instead, use the `wp_unregister_ability()` function. * - * @see wp_unregister_ability() - * * @since 0.1.0 * + * @see wp_unregister_ability() + * * @param string $name The name of the registered ability, with its namespace. * @return ?\WP_Ability The unregistered ability instance on success, null on failure. */ @@ -204,10 +204,10 @@ public function unregister( string $name ): ?WP_Ability { * * Do not use this method directly. Instead, use the `wp_get_abilities()` function. * - * @see wp_get_abilities() - * * @since 0.1.0 * + * @see wp_get_abilities() + * * @return \WP_Ability[] The array of registered abilities. */ public function get_all_registered(): array { @@ -231,10 +231,10 @@ public function is_registered( string $name ): bool { * * Do not use this method directly. Instead, use the `wp_get_ability()` function. * - * @see wp_get_ability() - * * @since 0.1.0 * + * @see wp_get_ability() + * * @param string $name The name of the registered ability, with its namespace. * @return ?\WP_Ability The registered ability instance, or null if it is not registered. */ diff --git a/includes/abilities-api/class-wp-ability.php b/includes/abilities-api/class-wp-ability.php index 2193b06..c55eabf 100644 --- a/includes/abilities-api/class-wp-ability.php +++ b/includes/abilities-api/class-wp-ability.php @@ -92,10 +92,10 @@ class WP_Ability { * * @access private * - * @see wp_register_ability() - * * @since 0.1.0 * + * @see wp_register_ability() + * * @param string $name The name of the ability, with its namespace. * @param array $properties An associative array of properties for the ability. This should * include `label`, `description`, `input_schema`, `output_schema`, From 48c925ea99d5961bb44b1d21806e13e229499575 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 1 Sep 2025 03:56:34 -0400 Subject: [PATCH 3/6] Introduce Props Bot workflow (#50) Props Bot is a GitHub Action that processes the activity on a pull request and any linked issues. The bot attempts to associate each GitHub use account with a linked w.org one and outputs a formatted list of `Co-authored-by` trailers that can be copied and pasted into the merge commit message. This ensures that everyone who contributed to a given merge receives props for their effort, and that the list of contributors is predictably formatted for easy processing. This bot is currently in the following repositories under the WordPress organization: - https://github.com/WordPress/wordpress-develop - https://github.com/WordPress/gutenberg - https://github.com/WordPress/plugin-check - https://github.com/WordPress/performance - https://github.com/WordPress/grunt-patch-wordpress More details about the bot can be found in the [original announcement post on the Make WordPress Core blog](https://make.wordpress.org/core/2024/02/01/new-commit-message-requirements-in-git-hello-props-bot/). Co-authored-by: Jonathan Desrosiers Co-authored-by: desrosj Co-authored-by: justlevine --- .github/workflows/props-bot.yml | 92 +++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 .github/workflows/props-bot.yml diff --git a/.github/workflows/props-bot.yml b/.github/workflows/props-bot.yml new file mode 100644 index 0000000..671f399 --- /dev/null +++ b/.github/workflows/props-bot.yml @@ -0,0 +1,92 @@ +name: Props Bot + +on: + # This event runs anytime a PR is (re)opened, updated, marked ready for review, or labeled. + # GitHub does not allow filtering the `labeled` event by a specific label. + # However, the logic below will short-circuit the workflow when the `props-bot` label is not the one being added. + # Note: The pull_request_target event is used instead of pull_request because this workflow needs permission to comment + # on the pull request. Because this event grants extra permissions to `GITHUB_TOKEN`, any code changes within the PR + # should be considered untrusted. See https://securitylab.github.com/research/github-actions-preventing-pwn-requests/. + pull_request_target: + types: + - opened + - synchronize + - reopened + - labeled + - ready_for_review + # This event runs anytime a comment is added or deleted. + # You cannot filter this event for PR comments only. + # However, the logic below does short-circuit the workflow for issues. + issue_comment: + types: + - created + # This event will run everytime a new PR review is initially submitted. + pull_request_review: + types: + - submitted + # This event runs anytime a PR review comment is created or deleted. + pull_request_review_comment: + types: + - created + +# Cancels all previous workflow runs for pull requests that have not completed. +concurrency: + # The concurrency group contains the workflow name and the branch name for pull requests + # or the commit hash for any other events. + group: ${{ github.workflow }}-${{ contains( fromJSON( '["pull_request_target", "pull_request_review", "pull_request_review_comment"]' ), github.event_name ) && github.head_ref || github.sha }} + cancel-in-progress: true + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + # Compiles a list of props for a pull request. + # + # Performs the following steps: + # - Collects a list of contributor props and leaves a comment. + # - Removes the props-bot label, if necessary. + props-bot: + name: Generate a list of props + runs-on: ubuntu-24.04 + permissions: + # The action needs permission `write` permission for PRs in order to add a comment. + pull-requests: write + contents: read + timeout-minutes: 20 + # The job will run when pull requests are open, ready for review and: + # + # - A comment is added to the pull request. + # - A review is created or commented on (unless PR originates from a fork). + # - The pull request is opened, synchronized, marked ready for review, or reopened. + # - The `props-bot` label is added to the pull request. + if: | + ( + github.event_name == 'issue_comment' && github.event.issue.pull_request || + ( contains( fromJSON( '["pull_request_review", "pull_request_review_comment"]' ), github.event_name ) && ! github.event.pull_request.head.repo.fork ) || + github.event_name == 'pull_request_target' && github.event.action != 'labeled' || + 'props-bot' == github.event.label.name + ) && + ( ! github.event.pull_request.draft && github.event.pull_request.state == 'open' || ! github.event.issue.draft && github.event.issue.state == 'open' ) + + steps: + - name: Gather a list of contributors + uses: WordPress/props-bot-action@trunk + with: + format: 'git' + + - name: Remove the props-bot label + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + if: ${{ github.event.action == 'labeled' && 'props-bot' == github.event.label.name }} + with: + retries: 2 + retry-exempt-status-codes: 418 + script: | + github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: process.env.ISSUE_NUMBER, + name: 'props-bot' + }); + env: + ISSUE_NUMBER: ${{ github.event.number }} From 1b75e1d3c206d42f1aadc3f8531b04727be6192f Mon Sep 17 00:00:00 2001 From: sandipr942 <151612521+sandipr942@users.noreply.github.com> Date: Thu, 4 Sep 2025 15:40:10 +0530 Subject: [PATCH 4/6] Updated Inline Documentation Order (#57) Co-authored-by: sandipr942 --- includes/abilities-api.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/includes/abilities-api.php b/includes/abilities-api.php index 3f8ae29..f8fe863 100644 --- a/includes/abilities-api.php +++ b/includes/abilities-api.php @@ -18,10 +18,10 @@ * * Note: Do not use before the {@see 'abilities_api_init'} hook. * - * @see WP_Abilities_Registry::register() - * * @since 0.1.0 * + * @see WP_Abilities_Registry::register() + * * @param string $name The name of the ability. The name must be a string containing a namespace * prefix, i.e. `my-plugin/my-ability`. It can only contain lowercase * alphanumeric characters, dashes and the forward slash. @@ -63,10 +63,10 @@ function wp_register_ability( string $name, array $properties = array() ): ?WP_A /** * Unregisters an ability using Abilities API. * - * @see WP_Abilities_Registry::unregister() - * * @since 0.1.0 * + * @see WP_Abilities_Registry::unregister() + * * @param string $name The name of the registered ability, with its namespace. * @return ?\WP_Ability The unregistered ability instance on success, null on failure. */ @@ -77,10 +77,10 @@ function wp_unregister_ability( string $name ): ?WP_Ability { /** * Retrieves a registered ability using Abilities API. * - * @see WP_Abilities_Registry::get_registered() - * * @since 0.1.0 * + * @see WP_Abilities_Registry::get_registered() + * * @param string $name The name of the registered ability, with its namespace. * @return ?\WP_Ability The registered ability instance, or null if it is not registered. */ @@ -91,10 +91,10 @@ function wp_get_ability( string $name ): ?WP_Ability { /** * Retrieves all registered abilities using Abilities API. * - * @see WP_Abilities_Registry::get_all_registered() - * * @since 0.1.0 * + * @see WP_Abilities_Registry::get_all_registered() + * * @return \WP_Ability[] The array of registered abilities. */ function wp_get_abilities(): array { From 769de9e8bef180de8d446ae8510c439253f91d3d Mon Sep 17 00:00:00 2001 From: Dovid Levine Date: Fri, 5 Sep 2025 10:05:48 +0300 Subject: [PATCH 5/6] dev!: handle property registration inside WP_Ability. (#54) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Greg Ziółkowski Co-authored-by: justlevine Co-authored-by: gziolo Co-authored-by: felixarntz --- .../class-wp-abilities-registry.php | 78 +++-------------- includes/abilities-api/class-wp-ability.php | 83 ++++++++++++++++--- .../abilities-api/wpAbilitiesRegistry.php | 18 ++++ 3 files changed, 102 insertions(+), 77 deletions(-) diff --git a/includes/abilities-api/class-wp-abilities-registry.php b/includes/abilities-api/class-wp-abilities-registry.php index e36decd..c0505a0 100644 --- a/includes/abilities-api/class-wp-abilities-registry.php +++ b/includes/abilities-api/class-wp-abilities-registry.php @@ -85,87 +85,33 @@ public function register( string $name, array $properties = array() ): ?WP_Abili return null; } - if ( empty( $properties['label'] ) || ! is_string( $properties['label'] ) ) { - _doing_it_wrong( - __METHOD__, - esc_html__( 'The ability properties must contain a `label` string.' ), - '0.1.0' - ); - return null; - } - - if ( empty( $properties['description'] ) || ! is_string( $properties['description'] ) ) { - _doing_it_wrong( - __METHOD__, - esc_html__( 'The ability properties must contain a `description` string.' ), - '0.1.0' - ); - return null; - } - - if ( isset( $properties['input_schema'] ) && ! is_array( $properties['input_schema'] ) ) { - _doing_it_wrong( - __METHOD__, - esc_html__( 'The ability properties should provide a valid `input_schema` definition.' ), - '0.1.0' - ); - return null; - } - - if ( isset( $properties['output_schema'] ) && ! is_array( $properties['output_schema'] ) ) { - _doing_it_wrong( - __METHOD__, - esc_html__( 'The ability properties should provide a valid `output_schema` definition.' ), - '0.1.0' - ); - return null; - } - - if ( empty( $properties['execute_callback'] ) || ! is_callable( $properties['execute_callback'] ) ) { - _doing_it_wrong( - __METHOD__, - esc_html__( 'The ability properties must contain a valid `execute_callback` function.' ), - '0.1.0' - ); - return null; - } - - if ( isset( $properties['permission_callback'] ) && ! is_callable( $properties['permission_callback'] ) ) { + // The class is only used to instantiate the ability, and is not a property of the ability itself. + if ( isset( $properties['ability_class'] ) && ! is_a( $properties['ability_class'], WP_Ability::class, true ) ) { _doing_it_wrong( __METHOD__, - esc_html__( 'The ability properties should provide a valid `permission_callback` function.' ), + esc_html__( 'The ability properties should provide a valid `ability_class` that extends WP_Ability.' ), '0.1.0' ); return null; } + $ability_class = $properties['ability_class'] ?? WP_Ability::class; + unset( $properties['ability_class'] ); - if ( isset( $properties['meta'] ) && ! is_array( $properties['meta'] ) ) { - _doing_it_wrong( - __METHOD__, - esc_html__( 'The ability properties should provide a valid `meta` array.' ), - '0.1.0' + try { + // WP_Ability::validate_properties() will throw an exception if the properties are invalid. + $ability = new $ability_class( + $name, + $properties ); - return null; - } - - if ( isset( $properties['ability_class'] ) && ! is_a( $properties['ability_class'], WP_Ability::class, true ) ) { + } catch ( \InvalidArgumentException $e ) { _doing_it_wrong( __METHOD__, - esc_html__( 'The ability properties should provide a valid `ability_class` that extends WP_Ability.' ), + esc_html( $e->getMessage() ), '0.1.0' ); return null; } - // The class is only used to instantiate the ability, and is not a property of the ability itself. - $ability_class = $properties['ability_class'] ?? WP_Ability::class; - unset( $properties['ability_class'] ); - - $ability = new $ability_class( - $name, - $properties - ); - $this->registered_abilities[ $name ] = $ability; return $ability; } diff --git a/includes/abilities-api/class-wp-ability.php b/includes/abilities-api/class-wp-ability.php index c55eabf..11b0aff 100644 --- a/includes/abilities-api/class-wp-ability.php +++ b/includes/abilities-api/class-wp-ability.php @@ -100,21 +100,12 @@ class WP_Ability { * @param array $properties An associative array of properties for the ability. This should * include `label`, `description`, `input_schema`, `output_schema`, * `execute_callback`, `permission_callback`, and `meta`. - * - * @phpstan-param array{ - * label: string, - * description: string, - * input_schema?: array, - * output_schema?: array, - * execute_callback: callable( array $input): (mixed|\WP_Error), - * permission_callback?: ?callable( array $input ): (bool|\WP_Error), - * meta?: array, - * ..., - * } $properties */ public function __construct( string $name, array $properties ) { $this->name = $name; + $this->validate_properties( $properties ); + foreach ( $properties as $property_name => $property_value ) { if ( ! property_exists( $this, $property_name ) ) { _doing_it_wrong( @@ -202,6 +193,76 @@ public function get_meta(): array { return $this->meta; } + /** + * Validates the properties used to instantiate the ability. + * + * Errors are thrown as exceptions instead of \WP_Errors to allow for simpler handling and overloading. They are then + * caught and converted to a WP_Error when by WP_Abilities_Registry::register(). + * + * @since n.e.x.t + * + * @see WP_Abilities_Registry::register() + * + * @param array $properties An associative array of properties to validate. + * + * @return void + * @throws \InvalidArgumentException if the properties are invalid. + * + * @phpstan-assert array{ + * label: string, + * description: string, + * input_schema?: array, + * output_schema?: array, + * execute_callback: callable( array $input): (mixed|\WP_Error), + * permission_callback?: ?callable( array $input ): (bool|\WP_Error), + * meta?: array, + * ..., + * } $properties + */ + protected function validate_properties( array $properties ) { + if ( empty( $properties['label'] ) || ! is_string( $properties['label'] ) ) { + throw new \InvalidArgumentException( + esc_html__( 'The ability properties must contain a `label` string.' ) + ); + } + + if ( empty( $properties['description'] ) || ! is_string( $properties['description'] ) ) { + throw new \InvalidArgumentException( + esc_html__( 'The ability properties must contain a `description` string.' ) + ); + } + + if ( isset( $properties['input_schema'] ) && ! is_array( $properties['input_schema'] ) ) { + throw new \InvalidArgumentException( + esc_html__( 'The ability properties should provide a valid `input_schema` definition.' ) + ); + } + + if ( isset( $properties['output_schema'] ) && ! is_array( $properties['output_schema'] ) ) { + throw new \InvalidArgumentException( + esc_html__( 'The ability properties should provide a valid `output_schema` definition.' ) + ); + } + + if ( empty( $properties['execute_callback'] ) || ! is_callable( $properties['execute_callback'] ) ) { + throw new \InvalidArgumentException( + esc_html__( 'The ability properties must contain a valid `execute_callback` function.' ) + ); + } + + if ( isset( $properties['permission_callback'] ) && ! is_callable( $properties['permission_callback'] ) ) { + throw new \InvalidArgumentException( + esc_html__( 'The ability properties should provide a valid `permission_callback` function.' ) + ); + } + + if ( isset( $properties['meta'] ) && ! is_array( $properties['meta'] ) ) { + throw new \InvalidArgumentException( + esc_html__( 'The ability properties should provide a valid `meta` array.' ) + ); + } + } + /** * Validates input data against the input schema. * diff --git a/tests/unit/abilities-api/wpAbilitiesRegistry.php b/tests/unit/abilities-api/wpAbilitiesRegistry.php index dff53e9..01e5ebc 100644 --- a/tests/unit/abilities-api/wpAbilitiesRegistry.php +++ b/tests/unit/abilities-api/wpAbilitiesRegistry.php @@ -383,4 +383,22 @@ public function test_get_all_registered() { $this->assertSame( $ability_two_name, $result[ $ability_two_name ]->get_name() ); $this->assertSame( $ability_three_name, $result[ $ability_three_name ]->get_name() ); } + + /** + * Direct instantiation of WP_Ability with invalid properties should throw an exception. + * + * @covers WP_Ability::__construct + * @covers WP_Ability::validate_properties + */ + public function test_wp_ability_invalid_properties_throws_exception() { + $this->expectException( \InvalidArgumentException::class ); + new WP_Ability( + 'test/invalid', + array( + 'label' => '', + 'description' => '', + 'execute_callback' => null, + ) + ); + } } From 21af812bc2dc3677aa71b3a6875dc5407a477adf Mon Sep 17 00:00:00 2001 From: Bartosz Budzanowski Date: Fri, 5 Sep 2025 09:28:21 +0200 Subject: [PATCH 6/6] Fix REST API initialization for composer package usage (#55) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix REST API initialization for composer package usage - Move REST API route registration from abilities-api.php to bootstrap.php - Ensure REST API routes are initialized when used as composer package - Prevent duplicate registration by placing hook inside class existence check - Consolidate initialization logic in single location 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * Remove trailing whitespace * Use tabs for indent. --------- Co-authored-by: Claude Co-authored-by: Greg Ziółkowski Co-authored-by: budzanowski Co-authored-by: gziolo Co-authored-by: justlevine --- abilities-api.php | 4 ---- includes/bootstrap.php | 9 +++++++++ tests/bootstrap.php | 3 ++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/abilities-api.php b/abilities-api.php index d79cc55..b52bee8 100644 --- a/abilities-api.php +++ b/abilities-api.php @@ -28,7 +28,3 @@ require_once WP_ABILITIES_API_DIR . 'includes/bootstrap.php'; - -if ( function_exists( 'add_action' ) ) { - add_action( 'rest_api_init', array( 'WP_REST_Abilities_Init', 'register_routes' ) ); -} diff --git a/includes/bootstrap.php b/includes/bootstrap.php index 0a3a6ca..052fb3c 100644 --- a/includes/bootstrap.php +++ b/includes/bootstrap.php @@ -13,6 +13,10 @@ declare( strict_types = 1 ); +if ( ! defined( 'ABSPATH' ) ) { + return; // Not in WordPress context +} + // Version of the plugin. if ( ! defined( 'WP_ABILITIES_API_VERSION' ) ) { define( 'WP_ABILITIES_API_VERSION', '0.1.0' ); @@ -34,4 +38,9 @@ // Load REST API init class for plugin bootstrap. if ( ! class_exists( 'WP_REST_Abilities_Init' ) ) { require_once __DIR__ . '/rest-api/class-wp-rest-abilities-init.php'; + + // Initialize REST API routes when WordPress is available. + if ( function_exists( 'add_action' ) ) { + add_action( 'rest_api_init', array( 'WP_REST_Abilities_Init', 'register_routes' ) ); + } } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 85dfff7..dcd7709 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -34,7 +34,8 @@ tests_add_filter( 'muplugins_loaded', static function (): void { - require_once dirname( __DIR__ ) . '/abilities-api.php'; + // Require ( to bypass require_once ). + require dirname( __DIR__ ) . '/includes/bootstrap.php'; } );