PluginProbe
Gutenberg / 24.1.0
Gutenberg v24.1.0
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 +262 -119 23.2.2 → 24.1.0 View file →
@@ -1,72 +1,122 @@
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 - 'filePath' => $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 `filePath` if set.
63 - * If both `content` and `filePath` are not set, the icon will not be registered.
64 - * @type string $filePath Optional. The full path to the file containing the icon content.
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.
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 */
68 - protected function register( $icon_name, $icon_properties ) {
118 + public function register( $icon_name, $icon_properties ) {
69 119 if ( ! isset( $icon_name ) || ! is_string( $icon_name ) ) {
70 120 _doing_it_wrong(
71 121 __METHOD__,
72 122 __( 'Icon name must be a string.', 'gutenberg' ),
@@ -74,37 +124,38 @@
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 }
105 156
106 - $allowed_keys = array_fill_keys( array( 'label', 'content', 'filePath' ), 1 );
157 + $allowed_keys = array_fill_keys( array( 'label', 'content', 'file_path' ), 1 );
107 158 foreach ( array_keys( $icon_properties ) as $key ) {
108 159 if ( ! array_key_exists( $key, $allowed_keys ) ) {
109 160 _doing_it_wrong(
110 161 __METHOD__,
@@ -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' ),
@@ -128,14 +192,14 @@
128 192 return false;
129 193 }
130 194
131 195 if (
132 - ( ! isset( $icon_properties['content'] ) && ! isset( $icon_properties['filePath'] ) ) ||
133 - ( isset( $icon_properties['content'] ) && isset( $icon_properties['filePath'] ) )
196 + ( ! isset( $icon_properties['content'] ) && ! isset( $icon_properties['file_path'] ) ) ||
197 + ( isset( $icon_properties['content'] ) && isset( $icon_properties['file_path'] ) )
134 198 ) {
135 199 _doing_it_wrong(
136 200 __METHOD__,
137 - __( 'Icons must provide either `content` or `filePath`.', 'gutenberg' ),
201 + __( 'Icons must provide either `content` or `file_path`.', 'gutenberg' ),
138 202 '7.0.0'
139 203 );
140 204 return false;
141 205 }
@@ -158,21 +222,123 @@
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 /**
255 + * Unregisters an icon.
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 + *
303 + * @param string $icon_name Icon name including namespace.
304 + * @return string|null The content of the icon, if found.
305 + */
306 + protected function get_content( $icon_name ) {
307 + if ( ! isset( $this->registered_icons[ $icon_name ]['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;
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 +
327 + if ( empty( $content ) ) {
328 + wp_trigger_error(
329 + __METHOD__,
330 + __( 'Icon content does not contain valid SVG markup.', 'gutenberg' )
331 + );
332 + return null;
333 + }
334 +
335 + $this->registered_icons[ $icon_name ]['content'] = $content;
336 + }
337 + return $this->registered_icons[ $icon_name ]['content'];
338 + }
339 +
340 + /**
175 341 * Modified to also search in icon labels
176 342 */
177 343 public function get_registered_icons( $search = '' ) {
178 344 $icons = array();
@@ -192,18 +358,38 @@
192 358 return $icons;
193 359 }
194 360
195 361 /**
196 - * 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.
197 368 */
198 - 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();
199 373
200 - /**
201 - * Redefined to access new `$instance`
202 - */
203 - public static function get_instance() {
204 - if ( null === self::$instance ) {
205 - 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;
206 392 }
207 393
208 394 return self::$instance;
209 395 }
@@ -209,54 +395,11 @@
209 395 }
210 396 }
211 397
212 398 /**
213 - * Forces WP_Icons_Registry_Gutenberg instantiation and overrides WP_Icons_Registry
214 - * so that all code using WP_Icons_Registry::{method_name}() receives the Gutenberg
215 - * 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.
216 401 */
217 402 function gutenberg_override_wp_icons_registry() {
218 - $reflection = new ReflectionClass( WP_Icons_Registry::class );
219 - $property = $reflection->getProperty( 'instance' );
220 - /*
221 - * ReflectionProperty::setAccessible is:
222 - * - redundant as of 8.1.0, which made all properties accessible
223 - * - deprecated as of 8.5.0
224 - * - needed until 8.1.0, as property `instance` is private
225 - */
226 - if ( PHP_VERSION_ID < 80100 ) {
227 - $property->setAccessible( true );
228 - }
229 - $original_registry = $property->getValue( null );
230 - $gutenberg_registry = WP_Icons_Registry_Gutenberg::get_instance();
231 -
232 - // If the original registry was already instantiated, replay any icons outside
233 - // the `core/` namespace onto the Gutenberg registry so they are not lost.
234 - if ( null !== $original_registry ) {
235 - $register_method = new ReflectionMethod( WP_Icons_Registry_Gutenberg::class, 'register' );
236 - /*
237 - * ReflectionMethod::setAccessible is:
238 - * - redundant as of 8.1.0, which made all properties accessible
239 - * - deprecated as of 8.5.0
240 - * - needed until 8.1.0, as property `instance` is private
241 - */
242 - if ( PHP_VERSION_ID < 80100 ) {
243 - $register_method->setAccessible( true );
244 - }
245 - foreach ( $original_registry->get_registered_icons() as $icon ) {
246 - if ( strpos( $icon['name'], 'core/' ) === 0 ) {
247 - continue;
248 - }
249 - $icon_properties = array( 'label' => $icon['label'] );
250 - if ( ! empty( $icon['content'] ) ) {
251 - $icon_properties['content'] = $icon['content'];
252 - } elseif ( ! empty( $icon['filePath'] ) ) {
253 - $icon_properties['filePath'] = $icon['filePath'];
254 - } else {
255 - continue;
256 - }
257 - $register_method->invoke( $gutenberg_registry, $icon['name'], $icon_properties );
258 - }
259 - }
260 - $property->setValue( null, $gutenberg_registry );
403 + WP_Icons_Registry_Gutenberg::get_instance();
261 404 }
262 405 add_action( 'init', 'gutenberg_override_wp_icons_registry', 1 );