Skip to content
This repository was archived by the owner on Feb 5, 2026. It is now read-only.

Commit e4e3b37

Browse files
authored
Add initial core abilities for WordPress 6.9 (#108)
## Add initial core abilities for WordPress Implements three foundational abilities in the `core/` namespace: - **core/get-site-info**: Returns site configuration fields (name, url, version, etc.) Supports optional field filtering with all fields returned by default. Requires `manage_options` capability. - **core/get-user-info**: Returns authenticated user profile data (id, roles, locale, etc.) Requires user authentication. - **core/get-environment-info**: Returns runtime environment details (PHP version, database server, WordPress version, environment type). Requires `manage_options` capability. ### Key decisions - Uses `core/` namespace to align with Gutenberg conventions - Organizes abilities into “site” and “user” categories - Self-documenting schemas with default values for optional inputs - Input normalization handles empty REST API calls gracefully Addresses **#105** **Co-authored-by:** - Jameswlepage <isotropic@git.wordpress.org> - JasonTheAdams <jason_the_adams@git.wordpress.org> - gziolo <gziolo@git.wordpress.org> - justlevine <justlevine@git.wordpress.org>
1 parent d9c061f commit e4e3b37

File tree

5 files changed

+536
-2
lines changed

5 files changed

+536
-2
lines changed

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,27 @@ public function get_input_schema(): array {
320320
return $this->input_schema;
321321
}
322322

323+
/**
324+
* Applies the defined input default when no input is provided.
325+
*
326+
* @since 0.4.0
327+
*
328+
* @param mixed $input Optional. The raw input provided for the ability. Default `null`.
329+
* @return mixed The input with the schema default applied when available.
330+
*/
331+
public function normalize_input( $input = null ) {
332+
if ( null !== $input ) {
333+
return $input;
334+
}
335+
336+
$input_schema = $this->get_input_schema();
337+
if ( ! empty( $input_schema ) && array_key_exists( 'default', $input_schema ) ) {
338+
return $input_schema['default'];
339+
}
340+
341+
return null;
342+
}
343+
323344
/**
324345
* Retrieves the output schema for the ability.
325346
*
@@ -436,6 +457,7 @@ protected function invoke_callback( callable $callback, $input = null ) {
436457
* @return bool|\WP_Error Whether the ability has the necessary permission.
437458
*/
438459
public function check_permissions( $input = null ) {
460+
$input = $this->normalize_input( $input );
439461
$is_valid = $this->validate_input( $input );
440462
if ( is_wp_error( $is_valid ) ) {
441463
return $is_valid;
@@ -522,6 +544,7 @@ protected function validate_output( $output ) {
522544
* @return mixed|\WP_Error The result of the ability execution, or WP_Error on failure.
523545
*/
524546
public function execute( $input = null ) {
547+
$input = $this->normalize_input( $input );
525548
$has_permissions = $this->check_permissions( $input );
526549
if ( true !== $has_permissions ) {
527550
if ( is_wp_error( $has_permissions ) ) {
Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
<?php
2+
/**
3+
* Core Abilities registration.
4+
*
5+
* @package WordPress
6+
* @subpackage Abilities_API
7+
* @since 0.3.0
8+
*/
9+
10+
declare( strict_types = 1 );
11+
12+
/**
13+
* Registers the default core abilities that ship with the Abilities API.
14+
*
15+
* @since 0.3.0
16+
*/
17+
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound -- Core class intended for WordPress core.
18+
final class WP_Core_Abilities {
19+
/**
20+
* Category slugs for core abilities.
21+
*
22+
* @since 0.3.0
23+
*/
24+
public const CATEGORY_SITE = 'site';
25+
public const CATEGORY_USER = 'user';
26+
/**
27+
* Registers the core abilities categories.
28+
*
29+
* @since 0.3.0
30+
*
31+
* @return void
32+
*/
33+
public static function register_category(): void {
34+
// Site-related capabilities
35+
wp_register_ability_category(
36+
self::CATEGORY_SITE,
37+
array(
38+
'label' => __( 'Site' ),
39+
'description' => __( 'Abilities that retrieve or modify site information and settings.' ),
40+
)
41+
);
42+
43+
// User-related capabilities
44+
wp_register_ability_category(
45+
self::CATEGORY_USER,
46+
array(
47+
'label' => __( 'User' ),
48+
'description' => __( 'Abilities that retrieve or modify user information and settings.' ),
49+
)
50+
);
51+
}
52+
53+
/**
54+
* Registers the default core abilities.
55+
*
56+
* @since 0.3.0
57+
*
58+
* @return void
59+
*/
60+
public static function register(): void {
61+
self::register_get_site_info();
62+
self::register_get_user_info();
63+
self::register_get_environment_info();
64+
}
65+
66+
/**
67+
* Registers the `core/get-site-info` ability.
68+
*
69+
* @since 0.3.0
70+
*
71+
* @return void
72+
*/
73+
protected static function register_get_site_info(): void {
74+
$fields = array(
75+
'name',
76+
'description',
77+
'url',
78+
'wpurl',
79+
'admin_email',
80+
'charset',
81+
'language',
82+
'version',
83+
);
84+
85+
wp_register_ability(
86+
'core/get-site-info',
87+
array(
88+
'label' => __( 'Get Site Information' ),
89+
'description' => __( 'Returns site information configured in WordPress. By default returns all fields, or optionally a filtered subset.' ),
90+
'category' => self::CATEGORY_SITE,
91+
'input_schema' => array(
92+
'type' => 'object',
93+
'properties' => array(
94+
'fields' => array(
95+
'type' => 'array',
96+
'items' => array(
97+
'type' => 'string',
98+
'enum' => $fields,
99+
),
100+
'description' => __( 'Optional: Limit response to specific fields. If omitted, all fields are returned.' ),
101+
),
102+
),
103+
'additionalProperties' => false,
104+
'default' => array(),
105+
),
106+
'output_schema' => array(
107+
'type' => 'object',
108+
'properties' => array(
109+
'name' => array(
110+
'type' => 'string',
111+
'description' => __( 'The site title.' ),
112+
),
113+
'description' => array(
114+
'type' => 'string',
115+
'description' => __( 'The site tagline.' ),
116+
),
117+
'url' => array(
118+
'type' => 'string',
119+
'description' => __( 'The site home URL.' ),
120+
),
121+
'wpurl' => array(
122+
'type' => 'string',
123+
'description' => __( 'The WordPress installation URL.' ),
124+
),
125+
'admin_email' => array(
126+
'type' => 'string',
127+
'description' => __( 'The site administrator email address.' ),
128+
),
129+
'charset' => array(
130+
'type' => 'string',
131+
'description' => __( 'The site character encoding.' ),
132+
),
133+
'language' => array(
134+
'type' => 'string',
135+
'description' => __( 'The site language locale code.' ),
136+
),
137+
'version' => array(
138+
'type' => 'string',
139+
'description' => __( 'The WordPress version.' ),
140+
),
141+
),
142+
'additionalProperties' => false,
143+
),
144+
'execute_callback' => static function ( $input = array() ): array {
145+
$input = is_array( $input ) ? $input : array();
146+
$all_fields = array( 'name', 'description', 'url', 'wpurl', 'admin_email', 'charset', 'language', 'version' );
147+
$requested_fields = ! empty( $input['fields'] ) ? $input['fields'] : $all_fields;
148+
149+
$result = array();
150+
foreach ( $requested_fields as $field ) {
151+
$result[ $field ] = get_bloginfo( $field );
152+
}
153+
154+
return $result;
155+
},
156+
'permission_callback' => static function (): bool {
157+
return current_user_can( 'manage_options' );
158+
},
159+
'meta' => array(
160+
'annotations' => array(
161+
'readonly' => true,
162+
'destructive' => false,
163+
'idempotent' => true,
164+
),
165+
'show_in_rest' => true,
166+
),
167+
)
168+
);
169+
}
170+
171+
/**
172+
* Registers the `core/get-user-info` ability.
173+
*
174+
* @since 0.3.0
175+
*
176+
* @return void
177+
*/
178+
protected static function register_get_user_info(): void {
179+
wp_register_ability(
180+
'core/get-user-info',
181+
array(
182+
'label' => __( 'Get User Information' ),
183+
'description' => __( 'Returns basic profile details for the current authenticated user to support personalization, auditing, and access-aware behavior.' ),
184+
'category' => self::CATEGORY_USER,
185+
'output_schema' => array(
186+
'type' => 'object',
187+
'required' => array( 'id', 'display_name', 'user_nicename', 'user_login', 'roles', 'locale' ),
188+
'properties' => array(
189+
'id' => array(
190+
'type' => 'integer',
191+
'description' => __( 'The user ID.' ),
192+
),
193+
'display_name' => array(
194+
'type' => 'string',
195+
'description' => __( 'The display name of the user.' ),
196+
),
197+
'user_nicename' => array(
198+
'type' => 'string',
199+
'description' => __( 'The URL-friendly name for the user.' ),
200+
),
201+
'user_login' => array(
202+
'type' => 'string',
203+
'description' => __( 'The login username for the user.' ),
204+
),
205+
'roles' => array(
206+
'type' => 'array',
207+
'description' => __( 'The roles assigned to the user.' ),
208+
'items' => array(
209+
'type' => 'string',
210+
),
211+
),
212+
'locale' => array(
213+
'type' => 'string',
214+
'description' => __( 'The locale string for the user, such as en_US.' ),
215+
),
216+
),
217+
'additionalProperties' => false,
218+
),
219+
'execute_callback' => static function (): array {
220+
$current_user = wp_get_current_user();
221+
222+
return array(
223+
'id' => $current_user->ID,
224+
'display_name' => $current_user->display_name,
225+
'user_nicename' => $current_user->user_nicename,
226+
'user_login' => $current_user->user_login,
227+
'roles' => $current_user->roles,
228+
'locale' => get_user_locale( $current_user ),
229+
);
230+
},
231+
'permission_callback' => static function (): bool {
232+
return is_user_logged_in();
233+
},
234+
'meta' => array(
235+
'annotations' => array(
236+
'readonly' => true,
237+
'destructive' => false,
238+
'idempotent' => true,
239+
),
240+
'show_in_rest' => false,
241+
),
242+
)
243+
);
244+
}
245+
246+
/**
247+
* Registers the `core/get-environment-info` ability.
248+
*
249+
* @since 0.3.0
250+
*
251+
* @return void
252+
*/
253+
protected static function register_get_environment_info(): void {
254+
wp_register_ability(
255+
'core/get-environment-info',
256+
array(
257+
'label' => __( 'Get Environment Info' ),
258+
'description' => __( 'Returns core details about the site\'s runtime context for diagnostics and compatibility (environment, PHP runtime, database server info, WordPress version).' ),
259+
'category' => self::CATEGORY_SITE,
260+
'output_schema' => array(
261+
'type' => 'object',
262+
'required' => array( 'environment', 'php_version', 'db_server_info', 'wp_version' ),
263+
'properties' => array(
264+
'environment' => array(
265+
'type' => 'string',
266+
'description' => __( 'The site\'s runtime environment classification (e.g., production, staging, development).' ),
267+
'examples' => array( 'production', 'staging', 'development', 'local' ),
268+
),
269+
'php_version' => array(
270+
'type' => 'string',
271+
'description' => __( 'The PHP runtime version executing WordPress.' ),
272+
),
273+
'db_server_info' => array(
274+
'type' => 'string',
275+
'description' => __( 'The database server vendor and version string reported by the driver.' ),
276+
'examples' => array( '8.0.34', '10.11.6-MariaDB' ),
277+
),
278+
'wp_version' => array(
279+
'type' => 'string',
280+
'description' => __( 'The WordPress core version running on this site.' ),
281+
),
282+
),
283+
'additionalProperties' => false,
284+
),
285+
'execute_callback' => static function (): array {
286+
global $wpdb;
287+
288+
$env = wp_get_environment_type();
289+
$php_version = phpversion();
290+
$db_server_info = '';
291+
if ( isset( $wpdb ) && is_object( $wpdb ) && method_exists( $wpdb, 'db_server_info' ) ) {
292+
$db_server_info = $wpdb->db_server_info() ?? '';
293+
}
294+
$wp_version = get_bloginfo( 'version' );
295+
296+
return array(
297+
'environment' => $env,
298+
'php_version' => $php_version,
299+
'db_server_info' => $db_server_info,
300+
'wp_version' => $wp_version,
301+
);
302+
},
303+
'permission_callback' => static function (): bool {
304+
return current_user_can( 'manage_options' );
305+
},
306+
'meta' => array(
307+
'annotations' => array(
308+
'readonly' => true,
309+
'destructive' => false,
310+
'idempotent' => true,
311+
),
312+
'show_in_rest' => true,
313+
),
314+
)
315+
);
316+
}
317+
}

includes/bootstrap.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,20 @@
4141
require_once __DIR__ . '/abilities-api.php';
4242
}
4343

44+
// Load core abilities class.
45+
if ( ! class_exists( 'WP_Core_Abilities' ) ) {
46+
require_once __DIR__ . '/abilities/class-wp-core-abilities.php';
47+
}
48+
49+
// Register core abilities category and abilities when requested via filter or when not in test environment.
50+
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Plugin-specific hook for feature plugin context.
51+
if ( ! ( defined( 'WP_RUN_CORE_TESTS' ) || defined( 'WP_TESTS_CONFIG_FILE_PATH' ) || ( function_exists( 'getenv' ) && false !== getenv( 'WP_PHPUNIT__DIR' ) ) ) || apply_filters( 'abilities_api_register_core_abilities', false ) ) {
52+
if ( function_exists( 'add_action' ) ) {
53+
add_action( 'abilities_api_categories_init', array( 'WP_Core_Abilities', 'register_category' ) );
54+
add_action( 'abilities_api_init', array( 'WP_Core_Abilities', 'register' ) );
55+
}
56+
}
57+
4458
// Load REST API init class for plugin bootstrap.
4559
if ( ! class_exists( 'WP_REST_Abilities_Init' ) ) {
4660
require_once __DIR__ . '/rest-api/class-wp-rest-abilities-init.php';

0 commit comments

Comments
 (0)