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


Generated : Tue Nov 3 08:20:36 2026 Cross-referenced by PHPXref