[ 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 `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  }


Generated : Tue Sep 22 08:20:31 2026 Cross-referenced by PHPXref