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'] 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..e827056064e7b1 --- /dev/null +++ b/lib/compat/wordpress-7.1/class-gutenberg-icons-registry-7-1.php @@ -0,0 +1,89 @@ + $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; + } +} 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/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 2d437a04f92ddc..615af06eff8607 100644 --- a/lib/load.php +++ b/lib/load.php @@ -76,6 +76,11 @@ 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'; + require __DIR__ . '/compat/wordpress-7.1/rest-api.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/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. */