| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Icons API: WP_Icons_Registry class 4 * 5 * @package WordPress 6 * @since 7.0.0 7 */ 8 9 /** 10 * Core class used for interacting with registered icons. 11 * 12 * @since 7.0.0 13 */ 14 class WP_Icons_Registry { 15 /** 16 * Registered icons array. 17 * 18 * @since 7.0.0 19 * @var array[] 20 */ 21 protected $registered_icons = array(); 22 23 /** 24 * Container for the main instance of the class. 25 * 26 * @since 7.0.0 27 * @var WP_Icons_Registry|null 28 */ 29 protected static $instance = null; 30 31 /** 32 * Constructor. 33 * 34 * WP_Icons_Registry is a singleton class, so keep this protected. 35 * 36 * Icons are populated via `_wp_register_default_icons()` during the 37 * `init` action. Third-party icons can be registered via 38 * {@see wp_register_icon()} once their collection is registered. 39 * 40 * @since 7.0.0 41 */ 42 protected function __construct() {} 43 44 /** 45 * Registers an icon. 46 * 47 * @since 7.0.0 48 * @since 7.1.0 The icon name must be namespaced in the form "collection/icon-name". 49 * @since 7.2.0 Added the `keywords` property. 50 * 51 * @param string $icon_name Namespaced icon name in the form "collection/icon-name" 52 * (e.g. "core/arrow-left"). 53 * @param array $icon_properties { 54 * List of properties for the icon. 55 * 56 * @type string $label Required. A human-readable label for the icon. 57 * @type string $content Optional. SVG markup for the icon. 58 * If not provided, the content will be retrieved from the `file_path` if set. 59 * If both `content` and `file_path` are not set, the icon will not be registered. 60 * @type string $file_path Optional. The full path to the file containing the icon content. 61 * @type string[] $keywords Optional. Additional search terms for the icon, matched by 62 * `get_registered_icons()` alongside the name and label. 63 * } 64 * @return bool True if the icon was registered with success and false otherwise. 65 * 66 * @phpstan-param lowercase-string&non-falsy-string $icon_name 67 */ 68 public function register( $icon_name, $icon_properties ) { 69 if ( ! isset( $icon_name ) || ! is_string( $icon_name ) ) { 70 _doing_it_wrong( 71 __METHOD__, 72 __( 'Icon name must be a string.' ), 73 '7.0.0' 74 ); 75 return false; 76 } 77 78 // Require a namespaced name in the form "collection/icon-name". 79 if ( ! str_contains( $icon_name, '/' ) ) { 80 _doing_it_wrong( 81 __METHOD__, 82 __( 'Icon name must be namespaced in the form "collection/icon-name".' ), 83 '7.1.0' 84 ); 85 return false; 86 } 87 88 // Split the namespaced name into a collection slug and an unqualified icon name. 89 list( $collection, $unqualified_name ) = explode( '/', $icon_name, 2 ); 90 91 if ( preg_match( '/[A-Z]/', $unqualified_name ) ) { 92 _doing_it_wrong( 93 __METHOD__, 94 __( 'Icon names must not contain uppercase characters.' ), 95 '7.1.0' 96 ); 97 return false; 98 } 99 100 if ( ! preg_match( '/^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?$/', $unqualified_name ) ) { 101 _doing_it_wrong( 102 __METHOD__, 103 __( 'Icon names must start and end with a lowercase letter or digit and contain only lowercase letters, digits, hyphens, and underscores.' ), 104 '7.1.0' 105 ); 106 return false; 107 } 108 109 $allowed_keys = array_fill_keys( array( 'label', 'content', 'file_path', 'keywords' ), 1 ); 110 foreach ( array_keys( $icon_properties ) as $key ) { 111 if ( ! array_key_exists( $key, $allowed_keys ) ) { 112 _doing_it_wrong( 113 __METHOD__, 114 sprintf( 115 /* translators: %s: The name of a user-provided key. */ 116 __( 'Invalid icon property: "%s".' ), 117 $key 118 ), 119 '7.0.0' 120 ); 121 return false; 122 } 123 } 124 125 if ( ! WP_Icon_Collections_Registry::get_instance()->is_registered( $collection ) ) { 126 _doing_it_wrong( 127 __METHOD__, 128 sprintf( 129 /* translators: %s: Icon collection slug. */ 130 __( 'Icon collection "%s" is not registered.' ), 131 $collection 132 ), 133 '7.1.0' 134 ); 135 return false; 136 } 137 138 if ( ! isset( $icon_properties['label'] ) || ! is_string( $icon_properties['label'] ) ) { 139 _doing_it_wrong( 140 __METHOD__, 141 __( 'Icon label must be a string.' ), 142 '7.0.0' 143 ); 144 return false; 145 } 146 147 if ( array_key_exists( 'keywords', $icon_properties ) ) { 148 if ( ! is_array( $icon_properties['keywords'] ) ) { 149 _doing_it_wrong( 150 __METHOD__, 151 __( 'Icon keywords must be an array of strings.' ), 152 '7.2.0' 153 ); 154 return false; 155 } 156 157 foreach ( $icon_properties['keywords'] as $keyword ) { 158 if ( ! is_string( $keyword ) ) { 159 _doing_it_wrong( 160 __METHOD__, 161 __( 'Icon keywords must be an array of strings.' ), 162 '7.2.0' 163 ); 164 return false; 165 } 166 } 167 } 168 169 if ( 170 ( ! isset( $icon_properties['content'] ) && ! isset( $icon_properties['file_path'] ) ) || 171 ( isset( $icon_properties['content'] ) && isset( $icon_properties['file_path'] ) ) 172 ) { 173 _doing_it_wrong( 174 __METHOD__, 175 __( 'Icons must provide either `content` or `file_path`.' ), 176 '7.0.0' 177 ); 178 return false; 179 } 180 181 if ( isset( $icon_properties['content'] ) ) { 182 if ( ! is_string( $icon_properties['content'] ) ) { 183 _doing_it_wrong( 184 __METHOD__, 185 __( 'Icon content must be a string.' ), 186 '7.0.0' 187 ); 188 return false; 189 } 190 191 $sanitized_icon_content = $this->sanitize_icon_content( $icon_properties['content'] ); 192 if ( empty( $sanitized_icon_content ) ) { 193 _doing_it_wrong( 194 __METHOD__, 195 __( 'Icon content does not contain valid SVG markup.' ), 196 '7.0.0' 197 ); 198 return false; 199 } 200 201 $icon_properties['content'] = $sanitized_icon_content; 202 } 203 204 $qualified_name = $collection . '/' . $unqualified_name; 205 206 if ( $this->is_registered( $qualified_name ) ) { 207 _doing_it_wrong( 208 __METHOD__, 209 __( 'Icon is already registered.' ), 210 '7.1.0' 211 ); 212 return false; 213 } 214 215 $icon = array_merge( 216 $icon_properties, 217 array( 218 'name' => $qualified_name, 219 'collection' => $collection, 220 ) 221 ); 222 223 $this->registered_icons[ $qualified_name ] = $icon; 224 225 return true; 226 } 227 228 /** 229 * Unregisters an icon. 230 * 231 * @since 7.1.0 232 * 233 * @param string $icon_name Namespaced icon name in the form "collection/icon-name" 234 * (e.g. "core/arrow-left"). 235 * @return bool True if the icon was unregistered successfully, false otherwise. 236 */ 237 public function unregister( $icon_name ) { 238 if ( ! $this->is_registered( $icon_name ) ) { 239 _doing_it_wrong( 240 __METHOD__, 241 sprintf( 242 /* translators: %s: Icon name. */ 243 __( 'Icon "%s" is not registered.' ), 244 $icon_name 245 ), 246 '7.1.0' 247 ); 248 return false; 249 } 250 251 unset( $this->registered_icons[ $icon_name ] ); 252 return true; 253 } 254 255 /** 256 * Builds the allowed attribute list for wp_kses() from attribute names. 257 * 258 * @since 7.2.0 259 * 260 * @param non-falsy-string ...$attribute_names Attribute names to allow. 261 * @return array<non-falsy-string, true> Attribute names mapped to true. 262 */ 263 private function get_allowed_attribute_list( ...$attribute_names ): array { 264 return array_fill_keys( $attribute_names, true ); 265 } 266 267 /** 268 * Sanitizes the icon SVG content. 269 * 270 * @since 7.0.0 271 * 272 * @param string $icon_content The icon SVG content to sanitize. 273 * @return string The sanitized icon SVG content. 274 */ 275 protected function sanitize_icon_content( $icon_content ) { 276 $stroke_attributes = $this->get_allowed_attribute_list( 277 'style', 278 'stroke', 279 'stroke-width', 280 'stroke-linecap', 281 'stroke-linejoin', 282 'stroke-miterlimit', 283 'vector-effect', 284 ); 285 286 $allowed_tags = array( 287 'svg' => array_merge( 288 $this->get_allowed_attribute_list( 289 'class', 290 'xmlns', 291 'width', 292 'height', 293 'viewbox', 294 'aria-hidden', 295 'role', 296 'focusable', 297 'fill', 298 'fill-rule', 299 'clip-rule', 300 ), 301 $stroke_attributes 302 ), 303 'path' => array_merge( 304 $this->get_allowed_attribute_list( 305 'fill', 306 'fill-rule', 307 'clip-rule', 308 'd', 309 'opacity', 310 'transform', 311 ), 312 $stroke_attributes 313 ), 314 'polygon' => array_merge( 315 $this->get_allowed_attribute_list( 316 'fill', 317 'fill-rule', 318 'clip-rule', 319 'points', 320 'transform', 321 'focusable', 322 ), 323 $stroke_attributes 324 ), 325 'rect' => array_merge( 326 $this->get_allowed_attribute_list( 327 'fill', 328 'fill-rule', 329 'clip-rule', 330 'x', 331 'y', 332 'width', 333 'height', 334 'rx', 335 'ry', 336 'transform', 337 ), 338 $stroke_attributes 339 ), 340 'circle' => array_merge( 341 $this->get_allowed_attribute_list( 342 'fill', 343 'fill-rule', 344 'clip-rule', 345 'cx', 346 'cy', 347 'r', 348 'transform', 349 ), 350 $stroke_attributes 351 ), 352 ); 353 return wp_kses( $icon_content, $allowed_tags ); 354 } 355 356 /** 357 * Retrieves the content of a registered icon. 358 * 359 * @since 7.0.0 360 * 361 * @param string $icon_name Icon name including namespace. 362 * @return string|null The content of the icon, if found. 363 */ 364 protected function get_content( $icon_name ) { 365 if ( ! isset( $this->registered_icons[ $icon_name ]['content'] ) ) { 366 $file_path = $this->registered_icons[ $icon_name ]['file_path'] ?? ''; 367 $is_stringy = is_string( $file_path ) || ( is_object( $file_path ) && method_exists( $file_path, '__toString' ) ); 368 $icon_path = $is_stringy ? realpath( (string) $file_path ) : false; 369 370 if ( 371 ! is_string( $icon_path ) || 372 ! str_ends_with( $icon_path, '.svg' ) || 373 ! is_file( $icon_path ) || 374 ! is_readable( $icon_path ) 375 ) { 376 wp_trigger_error( 377 __METHOD__, 378 __( 'Icon file is missing or unreadable.' ) 379 ); 380 return null; 381 } 382 383 $content = $this->sanitize_icon_content( file_get_contents( $icon_path ) ); 384 385 if ( empty( $content ) ) { 386 wp_trigger_error( 387 __METHOD__, 388 __( 'Icon content does not contain valid SVG markup.' ) 389 ); 390 return null; 391 } 392 393 $this->registered_icons[ $icon_name ]['content'] = $content; 394 } 395 return $this->registered_icons[ $icon_name ]['content']; 396 } 397 398 /** 399 * Retrieves an array containing the properties of a registered icon. 400 * 401 * @since 7.0.0 402 * 403 * @param string $icon_name Icon name including namespace. 404 * @return array|null Registered icon properties or `null` if the icon is not registered. 405 */ 406 public function get_registered_icon( $icon_name ) { 407 if ( ! $this->is_registered( $icon_name ) ) { 408 return null; 409 } 410 411 $icon = $this->registered_icons[ $icon_name ]; 412 $icon['content'] ??= $this->get_content( $icon_name ); 413 414 return $icon; 415 } 416 417 /** 418 * Determines whether an icon matches a search term. 419 * 420 * The term is matched case-insensitively against the icon's name, its label, 421 * and any of its keywords. 422 * 423 * @since 7.2.0 424 * 425 * @param array $icon Registered icon properties. 426 * @param string $search Search term. 427 * @return bool True if the icon matches the search term, false otherwise. 428 */ 429 protected function icon_matches_search( $icon, $search ) { 430 if ( false !== stripos( $icon['name'], $search ) ) { 431 return true; 432 } 433 434 if ( false !== stripos( $icon['label'], $search ) ) { 435 return true; 436 } 437 438 foreach ( $icon['keywords'] ?? array() as $keyword ) { 439 if ( false !== stripos( $keyword, $search ) ) { 440 return true; 441 } 442 } 443 444 return false; 445 } 446 447 /** 448 * Retrieves all registered icons. 449 * 450 * @since 7.0.0 451 * @since 7.1.0 Search also matches icon labels. 452 * @since 7.2.0 Search also matches icon keywords. 453 * 454 * @param string $search Optional. Search term matched against each icon's name, 455 * label, and keywords. Default empty string, which returns 456 * every registered icon. 457 * @return array[] Array of arrays containing the registered icon properties. 458 */ 459 public function get_registered_icons( $search = '' ) { 460 $icons = array(); 461 462 foreach ( $this->registered_icons as $icon ) { 463 if ( ! empty( $search ) && ! $this->icon_matches_search( $icon, $search ) ) { 464 continue; 465 } 466 467 $icon['content'] ??= $this->get_content( $icon['name'] ); 468 $icons[] = $icon; 469 } 470 471 return $icons; 472 } 473 474 /** 475 * Checks if an icon is registered. 476 * 477 * @since 7.0.0 478 * 479 * @param string $icon_name Icon name including namespace. 480 * @return bool True if the icon is registered, false otherwise. 481 */ 482 public function is_registered( $icon_name ) { 483 return isset( $this->registered_icons[ $icon_name ] ); 484 } 485 486 /** 487 * Utility method to retrieve the main instance of the class. 488 * 489 * The instance will be created if it does not exist yet. 490 * 491 * @since 7.0.0 492 * 493 * @return WP_Icons_Registry The main instance. 494 */ 495 public static function get_instance() { 496 self::$instance ??= new self(); 497 498 return self::$instance; 499 } 500 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Tue Oct 13 08:20:33 2026 | Cross-referenced by PHPXref |