PluginProbe
Gutenberg / trunk
Gutenberg vtrunk
24.1.0 24.0.0 23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 All 404 releases
← All changes | lib/class-wp-icons-registry-gutenberg.php +235 -127 23.5.2 → trunk View file →
@@ -1,67 +1,117 @@
1 1 <?php
2 2
3 -class WP_Icons_Registry_Gutenberg extends WP_Icons_Registry {
4 - /**
5 - * Modified to point $manifest_path to Gutenberg packages
6 - */
7 - protected function __construct() {
8 - $icons_directory = gutenberg_dir_path() . 'packages/icons/src';
9 - $icons_directory = trailingslashit( $icons_directory );
10 - $manifest_path = $icons_directory . 'manifest.php';
3 +/**
4 + * Returns the SVG elements and attributes allowed for registered icons.
5 + *
6 + * @return array[] Allowed SVG elements and attributes.
7 + * @phpstan-return array<non-falsy-string, array<non-falsy-string, true>>
8 + */
9 +function gutenberg_get_allowed_icon_svg_tags(): array {
10 + $allow_attributes = static function ( string ...$attribute_names ): array {
11 + return array_fill_keys( $attribute_names, true );
12 + };
11 13
12 - if ( ! is_readable( $manifest_path ) ) {
13 - wp_trigger_error(
14 - __METHOD__,
15 - __( 'Core icon collection manifest is missing or unreadable.', 'gutenberg' )
16 - );
17 - return;
18 - }
14 + $stroke_attributes = $allow_attributes(
15 + 'style',
16 + 'stroke',
17 + 'stroke-width',
18 + 'stroke-linecap',
19 + 'stroke-linejoin',
20 + 'stroke-miterlimit',
21 + 'vector-effect',
22 + );
19 23
20 - $collection = include $manifest_path;
24 + return array(
25 + 'svg' => array_merge(
26 + $allow_attributes(
27 + 'class',
28 + 'xmlns',
29 + 'width',
30 + 'height',
31 + 'viewbox',
32 + 'aria-hidden',
33 + 'role',
34 + 'focusable',
35 + 'fill',
36 + 'fill-rule',
37 + 'clip-rule',
38 + ),
39 + $stroke_attributes
40 + ),
41 + 'path' => array_merge(
42 + $allow_attributes(
43 + 'fill',
44 + 'fill-rule',
45 + 'clip-rule',
46 + 'd',
47 + 'opacity',
48 + 'transform',
49 + ),
50 + $stroke_attributes
51 + ),
52 + 'polygon' => array_merge(
53 + $allow_attributes(
54 + 'fill',
55 + 'fill-rule',
56 + 'clip-rule',
57 + 'points',
58 + 'transform',
59 + 'focusable',
60 + ),
61 + $stroke_attributes
62 + ),
63 + 'rect' => array_merge(
64 + $allow_attributes(
65 + 'fill',
66 + 'fill-rule',
67 + 'clip-rule',
68 + 'x',
69 + 'y',
70 + 'width',
71 + 'height',
72 + 'rx',
73 + 'ry',
74 + 'transform',
75 + ),
76 + $stroke_attributes
77 + ),
78 + 'circle' => array_merge(
79 + $allow_attributes(
80 + 'fill',
81 + 'fill-rule',
82 + 'clip-rule',
83 + 'cx',
84 + 'cy',
85 + 'r',
86 + 'transform',
87 + ),
88 + $stroke_attributes
89 + ),
90 + );
91 +}
21 92
22 - if ( empty( $collection ) ) {
23 - wp_trigger_error(
24 - __METHOD__,
25 - __( 'Core icon collection manifest is empty or invalid.', 'gutenberg' )
26 - );
27 - return;
28 - }
29 93
30 - foreach ( $collection as $icon_name => $icon_data ) {
31 - if (
32 - empty( $icon_data['filePath'] )
33 - || ! is_string( $icon_data['filePath'] )
34 - ) {
35 - _doing_it_wrong(
36 - __METHOD__,
37 - __( 'Core icon collection manifest must provide valid a "filePath" for each icon.', 'gutenberg' ),
38 - '7.0.0'
39 - );
40 - return;
41 - }
94 +class WP_Icons_Registry_Gutenberg extends WP_Icons_Registry {
95 + /**
96 + * Overridden to skip the parent's core icon registration, which uses the
97 + * core manifest path. Core icons are registered via
98 + * `gutenberg_register_default_icons()` using the Gutenberg manifest instead.
99 + */
100 + protected function __construct() {}
42 101
43 - $this->register(
44 - 'core/' . $icon_name,
45 - array(
46 - 'label' => $icon_data['label'],
47 - 'file_path' => $icons_directory . $icon_data['filePath'],
48 - )
49 - );
50 - }
51 - }
52 -
53 102 /**
54 103 * Registers an icon.
55 104 *
56 - * @param string $icon_name Icon name including namespace.
105 + * @param string $icon_name Namespaced icon name in the form "collection/icon-name"
106 + * (e.g. "core/arrow-left").
57 107 * @param array $icon_properties {
58 108 * List of properties for the icon.
59 109 *
60 - * @type string $label Required. A human-readable label for the icon.
61 - * @type string $content Optional. SVG markup for the icon.
62 - * If not provided, the content will be retrieved from the `file_path` if set.
63 - * If both `content` and `file_path` are not set, the icon will not be registered.
110 + * @type string $label Required. A human-readable label for the icon.
111 + * @type string $content Optional. SVG markup for the icon.
112 + * If not provided, the content will be retrieved from the `file_path` if set.
113 + * If both `content` and `file_path` are not set, the icon will not be registered.
64 114 * @type string $file_path Optional. The full path to the file containing the icon content.
65 115 * }
66 116 * @return bool True if the icon was registered with success and false otherwise.
67 117 */
@@ -74,31 +124,32 @@
74 124 );
75 125 return false;
76 126 }
77 127
78 - if ( preg_match( '/[A-Z]/', $icon_name ) ) {
128 + if ( ! str_contains( $icon_name, '/' ) ) {
79 129 _doing_it_wrong(
80 130 __METHOD__,
81 - __( 'Icon names must not contain uppercase characters.', 'gutenberg' ),
131 + __( 'Icon name must be namespaced in the form "collection/icon-name".', 'gutenberg' ),
82 132 '7.1.0'
83 133 );
84 134 return false;
85 135 }
86 136
87 - $name_matcher = '/^[a-z][a-z0-9-]*\/[a-z][a-z0-9-]*$/';
88 - if ( ! preg_match( $name_matcher, $icon_name ) ) {
137 + list( $collection, $unqualified_name ) = explode( '/', $icon_name, 2 );
138 +
139 + if ( preg_match( '/[A-Z]/', $unqualified_name ) ) {
89 140 _doing_it_wrong(
90 141 __METHOD__,
91 - __( 'Icon names must contain a namespace prefix. Example: my-plugin/my-custom-icon', 'gutenberg' ),
142 + __( 'Icon names must not contain uppercase characters.', 'gutenberg' ),
92 143 '7.1.0'
93 144 );
94 145 return false;
95 146 }
96 147
97 - if ( $this->is_registered( $icon_name ) ) {
148 + if ( ! preg_match( '/^[a-z0-9]([a-z0-9_-]*[a-z0-9])?$/', $unqualified_name ) ) {
98 149 _doing_it_wrong(
99 150 __METHOD__,
100 - __( 'Icon is already registered.', 'gutenberg' ),
151 + __( 'Icon names must start and end with a lowercase letter or digit and contain only lowercase letters, digits, hyphens, and underscores.', 'gutenberg' ),
101 152 '7.1.0'
102 153 );
103 154 return false;
104 155 }
@@ -118,8 +169,21 @@
118 169 return false;
119 170 }
120 171 }
121 172
173 + if ( ! WP_Icon_Collections_Registry::get_instance()->is_registered( $collection ) ) {
174 + _doing_it_wrong(
175 + __METHOD__,
176 + sprintf(
177 + /* translators: %s: Icon collection slug. */
178 + __( 'Icon collection "%s" is not registered.', 'gutenberg' ),
179 + $collection
180 + ),
181 + '7.1.0'
182 + );
183 + return false;
184 + }
185 +
122 186 if ( ! isset( $icon_properties['label'] ) || ! is_string( $icon_properties['label'] ) ) {
123 187 _doing_it_wrong(
124 188 __METHOD__,
125 189 __( 'Icon label must be a string.', 'gutenberg' ),
@@ -158,33 +222,109 @@
158 222 '7.0.0'
159 223 );
160 224 return false;
161 225 }
226 +
227 + $icon_properties['content'] = $sanitized_icon_content;
162 228 }
163 229
230 + $qualified_name = $collection . '/' . $unqualified_name;
231 +
232 + if ( $this->is_registered( $qualified_name ) ) {
233 + _doing_it_wrong(
234 + __METHOD__,
235 + __( 'Icon is already registered.', 'gutenberg' ),
236 + '7.1.0'
237 + );
238 + return false;
239 + }
240 +
164 241 $icon = array_merge(
165 242 $icon_properties,
166 - array( 'name' => $icon_name )
243 + array(
244 + 'name' => $qualified_name,
245 + 'collection' => $collection,
246 + )
167 247 );
168 248
169 - $this->registered_icons[ $icon_name ] = $icon;
249 + $this->registered_icons[ $qualified_name ] = $icon;
170 250
171 251 return true;
172 252 }
173 253
174 254 /**
175 - * Redefined to read the icon content from the `file_path` property.
255 + * Unregisters an icon.
176 256 *
257 + * @param string $icon_name Namespaced icon name in the form "collection/icon-name"
258 + * (e.g. "core/arrow-left").
259 + * @return bool True if the icon was unregistered successfully, else false.
260 + */
261 + public function unregister( $icon_name ) {
262 + if ( ! $this->is_registered( $icon_name ) ) {
263 + _doing_it_wrong(
264 + __METHOD__,
265 + sprintf(
266 + /* translators: %s: Icon name. */
267 + __( 'Icon "%s" is not registered.', 'gutenberg' ),
268 + $icon_name
269 + ),
270 + '7.1.0'
271 + );
272 + return false;
273 + }
274 +
275 + unset( $this->registered_icons[ $icon_name ] );
276 + return true;
277 + }
278 +
279 + /**
280 + * Sanitizes the icon SVG content.
281 + *
282 + * Overrides the base class to allow the `rect` and `circle` shapes, plus the
283 + * stroke-related attributes and inline styles required by stroke-based icons.
284 + *
285 + * The signature is intentionally left without type declarations to stay
286 + * compatible with the parent WP_Icons_Registry::sanitize_icon_content()
287 + * shipped in WordPress core, which declares none.
288 + *
289 + * @param string $icon_content The icon SVG content to sanitize.
290 + * @return string The sanitized icon SVG content.
291 + */
292 + protected function sanitize_icon_content( $icon_content ) {
293 + return wp_kses( $icon_content, gutenberg_get_allowed_icon_svg_tags() );
294 + }
295 +
296 + /**
297 + * Retrieves the content of a registered icon.
298 + *
299 + * Overridden so that the file validation is applied even when the base
300 + * `WP_Icons_Registry` is provided by WordPress core rather than the
301 + * Gutenberg compat shim.
302 + *
177 303 * @param string $icon_name Icon name including namespace.
178 304 * @return string|null The content of the icon, if found.
179 305 */
180 306 protected function get_content( $icon_name ) {
181 307 if ( ! isset( $this->registered_icons[ $icon_name ]['content'] ) ) {
182 - $content = file_get_contents(
183 - $this->registered_icons[ $icon_name ]['file_path']
184 - );
185 - $content = $this->sanitize_icon_content( $content );
308 + $file_path = $this->registered_icons[ $icon_name ]['file_path'] ?? '';
309 + $is_stringy = is_string( $file_path ) || ( is_object( $file_path ) && method_exists( $file_path, '__toString' ) );
310 + $icon_path = $is_stringy ? realpath( (string) $file_path ) : false;
186 311
312 + if (
313 + ! is_string( $icon_path ) ||
314 + ! str_ends_with( $icon_path, '.svg' ) ||
315 + ! is_file( $icon_path ) ||
316 + ! is_readable( $icon_path )
317 + ) {
318 + wp_trigger_error(
319 + __METHOD__,
320 + __( 'Icon file is missing or unreadable.', 'gutenberg' )
321 + );
322 + return null;
323 + }
324 +
325 + $content = $this->sanitize_icon_content( file_get_contents( $icon_path ) );
326 +
187 327 if ( empty( $content ) ) {
188 328 wp_trigger_error(
189 329 __METHOD__,
190 330 __( 'Icon content does not contain valid SVG markup.', 'gutenberg' )
@@ -218,18 +358,38 @@
218 358 return $icons;
219 359 }
220 360
221 361 /**
222 - * Redefined to break away from base class.
362 + * Returns the shared registry instance.
363 + *
364 + * The base `$instance` slot is intentionally not redefined, so both
365 + * `WP_Icons_Registry::get_instance()` (used by core) and this method share
366 + * one instance. An existing base registry is upgraded, replaying any
367 + * non-`core/` and non-`core-admin/` icons so they are not lost.
223 368 */
224 - protected static $instance = null;
369 + public static function get_instance() {
370 + if ( ! self::$instance instanceof self ) {
371 + $original_registry = self::$instance;
372 + $gutenberg_registry = new self();
225 373
226 - /**
227 - * Redefined to access new `$instance`
228 - */
229 - public static function get_instance() {
230 - if ( null === self::$instance ) {
231 - self::$instance = new self();
374 + if ( null !== $original_registry ) {
375 + foreach ( $original_registry->get_registered_icons() as $icon ) {
376 + if ( str_starts_with( $icon['name'], 'core/' ) || str_starts_with( $icon['name'], 'core-admin/' ) ) {
377 + continue;
378 + }
379 + $icon_properties = array( 'label' => $icon['label'] );
380 + if ( ! empty( $icon['content'] ) ) {
381 + $icon_properties['content'] = $icon['content'];
382 + } elseif ( ! empty( $icon['file_path'] ) ) {
383 + $icon_properties['file_path'] = $icon['file_path'];
384 + } else {
385 + continue;
386 + }
387 + $gutenberg_registry->register( $icon['name'], $icon_properties );
388 + }
389 + }
390 +
391 + self::$instance = $gutenberg_registry;
232 392 }
233 393
234 394 return self::$instance;
235 395 }
@@ -235,63 +395,11 @@
235 395 }
236 396 }
237 397
238 398 /**
239 - * Forces WP_Icons_Registry_Gutenberg instantiation and overrides WP_Icons_Registry
240 - * so that all code using WP_Icons_Registry::{method_name}() receives the Gutenberg
241 - * registry.
399 + * Overrides the base `WP_Icons_Registry` singleton with the Gutenberg registry so
400 + * that all code using `WP_Icons_Registry::{method_name}()` receives it.
242 401 */
243 402 function gutenberg_override_wp_icons_registry() {
244 - /*
245 - * The plugin registers the `core/` icons from its own manifest, so core's
246 - * registration would only re-register the same names on this registry.
247 - */
248 - $wp_priority = has_action( 'init', '_wp_register_default_icons' );
249 - if ( false !== $wp_priority ) {
250 - remove_action( 'init', '_wp_register_default_icons', $wp_priority );
251 - }
252 -
253 - $reflection = new ReflectionClass( WP_Icons_Registry::class );
254 - $property = $reflection->getProperty( 'instance' );
255 - /*
256 - * ReflectionProperty::setAccessible is:
257 - * - redundant as of 8.1.0, which made all properties accessible
258 - * - deprecated as of 8.5.0
259 - * - needed until 8.1.0, as property `instance` is private
260 - */
261 - if ( PHP_VERSION_ID < 80100 ) {
262 - $property->setAccessible( true );
263 - }
264 - $original_registry = $property->getValue( null );
265 - $gutenberg_registry = WP_Icons_Registry_Gutenberg::get_instance();
266 -
267 - // If the original registry was already instantiated, replay any icons outside
268 - // the `core/` namespace onto the Gutenberg registry so they are not lost.
269 - if ( null !== $original_registry ) {
270 - $register_method = new ReflectionMethod( WP_Icons_Registry_Gutenberg::class, 'register' );
271 - /*
272 - * ReflectionMethod::setAccessible is:
273 - * - redundant as of 8.1.0, which made all properties accessible
274 - * - deprecated as of 8.5.0
275 - * - needed until 8.1.0, as property `instance` is private
276 - */
277 - if ( PHP_VERSION_ID < 80100 ) {
278 - $register_method->setAccessible( true );
279 - }
280 - foreach ( $original_registry->get_registered_icons() as $icon ) {
281 - if ( strpos( $icon['name'], 'core/' ) === 0 ) {
282 - continue;
283 - }
284 - $icon_properties = array( 'label' => $icon['label'] );
285 - if ( ! empty( $icon['content'] ) ) {
286 - $icon_properties['content'] = $icon['content'];
287 - } elseif ( ! empty( $icon['file_path'] ) ) {
288 - $icon_properties['file_path'] = $icon['file_path'];
289 - } else {
290 - continue;
291 - }
292 - $register_method->invoke( $gutenberg_registry, $icon['name'], $icon_properties );
293 - }
294 - }
295 - $property->setValue( null, $gutenberg_registry );
403 + WP_Icons_Registry_Gutenberg::get_instance();
296 404 }
297 405 add_action( 'init', 'gutenberg_override_wp_icons_registry', 1 );