[ 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_term_description':
 899          case 'pre_user_description':
 900              $tags                = $allowedtags;
 901              $tags['a']['rel']    = true;
 902              $tags['a']['target'] = true;
 903              /** This filter is documented in wp-includes/kses.php */
 904              return apply_filters( 'wp_kses_allowed_html', $tags, $context );
 905  
 906          case 'strip':
 907              /** This filter is documented in wp-includes/kses.php */
 908              return apply_filters( 'wp_kses_allowed_html', array(), $context );
 909  
 910          case 'entities':
 911              /** This filter is documented in wp-includes/kses.php */
 912              return apply_filters( 'wp_kses_allowed_html', $allowedentitynames, $context );
 913  
 914          case 'data':
 915          default:
 916              /** This filter is documented in wp-includes/kses.php */
 917              return apply_filters( 'wp_kses_allowed_html', $allowedtags, $context );
 918      }
 919  }
 920  
 921  /**
 922   * You add any KSES hooks here.
 923   *
 924   * There is currently only one KSES WordPress hook, {@see 'pre_kses'}, and it is called here.
 925   * All parameters are passed to the hooks and expected to receive a string.
 926   *
 927   * @since 1.0.0
 928   *
 929   * @param string         $content           Content to filter through KSES.
 930   * @param array[]|string $allowed_html      An array of allowed HTML elements and attributes,
 931   *                                          or a context name such as 'post'. See wp_kses_allowed_html()
 932   *                                          for the list of accepted context names.
 933   * @param string[]       $allowed_protocols Array of allowed URL protocols.
 934   * @return string Filtered content through {@see 'pre_kses'} hook.
 935   */
 936  function wp_kses_hook( $content, $allowed_html, $allowed_protocols ) {
 937      /**
 938       * Filters content to be run through KSES.
 939       *
 940       * @since 2.3.0
 941       *
 942       * @param string         $content           Content to filter through KSES.
 943       * @param array[]|string $allowed_html      An array of allowed HTML elements and attributes,
 944       *                                          or a context name such as 'post'. See wp_kses_allowed_html()
 945       *                                          for the list of accepted context names.
 946       * @param string[]       $allowed_protocols Array of allowed URL protocols.
 947       */
 948      return apply_filters( 'pre_kses', $content, $allowed_html, $allowed_protocols );
 949  }
 950  
 951  /**
 952   * Returns the version number of KSES.
 953   *
 954   * @since 1.0.0
 955   *
 956   * @return string KSES version number.
 957   */
 958  function wp_kses_version() {
 959      return '0.2.2';
 960  }
 961  
 962  /**
 963   * Searches for HTML tags, no matter how malformed.
 964   *
 965   * It also matches stray `>` characters.
 966   *
 967   * @since 1.0.0
 968   * @since 6.6.0 Recognize additional forms of invalid HTML which convert into comments.
 969   *
 970   * @global array[]|string $pass_allowed_html      An array of allowed HTML elements and attributes,
 971   *                                                or a context name such as 'post'.
 972   * @global string[]       $pass_allowed_protocols Array of allowed URL protocols.
 973   *
 974   * @param string         $content           Content to filter.
 975   * @param array[]|string $allowed_html      An array of allowed HTML elements and attributes,
 976   *                                          or a context name such as 'post'. See wp_kses_allowed_html()
 977   *                                          for the list of accepted context names.
 978   * @param string[]       $allowed_protocols Array of allowed URL protocols.
 979   * @return string Content with fixed HTML tags
 980   */
 981  function wp_kses_split( $content, $allowed_html, $allowed_protocols ) {
 982      global $pass_allowed_html, $pass_allowed_protocols;
 983  
 984      $pass_allowed_html      = $allowed_html;
 985      $pass_allowed_protocols = $allowed_protocols;
 986  
 987      $token_pattern = <<<REGEX
 988  ~
 989      (                      # Detect comments of various flavors before attempting to find tags.
 990          (<!--.*?(-->|$))   #  - Normative HTML comments.
 991          |
 992          </[^a-zA-Z][^>]*>  #  - Closing tags with invalid tag names.
 993          |
 994          <![^>]*>           #  - Invalid markup declaration nodes. Not all invalid nodes
 995                             #    are matched so as to avoid breaking legacy behaviors.
 996      )
 997      |
 998      (<[^>]*(>|$)|>)        # Tag-like spans of text.
 999  ~x
1000  REGEX;
1001      return preg_replace_callback( $token_pattern, '_wp_kses_split_callback', $content );
1002  }
1003  
1004  /**
1005   * Returns an array of HTML attribute names whose value contains a URL.
1006   *
1007   * This function returns a list of all HTML attributes that must contain
1008   * a URL according to the HTML specification.
1009   *
1010   * This list includes URI attributes both allowed and disallowed by KSES.
1011   *
1012   * @link https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes
1013   *
1014   * @since 5.0.1
1015   *
1016   * @return string[] HTML attribute names whose value contains a URL.
1017   */
1018  function wp_kses_uri_attributes() {
1019      $uri_attributes = array(
1020          'action',
1021          'archive',
1022          'background',
1023          'cite',
1024          'classid',
1025          'codebase',
1026          'data',
1027          'formaction',
1028          'href',
1029          'icon',
1030          'longdesc',
1031          'manifest',
1032          'poster',
1033          'profile',
1034          'src',
1035          'usemap',
1036          'xmlns',
1037      );
1038  
1039      /**
1040       * Filters the list of attributes that are required to contain a URL.
1041       *
1042       * Use this filter to add any `data-` attributes that are required to be
1043       * validated as a URL.
1044       *
1045       * @since 5.0.1
1046       *
1047       * @param string[] $uri_attributes HTML attribute names whose value contains a URL.
1048       */
1049      $uri_attributes = apply_filters( 'wp_kses_uri_attributes', $uri_attributes );
1050  
1051      return $uri_attributes;
1052  }
1053  
1054  /**
1055   * Callback for `wp_kses_split()`.
1056   *
1057   * @since 3.1.0
1058   * @access private
1059   * @ignore
1060   *
1061   * @global array[]|string $pass_allowed_html      An array of allowed HTML elements and attributes,
1062   *                                                or a context name such as 'post'.
1063   * @global string[]       $pass_allowed_protocols Array of allowed URL protocols.
1064   *
1065   * @param array $matches preg_replace regexp matches
1066   * @return string
1067   */
1068  function _wp_kses_split_callback( $matches ) {
1069      global $pass_allowed_html, $pass_allowed_protocols;
1070  
1071      return wp_kses_split2( $matches[0], $pass_allowed_html, $pass_allowed_protocols );
1072  }
1073  
1074  /**
1075   * Callback for `wp_kses_split()` for fixing malformed HTML tags.
1076   *
1077   * This function does a lot of work. It rejects some very malformed things like
1078   * `<:::>`. It returns an empty string, if the element isn't allowed (look ma, no
1079   * `strip_tags()`!). Otherwise it splits the tag into an element and an attribute
1080   * list.
1081   *
1082   * After the tag is split into an element and an attribute list, it is run
1083   * through another filter which will remove illegal attributes and once that is
1084   * completed, will be returned.
1085   *
1086   * @access private
1087   * @ignore
1088   * @since 1.0.0
1089   * @since 6.6.0 Recognize additional forms of invalid HTML which convert into comments.
1090   *
1091   * @param string         $content           Content to filter.
1092   * @param array[]|string $allowed_html      An array of allowed HTML elements and attributes,
1093   *                                          or a context name such as 'post'. See wp_kses_allowed_html()
1094   *                                          for the list of accepted context names.
1095   * @param string[]       $allowed_protocols Array of allowed URL protocols.
1096   *
1097   * @return string Fixed HTML element
1098   */
1099  function wp_kses_split2( $content, $allowed_html, $allowed_protocols ) {
1100      $content = wp_kses_stripslashes( $content );
1101  
1102      /*
1103       * The regex pattern used to split HTML into chunks attempts
1104       * to split on HTML token boundaries. This function should
1105       * thus receive chunks that _either_ start with meaningful
1106       * syntax tokens, like a tag `<div>` or a comment `<!-- ... -->`.
1107       *
1108       * If the first character of the `$content` chunk _isn't_ one
1109       * of these syntax elements, which always starts with `<`, then
1110       * the match had to be for the final alternation of `>`. In such
1111       * case, it's probably standing on its own and could be encoded
1112       * with a character reference to remove ambiguity.
1113       *
1114       * In other words, if this chunk isn't from a match of a syntax
1115       * token, it's just a plaintext greater-than (`>`) sign.
1116       */
1117      if ( ! str_starts_with( $content, '<' ) ) {
1118          return '&gt;';
1119      }
1120  
1121      /*
1122       * When certain invalid syntax constructs appear, the HTML parser
1123       * shifts into what's called the "bogus comment state." This is a
1124       * plaintext state that consumes everything until the nearest `>`
1125       * and then transforms the entire span into an HTML comment.
1126       *
1127       * Preserve these comments and do not treat them like tags.
1128       *
1129       * @see https://html.spec.whatwg.org/#bogus-comment-state
1130       */
1131      if ( 1 === preg_match( '~^(?:</[^a-zA-Z][^>]*>|<![a-z][^>]*>)$~', $content ) ) {
1132          /**
1133           * Since the pattern matches `</…>` and also `<!…>`, this will
1134           * preserve the type of the cleaned-up token in the output.
1135           */
1136          $opener  = $content[1];
1137          $content = substr( $content, 2, -1 );
1138  
1139          do {
1140              $prev    = $content;
1141              $content = wp_kses( $content, $allowed_html, $allowed_protocols );
1142          } while ( $prev !== $content );
1143  
1144          // Recombine the modified inner content with the original token structure.
1145          return "<{$opener}{$content}>";
1146      }
1147  
1148      /*
1149       * Normative HTML comments should be handled separately as their
1150       * parsing rules differ from those for tags and text nodes.
1151       */
1152      if ( str_starts_with( $content, '<!--' ) ) {
1153          $content = str_replace( array( '<!--', '-->' ), '', $content );
1154  
1155          while ( ( $newstring = wp_kses( $content, $allowed_html, $allowed_protocols ) ) !== $content ) {
1156              $content = $newstring;
1157          }
1158  
1159          if ( '' === $content ) {
1160              return '';
1161          }
1162  
1163          // Prevent multiple dashes in comments.
1164          $content = preg_replace( '/--+/', '-', $content );
1165          // Prevent three dashes closing a comment.
1166          $content = preg_replace( '/-$/', '', $content );
1167  
1168          return "<!--{$content}-->";
1169      }
1170  
1171      // It's seriously malformed.
1172      if ( ! preg_match( '%^<\s*(/\s*)?([a-zA-Z0-9-]+)([^>]*)>?$%', $content, $matches ) ) {
1173          return '';
1174      }
1175  
1176      $slash    = trim( $matches[1] );
1177      $elem     = $matches[2];
1178      $attrlist = $matches[3];
1179  
1180      if ( ! is_array( $allowed_html ) ) {
1181          $allowed_html = wp_kses_allowed_html( $allowed_html );
1182      }
1183  
1184      // They are using a not allowed HTML element.
1185      if ( ! isset( $allowed_html[ strtolower( $elem ) ] ) ) {
1186          return '';
1187      }
1188  
1189      // No attributes are allowed for closing elements.
1190      if ( '' !== $slash ) {
1191          return "</$elem>";
1192      }
1193  
1194      return wp_kses_attr( $elem, $attrlist, $allowed_html, $allowed_protocols );
1195  }
1196  
1197  /**
1198   * Removes all attributes, if none are allowed for this element.
1199   *
1200   * If some are allowed it calls `wp_kses_hair()` to split them further, and then
1201   * it builds up new HTML code from the data that `wp_kses_hair()` returns. It also
1202   * removes `<` and `>` characters, if there are any left. One more thing it does
1203   * is to check if the tag has a closing XHTML slash, and if it does, it puts one
1204   * in the returned code as well.
1205   *
1206   * An array of allowed values can be defined for attributes. If the attribute value
1207   * doesn't fall into the list, the attribute will be removed from the tag.
1208   *
1209   * Attributes can be marked as required. If a required attribute is not present,
1210   * KSES will remove all attributes from the tag. As KSES doesn't match opening and
1211   * closing tags, it's not possible to safely remove the tag itself, the safest
1212   * fallback is to strip all attributes from the tag, instead.
1213   *
1214   * @since 1.0.0
1215   * @since 5.9.0 Added support for an array of allowed values for attributes.
1216   *              Added support for required attributes.
1217   *
1218   * @param string         $element           HTML element/tag.
1219   * @param string         $attr              HTML attributes from HTML element to closing HTML element tag.
1220   * @param array[]|string $allowed_html      An array of allowed HTML elements and attributes,
1221   *                                          or a context name such as 'post'. See wp_kses_allowed_html()
1222   *                                          for the list of accepted context names.
1223   * @param string[]       $allowed_protocols Array of allowed URL protocols.
1224   * @return string Sanitized HTML element.
1225   */
1226  function wp_kses_attr( $element, $attr, $allowed_html, $allowed_protocols ) {
1227      if ( ! is_array( $allowed_html ) ) {
1228          $allowed_html = wp_kses_allowed_html( $allowed_html );
1229      }
1230  
1231      // Is there a closing XHTML slash at the end of the attributes?
1232      $xhtml_slash = '';
1233      if ( preg_match( '%\s*/\s*$%', $attr ) ) {
1234          $xhtml_slash = ' /';
1235      }
1236  
1237      // Are any attributes allowed at all for this element?
1238      $element_low = strtolower( $element );
1239      if ( empty( $allowed_html[ $element_low ] ) || true === $allowed_html[ $element_low ] ) {
1240          return "<$element$xhtml_slash>";
1241      }
1242  
1243      // Split it.
1244      $attrarr = wp_kses_hair( $attr, $allowed_protocols );
1245  
1246      // Check if there are attributes that are required.
1247      $required_attrs = array_filter(
1248          $allowed_html[ $element_low ],
1249          static function ( $required_attr_limits ) {
1250              return isset( $required_attr_limits['required'] ) && true === $required_attr_limits['required'];
1251          }
1252      );
1253  
1254      /*
1255       * If a required attribute check fails, we can return nothing for a self-closing tag,
1256       * but for a non-self-closing tag the best option is to return the element with attributes,
1257       * as KSES doesn't handle matching the relevant closing tag.
1258       */
1259      $stripped_tag = '';
1260      if ( empty( $xhtml_slash ) ) {
1261          $stripped_tag = "<$element>";
1262      }
1263  
1264      // Go through $attrarr, and save the allowed attributes for this element in $attr2.
1265      $attr2 = '';
1266      foreach ( $attrarr as $arreach ) {
1267          // Check if this attribute is required.
1268          $required = isset( $required_attrs[ strtolower( $arreach['name'] ) ] );
1269  
1270          if ( wp_kses_attr_check( $arreach['name'], $arreach['value'], $arreach['whole'], $arreach['vless'], $element, $allowed_html ) ) {
1271              $attr2 .= ' ' . $arreach['whole'];
1272  
1273              // If this was a required attribute, we can mark it as found.
1274              if ( $required ) {
1275                  unset( $required_attrs[ strtolower( $arreach['name'] ) ] );
1276              }
1277          } elseif ( $required ) {
1278              // This attribute was required, but didn't pass the check. The entire tag is not allowed.
1279              return $stripped_tag;
1280          }
1281      }
1282  
1283      // If some required attributes weren't set, the entire tag is not allowed.
1284      if ( ! empty( $required_attrs ) ) {
1285          return $stripped_tag;
1286      }
1287  
1288      // Remove any "<" or ">" characters.
1289      $attr2 = preg_replace( '/[<>]/', '', $attr2 );
1290  
1291      return "<$element$attr2$xhtml_slash>";
1292  }
1293  
1294  /**
1295   * Determines whether an attribute is allowed.
1296   *
1297   * @since 4.2.3
1298   * @since 5.0.0 Added support for `data-*` wildcard attributes.
1299   *
1300   * @param string $name         The attribute name. Passed by reference. Returns empty string when not allowed.
1301   * @param string $value        The attribute value. Passed by reference. Returns a filtered value.
1302   * @param string $whole        The `name=value` input. Passed by reference. Returns filtered input.
1303   * @param string $vless        Whether the attribute is valueless. Use 'y' or 'n'.
1304   * @param string $element      The name of the element to which this attribute belongs.
1305   * @param array  $allowed_html The full list of allowed elements and attributes.
1306   * @return bool Whether or not the attribute is allowed.
1307   */
1308  function wp_kses_attr_check( &$name, &$value, &$whole, $vless, $element, $allowed_html ) {
1309      $name_low    = strtolower( $name );
1310      $element_low = strtolower( $element );
1311  
1312      if ( ! isset( $allowed_html[ $element_low ] ) ) {
1313          $name  = '';
1314          $value = '';
1315          $whole = '';
1316          return false;
1317      }
1318  
1319      $allowed_attr = $allowed_html[ $element_low ];
1320  
1321      if ( ! isset( $allowed_attr[ $name_low ] ) || '' === $allowed_attr[ $name_low ] ) {
1322          /*
1323           * Allow `data-*` attributes.
1324           *
1325           * When specifying `$allowed_html`, the attribute name should be set as
1326           * `data-*` (not to be mixed with the HTML 4.0 `data` attribute, see
1327           * https://www.w3.org/TR/html40/struct/objects.html#adef-data).
1328           *
1329           * Note: the attribute name should only contain `A-Za-z0-9_-` chars.
1330           */
1331          if ( str_starts_with( $name_low, 'data-' ) && ! empty( $allowed_attr['data-*'] )
1332              && preg_match( '/^data-[a-z0-9_-]+$/', $name_low, $match )
1333          ) {
1334              /*
1335               * Add the whole attribute name to the allowed attributes and set any restrictions
1336               * for the `data-*` attribute values for the current element.
1337               */
1338              $allowed_attr[ $match[0] ] = $allowed_attr['data-*'];
1339          } else {
1340              $name  = '';
1341              $value = '';
1342              $whole = '';
1343              return false;
1344          }
1345      }
1346  
1347      if ( 'style' === $name_low ) {
1348          $new_value = safecss_filter_attr( $value );
1349  
1350          if ( empty( $new_value ) ) {
1351              $name  = '';
1352              $value = '';
1353              $whole = '';
1354              return false;
1355          }
1356  
1357          $whole = str_replace( $value, $new_value, $whole );
1358          $value = $new_value;
1359      }
1360  
1361      if ( is_array( $allowed_attr[ $name_low ] ) ) {
1362          // There are some checks.
1363          foreach ( $allowed_attr[ $name_low ] as $currkey => $currval ) {
1364              if ( ! wp_kses_check_attr_val( $value, $vless, $currkey, $currval ) ) {
1365                  $name  = '';
1366                  $value = '';
1367                  $whole = '';
1368                  return false;
1369              }
1370          }
1371      }
1372  
1373      return true;
1374  }
1375  
1376  /**
1377   * Builds an attribute list from string containing attributes.
1378   *
1379   * This function does a lot of work. It parses an attribute list into an array
1380   * with attribute data, and tries to do the right thing even if it gets weird
1381   * input. It will add quotes around attribute values that don't have any quotes
1382   * or apostrophes around them, to make it easier to produce HTML code that will
1383   * conform to W3C's HTML specification. It will also remove bad URL protocols
1384   * from attribute values. It also reduces duplicate attributes by using the
1385   * attribute defined first (`foo='bar' foo='baz'` will result in `foo='bar'`).
1386   *
1387   * @since 1.0.0
1388   *
1389   * @param string   $attr              Attribute list from HTML element to closing HTML element tag.
1390   * @param string[] $allowed_protocols Array of allowed URL protocols.
1391   * @return array[] Array of attribute information after parsing.
1392   */
1393  function wp_kses_hair( $attr, $allowed_protocols ) {
1394      $attrarr  = array();
1395      $mode     = 0;
1396      $attrname = '';
1397      $uris     = wp_kses_uri_attributes();
1398  
1399      // Loop through the whole attribute list.
1400  
1401      while ( strlen( $attr ) !== 0 ) {
1402          $working = 0; // Was the last operation successful?
1403  
1404          switch ( $mode ) {
1405              case 0:
1406                  if ( preg_match( '/^([_a-zA-Z][-_a-zA-Z0-9:.]*)/', $attr, $match ) ) {
1407                      $attrname = $match[1];
1408                      $working  = 1;
1409                      $mode     = 1;
1410                      $attr     = preg_replace( '/^[_a-zA-Z][-_a-zA-Z0-9:.]*/', '', $attr );
1411                  }
1412  
1413                  break;
1414  
1415              case 1:
1416                  if ( preg_match( '/^\s*=\s*/', $attr ) ) { // Equals sign.
1417                      $working = 1;
1418                      $mode    = 2;
1419                      $attr    = preg_replace( '/^\s*=\s*/', '', $attr );
1420                      break;
1421                  }
1422  
1423                  if ( preg_match( '/^\s+/', $attr ) ) { // Valueless.
1424                      $working = 1;
1425                      $mode    = 0;
1426  
1427                      if ( false === array_key_exists( $attrname, $attrarr ) ) {
1428                          $attrarr[ $attrname ] = array(
1429                              'name'  => $attrname,
1430                              'value' => '',
1431                              'whole' => $attrname,
1432                              'vless' => 'y',
1433                          );
1434                      }
1435  
1436                      $attr = preg_replace( '/^\s+/', '', $attr );
1437                  }
1438  
1439                  break;
1440  
1441              case 2:
1442                  if ( preg_match( '%^"([^"]*)"(\s+|/?$)%', $attr, $match ) ) {
1443                      // "value"
1444                      $thisval = $match[1];
1445                      if ( in_array( strtolower( $attrname ), $uris, true ) ) {
1446                          $thisval = wp_kses_bad_protocol( $thisval, $allowed_protocols );
1447                      }
1448  
1449                      if ( false === array_key_exists( $attrname, $attrarr ) ) {
1450                          $attrarr[ $attrname ] = array(
1451                              'name'  => $attrname,
1452                              'value' => $thisval,
1453                              'whole' => "$attrname=\"$thisval\"",
1454                              'vless' => 'n',
1455                          );
1456                      }
1457  
1458                      $working = 1;
1459                      $mode    = 0;
1460                      $attr    = preg_replace( '/^"[^"]*"(\s+|$)/', '', $attr );
1461                      break;
1462                  }
1463  
1464                  if ( preg_match( "%^'([^']*)'(\s+|/?$)%", $attr, $match ) ) {
1465                      // 'value'
1466                      $thisval = $match[1];
1467                      if ( in_array( strtolower( $attrname ), $uris, true ) ) {
1468                          $thisval = wp_kses_bad_protocol( $thisval, $allowed_protocols );
1469                      }
1470  
1471                      if ( false === array_key_exists( $attrname, $attrarr ) ) {
1472                          $attrarr[ $attrname ] = array(
1473                              'name'  => $attrname,
1474                              'value' => $thisval,
1475                              'whole' => "$attrname='$thisval'",
1476                              'vless' => 'n',
1477                          );
1478                      }
1479  
1480                      $working = 1;
1481                      $mode    = 0;
1482                      $attr    = preg_replace( "/^'[^']*'(\s+|$)/", '', $attr );
1483                      break;
1484                  }
1485  
1486                  if ( preg_match( "%^([^\s\"']+)(\s+|/?$)%", $attr, $match ) ) {
1487                      // value
1488                      $thisval = $match[1];
1489                      if ( in_array( strtolower( $attrname ), $uris, true ) ) {
1490                          $thisval = wp_kses_bad_protocol( $thisval, $allowed_protocols );
1491                      }
1492  
1493                      if ( false === array_key_exists( $attrname, $attrarr ) ) {
1494                          $attrarr[ $attrname ] = array(
1495                              'name'  => $attrname,
1496                              'value' => $thisval,
1497                              'whole' => "$attrname=\"$thisval\"",
1498                              'vless' => 'n',
1499                          );
1500                      }
1501  
1502                      // We add quotes to conform to W3C's HTML spec.
1503                      $working = 1;
1504                      $mode    = 0;
1505                      $attr    = preg_replace( "%^[^\s\"']+(\s+|$)%", '', $attr );
1506                  }
1507  
1508                  break;
1509          } // End switch.
1510  
1511          if ( 0 === $working ) { // Not well-formed, remove and try again.
1512              $attr = wp_kses_html_error( $attr );
1513              $mode = 0;
1514          }
1515      } // End while.
1516  
1517      if ( 1 === $mode && false === array_key_exists( $attrname, $attrarr ) ) {
1518          /*
1519           * Special case, for when the attribute list ends with a valueless
1520           * attribute like "selected".
1521           */
1522          $attrarr[ $attrname ] = array(
1523              'name'  => $attrname,
1524              'value' => '',
1525              'whole' => $attrname,
1526              'vless' => 'y',
1527          );
1528      }
1529  
1530      return $attrarr;
1531  }
1532  
1533  /**
1534   * Finds all attributes of an HTML element.
1535   *
1536   * Does not modify input.  May return "evil" output.
1537   *
1538   * Based on `wp_kses_split2()` and `wp_kses_attr()`.
1539   *
1540   * @since 4.2.3
1541   *
1542   * @param string $element HTML element.
1543   * @return array|false List of attributes found in the element. Returns false on failure.
1544   */
1545  function wp_kses_attr_parse( $element ) {
1546      $valid = preg_match( '%^(<\s*)(/\s*)?([a-zA-Z0-9]+\s*)([^>]*)(>?)$%', $element, $matches );
1547      if ( 1 !== $valid ) {
1548          return false;
1549      }
1550  
1551      $begin  = $matches[1];
1552      $slash  = $matches[2];
1553      $elname = $matches[3];
1554      $attr   = $matches[4];
1555      $end    = $matches[5];
1556  
1557      if ( '' !== $slash ) {
1558          // Closing elements do not get parsed.
1559          return false;
1560      }
1561  
1562      // Is there a closing XHTML slash at the end of the attributes?
1563      if ( 1 === preg_match( '%\s*/\s*$%', $attr, $matches ) ) {
1564          $xhtml_slash = $matches[0];
1565          $attr        = substr( $attr, 0, -strlen( $xhtml_slash ) );
1566      } else {
1567          $xhtml_slash = '';
1568      }
1569  
1570      // Split it.
1571      $attrarr = wp_kses_hair_parse( $attr );
1572      if ( false === $attrarr ) {
1573          return false;
1574      }
1575  
1576      // Make sure all input is returned by adding front and back matter.
1577      array_unshift( $attrarr, $begin . $slash . $elname );
1578      array_push( $attrarr, $xhtml_slash . $end );
1579  
1580      return $attrarr;
1581  }
1582  
1583  /**
1584   * Builds an attribute list from string containing attributes.
1585   *
1586   * Does not modify input.  May return "evil" output.
1587   * In case of unexpected input, returns false instead of stripping things.
1588   *
1589   * Based on `wp_kses_hair()` but does not return a multi-dimensional array.
1590   *
1591   * @since 4.2.3
1592   *
1593   * @param string $attr Attribute list from HTML element to closing HTML element tag.
1594   * @return array|false List of attributes found in $attr. Returns false on failure.
1595   */
1596  function wp_kses_hair_parse( $attr ) {
1597      if ( '' === $attr ) {
1598          return array();
1599      }
1600  
1601      $regex =
1602          '(?:
1603                  [_a-zA-Z][-_a-zA-Z0-9:.]* # Attribute name.
1604              |
1605                  \[\[?[^\[\]]+\]\]?        # Shortcode in the name position implies unfiltered_html.
1606          )
1607          (?:                               # Attribute value.
1608              \s*=\s*                       # All values begin with "=".
1609              (?:
1610                  "[^"]*"                   # Double-quoted.
1611              |
1612                  \'[^\']*\'                # Single-quoted.
1613              |
1614                  [^\s"\']+                 # Non-quoted.
1615                  (?:\s|$)                  # Must have a space.
1616              )
1617          |
1618              (?:\s|$)                      # If attribute has no value, space is required.
1619          )
1620          \s*                               # Trailing space is optional except as mentioned above.
1621          ';
1622  
1623      /*
1624       * Although it is possible to reduce this procedure to a single regexp,
1625       * we must run that regexp twice to get exactly the expected result.
1626       *
1627       * Note: do NOT remove the `x` modifiers as they are essential for the above regex!
1628       */
1629  
1630      $validation = "/^($regex)+$/x";
1631      $extraction = "/$regex/x";
1632  
1633      if ( 1 === preg_match( $validation, $attr ) ) {
1634          preg_match_all( $extraction, $attr, $attrarr );
1635          return $attrarr[0];
1636      } else {
1637          return false;
1638      }
1639  }
1640  
1641  /**
1642   * Performs different checks for attribute values.
1643   *
1644   * The currently implemented checks are "maxlen", "minlen", "maxval", "minval",
1645   * and "valueless".
1646   *
1647   * @since 1.0.0
1648   *
1649   * @param string $value      Attribute value.
1650   * @param string $vless      Whether the attribute is valueless. Use 'y' or 'n'.
1651   * @param string $checkname  What $checkvalue is checking for.
1652   * @param mixed  $checkvalue What constraint the value should pass.
1653   * @return bool Whether check passes.
1654   */
1655  function wp_kses_check_attr_val( $value, $vless, $checkname, $checkvalue ) {
1656      $ok = true;
1657  
1658      switch ( strtolower( $checkname ) ) {
1659          case 'maxlen':
1660              /*
1661               * The maxlen check makes sure that the attribute value has a length not
1662               * greater than the given value. This can be used to avoid Buffer Overflows
1663               * in WWW clients and various Internet servers.
1664               */
1665  
1666              if ( strlen( $value ) > $checkvalue ) {
1667                  $ok = false;
1668              }
1669              break;
1670  
1671          case 'minlen':
1672              /*
1673               * The minlen check makes sure that the attribute value has a length not
1674               * smaller than the given value.
1675               */
1676  
1677              if ( strlen( $value ) < $checkvalue ) {
1678                  $ok = false;
1679              }
1680              break;
1681  
1682          case 'maxval':
1683              /*
1684               * The maxval check does two things: it checks that the attribute value is
1685               * an integer from 0 and up, without an excessive amount of zeroes or
1686               * whitespace (to avoid Buffer Overflows). It also checks that the attribute
1687               * value is not greater than the given value.
1688               * This check can be used to avoid Denial of Service attacks.
1689               */
1690  
1691              if ( ! preg_match( '/^\s{0,6}[0-9]{1,6}\s{0,6}$/', $value ) ) {
1692                  $ok = false;
1693              }
1694              if ( $value > $checkvalue ) {
1695                  $ok = false;
1696              }
1697              break;
1698  
1699          case 'minval':
1700              /*
1701               * The minval check makes sure that the attribute value is a positive integer,
1702               * and that it is not smaller than the given value.
1703               */
1704  
1705              if ( ! preg_match( '/^\s{0,6}[0-9]{1,6}\s{0,6}$/', $value ) ) {
1706                  $ok = false;
1707              }
1708              if ( $value < $checkvalue ) {
1709                  $ok = false;
1710              }
1711              break;
1712  
1713          case 'valueless':
1714              /*
1715               * The valueless check makes sure if the attribute has a value
1716               * (like `<a href="blah">`) or not (`<option selected>`). If the given value
1717               * is a "y" or a "Y", the attribute must not have a value.
1718               * If the given value is an "n" or an "N", the attribute must have a value.
1719               */
1720  
1721              if ( strtolower( $checkvalue ) !== $vless ) {
1722                  $ok = false;
1723              }
1724              break;
1725  
1726          case 'values':
1727              /*
1728               * The values check is used when you want to make sure that the attribute
1729               * has one of the given values.
1730               */
1731  
1732              if ( false === array_search( strtolower( $value ), $checkvalue, true ) ) {
1733                  $ok = false;
1734              }
1735              break;
1736  
1737          case 'value_callback':
1738              /*
1739               * The value_callback check is used when you want to make sure that the attribute
1740               * value is accepted by the callback function.
1741               */
1742  
1743              if ( ! call_user_func( $checkvalue, $value ) ) {
1744                  $ok = false;
1745              }
1746              break;
1747      } // End switch.
1748  
1749      return $ok;
1750  }
1751  
1752  /**
1753   * Sanitizes a string and removed disallowed URL protocols.
1754   *
1755   * This function removes all non-allowed protocols from the beginning of the
1756   * string. It ignores whitespace and the case of the letters, and it does
1757   * understand HTML entities. It does its work recursively, so it won't be
1758   * fooled by a string like `javascript:javascript:alert(57)`.
1759   *
1760   * @since 1.0.0
1761   *
1762   * @param string   $content           Content to filter bad protocols from.
1763   * @param string[] $allowed_protocols Array of allowed URL protocols.
1764   * @return string Filtered content.
1765   */
1766  function wp_kses_bad_protocol( $content, $allowed_protocols ) {
1767      $content = wp_kses_no_null( $content );
1768  
1769      // Short-circuit if the string starts with `https://` or `http://`. Most common cases.
1770      if (
1771          ( str_starts_with( $content, 'https://' ) && in_array( 'https', $allowed_protocols, true ) ) ||
1772          ( str_starts_with( $content, 'http://' ) && in_array( 'http', $allowed_protocols, true ) )
1773      ) {
1774          return $content;
1775      }
1776  
1777      $iterations = 0;
1778  
1779      do {
1780          $original_content = $content;
1781          $content          = wp_kses_bad_protocol_once( $content, $allowed_protocols );
1782      } while ( $original_content !== $content && ++$iterations < 6 );
1783  
1784      if ( $original_content !== $content ) {
1785          return '';
1786      }
1787  
1788      return $content;
1789  }
1790  
1791  /**
1792   * Removes any invalid control characters in a text string.
1793   *
1794   * Also removes any instance of the `\0` string.
1795   *
1796   * @since 1.0.0
1797   *
1798   * @param string $content Content to filter null characters from.
1799   * @param array  $options Set 'slash_zero' => 'keep' when '\0' is allowed. Default is 'remove'.
1800   * @return string Filtered content.
1801   */
1802  function wp_kses_no_null( $content, $options = null ) {
1803      if ( ! isset( $options['slash_zero'] ) ) {
1804          $options = array( 'slash_zero' => 'remove' );
1805      }
1806  
1807      $content = preg_replace( '/[\x00-\x08\x0B\x0C\x0E-\x1F]/', '', $content );
1808      if ( 'remove' === $options['slash_zero'] ) {
1809          $content = preg_replace( '/\\\\+0+/', '', $content );
1810      }
1811  
1812      return $content;
1813  }
1814  
1815  /**
1816   * Strips slashes from in front of quotes.
1817   *
1818   * This function changes the character sequence `\"` to just `"`. It leaves all other
1819   * slashes alone. The quoting from `preg_replace(//e)` requires this.
1820   *
1821   * @since 1.0.0
1822   *
1823   * @param string $content String to strip slashes from.
1824   * @return string Fixed string with quoted slashes.
1825   */
1826  function wp_kses_stripslashes( $content ) {
1827      return preg_replace( '%\\\\"%', '"', $content );
1828  }
1829  
1830  /**
1831   * Converts the keys of an array to lowercase.
1832   *
1833   * @since 1.0.0
1834   *
1835   * @param array $inarray Unfiltered array.
1836   * @return array Fixed array with all lowercase keys.
1837   */
1838  function wp_kses_array_lc( $inarray ) {
1839      $outarray = array();
1840  
1841      foreach ( (array) $inarray as $inkey => $inval ) {
1842          $outkey              = strtolower( $inkey );
1843          $outarray[ $outkey ] = array();
1844  
1845          foreach ( (array) $inval as $inkey2 => $inval2 ) {
1846              $outkey2                         = strtolower( $inkey2 );
1847              $outarray[ $outkey ][ $outkey2 ] = $inval2;
1848          }
1849      }
1850  
1851      return $outarray;
1852  }
1853  
1854  /**
1855   * Handles parsing errors in `wp_kses_hair()`.
1856   *
1857   * The general plan is to remove everything to and including some whitespace,
1858   * but it deals with quotes and apostrophes as well.
1859   *
1860   * @since 1.0.0
1861   *
1862   * @param string $attr
1863   * @return string
1864   */
1865  function wp_kses_html_error( $attr ) {
1866      return preg_replace( '/^("[^"]*("|$)|\'[^\']*(\'|$)|\S)*\s*/', '', $attr );
1867  }
1868  
1869  /**
1870   * Sanitizes content from bad protocols and other characters.
1871   *
1872   * This function searches for URL protocols at the beginning of the string, while
1873   * handling whitespace and HTML entities.
1874   *
1875   * @since 1.0.0
1876   *
1877   * @param string   $content           Content to check for bad protocols.
1878   * @param string[] $allowed_protocols Array of allowed URL protocols.
1879   * @param int      $count             Depth of call recursion to this function.
1880   * @return string Sanitized content.
1881   */
1882  function wp_kses_bad_protocol_once( $content, $allowed_protocols, $count = 1 ) {
1883      $content  = preg_replace( '/(&#0*58(?![;0-9])|&#x0*3a(?![;a-f0-9]))/i', '$1;', $content );
1884      $content2 = preg_split( '/:|&#0*58;|&#x0*3a;|&colon;/i', $content, 2 );
1885  
1886      if ( isset( $content2[1] ) && ! preg_match( '%/\?%', $content2[0] ) ) {
1887          $content  = trim( $content2[1] );
1888          $protocol = wp_kses_bad_protocol_once2( $content2[0], $allowed_protocols );
1889          if ( 'feed:' === $protocol ) {
1890              if ( $count > 2 ) {
1891                  return '';
1892              }
1893              $content = wp_kses_bad_protocol_once( $content, $allowed_protocols, ++$count );
1894              if ( empty( $content ) ) {
1895                  return $content;
1896              }
1897          }
1898          $content = $protocol . $content;
1899      }
1900  
1901      return $content;
1902  }
1903  
1904  /**
1905   * Callback for `wp_kses_bad_protocol_once()` regular expression.
1906   *
1907   * This function processes URL protocols, checks to see if they're in the
1908   * list of allowed protocols or not, and returns different data depending
1909   * on the answer.
1910   *
1911   * @access private
1912   * @ignore
1913   * @since 1.0.0
1914   *
1915   * @param string   $scheme            URI scheme to check against the list of allowed protocols.
1916   * @param string[] $allowed_protocols Array of allowed URL protocols.
1917   * @return string Sanitized content.
1918   */
1919  function wp_kses_bad_protocol_once2( $scheme, $allowed_protocols ) {
1920      $scheme = wp_kses_decode_entities( $scheme );
1921      $scheme = preg_replace( '/\s/', '', $scheme );
1922      $scheme = wp_kses_no_null( $scheme );
1923      $scheme = strtolower( $scheme );
1924  
1925      $allowed = false;
1926      foreach ( (array) $allowed_protocols as $one_protocol ) {
1927          if ( strtolower( $one_protocol ) === $scheme ) {
1928              $allowed = true;
1929              break;
1930          }
1931      }
1932  
1933      if ( $allowed ) {
1934          return "$scheme:";
1935      } else {
1936          return '';
1937      }
1938  }
1939  
1940  /**
1941   * Converts and fixes HTML entities.
1942   *
1943   * This function normalizes HTML entities. It will convert `AT&T` to the correct
1944   * `AT&amp;T`, `&#00058;` to `&#058;`, `&#XYZZY;` to `&amp;#XYZZY;` and so on.
1945   *
1946   * When `$context` is set to 'xml', HTML entities are converted to their code points.  For
1947   * example, `AT&T&hellip;&#XYZZY;` is converted to `AT&amp;T…&amp;#XYZZY;`.
1948   *
1949   * @since 1.0.0
1950   * @since 5.5.0 Added `$context` parameter.
1951   *
1952   * @param string $content Content to normalize entities.
1953   * @param string $context Context for normalization. Can be either 'html' or 'xml'.
1954   *                        Default 'html'.
1955   * @return string Content with normalized entities.
1956   */
1957  function wp_kses_normalize_entities( $content, $context = 'html' ) {
1958      // Disarm all entities by converting & to &amp;
1959      $content = str_replace( '&', '&amp;', $content );
1960  
1961      // Change back the allowed entities in our list of allowed entities.
1962      if ( 'xml' === $context ) {
1963          $content = preg_replace_callback( '/&amp;([A-Za-z]{2,8}[0-9]{0,2});/', 'wp_kses_xml_named_entities', $content );
1964      } else {
1965          $content = preg_replace_callback( '/&amp;([A-Za-z]{2,8}[0-9]{0,2});/', 'wp_kses_named_entities', $content );
1966      }
1967      $content = preg_replace_callback( '/&amp;#(0*[0-9]{1,7});/', 'wp_kses_normalize_entities2', $content );
1968      $content = preg_replace_callback( '/&amp;#[Xx](0*[0-9A-Fa-f]{1,6});/', 'wp_kses_normalize_entities3', $content );
1969  
1970      return $content;
1971  }
1972  
1973  /**
1974   * Callback for `wp_kses_normalize_entities()` regular expression.
1975   *
1976   * This function only accepts valid named entity references, which are finite,
1977   * case-sensitive, and highly scrutinized by HTML and XML validators.
1978   *
1979   * @since 3.0.0
1980   *
1981   * @global array $allowedentitynames
1982   *
1983   * @param array $matches preg_replace_callback() matches array.
1984   * @return string Correctly encoded entity.
1985   */
1986  function wp_kses_named_entities( $matches ) {
1987      global $allowedentitynames;
1988  
1989      if ( empty( $matches[1] ) ) {
1990          return '';
1991      }
1992  
1993      $i = $matches[1];
1994      return ( ! in_array( $i, $allowedentitynames, true ) ) ? "&amp;$i;" : "&$i;";
1995  }
1996  
1997  /**
1998   * Callback for `wp_kses_normalize_entities()` regular expression.
1999   *
2000   * This function only accepts valid named entity references, which are finite,
2001   * case-sensitive, and highly scrutinized by XML validators.  HTML named entity
2002   * references are converted to their code points.
2003   *
2004   * @since 5.5.0
2005   *
2006   * @global array $allowedentitynames
2007   * @global array $allowedxmlentitynames
2008   *
2009   * @param array $matches preg_replace_callback() matches array.
2010   * @return string Correctly encoded entity.
2011   */
2012  function wp_kses_xml_named_entities( $matches ) {
2013      global $allowedentitynames, $allowedxmlentitynames;
2014  
2015      if ( empty( $matches[1] ) ) {
2016          return '';
2017      }
2018  
2019      $i = $matches[1];
2020  
2021      if ( in_array( $i, $allowedxmlentitynames, true ) ) {
2022          return "&$i;";
2023      } elseif ( in_array( $i, $allowedentitynames, true ) ) {
2024          return html_entity_decode( "&$i;", ENT_HTML5 );
2025      }
2026  
2027      return "&amp;$i;";
2028  }
2029  
2030  /**
2031   * Callback for `wp_kses_normalize_entities()` regular expression.
2032   *
2033   * This function helps `wp_kses_normalize_entities()` to only accept 16-bit
2034   * values and nothing more for `&#number;` entities.
2035   *
2036   * @access private
2037   * @ignore
2038   * @since 1.0.0
2039   *
2040   * @param array $matches `preg_replace_callback()` matches array.
2041   * @return string Correctly encoded entity.
2042   */
2043  function wp_kses_normalize_entities2( $matches ) {
2044      if ( empty( $matches[1] ) ) {
2045          return '';
2046      }
2047  
2048      $i = $matches[1];
2049  
2050      if ( valid_unicode( $i ) ) {
2051          $i = str_pad( ltrim( $i, '0' ), 3, '0', STR_PAD_LEFT );
2052          $i = "&#$i;";
2053      } else {
2054          $i = "&amp;#$i;";
2055      }
2056  
2057      return $i;
2058  }
2059  
2060  /**
2061   * Callback for `wp_kses_normalize_entities()` for regular expression.
2062   *
2063   * This function helps `wp_kses_normalize_entities()` to only accept valid Unicode
2064   * numeric entities in hex form.
2065   *
2066   * @since 2.7.0
2067   * @access private
2068   * @ignore
2069   *
2070   * @param array $matches `preg_replace_callback()` matches array.
2071   * @return string Correctly encoded entity.
2072   */
2073  function wp_kses_normalize_entities3( $matches ) {
2074      if ( empty( $matches[1] ) ) {
2075          return '';
2076      }
2077  
2078      $hexchars = $matches[1];
2079  
2080      return ( ! valid_unicode( hexdec( $hexchars ) ) ) ? "&amp;#x$hexchars;" : '&#x' . ltrim( $hexchars, '0' ) . ';';
2081  }
2082  
2083  /**
2084   * Determines if a Unicode codepoint is valid.
2085   *
2086   * @since 2.7.0
2087   *
2088   * @param int $i Unicode codepoint.
2089   * @return bool Whether or not the codepoint is a valid Unicode codepoint.
2090   */
2091  function valid_unicode( $i ) {
2092      $i = (int) $i;
2093  
2094      return ( 0x9 === $i || 0xa === $i || 0xd === $i ||
2095          ( 0x20 <= $i && $i <= 0xd7ff ) ||
2096          ( 0xe000 <= $i && $i <= 0xfffd ) ||
2097          ( 0x10000 <= $i && $i <= 0x10ffff )
2098      );
2099  }
2100  
2101  /**
2102   * Converts all numeric HTML entities to their named counterparts.
2103   *
2104   * This function decodes numeric HTML entities (`&#65;` and `&#x41;`).
2105   * It doesn't do anything with named entities like `&auml;`, but we don't
2106   * need them in the allowed URL protocols system anyway.
2107   *
2108   * @since 1.0.0
2109   *
2110   * @param string $content Content to change entities.
2111   * @return string Content after decoded entities.
2112   */
2113  function wp_kses_decode_entities( $content ) {
2114      $content = preg_replace_callback( '/&#([0-9]+);/', '_wp_kses_decode_entities_chr', $content );
2115      $content = preg_replace_callback( '/&#[Xx]([0-9A-Fa-f]+);/', '_wp_kses_decode_entities_chr_hexdec', $content );
2116  
2117      return $content;
2118  }
2119  
2120  /**
2121   * Regex callback for `wp_kses_decode_entities()`.
2122   *
2123   * @since 2.9.0
2124   * @access private
2125   * @ignore
2126   *
2127   * @param array $matches preg match
2128   * @return string
2129   */
2130  function _wp_kses_decode_entities_chr( $matches ) {
2131      return chr( $matches[1] );
2132  }
2133  
2134  /**
2135   * Regex callback for `wp_kses_decode_entities()`.
2136   *
2137   * @since 2.9.0
2138   * @access private
2139   * @ignore
2140   *
2141   * @param array $matches preg match
2142   * @return string
2143   */
2144  function _wp_kses_decode_entities_chr_hexdec( $matches ) {
2145      return chr( hexdec( $matches[1] ) );
2146  }
2147  
2148  /**
2149   * Sanitize content with allowed HTML KSES rules.
2150   *
2151   * This function expects slashed data.
2152   *
2153   * @since 1.0.0
2154   *
2155   * @param string $data Content to filter, expected to be escaped with slashes.
2156   * @return string Filtered content.
2157   */
2158  function wp_filter_kses( $data ) {
2159      return addslashes( wp_kses( stripslashes( $data ), current_filter() ) );
2160  }
2161  
2162  /**
2163   * Sanitize content with allowed HTML KSES rules.
2164   *
2165   * This function expects unslashed data.
2166   *
2167   * @since 2.9.0
2168   *
2169   * @param string $data Content to filter, expected to not be escaped.
2170   * @return string Filtered content.
2171   */
2172  function wp_kses_data( $data ) {
2173      return wp_kses( $data, current_filter() );
2174  }
2175  
2176  /**
2177   * Sanitizes content for allowed HTML tags for post content.
2178   *
2179   * Post content refers to the page contents of the 'post' type and not `$_POST`
2180   * data from forms.
2181   *
2182   * This function expects slashed data.
2183   *
2184   * @since 2.0.0
2185   *
2186   * @param string $data Post content to filter, expected to be escaped with slashes.
2187   * @return string Filtered post content with allowed HTML tags and attributes intact.
2188   */
2189  function wp_filter_post_kses( $data ) {
2190      return addslashes( wp_kses( stripslashes( $data ), 'post' ) );
2191  }
2192  
2193  /**
2194   * Sanitizes global styles user content removing unsafe rules.
2195   *
2196   * @since 5.9.0
2197   *
2198   * @param string $data Post content to filter.
2199   * @return string Filtered post content with unsafe rules removed.
2200   */
2201  function wp_filter_global_styles_post( $data ) {
2202      $decoded_data        = json_decode( wp_unslash( $data ), true );
2203      $json_decoding_error = json_last_error();
2204      if (
2205          JSON_ERROR_NONE === $json_decoding_error &&
2206          is_array( $decoded_data ) &&
2207          isset( $decoded_data['isGlobalStylesUserThemeJSON'] ) &&
2208          $decoded_data['isGlobalStylesUserThemeJSON']
2209      ) {
2210          unset( $decoded_data['isGlobalStylesUserThemeJSON'] );
2211  
2212          $data_to_encode = WP_Theme_JSON::remove_insecure_properties( $decoded_data, 'custom' );
2213  
2214          $data_to_encode['isGlobalStylesUserThemeJSON'] = true;
2215          return wp_slash( wp_json_encode( $data_to_encode ) );
2216      }
2217      return $data;
2218  }
2219  
2220  /**
2221   * Sanitizes content for allowed HTML tags for post content.
2222   *
2223   * Post content refers to the page contents of the 'post' type and not `$_POST`
2224   * data from forms.
2225   *
2226   * This function expects unslashed data.
2227   *
2228   * @since 2.9.0
2229   *
2230   * @param string $data Post content to filter.
2231   * @return string Filtered post content with allowed HTML tags and attributes intact.
2232   */
2233  function wp_kses_post( $data ) {
2234      return wp_kses( $data, 'post' );
2235  }
2236  
2237  /**
2238   * Navigates through an array, object, or scalar, and sanitizes content for
2239   * allowed HTML tags for post content.
2240   *
2241   * @since 4.4.2
2242   *
2243   * @see map_deep()
2244   *
2245   * @param mixed $data The array, object, or scalar value to inspect.
2246   * @return mixed The filtered content.
2247   */
2248  function wp_kses_post_deep( $data ) {
2249      return map_deep( $data, 'wp_kses_post' );
2250  }
2251  
2252  /**
2253   * Strips all HTML from a text string.
2254   *
2255   * This function expects slashed data.
2256   *
2257   * @since 2.1.0
2258   *
2259   * @param string $data Content to strip all HTML from.
2260   * @return string Filtered content without any HTML.
2261   */
2262  function wp_filter_nohtml_kses( $data ) {
2263      return addslashes( wp_kses( stripslashes( $data ), 'strip' ) );
2264  }
2265  
2266  /**
2267   * Adds all KSES input form content filters.
2268   *
2269   * All hooks have default priority. The `wp_filter_kses()` function is added to
2270   * the 'pre_comment_content' and 'title_save_pre' hooks.
2271   *
2272   * The `wp_filter_post_kses()` function is added to the 'content_save_pre',
2273   * 'excerpt_save_pre', and 'content_filtered_save_pre' hooks.
2274   *
2275   * @since 2.0.0
2276   */
2277  function kses_init_filters() {
2278      // Normal filtering.
2279      add_filter( 'title_save_pre', 'wp_filter_kses' );
2280  
2281      // Comment filtering.
2282      if ( current_user_can( 'unfiltered_html' ) ) {
2283          add_filter( 'pre_comment_content', 'wp_filter_post_kses' );
2284      } else {
2285          add_filter( 'pre_comment_content', 'wp_filter_kses' );
2286      }
2287  
2288      // Global Styles filtering: Global Styles filters should be executed before normal post_kses HTML filters.
2289      add_filter( 'content_save_pre', 'wp_filter_global_styles_post', 9 );
2290      add_filter( 'content_filtered_save_pre', 'wp_filter_global_styles_post', 9 );
2291  
2292      // Post filtering.
2293      add_filter( 'content_save_pre', 'wp_filter_post_kses' );
2294      add_filter( 'excerpt_save_pre', 'wp_filter_post_kses' );
2295      add_filter( 'content_filtered_save_pre', 'wp_filter_post_kses' );
2296  }
2297  
2298  /**
2299   * Removes all KSES input form content filters.
2300   *
2301   * A quick procedural method to removing all of the filters that KSES uses for
2302   * content in WordPress Loop.
2303   *
2304   * Does not remove the `kses_init()` function from {@see 'init'} hook (priority is
2305   * default). Also does not remove `kses_init()` function from {@see 'set_current_user'}
2306   * hook (priority is also default).
2307   *
2308   * @since 2.0.6
2309   */
2310  function kses_remove_filters() {
2311      // Normal filtering.
2312      remove_filter( 'title_save_pre', 'wp_filter_kses' );
2313  
2314      // Comment filtering.
2315      remove_filter( 'pre_comment_content', 'wp_filter_post_kses' );
2316      remove_filter( 'pre_comment_content', 'wp_filter_kses' );
2317  
2318      // Global Styles filtering.
2319      remove_filter( 'content_save_pre', 'wp_filter_global_styles_post', 9 );
2320      remove_filter( 'content_filtered_save_pre', 'wp_filter_global_styles_post', 9 );
2321  
2322      // Post filtering.
2323      remove_filter( 'content_save_pre', 'wp_filter_post_kses' );
2324      remove_filter( 'excerpt_save_pre', 'wp_filter_post_kses' );
2325      remove_filter( 'content_filtered_save_pre', 'wp_filter_post_kses' );
2326  }
2327  
2328  /**
2329   * Sets up most of the KSES filters for input form content.
2330   *
2331   * First removes all of the KSES filters in case the current user does not need
2332   * to have KSES filter the content. If the user does not have `unfiltered_html`
2333   * capability, then KSES filters are added.
2334   *
2335   * @since 2.0.0
2336   */
2337  function kses_init() {
2338      kses_remove_filters();
2339  
2340      if ( ! current_user_can( 'unfiltered_html' ) ) {
2341          kses_init_filters();
2342      }
2343  }
2344  
2345  /**
2346   * Filters an inline style attribute and removes disallowed rules.
2347   *
2348   * @since 2.8.1
2349   * @since 4.4.0 Added support for `min-height`, `max-height`, `min-width`, and `max-width`.
2350   * @since 4.6.0 Added support for `list-style-type`.
2351   * @since 5.0.0 Added support for `background-image`.
2352   * @since 5.1.0 Added support for `text-transform`.
2353   * @since 5.2.0 Added support for `background-position` and `grid-template-columns`.
2354   * @since 5.3.0 Added support for `grid`, `flex` and `column` layout properties.
2355   *              Extended `background-*` support for individual properties.
2356   * @since 5.3.1 Added support for gradient backgrounds.
2357   * @since 5.7.1 Added support for `object-position`.
2358   * @since 5.8.0 Added support for `calc()` and `var()` values.
2359   * @since 6.1.0 Added support for `min()`, `max()`, `minmax()`, `clamp()`,
2360   *              nested `var()` values, and assigning values to CSS variables.
2361   *              Added support for `object-fit`, `gap`, `column-gap`, `row-gap`, and `flex-wrap`.
2362   *              Extended `margin-*` and `padding-*` support for logical properties.
2363   * @since 6.2.0 Added support for `aspect-ratio`, `position`, `top`, `right`, `bottom`, `left`,
2364   *              and `z-index` CSS properties.
2365   * @since 6.3.0 Extended support for `filter` to accept a URL and added support for repeat().
2366   *              Added support for `box-shadow`.
2367   * @since 6.4.0 Added support for `writing-mode`.
2368   * @since 6.5.0 Added support for `background-repeat`.
2369   * @since 6.6.0 Added support for `grid-column`, `grid-row`, and `container-type`.
2370   *
2371   * @param string $css        A string of CSS rules.
2372   * @param string $deprecated Not used.
2373   * @return string Filtered string of CSS rules.
2374   */
2375  function safecss_filter_attr( $css, $deprecated = '' ) {
2376      if ( ! empty( $deprecated ) ) {
2377          _deprecated_argument( __FUNCTION__, '2.8.1' ); // Never implemented.
2378      }
2379  
2380      $css = wp_kses_no_null( $css );
2381      $css = str_replace( array( "\n", "\r", "\t" ), '', $css );
2382  
2383      $allowed_protocols = wp_allowed_protocols();
2384  
2385      $css_array = explode( ';', trim( $css ) );
2386  
2387      /**
2388       * Filters the list of allowed CSS attributes.
2389       *
2390       * @since 2.8.1
2391       *
2392       * @param string[] $attr Array of allowed CSS attributes.
2393       */
2394      $allowed_attr = apply_filters(
2395          'safe_style_css',
2396          array(
2397              'background',
2398              'background-color',
2399              'background-image',
2400              'background-position',
2401              'background-repeat',
2402              'background-size',
2403              'background-attachment',
2404              'background-blend-mode',
2405  
2406              'border',
2407              'border-radius',
2408              'border-width',
2409              'border-color',
2410              'border-style',
2411              'border-right',
2412              'border-right-color',
2413              'border-right-style',
2414              'border-right-width',
2415              'border-bottom',
2416              'border-bottom-color',
2417              'border-bottom-left-radius',
2418              'border-bottom-right-radius',
2419              'border-bottom-style',
2420              'border-bottom-width',
2421              'border-bottom-right-radius',
2422              'border-bottom-left-radius',
2423              'border-left',
2424              'border-left-color',
2425              'border-left-style',
2426              'border-left-width',
2427              'border-top',
2428              'border-top-color',
2429              'border-top-left-radius',
2430              'border-top-right-radius',
2431              'border-top-style',
2432              'border-top-width',
2433              'border-top-left-radius',
2434              'border-top-right-radius',
2435  
2436              'border-spacing',
2437              'border-collapse',
2438              'caption-side',
2439  
2440              'columns',
2441              'column-count',
2442              'column-fill',
2443              'column-gap',
2444              'column-rule',
2445              'column-span',
2446              'column-width',
2447  
2448              'color',
2449              'filter',
2450              'font',
2451              'font-family',
2452              'font-size',
2453              'font-style',
2454              'font-variant',
2455              'font-weight',
2456              'letter-spacing',
2457              'line-height',
2458              'text-align',
2459              'text-decoration',
2460              'text-indent',
2461              'text-transform',
2462  
2463              'height',
2464              'min-height',
2465              'max-height',
2466  
2467              'width',
2468              'min-width',
2469              'max-width',
2470  
2471              'margin',
2472              'margin-right',
2473              'margin-bottom',
2474              'margin-left',
2475              'margin-top',
2476              'margin-block-start',
2477              'margin-block-end',
2478              'margin-inline-start',
2479              'margin-inline-end',
2480  
2481              'padding',
2482              'padding-right',
2483              'padding-bottom',
2484              'padding-left',
2485              'padding-top',
2486              'padding-block-start',
2487              'padding-block-end',
2488              'padding-inline-start',
2489              'padding-inline-end',
2490  
2491              'flex',
2492              'flex-basis',
2493              'flex-direction',
2494              'flex-flow',
2495              'flex-grow',
2496              'flex-shrink',
2497              'flex-wrap',
2498  
2499              'gap',
2500              'column-gap',
2501              'row-gap',
2502  
2503              'grid-template-columns',
2504              'grid-auto-columns',
2505              'grid-column-start',
2506              'grid-column-end',
2507              'grid-column',
2508              'grid-column-gap',
2509              'grid-template-rows',
2510              'grid-auto-rows',
2511              'grid-row-start',
2512              'grid-row-end',
2513              'grid-row',
2514              'grid-row-gap',
2515              'grid-gap',
2516  
2517              'justify-content',
2518              'justify-items',
2519              'justify-self',
2520              'align-content',
2521              'align-items',
2522              'align-self',
2523  
2524              'clear',
2525              'cursor',
2526              'direction',
2527              'float',
2528              'list-style-type',
2529              'object-fit',
2530              'object-position',
2531              'opacity',
2532              'overflow',
2533              'vertical-align',
2534              'writing-mode',
2535  
2536              'position',
2537              'top',
2538              'right',
2539              'bottom',
2540              'left',
2541              'z-index',
2542              'box-shadow',
2543              'aspect-ratio',
2544              'container-type',
2545  
2546              // Custom CSS properties.
2547              '--*',
2548          )
2549      );
2550  
2551      /*
2552       * CSS attributes that accept URL data types.
2553       *
2554       * This is in accordance to the CSS spec and unrelated to
2555       * the sub-set of supported attributes above.
2556       *
2557       * See: https://developer.mozilla.org/en-US/docs/Web/CSS/url
2558       */
2559      $css_url_data_types = array(
2560          'background',
2561          'background-image',
2562  
2563          'cursor',
2564          'filter',
2565  
2566          'list-style',
2567          'list-style-image',
2568      );
2569  
2570      /*
2571       * CSS attributes that accept gradient data types.
2572       *
2573       */
2574      $css_gradient_data_types = array(
2575          'background',
2576          'background-image',
2577      );
2578  
2579      if ( empty( $allowed_attr ) ) {
2580          return $css;
2581      }
2582  
2583      $css = '';
2584      foreach ( $css_array as $css_item ) {
2585          if ( '' === $css_item ) {
2586              continue;
2587          }
2588  
2589          $css_item        = trim( $css_item );
2590          $css_test_string = $css_item;
2591          $found           = false;
2592          $url_attr        = false;
2593          $gradient_attr   = false;
2594          $is_custom_var   = false;
2595  
2596          if ( ! str_contains( $css_item, ':' ) ) {
2597              $found = true;
2598          } else {
2599              $parts        = explode( ':', $css_item, 2 );
2600              $css_selector = trim( $parts[0] );
2601  
2602              // Allow assigning values to CSS variables.
2603              if ( in_array( '--*', $allowed_attr, true ) && preg_match( '/^--[a-zA-Z0-9-_]+$/', $css_selector ) ) {
2604                  $allowed_attr[] = $css_selector;
2605                  $is_custom_var  = true;
2606              }
2607  
2608              if ( in_array( $css_selector, $allowed_attr, true ) ) {
2609                  $found         = true;
2610                  $url_attr      = in_array( $css_selector, $css_url_data_types, true );
2611                  $gradient_attr = in_array( $css_selector, $css_gradient_data_types, true );
2612              }
2613  
2614              if ( $is_custom_var ) {
2615                  $css_value     = trim( $parts[1] );
2616                  $url_attr      = str_starts_with( $css_value, 'url(' );
2617                  $gradient_attr = str_contains( $css_value, '-gradient(' );
2618              }
2619          }
2620  
2621          if ( $found && $url_attr ) {
2622              // Simplified: matches the sequence `url(*)`.
2623              preg_match_all( '/url\([^)]+\)/', $parts[1], $url_matches );
2624  
2625              foreach ( $url_matches[0] as $url_match ) {
2626                  // Clean up the URL from each of the matches above.
2627                  preg_match( '/^url\(\s*([\'\"]?)(.*)(\g1)\s*\)$/', $url_match, $url_pieces );
2628  
2629                  if ( empty( $url_pieces[2] ) ) {
2630                      $found = false;
2631                      break;
2632                  }
2633  
2634                  $url = trim( $url_pieces[2] );
2635  
2636                  if ( empty( $url ) || wp_kses_bad_protocol( $url, $allowed_protocols ) !== $url ) {
2637                      $found = false;
2638                      break;
2639                  } else {
2640                      // Remove the whole `url(*)` bit that was matched above from the CSS.
2641                      $css_test_string = str_replace( $url_match, '', $css_test_string );
2642                  }
2643              }
2644          }
2645  
2646          if ( $found && $gradient_attr ) {
2647              $css_value = trim( $parts[1] );
2648              if ( preg_match( '/^(repeating-)?(linear|radial|conic)-gradient\(([^()]|rgb[a]?\([^()]*\))*\)$/', $css_value ) ) {
2649                  // Remove the whole `gradient` bit that was matched above from the CSS.
2650                  $css_test_string = str_replace( $css_value, '', $css_test_string );
2651              }
2652          }
2653  
2654          if ( $found ) {
2655              /*
2656               * Allow CSS functions like var(), calc(), etc. by removing them from the test string.
2657               * Nested functions and parentheses are also removed, so long as the parentheses are balanced.
2658               */
2659              $css_test_string = preg_replace(
2660                  '/\b(?:var|calc|min|max|minmax|clamp|repeat)(\((?:[^()]|(?1))*\))/',
2661                  '',
2662                  $css_test_string
2663              );
2664  
2665              /*
2666               * Disallow CSS containing \ ( & } = or comments, except for within url(), var(), calc(), etc.
2667               * which were removed from the test string above.
2668               */
2669              $allow_css = ! preg_match( '%[\\\(&=}]|/\*%', $css_test_string );
2670  
2671              /**
2672               * Filters the check for unsafe CSS in `safecss_filter_attr`.
2673               *
2674               * Enables developers to determine whether a section of CSS should be allowed or discarded.
2675               * By default, the value will be false if the part contains \ ( & } = or comments.
2676               * Return true to allow the CSS part to be included in the output.
2677               *
2678               * @since 5.5.0
2679               *
2680               * @param bool   $allow_css       Whether the CSS in the test string is considered safe.
2681               * @param string $css_test_string The CSS string to test.
2682               */
2683              $allow_css = apply_filters( 'safecss_filter_attr_allow_css', $allow_css, $css_test_string );
2684  
2685              // Only add the CSS part if it passes the regex check.
2686              if ( $allow_css ) {
2687                  if ( '' !== $css ) {
2688                      $css .= ';';
2689                  }
2690  
2691                  $css .= $css_item;
2692              }
2693          }
2694      }
2695  
2696      return $css;
2697  }
2698  
2699  /**
2700   * Helper function to add global attributes to a tag in the allowed HTML list.
2701   *
2702   * @since 3.5.0
2703   * @since 5.0.0 Added support for `data-*` wildcard attributes.
2704   * @since 6.0.0 Added `dir`, `lang`, and `xml:lang` to global attributes.
2705   * @since 6.3.0 Added `aria-controls`, `aria-current`, and `aria-expanded` attributes.
2706   * @since 6.4.0 Added `aria-live` and `hidden` attributes.
2707   *
2708   * @access private
2709   * @ignore
2710   *
2711   * @param array $value An array of attributes.
2712   * @return array The array of attributes with global attributes added.
2713   */
2714  function _wp_add_global_attributes( $value ) {
2715      $global_attributes = array(
2716          'aria-controls'    => true,
2717          'aria-current'     => true,
2718          'aria-describedby' => true,
2719          'aria-details'     => true,
2720          'aria-expanded'    => true,
2721          'aria-hidden'      => true,
2722          'aria-label'       => true,
2723          'aria-labelledby'  => true,
2724          'aria-live'        => true,
2725          'class'            => true,
2726          'data-*'           => true,
2727          'dir'              => true,
2728          'hidden'           => true,
2729          'id'               => true,
2730          'lang'             => true,
2731          'style'            => true,
2732          'title'            => true,
2733          'role'             => true,
2734          'xml:lang'         => true,
2735      );
2736  
2737      if ( true === $value ) {
2738          $value = array();
2739      }
2740  
2741      if ( is_array( $value ) ) {
2742          return array_merge( $value, $global_attributes );
2743      }
2744  
2745      return $value;
2746  }
2747  
2748  /**
2749   * Helper function to check if this is a safe PDF URL.
2750   *
2751   * @since 5.9.0
2752   * @access private
2753   * @ignore
2754   *
2755   * @param string $url The URL to check.
2756   * @return bool True if the URL is safe, false otherwise.
2757   */
2758  function _wp_kses_allow_pdf_objects( $url ) {
2759      // We're not interested in URLs that contain query strings or fragments.
2760      if ( str_contains( $url, '?' ) || str_contains( $url, '#' ) ) {
2761          return false;
2762      }
2763  
2764      // If it doesn't have a PDF extension, it's not safe.
2765      if ( ! str_ends_with( $url, '.pdf' ) ) {
2766          return false;
2767      }
2768  
2769      // If the URL host matches the current site's media URL, it's safe.
2770      $upload_info = wp_upload_dir( null, false );
2771      $parsed_url  = wp_parse_url( $upload_info['url'] );
2772      $upload_host = isset( $parsed_url['host'] ) ? $parsed_url['host'] : '';
2773      $upload_port = isset( $parsed_url['port'] ) ? ':' . $parsed_url['port'] : '';
2774  
2775      if ( str_starts_with( $url, "http://$upload_host$upload_port/" )
2776          || str_starts_with( $url, "https://$upload_host$upload_port/" )
2777      ) {
2778          return true;
2779      }
2780  
2781      return false;
2782  }


Generated : Fri Feb 21 08:20:01 2025 Cross-referenced by PHPXref