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 );
+ }
+ }
+}