diff --git a/backport-changelog/7.1/11245.md b/backport-changelog/7.1/11245.md new file mode 100644 index 00000000000000..7324c0d65f4cc4 --- /dev/null +++ b/backport-changelog/7.1/11245.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/11245 + +* https://github.com/WordPress/gutenberg/pull/76079 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 e827056064e7b1..727064c9b9e7a0 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 @@ -50,6 +50,127 @@ protected function __construct() { } } + /** + * Registers an icon. + * + * @param string $icon_name Icon name including namespace. + * @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. + * } + * @return bool True if the icon was registered with success and false otherwise. + */ + 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; + } + + if ( preg_match( '/[A-Z]/', $icon_name ) ) { + _doing_it_wrong( + __METHOD__, + __( 'Icon names must not contain uppercase characters.' ), + '7.1.0' + ); + return false; + } + + $name_matcher = '/^[a-z][a-z0-9-]*\/[a-z][a-z0-9-]*$/'; + if ( ! preg_match( $name_matcher, $icon_name ) ) { + _doing_it_wrong( + __METHOD__, + __( 'Icon names must contain a namespace prefix. Example: my-plugin/my-custom-icon' ), + '7.1.0' + ); + return false; + } + + if ( $this->is_registered( $icon_name ) ) { + _doing_it_wrong( + __METHOD__, + __( 'Icon is already registered.' ), + '7.1.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; + } + /** * Modified to also search in icon labels */ diff --git a/phpunit/experimental/class-gutenberg-icons-registry-7-1-test.php b/phpunit/experimental/class-gutenberg-icons-registry-7-1-test.php new file mode 100644 index 00000000000000..89696277f4720a --- /dev/null +++ b/phpunit/experimental/class-gutenberg-icons-registry-7-1-test.php @@ -0,0 +1,127 @@ +registry = Gutenberg_Icons_Registry_7_1::get_instance(); + } + + public function tear_down() { + $instance_property = new ReflectionProperty( Gutenberg_Icons_Registry_7_1::class, 'instance' ); + + /* + * ReflectionProperty::setAccessible is: + * - redundant as of 8.1.0, which made all properties accessible + * - deprecated as of 8.5.0 + * - needed until 8.1.0, as property `instance` is private + */ + if ( PHP_VERSION_ID < 80100 ) { + $instance_property->setAccessible( true ); + } + + $instance_property->setValue( null, null ); + + $this->registry = null; + parent::tear_down(); + } + + /** + * Invokes Gutenberg_Icons_Registry_7_1::register despite it being private + * + * @param string $icon_name Icon name including namespace. + * @param array $icon_properties Icon properties (label, content, filePath). + * @return bool True if the icon was registered successfully. + */ + private function register( $icon_name, $icon_properties ) { + $method = new ReflectionMethod( $this->registry, 'register' ); + + /* + * ReflectionMethod::setAccessible is: + * - redundant as of 8.1.0, which made all properties accessible + * - deprecated as of 8.5.0 + * - needed until 8.1.0, as property `instance` is private + */ + if ( PHP_VERSION_ID < 80100 ) { + $method->setAccessible( true ); + } + + return $method->invoke( $this->registry, $icon_name, $icon_properties ); + } + + /** + * Should accept valid icon names. + */ + public function test_register_icon() { + $name = 'test-plugin/my-icon'; + $settings = array( + 'label' => 'My Icon', + 'content' => '', + ); + + $result = $this->register( $name, $settings ); + $this->assertTrue( $result ); + $this->assertTrue( $this->registry->is_registered( $name ) ); + } + + /** + * Provides invalid icon names. + * + * @return array[] + */ + public function data_invalid_icon_names() { + return array( + 'non-string name' => array( 1 ), + 'no namespace' => array( 'plus' ), + 'uppercase characters' => array( 'Test/Plus' ), + 'invalid characters' => array( 'test/_doing_it_wrong' ), + ); + } + + /** + * Should fail to re-register the same icon. + * + * @expectedIncorrectUsage Gutenberg_Icons_Registry_7_1::register + */ + public function test_register_icon_twice() { + $name = 'test-plugin/duplicate'; + $settings = array( + 'label' => 'Icon', + 'content' => '', + ); + + $result = $this->register( $name, $settings ); + $this->assertTrue( $result ); + $result2 = $this->register( $name, $settings ); + $this->assertFalse( $result2 ); + } + + /** + * Should fail to register icon with invalid names. + * + * @dataProvider data_invalid_icon_names + * @expectedIncorrectUsage Gutenberg_Icons_Registry_7_1::register + */ + public function test_register_invalid_name() { + foreach ( $this->data_invalid_icon_names() as $name ) { + $settings = array( + 'label' => 'Icon', + 'content' => '', + ); + + $result = $this->register( $name, $settings ); + $this->assertFalse( $result ); + } + } +}