Make WordPress Core

Changeset 61602


Ignore:
Timestamp:
02/09/2026 04:59:52 PM (6 months 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.