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 +262 -119 23.4.0 → trunk 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 );