get_keys() { return (array) get_option( $this->option_name, array() ); } /** * Updates the recovery key records. * * @since 5.2.0 * @since 6.8.0 Each key should now be hashed using wp_fast_hash() instead of phpass. * * @param array $keys { * Associative array of token => data pairs, where the data is an associative * array of information about the key. * * @type array ...$0 { * Information about the key. * * @type string $hashed_key The hashed value of the key. * @type int $created_at The timestamp when the key was created. * } * } * @return bool True on success, false on failure. */ private function update_keys( array $keys ) { return update_option( $this->option_name, $keys, false ); } } se ) ) { if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { $support_errors->add( 'bad_response_code', wp_remote_retrieve_response_message( $response ) ); } elseif ( false === wp_is_local_html_output( wp_remote_retrieve_body( $response ) ) ) { $support_errors->add( 'bad_response_source', __( 'It looks like the response did not come from this site.' ) ); } } return $support_errors->errors; } /** * Checks whether a given HTML string is likely an output from this WordPress site. * * This function attempts to check for various common WordPress patterns whether they are included in the HTML string. * Since any of these actions may be disabled through third-party code, this function may also return null to indicate * that it was not possible to determine ownership. * * @since 5.7.0 * @access private * * @param string $html Full HTML output string, e.g. from a HTTP response. * @return bool|null True/false for whether HTML was generated by this site, null if unable to determine. */ function wp_is_local_html_output( $html ) { // 1. Check if HTML includes the site's Really Simple Discovery link. if ( has_action( 'wp_head', 'rsd_link' ) ) { $pattern = preg_replace( '#^https?:(?=//)#', '', esc_url( site_url( 'xmlrpc.php?rsd', 'rpc' ) ) ); // See rsd_link(). return str_contains( $html, $pattern ); } // 2. Check if HTML includes the site's REST API link. if ( has_action( 'wp_head', 'rest_output_link_wp_head' ) ) { // Try both HTTPS and HTTP since the URL depends on context. $pattern = preg_replace( '#^https?:(?=//)#', '', esc_url( get_rest_url() ) ); // See rest_output_link_wp_head(). return str_contains( $html, $pattern ); } // Otherwise the result cannot be determined. return null; } s_node( WP_HTML_Token $token ): bool { foreach ( $this->walk_up() as $item ) { if ( $token === $item ) { return true; } } return false; } /** * Returns how many nodes are currently in the stack of open elements. * * @since 6.4.0 * * @return int How many node are in the stack of open elements. */ public function count(): int { return count( $this->stack ); } /** * Returns the node at the end of the stack of open elements, * if one exists. If the stack is empty, returns null. * * @since 6.4.0 * * @return WP_HTML_Token|null Last node in the stack of open elements, if one exists, otherwise null. */ public function current_node(): ?WP_HTML_Token { $current_node = end( $this->stack ); return $current_node ? $current_node : null; } /** * Indicates if the current node is of a given type or name. * * It's possible to pass either a node type or a node name to this function. * In the case there is no current element it will always return `false`. * * Example: * * // Is the current node a text node? * $stack->current_node_is( '#text' ); * * // Is the current node a DIV element? * $stack->current_node_is( 'DIV' ); * * // Is the current node any element/tag? * $stack->current_node_is( '#tag' ); * * @see WP_HTML_Tag_Processor::get_token_type * @see WP_HTML_Tag_Processor::get_token_name * * @since 6.7.0 * * @access private * * @param string $identity Check if the current node has this name or type (depending on what is provided). * @return bool Whether there is a current element that matches the given identity, whether a token name or type. */ public function current_node_is( string $identity ): bool { $current_node = end( $this->stack ); if ( false === $current_node ) { return false; } $current_node_name = $current_node->node_name; return ( $current_node_name === $identity || ( '#doctype' === $identity && 'html' === $current_node_name ) || ( '#tag' === $identity && ctype_upper( $current_node_name ) ) ); } /** * Returns whether an element is in a specific scope. * * @since 6.4.0 * * @see https://html.spec.whatwg.org/#has-an-element-in-the-specific-scope * * @param string $tag_name Name of tag check. * @param string[] $termination_list List of elements that terminate the search. * @return bool Whether the element was found in a specific scope. */ public function has_element_in_specific_scope( string $tag_name, $termination_list ): bool { foreach ( $this->walk_up() as $node ) { $namespaced_name = 'html' === $node->namespace ? $node->node_name : "{$node->namespace} {$node->node_name}"; if ( $namespaced_name === $tag_name ) { return true; } if ( '(internal: H1 through H6 - do not use)' === $tag_name && in_array( $namespaced_name, array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) ) { return true; } if ( in_array( $namespaced_name, $termination_list, true ) ) { return false; } } return false; } /** * Returns whether a particular element is in scope. * * > The stack of open elements is said to have a particular element in * > scope when it has that element in the specific scope consisting of * > the following element types: * > * > - applet * > - caption * > - html * > - table * > - td * > - th * > - marquee * > - object * > - template * > - MathML mi * > - MathML mo * > - MathML mn * > - MathML ms * > - MathML mtext * > - MathML annotation-xml * > - SVG foreignObject * > - SVG desc * > - SVG title * * @since 6.4.0 * @since 6.7.0 Full support. * * @see https://html.spec.whatwg.org/#has-an-element-in-scope * * @param string $tag_name Name of tag to check. * @return bool Whether given element is in scope. */ public function has_element_in_scope( string $tag_name ): bool { return $this->has_element_in_specific_scope( $tag_name, array( 'APPLET', 'CAPTION', 'HTML', 'TABLE', 'TD', 'TH', 'MARQUEE', 'OBJECT', 'TEMPLATE', 'math MI', 'math MO', 'math MN', 'math MS', 'math MTEXT', 'math ANNOTATION-XML', 'svg FOREIGNOBJECT', 'svg DESC', 'svg TITLE', ) ); } /** * Returns whether a particular element is in list item scope. * * > The stack of open elements is said to have a particular element * > in list item scope when it has that element in the specific scope * > consisting of the following element types: * > * > - All the element types listed above for the has an element in scope algorithm. * > - ol in the HTML namespace * > - ul in the HTML namespace * * @since 6.4.0 * @since 6.5.0 Implemented: no longer throws on every invocation. * @since 6.7.0 Supports all required HTML elements. * * @see https://html.spec.whatwg.org/#has-an-element-in-list-item-scope * * @param string $tag_name Name of tag to check. * @return bool Whether given element is in scope. */ public function has_element_in_list_item_scope( string $tag_name ): bool { return $this->has_element_in_specific_scope( $tag_name, array( 'APPLET', 'BUTTON', 'CAPTION', 'HTML', 'TABLE', 'TD', 'TH', 'MARQUEE', 'OBJECT', 'OL', 'TEMPLATE', 'UL', 'math MI', 'math MO', 'math MN', 'math MS', 'math MTEXT', 'math ANNOTATION-XML', 'svg FOREIGNOBJECT', 'svg DESC', 'svg TITLE', ) ); } /** * Returns whether a particular element is in button scope. * * > The stack of open elements is said to have a particular element * > in button scope when it has that element in the specific scope * > consisting of the following element types: * > * > - All the element types listed above for the has an element in scope algorithm. * > - button in the HTML namespace * * @since 6.4.0 * @since 6.7.0 Supports all required HTML elements. * * @see https://html.spec.whatwg.org/#has-an-element-in-button-scope * * @param string $tag_name Name of tag to check. * @return bool Whether given element is in scope. */ public function has_element_in_button_scope( string $tag_name ): bool { return $this->has_element_in_specific_scope( $tag_name, array( 'APPLET', 'BUTTON', 'CAPTION', 'HTML', 'TABLE', 'TD', 'TH', 'MARQUEE', 'OBJECT', 'TEMPLATE', 'math MI', 'math MO', 'math MN', 'math MS', 'math MTEXT', 'math ANNOTATION-XML', 'svg FOREIGNOBJECT', 'svg DESC', 'svg TITLE', ) ); } /** * Returns whether a particular element is in table scope. * * > The stack of open elements is said to have a particular element * > in table scope when it has that element in the specific scope * > consisting of the following element types: * > * > - html in the HTML namespace * > - table in the HTML namespace * > - template in the HTML namespace * * @since 6.4.0 * @since 6.7.0 Full implementation. * * @see https://html.spec.whatwg.org/#has-an-element-in-table-scope * * @param string $tag_name Name of tag to check. * @return bool Whether given element is in scope. */ public function has_element_in_table_scope( string $tag_name ): bool { return $this->has_element_in_specific_scope( $tag_name, array( 'HTML', 'TABLE', 'TEMPLATE', ) ); } /** * Returns whether a particular element is in select scope. * * This test differs from the others like it, in that its rules are inverted. * Instead of arriving at a match when one of any tag in a termination group * is reached, this one terminates if any other tag is reached. * * > The stack of open elements is said to have a particular element in select scope when it has * > that element in the specific scope consisting of all element types except the following: * > - optgroup in the HTML namespace * > - option in the HTML namespace * * @since 6.4.0 Stub implementation (throws). * @since 6.7.0 Full implementation. * * @see https://html.spec.whatwg.org/#has-an-element-in-select-scope * * @param string $tag_name Name of tag to check. * @return bool Whether the given element is in SELECT scope. */ public function has_element_in_select_scope( string $tag_name ): bool { foreach ( $this->walk_up() as $node ) { if ( $node->node_name === $tag_name ) { return true; } if ( 'OPTION' !== $node->node_name && 'OPTGROUP' !== $node->node_name ) { return false; } } return false; } /** * Returns whether a P is in BUTTON scope. * * @since 6.4.0 * * @see https://html.spec.whatwg.org/#has-an-element-in-button-scope * * @return bool Whether a P is in BUTTON scope. */ public function has_p_in_button_scope(): bool { return $this->has_p_in_button_scope; } /** * Pops a node off of the stack of open elements. * * @since 6.4.0 * * @see https://html.spec.whatwg.org/#stack-of-open-elements * * @return bool Whether a node was popped off of the stack. */ public function pop(): bool { $item = array_pop( $this->stack ); if ( null === $item ) { return false; } $this->after_element_pop( $item ); return true; } /** * Pops nodes off of the stack of open elements until an HTML tag with the given name has been popped. * * @since 6.4.0 * * @see WP_HTML_Open_Elements::pop * * @param string $html_tag_name Name of tag that needs to be popped off of the stack of open elements. * @return bool Whether a tag of the given name was found and popped off of the stack of open elements. */ public function pop_until( string $html_tag_name ): bool { foreach ( $this->walk_up() as $item ) { $this->pop(); if ( 'html' !== $item->namespace ) { continue; } if ( '(internal: H1 through H6 - do not use)' === $html_tag_name && in_array( $item->node_name, array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) ) { return true; } if ( $html_tag_name === $item->node_name ) { return true; } } return false; } /** * Pushes a node onto the stack of open elements. * * @since 6.4.0 * * @see https://html.spec.whatwg.org/#stack-of-open-elements * * @param WP_HTML_Token $stack_item Item to add onto stack. */ public function push( WP_HTML_Token $stack_item ): void { $this->stack[] = $stack_item; $this->after_element_push( $stack_item ); } /** * Removes a specific node from the stack of open elements. * * @since 6.4.0 * * @param WP_HTML_Token $token The node to remove from the stack of open elements. * @return bool Whether the node was found and removed from the stack of open elements. */ public function remove_node( WP_HTML_Token $token ): bool { foreach ( $this->walk_up() as $position_from_end => $item ) { if ( $token->bookmark_name !== $item->bookmark_name ) { continue; } $position_from_start = $this->count() - $position_from_end - 1; array_splice( $this->stack, $position_from_start, 1 ); $this->after_element_pop( $item ); return true; } return false; } /** * Steps through the stack of open elements, starting with the top element * (added first) and walking downwards to the one added last. * * This generator function is designed to be used inside a "foreach" loop. * * Example: * * $html = 'We are here'; * foreach ( $stack->walk_down() as $node ) { * echo "{$node->node_name} -> "; * } * > EM -> STRONG -> A -> * * To start with the most-recently added element and walk towards the top, * see WP_HTML_Open_Elements::walk_up(). * * @since 6.4.0 */ public function walk_down() { $count = count( $this->stack ); for ( $i = 0; $i < $count; $i++ ) { yield $this->stack[ $i ]; } } /** * Steps through the stack of open elements, starting with the bottom element * (added last) and walking upwards to the one added first. * * This generator function is designed to be used inside a "foreach" loop. * * Example: * * $html = 'We are here'; * foreach ( $stack->walk_up() as $node ) { * echo "{$node->node_name} -> "; * } * > A -> STRONG -> EM -> * * To start with the first added element and walk towards the bottom, * see WP_HTML_Open_Elements::walk_down(). * * @since 6.4.0 * @since 6.5.0 Accepts $above_this_node to start traversal above a given node, if it exists. * * @param WP_HTML_Token|null $above_this_node Optional. Start traversing above this node, * if provided and if the node exists. */ public function walk_up( ?WP_HTML_Token $above_this_node = null ) { $has_found_node = null === $above_this_node; for ( $i = count( $this->stack ) - 1; $i >= 0; $i-- ) { $node = $this->stack[ $i ]; if ( ! $has_found_node ) { $has_found_node = $node === $above_this_node; continue; } yield $node; } } /* * Internal helpers. */ /** * Updates internal flags after adding an element. * * Certain conditions (such as "has_p_in_button_scope") are maintained here as * flags that are only modified when adding and removing elements. This allows * the HTML Processor to quickly check for these conditions instead of iterating * over the open stack elements upon each new tag it encounters. These flags, * however, need to be maintained as items are added and removed from the stack. * * @since 6.4.0 * * @param WP_HTML_Token $item Element that was added to the stack of open elements. */ public function after_element_push( WP_HTML_Token $item ): void { $namespaced_name = 'html' === $item->namespace ? $item->node_name : "{$item->namespace} {$item->node_name}"; /* * When adding support for new elements, expand this switch to trap * cases where the precalculated value needs to change. */ switch ( $namespaced_name ) { case 'APPLET': case 'BUTTON': case 'CAPTION': case 'HTML': case 'TABLE': case 'TD': case 'TH': case 'MARQUEE': case 'OBJECT': case 'TEMPLATE': case 'math MI': case 'math MO': case 'math MN': case 'math MS': case 'math MTEXT': case 'math ANNOTATION-XML': case 'svg FOREIGNOBJECT': case 'svg DESC': case 'svg TITLE': $this->has_p_in_button_scope = false; break; case 'P': $this->has_p_in_button_scope = true; break; } if ( null !== $this->push_handler ) { ( $this->push_handler )( $item ); } } /** * Updates internal flags after removing an element. * * Certain conditions (such as "has_p_in_button_scope") are maintained here as * flags that are only modified when adding and removing elements. This allows * the HTML Processor to quickly check for these conditions instead of iterating * over the open stack elements upon each new tag it encounters. These flags, * however, need to be maintained as items are added and removed from the stack. * * @since 6.4.0 * * @param WP_HTML_Token $item Element that was removed from the stack of open elements. */ public function after_element_pop( WP_HTML_Token $item ): void { /* * When adding support for new elements, expand this switch to trap * cases where the precalculated value needs to change. */ switch ( $item->node_name ) { case 'APPLET': case 'BUTTON': case 'CAPTION': case 'HTML': case 'P': case 'TABLE': case 'TD': case 'TH': case 'MARQUEE': case 'OBJECT': case 'TEMPLATE': case 'math MI': case 'math MO': case 'math MN': case 'math MS': case 'math MTEXT': case 'math ANNOTATION-XML': case 'svg FOREIGNOBJECT': case 'svg DESC': case 'svg TITLE': $this->has_p_in_button_scope = $this->has_element_in_button_scope( 'P' ); break; } if ( null !== $this->pop_handler ) { ( $this->pop_handler )( $item ); } } /** * Clear the stack back to a table context. * * > When the steps above require the UA to clear the stack back to a table context, it means * > that the UA must, while the current node is not a table, template, or html element, pop * > elements from the stack of open elements. * * @see https://html.spec.whatwg.org/multipage/parsing.html#clear-the-stack-back-to-a-table-context * * @since 6.7.0 */ public function clear_to_table_context(): void { foreach ( $this->walk_up() as $item ) { if ( 'TABLE' === $item->node_name || 'TEMPLATE' === $item->node_name || 'HTML' === $item->node_name ) { break; } $this->pop(); } } /** * Clear the stack back to a table body context. * * > When the steps above require the UA to clear the stack back to a table body context, it * > means that the UA must, while the current node is not a tbody, tfoot, thead, template, or * > html element, pop elements from the stack of open elements. * * @see https://html.spec.whatwg.org/multipage/parsing.html#clear-the-stack-back-to-a-table-body-context * * @since 6.7.0 */ public function clear_to_table_body_context(): void { foreach ( $this->walk_up() as $item ) { if ( 'TBODY' === $item->node_name || 'TFOOT' === $item->node_name || 'THEAD' === $item->node_name || 'TEMPLATE' === $item->node_name || 'HTML' === $item->node_name ) { break; } $this->pop(); } } /** * Clear the stack back to a table row context. * * > When the steps above require the UA to clear the stack back to a table row context, it * > means that the UA must, while the current node is not a tr, template, or html element, pop * > elements from the stack of open elements. * * @see https://html.spec.whatwg.org/multipage/parsing.html#clear-the-stack-back-to-a-table-row-context * * @since 6.7.0 */ public function clear_to_table_row_context(): void { foreach ( $this->walk_up() as $item ) { if ( 'TR' === $item->node_name || 'TEMPLATE' === $item->node_name || 'HTML' === $item->node_name ) { break; } $this->pop(); } } /** * Wakeup magic method. * * @since 6.6.0 */ public function __wakeup() { throw new \LogicException( __CLASS__ . ' should never be unserialized' ); } } e request through the proxy. */ public function send_through_proxy( $uri ) { $check = parse_url( $uri ); // Malformed URL, can not process, but this could mean ssl, so let through anyway. if ( false === $check ) { return true; } $home = parse_url( get_option( 'siteurl' ) ); /** * Filters whether to preempt sending the request through the proxy. * * Returning false will bypass the proxy; returning true will send * the request through the proxy. Returning null bypasses the filter. * * @since 3.5.0 * * @param bool|null $override Whether to send the request through the proxy. Default null. * @param string $uri URL of the request. * @param array $check Associative array result of parsing the request URL with `parse_url()`. * @param array $home Associative array result of parsing the site URL with `parse_url()`. */ $result = apply_filters( 'pre_http_send_through_proxy', null, $uri, $check, $home ); if ( ! is_null( $result ) ) { return $result; } if ( 'localhost' === $check['host'] || ( isset( $home['host'] ) && $home['host'] === $check['host'] ) ) { return false; } if ( ! defined( 'WP_PROXY_BYPASS_HOSTS' ) ) { return true; } static $bypass_hosts = null; static $wildcard_regex = array(); if ( null === $bypass_hosts ) { $bypass_hosts = preg_split( '|,\s*|', WP_PROXY_BYPASS_HOSTS ); if ( str_contains( WP_PROXY_BYPASS_HOSTS, '*' ) ) { $wildcard_regex = array(); foreach ( $bypass_hosts as $host ) { $wildcard_regex[] = str_replace( '\*', '.+', preg_quote( $host, '/' ) ); } $wildcard_regex = '/^(' . implode( '|', $wildcard_regex ) . ')$/i'; } } if ( ! empty( $wildcard_regex ) ) { return ! preg_match( $wildcard_regex, $check['host'] ); } else { return ! in_array( $check['host'], $bypass_hosts, true ); } } } ription' => __( 'Performs sentiment analysis on provided text.', 'my-plugin' ), * 'category' => 'text-processing', * 'input_schema' => array( * 'type' => 'string', * 'description' => __( 'The text to be analyzed.', 'my-plugin' ), * 'minLength' => 10, * 'required' => true, * ), * 'output_schema' => array( * 'type' => 'string', * 'enum' => array( 'positive', 'negative', 'neutral' ), * 'description' => __( 'The sentiment result: positive, negative, or neutral.', 'my-plugin' ), * 'required' => true, * ), * 'execute_callback' => 'my_plugin_analyze_text', * 'permission_callback' => 'my_plugin_can_analyze_text', * 'meta' => array( * 'annotations' => array( * 'readonly' => true, * ), * 'show_in_rest' => true, * ), * ) * ); * } * add_action( 'wp_abilities_api_init', 'my_plugin_register_abilities' ); * * ### Naming Conventions * * Ability names must follow these rules: * * - Include a namespace prefix (e.g., `my-plugin/my-ability`). * - Use only lowercase alphanumeric characters, dashes, and forward slashes. * - Use descriptive, action-oriented names (e.g., `process-payment`, `generate-report`). * * ### Categories * * Abilities must be organized into categories. Ability categories provide better * discoverability and must be registered before the abilities that reference them: * * function my_plugin_register_categories(): void { * wp_register_ability_category( * 'text-processing', * array( * 'label' => __( 'Text Processing', 'my-plugin' ), * 'description' => __( 'Abilities for analyzing and transforming text.', 'my-plugin' ), * ) * ); * } * add_action( 'wp_abilities_api_categories_init', 'my_plugin_register_categories' ); * * ### Input and Output Schemas * * Schemas define the expected structure, type, and constraints for ability inputs * and outputs using JSON Schema syntax. They serve two critical purposes: automatic * validation of data passed to and returned from abilities, and self-documenting * API contracts for developers. * * WordPress implements a validator based on a subset of the JSON Schema Version 4 * specification (https://json-schema.org/specification-links.html#draft-4). * For details on supported JSON Schema properties and syntax, see the * related WordPress REST API Schema documentation: * https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#json-schema-basics * * Defining schemas is mandatory when there is a value to pass or return. * They ensure data integrity, improve developer experience, and enable * better documentation: * * 'input_schema' => array( * 'type' => 'string', * 'description' => __( 'The text to be analyzed.', 'my-plugin' ), * 'minLength' => 10, * 'required' => true, * ), * 'output_schema' => array( * 'type' => 'string', * 'enum' => array( 'positive', 'negative', 'neutral' ), * 'description' => __( 'The sentiment result: positive, negative, or neutral.', 'my-plugin' ), * 'required' => true, * ), * * ### Callbacks * * #### Execute Callback * * The execute callback performs the ability's core functionality. It receives * optional input data and returns either a result or `WP_Error` on failure. * * function my_plugin_analyze_text( string $input ): string|WP_Error { * $score = My_Plugin::perform_sentiment_analysis( $input ); * if ( is_wp_error( $score ) ) { * return $score; * } * return My_Plugin::interpret_sentiment_score( $score ); * } * * #### Permission Callback * * The permission callback determines whether the ability can be executed. * It receives the same input as the execute callback and must return a * boolean or `WP_Error`. Common use cases include checking user capabilities, * validating API keys, or verifying system state: * * function my_plugin_can_analyze_text( string $input ): bool|WP_Error { * return current_user_can( 'edit_posts' ); * } * * ### REST API Integration * * Abilities can be exposed through the REST API by setting `show_in_rest` * to `true` in the meta configuration: * * 'meta' => array( * 'show_in_rest' => true, * ), * * This allows abilities to be invoked via HTTP requests to the WordPress REST API. * * @since 6.9.0 * * @see WP_Abilities_Registry::register() * @see wp_register_ability_category() * @see wp_unregister_ability() * * @param string $name The name of the ability. Must be a namespaced string containing * a prefix, e.g., `my-plugin/my-ability`. Can only contain lowercase * alphanumeric characters, dashes, and forward slashes. * @param array $args { * An associative array of arguments for configuring the ability. * * @type string $label Required. The human-readable label for the ability. * @type string $description Required. A detailed description of what the ability does * and when it should be used. * @type string $category Required. The ability category slug this ability belongs to. * The ability category must be registered via `wp_register_ability_category()` * before registering the ability. * @type callable $execute_callback Required. A callback function to execute when the ability is invoked. * Receives optional mixed input data and must return either a result * value (any type) or a `WP_Error` object on failure. * @type callable $permission_callback Required. A callback function to check permissions before execution. * Receives optional mixed input data (same as `execute_callback`) and * must return `true`/`false` for simple checks, or `WP_Error` for * detailed error responses. * @type array $input_schema Optional. JSON Schema definition for validating the ability's input. * Must be a valid JSON Schema object defining the structure and * constraints for input data. Used for automatic validation and * API documentation. * @type array $output_schema Optional. JSON Schema definition for the ability's output. * Describes the structure of successful return values from * `execute_callback`. Used for documentation and validation. * @type array $meta { * Optional. Additional metadata for the ability. * * @type array $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. * When true, the ability can be invoked via HTTP requests. * Default false. * } * @type string $ability_class Optional. Fully-qualified custom class name to instantiate * instead of the default `WP_Ability` class. The custom class * must extend `WP_Ability`. Useful for advanced customization * of ability behavior. * } * @return WP_Ability|null The registered ability instance on success, `null` on failure. */ function wp_register_ability( string $name, array $args ): ?WP_Ability { if ( ! doing_action( 'wp_abilities_api_init' ) ) { _doing_it_wrong( __FUNCTION__, sprintf( /* translators: 1: wp_abilities_api_init, 2: string value of the ability name. */ __( 'Abilities must be registered on the %1$s action. The ability %2$s was not registered.' ), 'wp_abilities_api_init', '' . esc_html( $name ) . '' ), '6.9.0' ); return null; } $registry = WP_Abilities_Registry::get_instance(); if ( null === $registry ) { return null; } return $registry->register( $name, $args ); } /** * Unregisters an ability from the Abilities API. * * Removes a previously registered ability from the global registry. Use this to * disable abilities provided by other plugins or when an ability is no longer needed. * * Can be called at any time after the ability has been registered. * * Example: * * if ( wp_has_ability( 'other-plugin/some-ability' ) ) { * wp_unregister_ability( 'other-plugin/some-ability' ); * } * * @since 6.9.0 * * @see WP_Abilities_Registry::unregister() * @see wp_register_ability() * * @param string $name The name of the ability to unregister, including namespace prefix * (e.g., 'my-plugin/my-ability'). * @return WP_Ability|null The unregistered ability instance on success, `null` on failure. */ function wp_unregister_ability( string $name ): ?WP_Ability { $registry = WP_Abilities_Registry::get_instance(); if ( null === $registry ) { return null; } return $registry->unregister( $name ); } /** * Checks if an ability is registered. * * Use this for conditional logic and feature detection before attempting to * retrieve or use an ability. * * Example: * * // Displays different UI based on available abilities. * if ( wp_has_ability( 'premium-plugin/advanced-export' ) ) { * echo 'Export with Premium Features'; * } else { * echo 'Basic Export'; * } * * @since 6.9.0 * * @see WP_Abilities_Registry::is_registered() * @see wp_get_ability() * * @param string $name The name of the ability to check, including namespace prefix * (e.g., 'my-plugin/my-ability'). * @return bool `true` if the ability is registered, `false` otherwise. */ function wp_has_ability( string $name ): bool { $registry = WP_Abilities_Registry::get_instance(); if ( null === $registry ) { return false; } return $registry->is_registered( $name ); } /** * Retrieves a registered ability. * * Returns the ability instance for inspection or use. The instance provides access * to the ability's configuration, metadata, and execution methods. * * Example: * * // Prints information about a registered ability. * $ability = wp_get_ability( 'my-plugin/export-data' ); * if ( $ability ) { * echo $ability->get_label() . ': ' . $ability->get_description(); * } * * @since 6.9.0 * * @see WP_Abilities_Registry::get_registered() * @see wp_has_ability() * * @param string $name The name of the ability, including namespace prefix * (e.g., 'my-plugin/my-ability'). * @return WP_Ability|null The registered ability instance, or `null` if not registered. */ function wp_get_ability( string $name ): ?WP_Ability { $registry = WP_Abilities_Registry::get_instance(); if ( null === $registry ) { return null; } return $registry->get_registered( $name ); } /** * Retrieves all registered abilities. * * Returns an array of all ability instances currently registered in the system. * Use this for discovery, debugging, or building administrative interfaces. * * Example: * * // Prints information about all available abilities. * $abilities = wp_get_abilities(); * foreach ( $abilities as $ability ) { * echo $ability->get_label() . ': ' . $ability->get_description() . "\n"; * } * * @since 6.9.0 * * @see WP_Abilities_Registry::get_all_registered() * * @return WP_Ability[] An array of registered WP_Ability instances. Returns an empty * array if no abilities are registered or if the registry is unavailable. */ function wp_get_abilities(): array { $registry = WP_Abilities_Registry::get_instance(); if ( null === $registry ) { return array(); } return $registry->get_all_registered(); } /** * Registers a new ability category. * * Ability categories provide a way to organize and group related abilities for better * discoverability and management. Ability categories must be registered before abilities * that reference them. * * Ability categories must be registered on the `wp_abilities_api_categories_init` action hook. * * Example: * * function my_plugin_register_categories() { * wp_register_ability_category( * 'content-management', * array( * 'label' => __( 'Content Management', 'my-plugin' ), * 'description' => __( 'Abilities for managing and organizing content.', 'my-plugin' ), * ) * ); * } * add_action( 'wp_abilities_api_categories_init', 'my_plugin_register_categories' ); * * @since 6.9.0 * * @see WP_Ability_Categories_Registry::register() * @see wp_register_ability() * @see wp_unregister_ability_category() * * @param string $slug The unique slug for the ability category. Must contain only lowercase * alphanumeric characters and dashes (e.g., 'data-export'). * @param array $args { * An associative array of arguments for the ability category. * * @type string $label Required. The human-readable label for the ability category. * @type string $description Required. A description of what abilities in this category do. * @type array $meta Optional. Additional metadata for the ability category. * } * @return WP_Ability_Category|null The registered ability category instance on success, `null` on failure. */ function wp_register_ability_category( string $slug, array $args ): ?WP_Ability_Category { if ( ! doing_action( 'wp_abilities_api_categories_init' ) ) { _doing_it_wrong( __FUNCTION__, sprintf( /* translators: 1: wp_abilities_api_categories_init, 2: ability category slug. */ __( 'Ability categories must be registered on the %1$s action. The ability category %2$s was not registered.' ), 'wp_abilities_api_categories_init', '' . esc_html( $slug ) . '' ), '6.9.0' ); return null; } $registry = WP_Ability_Categories_Registry::get_instance(); if ( null === $registry ) { return null; } return $registry->register( $slug, $args ); } /** * Unregisters an ability category. * * Removes a previously registered ability category from the global registry. Use this to * disable ability categories that are no longer needed. * * Can be called at any time after the ability category has been registered. * * Example: * * if ( wp_has_ability_category( 'deprecated-category' ) ) { * wp_unregister_ability_category( 'deprecated-category' ); * } * * @since 6.9.0 * * @see WP_Ability_Categories_Registry::unregister() * @see wp_register_ability_category() * * @param string $slug The slug of the ability category to unregister. * @return WP_Ability_Category|null The unregistered ability category instance on success, `null` on failure. */ function wp_unregister_ability_category( string $slug ): ?WP_Ability_Category { $registry = WP_Ability_Categories_Registry::get_instance(); if ( null === $registry ) { return null; } return $registry->unregister( $slug ); } /** * Checks if an ability category is registered. * * Use this for conditional logic and feature detection before attempting to * retrieve or use an ability category. * * Example: * * // Displays different UI based on available ability categories. * if ( wp_has_ability_category( 'premium-features' ) ) { * echo 'Premium Features Available'; * } else { * echo 'Standard Features'; * } * * @since 6.9.0 * * @see WP_Ability_Categories_Registry::is_registered() * @see wp_get_ability_category() * * @param string $slug The slug of the ability category to check. * @return bool `true` if the ability category is registered, `false` otherwise. */ function wp_has_ability_category( string $slug ): bool { $registry = WP_Ability_Categories_Registry::get_instance(); if ( null === $registry ) { return false; } return $registry->is_registered( $slug ); } /** * Retrieves a registered ability category. * * Returns the ability category instance for inspection or use. The instance provides access * to the ability category's configuration and metadata. * * Example: * * // Prints information about a registered ability category. * $ability_category = wp_get_ability_category( 'content-management' ); * if ( $ability_category ) { * echo $ability_category->get_label() . ': ' . $ability_category->get_description(); * } * * @since 6.9.0 * * @see WP_Ability_Categories_Registry::get_registered() * @see wp_has_ability_category() * @see wp_get_ability_categories() * * @param string $slug The slug of the ability category. * @return WP_Ability_Category|null The ability category instance, or `null` if not registered. */ function wp_get_ability_category( string $slug ): ?WP_Ability_Category { $registry = WP_Ability_Categories_Registry::get_instance(); if ( null === $registry ) { return null; } return $registry->get_registered( $slug ); } /** * Retrieves all registered ability categories. * * Returns an array of all ability category instances currently registered in the system. * Use this for discovery, debugging, or building administrative interfaces. * * Example: * * // Prints information about all available ability categories. * $ability_categories = wp_get_ability_categories(); * foreach ( $ability_categories as $ability_category ) { * echo $ability_category->get_label() . ': ' . $ability_category->get_description() . "\n"; * } * * @since 6.9.0 * * @see WP_Ability_Categories_Registry::get_all_registered() * @see wp_get_ability_category() * * @return WP_Ability_Category[] An array of registered ability category instances. Returns an empty array * if no ability categories are registered or if the registry is unavailable. */ function wp_get_ability_categories(): array { $registry = WP_Ability_Categories_Registry::get_instance(); if ( null === $registry ) { return array(); } return $registry->get_all_registered(); } les( array( 'dimensions' => $dimensions_block_styles ) ); if ( ! empty( $styles['css'] ) ) { // Inject dimensions styles to the first element, presuming it's the wrapper, if it exists. $tags = new WP_HTML_Tag_Processor( $block_content ); if ( $tags->next_tag() ) { $existing_style = $tags->get_attribute( 'style' ); $updated_style = ''; if ( ! empty( $existing_style ) ) { $updated_style = $existing_style; if ( ! str_ends_with( $existing_style, ';' ) ) { $updated_style .= ';'; } } $updated_style .= $styles['css']; $tags->set_attribute( 'style', $updated_style ); if ( ! empty( $styles['classnames'] ) ) { foreach ( explode( ' ', $styles['classnames'] ) as $class_name ) { if ( str_contains( $class_name, 'aspect-ratio' ) && ! isset( $block_attributes['style']['dimensions']['aspectRatio'] ) ) { continue; } $tags->add_class( $class_name ); } } } return $tags->get_updated_html(); } return $block_content; } add_filter( 'render_block', 'wp_render_dimensions_support', 10, 2 ); // Register the block support. WP_Block_Supports::get_instance()->register( 'dimensions', array( 'register_attribute' => 'wp_register_dimensions_support', 'apply' => 'wp_apply_dimensions_support', ) ); rty to leverage the preexisting style * generation for simple block style variations. This way they get the * custom selectors they need. * * The inner elements and block styles for the variation itself are * still included at the top level but scoped by the variation's selector * when the stylesheet is generated. */ $elements_data = $variation_data['elements'] ?? array(); $blocks_data = $variation_data['blocks'] ?? array(); unset( $variation_data['elements'] ); unset( $variation_data['blocks'] ); _wp_array_set( $blocks_data, array( $parsed_block['blockName'], 'variations', $variation_instance ), $variation_data ); $config = array( 'version' => WP_Theme_JSON::LATEST_SCHEMA, 'styles' => array( 'elements' => $elements_data, 'blocks' => $blocks_data, ), ); // Turn off filter that excludes block nodes. They are needed here for the variation's inner block types. if ( ! is_admin() ) { remove_filter( 'wp_theme_json_get_style_nodes', 'wp_filter_out_block_nodes' ); } // Temporarily prevent variation instance from being sanitized while processing theme.json. $styles_registry = WP_Block_Styles_Registry::get_instance(); $styles_registry->register( $parsed_block['blockName'], array( 'name' => $variation_instance ) ); $variation_theme_json = new WP_Theme_JSON( $config, 'blocks' ); $variation_styles = $variation_theme_json->get_stylesheet( array( 'styles' ), array( 'custom' ), array( 'include_block_style_variations' => true, 'skip_root_layout_styles' => true, 'scope' => ".$class_name", ) ); // Clean up temporary block style now instance styles have been processed. $styles_registry->unregister( $parsed_block['blockName'], $variation_instance ); // Restore filter that excludes block nodes. if ( ! is_admin() ) { add_filter( 'wp_theme_json_get_style_nodes', 'wp_filter_out_block_nodes' ); } if ( empty( $variation_styles ) ) { return $parsed_block; } wp_register_style( 'block-style-variation-styles', false, array( 'wp-block-library', 'global-styles' ) ); wp_add_inline_style( 'block-style-variation-styles', $variation_styles ); /* * Add variation instance class name to block's className string so it can * be enforced in the block markup via render_block filter. */ _wp_array_set( $parsed_block, array( 'attrs', 'className' ), $updated_class_name ); return $parsed_block; } /** * Ensures the variation block support class name generated and added to * block attributes in the `render_block_data` filter gets applied to the * block's markup. * * @since 6.6.0 * @access private * * @see wp_render_block_style_variation_support_styles * * @param string $block_content Rendered block content. * @param array $block Block object. * * @return string Filtered block content. */ function wp_render_block_style_variation_class_name( $block_content, $block ) { if ( ! $block_content || empty( $block['attrs']['className'] ) ) { return $block_content; } /* * Matches a class prefixed by `is-style`, followed by the * variation slug, then `--`, and finally an instance number. */ preg_match( '/\bis-style-(\S+?--\d+)\b/', $block['attrs']['className'], $matches ); if ( empty( $matches ) ) { return $block_content; } $tags = new WP_HTML_Tag_Processor( $block_content ); if ( $tags->next_tag() ) { /* * Ensure the variation instance class name set in the * `render_block_data` filter is applied in markup. * See `wp_render_block_style_variation_support_styles`. */ $tags->add_class( $matches[0] ); } return $tags->get_updated_html(); } /** * Enqueues styles for block style variations. * * @since 6.6.0 * @access private */ function wp_enqueue_block_style_variation_styles() { wp_enqueue_style( 'block-style-variation-styles' ); } // Register the block support. WP_Block_Supports::get_instance()->register( 'block-style-variation', array() ); add_filter( 'render_block_data', 'wp_render_block_style_variation_support_styles', 10, 2 ); add_filter( 'render_block', 'wp_render_block_style_variation_class_name', 10, 2 ); add_action( 'wp_enqueue_scripts', 'wp_enqueue_block_style_variation_styles', 1 ); /** * Registers block style variations read in from theme.json partials. * * @since 6.6.0 * @access private * * @param array $variations Shared block style variations. */ function wp_register_block_style_variations_from_theme_json_partials( $variations ) { if ( empty( $variations ) ) { return; } $registry = WP_Block_Styles_Registry::get_instance(); foreach ( $variations as $variation ) { if ( empty( $variation['blockTypes'] ) || empty( $variation['styles'] ) ) { continue; } $variation_name = $variation['slug'] ?? _wp_to_kebab_case( $variation['title'] ); $variation_label = $variation['title'] ?? $variation_name; foreach ( $variation['blockTypes'] as $block_type ) { $registered_styles = $registry->get_registered_styles_for_block( $block_type ); // Register block style variation if it hasn't already been registered. if ( ! array_key_exists( $variation_name, $registered_styles ) ) { register_block_style( $block_type, array( 'name' => $variation_name, 'label' => $variation_label, ) ); } } } }