[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/ -> class-wp-icons-registry.php (source)

   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  }


Generated : Tue Oct 13 08:20:33 2026 Cross-referenced by PHPXref