Make WordPress Core

Changeset 61602


Ignore:
Timestamp:
02/09/2026 04:59:52 PM (4 weeks ago)
Author:
jorgefilipecosta
Message:

Abilities API: Allow nested namespace ability names (2-4 segments).

Expand ability name validation from exactly 2 segments (namespace/ability) to 2-4 segments, enabling names like my-plugin/resource/find and my-plugin/resource/sub/find.
This allows plugins to organize abilities into logical resource groups. The validation regex changes from /^[a-z0-9-]+\/[a-z0-9-]+$/ to /^[a-z0-9-]+(?:\/[a-z0-9-]+){1,3}$/, which accepts the first segment plus 1-3 additional slash-delimited segments.
Updates the validation regex, error messages, docblocks, and adds corresponding unit and REST API tests.

Props jorgefilipecosta, justlevine, jorbin.
Fixes #64596.

Location:
trunk
Files:
5 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/abilities-api.php

    r61130 r61602  
    133133 * Ability names must follow these rules:
    134134 *
    135  *  - Include a namespace prefix (e.g., `my-plugin/my-ability`).
     135 *  - Contain 2 to 4 segments separated by forward slashes
     136 *    (e.g., `my-plugin/my-ability`, `my-plugin/resource/find`, `my-plugin/resource/sub/find`).
    136137 *  - Use only lowercase alphanumeric characters, dashes, and forward slashes.
    137138 *  - Use descriptive, action-oriented names (e.g., `process-payment`, `generate-report`).
     
    226227 * @see wp_unregister_ability()
    227228 *
    228  * @param string               $name The name of the ability. Must be a namespaced string containing
    229  *                                   a prefix, e.g., `my-plugin/my-ability`. Can only contain lowercase
    230  *                                   alphanumeric characters, dashes, and forward slashes.
     229 * @param string               $name The name of the ability. Must be the fully-namespaced
     230 *                                   string identifier, e.g. `my-plugin/my-ability` or `my-plugin/resource/my-ability`.
    231231 * @param array<string, mixed> $args {
    232232 *     An associative array of arguments for configuring the ability.
     
    319319 *
    320320 * @param string $name The name of the ability to unregister, including namespace prefix
    321  *                     (e.g., 'my-plugin/my-ability').
     321 *                     (e.g., 'my-plugin/my-ability' or 'my-plugin/resource/find').
    322322 * @return WP_Ability|null The unregistered ability instance on success, `null` on failure.
    323323 */
     
    352352 *
    353353 * @param string $name The name of the ability to check, including namespace prefix
    354  *                     (e.g., 'my-plugin/my-ability').
     354 *                     (e.g., 'my-plugin/my-ability' or 'my-plugin/resource/find').
    355355 * @return bool `true` if the ability is registered, `false` otherwise.
    356356 */
     
    384384 *
    385385 * @param string $name The name of the ability, including namespace prefix
    386  *                     (e.g., 'my-plugin/my-ability').
     386 *                     (e.g., 'my-plugin/my-ability' or 'my-plugin/resource/find').
    387387 * @return WP_Ability|null The registered ability instance, or `null` if not registered.
    388388 */
  • trunk/src/wp-includes/abilities-api/class-wp-abilities-registry.php

    r61086 r61602  
    4444     * @see wp_register_ability()
    4545     *
    46      * @param string               $name The name of the ability. The name must be a string containing a namespace
    47      *                                   prefix, i.e. `my-plugin/my-ability`. It can only contain lowercase
    48      *                                   alphanumeric characters, dashes and the forward slash.
     46     * @param string               $name The name of the ability. Must be the fully-namespaced
     47 *                                       string identifier, e.g. `my-plugin/my-ability` or `my-plugin/resource/my-ability`.
    4948     * @param array<string, mixed> $args {
    5049     *     An associative array of arguments for the ability.
     
    7978     */
    8079    public function register( string $name, array $args ): ?WP_Ability {
    81         if ( ! preg_match( '/^[a-z0-9-]+\/[a-z0-9-]+$/', $name ) ) {
     80        if ( ! preg_match( '/^[a-z0-9-]+(?:\/[a-z0-9-]+){1,3}$/', $name ) ) {
    8281            _doing_it_wrong(
    8382                __METHOD__,
    8483                __(
    85                     'Ability 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.'
     84                    'Ability name must contain 2 to 4 segments separated by forward slashes, e.g. "my-plugin/my-ability" or "my-plugin/resource/my-ability". It can only contain lowercase alphanumeric characters, dashes, and forward slashes.'
    8685                ),
    8786                '6.9.0'
  • trunk/src/wp-includes/abilities-api/class-wp-ability.php

    r61505 r61602  
    5353    /**
    5454     * The name of the ability, with its namespace.
    55      * Example: `my-plugin/my-ability`.
     55     * Examples: `my-plugin/my-ability`, `my-plugin/resource/find`.
    5656     *
    5757     * @since 6.9.0
     
    341341    /**
    342342     * Retrieves the name of the ability, with its namespace.
    343      * Example: `my-plugin/my-ability`.
     343     * Examples: `my-plugin/my-ability`, `my-plugin/resource/find`.
    344344     *
    345345     * @since 6.9.0
  • trunk/tests/phpunit/tests/abilities-api/wpAbilitiesRegistry.php

    r61390 r61602  
    138138
    139139    /**
     140     * Should accept ability name with 3 segments (2 slashes).
     141     *
     142     * @ticket 64098
     143     *
     144     * @covers WP_Abilities_Registry::register
     145     */
     146    public function test_register_valid_name_with_three_segments() {
     147        $result = $this->registry->register( 'test/sub/add-numbers', self::$test_ability_args );
     148        $this->assertInstanceOf( WP_Ability::class, $result );
     149        $this->assertSame( 'test/sub/add-numbers', $result->get_name() );
     150    }
     151
     152    /**
     153     * Should accept ability name with 4 segments (3 slashes).
     154     *
     155     * @ticket 64098
     156     *
     157     * @covers WP_Abilities_Registry::register
     158     */
     159    public function test_register_valid_name_with_four_segments() {
     160        $result = $this->registry->register( 'test/sub/deep/add-numbers', self::$test_ability_args );
     161        $this->assertInstanceOf( WP_Ability::class, $result );
     162        $this->assertSame( 'test/sub/deep/add-numbers', $result->get_name() );
     163    }
     164
     165    /**
     166     * Should reject ability name with 5 segments (exceeds maximum of 4).
     167     *
     168     * @ticket 64098
     169     *
     170     * @covers WP_Abilities_Registry::register
     171     *
     172     * @expectedIncorrectUsage WP_Abilities_Registry::register
     173     */
     174    public function test_register_invalid_name_with_five_segments() {
     175        $result = $this->registry->register( 'test/a/b/c/too-deep', self::$test_ability_args );
     176        $this->assertNull( $result );
     177    }
     178
     179    /**
     180     * Should reject ability name with empty segments (double slashes).
     181     *
     182     * @ticket 64098
     183     *
     184     * @covers WP_Abilities_Registry::register
     185     *
     186     * @expectedIncorrectUsage WP_Abilities_Registry::register
     187     */
     188    public function test_register_invalid_name_with_empty_segment() {
     189        $result = $this->registry->register( 'test//add-numbers', self::$test_ability_args );
     190        $this->assertNull( $result );
     191    }
     192
     193    /**
     194     * Should reject ability name with trailing slash.
     195     *
     196     * @ticket 64098
     197     *
     198     * @covers WP_Abilities_Registry::register
     199     *
     200     * @expectedIncorrectUsage WP_Abilities_Registry::register
     201     */
     202    public function test_register_invalid_name_with_trailing_slash() {
     203        $result = $this->registry->register( 'test/add-numbers/', self::$test_ability_args );
     204        $this->assertNull( $result );
     205    }
     206
     207    /**
    140208     * Should reject ability registration without a label.
    141209     *
  • trunk/tests/phpunit/tests/rest-api/wpRestAbilitiesV1RunController.php

    r61373 r61602  
    380380        );
    381381
     382        // Ability with nested namespace (3 segments).
     383        $this->register_test_ability(
     384            'test/math/add',
     385            array(
     386                'label'               => 'Nested Add',
     387                'description'         => 'Adds numbers with nested namespace',
     388                'category'            => 'math',
     389                'input_schema'        => array(
     390                    'type'                 => 'object',
     391                    'properties'           => array(
     392                        'a' => array(
     393                            'type'        => 'number',
     394                            'description' => 'First number',
     395                        ),
     396                        'b' => array(
     397                            'type'        => 'number',
     398                            'description' => 'Second number',
     399                        ),
     400                    ),
     401                    'required'             => array( 'a', 'b' ),
     402                    'additionalProperties' => false,
     403                ),
     404                'output_schema'       => array(
     405                    'type' => 'number',
     406                ),
     407                'execute_callback'    => static function ( array $input ) {
     408                    return $input['a'] + $input['b'];
     409                },
     410                'permission_callback' => static function () {
     411                    return current_user_can( 'edit_posts' );
     412                },
     413                'meta'                => array(
     414                    'show_in_rest' => true,
     415                ),
     416            )
     417        );
     418
    382419        // Read-only ability for query params testing.
    383420        $this->register_test_ability(
     
    431468        $this->assertEquals( 200, $response->get_status() );
    432469        $this->assertEquals( 8, $response->get_data() );
     470    }
     471
     472    /**
     473     * Test executing an ability with a nested namespace (3 segments) via REST.
     474     *
     475     * @ticket 64098
     476     */
     477    public function test_execute_nested_namespace_ability(): void {
     478        $request = new WP_REST_Request( 'POST', '/wp-abilities/v1/abilities/test/math/add/run' );
     479        $request->set_header( 'Content-Type', 'application/json' );
     480        $request->set_body(
     481            wp_json_encode(
     482                array(
     483                    'input' => array(
     484                        'a' => 10,
     485                        'b' => 7,
     486                    ),
     487                )
     488            )
     489        );
     490
     491        $response = $this->server->dispatch( $request );
     492
     493        $this->assertEquals( 200, $response->get_status() );
     494        $this->assertEquals( 17, $response->get_data() );
    433495    }
    434496
Note: See TracChangeset for help on using the changeset viewer.