| [ 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 `public` 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 bool $public Optional. Whether the icon is exposed through the REST API, and 62 * therefore selectable in the editor's icon picker. Non-public icons 63 * stay available to server-side code via {@see wp_get_icon()}. 64 * Default true. 65 * } 66 * @return bool True if the icon was registered with success and false otherwise. 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', 'public' ), 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 ( isset( $icon_properties['public'] ) && ! is_bool( $icon_properties['public'] ) ) { 148 _doing_it_wrong( 149 __METHOD__, 150 __( 'Icon public property must be a boolean.' ), 151 '7.2.0' 152 ); 153 return false; 154 } 155 156 if ( 157 ( ! isset( $icon_properties['content'] ) && ! isset( $icon_properties['file_path'] ) ) || 158 ( isset( $icon_properties['content'] ) && isset( $icon_properties['file_path'] ) ) 159 ) { 160 _doing_it_wrong( 161 __METHOD__, 162 __( 'Icons must provide either `content` or `file_path`.' ), 163 '7.0.0' 164 ); 165 return false; 166 } 167 168 if ( isset( $icon_properties['content'] ) ) { 169 if ( ! is_string( $icon_properties['content'] ) ) { 170 _doing_it_wrong( 171 __METHOD__, 172 __( 'Icon content must be a string.' ), 173 '7.0.0' 174 ); 175 return false; 176 } 177 178 $sanitized_icon_content = $this->sanitize_icon_content( $icon_properties['content'] ); 179 if ( empty( $sanitized_icon_content ) ) { 180 _doing_it_wrong( 181 __METHOD__, 182 __( 'Icon content does not contain valid SVG markup.' ), 183 '7.0.0' 184 ); 185 return false; 186 } 187 188 $icon_properties['content'] = $sanitized_icon_content; 189 } 190 191 $qualified_name = $collection . '/' . $unqualified_name; 192 193 if ( $this->is_registered( $qualified_name ) ) { 194 _doing_it_wrong( 195 __METHOD__, 196 __( 'Icon is already registered.' ), 197 '7.1.0' 198 ); 199 return false; 200 } 201 202 $icon = array_merge( 203 $icon_properties, 204 array( 205 'name' => $qualified_name, 206 'collection' => $collection, 207 ) 208 ); 209 210 $this->registered_icons[ $qualified_name ] = $icon; 211 212 return true; 213 } 214 215 /** 216 * Unregisters an icon. 217 * 218 * @since 7.1.0 219 * 220 * @param string $icon_name Namespaced icon name in the form "collection/icon-name" 221 * (e.g. "core/arrow-left"). 222 * @return bool True if the icon was unregistered successfully, false otherwise. 223 */ 224 public function unregister( $icon_name ) { 225 if ( ! $this->is_registered( $icon_name ) ) { 226 _doing_it_wrong( 227 __METHOD__, 228 sprintf( 229 /* translators: %s: Icon name. */ 230 __( 'Icon "%s" is not registered.' ), 231 $icon_name 232 ), 233 '7.1.0' 234 ); 235 return false; 236 } 237 238 unset( $this->registered_icons[ $icon_name ] ); 239 return true; 240 } 241 242 /** 243 * Builds the allowed attribute list for wp_kses() from attribute names. 244 * 245 * @since 7.2.0 246 * 247 * @param non-falsy-string ...$attribute_names Attribute names to allow. 248 * @return array<non-falsy-string, true> Attribute names mapped to true. 249 */ 250 private function get_allowed_attribute_list( ...$attribute_names ): array { 251 return array_fill_keys( $attribute_names, true ); 252 } 253 254 /** 255 * Sanitizes the icon SVG content. 256 * 257 * @since 7.0.0 258 * 259 * @param string $icon_content The icon SVG content to sanitize. 260 * @return string The sanitized icon SVG content. 261 */ 262 protected function sanitize_icon_content( $icon_content ) { 263 $stroke_attributes = $this->get_allowed_attribute_list( 264 'style', 265 'stroke', 266 'stroke-width', 267 'stroke-linecap', 268 'stroke-linejoin', 269 'stroke-miterlimit', 270 'vector-effect', 271 ); 272 273 $allowed_tags = array( 274 'svg' => array_merge( 275 $this->get_allowed_attribute_list( 276 'class', 277 'xmlns', 278 'width', 279 'height', 280 'viewbox', 281 'aria-hidden', 282 'role', 283 'focusable', 284 'fill', 285 'fill-rule', 286 'clip-rule', 287 ), 288 $stroke_attributes 289 ), 290 'path' => array_merge( 291 $this->get_allowed_attribute_list( 292 'fill', 293 'fill-rule', 294 'clip-rule', 295 'd', 296 'opacity', 297 'transform', 298 ), 299 $stroke_attributes 300 ), 301 'polygon' => array_merge( 302 $this->get_allowed_attribute_list( 303 'fill', 304 'fill-rule', 305 'clip-rule', 306 'points', 307 'transform', 308 'focusable', 309 ), 310 $stroke_attributes 311 ), 312 'rect' => array_merge( 313 $this->get_allowed_attribute_list( 314 'fill', 315 'fill-rule', 316 'clip-rule', 317 'x', 318 'y', 319 'width', 320 'height', 321 'rx', 322 'ry', 323 'transform', 324 ), 325 $stroke_attributes 326 ), 327 'circle' => array_merge( 328 $this->get_allowed_attribute_list( 329 'fill', 330 'fill-rule', 331 'clip-rule', 332 'cx', 333 'cy', 334 'r', 335 'transform', 336 ), 337 $stroke_attributes 338 ), 339 ); 340 return wp_kses( $icon_content, $allowed_tags ); 341 } 342 343 /** 344 * Retrieves the content of a registered icon. 345 * 346 * @since 7.0.0 347 * 348 * @param string $icon_name Icon name including namespace. 349 * @return string|null The content of the icon, if found. 350 */ 351 protected function get_content( $icon_name ) { 352 if ( ! isset( $this->registered_icons[ $icon_name ]['content'] ) ) { 353 $file_path = $this->registered_icons[ $icon_name ]['file_path'] ?? ''; 354 $is_stringy = is_string( $file_path ) || ( is_object( $file_path ) && method_exists( $file_path, '__toString' ) ); 355 $icon_path = $is_stringy ? realpath( (string) $file_path ) : false; 356 357 if ( 358 ! is_string( $icon_path ) || 359 ! str_ends_with( $icon_path, '.svg' ) || 360 ! is_file( $icon_path ) || 361 ! is_readable( $icon_path ) 362 ) { 363 wp_trigger_error( 364 __METHOD__, 365 __( 'Icon file is missing or unreadable.' ) 366 ); 367 return null; 368 } 369 370 $content = $this->sanitize_icon_content( file_get_contents( $icon_path ) ); 371 372 if ( empty( $content ) ) { 373 wp_trigger_error( 374 __METHOD__, 375 __( 'Icon content does not contain valid SVG markup.' ) 376 ); 377 return null; 378 } 379 380 $this->registered_icons[ $icon_name ]['content'] = $content; 381 } 382 return $this->registered_icons[ $icon_name ]['content']; 383 } 384 385 /** 386 * Retrieves an array containing the properties of a registered icon. 387 * 388 * @since 7.0.0 389 * 390 * @param string $icon_name Icon name including namespace. 391 * @return array|null Registered icon properties or `null` if the icon is not registered. 392 */ 393 public function get_registered_icon( $icon_name ) { 394 if ( ! $this->is_registered( $icon_name ) ) { 395 return null; 396 } 397 398 $icon = $this->registered_icons[ $icon_name ]; 399 $icon['content'] ??= $this->get_content( $icon_name ); 400 401 return $icon; 402 } 403 404 /** 405 * Retrieves all registered icons. 406 * 407 * @since 7.0.0 408 * @since 7.1.0 Search also matches icon labels. 409 * 410 * @param string $search Optional. Search term by which to filter the icons. 411 * @return array[] Array of arrays containing the registered icon properties. 412 */ 413 public function get_registered_icons( $search = '' ) { 414 $icons = array(); 415 416 foreach ( $this->registered_icons as $icon ) { 417 if ( ! empty( $search ) 418 && false === stripos( $icon['name'], $search ) 419 && false === stripos( $icon['label'] ?? '', $search ) 420 ) { 421 continue; 422 } 423 424 $icon['content'] ??= $this->get_content( $icon['name'] ); 425 $icons[] = $icon; 426 } 427 428 return $icons; 429 } 430 431 /** 432 * Checks if an icon is registered. 433 * 434 * @since 7.0.0 435 * 436 * @param string $icon_name Icon name including namespace. 437 * @return bool True if the icon is registered, false otherwise. 438 */ 439 public function is_registered( $icon_name ) { 440 return isset( $this->registered_icons[ $icon_name ] ); 441 } 442 443 /** 444 * Utility method to retrieve the main instance of the class. 445 * 446 * The instance will be created if it does not exist yet. 447 * 448 * @since 7.0.0 449 * 450 * @return WP_Icons_Registry The main instance. 451 */ 452 public static function get_instance() { 453 self::$instance ??= new self(); 454 455 return self::$instance; 456 } 457 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Tue Sep 22 08:20:31 2026 | Cross-referenced by PHPXref |