File: class-wp-abilities-registry.php

package info (click to toggle)
wordpress 6.9%2Bdfsg1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 136,360 kB
  • sloc: javascript: 534,044; php: 301,426; cs: 6,126; sh: 457; xml: 22; makefile: 14
file content (330 lines) | stat: -rw-r--r-- 11,793 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
<?php
/**
 * Abilities API
 *
 * Defines WP_Abilities_Registry class.
 *
 * @package WordPress
 * @subpackage Abilities API
 * @since 6.9.0
 */

declare( strict_types = 1 );

/**
 * Manages the registration and lookup of abilities.
 *
 * @since 6.9.0
 * @access private
 */
final class WP_Abilities_Registry {
	/**
	 * The singleton instance of the registry.
	 *
	 * @since 6.9.0
	 * @var self|null
	 */
	private static $instance = null;

	/**
	 * Holds the registered abilities.
	 *
	 * @since 6.9.0
	 * @var WP_Ability[]
	 */
	private $registered_abilities = array();

	/**
	 * Registers a new ability.
	 *
	 * Do not use this method directly. Instead, use the `wp_register_ability()` function.
	 *
	 * @since 6.9.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.
	 * @param array<string, mixed> $args {
	 *     An associative array of arguments for the ability.
	 *
	 *     @type string               $label                 The human-readable label for the ability.
	 *     @type string               $description           A detailed description of what the ability does.
	 *     @type string               $category              The ability category slug this ability belongs to.
	 *     @type callable             $execute_callback      A callback function to execute when the ability is invoked.
	 *                                                       Receives optional mixed input and returns mixed result or WP_Error.
	 *     @type callable             $permission_callback   A callback function to check permissions before execution.
	 *                                                       Receives optional mixed input and returns bool or WP_Error.
	 *     @type array<string, mixed> $input_schema          Optional. JSON Schema definition for the ability's input.
	 *     @type array<string, mixed> $output_schema         Optional. JSON Schema definition for the ability's output.
	 *     @type array<string, mixed> $meta                  {
	 *         Optional. Additional metadata for the ability.
	 *
	 *         @type array<string, bool|null> $annotations  {
	 *             Optional. Semantic annotations describing the ability's behavioral characteristics.
	 *             These annotations are hints for tooling and documentation.
	 *
	 *             @type bool|null $readonly    Optional. If true, the ability does not modify its environment.
	 *             @type bool|null $destructive Optional. If true, the ability may perform destructive updates to its environment.
	 *                                          If false, the ability performs only additive updates.
	 *             @type bool|null $idempotent  Optional. If true, calling the ability repeatedly with the same arguments
	 *                                          will have no additional effect on its environment.
	 *         }
	 *         @type bool                     $show_in_rest Optional. Whether to expose this ability in the REST API. Default false.
	 *     }
	 *     @type string               $ability_class         Optional. Custom class to instantiate instead of WP_Ability.
	 * }
	 * @return WP_Ability|null The registered ability instance on success, null on failure.
	 */
	public function register( string $name, array $args ): ?WP_Ability {
		if ( ! preg_match( '/^[a-z0-9-]+\/[a-z0-9-]+$/', $name ) ) {
			_doing_it_wrong(
				__METHOD__,
				__(
					'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.'
				),
				'6.9.0'
			);
			return null;
		}

		if ( $this->is_registered( $name ) ) {
			_doing_it_wrong(
				__METHOD__,
				/* translators: %s: Ability name. */
				sprintf( __( 'Ability "%s" is already registered.' ), esc_html( $name ) ),
				'6.9.0'
			);
			return null;
		}

		/**
		 * Filters the ability arguments before they are validated and used to instantiate the ability.
		 *
		 * @since 6.9.0
		 *
		 * @param array<string, mixed> $args {
		 *     An associative array of arguments for the ability.
		 *
		 *     @type string               $label                 The human-readable label for the ability.
		 *     @type string               $description           A detailed description of what the ability does.
		 *     @type string               $category              The ability category slug this ability belongs to.
		 *     @type callable             $execute_callback      A callback function to execute when the ability is invoked.
		 *                                                       Receives optional mixed input and returns mixed result or WP_Error.
		 *     @type callable             $permission_callback   A callback function to check permissions before execution.
		 *                                                       Receives optional mixed input and returns bool or WP_Error.
		 *     @type array<string, mixed> $input_schema          Optional. JSON Schema definition for the ability's input.
		 *     @type array<string, mixed> $output_schema         Optional. JSON Schema definition for the ability's output.
		 *     @type array<string, mixed> $meta                  {
		 *         Optional. Additional metadata for the ability.
		 *
		 *         @type array<string, bool|string> $annotations  Optional. Annotation metadata for the ability.
		 *         @type bool                       $show_in_rest Optional. Whether to expose this ability in the REST API. Default false.
		 *     }
		 *     @type string               $ability_class         Optional. Custom class to instantiate instead of WP_Ability.
		 * }
		 * @param string               $name The name of the ability, with its namespace.
		 */
		$args = apply_filters( 'wp_register_ability_args', $args, $name );

		// Validate ability category exists if provided (will be validated as required in WP_Ability).
		if ( isset( $args['category'] ) ) {
			if ( ! wp_has_ability_category( $args['category'] ) ) {
				_doing_it_wrong(
					__METHOD__,
					sprintf(
						/* translators: %1$s: ability category slug, %2$s: ability name */
						__( 'Ability category "%1$s" is not registered. Please register the ability category before assigning it to ability "%2$s".' ),
						esc_html( $args['category'] ),
						esc_html( $name )
					),
					'6.9.0'
				);
				return null;
			}
		}

		// The class is only used to instantiate the ability, and is not a property of the ability itself.
		if ( isset( $args['ability_class'] ) && ! is_a( $args['ability_class'], WP_Ability::class, true ) ) {
			_doing_it_wrong(
				__METHOD__,
				__( 'The ability args should provide a valid `ability_class` that extends WP_Ability.' ),
				'6.9.0'
			);
			return null;
		}

		/** @var class-string<WP_Ability> */
		$ability_class = $args['ability_class'] ?? WP_Ability::class;
		unset( $args['ability_class'] );

		try {
			// WP_Ability::prepare_properties() will throw an exception if the properties are invalid.
			$ability = new $ability_class( $name, $args );
		} catch ( InvalidArgumentException $e ) {
			_doing_it_wrong(
				__METHOD__,
				$e->getMessage(),
				'6.9.0'
			);
			return null;
		}

		$this->registered_abilities[ $name ] = $ability;
		return $ability;
	}

