From d8d085ed9bf91d4e6ec1559371ac3aa7808d3724 Mon Sep 17 00:00:00 2001 From: Miguel Fonseca <150562+mcsf@users.noreply.github.com> Date: Tue, 24 Feb 2026 17:45:30 +0000 Subject: [PATCH 1/6] Try: Extend classes of Icons APIs for post-7.0 work In this proof of concept, we enhance the Icons registry via compat classes to: - Also search within icon labels, not just icon names - Point at Gutenberg's own manifest.php file rather than Core's - Edit the label of icon `core/table` to "Table Verse" in order to test searching for "verse" (which should return `core/verse` and `core/table`). The body of Gutenberg_Icons_Registry_7_1 illustrates the downside of keeping most classes in WP_Icons_Registry and WP_REST_Icons_Controller private: almost everything needs to be redefined in the extended classp. If we used `protected` instead, only the following would really need to be defined in Gutenberg_Icons_Registry_7_1: - __construct: to point to the new manifest.php - get_registered_icons: to extend searching to labels - get_instance and $instance: to break away from the base class But currently we need to add the following: - register - get_content - sanitize_icon_content - get_registered_icon - is_registered --- lib/compat/wordpress-7.0/rest-api.php | 9 + .../class-gutenberg-icons-registry-7-1.php | 250 ++++++++++++++++++ ...ss-gutenberg-rest-icons-controller-7-1.php | 82 ++++++ lib/load.php | 4 + packages/icons/src/manifest.php | 8 +- 5 files changed, 352 insertions(+), 1 deletion(-) create mode 100644 lib/compat/wordpress-7.1/class-gutenberg-icons-registry-7-1.php create mode 100644 lib/compat/wordpress-7.1/class-gutenberg-rest-icons-controller-7-1.php diff --git a/lib/compat/wordpress-7.0/rest-api.php b/lib/compat/wordpress-7.0/rest-api.php index c8fa113ba82af7..f611aa7954ea79 100644 --- a/lib/compat/wordpress-7.0/rest-api.php +++ b/lib/compat/wordpress-7.0/rest-api.php @@ -15,6 +15,15 @@ function gutenberg_register_block_patterns_controller_endpoints() { } add_action( 'rest_api_init', 'gutenberg_register_block_patterns_controller_endpoints' ); +/** + * Registers the Icons REST API routes. + */ +function gutenberg_register_icons_controller_endpoints() { + $icons_controller = new Gutenberg_REST_Icons_Controller_7_1(); + $icons_controller->register_routes(); +} +add_action( 'rest_api_init', 'gutenberg_register_icons_controller_endpoints', PHP_INT_MAX ); + /** * Registers the Registered Templates REST API routes. * The template activation experiment registers its own routes, so we only register the registered templates controller if the experiment is not enabled. diff --git a/lib/compat/wordpress-7.1/class-gutenberg-icons-registry-7-1.php b/lib/compat/wordpress-7.1/class-gutenberg-icons-registry-7-1.php new file mode 100644 index 00000000000000..9b67f09ee0ff02 --- /dev/null +++ b/lib/compat/wordpress-7.1/class-gutenberg-icons-registry-7-1.php @@ -0,0 +1,250 @@ + $icon_data ) { + if ( + empty( $icon_data['filePath'] ) + || ! is_string( $icon_data['filePath'] ) + ) { + _doing_it_wrong( + __METHOD__, + __( 'Core icon collection manifest must provide valid a "filePath" for each icon.', 'gutenberg' ), + '7.0.0' + ); + return; + } + + $this->register( + 'core/' . $icon_name, + array( + 'label' => $icon_data['label'], + 'filePath' => $icons_directory . $icon_data['filePath'], + ) + ); + } + } + + /** + * Modified to also search in icon labels + */ + public function get_registered_icons( $search = '' ) { + $icons = array(); + + foreach ( $this->registered_icons as $icon ) { + if ( ! empty( $search ) + && false === stripos( $icon['name'], $search ) + && false === stripos( $icon['label'], $search ) + ) { + continue; + } + + $icon['content'] = $icon['content'] ?? $this->get_content( $icon['name'] ); + $icons[] = $icon; + } + + return $icons; + } + + /** + * Redefined to break away from base class. + */ + protected static $instance = null; + + /** + * Redefined to access new `$instance` + */ + public static function get_instance() { + if ( null === self::$instance ) { + self::$instance = new self(); + } + + return self::$instance; + } + + /* + * + * THE DEFINITIONS BELOW MAY BE REMOVED IF WP_ICONS_REGISTRY CHANGES + * VISIBILITY OF ITS METHODS FROM PRIVATE TO PROTECTED + * + */ + + /** + * Redefined to be accessible to `__construct` + */ + protected function register( $icon_name, $icon_properties ) { + if ( ! isset( $icon_name ) || ! is_string( $icon_name ) ) { + _doing_it_wrong( + __METHOD__, + __( 'Icon name must be a string.', 'gutenberg' ), + '7.0.0' + ); + return false; + } + + $allowed_keys = array_fill_keys( array( 'label', 'content', 'filePath' ), 1 ); + foreach ( array_keys( $icon_properties ) as $key ) { + if ( ! array_key_exists( $key, $allowed_keys ) ) { + _doing_it_wrong( + __METHOD__, + sprintf( + // translators: %s is the name of any user-provided key + __( 'Invalid icon property: "%s".', 'gutenberg' ), + $key + ), + '7.0.0' + ); + return false; + } + } + + if ( ! isset( $icon_properties['label'] ) || ! is_string( $icon_properties['label'] ) ) { + _doing_it_wrong( + __METHOD__, + __( 'Icon label must be a string.', 'gutenberg' ), + '7.0.0' + ); + return false; + } + + if ( + ( ! isset( $icon_properties['content'] ) && ! isset( $icon_properties['filePath'] ) ) || + ( isset( $icon_properties['content'] ) && isset( $icon_properties['filePath'] ) ) + ) { + _doing_it_wrong( + __METHOD__, + __( 'Icons must provide either `content` or `filePath`.', 'gutenberg' ), + '7.0.0' + ); + return false; + } + + if ( isset( $icon_properties['content'] ) ) { + if ( ! is_string( $icon_properties['content'] ) ) { + _doing_it_wrong( + __METHOD__, + __( 'Icon content must be a string.', 'gutenberg' ), + '7.0.0' + ); + return false; + } + + $sanitized_icon_content = $this->sanitize_icon_content( $icon_properties['content'] ); + if ( empty( $sanitized_icon_content ) ) { + _doing_it_wrong( + __METHOD__, + __( 'Icon content does not contain valid SVG markup.', 'gutenberg' ), + '7.0.0' + ); + return false; + } + } + + $icon = array_merge( + $icon_properties, + array( 'name' => $icon_name ) + ); + + $this->registered_icons[ $icon_name ] = $icon; + + return true; + } + + /** + * Redefined to be accessible to `get_registered_icon` and `get_registered_icons` + */ + protected function get_content( $icon_name ) { + if ( ! isset( $this->registered_icons[ $icon_name ]['content'] ) ) { + $content = file_get_contents( + $this->registered_icons[ $icon_name ]['filePath'] + ); + $content = $this->sanitize_icon_content( $content ); + + if ( empty( $content ) ) { + wp_trigger_error( + __METHOD__, + __( 'Icon content does not contain valid SVG markup.', 'gutenberg' ) + ); + return null; + } + + $this->registered_icons[ $icon_name ]['content'] = $content; + } + return $this->registered_icons[ $icon_name ]['content']; + } + + /** + * Redefined to be accessible to `register` and `get_content` + */ + protected function sanitize_icon_content( $icon_content ) { + $allowed_tags = array( + 'svg' => array( + 'class' => true, + 'xmlns' => true, + 'width' => true, + 'height' => true, + 'viewbox' => true, + 'aria-hidden' => true, + 'role' => true, + 'focusable' => true, + ), + 'path' => array( + 'fill' => true, + 'fill-rule' => true, + 'd' => true, + 'transform' => true, + ), + 'polygon' => array( + 'fill' => true, + 'fill-rule' => true, + 'points' => true, + 'transform' => true, + 'focusable' => true, + ), + ); + return wp_kses( $icon_content, $allowed_tags ); + } + + /** + * Redefined to access the new `$registered_icons` + */ + public function get_registered_icon( $icon_name ) { + if ( ! $this->is_registered( $icon_name ) ) { + return null; + } + + $icon = $this->registered_icons[ $icon_name ]; + $icon['content'] = $icon['content'] ?? $this->get_content( $icon_name ); + + return $icon; + } + + public function is_registered( $icon_name ) { + return isset( $this->registered_icons[ $icon_name ] ); + } +} diff --git a/lib/compat/wordpress-7.1/class-gutenberg-rest-icons-controller-7-1.php b/lib/compat/wordpress-7.1/class-gutenberg-rest-icons-controller-7-1.php new file mode 100644 index 00000000000000..17793de625afcf --- /dev/null +++ b/lib/compat/wordpress-7.1/class-gutenberg-rest-icons-controller-7-1.php @@ -0,0 +1,82 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ), + true // Override the core route. + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[a-z][a-z0-9-]*/[a-z][a-z0-9-]*)', + array( + 'args' => array( + 'name' => array( + 'description' => __( 'Icon name.', 'gutenberg' ), + 'type' => 'string', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ), + true // Override the core route. + ); + } + + /** + * Modified to call Gutenberg_Icons_Registry_7_1 + */ + public function get_items( $request ) { + $response = array(); + $search = $request->get_param( 'search' ); + $icons = Gutenberg_Icons_Registry_7_1::get_instance()->get_registered_icons( $search ); + foreach ( $icons as $icon ) { + $prepared_icon = $this->prepare_item_for_response( $icon, $request ); + $response[] = $this->prepare_response_for_collection( $prepared_icon ); + } + return rest_ensure_response( $response ); + } + + /** + * Modified to call Gutenberg_Icons_Registry_7_1 + */ + public function get_icon( $name ) { + $registry = Gutenberg_Icons_Registry_7_1::get_instance(); + $icon = $registry->get_registered_icon( $name ); + + if ( null === $icon ) { + return new WP_Error( + 'rest_icon_not_found', + sprintf( + // translators: %s is the name of any user-provided name + __( 'Icon not found: "%s".', 'gutenberg' ), + $name + ), + array( 'status' => 404 ) + ); + } + + return $icon; + } +} diff --git a/lib/load.php b/lib/load.php index 2d437a04f92ddc..6b4e3cb9f55836 100644 --- a/lib/load.php +++ b/lib/load.php @@ -76,6 +76,10 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/compat/wordpress-7.0/rest-api.php'; require __DIR__ . '/compat/wordpress-7.0/global-styles.php'; + // WordPress 7.1 compat. + require __DIR__ . '/compat/wordpress-7.1/class-gutenberg-icons-registry-7-1.php'; + require __DIR__ . '/compat/wordpress-7.1/class-gutenberg-rest-icons-controller-7-1.php'; + // Plugin specific code. require_once __DIR__ . '/class-wp-rest-global-styles-controller-gutenberg.php'; require_once __DIR__ . '/class-wp-rest-edit-site-export-controller-gutenberg.php'; diff --git a/packages/icons/src/manifest.php b/packages/icons/src/manifest.php index d2eb04cff14e9f..4e36b921db02b0 100644 --- a/packages/icons/src/manifest.php +++ b/packages/icons/src/manifest.php @@ -334,7 +334,13 @@ 'filePath' => 'library/symbol-filled.svg', ), 'table' => array( - 'label' => _x( 'Table', 'icon label', 'gutenberg' ), + // FIXME: Label amended to test the search feature: + // + // /wp/v2/icons?search=verse + // -> core/verse + // -> core/table + // + 'label' => _x( 'Table Verse', 'icon label', 'gutenberg' ), 'filePath' => 'library/table.svg', ), 'tablet' => array( From a362028d584ff1a080ee271b35cf88f7a44bbb44 Mon Sep 17 00:00:00 2001 From: Miguel Fonseca <150562+mcsf@users.noreply.github.com> Date: Fri, 27 Feb 2026 12:22:02 +0000 Subject: [PATCH 2/6] Prefer `gutenberg_dir_path` Co-authored-by: Aki Hamano <54422211+t-hamano@users.noreply.github.com> --- lib/compat/wordpress-7.1/class-gutenberg-icons-registry-7-1.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/compat/wordpress-7.1/class-gutenberg-icons-registry-7-1.php b/lib/compat/wordpress-7.1/class-gutenberg-icons-registry-7-1.php index 9b67f09ee0ff02..f667792bf204f6 100644 --- a/lib/compat/wordpress-7.1/class-gutenberg-icons-registry-7-1.php +++ b/lib/compat/wordpress-7.1/class-gutenberg-icons-registry-7-1.php @@ -5,7 +5,7 @@ class Gutenberg_Icons_Registry_7_1 extends WP_Icons_Registry { * Modified to point $manifest_path at Gutenberg packages */ protected function __construct() { - $icons_directory = __DIR__ . '/../../../packages/icons/src/'; + $icons_directory = gutenberg_dir_path() . 'packages/icons/src'; $icons_directory = trailingslashit( $icons_directory ); $manifest_path = $icons_directory . 'manifest.php'; From 35adbd3afd29bb9ac62966549de0bb4d78681ea6 Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Tue, 3 Mar 2026 21:31:32 +0900 Subject: [PATCH 3/6] Move rest_api_init hook to 7.1 compat directory --- lib/compat/wordpress-7.0/rest-api.php | 9 --------- lib/compat/wordpress-7.1/rest-api.php | 16 ++++++++++++++++ lib/load.php | 1 + packages/icons/src/manifest.php | 8 +------- 4 files changed, 18 insertions(+), 16 deletions(-) create mode 100644 lib/compat/wordpress-7.1/rest-api.php diff --git a/lib/compat/wordpress-7.0/rest-api.php b/lib/compat/wordpress-7.0/rest-api.php index f611aa7954ea79..c8fa113ba82af7 100644 --- a/lib/compat/wordpress-7.0/rest-api.php +++ b/lib/compat/wordpress-7.0/rest-api.php @@ -15,15 +15,6 @@ function gutenberg_register_block_patterns_controller_endpoints() { } add_action( 'rest_api_init', 'gutenberg_register_block_patterns_controller_endpoints' ); -/** - * Registers the Icons REST API routes. - */ -function gutenberg_register_icons_controller_endpoints() { - $icons_controller = new Gutenberg_REST_Icons_Controller_7_1(); - $icons_controller->register_routes(); -} -add_action( 'rest_api_init', 'gutenberg_register_icons_controller_endpoints', PHP_INT_MAX ); - /** * Registers the Registered Templates REST API routes. * The template activation experiment registers its own routes, so we only register the registered templates controller if the experiment is not enabled. diff --git a/lib/compat/wordpress-7.1/rest-api.php b/lib/compat/wordpress-7.1/rest-api.php new file mode 100644 index 00000000000000..4d7ceaba5cd85f --- /dev/null +++ b/lib/compat/wordpress-7.1/rest-api.php @@ -0,0 +1,16 @@ +register_routes(); +} +add_action( 'rest_api_init', 'gutenberg_register_icons_controller_endpoints', PHP_INT_MAX ); diff --git a/lib/load.php b/lib/load.php index 6b4e3cb9f55836..615af06eff8607 100644 --- a/lib/load.php +++ b/lib/load.php @@ -79,6 +79,7 @@ function gutenberg_is_experiment_enabled( $name ) { // WordPress 7.1 compat. require __DIR__ . '/compat/wordpress-7.1/class-gutenberg-icons-registry-7-1.php'; require __DIR__ . '/compat/wordpress-7.1/class-gutenberg-rest-icons-controller-7-1.php'; + require __DIR__ . '/compat/wordpress-7.1/rest-api.php'; // Plugin specific code. require_once __DIR__ . '/class-wp-rest-global-styles-controller-gutenberg.php'; diff --git a/packages/icons/src/manifest.php b/packages/icons/src/manifest.php index 4e36b921db02b0..d2eb04cff14e9f 100644 --- a/packages/icons/src/manifest.php +++ b/packages/icons/src/manifest.php @@ -334,13 +334,7 @@ 'filePath' => 'library/symbol-filled.svg', ), 'table' => array( - // FIXME: Label amended to test the search feature: - // - // /wp/v2/icons?search=verse - // -> core/verse - // -> core/table - // - 'label' => _x( 'Table Verse', 'icon label', 'gutenberg' ), + 'label' => _x( 'Table', 'icon label', 'gutenberg' ), 'filePath' => 'library/table.svg', ), 'tablet' => array( From 7cad2b7d6c513b1d8eccaba56fb6e09c859d2b0f Mon Sep 17 00:00:00 2001 From: Miguel Fonseca <150562+mcsf@users.noreply.github.com> Date: Wed, 11 Mar 2026 14:07:58 +0000 Subject: [PATCH 4/6] WP_Icons_Registry: Port changes from Core Sync changes from https://core.trac.wordpress.org/changeset/61674 --- .../wordpress-7.0/class-wp-icons-registry.php | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/lib/compat/wordpress-7.0/class-wp-icons-registry.php b/lib/compat/wordpress-7.0/class-wp-icons-registry.php index 012c02721cbd00..1fa66e54001a04 100644 --- a/lib/compat/wordpress-7.0/class-wp-icons-registry.php +++ b/lib/compat/wordpress-7.0/class-wp-icons-registry.php @@ -7,7 +7,7 @@ class WP_Icons_Registry { * * @var array[] */ - private $registered_icons = array(); + protected $registered_icons = array(); /** @@ -15,14 +15,21 @@ class WP_Icons_Registry { * * @var WP_Icons_Registry|null */ - private static $instance = null; + protected static $instance = null; /** * Constructor. * - * WP_Icons_Registry is a singleton class, so keep this private. + * WP_Icons_Registry is a singleton class, so keep this protected. + * + * For WP 7.0, the Icons Registry is closed for third-party icon + * registry, serving only a subset of core icons. + * + * These icons are defined in @wordpress/packages as SVG files and as + * entries in a single manifest file. On init, the registry is loaded + * with those icons listed in the manifest. */ - private function __construct() { + protected function __construct() { $icons_directory = __DIR__ . '/../../../packages/icons/src/'; $icons_directory = trailingslashit( $icons_directory ); $manifest_path = $icons_directory . 'manifest.php'; @@ -83,7 +90,7 @@ private function __construct() { * } * @return bool True if the icon was registered with success and false otherwise. */ - private function register( $icon_name, $icon_properties ) { + protected function register( $icon_name, $icon_properties ) { if ( ! isset( $icon_name ) || ! is_string( $icon_name ) ) { _doing_it_wrong( __METHOD__, @@ -170,7 +177,7 @@ private function register( $icon_name, $icon_properties ) { * @param string $icon_content The icon SVG content to sanitize. * @return string The sanitized icon SVG content. */ - private function sanitize_icon_content( $icon_content ) { + protected function sanitize_icon_content( $icon_content ) { $allowed_tags = array( 'svg' => array( 'class' => true, @@ -205,7 +212,7 @@ private function sanitize_icon_content( $icon_content ) { * @param string $icon_name Icon name including namespace. * @return string|null The content of the icon, if found. */ - private function get_content( $icon_name ) { + protected function get_content( $icon_name ) { if ( ! isset( $this->registered_icons[ $icon_name ]['content'] ) ) { $content = file_get_contents( $this->registered_icons[ $icon_name ]['filePath'] From 1b9874ee69fb968e9e534fc350833e309a0d2dd8 Mon Sep 17 00:00:00 2001 From: Miguel Fonseca <150562+mcsf@users.noreply.github.com> Date: Wed, 11 Mar 2026 14:33:08 +0000 Subject: [PATCH 5/6] Trim extended class thanks to parent's `protected` visibility --- .../class-gutenberg-icons-registry-7-1.php | 163 +----------------- 1 file changed, 1 insertion(+), 162 deletions(-) diff --git a/lib/compat/wordpress-7.1/class-gutenberg-icons-registry-7-1.php b/lib/compat/wordpress-7.1/class-gutenberg-icons-registry-7-1.php index f667792bf204f6..e827056064e7b1 100644 --- a/lib/compat/wordpress-7.1/class-gutenberg-icons-registry-7-1.php +++ b/lib/compat/wordpress-7.1/class-gutenberg-icons-registry-7-1.php @@ -2,7 +2,7 @@ class Gutenberg_Icons_Registry_7_1 extends WP_Icons_Registry { /** - * Modified to point $manifest_path at Gutenberg packages + * Modified to point $manifest_path to Gutenberg packages */ protected function __construct() { $icons_directory = gutenberg_dir_path() . 'packages/icons/src'; @@ -86,165 +86,4 @@ public static function get_instance() { return self::$instance; } - - /* - * - * THE DEFINITIONS BELOW MAY BE REMOVED IF WP_ICONS_REGISTRY CHANGES - * VISIBILITY OF ITS METHODS FROM PRIVATE TO PROTECTED - * - */ - - /** - * Redefined to be accessible to `__construct` - */ - protected function register( $icon_name, $icon_properties ) { - if ( ! isset( $icon_name ) || ! is_string( $icon_name ) ) { - _doing_it_wrong( - __METHOD__, - __( 'Icon name must be a string.', 'gutenberg' ), - '7.0.0' - ); - return false; - } - - $allowed_keys = array_fill_keys( array( 'label', 'content', 'filePath' ), 1 ); - foreach ( array_keys( $icon_properties ) as $key ) { - if ( ! array_key_exists( $key, $allowed_keys ) ) { - _doing_it_wrong( - __METHOD__, - sprintf( - // translators: %s is the name of any user-provided key - __( 'Invalid icon property: "%s".', 'gutenberg' ), - $key - ), - '7.0.0' - ); - return false; - } - } - - if ( ! isset( $icon_properties['label'] ) || ! is_string( $icon_properties['label'] ) ) { - _doing_it_wrong( - __METHOD__, - __( 'Icon label must be a string.', 'gutenberg' ), - '7.0.0' - ); - return false; - } - - if ( - ( ! isset( $icon_properties['content'] ) && ! isset( $icon_properties['filePath'] ) ) || - ( isset( $icon_properties['content'] ) && isset( $icon_properties['filePath'] ) ) - ) { - _doing_it_wrong( - __METHOD__, - __( 'Icons must provide either `content` or `filePath`.', 'gutenberg' ), - '7.0.0' - ); - return false; - } - - if ( isset( $icon_properties['content'] ) ) { - if ( ! is_string( $icon_properties['content'] ) ) { - _doing_it_wrong( - __METHOD__, - __( 'Icon content must be a string.', 'gutenberg' ), - '7.0.0' - ); - return false; - } - - $sanitized_icon_content = $this->sanitize_icon_content( $icon_properties['content'] ); - if ( empty( $sanitized_icon_content ) ) { - _doing_it_wrong( - __METHOD__, - __( 'Icon content does not contain valid SVG markup.', 'gutenberg' ), - '7.0.0' - ); - return false; - } - } - - $icon = array_merge( - $icon_properties, - array( 'name' => $icon_name ) - ); - - $this->registered_icons[ $icon_name ] = $icon; - - return true; - } - - /** - * Redefined to be accessible to `get_registered_icon` and `get_registered_icons` - */ - protected function get_content( $icon_name ) { - if ( ! isset( $this->registered_icons[ $icon_name ]['content'] ) ) { - $content = file_get_contents( - $this->registered_icons[ $icon_name ]['filePath'] - ); - $content = $this->sanitize_icon_content( $content ); - - if ( empty( $content ) ) { - wp_trigger_error( - __METHOD__, - __( 'Icon content does not contain valid SVG markup.', 'gutenberg' ) - ); - return null; - } - - $this->registered_icons[ $icon_name ]['content'] = $content; - } - return $this->registered_icons[ $icon_name ]['content']; - } - - /** - * Redefined to be accessible to `register` and `get_content` - */ - protected function sanitize_icon_content( $icon_content ) { - $allowed_tags = array( - 'svg' => array( - 'class' => true, - 'xmlns' => true, - 'width' => true, - 'height' => true, - 'viewbox' => true, - 'aria-hidden' => true, - 'role' => true, - 'focusable' => true, - ), - 'path' => array( - 'fill' => true, - 'fill-rule' => true, - 'd' => true, - 'transform' => true, - ), - 'polygon' => array( - 'fill' => true, - 'fill-rule' => true, - 'points' => true, - 'transform' => true, - 'focusable' => true, - ), - ); - return wp_kses( $icon_content, $allowed_tags ); - } - - /** - * Redefined to access the new `$registered_icons` - */ - public function get_registered_icon( $icon_name ) { - if ( ! $this->is_registered( $icon_name ) ) { - return null; - } - - $icon = $this->registered_icons[ $icon_name ]; - $icon['content'] = $icon['content'] ?? $this->get_content( $icon_name ); - - return $icon; - } - - public function is_registered( $icon_name ) { - return isset( $this->registered_icons[ $icon_name ] ); - } } From 4445a89bd1f3d69a8e0364a54d421697523714f0 Mon Sep 17 00:00:00 2001 From: Miguel Fonseca <150562+mcsf@users.noreply.github.com> Date: Wed, 11 Mar 2026 14:34:32 +0000 Subject: [PATCH 6/6] Add test case for searching in icon labels --- .../class-wp-rest-icon-controller-test.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/phpunit/experimental/class-wp-rest-icon-controller-test.php b/phpunit/experimental/class-wp-rest-icon-controller-test.php index 1b6f43024f323c..885c88c85f0e3a 100644 --- a/phpunit/experimental/class-wp-rest-icon-controller-test.php +++ b/phpunit/experimental/class-wp-rest-icon-controller-test.php @@ -150,6 +150,23 @@ public function test_get_items_search_filters_results() { $this->assertContains( 'core/arrow-left', $icon_names, 'Search results should include core/arrow-left icon' ); } + /** + * Test that GET /wp/v2/icons/?search=%s searches icon labels too. + */ + public function test_get_items_search_includes_label() { + wp_set_current_user( self::$editor_id ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/icons' ); + + // The '@' character is only found in the *label* for core/at-symbol + $request->set_param( 'search', '@' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status() ); + $this->assertEquals( array( 'core/at-symbol' ), array_column( $data, 'name' ) ); + } + /** * Test that search is case-insensitive. */