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
gutenberg / lib / class-wp-icons-registry-gutenberg.php

class-wp-icons-registry-gutenberg.php in Gutenberg trunk, at lib/class-wp-icons-registry-gutenberg.php

406 lines 10.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
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 };
13
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 );
23
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 }
92
93
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() {}
101
102 /**
103 * Registers an icon.
104 *
105 * @param string $icon_name Namespaced icon name in the form "collection/icon-name"
106 * (e.g. "core/arrow-left").
107 * @param array $icon_properties {
108 * List of properties for the icon.
109 *
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.
115 * }
116 * @return bool True if the icon was registered with success and false otherwise.
117 */
118 public function register( $icon_name, $icon_properties ) {
119 if ( ! isset( $icon_name ) || ! is_string( $icon_name ) ) {
120 _doing_it_wrong(
121 __METHOD__,
122 __( 'Icon name must be a string.', 'gutenberg' ),
123 '7.0.0'
124 );
125 return false;
126 }
127
128 if ( ! str_contains( $icon_name, '/' ) ) {
129 _doing_it_wrong(
130 __METHOD__,
131 __( 'Icon name must be namespaced in the form "collection/icon-name".', 'gutenberg' ),
132 '7.1.0'
133 );
134 return false;
135 }
136
137 list( $collection, $unqualified_name ) = explode( '/', $icon_name, 2 );
138
139 if ( preg_match( '/[A-Z]/', $unqualified_name ) ) {
140 _doing_it_wrong(
141 __METHOD__,
142 __( 'Icon names must not contain uppercase characters.', 'gutenberg' ),
143 '7.1.0'
144 );
145 return false;
146 }
147
148 if ( ! preg_match( '/^[a-z0-9]([a-z0-9_-]*[a-z0-9])?$/', $unqualified_name ) ) {
149 _doing_it_wrong(
150 __METHOD__,
151 __( 'Icon names must start and end with a lowercase letter or digit and contain only lowercase letters, digits, hyphens, and underscores.', 'gutenberg' ),
152 '7.1.0'
153 );
154 return false;
155 }
156
157 $allowed_keys = array_fill_keys( array( 'label', 'content', 'file_path' ), 1 );
158 foreach ( array_keys( $icon_properties ) as $key ) {
159 if ( ! array_key_exists( $key, $allowed_keys ) ) {
160 _doing_it_wrong(
161 __METHOD__,
162 sprintf(
163 // translators: %s is the name of any user-provided key
164 __( 'Invalid icon property: "%s".', 'gutenberg' ),
165 $key
166 ),
167 '7.0.0'
168 );
169 return false;
170 }
171 }
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
186 if ( ! isset( $icon_properties['label'] ) || ! is_string( $icon_properties['label'] ) ) {
187 _doing_it_wrong(
188 __METHOD__,
189 __( 'Icon label must be a string.', 'gutenberg' ),
190 '7.0.0'
191 );
192 return false;
193 }
194
195 if (
196 ( ! isset( $icon_properties['content'] ) && ! isset( $icon_properties['file_path'] ) ) ||
197 ( isset( $icon_properties['content'] ) && isset( $icon_properties['file_path'] ) )
198 ) {
199 _doing_it_wrong(
200 __METHOD__,
201 __( 'Icons must provide either `content` or `file_path`.', 'gutenberg' ),
202 '7.0.0'
203 );
204 return false;
205 }
206
207 if ( isset( $icon_properties['content'] ) ) {
208 if ( ! is_string( $icon_properties['content'] ) ) {
209 _doing_it_wrong(
210 __METHOD__,
211 __( 'Icon content must be a string.', 'gutenberg' ),
212 '7.0.0'
213 );
214 return false;
215 }
216
217 $sanitized_icon_content = $this->sanitize_icon_content( $icon_properties['content'] );
218 if ( empty( $sanitized_icon_content ) ) {
219 _doing_it_wrong(
220 __METHOD__,
221 __( 'Icon content does not contain valid SVG markup.', 'gutenberg' ),
222 '7.0.0'
223 );
224 return false;
225 }
226
227 $icon_properties['content'] = $sanitized_icon_content;
228 }
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
241 $icon = array_merge(
242 $icon_properties,
243 array(
244 'name' => $qualified_name,
245 'collection' => $collection,
246 )
247 );
248
249 $this->registered_icons[ $qualified_name ] = $icon;
250
251 return true;
252 }
253
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 /**
341 * Modified to also search in icon labels
342 */
343 public function get_registered_icons( $search = '' ) {
344 $icons = array();
345
346 foreach ( $this->registered_icons as $icon ) {
347 if ( ! empty( $search )
348 && false === stripos( $icon['name'], $search )
349 && false === stripos( $icon['label'], $search )
350 ) {
351 continue;
352 }
353
354 $icon['content'] = $icon['content'] ?? $this->get_content( $icon['name'] );
355 $icons[] = $icon;
356 }
357
358 return $icons;
359 }
360
361 /**
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.
368 */
369 public static function get_instance() {
370 if ( ! self::$instance instanceof self ) {
371 $original_registry = self::$instance;
372 $gutenberg_registry = new self();
373
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;
392 }
393
394 return self::$instance;
395 }
396 }
397
398 /**
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.
401 */
402 function gutenberg_override_wp_icons_registry() {
403 WP_Icons_Registry_Gutenberg::get_instance();
404 }
405 add_action( 'init', 'gutenberg_override_wp_icons_registry', 1 );
406