From e1309056681d2a23d24d7096b5fdd5e6021ef599 Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Thu, 11 Jun 2026 15:42:48 +0900 Subject: [PATCH 1/9] Icons: Use snake_case file_path internally in the icon registry. Align the icon registry's internal property with WordPress core's snake_case array-key convention by renaming the registered icon property from filePath to file_path. The conversion now happens only at the registration/mapping boundary in the constructor: the generated manifest keeps the upstream camelCase `filePath` key (as produced by Gutenberg), which is read and mapped to the internal `file_path` property when each icon is registered. This updates the registry validation, allowed property keys, `get_content()` lookup, and the related docblocks and error messages, while leaving the manifest and the copy:icon-library-manifest Grunt task untouched. Co-Authored-By: Claude --- src/wp-includes/class-wp-icons-registry.php | 24 ++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/wp-includes/class-wp-icons-registry.php b/src/wp-includes/class-wp-icons-registry.php index f82739fc5d91d..a442a2f99af94 100644 --- a/src/wp-includes/class-wp-icons-registry.php +++ b/src/wp-includes/class-wp-icons-registry.php @@ -81,8 +81,8 @@ protected function __construct() { $this->register( 'core/' . $icon_name, array( - 'label' => $icon_data['label'], - 'filePath' => $icons_directory . $icon_data['filePath'], + 'label' => $icon_data['label'], + 'file_path' => $icons_directory . $icon_data['filePath'], ) ); } @@ -97,11 +97,11 @@ protected function __construct() { * @param array $icon_properties { * List of properties for the icon. * - * @type string $label Required. A human-readable label for the icon. - * @type string $content Optional. SVG markup for the icon. - * If not provided, the content will be retrieved from the `filePath` if set. - * If both `content` and `filePath` are not set, the icon will not be registered. - * @type string $filePath Optional. The full path to the file containing the icon content. + * @type string $label Required. A human-readable label for the icon. + * @type string $content Optional. SVG markup for the icon. + * If not provided, the content will be retrieved from the `file_path` if set. + * If both `content` and `file_path` are not set, the icon will not be registered. + * @type string $file_path Optional. The full path to the file containing the icon content. * } * @return bool True if the icon was registered with success and false otherwise. */ @@ -115,7 +115,7 @@ protected function register( $icon_name, $icon_properties ) { return false; } - $allowed_keys = array_fill_keys( array( 'label', 'content', 'filePath' ), 1 ); + $allowed_keys = array_fill_keys( array( 'label', 'content', 'file_path' ), 1 ); foreach ( array_keys( $icon_properties ) as $key ) { if ( ! array_key_exists( $key, $allowed_keys ) ) { _doing_it_wrong( @@ -141,12 +141,12 @@ protected function register( $icon_name, $icon_properties ) { } if ( - ( ! isset( $icon_properties['content'] ) && ! isset( $icon_properties['filePath'] ) ) || - ( isset( $icon_properties['content'] ) && isset( $icon_properties['filePath'] ) ) + ( ! isset( $icon_properties['content'] ) && ! isset( $icon_properties['file_path'] ) ) || + ( isset( $icon_properties['content'] ) && isset( $icon_properties['file_path'] ) ) ) { _doing_it_wrong( __METHOD__, - __( 'Icons must provide either `content` or `filePath`.' ), + __( 'Icons must provide either `content` or `file_path`.' ), '7.0.0' ); return false; @@ -234,7 +234,7 @@ protected function sanitize_icon_content( $icon_content ) { protected function get_content( $icon_name ) { if ( ! isset( $this->registered_icons[ $icon_name ]['content'] ) ) { $content = file_get_contents( - $this->registered_icons[ $icon_name ]['filePath'] + $this->registered_icons[ $icon_name ]['file_path'] ); $content = $this->sanitize_icon_content( $content ); From d08cbfa12bfba43b9d16b8cd53c32d29744a1a18 Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Thu, 11 Jun 2026 17:56:58 +0900 Subject: [PATCH 2/9] Icons: Add APIs for collection and icon registration. Add the public icon collection and icon registration APIs on top of the snake_case icon registry. Introduces WP_Icon_Collections_Registry, icons.php (wp_register_icon/wp_unregister_icon/wp_register_icon_collection and the default registration callbacks), the collection-scoped REST routes, and the related tests. Icon properties use the internal snake_case `file_path` key established in the icon registry; the Gutenberg manifest's upstream camelCase `filePath` key is read and mapped to `file_path` at the registration boundary. Co-Authored-By: Claude --- .../class-wp-icon-collections-registry.php | 221 ++++++++++++++++ src/wp-includes/class-wp-icons-registry.php | 179 +++++++------ src/wp-includes/default-filters.php | 4 + src/wp-includes/icons.php | 142 ++++++++++ .../class-wp-rest-icons-controller.php | 84 ++++-- src/wp-settings.php | 2 + .../tests/icons/wpIconCollectionsRegistry.php | 178 +++++++++++++ tests/phpunit/tests/icons/wpIconsRegistry.php | 243 ++++++++++++++++++ .../tests/icons/wpRestIconsController.php | 79 ++++++ 9 files changed, 1038 insertions(+), 94 deletions(-) create mode 100644 src/wp-includes/class-wp-icon-collections-registry.php create mode 100644 src/wp-includes/icons.php create mode 100644 tests/phpunit/tests/icons/wpIconCollectionsRegistry.php create mode 100644 tests/phpunit/tests/icons/wpIconsRegistry.php diff --git a/src/wp-includes/class-wp-icon-collections-registry.php b/src/wp-includes/class-wp-icon-collections-registry.php new file mode 100644 index 0000000000000..4129bb0264e90 --- /dev/null +++ b/src/wp-includes/class-wp-icon-collections-registry.php @@ -0,0 +1,221 @@ +is_registered( $collection_slug ) ) { + _doing_it_wrong( + __METHOD__, + __( 'Icon collection is already registered.' ), + '7.1.0' + ); + return false; + } + + if ( ! is_array( $collection_properties ) ) { + _doing_it_wrong( + __METHOD__, + __( 'Icon collection properties must be an array.' ), + '7.1.0' + ); + return false; + } + + $allowed_keys = array_fill_keys( array( 'label', 'description' ), 1 ); + foreach ( array_keys( $collection_properties ) as $key ) { + if ( ! array_key_exists( $key, $allowed_keys ) ) { + _doing_it_wrong( + __METHOD__, + sprintf( + /* translators: %s: The name of a user-provided key. */ + __( 'Invalid icon collection property: "%s".' ), + $key + ), + '7.1.0' + ); + return false; + } + } + + if ( ! isset( $collection_properties['label'] ) || ! is_string( $collection_properties['label'] ) ) { + _doing_it_wrong( + __METHOD__, + __( 'Icon collection label must be a string.' ), + '7.1.0' + ); + return false; + } + + $defaults = array( + 'description' => '', + ); + + $collection = array_merge( + $defaults, + $collection_properties, + array( 'slug' => $collection_slug ) + ); + + $this->registered_collections[ $collection_slug ] = $collection; + + return true; + } + + /** + * Unregisters an icon collection. + * + * Any icons registered under the given collection are also unregistered. + * + * @since 7.1.0 + * + * @param string $collection_slug Icon collection slug. + * @return bool True if the collection was unregistered successfully, false otherwise. + */ + public function unregister( $collection_slug ) { + if ( ! $this->is_registered( $collection_slug ) ) { + _doing_it_wrong( + __METHOD__, + sprintf( + /* translators: %s: Icon collection slug. */ + __( 'Icon collection "%s" not found.' ), + $collection_slug + ), + '7.1.0' + ); + return false; + } + + $icons_registry = WP_Icons_Registry::get_instance(); + $prefix = $collection_slug . '/'; + foreach ( $icons_registry->get_registered_icons() as $icon ) { + if ( isset( $icon['collection'] ) && $icon['collection'] === $collection_slug ) { + $unqualified_name = substr( $icon['name'], strlen( $prefix ) ); + $icons_registry->unregister( $unqualified_name, $collection_slug ); + } + } + + unset( $this->registered_collections[ $collection_slug ] ); + + return true; + } + + /** + * Retrieves an array containing the properties of a registered icon collection. + * + * @since 7.1.0 + * + * @param string $collection_slug Icon collection slug. + * @return array|null Registered collection properties, or `null` if the collection is not registered. + */ + public function get_registered( $collection_slug ) { + if ( ! $this->is_registered( $collection_slug ) ) { + return null; + } + + return $this->registered_collections[ $collection_slug ]; + } + + /** + * Retrieves all registered icon collections. + * + * @since 7.1.0 + * + * @return array[] Array of arrays containing the registered icon collections properties. + */ + public function get_all_registered() { + return array_values( $this->registered_collections ); + } + + /** + * Checks if an icon collection is registered. + * + * @since 7.1.0 + * + * @param string|null $collection_slug Icon collection slug. + * @return bool True if the icon collection is registered, false otherwise. + */ + public function is_registered( $collection_slug ) { + return isset( $collection_slug, $this->registered_collections[ $collection_slug ] ); + } + + /** + * Utility method to retrieve the main instance of the class. + * + * The instance will be created if it does not exist yet. + * + * @since 7.1.0 + * + * @return WP_Icon_Collections_Registry The main instance. + */ + public static function get_instance() { + if ( null === self::$instance ) { + self::$instance = new self(); + } + + return self::$instance; + } +} diff --git a/src/wp-includes/class-wp-icons-registry.php b/src/wp-includes/class-wp-icons-registry.php index a442a2f99af94..d223eda879b1c 100644 --- a/src/wp-includes/class-wp-icons-registry.php +++ b/src/wp-includes/class-wp-icons-registry.php @@ -16,15 +16,14 @@ class WP_Icons_Registry { /** * Registered icons array. * - * @since 7.0.0 * @var array[] */ protected $registered_icons = array(); + /** * Container for the main instance of the class. * - * @since 7.0.0 * @var WP_Icons_Registry|null */ protected static $instance = null; @@ -34,78 +33,32 @@ class WP_Icons_Registry { * * WP_Icons_Registry is a singleton class, so keep this protected. * - * For 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 (Gutenberg repository) as - * SVG files and as entries in a single manifest file. On init, the - * registry is loaded with those icons listed in the manifest. - * - * @since 7.0.0 + * Icons are populated via `_wp_register_default_icons()` during the + * `init` action. Third-party icons can be registered via + * {@see wp_register_icon()} once their collection is registered. */ - protected function __construct() { - $icons_directory = __DIR__ . '/images/icon-library/'; - $manifest_path = __DIR__ . '/assets/icon-library-manifest.php'; - - if ( ! is_readable( $manifest_path ) ) { - wp_trigger_error( - __METHOD__, - __( 'Core icon collection manifest is missing or unreadable.' ) - ); - return; - } - - $collection = include $manifest_path; - - if ( empty( $collection ) ) { - wp_trigger_error( - __METHOD__, - __( 'Core icon collection manifest is empty or invalid.' ) - ); - return; - } - - foreach ( $collection as $icon_name => $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.' ), - '7.0.0' - ); - return; - } - - $this->register( - 'core/' . $icon_name, - array( - 'label' => $icon_data['label'], - 'file_path' => $icons_directory . $icon_data['filePath'], - ) - ); - } - } + protected function __construct() {} /** * Registers an icon. * * @since 7.0.0 + * @since 7.1.0 `collection` is required. The icon name no longer includes a namespace. * - * @param string $icon_name Icon name including namespace. + * @param string $icon_name Icon name (e.g. "arrow-left"). Must not contain a namespace prefix. * @param array $icon_properties { * List of properties for the icon. * - * @type string $label Required. A human-readable label for the icon. - * @type string $content Optional. SVG markup for the icon. - * If not provided, the content will be retrieved from the `file_path` if set. - * If both `content` and `file_path` are not set, the icon will not be registered. - * @type string $file_path Optional. The full path to the file containing the icon content. + * @type string $label Required. A human-readable label for the icon. + * @type string $collection Required. The slug of a registered icon collection that this icon belongs to. + * @type string $content Optional. SVG markup for the icon. + * If not provided, the content will be retrieved from the `file_path` if set. + * If both `content` and `file_path` are not set, the icon will not be registered. + * @type string $file_path Optional. The full path to the file containing the icon content. * } * @return bool True if the icon was registered with success and false otherwise. */ - protected function register( $icon_name, $icon_properties ) { + public function register( $icon_name, $icon_properties ) { if ( ! isset( $icon_name ) || ! is_string( $icon_name ) ) { _doing_it_wrong( __METHOD__, @@ -115,13 +68,31 @@ protected function register( $icon_name, $icon_properties ) { return false; } - $allowed_keys = array_fill_keys( array( 'label', 'content', 'file_path' ), 1 ); + if ( ! is_array( $icon_properties ) ) { + _doing_it_wrong( + __METHOD__, + __( 'Icon properties must be an array.' ), + '7.1.0' + ); + return false; + } + + if ( ! preg_match( '/^[a-z][a-z0-9-]*$/', $icon_name ) ) { + _doing_it_wrong( + __METHOD__, + __( 'Icon names must start with a lowercase letter and contain only lowercase letters, digits, and hyphens.' ), + '7.1.0' + ); + return false; + } + + $allowed_keys = array_fill_keys( array( 'label', 'content', 'file_path', 'collection' ), 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 + /* translators: %s: The name of a user-provided key. */ __( 'Invalid icon property: "%s".' ), $key ), @@ -131,6 +102,28 @@ protected function register( $icon_name, $icon_properties ) { } } + if ( ! isset( $icon_properties['collection'] ) || ! is_string( $icon_properties['collection'] ) ) { + _doing_it_wrong( + __METHOD__, + __( 'Icon collection is required and must be a string.' ), + '7.1.0' + ); + return false; + } + + if ( ! WP_Icon_Collections_Registry::get_instance()->is_registered( $icon_properties['collection'] ) ) { + _doing_it_wrong( + __METHOD__, + sprintf( + /* translators: %s: Icon collection slug. */ + __( 'Icon collection "%s" is not registered.' ), + $icon_properties['collection'] + ), + '7.1.0' + ); + return false; + } + if ( ! isset( $icon_properties['label'] ) || ! is_string( $icon_properties['label'] ) ) { _doing_it_wrong( __METHOD__, @@ -173,13 +166,53 @@ protected function register( $icon_name, $icon_properties ) { } } + $qualified_name = $icon_properties['collection'] . '/' . $icon_name; + + if ( $this->is_registered( $qualified_name ) ) { + _doing_it_wrong( + __METHOD__, + __( 'Icon is already registered.' ), + '7.1.0' + ); + return false; + } + $icon = array_merge( $icon_properties, - array( 'name' => $icon_name ) + array( 'name' => $qualified_name ) ); - $this->registered_icons[ $icon_name ] = $icon; + $this->registered_icons[ $qualified_name ] = $icon; + + return true; + } + + /** + * Unregisters an icon. + * + * @since 7.1.0 + * + * @param string $icon_name Icon name (e.g. "arrow-left"). Must not contain a namespace prefix. + * @param string $collection Slug of the collection the icon belongs to. + * @return bool True if the icon was unregistered successfully, false otherwise. + */ + public function unregister( $icon_name, $collection ) { + $qualified_name = $collection . '/' . $icon_name; + if ( ! $this->is_registered( $qualified_name ) ) { + _doing_it_wrong( + __METHOD__, + sprintf( + /* translators: %s: Icon name. */ + __( 'Icon "%s" is not registered.' ), + $qualified_name + ), + '7.1.0' + ); + return false; + } + + unset( $this->registered_icons[ $qualified_name ] ); return true; } @@ -189,8 +222,6 @@ protected function register( $icon_name, $icon_properties ) { * Logic borrowed from twentytwenty. * @see twentytwenty_get_theme_svg * - * @since 7.0.0 - * * @param string $icon_content The icon SVG content to sanitize. * @return string The sanitized icon SVG content. */ @@ -226,8 +257,6 @@ protected function sanitize_icon_content( $icon_content ) { /** * Retrieves the content of a registered icon. * - * @since 7.0.0 - * * @param string $icon_name Icon name including namespace. * @return string|null The content of the icon, if found. */ @@ -254,7 +283,6 @@ protected function get_content( $icon_name ) { /** * Retrieves an array containing the properties of a registered icon. * - * @since 7.0.0 * * @param string $icon_name Icon name including namespace. * @return array|null Registered icon properties or `null` if the icon is not registered. @@ -274,6 +302,7 @@ public function get_registered_icon( $icon_name ) { * Retrieves all registered icons. * * @since 7.0.0 + * @since 7.1.0 Search also matches icon labels. * * @param string $search Optional. Search term by which to filter the icons. * @return array[] Array of arrays containing the registered icon properties. @@ -282,8 +311,12 @@ public function get_registered_icons( $search = '' ) { $icons = array(); foreach ( $this->registered_icons as $icon ) { - if ( ! empty( $search ) && false === stripos( $icon['name'], $search ) ) { - continue; + if ( ! empty( $search ) ) { + $matches_name = false !== stripos( $icon['name'], $search ); + $matches_label = isset( $icon['label'] ) && false !== stripos( $icon['label'], $search ); + if ( ! $matches_name && ! $matches_label ) { + continue; + } } $icon['content'] = $icon['content'] ?? $this->get_content( $icon['name'] ); @@ -296,7 +329,6 @@ public function get_registered_icons( $search = '' ) { /** * Checks if an icon is registered. * - * @since 7.0.0 * * @param string $icon_name Icon name including namespace. * @return bool True if the icon is registered, false otherwise. @@ -310,7 +342,6 @@ public function is_registered( $icon_name ) { * * The instance will be created if it does not exist yet. * - * @since 7.0.0 * * @return WP_Icons_Registry The main instance. */ diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index 5581828a10b61..749535ba8fa66 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -813,6 +813,10 @@ add_action( 'before_delete_post', '_wp_before_delete_font_face', 10, 2 ); add_action( 'init', '_wp_register_default_font_collections' ); +// Icons. +add_action( 'init', '_wp_register_default_icon_collections', 0 ); +add_action( 'init', '_wp_register_default_icons' ); + // Add ignoredHookedBlocks metadata attribute to the template and template part post types. add_filter( 'rest_pre_insert_wp_template', 'inject_ignored_hooked_blocks_metadata_attributes' ); add_filter( 'rest_pre_insert_wp_template_part', 'inject_ignored_hooked_blocks_metadata_attributes' ); diff --git a/src/wp-includes/icons.php b/src/wp-includes/icons.php new file mode 100644 index 0000000000000..9c7835b5cc084 --- /dev/null +++ b/src/wp-includes/icons.php @@ -0,0 +1,142 @@ +register( $slug, $args ); +} + +/** + * Unregisters an icon collection. + * + * @since 7.1.0 + * + * @param string $slug Icon collection slug. + * @return bool True if the icon collection was unregistered successfully, else false. + */ +function wp_unregister_icon_collection( $slug ) { + return WP_Icon_Collections_Registry::get_instance()->unregister( $slug ); +} + +/** + * Registers a new icon. + * + * @since 7.1.0 + * + * @param string $icon_name Icon name (e.g. "arrow-left"). + * @param string $collection Slug of a registered icon collection that this icon belongs to. + * @param array $args { + * List of properties for the icon. + * + * @type string $label Required. A human-readable label for the icon. + * @type string $content Optional. SVG markup for the icon. + * If not provided, the content will be retrieved from the `file_path` if set. + * If both `content` and `file_path` are not set, the icon will not be registered. + * @type string $file_path Optional. The full path to the file containing the icon content. + * } + * @return bool True if the icon was registered successfully, else false. + */ +function wp_register_icon( $icon_name, $collection, $args ) { + $args['collection'] = $collection; + return WP_Icons_Registry::get_instance()->register( $icon_name, $args ); +} + +/** + * Unregisters an icon. + * + * @since 7.1.0 + * + * @param string $icon_name Icon name (e.g. "arrow-left"). + * @param string $collection Slug of the collection the icon belongs to. + * @return bool True if the icon was unregistered successfully, else false. + */ +function wp_unregister_icon( $icon_name, $collection ) { + return WP_Icons_Registry::get_instance()->unregister( $icon_name, $collection ); +} + +/** + * Registers the default icon collections. + * + * @since 7.1.0 + * @access private + */ +function _wp_register_default_icon_collections() { + wp_register_icon_collection( + 'core', + array( + 'label' => __( 'WordPress' ), + 'description' => __( 'Default icon collection.' ), + ) + ); +} + +/** + * Registers the default core icons from the manifest. + * + * @since 7.1.0 + * @access private + */ +function _wp_register_default_icons() { + $icons_directory = ABSPATH . WPINC . '/images/icon-library/'; + $manifest_path = ABSPATH . WPINC . '/assets/icon-library-manifest.php'; + + if ( ! is_readable( $manifest_path ) ) { + wp_trigger_error( + __FUNCTION__, + __( 'Core icon collection manifest is missing or unreadable.' ) + ); + return; + } + + $collection = include $manifest_path; + + if ( empty( $collection ) ) { + wp_trigger_error( + __FUNCTION__, + __( 'Core icon collection manifest is empty or invalid.' ) + ); + return; + } + + foreach ( $collection as $icon_name => $icon_data ) { + if ( + empty( $icon_data['filePath'] ) + || ! is_string( $icon_data['filePath'] ) + ) { + _doing_it_wrong( + __FUNCTION__, + __( 'Core icon collection manifest must provide a valid "filePath" for each icon.' ), + '7.0.0' + ); + return; + } + + wp_register_icon( + $icon_name, + 'core', + array( + 'label' => $icon_data['label'], + 'file_path' => $icons_directory . $icon_data['filePath'], + ) + ); + } +} diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-icons-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-icons-controller.php index 91126b498d338..08f4a2be37100 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-icons-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-icons-controller.php @@ -1,4 +1,5 @@ namespace = 'wp/v2'; @@ -33,6 +30,7 @@ public function __construct() { * Registers the routes for the objects of the controller. * * @since 7.0.0 + * @since 7.1.0 Added the `/icons/` collection-scoped route. */ public function register_routes() { register_rest_route( @@ -49,6 +47,26 @@ public function register_routes() { ) ); + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[a-z][a-z-]*)', + array( + 'args' => array( + 'namespace' => array( + 'description' => __( 'Icon collection slug.' ), + 'type' => 'string', + ), + ), + 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' ), + ) + ); + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[a-z][a-z0-9-]*/[a-z][a-z0-9-]*)', @@ -75,8 +93,6 @@ public function register_routes() { /** * Checks whether a given request has permission to read icons. * - * @since 7.0.0 - * * @param WP_REST_Request $request Full details about the request. * @return true|WP_Error True if the request has read access, WP_Error object otherwise. */ @@ -104,8 +120,6 @@ public function get_items_permissions_check( /** * Checks if a given request has access to read a specific icon. * - * @since 7.0.0 - * * @param WP_REST_Request $request Full details about the request. * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise. */ @@ -119,18 +133,37 @@ public function get_item_permissions_check( $request ) { } /** - * Retrieves all icons. + * Retrieves all icons, optionally scoped to a collection. * * @since 7.0.0 + * @since 7.1.0 Supports filtering by collection via the `namespace` URL segment. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function get_items( $request ) { + $collection = $request->get_param( 'namespace' ); + + if ( null !== $collection && ! WP_Icon_Collections_Registry::get_instance()->is_registered( $collection ) ) { + return new WP_Error( + 'rest_icon_collection_not_found', + sprintf( + /* translators: %s: Icon collection slug. */ + __( 'Icon collection not found: "%s".' ), + $collection + ), + array( 'status' => 404 ) + ); + } + $response = array(); $search = $request->get_param( 'search' ); $icons = WP_Icons_Registry::get_instance()->get_registered_icons( $search ); + foreach ( $icons as $icon ) { + if ( null !== $collection && ( ! isset( $icon['collection'] ) || $icon['collection'] !== $collection ) ) { + continue; + } $prepared_icon = $this->prepare_item_for_response( $icon, $request ); $response[] = $this->prepare_response_for_collection( $prepared_icon ); } @@ -140,8 +173,6 @@ public function get_items( $request ) { /** * Retrieves a specific icon. * - * @since 7.0.0 - * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ @@ -158,8 +189,6 @@ public function get_item( $request ) { /** * Retrieves a specific icon from the registry. * - * @since 7.0.0 - * * @param string $name Icon name. * @return array|WP_Error Icon data on success, or WP_Error object on failure. */ @@ -186,6 +215,7 @@ public function get_icon( $name ) { * Prepare a raw icon before it gets output in a REST API response. * * @since 7.0.0 + * @since 7.1.0 Added the `collection` field. * * @param array $item Raw icon as registered, before any changes. * @param WP_REST_Request $request Request object. @@ -194,9 +224,10 @@ public function get_icon( $name ) { public function prepare_item_for_response( $item, $request ) { $fields = $this->get_fields_for_response( $request ); $keys = array( - 'name' => 'name', - 'label' => 'label', - 'content' => 'content', + 'name' => 'name', + 'label' => 'label', + 'content' => 'content', + 'collection' => 'collection', ); $data = array(); foreach ( $keys as $item_key => $rest_key ) { @@ -215,6 +246,7 @@ public function prepare_item_for_response( $item, $request ) { * Retrieves the icon schema, conforming to JSON Schema. * * @since 7.0.0 + * @since 7.1.0 Added the `collection` property. * * @return array Item schema data. */ @@ -228,24 +260,30 @@ public function get_item_schema() { 'title' => 'icon', 'type' => 'object', 'properties' => array( - 'name' => array( + 'name' => array( 'description' => __( 'The icon name.' ), 'type' => 'string', 'readonly' => true, 'context' => array( 'view', 'edit', 'embed' ), ), - 'label' => array( + 'label' => array( 'description' => __( 'The icon label.' ), 'type' => 'string', 'readonly' => true, 'context' => array( 'view', 'edit', 'embed' ), ), - 'content' => array( + 'content' => array( 'description' => __( 'The icon content (SVG markup).' ), 'type' => 'string', 'readonly' => true, 'context' => array( 'view', 'edit', 'embed' ), ), + 'collection' => array( + 'description' => __( 'The slug of the collection this icon belongs to.' ), + 'type' => 'string', + 'readonly' => true, + 'context' => array( 'view', 'edit', 'embed' ), + ), ), ); @@ -258,12 +296,18 @@ public function get_item_schema() { * Retrieves the query params for the icons collection. * * @since 7.0.0 + * @since 7.1.0 Added the `namespace` parameter. * * @return array Collection parameters. */ public function get_collection_params() { $query_params = parent::get_collection_params(); $query_params['context']['default'] = 'view'; + $query_params['namespace'] = array( + 'description' => __( 'Limit results to icons belonging to the given collection slug.' ), + 'type' => 'string', + 'pattern' => '^[a-z][a-z-]*$', + ); return $query_params; } } diff --git a/src/wp-settings.php b/src/wp-settings.php index ef5c7784ee561..1fe638380e122 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -297,7 +297,9 @@ require ABSPATH . WPINC . '/ai-client.php'; require ABSPATH . WPINC . '/class-wp-connector-registry.php'; require ABSPATH . WPINC . '/connectors.php'; +require ABSPATH . WPINC . '/class-wp-icon-collections-registry.php'; require ABSPATH . WPINC . '/class-wp-icons-registry.php'; +require ABSPATH . WPINC . '/icons.php'; require ABSPATH . WPINC . '/widgets.php'; require ABSPATH . WPINC . '/class-wp-widget.php'; require ABSPATH . WPINC . '/class-wp-widget-factory.php'; diff --git a/tests/phpunit/tests/icons/wpIconCollectionsRegistry.php b/tests/phpunit/tests/icons/wpIconCollectionsRegistry.php new file mode 100644 index 0000000000000..4f1698e589062 --- /dev/null +++ b/tests/phpunit/tests/icons/wpIconCollectionsRegistry.php @@ -0,0 +1,178 @@ +collections = WP_Icon_Collections_Registry::get_instance(); + } + + public function tear_down() { + foreach ( array( 'plugin-a', 'plugin-b', 'my-collection' ) as $slug ) { + if ( $this->collections->is_registered( $slug ) ) { + $this->collections->unregister( $slug ); + } + } + parent::tear_down(); + } + + /** + * @ticket 64847 + * + * @covers ::register + */ + public function test_register_collection() { + $result = $this->collections->register( + 'my-collection', + array( + 'label' => 'My Collection', + 'description' => 'A collection.', + ) + ); + + $this->assertTrue( $result ); + $this->assertTrue( $this->collections->is_registered( 'my-collection' ) ); + + $registered = $this->collections->get_registered( 'my-collection' ); + $this->assertSame( 'my-collection', $registered['slug'] ); + $this->assertSame( 'My Collection', $registered['label'] ); + $this->assertSame( 'A collection.', $registered['description'] ); + } + + /** + * @ticket 64847 + * + * @dataProvider data_invalid_collection_slugs + * + * @covers ::register + * + * @expectedIncorrectUsage WP_Icon_Collections_Registry::register + * + * @param mixed $slug Invalid slug candidate. + */ + public function test_register_rejects_invalid_slug( $slug ) { + $result = $this->collections->register( $slug, array( 'label' => 'X' ) ); + $this->assertFalse( $result ); + } + + /** + * Data provider for invalid collection slug candidates. + * + * Collection slugs must be strings that start with a lowercase letter + * and contain only lowercase letters and hyphens (no digits, no slashes, + * no uppercase characters). + * + * @return array[] + */ + public function data_invalid_collection_slugs() { + return array( + 'non-string slug' => array( 1 ), + 'contains slash' => array( 'plugin/icons' ), + 'contains digits' => array( 'plugin1' ), + 'uppercase characters' => array( 'Plugin' ), + 'leading hyphen' => array( '-plugin' ), + ); + } + + /** + * @ticket 64847 + * + * @covers ::register + * + * @expectedIncorrectUsage WP_Icon_Collections_Registry::register + */ + public function test_register_twice_fails() { + $this->assertTrue( $this->collections->register( 'my-collection', array( 'label' => 'A' ) ) ); + $this->assertFalse( $this->collections->register( 'my-collection', array( 'label' => 'A' ) ) ); + } + + /** + * @ticket 64847 + * + * @covers ::register + * + * @expectedIncorrectUsage WP_Icon_Collections_Registry::register + */ + public function test_register_rejects_unknown_property() { + $result = $this->collections->register( + 'my-collection', + array( + 'label' => 'A', + 'bogus' => 'nope', + ) + ); + $this->assertFalse( $result ); + } + + /** + * @ticket 64847 + * + * @covers ::unregister + */ + public function test_unregister_collection_cascades_to_icons() { + $this->collections->register( 'plugin-a', array( 'label' => 'A' ) ); + $this->collections->register( 'plugin-b', array( 'label' => 'B' ) ); + + $icons = WP_Icons_Registry::get_instance(); + $icons->register( + 'alpha', + array( + 'label' => 'Alpha', + 'content' => '', + 'collection' => 'plugin-a', + ) + ); + $icons->register( + 'beta', + array( + 'label' => 'Beta', + 'content' => '', + 'collection' => 'plugin-a', + ) + ); + $icons->register( + 'gamma', + array( + 'label' => 'Gamma', + 'content' => '', + 'collection' => 'plugin-b', + ) + ); + + $this->assertTrue( $icons->is_registered( 'plugin-a/alpha' ) ); + $this->assertTrue( $icons->is_registered( 'plugin-a/beta' ) ); + + $this->assertTrue( $this->collections->unregister( 'plugin-a' ) ); + + $this->assertFalse( $icons->is_registered( 'plugin-a/alpha' ) ); + $this->assertFalse( $icons->is_registered( 'plugin-a/beta' ) ); + $this->assertTrue( $icons->is_registered( 'plugin-b/gamma' ) ); + + $icons->unregister( 'gamma', 'plugin-b' ); + } + + /** + * @ticket 64847 + * + * @covers ::unregister + * + * @expectedIncorrectUsage WP_Icon_Collections_Registry::unregister + */ + public function test_unregister_unknown_collection() { + $this->assertFalse( $this->collections->unregister( 'ghost' ) ); + } +} diff --git a/tests/phpunit/tests/icons/wpIconsRegistry.php b/tests/phpunit/tests/icons/wpIconsRegistry.php new file mode 100644 index 0000000000000..d8affd26690b0 --- /dev/null +++ b/tests/phpunit/tests/icons/wpIconsRegistry.php @@ -0,0 +1,243 @@ +registry = WP_Icons_Registry::get_instance(); + + $collections = WP_Icon_Collections_Registry::get_instance(); + if ( ! $collections->is_registered( 'test-collection' ) ) { + $collections->register( 'test-collection', array( 'label' => 'Test Plugin' ) ); + } + } + + public function tear_down() { + $reflection = new ReflectionClass( WP_Icons_Registry::class ); + $instance_property = $reflection->getProperty( 'instance' ); + if ( PHP_VERSION_ID < 80100 ) { + $instance_property->setAccessible( true ); + } + $instance_property->setValue( null, null ); + + $collections = WP_Icon_Collections_Registry::get_instance(); + if ( $collections->is_registered( 'test-collection' ) ) { + $collections->unregister( 'test-collection' ); + } + if ( $collections->is_registered( 'other-collection' ) ) { + $collections->unregister( 'other-collection' ); + } + + $this->registry = null; + parent::tear_down(); + } + + /** + * @ticket 64651 + * + * @covers ::register + */ + public function test_register_icon() { + $result = $this->registry->register( + 'my-icon', + array( + 'label' => 'My Icon', + 'content' => '', + 'collection' => 'test-collection', + ) + ); + + $this->assertTrue( $result ); + $this->assertTrue( $this->registry->is_registered( 'test-collection/my-icon' ) ); + } + + public function data_invalid_icon_names() { + return array( + 'non-string name' => array( 1 ), + 'contains slash' => array( 'test-collection/plus' ), + 'uppercase characters' => array( 'Plus' ), + 'invalid characters' => array( '_doing_it_wrong' ), + ); + } + + /** + * @ticket 64651 + * + * @covers ::register + * + * @expectedIncorrectUsage WP_Icons_Registry::register + */ + public function test_register_icon_twice() { + $settings = array( + 'label' => 'Icon', + 'content' => '', + 'collection' => 'test-collection', + ); + + $this->assertTrue( $this->registry->register( 'duplicate', $settings ) ); + $this->assertFalse( $this->registry->register( 'duplicate', $settings ) ); + } + + /** + * @ticket 64651 + * + * @dataProvider data_invalid_icon_names + * + * @covers ::register + * + * @expectedIncorrectUsage WP_Icons_Registry::register + * + * @param mixed $name Invalid icon name candidate. + */ + public function test_register_invalid_name( $name ) { + $result = $this->registry->register( + $name, + array( + 'label' => 'Icon', + 'content' => '', + 'collection' => 'test-collection', + ) + ); + $this->assertFalse( $result ); + } + + /** + * @ticket 64651 + * + * @covers ::register + * + * @expectedIncorrectUsage WP_Icons_Registry::register + */ + public function test_register_requires_collection() { + $result = $this->registry->register( + 'my-icon', + array( + 'label' => 'Icon', + 'content' => '', + ) + ); + $this->assertFalse( $result ); + } + + /** + * @ticket 64651 + * + * @covers ::register + * + * @expectedIncorrectUsage WP_Icons_Registry::register + */ + public function test_register_rejects_non_string_collection() { + $result = $this->registry->register( + 'my-icon', + array( + 'label' => 'Icon', + 'content' => '', + 'collection' => 123, + ) + ); + $this->assertFalse( $result ); + } + + /** + * @ticket 64651 + * + * @covers ::register + * + * @expectedIncorrectUsage WP_Icons_Registry::register + */ + public function test_register_rejects_unregistered_collection() { + $result = $this->registry->register( + 'my-icon', + array( + 'label' => 'Icon', + 'content' => '', + 'collection' => 'unregistered-collection', + ) + ); + $this->assertFalse( $result ); + } + + /** + * @ticket 64651 + * + * @covers ::register + */ + public function test_same_name_across_collections_does_not_collide() { + $collections = WP_Icon_Collections_Registry::get_instance(); + $collections->register( 'other-collection', array( 'label' => 'Other' ) ); + + $this->assertTrue( + $this->registry->register( + 'shared', + array( + 'label' => 'Shared A', + 'content' => '', + 'collection' => 'test-collection', + ) + ) + ); + $this->assertTrue( + $this->registry->register( + 'shared', + array( + 'label' => 'Shared B', + 'content' => '', + 'collection' => 'other-collection', + ) + ) + ); + + $this->assertTrue( $this->registry->is_registered( 'test-collection/shared' ) ); + $this->assertTrue( $this->registry->is_registered( 'other-collection/shared' ) ); + + $icon_a = $this->registry->get_registered_icon( 'test-collection/shared' ); + $icon_b = $this->registry->get_registered_icon( 'other-collection/shared' ); + $this->assertSame( 'Shared A', $icon_a['label'] ); + $this->assertSame( 'Shared B', $icon_b['label'] ); + } + + /** + * @ticket 64651 + * + * @covers ::unregister + */ + public function test_unregister_icon() { + $this->registry->register( + 'my-icon', + array( + 'label' => 'Icon', + 'content' => '', + 'collection' => 'test-collection', + ) + ); + + $this->assertTrue( $this->registry->is_registered( 'test-collection/my-icon' ) ); + $this->assertTrue( $this->registry->unregister( 'my-icon', 'test-collection' ) ); + $this->assertFalse( $this->registry->is_registered( 'test-collection/my-icon' ) ); + } + + /** + * @ticket 64651 + * + * @covers ::unregister + * + * @expectedIncorrectUsage WP_Icons_Registry::unregister + */ + public function test_unregister_unknown_icon() { + $this->assertFalse( $this->registry->unregister( 'ghost', 'test-collection' ) ); + } +} diff --git a/tests/phpunit/tests/icons/wpRestIconsController.php b/tests/phpunit/tests/icons/wpRestIconsController.php index f6fd935061f0e..10d45ceb21cd0 100644 --- a/tests/phpunit/tests/icons/wpRestIconsController.php +++ b/tests/phpunit/tests/icons/wpRestIconsController.php @@ -39,9 +39,88 @@ public static function wpTearDownAfterClass() { public function test_register_routes() { $routes = rest_get_server()->get_routes(); $this->assertArrayHasKey( '/wp/v2/icons', $routes ); + $this->assertArrayHasKey( '/wp/v2/icons/(?P[a-z][a-z-]*)', $routes ); $this->assertArrayHasKey( '/wp/v2/icons/(?P[a-z][a-z0-9-]*/[a-z][a-z0-9-]*)', $routes ); } + /** + * @ticket 64651 + * + * @covers WP_REST_Icons_Controller::get_items + */ + public function test_get_items_collection_scope() { + wp_register_icon_collection( 'rest-test-collection', array( 'label' => 'REST Test' ) ); + wp_register_icon( + 'bell', + 'rest-test-collection', + array( + 'label' => 'Bell', + 'content' => '', + ) + ); + + wp_set_current_user( self::$editor_id ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/icons/rest-test-collection' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status() ); + $this->assertIsArray( $data ); + + $names = array_column( $data, 'name' ); + $this->assertContains( 'rest-test-collection/bell', $names ); + foreach ( $data as $icon ) { + $this->assertSame( 'rest-test-collection', $icon['collection'] ); + } + + wp_unregister_icon_collection( 'rest-test-collection' ); + } + + /** + * @ticket 64651 + * + * @covers WP_REST_Icons_Controller::get_items + */ + public function test_get_items_unknown_collection_returns_404() { + wp_set_current_user( self::$editor_id ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/icons/unknown-collection' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_icon_collection_not_found', $response, 404 ); + } + + /** + * @ticket 64651 + * + * @covers WP_REST_Icons_Controller::prepare_item_for_response + */ + public function test_response_includes_collection_field() { + wp_register_icon_collection( 'rest-test-collection', array( 'label' => 'REST Test' ) ); + wp_register_icon( + 'bell', + 'rest-test-collection', + array( + 'label' => 'Bell', + 'content' => '', + ) + ); + + wp_set_current_user( self::$editor_id ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/icons/rest-test-collection/bell' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status() ); + $this->assertArrayHasKey( 'collection', $data ); + $this->assertSame( 'rest-test-collection', $data['collection'] ); + $this->assertSame( 'rest-test-collection/bell', $data['name'] ); + + wp_unregister_icon_collection( 'rest-test-collection' ); + } + /** * @doesNotPerformAssertions */ From 4391d929748a64fe1c5a111d9ac00ac6031600a7 Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Thu, 11 Jun 2026 18:35:21 +0900 Subject: [PATCH 3/9] Icons: Unhook default icon registration after init in tests. The icon registries warn via _doing_it_wrong() when a collection or icon is already registered. Because tests such as the Customize widgets suite fire `do_action( 'init' )` again after bootstrap, the default icon registration runs twice and triggers those notices, failing unrelated tests. Match the existing pattern used for font and connector registration by unhooking `_wp_register_default_icon_collections` and `_wp_register_default_icons` from `init` after the first run. Co-Authored-By: Claude --- tests/phpunit/includes/functions.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/phpunit/includes/functions.php b/tests/phpunit/includes/functions.php index d6b6218278ae3..d27af5c172a7a 100644 --- a/tests/phpunit/includes/functions.php +++ b/tests/phpunit/includes/functions.php @@ -375,6 +375,18 @@ function _unhook_font_registration() { } tests_add_filter( 'init', '_unhook_font_registration', 1000 ); +/** + * After the init action has been run once, trying to re-register icon collections and icons + * can cause errors. To avoid this, unhook the icon registration functions. + * + * @since 7.1.0 + */ +function _unhook_icon_registration() { + remove_action( 'init', '_wp_register_default_icon_collections', 0 ); + remove_action( 'init', '_wp_register_default_icons' ); +} +tests_add_filter( 'init', '_unhook_icon_registration', 1000 ); + /** * After the init action has been run once, trying to re-register connector settings can cause * duplicate registrations. To avoid this, unhook the connector registration functions. From 49c55c716b56f351407bfcbf9e1ca98d51541cb3 Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Thu, 11 Jun 2026 18:52:51 +0900 Subject: [PATCH 4/9] Icons: Add new icons collection route to schema test. The REST icons controller now registers a per-collection route, `/wp/v2/icons/(?P[a-z][a-z-]*)`. Add it to the expected routes list so test_expected_routes_in_schema reflects the registered routes. Co-Authored-By: Claude --- tests/phpunit/tests/rest-api/rest-schema-setup.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/phpunit/tests/rest-api/rest-schema-setup.php b/tests/phpunit/tests/rest-api/rest-schema-setup.php index 89bf2c481c567..f7483a6e61361 100644 --- a/tests/phpunit/tests/rest-api/rest-schema-setup.php +++ b/tests/phpunit/tests/rest-api/rest-schema-setup.php @@ -201,6 +201,7 @@ public function test_expected_routes_in_schema() { '/wp/v2/font-families/(?P[\d]+)/font-faces/(?P[\d]+)', '/wp/v2/font-families/(?P[\d]+)', '/wp/v2/icons', + '/wp/v2/icons/(?P[a-z][a-z-]*)', '/wp/v2/icons/(?P[a-z][a-z0-9-]*/[a-z][a-z0-9-]*)', '/wp-abilities/v1', '/wp-abilities/v1/categories', From 095efe2e859f08986b760ccc3053a6020bd3e9a5 Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Thu, 11 Jun 2026 19:09:16 +0900 Subject: [PATCH 5/9] Icons: Regenerate REST API client fixtures. The icons controller now exposes a `namespace` argument on the collection endpoint and a new per-collection route. Regenerate the QUnit fixtures so `git diff --exit-code` passes in CI. Co-Authored-By: Claude --- tests/qunit/fixtures/wp-api-generated.js | 58 ++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index fa03d9751fe99..561c2ecb414f4 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -12729,6 +12729,12 @@ mockedApiResponse.Schema = { "description": "Limit results to those matching a string.", "type": "string", "required": false + }, + "namespace": { + "description": "Limit results to icons belonging to the given collection slug.", + "type": "string", + "pattern": "^[a-z][a-z-]*$", + "required": false } } } @@ -12741,6 +12747,58 @@ mockedApiResponse.Schema = { ] } }, + "/wp/v2/icons/(?P[a-z][a-z-]*)": { + "namespace": "wp/v2", + "methods": [ + "GET" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": { + "namespace": { + "description": "Limit results to icons belonging to the given collection slug.", + "type": "string", + "pattern": "^[a-z][a-z-]*$", + "required": false + }, + "context": { + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string", + "enum": [ + "view", + "embed", + "edit" + ], + "default": "view", + "required": false + }, + "page": { + "description": "Current page of the collection.", + "type": "integer", + "default": 1, + "minimum": 1, + "required": false + }, + "per_page": { + "description": "Maximum number of items to be returned in result set.", + "type": "integer", + "default": 10, + "minimum": 1, + "maximum": 100, + "required": false + }, + "search": { + "description": "Limit results to those matching a string.", + "type": "string", + "required": false + } + } + } + ] + }, "/wp/v2/icons/(?P[a-z][a-z0-9-]*/[a-z][a-z0-9-]*)": { "namespace": "wp/v2", "methods": [ From e1e9b3baf4d6440c2cfc1c40ac027a38fde8d75d Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Sat, 13 Jun 2026 23:15:02 +0900 Subject: [PATCH 6/9] Icons: Validate icon file path before reading content. Custom icons can be registered with a `file_path`, but the path was never checked before `file_get_contents()`, so a missing, unreadable, non-SVG, or non-string path emitted a raw PHP warning and a misleading "invalid SVG markup" error. Guard `get_content()` by resolving the path via `realpath()`, then requiring an `.svg` extension, a regular file, and readability, returning null with a clear message otherwise. Add tests covering valid and invalid file paths. Co-Authored-By: Claude --- src/wp-includes/class-wp-icons-registry.php | 22 ++++- tests/phpunit/tests/icons/wpIconsRegistry.php | 91 +++++++++++++++++++ 2 files changed, 109 insertions(+), 4 deletions(-) diff --git a/src/wp-includes/class-wp-icons-registry.php b/src/wp-includes/class-wp-icons-registry.php index d223eda879b1c..eb6db5d4c5e32 100644 --- a/src/wp-includes/class-wp-icons-registry.php +++ b/src/wp-includes/class-wp-icons-registry.php @@ -262,10 +262,24 @@ protected function sanitize_icon_content( $icon_content ) { */ protected function get_content( $icon_name ) { if ( ! isset( $this->registered_icons[ $icon_name ]['content'] ) ) { - $content = file_get_contents( - $this->registered_icons[ $icon_name ]['file_path'] - ); - $content = $this->sanitize_icon_content( $content ); + $file_path = $this->registered_icons[ $icon_name ]['file_path'] ?? ''; + $is_stringy = is_string( $file_path ) || ( is_object( $file_path ) && method_exists( $file_path, '__toString' ) ); + $icon_path = $is_stringy ? realpath( (string) $file_path ) : false; + + if ( + ! is_string( $icon_path ) || + ! str_ends_with( $icon_path, '.svg' ) || + ! is_file( $icon_path ) || + ! is_readable( $icon_path ) + ) { + wp_trigger_error( + __METHOD__, + __( 'Icon file is missing or unreadable.' ) + ); + return null; + } + + $content = $this->sanitize_icon_content( file_get_contents( $icon_path ) ); if ( empty( $content ) ) { wp_trigger_error( diff --git a/tests/phpunit/tests/icons/wpIconsRegistry.php b/tests/phpunit/tests/icons/wpIconsRegistry.php index d8affd26690b0..914144b5d2f4e 100644 --- a/tests/phpunit/tests/icons/wpIconsRegistry.php +++ b/tests/phpunit/tests/icons/wpIconsRegistry.php @@ -16,6 +16,13 @@ class Tests_Icons_WpIconsRegistry extends WP_UnitTestCase { */ protected $registry; + /** + * Path to a temporary icon file created during a test, removed in tear_down. + * + * @var string|null + */ + private $temp_file = null; + public function set_up() { parent::set_up(); $this->registry = WP_Icons_Registry::get_instance(); @@ -42,10 +49,31 @@ public function tear_down() { $collections->unregister( 'other-collection' ); } + if ( $this->temp_file && file_exists( $this->temp_file ) ) { + unlink( $this->temp_file ); + } + $this->temp_file = null; + $this->registry = null; parent::tear_down(); } + /** + * Builds a unique temporary icon file path with the given extension. + * + * @param string|null $contents File contents, or null to leave the file uncreated. + * @param string $extension File extension, without the leading dot. + * @return string Absolute path to the temporary file. + */ + private function create_temp_icon_file( $contents, $extension = 'svg' ) { + $dir = get_temp_dir(); + $this->temp_file = trailingslashit( $dir ) . wp_unique_filename( $dir, uniqid() . '.' . $extension ); + if ( null !== $contents ) { + file_put_contents( $this->temp_file, $contents ); + } + return $this->temp_file; + } + /** * @ticket 64651 * @@ -240,4 +268,67 @@ public function test_unregister_icon() { public function test_unregister_unknown_icon() { $this->assertFalse( $this->registry->unregister( 'ghost', 'test-collection' ) ); } + + /** + * @ticket 64651 + * + * @covers ::get_content + */ + public function test_get_content_reads_from_valid_file_path() { + $path = $this->create_temp_icon_file( '' ); + + $this->registry->register( + 'from-file', + array( + 'label' => 'From File', + 'file_path' => $path, + 'collection' => 'test-collection', + ) + ); + + $icon = $this->registry->get_registered_icon( 'test-collection/from-file' ); + $this->assertStringContainsString( ' Data sets of [ $contents, $extension ]. + */ + public function data_invalid_icon_files() { + return array( + 'missing file' => array( null, 'svg' ), + 'non-svg extension' => array( '', 'txt' ), + 'invalid svg content' => array( '', 'svg' ), + ); + } + + /** + * @ticket 64651 + * + * @dataProvider data_invalid_icon_files + * + * @covers ::get_content + * + * @param string|null $contents File contents, or null to leave the file uncreated. + * @param string $extension File extension, without the leading dot. + */ + public function test_get_content_returns_null_for_invalid_file( $contents, $extension ) { + $path = $this->create_temp_icon_file( $contents, $extension ); + + $this->registry->register( + 'invalid-file', + array( + 'label' => 'Invalid File', + 'file_path' => $path, + 'collection' => 'test-collection', + ) + ); + + add_filter( 'wp_trigger_error_trigger_error', '__return_false' ); + $icon = $this->registry->get_registered_icon( 'test-collection/invalid-file' ); + remove_filter( 'wp_trigger_error_trigger_error', '__return_false' ); + + $this->assertNull( $icon['content'] ); + } } From 0f18d8ffd9ef1968b67277b6712ac47d120ee990 Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Sat, 13 Jun 2026 23:43:16 +0900 Subject: [PATCH 7/9] Icons: Allow digits and leading hyphens in collection slugs. The collection slug pattern required a leading lowercase letter and rejected digits, which excluded legitimate plugin slugs such as `a8c` or vendor names containing numbers. Relax the validation to accept any combination of lowercase alphanumeric characters and hyphens, and update the error message and tests to match. Co-Authored-By: Claude --- src/wp-includes/class-wp-icon-collections-registry.php | 4 ++-- tests/phpunit/tests/icons/wpIconCollectionsRegistry.php | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/wp-includes/class-wp-icon-collections-registry.php b/src/wp-includes/class-wp-icon-collections-registry.php index 4129bb0264e90..138ce1d66f431 100644 --- a/src/wp-includes/class-wp-icon-collections-registry.php +++ b/src/wp-includes/class-wp-icon-collections-registry.php @@ -58,10 +58,10 @@ public function register( $collection_slug, $collection_properties ) { return false; } - if ( ! preg_match( '/^[a-z][a-z-]*$/', $collection_slug ) ) { + if ( ! preg_match( '/^[a-z0-9-]+$/', $collection_slug ) ) { _doing_it_wrong( __METHOD__, - __( 'Icon collection slug must start with a lowercase letter and may only contain lowercase letters and hyphens.' ), + __( 'Icon collection slug must only contain lowercase alphanumeric characters and hyphens.' ), '7.1.0' ); return false; diff --git a/tests/phpunit/tests/icons/wpIconCollectionsRegistry.php b/tests/phpunit/tests/icons/wpIconCollectionsRegistry.php index 4f1698e589062..be6815f4d10d1 100644 --- a/tests/phpunit/tests/icons/wpIconCollectionsRegistry.php +++ b/tests/phpunit/tests/icons/wpIconCollectionsRegistry.php @@ -82,9 +82,8 @@ public function data_invalid_collection_slugs() { return array( 'non-string slug' => array( 1 ), 'contains slash' => array( 'plugin/icons' ), - 'contains digits' => array( 'plugin1' ), 'uppercase characters' => array( 'Plugin' ), - 'leading hyphen' => array( '-plugin' ), + 'underscore' => array( 'my_plugin' ), ); } From 2c5cda5633a51d0c21f9c79910a594622654d268 Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Sat, 13 Jun 2026 23:52:33 +0900 Subject: [PATCH 8/9] Icons: Restore @since tags and drop blank line after the opening PHP tag. Several @since 7.0.0 tags were accidentally dropped from properties and internal methods while reworking the Icons API, leaving docblocks without a version per the inline documentation standards. Restore them. Also remove the stray blank line after the opening PHP tag to match the rest of the core class files. Co-Authored-By: Claude --- .../class-wp-icon-collections-registry.php | 1 - src/wp-includes/class-wp-icons-registry.php | 13 +++++++++++-- .../endpoints/class-wp-rest-icons-controller.php | 10 ++++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/class-wp-icon-collections-registry.php b/src/wp-includes/class-wp-icon-collections-registry.php index 138ce1d66f431..5209da8835f27 100644 --- a/src/wp-includes/class-wp-icon-collections-registry.php +++ b/src/wp-includes/class-wp-icon-collections-registry.php @@ -1,5 +1,4 @@ namespace = 'wp/v2'; @@ -93,6 +95,8 @@ public function register_routes() { /** * Checks whether a given request has permission to read icons. * + * @since 7.0.0 + * * @param WP_REST_Request $request Full details about the request. * @return true|WP_Error True if the request has read access, WP_Error object otherwise. */ @@ -120,6 +124,8 @@ public function get_items_permissions_check( /** * Checks if a given request has access to read a specific icon. * + * @since 7.0.0 + * * @param WP_REST_Request $request Full details about the request. * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise. */ @@ -173,6 +179,8 @@ public function get_items( $request ) { /** * Retrieves a specific icon. * + * @since 7.0.0 + * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ @@ -189,6 +197,8 @@ public function get_item( $request ) { /** * Retrieves a specific icon from the registry. * + * @since 7.0.0 + * * @param string $name Icon name. * @return array|WP_Error Icon data on success, or WP_Error object on failure. */ From 5eb200a67867c12e38294b7d231c508d378f3128 Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Mon, 15 Jun 2026 19:05:28 +0900 Subject: [PATCH 9/9] Icons: Require namespaced icon names in the form "collection/icon-name". Derive the collection from the icon name instead of passing it separately, so an icon is always identified by a single namespaced name. The collection prefix is now required; a non-namespaced name is rejected via _doing_it_wrong(). This drops the $collection parameter from wp_register_icon()/wp_unregister_icon() and the "collection" icon property, and simplifies the cascade unregister when a collection is removed. The "core" collection is reserved for WordPress core icons. Co-Authored-By: Claude --- .../class-wp-icon-collections-registry.php | 4 +- src/wp-includes/class-wp-icons-registry.php | 67 +++++++------- src/wp-includes/icons.php | 21 +++-- .../tests/icons/wpIconCollectionsRegistry.php | 23 ++--- tests/phpunit/tests/icons/wpIconsRegistry.php | 92 +++++++++---------- .../tests/icons/wpRestIconsController.php | 6 +- 6 files changed, 106 insertions(+), 107 deletions(-) diff --git a/src/wp-includes/class-wp-icon-collections-registry.php b/src/wp-includes/class-wp-icon-collections-registry.php index 5209da8835f27..b396b50856bd4 100644 --- a/src/wp-includes/class-wp-icon-collections-registry.php +++ b/src/wp-includes/class-wp-icon-collections-registry.php @@ -149,11 +149,9 @@ public function unregister( $collection_slug ) { } $icons_registry = WP_Icons_Registry::get_instance(); - $prefix = $collection_slug . '/'; foreach ( $icons_registry->get_registered_icons() as $icon ) { if ( isset( $icon['collection'] ) && $icon['collection'] === $collection_slug ) { - $unqualified_name = substr( $icon['name'], strlen( $prefix ) ); - $icons_registry->unregister( $unqualified_name, $collection_slug ); + $icons_registry->unregister( $icon['name'] ); } } diff --git a/src/wp-includes/class-wp-icons-registry.php b/src/wp-includes/class-wp-icons-registry.php index e801e4a6a73a9..a4ea13da37496 100644 --- a/src/wp-includes/class-wp-icons-registry.php +++ b/src/wp-includes/class-wp-icons-registry.php @@ -45,18 +45,18 @@ protected function __construct() {} * Registers an icon. * * @since 7.0.0 - * @since 7.1.0 `collection` is required. The icon name no longer includes a namespace. + * @since 7.1.0 The icon name must be namespaced in the form "collection/icon-name". * - * @param string $icon_name Icon name (e.g. "arrow-left"). Must not contain a namespace prefix. + * @param string $icon_name Namespaced icon name in the form "collection/icon-name" + * (e.g. "core/arrow-left"). * @param array $icon_properties { * List of properties for the icon. * - * @type string $label Required. A human-readable label for the icon. - * @type string $collection Required. The slug of a registered icon collection that this icon belongs to. - * @type string $content Optional. SVG markup for the icon. - * If not provided, the content will be retrieved from the `file_path` if set. - * If both `content` and `file_path` are not set, the icon will not be registered. - * @type string $file_path Optional. The full path to the file containing the icon content. + * @type string $label Required. A human-readable label for the icon. + * @type string $content Optional. SVG markup for the icon. + * If not provided, the content will be retrieved from the `file_path` if set. + * If both `content` and `file_path` are not set, the icon will not be registered. + * @type string $file_path Optional. The full path to the file containing the icon content. * } * @return bool True if the icon was registered with success and false otherwise. */ @@ -79,7 +79,20 @@ public function register( $icon_name, $icon_properties ) { return false; } - if ( ! preg_match( '/^[a-z][a-z0-9-]*$/', $icon_name ) ) { + // Require a namespaced name in the form "collection/icon-name". + if ( false === strpos( $icon_name, '/' ) ) { + _doing_it_wrong( + __METHOD__, + __( 'Icon name must be namespaced in the form "collection/icon-name".' ), + '7.1.0' + ); + return false; + } + + // Split the namespaced name into a collection slug and an unqualified icon name. + list( $collection, $unqualified_name ) = explode( '/', $icon_name, 2 ); + + if ( ! preg_match( '/^[a-z][a-z0-9-]*$/', $unqualified_name ) ) { _doing_it_wrong( __METHOD__, __( 'Icon names must start with a lowercase letter and contain only lowercase letters, digits, and hyphens.' ), @@ -88,7 +101,7 @@ public function register( $icon_name, $icon_properties ) { return false; } - $allowed_keys = array_fill_keys( array( 'label', 'content', 'file_path', 'collection' ), 1 ); + $allowed_keys = array_fill_keys( array( 'label', 'content', 'file_path' ), 1 ); foreach ( array_keys( $icon_properties ) as $key ) { if ( ! array_key_exists( $key, $allowed_keys ) ) { _doing_it_wrong( @@ -104,22 +117,13 @@ public function register( $icon_name, $icon_properties ) { } } - if ( ! isset( $icon_properties['collection'] ) || ! is_string( $icon_properties['collection'] ) ) { - _doing_it_wrong( - __METHOD__, - __( 'Icon collection is required and must be a string.' ), - '7.1.0' - ); - return false; - } - - if ( ! WP_Icon_Collections_Registry::get_instance()->is_registered( $icon_properties['collection'] ) ) { + if ( ! WP_Icon_Collections_Registry::get_instance()->is_registered( $collection ) ) { _doing_it_wrong( __METHOD__, sprintf( /* translators: %s: Icon collection slug. */ __( 'Icon collection "%s" is not registered.' ), - $icon_properties['collection'] + $collection ), '7.1.0' ); @@ -168,7 +172,7 @@ public function register( $icon_name, $icon_properties ) { } } - $qualified_name = $icon_properties['collection'] . '/' . $icon_name; + $qualified_name = $collection . '/' . $unqualified_name; if ( $this->is_registered( $qualified_name ) ) { _doing_it_wrong( @@ -181,7 +185,10 @@ public function register( $icon_name, $icon_properties ) { $icon = array_merge( $icon_properties, - array( 'name' => $qualified_name ) + array( + 'name' => $qualified_name, + 'collection' => $collection, + ) ); $this->registered_icons[ $qualified_name ] = $icon; @@ -194,27 +201,25 @@ public function register( $icon_name, $icon_properties ) { * * @since 7.1.0 * - * @param string $icon_name Icon name (e.g. "arrow-left"). Must not contain a namespace prefix. - * @param string $collection Slug of the collection the icon belongs to. + * @param string $icon_name Namespaced icon name in the form "collection/icon-name" + * (e.g. "core/arrow-left"). * @return bool True if the icon was unregistered successfully, false otherwise. */ - public function unregister( $icon_name, $collection ) { - $qualified_name = $collection . '/' . $icon_name; - - if ( ! $this->is_registered( $qualified_name ) ) { + public function unregister( $icon_name ) { + if ( ! $this->is_registered( $icon_name ) ) { _doing_it_wrong( __METHOD__, sprintf( /* translators: %s: Icon name. */ __( 'Icon "%s" is not registered.' ), - $qualified_name + $icon_name ), '7.1.0' ); return false; } - unset( $this->registered_icons[ $qualified_name ] ); + unset( $this->registered_icons[ $icon_name ] ); return true; } diff --git a/src/wp-includes/icons.php b/src/wp-includes/icons.php index 9c7835b5cc084..bb8e5da3ef08e 100644 --- a/src/wp-includes/icons.php +++ b/src/wp-includes/icons.php @@ -42,8 +42,11 @@ function wp_unregister_icon_collection( $slug ) { * * @since 7.1.0 * - * @param string $icon_name Icon name (e.g. "arrow-left"). - * @param string $collection Slug of a registered icon collection that this icon belongs to. + * @param string $icon_name Namespaced icon name in the form "collection/icon-name" + * (e.g. "my-plugin/arrow-left"). The "core" collection is + * reserved for WordPress core icons; third-party code should + * register icons under its own collection rather than the + * "core" collection. * @param array $args { * List of properties for the icon. * @@ -55,8 +58,7 @@ function wp_unregister_icon_collection( $slug ) { * } * @return bool True if the icon was registered successfully, else false. */ -function wp_register_icon( $icon_name, $collection, $args ) { - $args['collection'] = $collection; +function wp_register_icon( $icon_name, $args ) { return WP_Icons_Registry::get_instance()->register( $icon_name, $args ); } @@ -65,12 +67,12 @@ function wp_register_icon( $icon_name, $collection, $args ) { * * @since 7.1.0 * - * @param string $icon_name Icon name (e.g. "arrow-left"). - * @param string $collection Slug of the collection the icon belongs to. + * @param string $icon_name Namespaced icon name in the form "collection/icon-name" + * (e.g. "core/arrow-left"). * @return bool True if the icon was unregistered successfully, else false. */ -function wp_unregister_icon( $icon_name, $collection ) { - return WP_Icons_Registry::get_instance()->unregister( $icon_name, $collection ); +function wp_unregister_icon( $icon_name ) { + return WP_Icons_Registry::get_instance()->unregister( $icon_name ); } /** @@ -131,8 +133,7 @@ function _wp_register_default_icons() { } wp_register_icon( - $icon_name, - 'core', + 'core/' . $icon_name, array( 'label' => $icon_data['label'], 'file_path' => $icons_directory . $icon_data['filePath'], diff --git a/tests/phpunit/tests/icons/wpIconCollectionsRegistry.php b/tests/phpunit/tests/icons/wpIconCollectionsRegistry.php index be6815f4d10d1..a2b5a43dd489d 100644 --- a/tests/phpunit/tests/icons/wpIconCollectionsRegistry.php +++ b/tests/phpunit/tests/icons/wpIconCollectionsRegistry.php @@ -128,27 +128,24 @@ public function test_unregister_collection_cascades_to_icons() { $icons = WP_Icons_Registry::get_instance(); $icons->register( - 'alpha', + 'plugin-a/alpha', array( - 'label' => 'Alpha', - 'content' => '', - 'collection' => 'plugin-a', + 'label' => 'Alpha', + 'content' => '', ) ); $icons->register( - 'beta', + 'plugin-a/beta', array( - 'label' => 'Beta', - 'content' => '', - 'collection' => 'plugin-a', + 'label' => 'Beta', + 'content' => '', ) ); $icons->register( - 'gamma', + 'plugin-b/gamma', array( - 'label' => 'Gamma', - 'content' => '', - 'collection' => 'plugin-b', + 'label' => 'Gamma', + 'content' => '', ) ); @@ -161,7 +158,7 @@ public function test_unregister_collection_cascades_to_icons() { $this->assertFalse( $icons->is_registered( 'plugin-a/beta' ) ); $this->assertTrue( $icons->is_registered( 'plugin-b/gamma' ) ); - $icons->unregister( 'gamma', 'plugin-b' ); + $icons->unregister( 'plugin-b/gamma' ); } /** diff --git a/tests/phpunit/tests/icons/wpIconsRegistry.php b/tests/phpunit/tests/icons/wpIconsRegistry.php index 914144b5d2f4e..e9fef3978b1c8 100644 --- a/tests/phpunit/tests/icons/wpIconsRegistry.php +++ b/tests/phpunit/tests/icons/wpIconsRegistry.php @@ -81,11 +81,10 @@ private function create_temp_icon_file( $contents, $extension = 'svg' ) { */ public function test_register_icon() { $result = $this->registry->register( - 'my-icon', + 'test-collection/my-icon', array( - 'label' => 'My Icon', - 'content' => '', - 'collection' => 'test-collection', + 'label' => 'My Icon', + 'content' => '', ) ); @@ -95,10 +94,11 @@ public function test_register_icon() { public function data_invalid_icon_names() { return array( - 'non-string name' => array( 1 ), - 'contains slash' => array( 'test-collection/plus' ), - 'uppercase characters' => array( 'Plus' ), - 'invalid characters' => array( '_doing_it_wrong' ), + 'non-string name' => array( 1 ), + 'non-namespaced name' => array( 'plus' ), + 'empty unqualified name' => array( 'test-collection/' ), + 'uppercase characters' => array( 'test-collection/Plus' ), + 'invalid characters' => array( 'test-collection/_doing_it_wrong' ), ); } @@ -111,13 +111,12 @@ public function data_invalid_icon_names() { */ public function test_register_icon_twice() { $settings = array( - 'label' => 'Icon', - 'content' => '', - 'collection' => 'test-collection', + 'label' => 'Icon', + 'content' => '', ); - $this->assertTrue( $this->registry->register( 'duplicate', $settings ) ); - $this->assertFalse( $this->registry->register( 'duplicate', $settings ) ); + $this->assertTrue( $this->registry->register( 'test-collection/duplicate', $settings ) ); + $this->assertFalse( $this->registry->register( 'test-collection/duplicate', $settings ) ); } /** @@ -135,22 +134,24 @@ public function test_register_invalid_name( $name ) { $result = $this->registry->register( $name, array( - 'label' => 'Icon', - 'content' => '', - 'collection' => 'test-collection', + 'label' => 'Icon', + 'content' => '', ) ); $this->assertFalse( $result ); } /** + * Should reject a non-namespaced name, since the collection is derived from + * the namespaced icon name in the form "collection/icon-name". + * * @ticket 64651 * * @covers ::register * * @expectedIncorrectUsage WP_Icons_Registry::register */ - public function test_register_requires_collection() { + public function test_register_rejects_non_namespaced_name() { $result = $this->registry->register( 'my-icon', array( @@ -162,25 +163,30 @@ public function test_register_requires_collection() { } /** + * Should reject `collection` passed as an icon property, since the collection + * is derived from the namespaced icon name instead. + * * @ticket 64651 * * @covers ::register * * @expectedIncorrectUsage WP_Icons_Registry::register */ - public function test_register_rejects_non_string_collection() { + public function test_register_rejects_collection_property() { $result = $this->registry->register( - 'my-icon', + 'test-collection/my-icon', array( 'label' => 'Icon', 'content' => '', - 'collection' => 123, + 'collection' => 'test-collection', ) ); $this->assertFalse( $result ); } /** + * Should fail when the name references a collection that is not registered. + * * @ticket 64651 * * @covers ::register @@ -189,11 +195,10 @@ public function test_register_rejects_non_string_collection() { */ public function test_register_rejects_unregistered_collection() { $result = $this->registry->register( - 'my-icon', + 'unregistered-collection/my-icon', array( - 'label' => 'Icon', - 'content' => '', - 'collection' => 'unregistered-collection', + 'label' => 'Icon', + 'content' => '', ) ); $this->assertFalse( $result ); @@ -210,21 +215,19 @@ public function test_same_name_across_collections_does_not_collide() { $this->assertTrue( $this->registry->register( - 'shared', + 'test-collection/shared', array( - 'label' => 'Shared A', - 'content' => '', - 'collection' => 'test-collection', + 'label' => 'Shared A', + 'content' => '', ) ) ); $this->assertTrue( $this->registry->register( - 'shared', + 'other-collection/shared', array( - 'label' => 'Shared B', - 'content' => '', - 'collection' => 'other-collection', + 'label' => 'Shared B', + 'content' => '', ) ) ); @@ -245,16 +248,15 @@ public function test_same_name_across_collections_does_not_collide() { */ public function test_unregister_icon() { $this->registry->register( - 'my-icon', + 'test-collection/my-icon', array( - 'label' => 'Icon', - 'content' => '', - 'collection' => 'test-collection', + 'label' => 'Icon', + 'content' => '', ) ); $this->assertTrue( $this->registry->is_registered( 'test-collection/my-icon' ) ); - $this->assertTrue( $this->registry->unregister( 'my-icon', 'test-collection' ) ); + $this->assertTrue( $this->registry->unregister( 'test-collection/my-icon' ) ); $this->assertFalse( $this->registry->is_registered( 'test-collection/my-icon' ) ); } @@ -266,7 +268,7 @@ public function test_unregister_icon() { * @expectedIncorrectUsage WP_Icons_Registry::unregister */ public function test_unregister_unknown_icon() { - $this->assertFalse( $this->registry->unregister( 'ghost', 'test-collection' ) ); + $this->assertFalse( $this->registry->unregister( 'test-collection/ghost' ) ); } /** @@ -278,11 +280,10 @@ public function test_get_content_reads_from_valid_file_path() { $path = $this->create_temp_icon_file( '' ); $this->registry->register( - 'from-file', + 'test-collection/from-file', array( - 'label' => 'From File', - 'file_path' => $path, - 'collection' => 'test-collection', + 'label' => 'From File', + 'file_path' => $path, ) ); @@ -317,11 +318,10 @@ public function test_get_content_returns_null_for_invalid_file( $contents, $exte $path = $this->create_temp_icon_file( $contents, $extension ); $this->registry->register( - 'invalid-file', + 'test-collection/invalid-file', array( - 'label' => 'Invalid File', - 'file_path' => $path, - 'collection' => 'test-collection', + 'label' => 'Invalid File', + 'file_path' => $path, ) ); diff --git a/tests/phpunit/tests/icons/wpRestIconsController.php b/tests/phpunit/tests/icons/wpRestIconsController.php index 10d45ceb21cd0..ce490d48d06f8 100644 --- a/tests/phpunit/tests/icons/wpRestIconsController.php +++ b/tests/phpunit/tests/icons/wpRestIconsController.php @@ -51,8 +51,7 @@ public function test_register_routes() { public function test_get_items_collection_scope() { wp_register_icon_collection( 'rest-test-collection', array( 'label' => 'REST Test' ) ); wp_register_icon( - 'bell', - 'rest-test-collection', + 'rest-test-collection/bell', array( 'label' => 'Bell', 'content' => '', @@ -99,8 +98,7 @@ public function test_get_items_unknown_collection_returns_404() { public function test_response_includes_collection_field() { wp_register_icon_collection( 'rest-test-collection', array( 'label' => 'REST Test' ) ); wp_register_icon( - 'bell', - 'rest-test-collection', + 'rest-test-collection/bell', array( 'label' => 'Bell', 'content' => '',