[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/ -> kses.php (source)

   1  <?php
   2  /**
   3   * kses 0.2.2 - HTML/XHTML filter that only allows some elements and attributes
   4   * Copyright (C) 2002, 2003, 2005  Ulf Harnhammar
   5   *
   6   * This program is free software and open source software; you can redistribute
   7   * it and/or modify it under the terms of the GNU General Public License as
   8   * published by the Free Software Foundation; either version 2 of the License,
   9   * or (at your option) any later version.
  10   *
  11   * This program is distributed in the hope that it will be useful, but WITHOUT
  12   * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13   * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
  14   * more details.
  15   *
  16   * You should have received a copy of the GNU General Public License along
  17   * with this program; if not, write to the Free Software Foundation, Inc.,
  18   * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
  19   * http://www.gnu.org/licenses/gpl.html
  20   *
  21   * [kses strips evil scripts!]
  22   *
  23   * Added wp_ prefix to avoid conflicts with existing kses users
  24   *
  25   * @version 0.2.2
  26   * @copyright (C) 2002, 2003, 2005
  27   * @author Ulf Harnhammar <http://advogato.org/person/metaur/>
  28   *
  29   * @package External
  30   * @subpackage KSES
  31   */
  32  
  33  /**
  34   * Specifies the default allowable HTML tags.
  35   *
  36   * Using `CUSTOM_TAGS` is not recommended and should be considered deprecated. The
  37   * {@see 'wp_kses_allowed_html'} filter is more powerful and supplies context.
  38   *
  39   * When using this constant, make sure to set all of these globals to arrays:
  40   *
  41   *  - `$allowedposttags`
  42   *  - `$allowedtags`
  43   *  - `$allowedentitynames`
  44   *  - `$allowedxmlentitynames`
  45   *
  46   * @see wp_kses_allowed_html()
  47   * @since 1.2.0
  48   *
  49   * @var array[]|false Array of default allowable HTML tags, or false to use the defaults.
  50   */
  51  if ( ! defined( 'CUSTOM_TAGS' ) ) {
  52      define( 'CUSTOM_TAGS', false );
  53  }
  54  
  55  // Ensure that these variables are added to the global namespace
  56  // (e.g. if using namespaces / autoload in the current PHP environment).
  57  global $allowedposttags, $allowedtags, $allowedentitynames, $allowedxmlentitynames;
  58  
  59  if ( ! CUSTOM_TAGS ) {
  60      /**
  61       * KSES global for default allowable HTML tags.
  62       *
  63       * Can be overridden with the `CUSTOM_TAGS` constant.
  64       *
  65       * @var array[] $allowedposttags Array of default allowable HTML tags.
  66       * @since 2.0.0
  67       */
  68      $allowedposttags = array(
  69          'address'    => array(),
  70          'a'          => array(
  71              'href'     => true,
  72              'rel'      => true,
  73              'rev'      => true,
  74              'name'     => true,
  75              'target'   => true,
  76              'download' => array(
  77                  'valueless' => 'y',
  78              ),
  79          ),
  80          'abbr'       => array(),
  81          'acronym'    => array(),
  82          'area'       => array(
  83              'alt'    => true,
  84              'coords' => true,
  85              'href'   => true,
  86              'nohref' => true,
  87              'shape'  => true,
  88              'target' => true,
  89          ),
  90          'article'    => array(
  91              'align' => true,
  92          ),
  93          'aside'      => array(
  94              'align' => true,
  95          ),
  96          'audio'      => array(
  97              'autoplay' => true,
  98              'controls' => true,
  99              'loop'     => true,
 100              'muted'    => true,
 101              'preload'  => true,
 102              'src'      => true,
 103          ),
 104          'b'          => array(),
 105          'bdo'        => array(),
 106          'big'        => array(),
 107          'blockquote' => array(
 108              'cite' => true,
 109          ),
 110          'br'         => array(),
 111          'button'     => array(
 112              'disabled' => true,
 113              'name'     => true,
 114              'type'     => true,
 115              'value'    => true,
 116          ),
 117          'caption'    => array(
 118              'align' => true,
 119          ),
 120          'cite'       => array(),
 121          'code'       => array(),
 122          'col'        => array(
 123              'align'   => true,
 124              'char'    => true,
 125              'charoff' => true,
 126              'span'    => true,
 127              'valign'  => true,
 128              'width'   => true,
 129          ),
 130          'colgroup'   => array(
 131              'align'   => true,
 132              'char'    => true,
 133              'charoff' => true,
 134              'span'    => true,
 135              'valign'  => true,
 136              'width'   => true,
 137          ),
 138          'del'        => array(
 139              'datetime' => true,
 140          ),
 141          'dd'         => array(),
 142          'dfn'        => array(),
 143          'details'    => array(
 144              'align' => true,
 145              'open'  => true,
 146          ),
 147          'div'        => array(
 148              'align' => true,
 149          ),
 150          'dl'         => array(),
 151          'dt'         => array(),
 152          'em'         => array(),
 153          'fieldset'   => array(),
 154          'figure'     => array(
 155              'align' => true,
 156          ),
 157          'figcaption' => array(
 158              'align' => true,
 159          ),
 160          'font'       => array(
 161              'color' => true,
 162              'face'  => true,
 163              'size'  => true,
 164          ),
 165          'footer'     => array(
 166              'align' => true,
 167          ),
 168          'h1'         => array(
 169              'align' => true,
 170          ),
 171          'h2'         => array(
 172              'align' => true,
 173          ),
 174          'h3'         => array(
 175              'align' => true,
 176          ),
 177          'h4'         => array(
 178              'align' => true,
 179          ),
 180          'h5'         => array(
 181              'align' => true,
 182          ),
 183          'h6'         => array(
 184              'align' => true,
 185          ),
 186          'header'     => array(
 187              'align' => true,
 188          ),
 189          'hgroup'     => array(
 190              'align' => true,
 191          ),
 192          'hr'         => array(
 193              'align'   => true,
 194              'noshade' => true,
 195              'size'    => true,
 196              'width'   => true,
 197          ),
 198          'i'          => array(),
 199          'img'        => array(
 200              'alt'      => true,
 201              'align'    => true,
 202              'border'   => true,
 203              'height'   => true,
 204              'hspace'   => true,
 205              'loading'  => true,
 206              'longdesc' => true,
 207              'vspace'   => true,
 208              'src'      => true,
 209              'usemap'   => true,
 210              'width'    => true,
 211          ),
 212          'ins'        => array(
 213              'datetime' => true,
 214              'cite'     => true,
 215          ),
 216          'kbd'        => array(),
 217          'label'      => array(
 218              'for' => true,
 219          ),
 220          'legend'     => array(
 221              'align' => true,
 222          ),
 223          'li'         => array(
 224              'align' => true,
 225              'value' => true,
 226          ),
 227          'main'       => array(
 228              'align' => true,
 229          ),
 230          'map'        => array(
 231              'name' => true,
 232          ),
 233          'mark'       => array(),
 234          'menu'       => array(
 235              'type' => true,
 236          ),
 237          'nav'        => array(
 238              'align' => true,
 239          ),
 240          'object'     => array(
 241              'data' => array(
 242                  'required'       => true,
 243                  'value_callback' => '_wp_kses_allow_pdf_objects',
 244              ),
 245              'type' => array(
 246                  'required' => true,
 247                  'values'   => array( 'application/pdf' ),
 248              ),
 249          ),
 250          'p'          => array(
 251              'align' => true,
 252          ),
 253          'pre'        => array(
 254              'width' => true,
 255          ),
 256          'q'          => array(
 257              'cite' => true,
 258          ),
 259          'rb'         => array(),
 260          'rp'         => array(),
 261          'rt'         => array(),
 262          'rtc'        => array(),
 263          'ruby'       => array(),
 264          's'          => array(),
 265          'samp'       => array(),
 266          'span'       => array(
 267              'align' => true,
 268          ),
 269          'section'    => array(
 270              'align' => true,
 271          ),
 272          'small'      => array(),
 273          'strike'     => array(),
 274          'strong'     => array(),
 275          'sub'        => array(),
 276          'summary'    => array(
 277              'align' => true,
 278          ),
 279          'sup'        => array(),
 280          'table'      => array(
 281              'align'       => true,
 282              'bgcolor'     => true,
 283              'border'      => true,
 284              'cellpadding' => true,
 285              'cellspacing' => true,
 286              'rules'       => true,
 287              'summary'     => true,
 288              'width'       => true,
 289          ),
 290          'tbody'      => array(
 291              'align'   => true,
 292              'char'    => true,
 293              'charoff' => true,
 294              'valign'  => true,
 295          ),
 296          'td'         => array(
 297              'abbr'    => true,
 298              'align'   => true,
 299              'axis'    => true,
 300              'bgcolor' => true,
 301              'char'    => true,
 302              'charoff' => true,
 303              'colspan' => true,
 304              'headers' => true,
 305              'height'  => true,
 306              'nowrap'  => true,
 307              'rowspan' => true,
 308              'scope'   => true,
 309              'valign'  => true,
 310              'width'   => true,
 311          ),
 312          'textarea'   => array(
 313              'cols'     => true,
 314              'rows'     => true,
 315              'disabled' => true,
 316              'name'     => true,
 317              'readonly' => true,
 318          ),
 319          'tfoot'      => array(
 320              'align'   => true,
 321              'char'    => true,
 322              'charoff' => true,
 323              'valign'  => true,
 324          ),
 325          'th'         => array(
 326              'abbr'    => true,
 327              'align'   => true,
 328              'axis'    => true,
 329              'bgcolor' => true,
 330              'char'    => true,
 331              'charoff' => true,
 332              'colspan' => true,
 333              'headers' => true,
 334              'height'  => true,
 335              'nowrap'  => true,
 336              'rowspan' => true,
 337              'scope'   => true,
 338              'valign'  => true,
 339              'width'   => true,
 340          ),
 341          'thead'      => array(
 342              'align'   => true,
 343              'char'    => true,
 344              'charoff' => true,
 345              'valign'  => true,
 346          ),
 347          'title'      => array(),
 348          'tr'         => array(
 349              'align'   => true,
 350              'bgcolor' => true,
 351              'char'    => true,
 352              'charoff' => true,
 353              'valign'  => true,
 354          ),
 355          'track'      => array(
 356              'default' => true,
 357              'kind'    => true,
 358              'label'   => true,
 359              'src'     => true,
 360              'srclang' => true,
 361          ),
 362          'tt'         => array(),
 363          'u'          => array(),
 364          'ul'         => array(
 365              'type' => true,
 366          ),
 367          'ol'         => array(
 368              'start'    => true,
 369              'type'     => true,
 370              'reversed' => true,
 371          ),
 372          'var'        => array(),
 373          'video'      => array(
 374              'autoplay'    => true,
 375              'controls'    => true,
 376              'height'      => true,
 377              'loop'        => true,
 378              'muted'       => true,
 379              'playsinline' => true,
 380              'poster'      => true,
 381              'preload'     => true,
 382              'src'         => true,
 383              'width'       => true,
 384          ),
 385      );
 386  
 387      /**
 388       * @var array[] $allowedtags Array of KSES allowed HTML elements.
 389       * @since 1.0.0
 390       */
 391      $allowedtags = array(
 392          'a'          => array(
 393              'href'  => true,
 394              'title' => true,
 395          ),
 396          'abbr'       => array(
 397              'title' => true,
 398          ),
 399          'acronym'    => array(
 400              'title' => true,
 401          ),
 402          'b'          => array(),
 403          'blockquote' => array(
 404              'cite' => true,
 405          ),
 406          'cite'       => array(),
 407          'code'       => array(),
 408          'del'        => array(
 409              'datetime' => true,
 410          ),
 411          'em'         => array(),
 412          'i'          => array(),
 413          'q'          => array(
 414              'cite' => true,
 415          ),
 416          's'          => array(),
 417          'strike'     => array(),
 418          'strong'     => array(),
 419      );
 420  
 421      /**
 422       * @var string[] $allowedentitynames Array of KSES allowed HTML entity names.
 423       * @since 1.0.0
 424       */
 425      $allowedentitynames = array(
 426          'nbsp',
 427          'iexcl',
 428          'cent',
 429          'pound',
 430          'curren',
 431          'yen',
 432          'brvbar',
 433          'sect',
 434          'uml',
 435          'copy',
 436          'ordf',
 437          'laquo',
 438          'not',
 439          'shy',
 440          'reg',
 441          'macr',
 442          'deg',
 443          'plusmn',
 444          'acute',
 445          'micro',
 446          'para',
 447          'middot',
 448          'cedil',
 449          'ordm',
 450          'raquo',
 451          'iquest',
 452          'Agrave',
 453          'Aacute',
 454          'Acirc',
 455          'Atilde',
 456          'Auml',
 457          'Aring',
 458          'AElig',
 459          'Ccedil',
 460          'Egrave',
 461          'Eacute',
 462          'Ecirc',
 463          'Euml',
 464          'Igrave',
 465          'Iacute',
 466          'Icirc',
 467          'Iuml',
 468          'ETH',
 469          'Ntilde',
 470          'Ograve',
 471          'Oacute',
 472          'Ocirc',
 473          'Otilde',
 474          'Ouml',
 475          'times',
 476          'Oslash',
 477          'Ugrave',
 478          'Uacute',
 479          'Ucirc',
 480          'Uuml',
 481          'Yacute',
 482          'THORN',
 483          'szlig',
 484          'agrave',
 485          'aacute',
 486          'acirc',
 487          'atilde',
 488          'auml',
 489          'aring',
 490          'aelig',
 491          'ccedil',
 492          'egrave',
 493          'eacute',
 494          'ecirc',
 495          'euml',
 496          'igrave',
 497          'iacute',
 498          'icirc',
 499          'iuml',
 500          'eth',
 501          'ntilde',
 502          'ograve',
 503          'oacute',
 504          'ocirc',
 505          'otilde',
 506          'ouml',
 507          'divide',
 508          'oslash',
 509          'ugrave',
 510          'uacute',
 511          'ucirc',
 512          'uuml',
 513          'yacute',
 514          'thorn',
 515          'yuml',
 516          'quot',
 517          'amp',
 518          'lt',
 519          'gt',
 520          'apos',
 521          'OElig',
 522          'oelig',
 523          'Scaron',
 524          'scaron',
 525          'Yuml',
 526          'circ',
 527          'tilde',
 528          'ensp',
 529          'emsp',
 530          'thinsp',
 531          'zwnj',
 532          'zwj',
 533          'lrm',
 534          'rlm',
 535          'ndash',
 536          'mdash',
 537          'lsquo',
 538          'rsquo',
 539          'sbquo',
 540          'ldquo',
 541          'rdquo',
 542          'bdquo',
 543          'dagger',
 544          'Dagger',
 545          'permil',
 546          'lsaquo',
 547          'rsaquo',
 548          'euro',
 549          'fnof',
 550          'Alpha',
 551          'Beta',
 552          'Gamma',
 553          'Delta',
 554          'Epsilon',
 555          'Zeta',
 556          'Eta',
 557          'Theta',
 558          'Iota',
 559          'Kappa',
 560          'Lambda',
 561          'Mu',
 562          'Nu',
 563          'Xi',
 564          'Omicron',
 565          'Pi',
 566          'Rho',
 567          'Sigma',
 568          'Tau',
 569          'Upsilon',
 570          'Phi',
 571          'Chi',
 572          'Psi',
 573          'Omega',
 574          'alpha',
 575          'beta',
 576          'gamma',
 577          'delta',
 578          'epsilon',
 579          'zeta',
 580          'eta',
 581          'theta',
 582          'iota',
 583          'kappa',
 584          'lambda',
 585          'mu',
 586          'nu',
 587          'xi',
 588          'omicron',
 589          'pi',
 590          'rho',
 591          'sigmaf',
 592          'sigma',
 593          'tau',
 594          'upsilon',
 595          'phi',
 596          'chi',
 597          'psi',
 598          'omega',
 599          'thetasym',
 600          'upsih',
 601          'piv',
 602          'bull',
 603          'hellip',
 604          'prime',
 605          'Prime',
 606          'oline',
 607          'frasl',
 608          'weierp',
 609          'image',
 610          'real',
 611          'trade',
 612          'alefsym',
 613          'larr',
 614          'uarr',
 615          'rarr',
 616          'darr',
 617          'harr',
 618          'crarr',
 619          'lArr',
 620          'uArr',
 621          'rArr',
 622          'dArr',
 623          'hArr',
 624          'forall',
 625          'part',
 626          'exist',
 627          'empty',
 628          'nabla',
 629          'isin',
 630          'notin',
 631          'ni',
 632          'prod',
 633          'sum',
 634          'minus',
 635          'lowast',
 636          'radic',
 637          'prop',
 638          'infin',
 639          'ang',
 640          'and',
 641          'or',
 642          'cap',
 643          'cup',
 644          'int',
 645          'sim',
 646          'cong',
 647          'asymp',
 648          'ne',
 649          'equiv',
 650          'le',
 651          'ge',
 652          'sub',
 653          'sup',
 654          'nsub',
 655          'sube',
 656          'supe',
 657          'oplus',
 658          'otimes',
 659          'perp',
 660          'sdot',
 661          'lceil',
 662          'rceil',
 663          'lfloor',
 664          'rfloor',
 665          'lang',
 666          'rang',
 667          'loz',
 668          'spades',
 669          'clubs',
 670          'hearts',
 671          'diams',
 672          'sup1',
 673          'sup2',
 674          'sup3',
 675          'frac14',
 676          'frac12',
 677          'frac34',
 678          'there4',
 679      );
 680  
 681      /**
 682       * @var string[] $allowedxmlentitynames Array of KSES allowed XML entity names.
 683       * @since 5.5.0
 684       */
 685      $allowedxmlentitynames = array(
 686          'amp',
 687          'lt',
 688          'gt',
 689          'apos',
 690          'quot',
 691      );
 692  
 693      $allowedposttags = array_map( '_wp_add_global_attributes', $allowedposttags );
 694  } else {
 695      $required_kses_globals = array(
 696          'allowedposttags',
 697          'allowedtags',
 698          'allowedentitynames',
 699          'allowedxmlentitynames',
 700      );
 701      $missing_kses_globals  = array();
 702  
 703      foreach ( $required_kses_globals as $global_name ) {
 704          if ( ! isset( $GLOBALS[ $global_name ] ) || ! is_array( $GLOBALS[ $global_name ] ) ) {
 705              $missing_kses_globals[] = '<code>$' . $global_name . '</code>';
 706          }
 707      }
 708  
 709      if ( $missing_kses_globals ) {
 710          _doing_it_wrong(
 711              'wp_kses_allowed_html',
 712              sprintf(
 713                  /* translators: 1: CUSTOM_TAGS, 2: Global variable names. */
 714                  __( 'When using the %1$s constant, make sure to set these globals to an array: %2$s.' ),
 715                  '<code>CUSTOM_TAGS</code>',
 716                  implode( ', ', $missing_kses_globals )
 717              ),
 718              '6.2.0'
 719          );
 720      }
 721  
 722      $allowedtags     = wp_kses_array_lc( $allowedtags );
 723      $allowedposttags = wp_kses_array_lc( $allowedposttags );
 724  }
 725  
 726  /**
 727   * Filters text content and strips out disallowed HTML.
 728   *
 729   * This function makes sure that only the allowed HTML element names, attribute
 730   * names, attribute values, and HTML entities will occur in the given text string.
 731   *
 732   * This function expects unslashed data.
 733   *
 734   * @see wp_kses_post() for specifically filtering post content and fields.
 735   * @see wp_allowed_protocols() for the default allowed protocols in link URLs.
 736   *
 737   * @since 1.0.0
 738   *
 739   * @param string         $content           Text content to filter.
 740   * @param array[]|string $allowed_html      An array of allowed HTML elements and attributes,
 741   *                                          or a context name such as 'post'. See wp_kses_allowed_html()
 742   *                                          for the list of accepted context names.
 743   * @param string[]       $allowed_protocols Optional. Array of allowed URL protocols.
 744   *                                          Defaults to the result of wp_allowed_protocols().
 745   * @return string Filtered content containing only the allowed HTML.
 746   */
 747  function wp_kses( $content, $allowed_html, $allowed_protocols = array() ) {
 748      if ( empty( $allowed_protocols ) ) {
 749          $allowed_protocols = wp_allowed_protocols();
 750      }
 751  
 752      $content = wp_kses_no_null( $content, array( 'slash_zero' => 'keep' ) );
 753      $content = wp_kses_normalize_entities( $content );
 754      $content = wp_kses_hook( $content, $allowed_html, $allowed_protocols );
 755  
 756      return wp_kses_split( $content, $allowed_html, $allowed_protocols );
 757  }
 758  
 759  /**
 760   * Filters one HTML attribute and ensures its value is allowed.
 761   *
 762   * This function can escape data in some situations where `wp_kses()` must strip the whole attribute.
 763   *
 764   * @since 4.2.3
 765   *
 766   * @param string $attr    The 'whole' attribute, including name and value.
 767   * @param string $element The HTML element name to which the attribute belongs.
 768   * @return string Filtered attribute.
 769   */
 770  function wp_kses_one_attr( $attr, $element ) {
 771      $uris              = wp_kses_uri_attributes();
 772      $allowed_html      = wp_kses_allowed_html( 'post' );
 773      $allowed_protocols = wp_allowed_protocols();
 774      $attr              = wp_kses_no_null( $attr, array( 'slash_zero' => 'keep' ) );
 775  
 776      // Preserve leading and trailing whitespace.
 777      $matches = array();
 778      preg_match( '/^\s*/', $attr, $matches );
 779      $lead = $matches[0];
 780      preg_match( '/\s*$/', $attr, $matches );
 781      $trail = $matches[0];
 782      if ( empty( $trail ) ) {
 783          $attr = substr( $attr, strlen( $lead ) );
 784      } else {
 785          $attr = substr( $attr, strlen( $lead ), -strlen( $trail ) );
 786      }
 787  
 788      // Parse attribute name and value from input.
 789      $split = preg_split( '/\s*=\s*/', $attr, 2 );
 790      $name  = $split[0];
 791      if ( count( $split ) === 2 ) {
 792          $value = $split[1];
 793  
 794          /*
 795           * Remove quotes surrounding $value.
 796           * Also guarantee correct quoting in $attr for this one attribute.
 797           */
 798          if ( '' === $value ) {
 799              $quote = '';
 800          } else {
 801              $quote = $value[0];
 802          }
 803          if ( '"' === $quote || "'" === $quote ) {
 804              if ( ! str_ends_with( $value, $quote ) ) {
 805                  return '';
 806              }
 807              $value = substr( $value, 1, -1 );
 808          } else {
 809              $quote = '"';
 810          }
 811  
 812          // Sanitize quotes, angle braces, and entities.
 813          $value = esc_attr( $value );
 814  
 815          // Sanitize URI values.
 816          if ( in_array( strtolower( $name ), $uris, true ) ) {
 817              $value = wp_kses_bad_protocol( $value, $allowed_protocols );
 818          }
 819  
 820          $attr  = "$name=$quote$value$quote";
 821          $vless = 'n';
 822      } else {
 823          $value = '';
 824          $vless = 'y';
 825      }
 826  
 827      // Sanitize attribute by name.
 828      wp_kses_attr_check( $name, $value, $attr, $vless, $element, $allowed_html );
 829  
 830      // Restore whitespace.
 831      return $lead . $attr . $trail;
 832  }
 833  
 834  /**
 835   * Returns an array of allowed HTML tags and attributes for a given context.
 836   *
 837   * @since 3.5.0
 838   * @since 5.0.1 `form` removed as allowable HTML tag.
 839   *
 840   * @global array $allowedposttags
 841   * @global array $allowedtags
 842   * @global array $allowedentitynames
 843   *
 844   * @param string|array $context The context for which to retrieve tags. Allowed values are 'post',
 845   *                              'strip', 'data', 'entities', or the name of a field filter such as
 846   *                              'pre_user_description', or an array of allowed HTML elements and attributes.
 847   * @return array Array of allowed HTML tags and their allowed attributes.
 848   */
 849  function wp_kses_allowed_html( $context = '' ) {
 850      global $allowedposttags, $allowedtags, $allowedentitynames;
 851  
 852      if ( is_array( $context ) ) {
 853          // When `$context` is an array it's actually an array of allowed HTML elements and attributes.
 854          $html    = $context;
 855          $context = 'explicit';
 856  
 857          /**
 858           * Filters the HTML tags that are allowed for a given context.
 859           *
 860           * HTML tags and attribute names are case-insensitive in HTML but must be
 861           * added to the KSES allow list in lowercase. An item added to the allow list
 862           * in upper or mixed case will not recognized as permitted by KSES.
 863           *
 864           * @since 3.5.0
 865           *
 866           * @param array[] $html    Allowed HTML tags.
 867           * @param string  $context Context name.
 868           */
 869          return apply_filters( 'wp_kses_allowed_html', $html, $context );
 870      }
 871  
 872      switch ( $context ) {
 873          case 'post':
 874              /** This filter is documented in wp-includes/kses.php */
 875              $tags = apply_filters( 'wp_kses_allowed_html', $allowedposttags, $context );
 876  
 877              // 5.0.1 removed the `<form>` tag, allow it if a filter is allowing it's sub-elements `<input>` or `<select>`.
 878              if ( ! CUSTOM_TAGS && ! isset( $tags['form'] ) && ( isset( $tags['input'] ) || isset( $tags['select'] ) ) ) {
 879                  $tags = $allowedposttags;
 880  
 881                  $tags['form'] = array(
 882                      'action'         => true,
 883                      'accept'         => true,
 884                      'accept-charset' => true,
 885                      'enctype'        => true,
 886                      'method'         => true,
 887                      'name'           => true,
 888                      'target'         => true,
 889                  );
 890  
 891                  /** This filter is documented in wp-includes/kses.php */
 892                  $tags = apply_filters( 'wp_kses_allowed_html', $tags, $context );
 893              }
 894  
 895              return $tags;
 896  
 897          case 'user_description':
 898          case 'pre_user_description':
 899              $tags             = $allowedtags;
 900              $tags['a']['rel'] = true;
 901              /** This filter is documented in wp-includes/kses.php */
 902              return apply_filters( 'wp_kses_allowed_html', $tags, $context );
 903  
 904          case 'strip':
 905              /** This filter is documented in wp-includes/kses.php */
 906              return apply_filters( 'wp_kses_allowed_html', array(), $context );
 907  
 908          case 'entities':
 909              /** This filter is documented in wp-includes/kses.php */
 910              return apply_filters( 'wp_kses_allowed_html', $allowedentitynames, $context );
 911  
 912          case 'data':
 913          default:
 914              /** This filter is documented in wp-includes/kses.php */
 915              return apply_filters( 'wp_kses_allowed_html', $allowedtags, $context );
 916      }
 917  }
 918  
 919  /**
 920   * You add any KSES hooks here.
 921   *
 922   * There is currently only one KSES WordPress hook, {@see 'pre_kses'}, and it is called here.
 923   * All parameters are passed to the hooks and expected to receive a string.
 924   *
 925   * @since 1.0.0
 926   *
 927   * @param string         $content           Content to filter through KSES.
 928   * @param array[]|string $allowed_html      An array of allowed HTML elements and attributes,
 929   *                                          or a context name such as 'post'. See wp_kses_allowed_html()
 930   *                                          for the list of accepted context names.
 931   * @param string[]       $allowed_protocols Array of allowed URL protocols.
 932   * @return string Filtered content through {@see 'pre_kses'} hook.
 933   */
 934  function wp_kses_hook( $content, $allowed_html, $allowed_protocols ) {
 935      /**
 936       * Filters content to be run through KSES.
 937       *
 938       * @since 2.3.0
 939       *
 940       * @param string         $content           Content to filter through KSES.
 941       * @param array[]|string $allowed_html      An array of allowed HTML elements and attributes,
 942       *                                          or a context name such as 'post'. See wp_kses_allowed_html()
 943       *                                          for the list of accepted context names.
 944       * @param string[]       $allowed_protocols Array of allowed URL protocols.
 945       */
 946      return apply_filters( 'pre_kses', $content, $allowed_html, $allowed_protocols );
 947  }
 948  
 949  /**
 950   * Returns the version number of KSES.
 951   *
 952   * @since 1.0.0
 953   *
 954   * @return string KSES version number.
 955   */
 956  function wp_kses_version() {
 957      return '0.2.2';
 958  }
 959  
 960  /**
 961   * Searches for HTML tags, no matter how malformed.
 962   *
 963   * It also matches stray `>` characters.
 964   *
 965   * @since 1.0.0
 966   *
 967   * @global array[]|string $pass_allowed_html      An array of allowed HTML elements and attributes,
 968   *                                                or a context name such as 'post'.
 969   * @global string[]       $pass_allowed_protocols Array of allowed URL protocols.
 970   *
 971   * @param string         $content           Content to filter.
 972   * @param array[]|string $allowed_html      An array of allowed HTML elements and attributes,
 973   *                                          or a context name such as 'post'. See wp_kses_allowed_html()
 974   *                                          for the list of accepted context names.
 975   * @param string[]       $allowed_protocols Array of allowed URL protocols.
 976   * @return string Content with fixed HTML tags
 977   */
 978  function wp_kses_split( $content, $allowed_html, $allowed_protocols ) {
 979      global $pass_allowed_html, $pass_allowed_protocols;
 980  
 981      $pass_allowed_html      = $allowed_html;
 982      $pass_allowed_protocols = $allowed_protocols;
 983  
 984      return preg_replace_callback( '%(<!--.*?(-->|$))|(<[^>]*(>|$)|>)%', '_wp_kses_split_callback', $content );
 985  }
 986  
 987  /**
 988   * Returns an array of HTML attribute names whose value contains a URL.
 989   *
 990   * This function returns a list of all HTML attributes that must contain
 991   * a URL according to the HTML specification.
 992   *
 993   * This list includes URI attributes both allowed and disallowed by KSES.
 994   *
 995   * @link https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes
 996   *
 997   * @since 5.0.1
 998   *
 999   * @return string[] HTML attribute names whose value contains a URL.
1000   */
1001  function wp_kses_uri_attributes() {
1002      $uri_attributes = array(
1003          'action',
1004          'archive',
1005          'background',
1006          'cite',
1007          'classid',
1008          'codebase',
1009          'data',
1010          'formaction',
1011          'href',
1012          'icon',
1013          'longdesc',
1014          'manifest',
1015          'poster',
1016          'profile',
1017          'src',
1018          'usemap',
1019          'xmlns',
1020      );
1021  
1022      /**
1023       * Filters the list of attributes that are required to contain a URL.
1024       *
1025       * Use this filter to add any `data-` attributes that are required to be
1026       * validated as a URL.
1027       *
1028       * @since 5.0.1
1029       *
1030       * @param string[] $uri_attributes HTML attribute names whose value contains a URL.
1031       */
1032      $uri_attributes = apply_filters( 'wp_kses_uri_attributes', $uri_attributes );
1033  
1034      return $uri_attributes;
1035  }
1036  
1037  /**
1038   * Callback for `wp_kses_split()`.
1039   *
1040   * @since 3.1.0
1041   * @access private
1042   * @ignore
1043   *
1044   * @global array[]|string $pass_allowed_html      An array of allowed HTML elements and attributes,
1045   *                                                or a context name such as 'post'.
1046   * @global string[]       $pass_allowed_protocols Array of allowed URL protocols.
1047   *
1048   * @param array $matches preg_replace regexp matches
1049   * @return string
1050   */
1051  function _wp_kses_split_callback( $matches ) {
1052      global $pass_allowed_html, $pass_allowed_protocols;
1053  
1054      return wp_kses_split2( $matches[0], $pass_allowed_html, $pass_allowed_protocols );
1055  }
1056  
1057  /**
1058   * Callback for `wp_kses_split()` for fixing malformed HTML tags.
1059   *
1060   * This function does a lot of work. It rejects some very malformed things like
1061   * `<:::>`. It returns an empty string, if the element isn't allowed (look ma, no
1062   * `strip_tags()`!). Otherwise it splits the tag into an element and an attribute
1063   * list.
1064   *
1065   * After the tag is split into an element and an attribute list, it is run
1066   * through another filter which will remove illegal attributes and once that is
1067   * completed, will be returned.
1068   *
1069   * @access private
1070   * @ignore
1071   * @since 1.0.0
1072   *
1073   * @param string         $content           Content to filter.
1074   * @param array[]|string $allowed_html      An array of allowed HTML elements and attributes,
1075   *                                          or a context name such as 'post'. See wp_kses_allowed_html()
1076   *                                          for the list of accepted context names.
1077   * @param string[]       $allowed_protocols Array of allowed URL protocols.
1078   * @return string Fixed HTML element
1079   */
1080  function wp_kses_split2( $content, $allowed_html, $allowed_protocols ) {
1081      $content = wp_kses_stripslashes( $content );
1082  
1083      // It matched a ">" character.
1084      if ( ! str_starts_with( $content, '<' ) ) {
1085          return '&gt;';
1086      }
1087  
1088      // Allow HTML comments.
1089      if ( str_starts_with( $content, '<!--' ) ) {
1090          $content = str_replace( array( '<!--', '-->' ), '', $content );
1091  
1092          while ( ( $newstring = wp_kses( $content, $allowed_html, $allowed_protocols ) ) !== $content ) {
1093              $content = $newstring;
1094          }
1095  
1096          if ( '' === $content ) {
1097              return '';
1098          }
1099  
1100          // Prevent multiple dashes in comments.
1101          $content = preg_replace( '/--+/', '-', $content );
1102          // Prevent three dashes closing a comment.
1103          $content = preg_replace( '/-$/', '', $content );
1104  
1105          return "<!--{$content}-->";
1106      }
1107  
1108      // It's seriously malformed.
1109      if ( ! preg_match( '%^<\s*(/\s*)?([a-zA-Z0-9-]+)([^>]*)>?$%', $content, $matches ) ) {
1110          return '';
1111      }
1112  
1113      $slash    = trim( $matches[1] );
1114      $elem     = $matches[2];
1115      $attrlist = $matches[3];
1116  
1117      if ( ! is_array( $allowed_html ) ) {
1118          $allowed_html = wp_kses_allowed_html( $allowed_html );
1119      }
1120  
1121      // They are using a not allowed HTML element.
1122      if ( ! isset( $allowed_html[ strtolower( $elem ) ] ) ) {
1123          return '';
1124      }
1125  
1126      // No attributes are allowed for closing elements.
1127      if ( '' !== $slash ) {
1128          return "</$elem>";
1129      }
1130  
1131      return wp_kses_attr( $elem, $attrlist, $allowed_html, $allowed_protocols );
1132  }
1133  
1134  /**
1135   * Removes all attributes, if none are allowed for this element.
1136   *
1137   * If some are allowed it calls `wp_kses_hair()` to split them further, and then
1138   * it builds up new HTML code from the data that `wp_kses_hair()` returns. It also
1139   * removes `<` and `>` characters, if there are any left. One more thing it does
1140   * is to check if the tag has a closing XHTML slash, and if it does, it puts one
1141   * in the returned code as well.
1142   *
1143   * An array of allowed values can be defined for attributes. If the attribute value
1144   * doesn't fall into the list, the attribute will be removed from the tag.
1145   *
1146   * Attributes can be marked as required. If a required attribute is not present,
1147   * KSES will remove all attributes from the tag. As KSES doesn't match opening and
1148   * closing tags, it's not possible to safely remove the tag itself, the safest
1149   * fallback is to strip all attributes from the tag, instead.
1150   *
1151   * @since 1.0.0
1152   * @since 5.9.0 Added support for an array of allowed values for attributes.
1153   *              Added support for required attributes.
1154   *
1155   * @param string         $element           HTML element/tag.
1156   * @param string         $attr              HTML attributes from HTML element to closing HTML element tag.
1157   * @param array[]|string $allowed_html      An array of allowed HTML elements and attributes,
1158   *                                          or a context name such as 'post'. See wp_kses_allowed_html()
1159   *                                          for the list of accepted context names.
1160   * @param string[]       $allowed_protocols Array of allowed URL protocols.
1161   * @return string Sanitized HTML element.
1162   */
1163  function wp_kses_attr( $element, $attr, $allowed_html, $allowed_protocols ) {
1164      if ( ! is_array( $allowed_html ) ) {
1165          $allowed_html = wp_kses_allowed_html( $allowed_html );
1166      }
1167  
1168      // Is there a closing XHTML slash at the end of the attributes?
1169      $xhtml_slash = '';
1170      if ( preg_match( '%\s*/\s*$%', $attr ) ) {
1171          $xhtml_slash = ' /';
1172      }
1173  
1174      // Are any attributes allowed at all for this element?
1175      $element_low = strtolower( $element );
1176      if ( empty( $allowed_html[ $element_low ] ) || true === $allowed_html[ $element_low ] ) {
1177          return "<$element$xhtml_slash>";
1178      }
1179  
1180      // Split it.
1181      $attrarr = wp_kses_hair( $attr, $allowed_protocols );
1182  
1183      // Check if there are attributes that are required.
1184      $required_attrs = array_filter(
1185          $allowed_html[ $element_low ],
1186          static function ( $required_attr_limits ) {
1187              return isset( $required_attr_limits['required'] ) && true === $required_attr_limits['required'];
1188          }
1189      );
1190  
1191      /*
1192       * If a required attribute check fails, we can return nothing for a self-closing tag,
1193       * but for a non-self-closing tag the best option is to return the element with attributes,
1194       * as KSES doesn't handle matching the relevant closing tag.
1195       */
1196      $stripped_tag = '';
1197      if ( empty( $xhtml_slash ) ) {
1198          $stripped_tag = "<$element>";
1199      }
1200  
1201      // Go through $attrarr, and save the allowed attributes for this element in $attr2.
1202      $attr2 = '';
1203      foreach ( $attrarr as $arreach ) {
1204          // Check if this attribute is required.
1205          $required = isset( $required_attrs[ strtolower( $arreach['name'] ) ] );
1206  
1207          if ( wp_kses_attr_check( $arreach['name'], $arreach['value'], $arreach['whole'], $arreach['vless'], $element, $allowed_html ) ) {
1208              $attr2 .= ' ' . $arreach['whole'];
1209  
1210              // If this was a required attribute, we can mark it as found.
1211              if ( $required ) {
1212                  unset( $required_attrs[ strtolower( $arreach['name'] ) ] );
1213              }
1214          } elseif ( $required ) {
1215              // This attribute was required, but didn't pass the check. The entire tag is not allowed.
1216              return $stripped_tag;
1217          }
1218      }
1219  
1220      // If some required attributes weren't set, the entire tag is not allowed.
1221      if ( ! empty( $required_attrs ) ) {
1222          return $stripped_tag;
1223      }
1224  
1225      // Remove any "<" or ">" characters.
1226      $attr2 = preg_replace( '/[<>]/', '', $attr2 );
1227  
1228      return "<$element$attr2$xhtml_slash>";
1229  }
1230  
1231  /**
1232   * Determines whether an attribute is allowed.
1233   *
1234   * @since 4.2.3
1235   * @since 5.0.0 Added support for `data-*` wildcard attributes.
1236   *
1237   * @param string $name         The attribute name. Passed by reference. Returns empty string when not allowed.
1238   * @param string $value        The attribute value. Passed by reference. Returns a filtered value.
1239   * @param string $whole        The `name=value` input. Passed by reference. Returns filtered input.
1240   * @param string $vless        Whether the attribute is valueless. Use 'y' or 'n'.
1241   * @param string $element      The name of the element to which this attribute belongs.
1242   * @param array  $allowed_html The full list of allowed elements and attributes.
1243   * @return bool Whether or not the attribute is allowed.
1244   */
1245  function wp_kses_attr_check( &$name, &$value, &$whole, $vless, $element, $allowed_html ) {
1246      $name_low    = strtolower( $name );
1247      $element_low = strtolower( $element );
1248  
1249      if ( ! isset( $allowed_html[ $element_low ] ) ) {
1250          $name  = '';
1251          $value = '';
1252          $whole = '';
1253          return false;
1254      }
1255  
1256      $allowed_attr = $allowed_html[ $element_low ];
1257  
1258      if ( ! isset( $allowed_attr[ $name_low ] ) || '' === $allowed_attr[ $name_low ] ) {
1259          /*
1260           * Allow `data-*` attributes.
1261           *
1262           * When specifying `$allowed_html`, the attribute name should be set as
1263           * `data-*` (not to be mixed with the HTML 4.0 `data` attribute, see
1264           * https://www.w3.org/TR/html40/struct/objects.html#adef-data).
1265           *
1266           * Note: the attribute name should only contain `A-Za-z0-9_-` chars,
1267           * double hyphens `--` are not accepted by WordPress.
1268           */
1269          if ( str_starts_with( $name_low, 'data-' ) && ! empty( $allowed_attr['data-*'] )
1270              && preg_match( '/^data(?:-[a-z0-9_]+)+$/', $name_low, $match )
1271          ) {
1272              /*
1273               * Add the whole attribute name to the allowed attributes and set any restrictions
1274               * for the `data-*` attribute values for the current element.
1275               */
1276              $allowed_attr[ $match[0] ] = $allowed_attr['data-*'];
1277          } else {
1278              $name  = '';
1279              $value = '';
1280              $whole = '';
1281              return false;
1282          }
1283      }
1284  
1285      if ( 'style' === $name_low ) {
1286          $new_value = safecss_filter_attr( $value );
1287  
1288          if ( empty( $new_value ) ) {
1289              $name  = '';
1290              $value = '';
1291              $whole = '';
1292              return false;
1293          }
1294  
1295          $whole = str_replace( $value, $new_value, $whole );
1296          $value = $new_value;
1297      }
1298  
1299      if ( is_array( $allowed_attr[ $name_low ] ) ) {
1300          // There are some checks.
1301          foreach ( $allowed_attr[ $name_low ] as $currkey => $currval ) {
1302              if ( ! wp_kses_check_attr_val( $value, $vless, $currkey, $currval ) ) {
1303                  $name  = '';
1304                  $value = '';
1305                  $whole = '';
1306                  return false;
1307              }
1308          }
1309      }
1310  
1311      return true;
1312  }
1313  
1314  /**
1315   * Builds an attribute list from string containing attributes.
1316   *
1317   * This function does a lot of work. It parses an attribute list into an array
1318   * with attribute data, and tries to do the right thing even if it gets weird
1319   * input. It will add quotes around attribute values that don't have any quotes
1320   * or apostrophes around them, to make it easier to produce HTML code that will
1321   * conform to W3C's HTML specification. It will also remove bad URL protocols
1322   * from attribute values. It also reduces duplicate attributes by using the
1323   * attribute defined first (`foo='bar' foo='baz'` will result in `foo='bar'`).
1324   *
1325   * @since 1.0.0
1326   *
1327   * @param string   $attr              Attribute list from HTML element to closing HTML element tag.
1328   * @param string[] $allowed_protocols Array of allowed URL protocols.
1329   * @return array[] Array of attribute information after parsing.
1330   */
1331  function wp_kses_hair( $attr, $allowed_protocols ) {
1332      $attrarr  = array();
1333      $mode     = 0;
1334      $attrname = '';
1335      $uris     = wp_kses_uri_attributes();
1336  
1337      // Loop through the whole attribute list.
1338  
1339      while ( strlen( $attr ) !== 0 ) {
1340          $working = 0; // Was the last operation successful?
1341  
1342          switch ( $mode ) {
1343              case 0:
1344                  if ( preg_match( '/^([_a-zA-Z][-_a-zA-Z0-9:.]*)/', $attr, $match ) ) {
1345                      $attrname = $match[1];
1346                      $working  = 1;
1347                      $mode     = 1;
1348                      $attr     = preg_replace( '/^[_a-zA-Z][-_a-zA-Z0-9:.]*/', '', $attr );
1349                  }
1350  
1351                  break;
1352  
1353              case 1:
1354                  if ( preg_match( '/^\s*=\s*/', $attr ) ) { // Equals sign.
1355                      $working = 1;
1356                      $mode    = 2;
1357                      $attr    = preg_replace( '/^\s*=\s*/', '', $attr );
1358                      break;
1359                  }
1360  
1361                  if ( preg_match( '/^\s+/', $attr ) ) { // Valueless.
1362                      $working = 1;
1363                      $mode    = 0;
1364  
1365                      if ( false === array_key_exists( $attrname, $attrarr ) ) {
1366                          $attrarr[ $attrname ] = array(
1367                              'name'  => $attrname,
1368                              'value' => '',
1369                              'whole' => $attrname,
1370                              'vless' => 'y',
1371                          );
1372                      }
1373  
1374                      $attr = preg_replace( '/^\s+/', '', $attr );
1375                  }
1376  
1377                  break;
1378  
1379              case 2:
1380                  if ( preg_match( '%^"([^"]*)"(\s+|/?$)%', $attr, $match ) ) {
1381                      // "value"
1382                      $thisval = $match[1];
1383                      if ( in_array( strtolower( $attrname ), $uris, true ) ) {
1384                          $thisval = wp_kses_bad_protocol( $thisval, $allowed_protocols );
1385                      }
1386  
1387                      if ( false === array_key_exists( $attrname, $attrarr ) ) {
1388                          $attrarr[ $attrname ] = array(
1389                              'name'  => $attrname,
1390                              'value' => $thisval,
1391                              'whole' => "$attrname=\"$thisval\"",
1392                              'vless' => 'n',
1393                          );
1394                      }
1395  
1396                      $working = 1;
1397                      $mode    = 0;
1398                      $attr    = preg_replace( '/^"[^"]*"(\s+|$)/', '', $attr );
1399                      break;
1400                  }
1401  
1402                  if ( preg_match( "%^'([^']*)'(\s+|/?$)%", $attr, $match ) ) {
1403                      // 'value'
1404                      $thisval = $match[1];
1405                      if ( in_array( strtolower( $attrname ), $uris, true ) ) {
1406                          $thisval = wp_kses_bad_protocol( $thisval, $allowed_protocols );
1407                      }
1408  
1409                      if ( false === array_key_exists( $attrname, $attrarr ) ) {
1410                          $attrarr[ $attrname ] = array(
1411                              'name'  => $attrname,
1412                              'value' => $thisval,
1413                              'whole' => "$attrname='$thisval'",
1414                              'vless' => 'n',
1415                          );
1416                      }
1417  
1418                      $working = 1;
1419                      $mode    = 0;
1420                      $attr    = preg_replace( "/^'[^']*'(\s+|$)/", '', $attr );
1421                      break;
1422                  }
1423  
1424                  if ( preg_match( "%^([^\s\"']+)(\s+|/?$)%", $attr, $match ) ) {
1425                      // value
1426                      $thisval = $match[1];
1427                      if ( in_array( strtolower( $attrname ), $uris, true ) ) {
1428                          $thisval = wp_kses_bad_protocol( $thisval, $allowed_protocols );
1429                      }
1430  
1431                      if ( false === array_key_exists( $attrname, $attrarr ) ) {
1432                          $attrarr[ $attrname ] = array(
1433                              'name'  => $attrname,
1434                              'value' => $thisval,
1435                              'whole' => "$attrname=\"$thisval\"",
1436                              'vless' => 'n',
1437                          );
1438                      }
1439  
1440                      // We add quotes to conform to W3C's HTML spec.
1441                      $working = 1;
1442                      $mode    = 0;
1443                      $attr    = preg_replace( "%^[^\s\"']+(\s+|$)%", '', $attr );
1444                  }
1445  
1446                  break;
1447          } // End switch.
1448  
1449          if ( 0 === $working ) { // Not well-formed, remove and try again.
1450              $attr = wp_kses_html_error( $attr );
1451              $mode = 0;
1452          }
1453      } // End while.
1454  
1455      if ( 1 === $mode && false === array_key_exists( $attrname, $attrarr ) ) {
1456          /*
1457           * Special case, for when the attribute list ends with a valueless
1458           * attribute like "selected".
1459           */
1460          $attrarr[ $attrname ] = array(
1461              'name'  => $attrname,
1462              'value' => '',
1463              'whole' => $attrname,
1464              'vless' => 'y',
1465          );
1466      }
1467  
1468      return $attrarr;
1469  }
1470  
1471  /**
1472   * Finds all attributes of an HTML element.
1473   *
1474   * Does not modify input.  May return "evil" output.
1475   *
1476   * Based on `wp_kses_split2()` and `wp_kses_attr()`.
1477   *
1478   * @since 4.2.3
1479   *
1480   * @param string $element HTML element.
1481   * @return array|false List of attributes found in the element. Returns false on failure.
1482   */
1483  function wp_kses_attr_parse( $element ) {
1484      $valid = preg_match( '%^(<\s*)(/\s*)?([a-zA-Z0-9]+\s*)([^>]*)(>?)$%', $element, $matches );
1485      if ( 1 !== $valid ) {
1486          return false;
1487      }
1488  
1489      $begin  = $matches[1];
1490      $slash  = $matches[2];
1491      $elname = $matches[3];
1492      $attr   = $matches[4];
1493      $end    = $matches[5];
1494  
1495      if ( '' !== $slash ) {
1496          // Closing elements do not get parsed.
1497          return false;
1498      }
1499  
1500      // Is there a closing XHTML slash at the end of the attributes?
1501      if ( 1 === preg_match( '%\s*/\s*$%', $attr, $matches ) ) {
1502          $xhtml_slash = $matches[0];
1503          $attr        = substr( $attr, 0, -strlen( $xhtml_slash ) );
1504      } else {
1505          $xhtml_slash = '';
1506      }
1507  
1508      // Split it.
1509      $attrarr = wp_kses_hair_parse( $attr );
1510      if ( false === $attrarr ) {
1511          return false;
1512      }
1513  
1514      // Make sure all input is returned by adding front and back matter.
1515      array_unshift( $attrarr, $begin . $slash . $elname );
1516      array_push( $attrarr, $xhtml_slash . $end );
1517  
1518      return $attrarr;
1519  }
1520  
1521  /**
1522   * Builds an attribute list from string containing attributes.
1523   *
1524   * Does not modify input.  May return "evil" output.
1525   * In case of unexpected input, returns false instead of stripping things.
1526   *
1527   * Based on `wp_kses_hair()` but does not return a multi-dimensional array.
1528   *
1529   * @since 4.2.3
1530   *
1531   * @param string $attr Attribute list from HTML element to closing HTML element tag.
1532   * @return array|false List of attributes found in $attr. Returns false on failure.
1533   */
1534  function wp_kses_hair_parse( $attr ) {
1535      if ( '' === $attr ) {
1536          return array();
1537      }
1538  
1539      $regex =
1540          '(?:
1541                  [_a-zA-Z][-_a-zA-Z0-9:.]* # Attribute name.
1542              |
1543                  \[\[?[^\[\]]+\]\]?        # Shortcode in the name position implies unfiltered_html.
1544          )
1545          (?:                               # Attribute value.
1546              \s*=\s*                       # All values begin with "=".
1547              (?:
1548                  "[^"]*"                   # Double-quoted.
1549              |
1550                  \'[^\']*\'                # Single-quoted.
1551              |
1552                  [^\s"\']+                 # Non-quoted.
1553                  (?:\s|$)                  # Must have a space.
1554              )
1555          |
1556              (?:\s|$)                      # If attribute has no value, space is required.
1557          )
1558          \s*                               # Trailing space is optional except as mentioned above.
1559          ';
1560  
1561      /*
1562       * Although it is possible to reduce this procedure to a single regexp,
1563       * we must run that regexp twice to get exactly the expected result.
1564       *
1565       * Note: do NOT remove the `x` modifiers as they are essential for the above regex!
1566       */
1567  
1568      $validation = "/^($regex)+$/x";
1569      $extraction = "/$regex/x";
1570  
1571      if ( 1 === preg_match( $validation, $attr ) ) {
1572          preg_match_all( $extraction, $attr, $attrarr );
1573          return $attrarr[0];
1574      } else {
1575          return false;
1576      }
1577  }
1578  
1579  /**
1580   * Performs different checks for attribute values.
1581   *
1582   * The currently implemented checks are "maxlen", "minlen", "maxval", "minval",
1583   * and "valueless".
1584   *
1585   * @since 1.0.0
1586   *
1587   * @param string $value      Attribute value.
1588   * @param string $vless      Whether the attribute is valueless. Use 'y' or 'n'.
1589   * @param string $checkname  What $checkvalue is checking for.
1590   * @param mixed  $checkvalue What constraint the value should pass.
1591   * @return bool Whether check passes.
1592   */
1593  function wp_kses_check_attr_val( $value, $vless, $checkname, $checkvalue ) {
1594      $ok = true;
1595  
1596      switch ( strtolower( $checkname ) ) {
1597          case 'maxlen':
1598              /*
1599               * The maxlen check makes sure that the attribute value has a length not
1600               * greater than the given value. This can be used to avoid Buffer Overflows
1601               * in WWW clients and various Internet servers.
1602               */
1603  
1604              if ( strlen( $value ) > $checkvalue ) {
1605                  $ok = false;
1606              }
1607              break;
1608  
1609          case 'minlen':
1610              /*
1611               * The minlen check makes sure that the attribute value has a length not
1612               * smaller than the given value.
1613               */
1614  
1615              if ( strlen( $value ) < $checkvalue ) {
1616                  $ok = false;
1617              }
1618              break;
1619  
1620          case 'maxval':
1621              /*
1622               * The maxval check does two things: it checks that the attribute value is
1623               * an integer from 0 and up, without an excessive amount of zeroes or
1624               * whitespace (to avoid Buffer Overflows). It also checks that the attribute
1625               * value is not greater than the given value.
1626               * This check can be used to avoid Denial of Service attacks.
1627               */
1628  
1629              if ( ! preg_match( '/^\s{0,6}[0-9]{1,6}\s{0,6}$/', $value ) ) {
1630                  $ok = false;
1631              }
1632              if ( $value > $checkvalue ) {
1633                  $ok = false;
1634              }
1635              break;
1636  
1637          case 'minval':
1638              /*
1639               * The minval check makes sure that the attribute value is a positive integer,
1640               * and that it is not smaller than the given value.
1641               */
1642  
1643              if ( ! preg_match( '/^\s{0,6}[0-9]{1,6}\s{0,6}$/', $value ) ) {
1644                  $ok = false;
1645              }
1646              if ( $value < $checkvalue ) {
1647                  $ok = false;
1648              }
1649              break;
1650  
1651          case 'valueless':
1652              /*
1653               * The valueless check makes sure if the attribute has a value
1654               * (like `<a href="blah">`) or not (`<option selected>`). If the given value
1655               * is a "y" or a "Y", the attribute must not have a value.
1656               * If the given value is an "n" or an "N", the attribute must have a value.
1657               */
1658  
1659              if ( strtolower( $checkvalue ) !== $vless ) {
1660                  $ok = false;
1661              }
1662              break;
1663  
1664          case 'values':
1665              /*
1666               * The values check is used when you want to make sure that the attribute
1667               * has one of the given values.
1668               */
1669  
1670              if ( false === array_search( strtolower( $value ), $checkvalue, true ) ) {
1671                  $ok = false;
1672              }
1673              break;
1674  
1675          case 'value_callback':
1676              /*
1677               * The value_callback check is used when you want to make sure that the attribute
1678               * value is accepted by the callback function.
1679               */
1680  
1681              if ( ! call_user_func( $checkvalue, $value ) ) {
1682                  $ok = false;
1683              }
1684              break;
1685      } // End switch.
1686  
1687      return $ok;
1688  }
1689  
1690  /**
1691   * Sanitizes a string and removed disallowed URL protocols.
1692   *
1693   * This function removes all non-allowed protocols from the beginning of the
1694   * string. It ignores whitespace and the case of the letters, and it does
1695   * understand HTML entities. It does its work recursively, so it won't be
1696   * fooled by a string like `javascript:javascript:alert(57)`.
1697   *
1698   * @since 1.0.0
1699   *
1700   * @param string   $content           Content to filter bad protocols from.
1701   * @param string[] $allowed_protocols Array of allowed URL protocols.
1702   * @return string Filtered content.
1703   */
1704  function wp_kses_bad_protocol( $content, $allowed_protocols ) {
1705      $content = wp_kses_no_null( $content );
1706  
1707      // Short-circuit if the string starts with `https://` or `http://`. Most common cases.
1708      if (
1709          ( str_starts_with( $content, 'https://' ) && in_array( 'https', $allowed_protocols, true ) ) ||
1710          ( str_starts_with( $content, 'http://' ) && in_array( 'http', $allowed_protocols, true ) )
1711      ) {
1712          return $content;
1713      }
1714  
1715      $iterations = 0;
1716  
1717      do {
1718          $original_content = $content;
1719          $content          = wp_kses_bad_protocol_once( $content, $allowed_protocols );
1720      } while ( $original_content !== $content && ++$iterations < 6 );
1721  
1722      if ( $original_content !== $content ) {
1723          return '';
1724      }
1725  
1726      return $content;
1727  }
1728  
1729  /**
1730   * Removes any invalid control characters in a text string.
1731   *
1732   * Also removes any instance of the `\0` string.
1733   *
1734   * @since 1.0.0
1735   *
1736   * @param string $content Content to filter null characters from.
1737   * @param array  $options Set 'slash_zero' => 'keep' when '\0' is allowed. Default is 'remove'.
1738   * @return string Filtered content.
1739   */
1740  function wp_kses_no_null( $content, $options = null ) {
1741      if ( ! isset( $options['slash_zero'] ) ) {
1742          $options = array( 'slash_zero' => 'remove' );
1743      }
1744  
1745      $content = preg_replace( '/[\x00-\x08\x0B\x0C\x0E-\x1F]/', '', $content );
1746      if ( 'remove' === $options['slash_zero'] ) {
1747          $content = preg_replace( '/\\\\+0+/', '', $content );
1748      }
1749  
1750      return $content;
1751  }
1752  
1753  /**
1754   * Strips slashes from in front of quotes.
1755   *
1756   * This function changes the character sequence `\"` to just `"`. It leaves all other
1757   * slashes alone. The quoting from `preg_replace(//e)` requires this.
1758   *
1759   * @since 1.0.0
1760   *
1761   * @param string $content String to strip slashes from.
1762   * @return string Fixed string with quoted slashes.
1763   */
1764  function wp_kses_stripslashes( $content ) {
1765      return preg_replace( '%\\\\"%', '"', $content );
1766  }
1767  
1768  /**
1769   * Converts the keys of an array to lowercase.
1770   *
1771   * @since 1.0.0
1772   *
1773   * @param array $inarray Unfiltered array.
1774   * @return array Fixed array with all lowercase keys.
1775   */
1776  function wp_kses_array_lc( $inarray ) {
1777      $outarray = array();
1778  
1779      foreach ( (array) $inarray as $inkey => $inval ) {
1780          $outkey              = strtolower( $inkey );
1781          $outarray[ $outkey ] = array();
1782  
1783          foreach ( (array) $inval as $inkey2 => $inval2 ) {
1784              $outkey2                         = strtolower( $inkey2 );
1785              $outarray[ $outkey ][ $outkey2 ] = $inval2;
1786          }
1787      }
1788  
1789      return $outarray;
1790  }
1791  
1792  /**
1793   * Handles parsing errors in `wp_kses_hair()`.
1794   *
1795   * The general plan is to remove everything to and including some whitespace,
1796   * but it deals with quotes and apostrophes as well.
1797   *
1798   * @since 1.0.0
1799   *
1800   * @param string $attr
1801   * @return string
1802   */
1803  function wp_kses_html_error( $attr ) {
1804      return preg_replace( '/^("[^"]*("|$)|\'[^\']*(\'|$)|\S)*\s*/', '', $attr );
1805  }
1806  
1807  /**
1808   * Sanitizes content from bad protocols and other characters.
1809   *
1810   * This function searches for URL protocols at the beginning of the string, while
1811   * handling whitespace and HTML entities.
1812   *
1813   * @since 1.0.0
1814   *
1815   * @param string   $content           Content to check for bad protocols.
1816   * @param string[] $allowed_protocols Array of allowed URL protocols.
1817   * @param int      $count             Depth of call recursion to this function.
1818   * @return string Sanitized content.
1819   */
1820  function wp_kses_bad_protocol_once( $content, $allowed_protocols, $count = 1 ) {
1821      $content  = preg_replace( '/(&#0*58(?![;0-9])|&#x0*3a(?![;a-f0-9]))/i', '$1;', $content );
1822      $content2 = preg_split( '/:|&#0*58;|&#x0*3a;|&colon;/i', $content, 2 );
1823  
1824      if ( isset( $content2[1] ) && ! preg_match( '%/\?%', $content2[0] ) ) {
1825          $content  = trim( $content2[1] );
1826          $protocol = wp_kses_bad_protocol_once2( $content2[0], $allowed_protocols );
1827          if ( 'feed:' === $protocol ) {
1828              if ( $count > 2 ) {
1829                  return '';
1830              }
1831              $content = wp_kses_bad_protocol_once( $content, $allowed_protocols, ++$count );
1832              if ( empty( $content ) ) {
1833                  return $content;
1834              }
1835          }
1836          $content = $protocol . $content;
1837      }
1838  
1839      return $content;
1840  }
1841  
1842  /**
1843   * Callback for `wp_kses_bad_protocol_once()` regular expression.
1844   *
1845   * This function processes URL protocols, checks to see if they're in the
1846   * list of allowed protocols or not, and returns different data depending
1847   * on the answer.
1848   *
1849   * @access private
1850   * @ignore
1851   * @since 1.0.0
1852   *
1853   * @param string   $scheme            URI scheme to check against the list of allowed protocols.
1854   * @param string[] $allowed_protocols Array of allowed URL protocols.
1855   * @return string Sanitized content.
1856   */
1857  function wp_kses_bad_protocol_once2( $scheme, $allowed_protocols ) {
1858      $scheme = wp_kses_decode_entities( $scheme );
1859      $scheme = preg_replace( '/\s/', '', $scheme );
1860      $scheme = wp_kses_no_null( $scheme );
1861      $scheme = strtolower( $scheme );
1862  
1863      $allowed = false;
1864      foreach ( (array) $allowed_protocols as $one_protocol ) {
1865          if ( strtolower( $one_protocol ) === $scheme ) {
1866              $allowed = true;
1867              break;
1868          }
1869      }
1870  
1871      if ( $allowed ) {
1872          return "$scheme:";
1873      } else {
1874          return '';
1875      }
1876  }
1877  
1878  /**
1879   * Converts and fixes HTML entities.
1880   *
1881   * This function normalizes HTML entities. It will convert `AT&T` to the correct
1882   * `AT&amp;T`, `&#00058;` to `&#058;`, `&#XYZZY;` to `&amp;#XYZZY;` and so on.
1883   *
1884   * When `$context` is set to 'xml', HTML entities are converted to their code points.  For
1885   * example, `AT&T&hellip;&#XYZZY;` is converted to `AT&amp;T…&amp;#XYZZY;`.
1886   *
1887   * @since 1.0.0
1888   * @since 5.5.0 Added `$context` parameter.
1889   *
1890   * @param string $content Content to normalize entities.
1891   * @param string $context Context for normalization. Can be either 'html' or 'xml'.
1892   *                        Default 'html'.
1893   * @return string Content with normalized entities.
1894   */
1895  function wp_kses_normalize_entities( $content, $context = 'html' ) {
1896      // Disarm all entities by converting & to &amp;
1897      $content = str_replace( '&', '&amp;', $content );
1898  
1899      // Change back the allowed entities in our list of allowed entities.
1900      if ( 'xml' === $context ) {
1901          $content = preg_replace_callback( '/&amp;([A-Za-z]{2,8}[0-9]{0,2});/', 'wp_kses_xml_named_entities', $content );
1902      } else {
1903          $content = preg_replace_callback( '/&amp;([A-Za-z]{2,8}[0-9]{0,2});/', 'wp_kses_named_entities', $content );
1904      }
1905      $content = preg_replace_callback( '/&amp;#(0*[0-9]{1,7});/', 'wp_kses_normalize_entities2', $content );
1906      $content = preg_replace_callback( '/&amp;#[Xx](0*[0-9A-Fa-f]{1,6});/', 'wp_kses_normalize_entities3', $content );
1907  
1908      return $content;
1909  }
1910  
1911  /**
1912   * Callback for `wp_kses_normalize_entities()` regular expression.
1913   *
1914   * This function only accepts valid named entity references, which are finite,
1915   * case-sensitive, and highly scrutinized by HTML and XML validators.
1916   *
1917   * @since 3.0.0
1918   *
1919   * @global array $allowedentitynames
1920   *
1921   * @param array $matches preg_replace_callback() matches array.
1922   * @return string Correctly encoded entity.
1923   */
1924  function wp_kses_named_entities( $matches ) {
1925      global $allowedentitynames;
1926  
1927      if ( empty( $matches[1] ) ) {
1928          return '';
1929      }
1930  
1931      $i = $matches[1];
1932      return ( ! in_array( $i, $allowedentitynames, true ) ) ? "&amp;$i;" : "&$i;";
1933  }
1934  
1935  /**
1936   * Callback for `wp_kses_normalize_entities()` regular expression.
1937   *
1938   * This function only accepts valid named entity references, which are finite,
1939   * case-sensitive, and highly scrutinized by XML validators.  HTML named entity
1940   * references are converted to their code points.
1941   *
1942   * @since 5.5.0
1943   *
1944   * @global array $allowedentitynames
1945   * @global array $allowedxmlentitynames
1946   *
1947   * @param array $matches preg_replace_callback() matches array.
1948   * @return string Correctly encoded entity.
1949   */
1950  function wp_kses_xml_named_entities( $matches ) {
1951      global $allowedentitynames, $allowedxmlentitynames;
1952  
1953      if ( empty( $matches[1] ) ) {
1954          return '';
1955      }
1956  
1957      $i = $matches[1];
1958  
1959      if ( in_array( $i, $allowedxmlentitynames, true ) ) {
1960          return "&$i;";
1961      } elseif ( in_array( $i, $allowedentitynames, true ) ) {
1962          return html_entity_decode( "&$i;", ENT_HTML5 );
1963      }
1964  
1965      return "&amp;$i;";
1966  }
1967  
1968  /**
1969   * Callback for `wp_kses_normalize_entities()` regular expression.
1970   *
1971   * This function helps `wp_kses_normalize_entities()` to only accept 16-bit
1972   * values and nothing more for `&#number;` entities.
1973   *
1974   * @access private
1975   * @ignore
1976   * @since 1.0.0
1977   *
1978   * @param array $matches `preg_replace_callback()` matches array.
1979   * @return string Correctly encoded entity.
1980   */
1981  function wp_kses_normalize_entities2( $matches ) {
1982      if ( empty( $matches[1] ) ) {
1983          return '';
1984      }
1985  
1986      $i = $matches[1];
1987  
1988      if ( valid_unicode( $i ) ) {
1989          $i = str_pad( ltrim( $i, '0' ), 3, '0', STR_PAD_LEFT );
1990          $i = "&#$i;";
1991      } else {
1992          $i = "&amp;#$i;";
1993      }
1994  
1995      return $i;
1996  }
1997  
1998  /**
1999   * Callback for `wp_kses_normalize_entities()` for regular expression.
2000   *
2001   * This function helps `wp_kses_normalize_entities()` to only accept valid Unicode
2002   * numeric entities in hex form.
2003   *
2004   * @since 2.7.0
2005   * @access private
2006   * @ignore
2007   *
2008   * @param array $matches `preg_replace_callback()` matches array.
2009   * @return string Correctly encoded entity.
2010   */
2011  function wp_kses_normalize_entities3( $matches ) {
2012      if ( empty( $matches[1] ) ) {
2013          return '';
2014      }
2015  
2016      $hexchars = $matches[1];
2017  
2018      return ( ! valid_unicode( hexdec( $hexchars ) ) ) ? "&amp;#x$hexchars;" : '&#x' . ltrim( $hexchars, '0' ) . ';';
2019  }
2020  
2021  /**
2022   * Determines if a Unicode codepoint is valid.
2023   *
2024   * @since 2.7.0
2025   *
2026   * @param int $i Unicode codepoint.
2027   * @return bool Whether or not the codepoint is a valid Unicode codepoint.
2028   */
2029  function valid_unicode( $i ) {
2030      $i = (int) $i;
2031  
2032      return ( 0x9 === $i || 0xa === $i || 0xd === $i ||
2033          ( 0x20 <= $i && $i <= 0xd7ff ) ||
2034          ( 0xe000 <= $i && $i <= 0xfffd ) ||
2035          ( 0x10000 <= $i && $i <= 0x10ffff )
2036      );
2037  }
2038  
2039  /**
2040   * Converts all numeric HTML entities to their named counterparts.
2041   *
2042   * This function decodes numeric HTML entities (`&#65;` and `&#x41;`).
2043   * It doesn't do anything with named entities like `&auml;`, but we don't
2044   * need them in the allowed URL protocols system anyway.
2045   *
2046   * @since 1.0.0
2047   *
2048   * @param string $content Content to change entities.
2049   * @return string Content after decoded entities.
2050   */
2051  function wp_kses_decode_entities( $content ) {
2052      $content = preg_replace_callback( '/&#([0-9]+);/', '_wp_kses_decode_entities_chr', $content );
2053      $content = preg_replace_callback( '/&#[Xx]([0-9A-Fa-f]+);/', '_wp_kses_decode_entities_chr_hexdec', $content );
2054  
2055      return $content;
2056  }
2057  
2058  /**
2059   * Regex callback for `wp_kses_decode_entities()`.
2060   *
2061   * @since 2.9.0
2062   * @access private
2063   * @ignore
2064   *
2065   * @param array $matches preg match
2066   * @return string
2067   */
2068  function _wp_kses_decode_entities_chr( $matches ) {
2069      return chr( $matches[1] );
2070  }
2071  
2072  /**
2073   * Regex callback for `wp_kses_decode_entities()`.
2074   *
2075   * @since 2.9.0
2076   * @access private
2077   * @ignore
2078   *
2079   * @param array $matches preg match
2080   * @return string
2081   */
2082  function _wp_kses_decode_entities_chr_hexdec( $matches ) {
2083      return chr( hexdec( $matches[1] ) );
2084  }
2085  
2086  /**
2087   * Sanitize content with allowed HTML KSES rules.
2088   *
2089   * This function expects slashed data.
2090   *
2091   * @since 1.0.0
2092   *
2093   * @param string $data Content to filter, expected to be escaped with slashes.
2094   * @return string Filtered content.
2095   */
2096  function wp_filter_kses( $data ) {
2097      return addslashes( wp_kses( stripslashes( $data ), current_filter() ) );
2098  }
2099  
2100  /**
2101   * Sanitize content with allowed HTML KSES rules.
2102   *
2103   * This function expects unslashed data.
2104   *
2105   * @since 2.9.0
2106   *
2107   * @param string $data Content to filter, expected to not be escaped.
2108   * @return string Filtered content.
2109   */
2110  function wp_kses_data( $data ) {
2111      return wp_kses( $data, current_filter() );
2112  }
2113  
2114  /**
2115   * Sanitizes content for allowed HTML tags for post content.
2116   *
2117   * Post content refers to the page contents of the 'post' type and not `$_POST`
2118   * data from forms.
2119   *
2120   * This function expects slashed data.
2121   *
2122   * @since 2.0.0
2123   *
2124   * @param string $data Post content to filter, expected to be escaped with slashes.
2125   * @return string Filtered post content with allowed HTML tags and attributes intact.
2126   */
2127  function wp_filter_post_kses( $data ) {
2128      return addslashes( wp_kses( stripslashes( $data ), 'post' ) );
2129  }
2130  
2131  /**
2132   * Sanitizes global styles user content removing unsafe rules.
2133   *
2134   * @since 5.9.0
2135   *
2136   * @param string $data Post content to filter.
2137   * @return string Filtered post content with unsafe rules removed.
2138   */
2139  function wp_filter_global_styles_post( $data ) {
2140      $decoded_data        = json_decode( wp_unslash( $data ), true );
2141      $json_decoding_error = json_last_error();
2142      if (
2143          JSON_ERROR_NONE === $json_decoding_error &&
2144          is_array( $decoded_data ) &&
2145          isset( $decoded_data['isGlobalStylesUserThemeJSON'] ) &&
2146          $decoded_data['isGlobalStylesUserThemeJSON']
2147      ) {
2148          unset( $decoded_data['isGlobalStylesUserThemeJSON'] );
2149  
2150          $data_to_encode = WP_Theme_JSON::remove_insecure_properties( $decoded_data );
2151  
2152          $data_to_encode['isGlobalStylesUserThemeJSON'] = true;
2153          return wp_slash( wp_json_encode( $data_to_encode ) );
2154      }
2155      return $data;
2156  }
2157  
2158  /**
2159   * Sanitizes content for allowed HTML tags for post content.
2160   *
2161   * Post content refers to the page contents of the 'post' type and not `$_POST`
2162   * data from forms.
2163   *
2164   * This function expects unslashed data.
2165   *
2166   * @since 2.9.0
2167   *
2168   * @param string $data Post content to filter.
2169   * @return string Filtered post content with allowed HTML tags and attributes intact.
2170   */
2171  function wp_kses_post( $data ) {
2172      return wp_kses( $data, 'post' );
2173  }
2174  
2175  /**
2176   * Navigates through an array, object, or scalar, and sanitizes content for
2177   * allowed HTML tags for post content.
2178   *
2179   * @since 4.4.2
2180   *
2181   * @see map_deep()
2182   *
2183   * @param mixed $data The array, object, or scalar value to inspect.
2184   * @return mixed The filtered content.
2185   */
2186  function wp_kses_post_deep( $data ) {
2187      return map_deep( $data, 'wp_kses_post' );
2188  }
2189  
2190  /**
2191   * Strips all HTML from a text string.
2192   *
2193   * This function expects slashed data.
2194   *
2195   * @since 2.1.0
2196   *
2197   * @param string $data Content to strip all HTML from.
2198   * @return string Filtered content without any HTML.
2199   */
2200  function wp_filter_nohtml_kses( $data ) {
2201      return addslashes( wp_kses( stripslashes( $data ), 'strip' ) );
2202  }
2203  
2204  /**
2205   * Adds all KSES input form content filters.
2206   *
2207   * All hooks have default priority. The `wp_filter_kses()` function is added to
2208   * the 'pre_comment_content' and 'title_save_pre' hooks.
2209   *
2210   * The `wp_filter_post_kses()` function is added to the 'content_save_pre',
2211   * 'excerpt_save_pre', and 'content_filtered_save_pre' hooks.
2212   *
2213   * @since 2.0.0
2214   */
2215  function kses_init_filters() {
2216      // Normal filtering.
2217      add_filter( 'title_save_pre', 'wp_filter_kses' );
2218  
2219      // Comment filtering.
2220      if ( current_user_can( 'unfiltered_html' ) ) {
2221          add_filter( 'pre_comment_content', 'wp_filter_post_kses' );
2222      } else {
2223          add_filter( 'pre_comment_content', 'wp_filter_kses' );
2224      }
2225  
2226      // Global Styles filtering: Global Styles filters should be executed before normal post_kses HTML filters.
2227      add_filter( 'content_save_pre', 'wp_filter_global_styles_post', 9 );
2228      add_filter( 'content_filtered_save_pre', 'wp_filter_global_styles_post', 9 );
2229  
2230      // Post filtering.
2231      add_filter( 'content_save_pre', 'wp_filter_post_kses' );
2232      add_filter( 'excerpt_save_pre', 'wp_filter_post_kses' );
2233      add_filter( 'content_filtered_save_pre', 'wp_filter_post_kses' );
2234  }
2235  
2236  /**
2237   * Removes all KSES input form content filters.
2238   *
2239   * A quick procedural method to removing all of the filters that KSES uses for
2240   * content in WordPress Loop.
2241   *
2242   * Does not remove the `kses_init()` function from {@see 'init'} hook (priority is
2243   * default). Also does not remove `kses_init()` function from {@see 'set_current_user'}
2244   * hook (priority is also default).
2245   *
2246   * @since 2.0.6
2247   */
2248  function kses_remove_filters() {
2249      // Normal filtering.
2250      remove_filter( 'title_save_pre', 'wp_filter_kses' );
2251  
2252      // Comment filtering.
2253      remove_filter( 'pre_comment_content', 'wp_filter_post_kses' );
2254      remove_filter( 'pre_comment_content', 'wp_filter_kses' );
2255  
2256      // Global Styles filtering.
2257      remove_filter( 'content_save_pre', 'wp_filter_global_styles_post', 9 );
2258      remove_filter( 'content_filtered_save_pre', 'wp_filter_global_styles_post', 9 );
2259  
2260      // Post filtering.
2261      remove_filter( 'content_save_pre', 'wp_filter_post_kses' );
2262      remove_filter( 'excerpt_save_pre', 'wp_filter_post_kses' );
2263      remove_filter( 'content_filtered_save_pre', 'wp_filter_post_kses' );
2264  }
2265  
2266  /**
2267   * Sets up most of the KSES filters for input form content.
2268   *
2269   * First removes all of the KSES filters in case the current user does not need
2270   * to have KSES filter the content. If the user does not have `unfiltered_html`
2271   * capability, then KSES filters are added.
2272   *
2273   * @since 2.0.0
2274   */
2275  function kses_init() {
2276      kses_remove_filters();
2277  
2278      if ( ! current_user_can( 'unfiltered_html' ) ) {
2279          kses_init_filters();
2280      }
2281  }
2282  
2283  /**
2284   * Filters an inline style attribute and removes disallowed rules.
2285   *
2286   * @since 2.8.1
2287   * @since 4.4.0 Added support for `min-height`, `max-height`, `min-width`, and `max-width`.
2288   * @since 4.6.0 Added support for `list-style-type`.
2289   * @since 5.0.0 Added support for `background-image`.
2290   * @since 5.1.0 Added support for `text-transform`.
2291   * @since 5.2.0 Added support for `background-position` and `grid-template-columns`.
2292   * @since 5.3.0 Added support for `grid`, `flex` and `column` layout properties.
2293   *              Extended `background-*` support for individual properties.
2294   * @since 5.3.1 Added support for gradient backgrounds.
2295   * @since 5.7.1 Added support for `object-position`.
2296   * @since 5.8.0 Added support for `calc()` and `var()` values.
2297   * @since 6.1.0 Added support for `min()`, `max()`, `minmax()`, `clamp()`,
2298   *              nested `var()` values, and assigning values to CSS variables.
2299   *              Added support for `object-fit`, `gap`, `column-gap`, `row-gap`, and `flex-wrap`.
2300   *              Extended `margin-*` and `padding-*` support for logical properties.
2301   * @since 6.2.0 Added support for `aspect-ratio`, `position`, `top`, `right`, `bottom`, `left`,
2302   *              and `z-index` CSS properties.
2303   * @since 6.3.0 Extended support for `filter` to accept a URL and added support for repeat().
2304   *              Added support for `box-shadow`.
2305   * @since 6.4.0 Added support for `writing-mode`.
2306   * @since 6.5.0 Added support for `background-repeat`.
2307   *
2308   * @param string $css        A string of CSS rules.
2309   * @param string $deprecated Not used.
2310   * @return string Filtered string of CSS rules.
2311   */
2312  function safecss_filter_attr( $css, $deprecated = '' ) {
2313      if ( ! empty( $deprecated ) ) {
2314          _deprecated_argument( __FUNCTION__, '2.8.1' ); // Never implemented.
2315      }
2316  
2317      $css = wp_kses_no_null( $css );
2318      $css = str_replace( array( "\n", "\r", "\t" ), '', $css );
2319  
2320      $allowed_protocols = wp_allowed_protocols();
2321  
2322      $css_array = explode( ';', trim( $css ) );
2323  
2324      /**
2325       * Filters the list of allowed CSS attributes.
2326       *
2327       * @since 2.8.1
2328       *
2329       * @param string[] $attr Array of allowed CSS attributes.
2330       */
2331      $allowed_attr = apply_filters(
2332          'safe_style_css',
2333          array(
2334              'background',
2335              'background-color',
2336              'background-image',
2337              'background-position',
2338              'background-repeat',
2339              'background-size',
2340              'background-attachment',
2341              'background-blend-mode',
2342  
2343              'border',
2344              'border-radius',
2345              'border-width',
2346              'border-color',
2347              'border-style',
2348              'border-right',
2349              'border-right-color',
2350              'border-right-style',
2351              'border-right-width',
2352              'border-bottom',
2353              'border-bottom-color',
2354              'border-bottom-left-radius',
2355              'border-bottom-right-radius',
2356              'border-bottom-style',
2357              'border-bottom-width',
2358              'border-bottom-right-radius',
2359              'border-bottom-left-radius',
2360              'border-left',
2361              'border-left-color',
2362              'border-left-style',
2363              'border-left-width',
2364              'border-top',
2365              'border-top-color',
2366              'border-top-left-radius',
2367              'border-top-right-radius',
2368              'border-top-style',
2369              'border-top-width',
2370              'border-top-left-radius',
2371              'border-top-right-radius',
2372  
2373              'border-spacing',
2374              'border-collapse',
2375              'caption-side',
2376  
2377              'columns',
2378              'column-count',
2379              'column-fill',
2380              'column-gap',
2381              'column-rule',
2382              'column-span',
2383              'column-width',
2384  
2385              'color',
2386              'filter',
2387              'font',
2388              'font-family',
2389              'font-size',
2390              'font-style',
2391              'font-variant',
2392              'font-weight',
2393              'letter-spacing',
2394              'line-height',
2395              'text-align',
2396              'text-decoration',
2397              'text-indent',
2398              'text-transform',
2399  
2400              'height',
2401              'min-height',
2402              'max-height',
2403  
2404              'width',
2405              'min-width',
2406              'max-width',
2407  
2408              'margin',
2409              'margin-right',
2410              'margin-bottom',
2411              'margin-left',
2412              'margin-top',
2413              'margin-block-start',
2414              'margin-block-end',
2415              'margin-inline-start',
2416              'margin-inline-end',
2417  
2418              'padding',
2419              'padding-right',
2420              'padding-bottom',
2421              'padding-left',
2422              'padding-top',
2423              'padding-block-start',
2424              'padding-block-end',
2425              'padding-inline-start',
2426              'padding-inline-end',
2427  
2428              'flex',
2429              'flex-basis',
2430              'flex-direction',
2431              'flex-flow',
2432              'flex-grow',
2433              'flex-shrink',
2434              'flex-wrap',
2435  
2436              'gap',
2437              'column-gap',
2438              'row-gap',
2439  
2440              'grid-template-columns',
2441              'grid-auto-columns',
2442              'grid-column-start',
2443              'grid-column-end',
2444              'grid-column-gap',
2445              'grid-template-rows',
2446              'grid-auto-rows',
2447              'grid-row-start',
2448              'grid-row-end',
2449              'grid-row-gap',
2450              'grid-gap',
2451  
2452              'justify-content',
2453              'justify-items',
2454              'justify-self',
2455              'align-content',
2456              'align-items',
2457              'align-self',
2458  
2459              'clear',
2460              'cursor',
2461              'direction',
2462              'float',
2463              'list-style-type',
2464              'object-fit',
2465              'object-position',
2466              'overflow',
2467              'vertical-align',
2468              'writing-mode',
2469  
2470              'position',
2471              'top',
2472              'right',
2473              'bottom',
2474              'left',
2475              'z-index',
2476              'box-shadow',
2477              'aspect-ratio',
2478  
2479              // Custom CSS properties.
2480              '--*',
2481          )
2482      );
2483  
2484      /*
2485       * CSS attributes that accept URL data types.
2486       *
2487       * This is in accordance to the CSS spec and unrelated to
2488       * the sub-set of supported attributes above.
2489       *
2490       * See: https://developer.mozilla.org/en-US/docs/Web/CSS/url
2491       */
2492      $css_url_data_types = array(
2493          'background',
2494          'background-image',
2495  
2496          'cursor',
2497          'filter',
2498  
2499          'list-style',
2500          'list-style-image',
2501      );
2502  
2503      /*
2504       * CSS attributes that accept gradient data types.
2505       *
2506       */
2507      $css_gradient_data_types = array(
2508          'background',
2509          'background-image',
2510      );
2511  
2512      if ( empty( $allowed_attr ) ) {
2513          return $css;
2514      }
2515  
2516      $css = '';
2517      foreach ( $css_array as $css_item ) {
2518          if ( '' === $css_item ) {
2519              continue;
2520          }
2521  
2522          $css_item        = trim( $css_item );
2523          $css_test_string = $css_item;
2524          $found           = false;
2525          $url_attr        = false;
2526          $gradient_attr   = false;
2527          $is_custom_var   = false;
2528  
2529          if ( ! str_contains( $css_item, ':' ) ) {
2530              $found = true;
2531          } else {
2532              $parts        = explode( ':', $css_item, 2 );
2533              $css_selector = trim( $parts[0] );
2534  
2535              // Allow assigning values to CSS variables.
2536              if ( in_array( '--*', $allowed_attr, true ) && preg_match( '/^--[a-zA-Z0-9-_]+$/', $css_selector ) ) {
2537                  $allowed_attr[] = $css_selector;
2538                  $is_custom_var  = true;
2539              }
2540  
2541              if ( in_array( $css_selector, $allowed_attr, true ) ) {
2542                  $found         = true;
2543                  $url_attr      = in_array( $css_selector, $css_url_data_types, true );
2544                  $gradient_attr = in_array( $css_selector, $css_gradient_data_types, true );
2545              }
2546  
2547              if ( $is_custom_var ) {
2548                  $css_value     = trim( $parts[1] );
2549                  $url_attr      = str_starts_with( $css_value, 'url(' );
2550                  $gradient_attr = str_contains( $css_value, '-gradient(' );
2551              }
2552          }
2553  
2554          if ( $found && $url_attr ) {
2555              // Simplified: matches the sequence `url(*)`.
2556              preg_match_all( '/url\([^)]+\)/', $parts[1], $url_matches );
2557  
2558              foreach ( $url_matches[0] as $url_match ) {
2559                  // Clean up the URL from each of the matches above.
2560                  preg_match( '/^url\(\s*([\'\"]?)(.*)(\g1)\s*\)$/', $url_match, $url_pieces );
2561  
2562                  if ( empty( $url_pieces[2] ) ) {
2563                      $found = false;
2564                      break;
2565                  }
2566  
2567                  $url = trim( $url_pieces[2] );
2568  
2569                  if ( empty( $url ) || wp_kses_bad_protocol( $url, $allowed_protocols ) !== $url ) {
2570                      $found = false;
2571                      break;
2572                  } else {
2573                      // Remove the whole `url(*)` bit that was matched above from the CSS.
2574                      $css_test_string = str_replace( $url_match, '', $css_test_string );
2575                  }
2576              }
2577          }
2578  
2579          if ( $found && $gradient_attr ) {
2580              $css_value = trim( $parts[1] );
2581              if ( preg_match( '/^(repeating-)?(linear|radial|conic)-gradient\(([^()]|rgb[a]?\([^()]*\))*\)$/', $css_value ) ) {
2582                  // Remove the whole `gradient` bit that was matched above from the CSS.
2583                  $css_test_string = str_replace( $css_value, '', $css_test_string );
2584              }
2585          }
2586  
2587          if ( $found ) {
2588              /*
2589               * Allow CSS functions like var(), calc(), etc. by removing them from the test string.
2590               * Nested functions and parentheses are also removed, so long as the parentheses are balanced.
2591               */
2592              $css_test_string = preg_replace(
2593                  '/\b(?:var|calc|min|max|minmax|clamp|repeat)(\((?:[^()]|(?1))*\))/',
2594                  '',
2595                  $css_test_string
2596              );
2597  
2598              /*
2599               * Disallow CSS containing \ ( & } = or comments, except for within url(), var(), calc(), etc.
2600               * which were removed from the test string above.
2601               */
2602              $allow_css = ! preg_match( '%[\\\(&=}]|/\*%', $css_test_string );
2603  
2604              /**
2605               * Filters the check for unsafe CSS in `safecss_filter_attr`.
2606               *
2607               * Enables developers to determine whether a section of CSS should be allowed or discarded.
2608               * By default, the value will be false if the part contains \ ( & } = or comments.
2609               * Return true to allow the CSS part to be included in the output.
2610               *
2611               * @since 5.5.0
2612               *
2613               * @param bool   $allow_css       Whether the CSS in the test string is considered safe.
2614               * @param string $css_test_string The CSS string to test.
2615               */
2616              $allow_css = apply_filters( 'safecss_filter_attr_allow_css', $allow_css, $css_test_string );
2617  
2618              // Only add the CSS part if it passes the regex check.
2619              if ( $allow_css ) {
2620                  if ( '' !== $css ) {
2621                      $css .= ';';
2622                  }
2623  
2624                  $css .= $css_item;
2625              }
2626          }
2627      }
2628  
2629      return $css;
2630  }
2631  
2632  /**
2633   * Helper function to add global attributes to a tag in the allowed HTML list.
2634   *
2635   * @since 3.5.0
2636   * @since 5.0.0 Added support for `data-*` wildcard attributes.
2637   * @since 6.0.0 Added `dir`, `lang`, and `xml:lang` to global attributes.
2638   * @since 6.3.0 Added `aria-controls`, `aria-current`, and `aria-expanded` attributes.
2639   * @since 6.4.0 Added `aria-live` and `hidden` attributes.
2640   *
2641   * @access private
2642   * @ignore
2643   *
2644   * @param array $value An array of attributes.
2645   * @return array The array of attributes with global attributes added.
2646   */
2647  function _wp_add_global_attributes( $value ) {
2648      $global_attributes = array(
2649          'aria-controls'    => true,
2650          'aria-current'     => true,
2651          'aria-describedby' => true,
2652          'aria-details'     => true,
2653          'aria-expanded'    => true,
2654          'aria-hidden'      => true,
2655          'aria-label'       => true,
2656          'aria-labelledby'  => true,
2657          'aria-live'        => true,
2658          'class'            => true,
2659          'data-*'           => true,
2660          'dir'              => true,
2661          'hidden'           => true,
2662          'id'               => true,
2663          'lang'             => true,
2664          'style'            => true,
2665          'title'            => true,
2666          'role'             => true,
2667          'xml:lang'         => true,
2668      );
2669  
2670      if ( true === $value ) {
2671          $value = array();
2672      }
2673  
2674      if ( is_array( $value ) ) {
2675          return array_merge( $value, $global_attributes );
2676      }
2677  
2678      return $value;
2679  }
2680  
2681  /**
2682   * Helper function to check if this is a safe PDF URL.
2683   *
2684   * @since 5.9.0
2685   * @access private
2686   * @ignore
2687   *
2688   * @param string $url The URL to check.
2689   * @return bool True if the URL is safe, false otherwise.
2690   */
2691  function _wp_kses_allow_pdf_objects( $url ) {
2692      // We're not interested in URLs that contain query strings or fragments.
2693      if ( str_contains( $url, '?' ) || str_contains( $url, '#' ) ) {
2694          return false;
2695      }
2696  
2697      // If it doesn't have a PDF extension, it's not safe.
2698      if ( ! str_ends_with( $url, '.pdf' ) ) {
2699          return false;
2700      }
2701  
2702      // If the URL host matches the current site's media URL, it's safe.
2703      $upload_info = wp_upload_dir( null, false );
2704      $parsed_url  = wp_parse_url( $upload_info['url'] );
2705      $upload_host = isset( $parsed_url['host'] ) ? $parsed_url['host'] : '';
2706      $upload_port = isset( $parsed_url['port'] ) ? ':' . $parsed_url['port'] : '';
2707  
2708      if ( str_starts_with( $url, "http://$upload_host$upload_port/" )
2709          || str_starts_with( $url, "https://$upload_host$upload_port/" )
2710      ) {
2711          return true;
2712      }
2713  
2714      return false;
2715  }


Generated : Thu May 9 08:20:02 2024 Cross-referenced by PHPXref