	/**
	 * Unregisters an ability.
	 *
	 * Do not use this method directly. Instead, use the `wp_unregister_ability()` function.
	 *
	 * @since 6.9.0
	 *
	 * @see wp_unregister_ability()
	 *
	 * @param string $name The name of the registered ability, with its namespace.
	 * @return WP_Ability|null The unregistered ability instance on success, null on failure.
	 */
	public function unregister( string $name ): ?WP_Ability {
		if ( ! $this->is_registered( $name ) ) {
			_doing_it_wrong(
				__METHOD__,
				/* translators: %s: Ability name. */
				sprintf( __( 'Ability "%s" not found.' ), esc_html( $name ) ),
				'6.9.0'
			);
			return null;
		}

		$unregistered_ability = $this->registered_abilities[ $name ];
		unset( $this->registered_abilities[ $name ] );

		return $unregistered_ability;
	}

	/**
	 * Retrieves the list of all registered abilities.
	 *
	 * Do not use this method directly. Instead, use the `wp_get_abilities()` function.
	 *
	 * @since 6.9.0
	 *
	 * @see wp_get_abilities()
	 *
	 * @return WP_Ability[] The array of registered abilities.
	 */
	public function get_all_registered(): array {
		return $this->registered_abilities;
	}

	/**
	 * Checks if an ability is registered.
	 *
	 * Do not use this method directly. Instead, use the `wp_has_ability()` function.
	 *
	 * @since 6.9.0
	 *
	 * @see wp_has_ability()
	 *
	 * @param string $name The name of the registered ability, with its namespace.
	 * @return bool True if the ability is registered, false otherwise.
	 */
	public function is_registered( string $name ): bool {
		return isset( $this->registered_abilities[ $name ] );
	}

	/**
	 * Retrieves a registered ability.
	 *
	 * Do not use this method directly. Instead, use the `wp_get_ability()` function.
	 *
	 * @since 6.9.0
	 *
	 * @see wp_get_ability()
	 *
	 * @param string $name The name of the registered ability, with its namespace.
	 * @return WP_Ability|null The registered ability instance, or null if it is not registered.
	 */
	public function get_registered( string $name ): ?WP_Ability {
		if ( ! $this->is_registered( $name ) ) {
			_doing_it_wrong(
				__METHOD__,
				/* translators: %s: Ability name. */
				sprintf( __( 'Ability "%s" not found.' ), esc_html( $name ) ),
				'6.9.0'
			);
			return null;
		}
		return $this->registered_abilities[ $name ];
	}

	/**
	 * Utility method to retrieve the main instance of the registry class.
	 *
	 * The instance will be created if it does not exist yet.
	 *
	 * @since 6.9.0
	 *
	 * @return WP_Abilities_Registry|null The main registry instance, or null when `init` action has not fired.
	 */
	public static function get_instance(): ?self {
		if ( ! did_action( 'init' ) ) {
			_doing_it_wrong(
				__METHOD__,
				sprintf(
					// translators: %s: init action.
					__( 'Ability API should not be initialized before the %s action has fired.' ),
					'<code>init</code>'
				),
				'6.9.0'
			);
			return null;
		}

		if ( null === self::$instance ) {
			self::$instance = new self();

			// Ensure ability category registry is initialized first to allow categories to be registered
			// before abilities that depend on them.
			WP_Ability_Categories_Registry::get_instance();

			/**
			 * Fires when preparing abilities registry.
			 *
			 * Abilities should be created and register their hooks on this action rather
			 * than another action to ensure they're only loaded when needed.
			 *
			 * @since 6.9.0
			 *
			 * @param WP_Abilities_Registry $instance Abilities registry object.
			 */
			do_action( 'wp_abilities_api_init', self::$instance );
		}

		return self::$instance;
	}

	/**
	 * Wakeup magic method.
	 *
	 * @since 6.9.0
	 * @throws LogicException If the registry object is unserialized.
	 *                        This is a security hardening measure to prevent unserialization of the registry.
	 */
	public function __wakeup(): void {
		throw new LogicException( __CLASS__ . ' should never be unserialized.' );
	}

	/**
	 * Sleep magic method.
	 *
	 * @since 6.9.0
	 * @throws LogicException If the registry object is serialized.
	 *                        This is a security hardening measure to prevent serialization of the registry.
	 */
	public function __sleep(): array {
		throw new LogicException( __CLASS__ . ' should never be serialized.' );
	}
}