diff --git a/src/wp-includes/class-wp-icons-registry.php b/src/wp-includes/class-wp-icons-registry.php index 2e306bec77e53..ceec97b50e665 100644 --- a/src/wp-includes/class-wp-icons-registry.php +++ b/src/wp-includes/class-wp-icons-registry.php @@ -111,6 +111,34 @@ protected function register( $icon_name, $icon_properties ) { 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 ) ) { diff --git a/tests/phpunit/tests/icons/wpIconsRegistry.php b/tests/phpunit/tests/icons/wpIconsRegistry.php new file mode 100644 index 0000000000000..5cfc6df48101e --- /dev/null +++ b/tests/phpunit/tests/icons/wpIconsRegistry.php @@ -0,0 +1,118 @@ +registry = WP_Icons_Registry::get_instance(); + } + + public function tear_down() { + $instance_property = new ReflectionProperty( WP_Icons_Registry::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 WP_Icons_Registry::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 ); + } + + /** + * 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 WP_Icons_Registry::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 WP_Icons_Registry::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 ); + } + } +}