Skip to content

Commit 55227eb

Browse files
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. git-svn-id: https://develop.svn.wordpress.org/trunk@61602 602fd350-edb4-49c9-b593-d223f7449a82
1 parent edbdcbe commit 55227eb

5 files changed

Lines changed: 143 additions & 14 deletions

File tree

src/wp-includes/abilities-api.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,8 @@
132132
*
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`).
138139
*
@@ -225,9 +226,8 @@
225226
* @see wp_register_ability_category()
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.
233233
*
@@ -318,7 +318,7 @@ function wp_register_ability( string $name, array $args ): ?WP_Ability {
318318
* @see wp_register_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
*/
324324
function wp_unregister_ability( string $name ): ?WP_Ability {
@@ -351,7 +351,7 @@ function wp_unregister_ability( string $name ): ?WP_Ability {
351351
* @see wp_get_ability()
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
*/
357357
function wp_has_ability( string $name ): bool {
@@ -383,7 +383,7 @@ function wp_has_ability( string $name ): bool {
383383
* @see wp_has_ability()
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
*/
389389
function wp_get_ability( string $name ): ?WP_Ability {

src/wp-includes/abilities-api/class-wp-abilities-registry.php

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,8 @@ final class WP_Abilities_Registry {
4343
*
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.
5150
*
@@ -78,11 +77,11 @@ final class WP_Abilities_Registry {
7877
* @return WP_Ability|null The registered ability instance on success, null on failure.
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'
8887
);

src/wp-includes/abilities-api/class-wp-ability.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ class WP_Ability {
5252

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
5858
* @var string
@@ -340,7 +340,7 @@ protected function prepare_properties( array $args ): array {
340340

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
346346
*

tests/phpunit/tests/abilities-api/wpAbilitiesRegistry.php

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,74 @@ public function test_register_invalid_uppercase_characters_in_name() {
136136
$this->assertNull( $result );
137137
}
138138

139+
/**
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+
139207
/**
140208
* Should reject ability registration without a label.
141209
*

tests/phpunit/tests/rest-api/wpRestAbilitiesV1RunController.php

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,43 @@ private function register_test_abilities(): void {
379379
)
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(
384421
'test/query-params',
@@ -432,6 +469,31 @@ public function test_execute_regular_ability_post(): void {
432469
$this->assertEquals( 8, $response->get_data() );
433470
}
434471

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() );
495+
}
496+
435497
/**
436498
* Test executing a read-only ability with GET.
437499
*

0 commit comments

Comments
 (0)