[ 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  /**
  60   * Indicates which implementation of {@see \wp_kses()} is running.
  61   *
  62   * Nominally `legacy` unless temporarily-switched for {@see \wp_sanitize_html_kses()}.
  63   * It’s safe to latch this into `legacy`.
  64   *
  65   * @global 'legacy'|'html-api' $wp_kses_operating_mode
  66   *
  67   * @since 7.2.0
  68   */
  69  global $wp_kses_operating_mode;
  70  $wp_kses_operating_mode = 'legacy';
  71  
  72  if ( ! CUSTOM_TAGS ) {
  73      /**
  74       * KSES global for default allowable HTML tags.
  75       *
  76       * Can be overridden with the `CUSTOM_TAGS` constant.
  77       *
  78       * @var array[] $allowedposttags Array of default allowable HTML tags.
  79       * @since 2.0.0
  80       */
  81      $allowedposttags = array(
  82          'address'    => array(),
  83          'a'          => array(
  84              'href'     => true,
  85              'rel'      => true,
  86              'rev'      => true,
  87              'name'     => true,
  88              'target'   => true,
  89              'download' => array(
  90                  'valueless' => 'y',
  91              ),
  92          ),
  93          'abbr'       => array(),
  94          'acronym'    => array(),
  95          'area'       => array(
  96              'alt'    => true,
  97              'coords' => true,
  98              'href'   => true,
  99              'nohref' => true,
 100              'shape'  => true,
 101              'target' => true,
 102          ),
 103          'article'    => array(
 104              'align' => true,
 105          ),
 106          'aside'      => array(
 107              'align' => true,
 108          ),
 109          'audio'      => array(
 110              'autoplay' => true,
 111              'controls' => true,
 112              'loop'     => true,
 113              'muted'    => true,
 114              'preload'  => true,
 115              'src'      => true,
 116          ),
 117          'b'          => array(),
 118          'bdo'        => array(),
 119          'big'        => array(),
 120          'blockquote' => array(
 121              'cite' => true,
 122          ),
 123          'br'         => array(),
 124          'button'     => array(
 125              'command'             => true,
 126              'commandfor'          => true,
 127              'disabled'            => true,
 128              'name'                => true,
 129              'type'                => true,
 130              'value'               => true,
 131              'popovertarget'       => true,
 132              'popovertargetaction' => true,
 133              'aria-haspopup'       => true,
 134          ),
 135          'caption'    => array(
 136              'align' => true,
 137          ),
 138          'cite'       => array(),
 139          'code'       => array(),
 140          'col'        => array(
 141              'align'   => true,
 142              'char'    => true,
 143              'charoff' => true,
 144              'span'    => true,
 145              'valign'  => true,
 146              'width'   => true,
 147          ),
 148          'colgroup'   => array(
 149              'align'   => true,
 150              'char'    => true,
 151              'charoff' => true,
 152              'span'    => true,
 153              'valign'  => true,
 154              'width'   => true,
 155          ),
 156          'data'       => array(
 157              'value' => true,
 158          ),
 159          'del'        => array(
 160              'datetime' => true,
 161          ),
 162          'dd'         => array(),
 163          'dfn'        => array(),
 164          'details'    => array(
 165              'align' => true,
 166              'open'  => true,
 167              'name'  => true,
 168          ),
 169          'div'        => array(
 170              'align'   => true,
 171              'popover' => true,
 172          ),
 173          'dialog'     => array(
 174              'closedby'  => true,
 175              'open'      => true,
 176              'popover'   => true,
 177              'autofocus' => true,
 178          ),
 179          'dl'         => array(),
 180          'dt'         => array(),
 181          'em'         => array(),
 182          'fieldset'   => array(),
 183          'figure'     => array(
 184              'align' => true,
 185          ),
 186          'figcaption' => array(
 187              'align' => true,
 188          ),
 189          'font'       => array(
 190              'color' => true,
 191              'face'  => true,
 192              'size'  => true,
 193          ),
 194          'footer'     => array(
 195              'align' => true,
 196          ),
 197          'h1'         => array(
 198              'align' => true,
 199          ),
 200          'h2'         => array(
 201              'align' => true,
 202          ),
 203          'h3'         => array(
 204              'align' => true,
 205          ),
 206          'h4'         => array(
 207              'align' => true,
 208          ),
 209          'h5'         => array(
 210              'align' => true,
 211          ),
 212          'h6'         => array(
 213              'align' => true,
 214          ),
 215          'header'     => array(
 216              'align' => true,
 217          ),
 218          'hgroup'     => array(
 219              'align' => true,
 220          ),
 221          'hr'         => array(
 222              'align'   => true,
 223              'noshade' => true,
 224              'size'    => true,
 225              'width'   => true,
 226          ),
 227          'i'          => array(),
 228          'img'        => array(
 229              'alt'      => true,
 230              'align'    => true,
 231              'border'   => true,
 232              'height'   => true,
 233              'hspace'   => true,
 234              'loading'  => true,
 235              'longdesc' => true,
 236              'vspace'   => true,
 237              'src'      => true,
 238              'usemap'   => true,
 239              'width'    => true,
 240          ),
 241          'ins'        => array(
 242              'datetime' => true,
 243              'cite'     => true,
 244          ),
 245          'kbd'        => array(),
 246          'label'      => array(
 247              'for' => true,
 248          ),
 249          'legend'     => array(
 250              'align' => true,
 251          ),
 252          'li'         => array(
 253              'align' => true,
 254              'value' => true,
 255          ),
 256          'main'       => array(
 257              'align' => true,
 258          ),
 259          'map'        => array(
 260              'name' => true,
 261          ),
 262          'mark'       => array(),
 263          'menu'       => array(
 264              'type' => true,
 265          ),
 266          'meter'      => array(
 267              'high'    => true,
 268              'low'     => true,
 269              'max'     => true,
 270              'min'     => true,
 271              'optimum' => true,
 272              'value'   => true,
 273          ),
 274          'nav'        => array(
 275              'align' => true,
 276          ),
 277          'object'     => array(
 278              'data' => array(
 279                  'required'       => true,
 280                  'value_callback' => '_wp_kses_allow_pdf_objects',
 281              ),
 282              'type' => array(
 283                  'required' => true,
 284                  'values'   => array( 'application/pdf' ),
 285              ),
 286          ),
 287          'p'          => array(
 288              'align' => true,
 289          ),
 290          'pre'        => array(
 291              'width' => true,
 292          ),
 293          'progress'   => array(
 294              'max'   => true,
 295              'value' => true,
 296          ),
 297          'q'          => array(
 298              'cite' => true,
 299          ),
 300          'rb'         => array(),
 301          'rp'         => array(),
 302          'rt'         => array(),
 303          'rtc'        => array(),
 304          'ruby'       => array(),
 305          's'          => array(),
 306          'samp'       => array(),
 307          'search'     => array(),
 308          'span'       => array(
 309              'align' => true,
 310          ),
 311          'section'    => array(
 312              'align' => true,
 313          ),
 314          'small'      => array(),
 315          'strike'     => array(),
 316          'strong'     => array(),
 317          'sub'        => array(),
 318          'summary'    => array(
 319              'align' => true,
 320          ),
 321          'sup'        => array(),
 322          'table'      => array(
 323              'align'       => true,
 324              'bgcolor'     => true,
 325              'border'      => true,
 326              'cellpadding' => true,
 327              'cellspacing' => true,
 328              'rules'       => true,
 329              'summary'     => true,
 330              'width'       => true,
 331          ),
 332          'tbody'      => array(
 333              'align'   => true,
 334              'char'    => true,
 335              'charoff' => true,
 336              'valign'  => true,
 337          ),
 338          'td'         => array(
 339              'abbr'    => true,
 340              'align'   => true,
 341              'axis'    => true,
 342              'bgcolor' => true,
 343              'char'    => true,
 344              'charoff' => true,
 345              'colspan' => true,
 346              'headers' => true,
 347              'height'  => true,
 348              'nowrap'  => true,
 349              'rowspan' => true,
 350              'scope'   => true,
 351              'valign'  => true,
 352              'width'   => true,
 353          ),
 354          'textarea'   => array(
 355              'cols'     => true,
 356              'rows'     => true,
 357              'disabled' => true,
 358              'name'     => true,
 359              'readonly' => true,
 360          ),
 361          'tfoot'      => array(
 362              'align'   => true,
 363              'char'    => true,
 364              'charoff' => true,
 365              'valign'  => true,
 366          ),
 367          'th'         => array(
 368              'abbr'    => true,
 369              'align'   => true,
 370              'axis'    => true,
 371              'bgcolor' => true,
 372              'char'    => true,
 373              'charoff' => true,
 374              'colspan' => true,
 375              'headers' => true,
 376              'height'  => true,
 377              'nowrap'  => true,
 378              'rowspan' => true,
 379              'scope'   => true,
 380              'valign'  => true,
 381              'width'   => true,
 382          ),
 383          'thead'      => array(
 384              'align'   => true,
 385              'char'    => true,
 386              'charoff' => true,
 387              'valign'  => true,
 388          ),
 389          'time'       => array(
 390              'datetime' => true,
 391          ),
 392          'title'      => array(),
 393          'tr'         => array(
 394              'align'   => true,
 395              'bgcolor' => true,
 396              'char'    => true,
 397              'charoff' => true,
 398              'valign'  => true,
 399          ),
 400          'track'      => array(
 401              'default' => true,
 402              'kind'    => true,
 403              'label'   => true,
 404              'src'     => true,
 405              'srclang' => true,
 406          ),
 407          'tt'         => array(),
 408          'u'          => array(),
 409          'ul'         => array(
 410              'type'    => true,
 411              'popover' => true,
 412              'role'    => true,
 413          ),
 414          'ol'         => array(
 415              'start'    => true,
 416              'type'     => true,
 417              'reversed' => true,
 418          ),
 419          'var'        => array(),
 420          'video'      => array(
 421              'autoplay'    => true,
 422              'controls'    => true,
 423              'height'      => true,
 424              'loop'        => true,
 425              'muted'       => true,
 426              'playsinline' => true,
 427              'poster'      => true,
 428              'preload'     => true,
 429              'src'         => true,
 430              'width'       => true,
 431          ),
 432          'wbr'        => array(),
 433      );
 434  
 435      // https://www.w3.org/TR/mathml-core/#global-attributes
 436      // Except common attributes added by _wp_add_global_attributes.
 437      $math_global_attributes = array(
 438          'displaystyle'   => true,
 439          'scriptlevel'    => true,
 440          'mathbackground' => true,
 441          'mathcolor'      => true,
 442          'mathsize'       => true,
 443          // Common attributes also defined by _wp_add_global_attributes.
 444          // We do not want to add all those global attributes though.
 445          'class'          => true,
 446          'data-*'         => true,
 447          'dir'            => true,
 448          'id'             => true,
 449          'style'          => true,
 450      );
 451  
 452      $math_overunder_attributes = array(
 453          'accentunder' => true,
 454          'accent'      => true,
 455      );
 456  
 457      $allowedposttags = array_merge(
 458          $allowedposttags,
 459          array(
 460              // https://www.w3.org/TR/mathml-core/#the-top-level-math-element
 461              'math'          => array_merge(
 462                  $math_global_attributes,
 463                  array(
 464                      'display' => true,
 465                  )
 466              ),
 467  
 468              // https://www.w3.org/TR/mathml-core/#token-elements
 469              // https://www.w3.org/TR/mathml-core/#text-mtext
 470              'mtext'         => $math_global_attributes,
 471              // https://www.w3.org/TR/mathml-core/#the-mi-element
 472              'mi'            => array_merge(
 473                  $math_global_attributes,
 474                  array(
 475                      'mathvariant' => true,
 476                  )
 477              ),
 478              // https://www.w3.org/TR/mathml-core/#number-mn
 479              'mn'            => $math_global_attributes,
 480              // https://www.w3.org/TR/mathml-core/#operator-fence-separator-or-accent-mo
 481              'mo'            => array_merge(
 482                  $math_global_attributes,
 483                  array(
 484                      'form'          => true,
 485                      'fence'         => true,
 486                      'separator'     => true,
 487                      'lspace'        => true,
 488                      'rspace'        => true,
 489                      'stretchy'      => true,
 490                      'symmetric'     => true,
 491                      'maxsize'       => true,
 492                      'minsize'       => true,
 493                      'largeop'       => true,
 494                      'movablelimits' => true,
 495                  )
 496              ),
 497              // https://www.w3.org/TR/mathml-core/#space-mspace
 498              'mspace'        => array_merge(
 499                  $math_global_attributes,
 500                  array(
 501                      'width'  => true,
 502                      'height' => true,
 503                      'depth'  => true,
 504                  )
 505              ),
 506              // https://www.w3.org/TR/mathml-core/#string-literal-ms
 507              'ms'            => $math_global_attributes,
 508  
 509              // https://www.w3.org/TR/mathml-core/#general-layout-schemata
 510              // https://www.w3.org/TR/mathml-core/#horizontally-group-sub-expressions-mrow
 511              'mrow'          => $math_global_attributes,
 512              // https://www.w3.org/TR/mathml-core/#fractions-mfrac
 513              'mfrac'         => array_merge(
 514                  $math_global_attributes,
 515                  array(
 516                      'linethickness' => true,
 517                  )
 518              ),
 519              // https://www.w3.org/TR/mathml-core/#radicals-msqrt-mroot
 520              'msqrt'         => $math_global_attributes,
 521              'mroot'         => $math_global_attributes,
 522              // https://www.w3.org/TR/mathml-core/#style-change-mstyle
 523              'mstyle'        => $math_global_attributes,
 524              // https://www.w3.org/TR/mathml-core/#error-message-merror
 525              'merror'        => $math_global_attributes,
 526              // https://www.w3.org/TR/mathml-core/#adjust-space-around-content-mpadded
 527              'mpadded'       => array_merge(
 528                  $math_global_attributes,
 529                  array(
 530                      'width'   => true,
 531                      'height'  => true,
 532                      'depth'   => true,
 533                      'lspace'  => true,
 534                      'voffset' => true,
 535                  )
 536              ),
 537              // https://www.w3.org/TR/mathml-core/#making-sub-expressions-invisible-mphantom
 538              'mphantom'      => $math_global_attributes,
 539  
 540              // https://www.w3.org/TR/mathml-core/#script-and-limit-schemata
 541              // https://www.w3.org/TR/mathml-core/#subscripts-and-superscripts-msub-msup-msubsup
 542              'msub'          => $math_global_attributes,
 543              'msup'          => $math_global_attributes,
 544              'msubsup'       => $math_global_attributes,
 545              // https://www.w3.org/TR/mathml-core/#underscripts-and-overscripts-munder-mover-munderover
 546              'munder'        => array_merge( $math_global_attributes, $math_overunder_attributes ),
 547              'mover'         => array_merge( $math_global_attributes, $math_overunder_attributes ),
 548              'munderover'    => array_merge( $math_global_attributes, $math_overunder_attributes ),
 549              // https://www.w3.org/TR/mathml-core/#prescripts-and-tensor-indices-mmultiscripts
 550              'mmultiscripts' => $math_global_attributes,
 551              'mprescripts'   => $math_global_attributes,
 552  
 553              // https://www.w3.org/TR/mathml-core/#tabular-math
 554              // https://www.w3.org/TR/mathml-core/#table-or-matrix-mtable
 555              'mtable'        => array_merge(
 556                  $math_global_attributes,
 557                  array(
 558                      // Non-standard, used by temml/katex.
 559                      // https://developer.mozilla.org/en-US/docs/Web/MathML/Reference/Element/mtable
 560                      'columnalign'   => true,
 561                      'rowspacing'    => true,
 562                      'columnspacing' => true,
 563                      'align'         => true,
 564                      'rowalign'      => true,
 565                      'columnlines'   => true,
 566                      'rowlines'      => true,
 567                      'frame'         => true,
 568                      'framespacing'  => true,
 569                      'width'         => true,
 570                  )
 571              ),
 572              // https://www.w3.org/TR/mathml-core/#row-in-table-or-matrix-mtr
 573              'mtr'           => array_merge(
 574                  $math_global_attributes,
 575                  array(
 576                      // Non-standard, used by temml/katex.
 577                      // https://developer.mozilla.org/en-US/docs/Web/MathML/Reference/Element/mtr
 578                      'columnalign' => true,
 579                      'rowalign'    => true,
 580                  )
 581              ),
 582              // https://www.w3.org/TR/mathml-core/#entry-in-table-or-matrix-mtd
 583              'mtd'           => array_merge(
 584                  $math_global_attributes,
 585                  array(
 586                      'columnspan'  => true,
 587                      'rowspan'     => true,
 588                      // Non-standard, used by temml/katex.
 589                      // https://developer.mozilla.org/en-US/docs/Web/MathML/Reference/Element/mtd
 590                      'columnalign' => true,
 591                      'rowalign'    => true,
 592                  )
 593              ),
 594  
 595              // https://www.w3.org/TR/mathml-core/#semantics-and-presentation
 596              'semantics'     => $math_global_attributes,
 597              'annotation'    => array_merge(
 598                  $math_global_attributes,
 599                  array(
 600                      'encoding' => true,
 601                  )
 602              ),
 603  
 604              // Non-standard but widely supported, used by temml/katex.
 605              'menclose'      => array_merge(
 606                  $math_global_attributes,
 607                  array(
 608                      'notation' => true,
 609                  )
 610              ),
 611          )
 612      );
 613  
 614      /**
 615       * @var array[] $allowedtags Array of KSES allowed HTML elements.
 616       * @since 1.0.0
 617       */
 618      $allowedtags = array(
 619          'a'          => array(
 620              'href'  => true,
 621              'title' => true,
 622          ),
 623          'abbr'       => array(
 624              'title' => true,
 625          ),
 626          'acronym'    => array(
 627              'title' => true,
 628          ),
 629          'b'          => array(),
 630          'blockquote' => array(
 631              'cite' => true,
 632          ),
 633          'cite'       => array(),
 634          'code'       => array(),
 635          'del'        => array(
 636              'datetime' => true,
 637          ),
 638          'em'         => array(),
 639          'i'          => array(),
 640          'q'          => array(
 641              'cite' => true,
 642          ),
 643          's'          => array(),
 644          'strike'     => array(),
 645          'strong'     => array(),
 646      );
 647  
 648      /**
 649       * @var string[] $allowedentitynames Array of KSES allowed HTML entity names.
 650       * @since 1.0.0
 651       */
 652      $allowedentitynames = array(
 653          'nbsp',
 654          'iexcl',
 655          'cent',
 656          'pound',
 657          'curren',
 658          'yen',
 659          'brvbar',
 660          'sect',
 661          'uml',
 662          'copy',
 663          'ordf',
 664          'laquo',
 665          'not',
 666          'shy',
 667          'reg',
 668          'macr',
 669          'deg',
 670          'plusmn',
 671          'acute',
 672          'micro',
 673          'para',
 674          'middot',
 675          'cedil',
 676          'ordm',
 677          'raquo',
 678          'iquest',
 679          'Agrave',
 680          'Aacute',
 681          'Acirc',
 682          'Atilde',
 683          'Auml',
 684          'Aring',
 685          'AElig',
 686          'Ccedil',
 687          'Egrave',
 688          'Eacute',
 689          'Ecirc',
 690          'Euml',
 691          'Igrave',
 692          'Iacute',
 693          'Icirc',
 694          'Iuml',
 695          'ETH',
 696          'Ntilde',
 697          'Ograve',
 698          'Oacute',
 699          'Ocirc',
 700          'Otilde',
 701          'Ouml',
 702          'times',
 703          'Oslash',
 704          'Ugrave',
 705          'Uacute',
 706          'Ucirc',
 707          'Uuml',
 708          'Yacute',
 709          'THORN',
 710          'szlig',
 711          'agrave',
 712          'aacute',
 713          'acirc',
 714          'atilde',
 715          'auml',
 716          'aring',
 717          'aelig',
 718          'ccedil',
 719          'egrave',
 720          'eacute',
 721          'ecirc',
 722          'euml',
 723          'igrave',
 724          'iacute',
 725          'icirc',
 726          'iuml',
 727          'eth',
 728          'ntilde',
 729          'ograve',
 730          'oacute',
 731          'ocirc',
 732          'otilde',
 733          'ouml',
 734          'divide',
 735          'oslash',
 736          'ugrave',
 737          'uacute',
 738          'ucirc',
 739          'uuml',
 740          'yacute',
 741          'thorn',
 742          'yuml',
 743          'quot',
 744          'amp',
 745          'lt',
 746          'gt',
 747          'apos',
 748          'OElig',
 749          'oelig',
 750          'Scaron',
 751          'scaron',
 752          'Yuml',
 753          'circ',
 754          'tilde',
 755          'ensp',
 756          'emsp',
 757          'thinsp',
 758          'zwnj',
 759          'zwj',
 760          'lrm',
 761          'rlm',
 762          'ndash',
 763          'mdash',
 764          'lsquo',
 765          'rsquo',
 766          'sbquo',
 767          'ldquo',
 768          'rdquo',
 769          'bdquo',
 770          'dagger',
 771          'Dagger',
 772          'permil',
 773          'lsaquo',
 774          'rsaquo',
 775          'euro',
 776          'fnof',
 777          'Alpha',
 778          'Beta',
 779          'Gamma',
 780          'Delta',
 781          'Epsilon',
 782          'Zeta',
 783          'Eta',
 784          'Theta',
 785          'Iota',
 786          'Kappa',
 787          'Lambda',
 788          'Mu',
 789          'Nu',
 790          'Xi',
 791          'Omicron',
 792          'Pi',
 793          'Rho',
 794          'Sigma',
 795          'Tau',
 796          'Upsilon',
 797          'Phi',
 798          'Chi',
 799          'Psi',
 800          'Omega',
 801          'alpha',
 802          'beta',
 803          'gamma',
 804          'delta',
 805          'epsilon',
 806          'zeta',
 807          'eta',
 808          'theta',
 809          'iota',
 810          'kappa',
 811          'lambda',
 812          'mu',
 813          'nu',
 814          'xi',
 815          'omicron',
 816          'pi',
 817          'rho',
 818          'sigmaf',
 819          'sigma',
 820          'tau',
 821          'upsilon',
 822          'phi',
 823          'chi',
 824          'psi',
 825          'omega',
 826          'thetasym',
 827          'upsih',
 828          'piv',
 829          'bull',
 830          'hellip',
 831          'prime',
 832          'Prime',
 833          'oline',
 834          'frasl',
 835          'weierp',
 836          'image',
 837          'real',
 838          'trade',
 839          'alefsym',
 840          'larr',
 841          'uarr',
 842          'rarr',
 843          'darr',
 844          'harr',
 845          'crarr',
 846          'lArr',
 847          'uArr',
 848          'rArr',
 849          'dArr',
 850          'hArr',
 851          'forall',
 852          'part',
 853          'exist',
 854          'empty',
 855          'nabla',
 856          'isin',
 857          'notin',
 858          'ni',
 859          'prod',
 860          'sum',
 861          'minus',
 862          'lowast',
 863          'radic',
 864          'prop',
 865          'infin',
 866          'ang',
 867          'and',
 868          'or',
 869          'cap',
 870          'cup',
 871          'int',
 872          'sim',
 873          'cong',
 874          'asymp',
 875          'ne',
 876          'equiv',
 877          'le',
 878          'ge',
 879          'sub',
 880          'sup',
 881          'nsub',
 882          'sube',
 883          'supe',
 884          'oplus',
 885          'otimes',
 886          'perp',
 887          'sdot',
 888          'lceil',
 889          'rceil',
 890          'lfloor',
 891          'rfloor',
 892          'lang',
 893          'rang',
 894          'loz',
 895          'spades',
 896          'clubs',
 897          'hearts',
 898          'diams',
 899          'sup1',
 900          'sup2',
 901          'sup3',
 902          'frac14',
 903          'frac12',
 904          'frac34',
 905          'there4',
 906      );
 907  
 908      /**
 909       * @var string[] $allowedxmlentitynames Array of KSES allowed XML entity names.
 910       * @since 5.5.0
 911       */
 912      $allowedxmlentitynames = array(
 913          'amp',
 914          'lt',
 915          'gt',
 916          'apos',
 917          'quot',
 918      );
 919  
 920      $allowedposttags = array_map( '_wp_add_global_attributes', $allowedposttags );
 921  } else {
 922      $required_kses_globals = array(
 923          'allowedposttags',
 924          'allowedtags',
 925          'allowedentitynames',
 926          'allowedxmlentitynames',
 927      );
 928      $missing_kses_globals  = array();
 929  
 930      foreach ( $required_kses_globals as $global_name ) {
 931          if ( ! isset( $GLOBALS[ $global_name ] ) || ! is_array( $GLOBALS[ $global_name ] ) ) {
 932              $missing_kses_globals[] = '<code>$' . $global_name . '</code>';
 933          }
 934      }
 935  
 936      if ( $missing_kses_globals ) {
 937          _doing_it_wrong(
 938              'wp_kses_allowed_html',
 939              sprintf(
 940                  /* translators: 1: CUSTOM_TAGS, 2: Global variable names. */
 941                  __( 'When using the %1$s constant, make sure to set these globals to an array: %2$s.' ),
 942                  '<code>CUSTOM_TAGS</code>',
 943                  implode( ', ', $missing_kses_globals )
 944              ),
 945              '6.2.0'
 946          );
 947      }
 948  
 949      $allowedtags     = wp_kses_array_lc( $allowedtags );
 950      $allowedposttags = wp_kses_array_lc( $allowedposttags );
 951  }
 952  
 953  /**
 954   * Filters text content and strips out disallowed HTML.
 955   *
 956   * This function makes sure that only the allowed HTML element names, attribute
 957   * names, attribute values, and HTML entities will occur in the given text string.
 958   *
 959   * This function expects unslashed data.
 960   *
 961   * @see wp_kses_post() for specifically filtering post content and fields.
 962   * @see wp_allowed_protocols() for the default allowed protocols in link URLs.
 963   *
 964   * @see 'wp_kses_force_legacy_parser' to opt-out of the new HTML-API-based implementation.
 965   *
 966   * @since 1.0.0
 967   * @since 7.2.0 Rewritten with the HTML API for more reliable parsing.
 968   *
 969   * @global string $wp_kses_operating_mode
 970   *
 971   * @param string         $content           Text content to filter.
 972   * @param array[]|string $allowed_html      An array of allowed HTML elements and attributes,
 973   *                                          or a context name such as 'post'. {@see wp_kses_allowed_html()}
 974   *                                          for the list of accepted context names.
 975   * @param string[]       $allowed_protocols Optional. Array of allowed URL protocols.
 976   *                                          Defaults to the result of wp_allowed_protocols().
 977   * @return string Filtered content containing only the allowed HTML.
 978   */
 979  function wp_kses( $content, $allowed_html, $allowed_protocols = array() ) {
 980      global $wp_kses_operating_mode;
 981  
 982      /**
 983       * Filters whether to rely on the legacy parsing inside `wp_kses()`.
 984       *
 985       * @since 7.2.0
 986       *
 987       * @param bool $force_legacy_parser Whether to force using the legacy parser
 988       *                                  instead of relying on the HTML API.
 989       */
 990      if ( ! apply_filters( 'wp_kses_force_legacy_parser', true ) ) {
 991          return wp_sanitize_html_kses( (string) $content, $allowed_html, $allowed_protocols );
 992      }
 993  
 994      if ( empty( $allowed_protocols ) ) {
 995          $allowed_protocols = wp_allowed_protocols();
 996      }
 997  
 998      $content = wp_kses_no_null( $content, array( 'slash_zero' => 'keep' ) );
 999      $content = wp_kses_normalize_entities( $content );
1000  
1001      // Ensure that the legacy pre-kses filters run by setting the operating mode appropriately.
1002      try {
1003          $previous_kses_mode     = $wp_kses_operating_mode;
1004          $wp_kses_operating_mode = 'legacy';
1005          $content                = wp_kses_hook( $content, $allowed_html, $allowed_protocols );
1006      } finally {
1007          $wp_kses_operating_mode = $previous_kses_mode;
1008      }
1009  
1010      return wp_kses_split( $content, $allowed_html, $allowed_protocols );
1011  }
1012  
1013  /**
1014   * Filters HTML content, sanitizing according to given policies.
1015   *
1016   * Modern implementation of {@see wp_kses()} built with the HTML API.
1017   *
1018   * @since 7.2.0
1019   *
1020   * @global string $wp_kses_operating_mode
1021   *
1022   * @access private This function should not be called directly, as it serves a transitionary
1023   *                 role while replacing the implementation of {@see \wp_kses()}.
1024   *
1025   * @param string         $content           Text content to filter.
1026   * @param array[]|string $allowed_html      An array of allowed HTML elements and attributes,
1027   *                                          or a context name such as 'post'. See wp_kses_allowed_html()
1028   *                                          for the list of accepted context names.
1029   * @param string[]       $allowed_protocols Optional. Array of allowed URL protocols.
1030   *                                          Defaults to the result of wp_allowed_protocols().
1031   * @return string Filtered content containing only the allowed HTML.
1032   */
1033  function wp_sanitize_html_kses( $content, $allowed_html, $allowed_protocols = array() ) {
1034      global $wp_kses_operating_mode;
1035  
1036      $specified_allowed_html = $allowed_html;
1037  
1038      $allowed_protocols = empty( $allowed_protocols )
1039          ? wp_allowed_protocols()
1040          : $allowed_protocols;
1041  
1042      // Preserve legacy behavior of stripping unwanted C0 control characters.
1043      $content = preg_replace( '/[\x01-\x08\x0B\x0C\x0E-\x1F]/', '', $content );
1044  
1045      /*
1046       * Call legacy pre-kses filters that might have been added by plugins.
1047       *
1048       * Also set the operating mode to bypass the pre-filters from the legacy
1049       * implementation of `wp_kses()`, as these filters are now run in-band
1050       * during the processing of the input document.
1051       */
1052      try {
1053          $previous_kses_mode     = $wp_kses_operating_mode;
1054          $wp_kses_operating_mode = 'html-api';
1055          $content                = wp_kses_hook( $content, $specified_allowed_html, $allowed_protocols );
1056      } finally {
1057          $wp_kses_operating_mode = $previous_kses_mode;
1058      }
1059  
1060      $allowed_html = is_array( $allowed_html )
1061          ? $allowed_html
1062          : wp_kses_allowed_html( $allowed_html );
1063  
1064      /*
1065       * The explanation for this call is that “the quoting from `preg_replace(//e)`
1066       * requires” it, but this version of `wp_kses()` doesn’t rely on PCRE functions
1067       * to parse HTML. Given that this corrupts text, it will be skipped.
1068       */
1069      //$content = wp_kses_stripslashes( $content );
1070  
1071      $processor = new class( $content, $specified_allowed_html, $allowed_html, $allowed_protocols, wp_kses_uri_attributes() ) extends WP_HTML_Tag_Processor {
1072          /**
1073           * An array of allowed HTML elements and attributes, or a context name such as 'post'.
1074           *
1075           * It’s important to store this alongside the resolved allowable HTML because some
1076           * filters in some plugins look for the string values, e.g. for “post” instead of
1077           * the resolved array, and apply logic based on that context.
1078           *
1079           * @see wp_kses_allowed_html() for the list of accepted context names.
1080           * @see self::$allowed_html for the resolved array of allowable HTML elements and attributes.
1081           *
1082           * @since 7.2.0
1083           *
1084           * @var array[]|string
1085           */
1086          private $specified_allowed_html;
1087  
1088          /**
1089           * An array of allowed HTML elements and attributes.
1090           *
1091           * This array of allowable HTML elements and attributes is resolved from the value provided
1092           * to the sanitizer function. It’s resolved at the start to avoid repeatedly calling the
1093           * filter stack and array-merging computations. However, it’s still necessary to carry along
1094           * the provided context so that filters expecting the array-or-string version continue to
1095           * operate properly.
1096           *
1097           * @see self::$specified_allowed_html
1098           *
1099           * @since 7.2.0
1100           *
1101           * @var array[]
1102           */
1103          private $allowed_html;
1104  
1105          /**
1106           * Array of allowed URL protocols.
1107           *
1108           * @see \wp_allowed_protocols()
1109           *
1110           * @since 7.2.0
1111           *
1112           * @var string[]
1113           */
1114          private $allowed_protocols;
1115  
1116          /**
1117           * List of attributes whose values are expected to be considered URLs.
1118           *
1119           * @see \wp_kses_uri_attributes()
1120           *
1121           * @since 7.2.0
1122           *
1123           * @var string[]
1124           */
1125          private $uri_attributes;
1126  
1127          /**
1128           * Tracks balanced tags when inside foreign content.
1129           *
1130           * @since 7.2.0
1131           *
1132           * @var string[]
1133           */
1134          private $foreign_content_stack = array();
1135  
1136          /**
1137           * Tracks how deeply into a MathML ANNOTATION-XML element the current token is;
1138           * an optimization to avoid checking up the open-element stack on every token.
1139           *
1140           * @since 7.2.0
1141           *
1142           * @var int
1143           */
1144          private $math_annotation_xml_depth = 0;
1145  
1146          /**
1147           * Constructor function, used internally.
1148           *
1149           * @since 7.2.0
1150           * @access private
1151           *
1152           * @param string         $html                   Content to filter.
1153           * @param array[]|string $specified_allowed_html Context passed into sanitizer.
1154           * @param array[]        $allowed_html           Resolved allowable HTML.
1155           * @param string[]       $allowed_protocols      Resolved allowable protocols.
1156           * @param string[]       $uri_attributes         Resolved URI attributes.
1157           */
1158  		public function __construct( $html, $specified_allowed_html, $allowed_html, $allowed_protocols, $uri_attributes ) {
1159              parent::__construct( $html );
1160  
1161              $this->specified_allowed_html = $specified_allowed_html;
1162              $this->allowed_html           = $allowed_html;
1163              $this->allowed_protocols      = $allowed_protocols;
1164              $this->uri_attributes         = $uri_attributes;
1165          }
1166  
1167          /**
1168           * Returns the raw byte-span of the currently-matched token,
1169           * assuming that the parser is matched on a token.
1170           *
1171           * @since 7.2.0
1172           * @access private
1173           *
1174           * @return WP_HTML_Span|null
1175           */
1176  		private function get_span() {
1177              $this->set_bookmark( 'here' );
1178  
1179              if ( ! isset( $this->bookmarks['here'] ) ) {
1180                  return null;
1181              }
1182  
1183              return $this->bookmarks['here'];
1184          }
1185  
1186          /**
1187           * Sets the value of a given HTML attribute, adjusting URL attributes
1188           * to ensure compatibility with legacy {@see wp_kses()} behavior.
1189           *
1190           * @since 7.2.0
1191           * @access private
1192           *
1193           * @param string      $name  Name of HTML attribute to set.
1194           * @param string|true $value Value to set; `true` for boolean attribute.
1195           * @return bool Whether the attribute was set.
1196           */
1197  		public function set_attribute( $name, $value ): bool {
1198              $lower_name = strtolower( $name );
1199              $is_url_ish = in_array( $lower_name, $this->uri_attributes, true );
1200  
1201              if ( ! $is_url_ish || ! is_string( $value ) ) {
1202                  return parent::set_attribute( $name, $value );
1203              }
1204  
1205              $escaped = strtr(
1206                  $value,
1207                  array(
1208                      '<' => '&lt;',
1209                      '>' => '&gt;',
1210                      '&' => '&amp;',
1211                      '"' => '&quot;',
1212                      "'" => '&apos;',
1213                  )
1214              );
1215  
1216              // Set a benign placeholder to replace below.
1217              if ( ! parent::set_attribute( $name, true ) ) {
1218                  return false;
1219              }
1220  
1221              $this->lexical_updates[ $lower_name ]->text = " {$lower_name}=\"{$escaped}\"";
1222  
1223              return true;
1224          }
1225  
1226          /**
1227           * Indicates if the currently-matched token, given the parsed context, would require more
1228           * complicated parsing rules to proceed without risking escape from foreign content.
1229           *
1230           * @since 7.2.0
1231           * @access private
1232           *
1233           * @param bool $is_inside_mathml_text_integration_point Whether the token is inside a MathML text integration point.
1234           * @param bool $is_inside_svg_html_integration_point    Whether the token is inside an SVG HTML integration point.
1235           * @return bool Whether the current token risks escaping foreign content.
1236           */
1237  		private function could_escape_foreign_content( bool $is_inside_mathml_text_integration_point, bool $is_inside_svg_html_integration_point ): bool {
1238              $token_name   = $this->get_token_name();
1239              $is_closer    = $this->is_tag_closer();
1240              $namespace    = $this->get_namespace();
1241              $self_closing = ! $is_closer && $this->has_self_closing_flag();
1242  
1243              if ( ! $is_closer && $is_inside_svg_html_integration_point ) {
1244                  return true;
1245              }
1246  
1247              /*
1248               * These two elements are excepted in HTML from the normal processing
1249               * rules because they function in similar ways to character data.
1250               *
1251               * > The mglyph element is used to represent non-standard characters or
1252               * > symbols by images; the malignmark element establishes an alignment
1253               * > point for use within table constructs, and is otherwise invisible.
1254               *
1255               * They must contain no elements, so only allow self-closing tags.
1256               */
1257              if ( ! $is_closer && $is_inside_mathml_text_integration_point ) {
1258                  return ! ( $self_closing && ( 'MGLYPH' === $token_name || 'MALIGNMARK' === $token_name ) );
1259              }
1260  
1261              if (
1262                  ! $is_closer &&
1263                  'FONT' === $token_name &&
1264                  (
1265                      null !== $this->get_attribute( 'color' ) ||
1266                      null !== $this->get_attribute( 'face' ) ||
1267                      null !== $this->get_attribute( 'size' )
1268                  )
1269              ) {
1270                  return true;
1271              }
1272  
1273              if (
1274                  ! $is_closer &&
1275                  in_array(
1276                      $token_name,
1277                      array(
1278                          'B',
1279                          'BIG',
1280                          'BLOCKQUOTE',
1281                          'BODY',
1282                          'BR',
1283                          'CENTER',
1284                          'CODE',
1285                          'DD',
1286                          'DIV',
1287                          'DL',
1288                          'DT',
1289                          'EM',
1290                          'EMBED',
1291                          'H1',
1292                          'H2',
1293                          'H3',
1294                          'H4',
1295                          'H5',
1296                          'H6',
1297                          'HEAD',
1298                          'HR',
1299                          'I',
1300                          'IMG',
1301                          'LI',
1302                          'LISTING',
1303                          'MENU',
1304                          'META',
1305                          'NOBR',
1306                          'OL',
1307                          'P',
1308                          'PRE',
1309                          'RUBY',
1310                          'S',
1311                          'SMALL',
1312                          'SPAN',
1313                          'STRONG',
1314                          'STRIKE',
1315                          'SUB',
1316                          'SUP',
1317                          'TABLE',
1318                          'TT',
1319                          'U',
1320                          'UL',
1321                          'VAR',
1322                      ),
1323                      true
1324                  ) ||
1325                  (
1326                      $is_closer &&
1327                      in_array(
1328                          $token_name,
1329                          array(
1330                              'BR',
1331                              'P',
1332                          ),
1333                          true
1334                      )
1335                  )
1336              ) {
1337                  return true;
1338              }
1339  
1340              if ( 'math' === $namespace && ! $is_closer && ! $self_closing ) {
1341                  $encoding = $this->get_attribute( 'encoding' );
1342                  if (
1343                      'ANNOTATION-XML' === $token_name &&
1344                      is_string( $encoding ) &&
1345                      (
1346                          0 === strcasecmp( $encoding, 'text/html' ) ||
1347                          0 === strcasecmp( $encoding, 'application/xhtml+xml' )
1348                      )
1349                  ) {
1350                      return true;
1351                  }
1352  
1353                  /*
1354                   * When SVG becomes a direct descendant of a MathML ANNOTATION-XML,
1355                   * the namespace remains `math` but there could be an SVG element
1356                   * with an HTML integration point. Conservatively reject any child
1357                   * SVG element inside a MathML ANNOTATION-XML to prevent this.
1358                   */
1359                  if ( 'SVG' === $token_name && $this->math_annotation_xml_depth > 0 ) {
1360                      return true;
1361                  }
1362              }
1363  
1364              return false;
1365          }
1366  
1367          /**
1368           * Indicates if a given string contains text that would parse as a block delimiter.
1369           *
1370           * @since 7.2.0
1371           * @access private
1372           *
1373           * @param string $text Text to check for a block comment delimiter.
1374           * @return bool Whether a block comment delimiter of any kind was found in the given string.
1375           */
1376  		private static function contains_a_block_delimiter( string $text ): bool {
1377              if ( '' === $text ) {
1378                  return false;
1379              }
1380  
1381              $processor = new WP_Block_Processor( $text );
1382  
1383              return $processor->next_delimiter();
1384          }
1385  
1386          /**
1387           * Returns a sanitized copy of the input HTML.
1388           *
1389           * @since 7.2.0
1390           * @access private
1391           *
1392           * @return string Sanitized copy of given input HTML.
1393           */
1394  		public function sanitize() {
1395              $template_depth            = 0;
1396              $output                    = '';
1397              $special_newline_at        = PHP_INT_MIN;
1398              $foreign_content_starts_at = PHP_INT_MAX;
1399              $open_blocks               = array();
1400              $open_blocks_at            = array();
1401              $foreign_closed_blocks     = array();
1402  
1403              $is_in_mathml_text_integration_point = false;
1404              $is_in_svg_html_integration_point    = false;
1405  
1406              /**
1407               * These are treated as void elements inside the HTML API
1408               * due to the special handling of their inner text content.
1409               */
1410              $special_atomic_elements = array(
1411                  'IFRAME',
1412                  'NOEMBED',
1413                  'NOFRAMES',
1414                  'SCRIPT',
1415                  'STYLE',
1416                  'TEXTAREA',
1417                  'TITLE',
1418                  'XMP',
1419              );
1420  
1421              while ( $this->next_token() ) {
1422                  $token_name = $this->get_token_name();
1423                  $token_type = $this->get_token_type();
1424                  $namespace  = $this->get_namespace();
1425                  $is_closer  = $this->is_tag_closer();
1426                  $here       = $this->get_span();
1427  
1428                  /*
1429                   * Prevent allowing NOSCRIPT elements whose parsing rules change
1430                   * based on whether the scripting flag is enabled in a browser.
1431                   * Rely on trusted inputs for producing the appropriate NOSCRIPT
1432                   * content, and prevent untrusted inputs from generating it.
1433                   */
1434                  if ( 'NOSCRIPT' === $token_name && ! $is_closer ) {
1435                      break;
1436                  }
1437  
1438                  /*
1439                   * PLAINTEXT is unique in that it changes the tokenization. Everything in
1440                   * a document following the PLAINTEXT opening tag is interpreted as character
1441                   * data, meaning there are no more tags, comments, or anything. Because of
1442                   * this, it is the only element without a closing tag.
1443                   *
1444                   * Because of its unique role, it’s risky to leave in place because very few
1445                   * parsers are going to recognize the tag and properly switch modes for the
1446                   * rest of the document. Instead, the PLAINTEXT tag is removed and its content
1447                   * is re-written as an escaped text node.
1448                   *
1449                   * This changes the structure of the document, because no PLAINTEXT element
1450                   * will appear, and it won’t be possible to style its contents, but the element
1451                   * is discouraged anyway and should not be allowed from untrusted inputs.
1452                   */
1453                  if ( 'PLAINTEXT' === $token_name && 'html' === $namespace && ! $is_closer ) {
1454                      if ( $template_depth > 0 && ! isset( $this->allowed_html['template'] ) ) {
1455                          break;
1456                      }
1457  
1458                      $text = substr( $this->html, $here->start + $here->length );
1459  
1460                      $needs_special_newline = (
1461                          strlen( $output ) === $special_newline_at &&
1462                          1 === strspn( $text, "\n\r", 0, 1 )
1463                      );
1464  
1465                      if ( $needs_special_newline ) {
1466                          $output .= "\n";
1467                      }
1468  
1469                      $output .= strtr(
1470                          $text,
1471                          array(
1472                              "\x00" => "\u{FFFD}",
1473                              "\r\n" => "\n",
1474                              "\r"   => "\n",
1475                              '<'    => '&lt;',
1476                              '&'    => '&amp;',
1477                              '>'    => '&gt;',
1478                          )
1479                      );
1480  
1481                      break;
1482                  }
1483  
1484                  $is_in_text_integration_point = (
1485                      $is_in_mathml_text_integration_point ||
1486                      ( ! $is_closer && $is_in_svg_html_integration_point )
1487                  );
1488  
1489                  /*
1490                   * While content inside integration points is generally not allowed here,
1491                   * character data inside the MathML text elements _is_ allowed. This is
1492                   * because the rules only change slightly: NULL bytes are removed instead
1493                   * of being replaced with the Unicode replacement character U+FFFD; and
1494                   * active formats are reconstructed. The format reconstruction doesn’t
1495                   * occur here but a browser will still do so; this sanitizer is generally
1496                   * unaware of nesting structure.
1497                   */
1498                  if ( $is_in_text_integration_point && '#text' === $token_type ) {
1499                      $this->change_parsing_namespace( 'html' );
1500                      $text = $this->get_modifiable_text();
1501                      $this->change_parsing_namespace( $namespace );
1502                  } else {
1503                      $text = $this->get_modifiable_text();
1504                  }
1505  
1506                  /*
1507                   * Enter the foreign content and change the parsing namespace
1508                   * so that the parser recognizes real self-closing elements.
1509                   */
1510                  $is_svg_or_math        = 'MATH' === $token_name || 'SVG' === $token_name;
1511                  $has_self_closing_flag = ! $is_closer && $this->has_self_closing_flag();
1512                  if ( $is_svg_or_math && ! $is_closer && 'html' === $namespace ) {
1513                      $this->change_parsing_namespace( strtolower( $token_name ) );
1514                      $namespace = $this->get_namespace();
1515                  }
1516  
1517                  if ( 'html' !== $namespace && '#tag' === $token_type ) {
1518                      /*
1519                       * Ensure that only well-formed foreign content is allowed.
1520                       * Since un-balanced closing tags might implicitly close the
1521                       * open foreign-content element, these must be rejected.
1522                       */
1523                      if ( $is_closer ) {
1524                          $open_element = array_pop( $this->foreign_content_stack );
1525                          if ( null === $open_element || $token_name !== $open_element ) {
1526                              break;
1527                          }
1528  
1529                          if ( 'math' === $namespace && 'ANNOTATION-XML' === $open_element ) {
1530                              --$this->math_annotation_xml_depth;
1531                          }
1532  
1533                          /*
1534                           * Reset the foreign content tracker so it doesn’t truncate
1535                           * unintentionally after foreign content has properly closed.
1536                           */
1537                          if ( empty( $this->foreign_content_stack ) ) {
1538                              $foreign_content_starts_at = PHP_INT_MAX;
1539                          }
1540                      } else {
1541                          /*
1542                           * Track the opening of the last transition into foreign
1543                           * content so that it can be discarded when encountering
1544                           * tags that would require more substantial parsing.
1545                           */
1546                          if ( empty( $this->foreign_content_stack ) ) {
1547                              $foreign_content_starts_at = strlen( $output );
1548                          }
1549  
1550                          $this->foreign_content_stack[] = $token_name;
1551  
1552                          if ( 'math' === $namespace && 'ANNOTATION-XML' === $token_name ) {
1553                              ++$this->math_annotation_xml_depth;
1554                          }
1555                      }
1556                  }
1557  
1558                  if ( 'TEMPLATE' === $token_name && 'html' === $namespace && ! $is_closer ) {
1559                      ++$template_depth;
1560                  }
1561  
1562                  $skip_token = (
1563                      (
1564                          $template_depth > 0 &&
1565                          ! isset( $this->allowed_html['template'] )
1566                      ) ||
1567                      (
1568                          ! empty( $this->foreign_content_stack ) &&
1569                          ! isset( $this->allowed_html[ strtolower( $this->foreign_content_stack[0] ) ] )
1570                      )
1571                  );
1572  
1573                  switch ( $token_type ) {
1574                      case '#text':
1575                          if ( $skip_token ) {
1576                              break;
1577                          }
1578  
1579                          $needs_special_newline = (
1580                              strlen( $output ) === $special_newline_at &&
1581                              1 === strspn( $text, "\n\r", 0, 1 )
1582                          );
1583  
1584                          $text = strtr(
1585                              $text,
1586                              array(
1587                                  "\r" => '&#xD;',
1588                                  '<'  => '&lt;',
1589                                  '&'  => '&amp;',
1590                                  '>'  => '&gt;',
1591                              )
1592                          );
1593  
1594                          if ( $needs_special_newline ) {
1595                              $output .= "\n{$text}";
1596                          } else {
1597                              $output .= $text;
1598                          }
1599                          break;
1600  
1601                      /*
1602                       * Untrusted sources should not be creating these kinds of tokens,
1603                       * so remove them entirely from the output.
1604                       */
1605                      case '#doctype':
1606                      case '#presumptuous-tag':
1607                      case '#processing-instruction':
1608                          break;
1609  
1610                      /*
1611                       * It’s questionable whether these should be allowed through, but
1612                       * the legacy behavior supports it. Therefore, allow them as long
1613                       * as they don’t contain potentially confusing syntax characters.
1614                       */
1615                      case '#funky-comment':
1616                          if ( ! $skip_token && ! str_contains( $text, '<' ) ) {
1617                              $output .= substr( $this->html, $here->start, $here->length );
1618                          }
1619                          break;
1620  
1621                      /*
1622                       * `wp_kses()` runs iteratively on the content inside of these tokens,
1623                       * but the content is benign in a browser.
1624                       */
1625                      case '#comment':
1626                          if ( $skip_token ) {
1627                              break;
1628                          }
1629  
1630                          /*
1631                           * There are several kinds of malformed HTML which are handled by interpreting
1632                           * them as HTML comments. For example, `<?>` is called a “bogus comment” by the
1633                           * HTML specification, but when loaded by a browser is equivalent to `<!--?-->`.
1634                           * In this way, interacting with the DOM via JavaScript differs from handling
1635                           * the textual representation of a page in PHP.
1636                           *
1637                           * Ignore these non-normative comment forms to protect downstream parsers which
1638                           * might not be expecting their kinds of syntax. This prevents mis-parses for
1639                           * code which over-simplifies HTML parsing.
1640                           */
1641                          if ( WP_HTML_Tag_Processor::COMMENT_AS_HTML_COMMENT !== $this->get_comment_type() ) {
1642                              break;
1643                          }
1644  
1645                          // Apply special filtering for block comment delimiters with JSON attributes.
1646                          $comment = substr( $this->html, $here->start, $here->length );
1647  
1648                          /*
1649                           * A comment like `<!-- notes --!>` still appears as a normative HTML comment,
1650                           * but as an incorrectly-closed comment. Ignore these as well, as part of only
1651                           * allowing normative comment contents.
1652                           */
1653                          $was_incorrectly_closed = '!' === $comment[ strlen( $comment ) - 2 ];
1654                          if ( $was_incorrectly_closed ) {
1655                              break;
1656                          }
1657  
1658                          $block_processor = new WP_Block_Processor( $comment );
1659                          if ( $block_processor->next_token() && ! $block_processor->is_html() ) {
1660                              $block_type          = $block_processor->get_block_type();
1661                              $implicit_block_type = str_starts_with( $block_type, 'core/' )
1662                                  ? substr( $block_type, /* 'core/' */ 5 )
1663                                  : $block_type;
1664  
1665                              switch ( $block_processor->get_delimiter_type() ) {
1666                                  // Track when blocks open and when they don’t self-close.
1667                                  case WP_Block_Processor::OPENER:
1668                                      $open_blocks[]    = $implicit_block_type;
1669                                      $open_blocks_at[] = strlen( $output );
1670                                      break;
1671  
1672                                  // Track when blocks close.
1673                                  case WP_Block_Processor::CLOSER:
1674                                      if ( empty( $open_blocks ) ) {
1675                                          break 2;
1676                                      }
1677  
1678                                      /*
1679                                       * The default parser closes any open block, even when
1680                                       * the names don’t match. Preserve this behavior here
1681                                       * to avoid differences in sanitization and parsing.
1682                                       */
1683                                      $closed_block    = array_pop( $open_blocks );
1684                                      $closed_block_at = array_pop( $open_blocks_at );
1685  
1686                                      if ( 'html' !== $namespace && $closed_block_at < $foreign_content_starts_at ) {
1687                                          $foreign_closed_blocks[] = $closed_block;
1688                                      }
1689                              }
1690  
1691                              // Filter block attributes for opening delimiters.
1692                              $attributes = $block_processor->allocate_and_return_parsed_attributes();
1693                              $voider     = WP_Block_Processor::VOID === $block_processor->get_delimiter_type() ? '/' : '';
1694                              if ( $block_processor->opens_block() ) {
1695                                  if ( isset( $attributes ) ) {
1696                                      $attributes = filter_block_kses_value(
1697                                          $attributes,
1698                                          $this->specified_allowed_html,
1699                                          $this->allowed_protocols,
1700                                          array( 'blockName' => $block_type )
1701                                      );
1702                                  }
1703                              }
1704  
1705                              // Fully normalize all block delimiters: name, whitespace, attributes, flags.
1706                              if ( WP_Block_Processor::CLOSER === $block_processor->get_delimiter_type() ) {
1707                                  $text = " /wp:{$implicit_block_type} ";
1708                              } elseif ( ! empty( $attributes ) ) {
1709                                  $serialized_attributes = serialize_block_attributes( $attributes );
1710  
1711                                  $text = " wp:{$implicit_block_type} {$serialized_attributes} {$voider}";
1712                              } else {
1713                                  $text = " wp:{$implicit_block_type} {$voider}";
1714                              }
1715                          }
1716  
1717                          /*
1718                           * Legacy `wp_kses()` recursively calls itself on the contents of comments.
1719                           * Since comment content is not escaped, this changes the meaning of those
1720                           * comments when parsed. Still, code often expects to find tag-like syntax
1721                           * only when they are real tags. This legacy defect is preserved to avoid
1722                           * presenting content that downstream parsers might misinterpret as markup.
1723                           */
1724                          $text = strtr( $text, array( '<' => '&lt;' ) );
1725  
1726                          $output .= "<!--{$text}-->";
1727                          break;
1728  
1729                      /*
1730                       * True CDATA sections only exist within embedded SVG and MathML content,
1731                       * where they represent text data without any escaping. However, because
1732                       * parsers tend to vary on how to parse these, for untrusted inputs,
1733                       * rewrite all CDATA sections as normal escaped text.
1734                       */
1735                      case '#cdata-section':
1736                          if ( ! $skip_token ) {
1737                              $output .= strtr(
1738                                  $text,
1739                                  array(
1740                                      "\x00" => "\u{FFFD}",
1741                                      '<'    => '&lt;',
1742                                      '&'    => '&amp;',
1743                                      '>'    => '&gt;',
1744                                  )
1745                              );
1746                          }
1747  
1748                          break;
1749  
1750                      case '#tag':
1751                          /*
1752                           * Any failures inside foreign content should return the part of
1753                           * the post processed up until the entrance of the foreign content.
1754                           * This is necessary because it’s only inside foreign content that
1755                           * the self-closing flag indicates a self-closing element.
1756                           *
1757                           * While the HTML Processor can enter into SVG and MATH and track
1758                           * when they close, it’s substantially more complicated and requires
1759                           * considerable accounting. To avoid all of that, and to accept the
1760                           * kind of content that is nominal and safe, track only when the
1761                           * next tag _could_ lead to implicit changing of the parsing namespace
1762                           * or insertion mode.
1763                           */
1764                          if (
1765                              'html' !== $namespace &&
1766                              $this->could_escape_foreign_content(
1767                                  $is_in_mathml_text_integration_point,
1768                                  $is_in_svg_html_integration_point
1769                              )
1770                          ) {
1771                              break 2;
1772                          }
1773  
1774                          if ( $skip_token ) {
1775                              break;
1776                          }
1777  
1778                          $tag_name = strtolower( $token_name );
1779  
1780                          // Skip unallowed elements by tag name
1781                          if ( ! isset( $this->allowed_html[ $tag_name ] ) ) {
1782                              break;
1783                          }
1784  
1785                          if ( $is_closer ) {
1786                              $output .= "</{$tag_name}>";
1787                              break;
1788                          }
1789  
1790                          $is_special_atomic_element = (
1791                              'html' === $namespace &&
1792                              in_array( $token_name, $special_atomic_elements, true )
1793                          );
1794  
1795                          $expects_closer = ! (
1796                              'html' === $namespace
1797                                  ? ( WP_HTML_Processor::is_void( $token_name ) || $is_special_atomic_element )
1798                                  : $has_self_closing_flag
1799                          );
1800  
1801                          $self_closer = ( 'html' !== $namespace && $has_self_closing_flag ) ? ' /' : '';
1802                          $closing_tag = $is_special_atomic_element ? "</{$tag_name}>" : '';
1803  
1804                          $attribute_names    = $this->get_attribute_names_with_prefix( '' );
1805                          $element_attributes = $this->allowed_html[ $tag_name ];
1806  
1807                          // Check for required attributes.
1808                          $required_attributes = array();
1809                          if ( is_array( $element_attributes ) ) {
1810                              foreach ( $element_attributes as $name => $spec ) {
1811                                  if ( true === ( $spec['required'] ?? false ) ) {
1812                                      $required_attributes[ $name ] = true;
1813                                  }
1814                              }
1815                          }
1816  
1817                          /*
1818                           * Allow `data-*` attributes.
1819                           *
1820                           * When specifying `$allowed_html`, the attribute name should be set as
1821                           * `data-*` (not to be mixed with the HTML 4.0 `data` attribute, see
1822                           * https://www.w3.org/TR/html40/struct/objects.html#adef-data).
1823                           *
1824                           * Note: the attribute name should only contain `A-Za-z0-9_-` chars.
1825                           */
1826                          if ( ! empty( $element_attributes['data-*'] ) ) {
1827                              if ( is_array( $attribute_names ) ) {
1828                                  foreach ( $attribute_names as $name ) {
1829                                      if (
1830                                          1 === preg_match( '/^data-[a-z0-9_-]+$/', $name ) &&
1831                                          (
1832                                              ! isset( $element_attributes[ $name ] ) ||
1833                                              '' === $element_attributes[ $name ]
1834                                          )
1835                                      ) {
1836                                          $element_attributes[ $name ] = $element_attributes['data-*'];
1837                                      }
1838                                  }
1839                              }
1840  
1841                              unset( $element_attributes['data-*'] );
1842                          }
1843  
1844                          $tag_maker = new self(
1845                              "<{$tag_name}{$self_closer}>{$closing_tag}",
1846                              $this->specified_allowed_html,
1847                              $this->allowed_html,
1848                              $this->allowed_protocols,
1849                              $this->uri_attributes
1850                          );
1851                          $tag_maker->change_parsing_namespace( $namespace );
1852                          $tag_maker->next_token();
1853                          if ( is_array( $attribute_names ) ) {
1854                              foreach ( $attribute_names as $name ) {
1855                                  $spec = $element_attributes[ $name ] ?? null;
1856  
1857                                  // This attribute is not specified, thus not allowed. Skip it.
1858                                  if ( null === $spec || '' === $spec ) {
1859                                      continue;
1860                                  }
1861  
1862                                  /*
1863                                   * An Interactivity API binding replaces an attribute value when the content
1864                                   * is rendered, after KSES has checked it. Where KSES restricts attribute values
1865                                   * on an element, only bindings of attributes it allows there with any value
1866                                   * are kept.
1867                                   */
1868                                  if (
1869                                      str_starts_with( $name, 'data-wp-bind--' ) &&
1870                                      _wp_kses_directive_binds_value_restricted_attribute( $name, $this->allowed_html[ $tag_name ] )
1871                                  ) {
1872                                      continue;
1873                                  }
1874  
1875                                  $raw_value = $this->get_attribute( $name );
1876                                  $value     = is_string( $raw_value ) ? $raw_value : '';
1877  
1878                                  // Process the style attribute through CSS sanitization.
1879                                  if ( 'style' === $name ) {
1880                                      if ( ! is_string( $raw_value ) ) {
1881                                          continue;
1882                                      }
1883  
1884                                      $value = safecss_filter_attr( $value );
1885                                      if ( '' === trim( $value ) ) {
1886                                          continue;
1887                                      }
1888                                  }
1889  
1890                                  $is_url_ish = in_array( strtolower( $name ), $this->uri_attributes, true );
1891                                  if ( $is_url_ish ) {
1892                                      $value = wp_kses_bad_protocol( $value, $this->allowed_protocols );
1893                                  }
1894  
1895                                  /*
1896                                   * Process the remaining attributes according to their policies.
1897                                   *
1898                                   * Non-array values for the attribute specification are assumed
1899                                   * to be `true`, thus permitting the attribute.
1900                                   */
1901                                  if ( is_array( $spec ) ) {
1902                                      foreach ( $spec as $property => $constraint ) {
1903                                          $vless = true === $raw_value ? 'y' : 'n';
1904  
1905                                          if ( ! wp_kses_check_attr_val( $value, $vless, $property, $constraint ) ) {
1906                                              continue 2;
1907                                          }
1908                                      }
1909                                  }
1910  
1911                                  $did_set = ( true === $raw_value && '' === $value )
1912                                      ? $tag_maker->set_attribute( $name, true )
1913                                      : $tag_maker->set_attribute( $name, $value );
1914  
1915                                  if ( $did_set ) {
1916                                      unset( $required_attributes[ $name ] );
1917                                  }
1918                              }
1919                          }
1920  
1921                          $needs_special_newline = 'html' === $namespace && ( 'PRE' === $token_name || 'LISTING' === $token_name );
1922  
1923                          if ( ! empty( $required_attributes ) ) {
1924                              if ( ! $expects_closer ) {
1925                                  break;
1926                              }
1927  
1928                              /*
1929                               * Since this processor cannot track nesting of HTML elements
1930                               * generally, leave opening tags when required attributes are
1931                               * missing, but strip them of their attributes.
1932                               */
1933                              $output .= "<{$tag_name}>";
1934                              if ( $needs_special_newline ) {
1935                                  $special_newline_at = strlen( $output );
1936                              }
1937                              break;
1938                          }
1939  
1940                          if ( $is_special_atomic_element ) {
1941                              if ( 'TITLE' === $token_name || 'TEXTAREA' === $token_name ) {
1942                                  /*
1943                                   * RCDATA nodes can be safely escaped, but this must be
1944                                   * done after enqueuing the update to avoid double-escaping.
1945                                   */
1946                                  $tag_maker->set_modifiable_text( $text );
1947                                  $tag_maker->lexical_updates['modifiable text']->text = strtr(
1948                                      $tag_maker->lexical_updates['modifiable text']->text,
1949                                      array(
1950                                          "\x00" => "\u{FFFD}",
1951                                          "\r"   => '&#xD;',
1952                                      )
1953                                  );
1954                              } elseif ( ! self::contains_a_block_delimiter( $text ) ) {
1955                                  // Other nodes not containing a block delimiter are safe.
1956                                  $tag_maker->set_modifiable_text( $text );
1957                              } else {
1958                                  /*
1959                                   * But RAWTEXT and SCRIPT cannot be generally escaped, so reject
1960                                   * updates which would include something that could be misparsed
1961                                   * as a block comment delimiter.
1962                                   */
1963                                  $tag_maker->set_modifiable_text( '' );
1964                              }
1965                          }
1966  
1967                          $output .= $tag_maker->get_updated_html();
1968                          if ( $needs_special_newline ) {
1969                              $special_newline_at = strlen( $output );
1970                          }
1971  
1972                          break;
1973                  }
1974  
1975                  // Re-enter the HTML namespace.
1976                  if ( 'html' !== $namespace ) {
1977                      if ( $has_self_closing_flag ) {
1978                          array_pop( $this->foreign_content_stack );
1979  
1980                          if ( 'math' === $namespace && 'ANNOTATION-XML' === $token_name ) {
1981                              --$this->math_annotation_xml_depth;
1982                          }
1983                      }
1984  
1985                      if ( empty( $this->foreign_content_stack ) ) {
1986                          $is_in_mathml_text_integration_point = false;
1987                          $is_in_svg_html_integration_point    = false;
1988                          $this->change_parsing_namespace( 'html' );
1989                          $foreign_content_starts_at = PHP_INT_MAX;
1990                          $foreign_closed_blocks     = array();
1991                      } elseif ( '#tag' === $token_type && ! $has_self_closing_flag ) {
1992                          switch ( $token_name ) {
1993                              case 'MI':
1994                              case 'MN':
1995                              case 'MO':
1996                              case 'MS':
1997                              case 'MTEXT':
1998                                  $is_in_mathml_text_integration_point = ! $is_closer && 'math' === $namespace;
1999                                  break;
2000  
2001                              case 'DESC':
2002                              case 'FOREIGNOBJECT':
2003                              case 'TITLE':
2004                                  $is_in_svg_html_integration_point = ! $is_closer && 'svg' === $namespace;
2005                                  break;
2006                          }
2007                      }
2008                  }
2009  
2010                  if ( 'TEMPLATE' === $token_name && $template_depth > 0 && $is_closer && 'html' === $namespace ) {
2011                      --$template_depth;
2012                  }
2013              }
2014  
2015              /*
2016               * While there might have been an incomplete token in the output stream,
2017               * there is no need to render it to the output. They would disappear on
2018               * their own in a browser if they ended the document, but here they do
2019               * not end the document; instead, they are likely being inserted into an
2020               * existing document, where the incomplete token might mess with the rest
2021               * of the page’s HTML structure.
2022               */
2023  
2024              $sanitized = substr( $output, 0, $foreign_content_starts_at );
2025  
2026              // Templates should be contained within blocks, so close them first.
2027              if ( $template_depth > 0 && isset( $this->allowed_html['template'] ) ) {
2028                  $sanitized .= str_repeat( '</template>', $template_depth );
2029              }
2030  
2031              // Close any remaining-open blocks ensure isolation of block content.
2032              foreach ( $foreign_closed_blocks as $block_name ) {
2033                  $sanitized .= "<!-- /wp:{$block_name} -->";
2034              }
2035  
2036              for ( $i = count( $open_blocks ) - 1; $i >= 0; $i-- ) {
2037                  // Skip blocks that were opened when inside truncated foreign content.
2038                  if ( $open_blocks_at[ $i ] >= $foreign_content_starts_at ) {
2039                      continue;
2040                  }
2041  
2042                  $block_name = $open_blocks[ $i ];
2043                  $sanitized .= "<!-- /wp:{$block_name} -->";
2044              }
2045  
2046              return $sanitized;
2047          }
2048      };
2049  
2050      return $processor->sanitize();
2051  }
2052  
2053  /**
2054   * Filters one HTML attribute and ensures its value is allowed.
2055   *
2056   * This function can escape data in some situations where `wp_kses()` must strip the whole attribute.
2057   *
2058   * @since 4.2.3
2059   *
2060   * @param string $attr    The 'whole' attribute, including name and value.
2061   * @param string $element The HTML element name to which the attribute belongs.
2062   * @return string Filtered attribute.
2063   */
2064  function wp_kses_one_attr( $attr, $element ) {
2065      $uris              = wp_kses_uri_attributes();
2066      $allowed_html      = wp_kses_allowed_html( 'post' );
2067      $allowed_protocols = wp_allowed_protocols();
2068      $attr              = wp_kses_no_null( $attr, array( 'slash_zero' => 'keep' ) );
2069  
2070      // Preserve leading and trailing whitespace.
2071      $matches = array();
2072      preg_match( '/^\s*/', $attr, $matches );
2073      $lead = $matches[0];
2074      preg_match( '/\s*$/', $attr, $matches );
2075      $trail = $matches[0];
2076      if ( empty( $trail ) ) {
2077          $attr = substr( $attr, strlen( $lead ) );
2078      } else {
2079          $attr = substr( $attr, strlen( $lead ), -strlen( $trail ) );
2080      }
2081  
2082      // Parse attribute name and value from input.
2083      $split = preg_split( '/\s*=\s*/', $attr, 2 );
2084      $name  = $split[0];
2085      if ( count( $split ) === 2 ) {
2086          $value = $split[1];
2087  
2088          /*
2089           * Remove quotes surrounding $value.
2090           * Also guarantee correct quoting in $attr for this one attribute.
2091           */
2092          if ( '' === $value ) {
2093              $quote = '';
2094          } else {
2095              $quote = $value[0];
2096          }
2097          if ( '"' === $quote || "'" === $quote ) {
2098              if ( ! str_ends_with( $value, $quote ) ) {
2099                  return '';
2100              }
2101              $value = substr( $value, 1, -1 );
2102          } else {
2103              $quote = '"';
2104          }
2105  
2106          // Sanitize quotes, angle braces, and entities.
2107          $value = esc_attr( $value );
2108  
2109          // Sanitize URI values.
2110          if ( in_array( strtolower( $name ), $uris, true ) ) {
2111              $value = wp_kses_bad_protocol( $value, $allowed_protocols );
2112          }
2113  
2114          $attr  = "$name=$quote$value$quote";
2115          $vless = 'n';
2116      } else {
2117          $value = '';
2118          $vless = 'y';
2119      }
2120  
2121      // Sanitize attribute by name.
2122      wp_kses_attr_check( $name, $value, $attr, $vless, $element, $allowed_html );
2123  
2124      // Restore whitespace.
2125      return $lead . $attr . $trail;
2126  }
2127  
2128  /**
2129   * Returns an array of allowed HTML tags and attributes for a given context.
2130   *
2131   * @since 3.5.0
2132   * @since 5.0.1 `form` removed as allowable HTML tag.
2133   *
2134   * @global array $allowedposttags
2135   * @global array $allowedtags
2136   * @global array $allowedentitynames
2137   *
2138   * @param string|array $context The context for which to retrieve tags. Allowed values are 'post',
2139   *                              'strip', 'data', 'entities', or the name of a field filter such as
2140   *                              'pre_user_description', or an array of allowed HTML elements and attributes.
2141   * @return array Array of allowed HTML tags and their allowed attributes.
2142   */
2143  function wp_kses_allowed_html( $context = '' ) {
2144      global $allowedposttags, $allowedtags, $allowedentitynames;
2145  
2146      if ( is_array( $context ) ) {
2147          // When `$context` is an array it's actually an array of allowed HTML elements and attributes.
2148          $html    = $context;
2149          $context = 'explicit';
2150  
2151          /**
2152           * Filters the HTML tags that are allowed for a given context.
2153           *
2154           * HTML tags and attribute names are case-insensitive in HTML but must be
2155           * added to the KSES allow list in lowercase. An item added to the allow list
2156           * in upper or mixed case will not recognized as permitted by KSES.
2157           *
2158           * @since 3.5.0
2159           *
2160           * @param array[] $html    Allowed HTML tags.
2161           * @param string  $context Context name.
2162           */
2163          return apply_filters( 'wp_kses_allowed_html', $html, $context );
2164      }
2165  
2166      switch ( $context ) {
2167          case 'post':
2168              /** This filter is documented in wp-includes/kses.php */
2169              $tags = apply_filters( 'wp_kses_allowed_html', $allowedposttags, $context );
2170  
2171              // 5.0.1 removed the `<form>` tag, allow it if a filter is allowing it's sub-elements `<input>` or `<select>`.
2172              if ( ! CUSTOM_TAGS && ! isset( $tags['form'] ) && ( isset( $tags['input'] ) || isset( $tags['select'] ) ) ) {
2173                  $tags = $allowedposttags;
2174  
2175                  $tags['form'] = array(
2176                      'action'         => true,
2177                      'accept'         => true,
2178                      'accept-charset' => true,
2179                      'enctype'        => true,
2180                      'method'         => true,
2181                      'name'           => true,
2182                      'target'         => true,
2183                  );
2184  
2185                  /** This filter is documented in wp-includes/kses.php */
2186                  $tags = apply_filters( 'wp_kses_allowed_html', $tags, $context );
2187              }
2188  
2189              return $tags;
2190  
2191          case 'user_description':
2192          case 'pre_term_description':
2193          case 'pre_user_description':
2194              $tags                = $allowedtags;
2195              $tags['a']['rel']    = true;
2196              $tags['a']['target'] = true;
2197              /** This filter is documented in wp-includes/kses.php */
2198              return apply_filters( 'wp_kses_allowed_html', $tags, $context );
2199  
2200          case 'strip':
2201              /** This filter is documented in wp-includes/kses.php */
2202              return apply_filters( 'wp_kses_allowed_html', array(), $context );
2203  
2204          case 'entities':
2205              /** This filter is documented in wp-includes/kses.php */
2206              return apply_filters( 'wp_kses_allowed_html', $allowedentitynames, $context );
2207  
2208          case 'data':
2209          default:
2210              /** This filter is documented in wp-includes/kses.php */
2211              return apply_filters( 'wp_kses_allowed_html', $allowedtags, $context );
2212      }
2213  }
2214  
2215  /**
2216   * Allows the note mention chip markup in comment content.
2217   *
2218   * The notes `@` mention completer stores a mention as a chip carrying the
2219   * mentioned user's ID in a class token:
2220   * `<span class="wp-note-mention user-N">@Name</span>`. The default comment
2221   * allowlist does not allow `span` at all, so for users without
2222   * `unfiltered_html` the mention would be stripped on save.
2223   *
2224   * The allowance is deliberately narrow and always on: `span` is a
2225   * semantics-free element and _wp_kses_sanitize_note_mention_classes()
2226   * reduces its `class` to the two mention tokens right after kses runs, so
2227   * regular (including anonymous) commenters gain nothing beyond the inert
2228   * mention markup itself.
2229   *
2230   * @since 7.1.0
2231   * @access private
2232   *
2233   * @param array<string, array<string, bool>> $allowed The allowed tags structure for the context.
2234   * @param string                             $context The kses context.
2235   * @return array<string, array<string, bool>> Modified allowed tags structure.
2236   */
2237  function _wp_kses_allow_note_mention_span( $allowed, $context ): array {
2238      if ( ! is_array( $allowed ) ) {
2239          $allowed = array();
2240      }
2241      if ( 'pre_comment_content' !== $context ) {
2242          return $allowed;
2243      }
2244  
2245      if ( ! isset( $allowed['span'] ) || ! is_array( $allowed['span'] ) ) {
2246          $allowed['span'] = array();
2247      }
2248  
2249      $allowed['span']['class'] = true;
2250  
2251      return $allowed;
2252  }
2253  
2254  /**
2255   * Reduces `span` classes in comment content to the note mention tokens.
2256   *
2257   * _wp_kses_allow_note_mention_span() lets `class` through kses on `span` so
2258   * the mention chip survives, but `class` is an open-ended styling and
2259   * scripting hook, so this companion pass - running right after
2260   * `wp_filter_kses` at priority 10 - strips every class token except the two
2261   * the mention markup uses: `wp-note-mention` and `user-N`. `span` is the only
2262   * comment tag allowed to carry `class` at all, so walking `span` tags covers
2263   * the entire allowance.
2264   *
2265   * The pass only applies while the restrictive comment allowlist is active:
2266   * users with `unfiltered_html` are filtered through `wp_filter_post_kses`
2267   * (or not at all), where arbitrary classes are already permitted, and
2268   * narrowing their markup here would restrict what core allows them to post.
2269   *
2270   * @since 7.1.0
2271   * @access private
2272   *
2273   * @param string $content Slashed comment content, already filtered by kses.
2274   * @return string Slashed comment content with span classes reduced.
2275   */
2276  function _wp_kses_sanitize_note_mention_classes( $content ): string {
2277      if ( ! is_string( $content ) ) {
2278          $content = '';
2279      }
2280      if ( false === has_filter( 'pre_comment_content', 'wp_filter_kses' ) ) {
2281          return $content;
2282      }
2283  
2284      $processor = new WP_HTML_Tag_Processor( wp_unslash( $content ) );
2285  
2286      while ( $processor->next_tag( 'SPAN' ) ) {
2287          foreach ( $processor->class_list() as $token ) {
2288              if ( 'wp-note-mention' !== $token && ! preg_match( '/^user-[1-9][0-9]*$/', $token ) ) {
2289                  // Removing the last class also removes the attribute itself.
2290                  $processor->remove_class( $token );
2291              }
2292          }
2293      }
2294  
2295      return wp_slash( $processor->get_updated_html() );
2296  }
2297  
2298  /**
2299   * You add any KSES hooks here.
2300   *
2301   * There is currently only one KSES WordPress hook, {@see 'pre_kses'}, and it is called here.
2302   * All parameters are passed to the hooks and expected to receive a string.
2303   *
2304   * @since 1.0.0
2305   *
2306   * @param string         $content           Content to filter through KSES.
2307   * @param array[]|string $allowed_html      An array of allowed HTML elements and attributes,
2308   *                                          or a context name such as 'post'. See wp_kses_allowed_html()
2309   *                                          for the list of accepted context names.
2310   * @param string[]       $allowed_protocols Array of allowed URL protocols.
2311   * @return string Filtered content through {@see 'pre_kses'} hook.
2312   */
2313  function wp_kses_hook( $content, $allowed_html, $allowed_protocols ) {
2314      /**
2315       * Filters content to be run through KSES.
2316       *
2317       * @since 2.3.0
2318       *
2319       * @param string         $content           Content to filter through KSES.
2320       * @param array[]|string $allowed_html      An array of allowed HTML elements and attributes,
2321       *                                          or a context name such as 'post'. See wp_kses_allowed_html()
2322       *                                          for the list of accepted context names.
2323       * @param string[]       $allowed_protocols Array of allowed URL protocols.
2324       */
2325      return apply_filters( 'pre_kses', $content, $allowed_html, $allowed_protocols );
2326  }
2327  
2328  /**
2329   * Returns the version number of KSES.
2330   *
2331   * @since 1.0.0
2332   *
2333   * @return string KSES version number.
2334   */
2335  function wp_kses_version() {
2336      return '0.2.2';
2337  }
2338  
2339  /**
2340   * Searches for HTML tags, no matter how malformed.
2341   *
2342   * It also matches stray `>` characters.
2343   *
2344   * @since 1.0.0
2345   * @since 6.6.0 Recognize additional forms of invalid HTML which convert into comments.
2346   *
2347   * @global array[]|string $pass_allowed_html      An array of allowed HTML elements and attributes,
2348   *                                                or a context name such as 'post'.
2349   * @global string[]       $pass_allowed_protocols Array of allowed URL protocols.
2350   *
2351   * @param string         $content           Content to filter.
2352   * @param array[]|string $allowed_html      An array of allowed HTML elements and attributes,
2353   *                                          or a context name such as 'post'. See wp_kses_allowed_html()
2354   *                                          for the list of accepted context names.
2355   * @param string[]       $allowed_protocols Array of allowed URL protocols.
2356   * @return string Content with fixed HTML tags
2357   */
2358  function wp_kses_split( $content, $allowed_html, $allowed_protocols ) {
2359      global $pass_allowed_html, $pass_allowed_protocols;
2360  
2361      $pass_allowed_html      = $allowed_html;
2362      $pass_allowed_protocols = $allowed_protocols;
2363  
2364      $token_pattern = <<<REGEX
2365  ~
2366      (                      # Detect comments of various flavors before attempting to find tags.
2367          (<!--.*?(-->|$))   #  - Normative HTML comments.
2368          |
2369          </[^a-zA-Z][^>]*>  #  - Closing tags with invalid tag names.
2370          |
2371          <![^>]*>           #  - Invalid markup declaration nodes. Not all invalid nodes
2372                             #    are matched so as to avoid breaking legacy behaviors.
2373      )
2374      |
2375      (<[^>]*(>|$)|>)        # Tag-like spans of text.
2376  ~x
2377  REGEX;
2378      return preg_replace_callback( $token_pattern, '_wp_kses_split_callback', $content );
2379  }
2380  
2381  /**
2382   * Returns an array of HTML attribute names whose value contains a URL.
2383   *
2384   * This function returns a list of all HTML attributes that must contain
2385   * a URL according to the HTML specification.
2386   *
2387   * This list includes URI attributes both allowed and disallowed by KSES.
2388   *
2389   * @link https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes
2390   *
2391   * @since 5.0.1
2392   *
2393   * @return string[] HTML attribute names whose value contains a URL.
2394   */
2395  function wp_kses_uri_attributes() {
2396      $uri_attributes = array(
2397          'action',
2398          'archive',
2399          'background',
2400          'cite',
2401          'classid',
2402          'codebase',
2403          'data',
2404          'formaction',
2405          'href',
2406          'icon',
2407          'longdesc',
2408          'manifest',
2409          'poster',
2410          'profile',
2411          'src',
2412          'usemap',
2413          'xmlns',
2414      );
2415  
2416      /**
2417       * Filters the list of attributes that are required to contain a URL.
2418       *
2419       * Use this filter to add any `data-` attributes that are required to be
2420       * validated as a URL.
2421       *
2422       * @since 5.0.1
2423       *
2424       * @param string[] $uri_attributes HTML attribute names whose value contains a URL.
2425       */
2426      $uri_attributes = apply_filters( 'wp_kses_uri_attributes', $uri_attributes );
2427  
2428      return $uri_attributes;
2429  }
2430  
2431  /**
2432   * Callback for `wp_kses_split()`.
2433   *
2434   * @since 3.1.0
2435   * @access private
2436   * @ignore
2437   *
2438   * @global array[]|string $pass_allowed_html      An array of allowed HTML elements and attributes,
2439   *                                                or a context name such as 'post'.
2440   * @global string[]       $pass_allowed_protocols Array of allowed URL protocols.
2441   *
2442   * @param array $matches preg_replace regexp matches
2443   * @return string
2444   */
2445  function _wp_kses_split_callback( $matches ) {
2446      global $pass_allowed_html, $pass_allowed_protocols;
2447  
2448      return wp_kses_split2( $matches[0], $pass_allowed_html, $pass_allowed_protocols );
2449  }
2450  
2451  /**
2452   * Callback for `wp_kses_split()` for fixing malformed HTML tags.
2453   *
2454   * This function does a lot of work. It rejects some very malformed things like
2455   * `<:::>`. It returns an empty string, if the element isn't allowed (look ma, no
2456   * `strip_tags()`!). Otherwise it splits the tag into an element and an attribute
2457   * list.
2458   *
2459   * After the tag is split into an element and an attribute list, it is run
2460   * through another filter which will remove illegal attributes and once that is
2461   * completed, will be returned.
2462   *
2463   * @access private
2464   * @ignore
2465   * @since 1.0.0
2466   * @since 6.6.0 Recognize additional forms of invalid HTML which convert into comments.
2467   *
2468   * @param string         $content           Content to filter.
2469   * @param array[]|string $allowed_html      An array of allowed HTML elements and attributes,
2470   *                                          or a context name such as 'post'. See wp_kses_allowed_html()
2471   *                                          for the list of accepted context names.
2472   * @param string[]       $allowed_protocols Array of allowed URL protocols.
2473   * @return string Fixed HTML element
2474   */
2475  function wp_kses_split2( $content, $allowed_html, $allowed_protocols ) {
2476      $content = wp_kses_stripslashes( $content );
2477  
2478      /*
2479       * The regex pattern used to split HTML into chunks attempts
2480       * to split on HTML token boundaries. This function should
2481       * thus receive chunks that _either_ start with meaningful
2482       * syntax tokens, like a tag `<div>` or a comment `<!-- ... -->`.
2483       *
2484       * If the first character of the `$content` chunk _isn't_ one
2485       * of these syntax elements, which always starts with `<`, then
2486       * the match had to be for the final alternation of `>`. In such
2487       * case, it's probably standing on its own and could be encoded
2488       * with a character reference to remove ambiguity.
2489       *
2490       * In other words, if this chunk isn't from a match of a syntax
2491       * token, it's just a plaintext greater-than (`>`) sign.
2492       */
2493      if ( ! str_starts_with( $content, '<' ) ) {
2494          return '&gt;';
2495      }
2496  
2497      /*
2498       * When certain invalid syntax constructs appear, the HTML parser
2499       * shifts into what's called the "bogus comment state." This is a
2500       * plaintext state that consumes everything until the nearest `>`
2501       * and then transforms the entire span into an HTML comment.
2502       *
2503       * Preserve these comments and do not treat them like tags.
2504       *
2505       * @link https://html.spec.whatwg.org/#bogus-comment-state
2506       */
2507      if ( 1 === preg_match( '~^(?:</[^a-zA-Z][^>]*>|<![a-z][^>]*>)$~', $content ) ) {
2508          /**
2509           * Since the pattern matches `</…>` and also `<!…>`, this will
2510           * preserve the type of the cleaned-up token in the output.
2511           */
2512          $opener  = $content[1];
2513          $content = substr( $content, 2, -1 );
2514  
2515          do {
2516              $prev    = $content;
2517              $content = wp_kses( $content, $allowed_html, $allowed_protocols );
2518          } while ( $prev !== $content );
2519  
2520          // Recombine the modified inner content with the original token structure.
2521          return "<{$opener}{$content}>";
2522      }
2523  
2524      /*
2525       * Normative HTML comments should be handled separately as their
2526       * parsing rules differ from those for tags and text nodes.
2527       */
2528      if ( str_starts_with( $content, '<!--' ) ) {
2529          $content = str_replace( array( '<!--', '-->' ), '', $content );
2530  
2531          while ( ( $newstring = wp_kses( $content, $allowed_html, $allowed_protocols ) ) !== $content ) {
2532              $content = $newstring;
2533          }
2534  
2535          if ( '' === $content ) {
2536              return '';
2537          }
2538  
2539          // Prevent multiple dashes in comments.
2540          $content = preg_replace( '/--+/', '-', $content );
2541          // Prevent three dashes closing a comment.
2542          $content = preg_replace( '/-$/', '', $content );
2543  
2544          return "<!--{$content}-->";
2545      }
2546  
2547      // It's seriously malformed.
2548      if ( ! preg_match( '%^<\s*(/\s*)?([a-zA-Z0-9-]+)([^>]*)>?$%', $content, $matches ) ) {
2549          return '';
2550      }
2551  
2552      $slash    = trim( $matches[1] );
2553      $elem     = $matches[2];
2554      $attrlist = $matches[3];
2555  
2556      if ( ! is_array( $allowed_html ) ) {
2557          $allowed_html = wp_kses_allowed_html( $allowed_html );
2558      }
2559  
2560      // They are using a not allowed HTML element.
2561      if ( ! isset( $allowed_html[ strtolower( $elem ) ] ) ) {
2562          return '';
2563      }
2564  
2565      // No attributes are allowed for closing elements.
2566      if ( '' !== $slash ) {
2567          return "</$elem>";
2568      }
2569  
2570      return wp_kses_attr( $elem, $attrlist, $allowed_html, $allowed_protocols );
2571  }
2572  
2573  /**
2574   * Removes all attributes, if none are allowed for this element.
2575   *
2576   * If some are allowed it calls `wp_kses_hair()` to split them further, and then
2577   * it builds up new HTML code from the data that `wp_kses_hair()` returns. It also
2578   * removes `<` and `>` characters, if there are any left. One more thing it does
2579   * is to check if the tag has a closing XHTML slash, and if it does, it puts one
2580   * in the returned code as well.
2581   *
2582   * An array of allowed values can be defined for attributes. If the attribute value
2583   * doesn't fall into the list, the attribute will be removed from the tag.
2584   *
2585   * Attributes can be marked as required. If a required attribute is not present,
2586   * KSES will remove all attributes from the tag. As KSES doesn't match opening and
2587   * closing tags, it's not possible to safely remove the tag itself, the safest
2588   * fallback is to strip all attributes from the tag, instead.
2589   *
2590   * @since 1.0.0
2591   * @since 5.9.0 Added support for an array of allowed values for attributes.
2592   *              Added support for required attributes.
2593   *
2594   * @param string         $element           HTML element/tag.
2595   * @param string         $attr              HTML attributes from HTML element to closing HTML element tag.
2596   * @param array[]|string $allowed_html      An array of allowed HTML elements and attributes,
2597   *                                          or a context name such as 'post'. See wp_kses_allowed_html()
2598   *                                          for the list of accepted context names.
2599   * @param string[]       $allowed_protocols Array of allowed URL protocols.
2600   * @return string Sanitized HTML element.
2601   */
2602  function wp_kses_attr( $element, $attr, $allowed_html, $allowed_protocols ) {
2603      if ( ! is_array( $allowed_html ) ) {
2604          $allowed_html = wp_kses_allowed_html( $allowed_html );
2605      }
2606  
2607      // Is there a closing XHTML slash at the end of the attributes?
2608      $xhtml_slash = '';
2609      if ( preg_match( '%\s*/\s*$%', $attr ) ) {
2610          $xhtml_slash = ' /';
2611      }
2612  
2613      // Are any attributes allowed at all for this element?
2614      $element_low = strtolower( $element );
2615      if ( empty( $allowed_html[ $element_low ] ) || true === $allowed_html[ $element_low ] ) {
2616          return "<$element$xhtml_slash>";
2617      }
2618  
2619      // Split it.
2620      $attrarr = wp_kses_hair( $attr, $allowed_protocols );
2621  
2622      // Check if there are attributes that are required.
2623      $required_attrs = array_filter(
2624          $allowed_html[ $element_low ],
2625          static function ( $required_attr_limits ) {
2626              return isset( $required_attr_limits['required'] ) && true === $required_attr_limits['required'];
2627          }
2628      );
2629  
2630      /*
2631       * If a required attribute check fails, we can return nothing for a self-closing tag,
2632       * but for a non-self-closing tag the best option is to return the element with attributes,
2633       * as KSES doesn't handle matching the relevant closing tag.
2634       */
2635      $stripped_tag = '';
2636      if ( empty( $xhtml_slash ) ) {
2637          $stripped_tag = "<$element>";
2638      }
2639  
2640      // Go through $attrarr, and save the allowed attributes for this element in $attr2.
2641      $attr2 = '';
2642      foreach ( $attrarr as $arreach ) {
2643          // Check if this attribute is required.
2644          $required = isset( $required_attrs[ strtolower( $arreach['name'] ) ] );
2645  
2646          if ( wp_kses_attr_check( $arreach['name'], $arreach['value'], $arreach['whole'], $arreach['vless'], $element, $allowed_html ) ) {
2647              $attr2 .= ' ' . $arreach['whole'];
2648  
2649              // If this was a required attribute, we can mark it as found.
2650              if ( $required ) {
2651                  unset( $required_attrs[ strtolower( $arreach['name'] ) ] );
2652              }
2653          } elseif ( $required ) {
2654              // This attribute was required, but didn't pass the check. The entire tag is not allowed.
2655              return $stripped_tag;
2656          }
2657      }
2658  
2659      // If some required attributes weren't set, the entire tag is not allowed.
2660      if ( ! empty( $required_attrs ) ) {
2661          return $stripped_tag;
2662      }
2663  
2664      // Remove any "<" or ">" characters.
2665      $attr2 = preg_replace( '/[<>]/', '', $attr2 );
2666  
2667      return "<$element$attr2$xhtml_slash>";
2668  }
2669  
2670  /**
2671   * Determines whether an attribute is allowed.
2672   *
2673   * @since 4.2.3
2674   * @since 5.0.0 Added support for `data-*` wildcard attributes.
2675   *
2676   * @param string $name         The attribute name. Passed by reference. Returns empty string when not allowed.
2677   * @param string $value        The attribute value. Passed by reference. Returns a filtered value.
2678   * @param string $whole        The `name=value` input. Passed by reference. Returns filtered input.
2679   * @param string $vless        Whether the attribute is valueless. Use 'y' or 'n'.
2680   * @param string $element      The name of the element to which this attribute belongs.
2681   * @param array  $allowed_html The full list of allowed elements and attributes.
2682   * @return bool Whether or not the attribute is allowed.
2683   */
2684  function wp_kses_attr_check( &$name, &$value, &$whole, $vless, $element, $allowed_html ) {
2685      $name_low    = strtolower( $name );
2686      $element_low = strtolower( $element );
2687  
2688      if ( ! isset( $allowed_html[ $element_low ] ) ) {
2689          $name  = '';
2690          $value = '';
2691          $whole = '';
2692          return false;
2693      }
2694  
2695      $allowed_attr = $allowed_html[ $element_low ];
2696  
2697      if ( ! isset( $allowed_attr[ $name_low ] ) || '' === $allowed_attr[ $name_low ] ) {
2698          /*
2699           * Allow `data-*` attributes.
2700           *
2701           * When specifying `$allowed_html`, the attribute name should be set as
2702           * `data-*` (not to be mixed with the HTML 4.0 `data` attribute, see
2703           * https://www.w3.org/TR/html40/struct/objects.html#adef-data).
2704           *
2705           * Note: the attribute name should only contain `A-Za-z0-9_-` chars.
2706           */
2707          if ( str_starts_with( $name_low, 'data-' ) && ! empty( $allowed_attr['data-*'] )
2708              && preg_match( '/^data-[a-z0-9_-]+$/', $name_low, $match )
2709          ) {
2710              /*
2711               * Add the whole attribute name to the allowed attributes and set any restrictions
2712               * for the `data-*` attribute values for the current element.
2713               */
2714              $allowed_attr[ $match[0] ] = $allowed_attr['data-*'];
2715          } else {
2716              $name  = '';
2717              $value = '';
2718              $whole = '';
2719              return false;
2720          }
2721      }
2722  
2723      /*
2724       * An Interactivity API binding replaces an attribute value when the content is rendered,
2725       * after KSES has checked it. Where KSES restricts attribute values on an element, only
2726       * bindings of attributes it allows there with any value are kept.
2727       */
2728      if (
2729          str_starts_with( $name_low, 'data-wp-bind--' ) &&
2730          _wp_kses_directive_binds_value_restricted_attribute( $name_low, $allowed_attr )
2731      ) {
2732          $name  = '';
2733          $value = '';
2734          $whole = '';
2735          return false;
2736      }
2737  
2738      if ( 'style' === $name_low ) {
2739          $decoded_value = WP_HTML_Decoder::decode_attribute( $value );
2740          $new_value     = safecss_filter_attr( $decoded_value );
2741  
2742          if ( empty( $new_value ) ) {
2743              $name  = '';
2744              $value = '';
2745              $whole = '';
2746              return false;
2747          }
2748  
2749          if ( $new_value !== $decoded_value ) {
2750              $encoded_value = esc_attr( $new_value );
2751              $whole         = str_replace( $value, $encoded_value, $whole );
2752              $value         = $encoded_value;
2753          }
2754      }
2755  
2756      if ( is_array( $allowed_attr[ $name_low ] ) ) {
2757          // There are some checks.
2758          foreach ( $allowed_attr[ $name_low ] as $currkey => $currval ) {
2759              if ( ! wp_kses_check_attr_val( $value, $vless, $currkey, $currval ) ) {
2760                  $name  = '';
2761                  $value = '';
2762                  $whole = '';
2763                  return false;
2764              }
2765          }
2766      }
2767  
2768      return true;
2769  }
2770  
2771  /**
2772   * Given a string of HTML attributes and values, parse into a structured attribute list.
2773   *
2774   * This function performs a number of transformations while parsing attribute strings:
2775   *  - It normalizes attribute values and surrounds them with double quotes.
2776   *  - It normalizes HTML character references inside attribute values.
2777   *  - It removes “bad” URL protocols from attribute values.
2778   *
2779   * Otherwise this reads the attributes as if they were part of an HTML tag. It performs
2780   * these transformations to lower the risk of mis-parsing down the line and to perform
2781   * URL sanitization in line with the rest of the `kses` subsystem. Importantly, it does
2782   * not decode the attribute values, meaning that special HTML syntax characters will
2783   * be left with character references in the `value` property.
2784   *
2785   * Example:
2786   *
2787   *     $attrs = wp_kses_hair( 'class="is-wide" inert data-lazy=\'&lt;img&#00062\' =/🐮=/' );
2788   *     $attrs === array(
2789   *         'class'     => array( 'name' => 'class', 'value' => 'is-wide', 'whole' => 'class="is-wide"', 'vless' => 'n' ),
2790   *         'inert'     => array( 'name' => 'inert', 'value' => '', 'whole' => 'inert', 'vless' => 'y' ),
2791   *         'data-lazy' => array( 'name' => 'data-lazy', 'value' => '&lt;img&gt;', 'whole' => 'data-lazy="&lt;img&gt;"', 'vless' => 'n' ),
2792   *         '='         => array( 'name' => '=', 'value' => '', 'whole' => '=', 'vless' => 'y' ),
2793   *         '🐮'        => array( 'name' => '🐮', 'value' => '/', 'whole' => '🐮="/"', 'vless' => 'n' ),
2794   *     );
2795   *
2796   * @since 1.0.0
2797   * @since 7.0.0 Reliably parses HTML via the HTML API.
2798   *
2799   * @param string   $attr              Attribute list from HTML element to closing HTML element tag.
2800   * @param string[] $allowed_protocols Array of allowed URL protocols.
2801   * @return array<string, array{name: string, value: string, whole: string, vless: 'y'|'n'}> Array of attribute information after parsing.
2802   */
2803  function wp_kses_hair( $attr, $allowed_protocols ) {
2804      $attributes = array();
2805      $uris       = wp_kses_uri_attributes();
2806  
2807      $processor = new WP_HTML_Tag_Processor( "<wp {$attr}>" );
2808      $processor->next_token();
2809  
2810      $attribute_names = $processor->get_attribute_names_with_prefix( '' );
2811      if ( null === $attribute_names || 0 === count( $attribute_names ) ) {
2812          return $attributes;
2813      }
2814  
2815      $syntax_characters = array(
2816          '&' => '&amp;',
2817          '<' => '&lt;',
2818          '>' => '&gt;',
2819          "'" => '&apos;',
2820          '"' => '&quot;',
2821      );
2822  
2823      foreach ( $attribute_names as $name ) {
2824          $value   = $processor->get_attribute( $name );
2825          $is_bool = true === $value;
2826          if ( is_string( $value ) && in_array( $name, $uris, true ) ) {
2827              $value = wp_kses_bad_protocol( $value, $allowed_protocols );
2828          }
2829  
2830          // Reconstruct and normalize the attribute value.
2831          $recoded = $is_bool ? '' : strtr( $value, $syntax_characters );
2832          $whole   = $is_bool ? $name : "{$name}=\"{$recoded}\"";
2833  
2834          $attributes[ $name ] = array(
2835              'name'  => $name,
2836              'value' => $recoded,
2837              'whole' => $whole,
2838              'vless' => $is_bool ? 'y' : 'n',
2839          );
2840      }
2841  
2842      return $attributes;
2843  }
2844  
2845  /**
2846   * Finds all attributes of an HTML element.
2847   *
2848   * Does not modify input.  May return "evil" output.
2849   *
2850   * Based on `wp_kses_split2()` and `wp_kses_attr()`.
2851   *
2852   * @since 4.2.3
2853   *
2854   * @param string $element HTML element.
2855   * @return array|false List of attributes found in the element. Returns false on failure.
2856   */
2857  function wp_kses_attr_parse( $element ) {
2858      $valid = preg_match( '%^(<\s*)(/\s*)?([a-zA-Z0-9]+\s*)([^>]*)(>?)$%', $element, $matches );
2859      if ( 1 !== $valid ) {
2860          return false;
2861      }
2862  
2863      $begin  = $matches[1];
2864      $slash  = $matches[2];
2865      $elname = $matches[3];
2866      $attr   = $matches[4];
2867      $end    = $matches[5];
2868  
2869      if ( '' !== $slash ) {
2870          // Closing elements do not get parsed.
2871          return false;
2872      }
2873  
2874      // Is there a closing XHTML slash at the end of the attributes?
2875      if ( 1 === preg_match( '%\s*/\s*$%', $attr, $matches ) ) {
2876          $xhtml_slash = $matches[0];
2877          $attr        = substr( $attr, 0, -strlen( $xhtml_slash ) );
2878      } else {
2879          $xhtml_slash = '';
2880      }
2881  
2882      // Split it.
2883      $attrarr = wp_kses_hair_parse( $attr );
2884      if ( false === $attrarr ) {
2885          return false;
2886      }
2887  
2888      // Make sure all input is returned by adding front and back matter.
2889      array_unshift( $attrarr, $begin . $slash . $elname );
2890      array_push( $attrarr, $xhtml_slash . $end );
2891  
2892      return $attrarr;
2893  }
2894  
2895  /**
2896   * Builds an attribute list from string containing attributes.
2897   *
2898   * Does not modify input.  May return "evil" output.
2899   * In case of unexpected input, returns false instead of stripping things.
2900   *
2901   * Based on `wp_kses_hair()` but does not return a multi-dimensional array.
2902   *
2903   * @since 4.2.3
2904   *
2905   * @param string $attr Attribute list from HTML element to closing HTML element tag.
2906   * @return array|false List of attributes found in $attr. Returns false on failure.
2907   */
2908  function wp_kses_hair_parse( $attr ) {
2909      if ( '' === $attr ) {
2910          return array();
2911      }
2912  
2913      $regex =
2914          '(?:
2915                  [_a-zA-Z][-_a-zA-Z0-9:.]* # Attribute name.
2916              |
2917                  \[\[?[^\[\]]+\]\]?        # Shortcode in the name position implies unfiltered_html.
2918          )
2919          (?:                               # Attribute value.
2920              \s*=\s*                       # All values begin with "=".
2921              (?:
2922                  "[^"]*"                   # Double-quoted.
2923              |
2924                  \'[^\']*\'                # Single-quoted.
2925              |
2926                  [^\s"\']+                 # Non-quoted.
2927                  (?:\s|$)                  # Must have a space.
2928              )
2929          |
2930              (?:\s|$)                      # If attribute has no value, space is required.
2931          )
2932          \s*                               # Trailing space is optional except as mentioned above.
2933          ';
2934  
2935      /*
2936       * Although it is possible to reduce this procedure to a single regexp,
2937       * we must run that regexp twice to get exactly the expected result.
2938       *
2939       * Note: do NOT remove the `x` modifiers as they are essential for the above regex!
2940       */
2941  
2942      $validation = "/^($regex)+$/x";
2943      $extraction = "/$regex/x";
2944  
2945      if ( 1 === preg_match( $validation, $attr ) ) {
2946          preg_match_all( $extraction, $attr, $attrarr );
2947          return $attrarr[0];
2948      } else {
2949          return false;
2950      }
2951  }
2952  
2953  /**
2954   * Performs different checks for attribute values.
2955   *
2956   * The currently implemented checks are "maxlen", "minlen", "maxval", "minval",
2957   * and "valueless".
2958   *
2959   * @since 1.0.0
2960   *
2961   * @param string $value      Attribute value.
2962   * @param string $vless      Whether the attribute is valueless. Use 'y' or 'n'.
2963   * @param string $checkname  What $checkvalue is checking for.
2964   * @param mixed  $checkvalue What constraint the value should pass.
2965   * @return bool Whether check passes.
2966   */
2967  function wp_kses_check_attr_val( $value, $vless, $checkname, $checkvalue ) {
2968      $ok = true;
2969  
2970      switch ( strtolower( $checkname ) ) {
2971          case 'maxlen':
2972              /*
2973               * The maxlen check makes sure that the attribute value has a length not
2974               * greater than the given value. This can be used to avoid Buffer Overflows
2975               * in WWW clients and various Internet servers.
2976               */
2977  
2978              if ( strlen( $value ) > $checkvalue ) {
2979                  $ok = false;
2980              }
2981              break;
2982  
2983          case 'minlen':
2984              /*
2985               * The minlen check makes sure that the attribute value has a length not
2986               * smaller than the given value.
2987               */
2988  
2989              if ( strlen( $value ) < $checkvalue ) {
2990                  $ok = false;
2991              }
2992              break;
2993  
2994          case 'maxval':
2995              /*
2996               * The maxval check does two things: it checks that the attribute value is
2997               * an integer from 0 and up, without an excessive amount of zeroes or
2998               * whitespace (to avoid Buffer Overflows). It also checks that the attribute
2999               * value is not greater than the given value.
3000               * This check can be used to avoid Denial of Service attacks.
3001               */
3002  
3003              if ( ! preg_match( '/^\s{0,6}[0-9]{1,6}\s{0,6}$/', $value ) ) {
3004                  $ok = false;
3005              }
3006              if ( $value > $checkvalue ) {
3007                  $ok = false;
3008              }
3009              break;
3010  
3011          case 'minval':
3012              /*
3013               * The minval check makes sure that the attribute value is a positive integer,
3014               * and that it is not smaller than the given value.
3015               */
3016  
3017              if ( ! preg_match( '/^\s{0,6}[0-9]{1,6}\s{0,6}$/', $value ) ) {
3018                  $ok = false;
3019              }
3020              if ( $value < $checkvalue ) {
3021                  $ok = false;
3022              }
3023              break;
3024  
3025          case 'valueless':
3026              /*
3027               * The valueless check makes sure if the attribute has a value
3028               * (like `<a href="blah">`) or not (`<option selected>`). If the given value
3029               * is a "y" or a "Y", the attribute must not have a value.
3030               * If the given value is an "n" or an "N", the attribute must have a value.
3031               */
3032  
3033              if ( strtolower( $checkvalue ) !== $vless ) {
3034                  $ok = false;
3035              }
3036              break;
3037  
3038          case 'values':
3039              /*
3040               * The values check is used when you want to make sure that the attribute
3041               * has one of the given values.
3042               */
3043  
3044              if ( ! in_array( strtolower( $value ), $checkvalue, true ) ) {
3045                  $ok = false;
3046              }
3047              break;
3048  
3049          case 'value_callback':
3050              /*
3051               * The value_callback check is used when you want to make sure that the attribute
3052               * value is accepted by the callback function.
3053               */
3054  
3055              if ( ! call_user_func( $checkvalue, $value ) ) {
3056                  $ok = false;
3057              }
3058              break;
3059      } // End switch.
3060  
3061      return $ok;
3062  }
3063  
3064  /**
3065   * Sanitizes a string and removed disallowed URL protocols.
3066   *
3067   * This function removes all non-allowed protocols from the beginning of the
3068   * string. It ignores whitespace and the case of the letters, and it does
3069   * understand HTML entities. It does its work recursively, so it won't be
3070   * fooled by a string like `javascript:javascript:alert(57)`.
3071   *
3072   * @since 1.0.0
3073   *
3074   * @param string   $content           Content to filter bad protocols from.
3075   * @param string[] $allowed_protocols Array of allowed URL protocols.
3076   * @return string Filtered content.
3077   */
3078  function wp_kses_bad_protocol( $content, $allowed_protocols ) {
3079      $content = wp_kses_no_null( $content );
3080  
3081      // Short-circuit if the string starts with `https://` or `http://`. Most common cases.
3082      if (
3083          ( str_starts_with( $content, 'https://' ) && in_array( 'https', $allowed_protocols, true ) ) ||
3084          ( str_starts_with( $content, 'http://' ) && in_array( 'http', $allowed_protocols, true ) )
3085      ) {
3086          return $content;
3087      }
3088  
3089      $iterations = 0;
3090  
3091      do {
3092          $original_content = $content;
3093          $content          = wp_kses_bad_protocol_once( $content, $allowed_protocols );
3094      } while ( $original_content !== $content && ++$iterations < 6 );
3095  
3096      if ( $original_content !== $content ) {
3097          return '';
3098      }
3099  
3100      return $content;
3101  }
3102  
3103  /**
3104   * Removes any invalid control characters in a text string.
3105   *
3106   * Also removes any instance of the `\0` string.
3107   *
3108   * @since 1.0.0
3109   *
3110   * @param string $content Content to filter null characters from.
3111   * @param array  $options Set 'slash_zero' => 'keep' when '\0' is allowed. Default is 'remove'.
3112   * @return string Filtered content.
3113   */
3114  function wp_kses_no_null( $content, $options = null ) {
3115      if ( ! isset( $options['slash_zero'] ) ) {
3116          $options = array( 'slash_zero' => 'remove' );
3117      }
3118  
3119      $content = preg_replace( '/[\x00-\x08\x0B\x0C\x0E-\x1F]/', '', $content );
3120      if ( 'remove' === $options['slash_zero'] ) {
3121          $content = preg_replace( '/\\\\+0+/', '', $content );
3122      }
3123  
3124      return $content;
3125  }
3126  
3127  /**
3128   * Strips slashes from in front of quotes.
3129   *
3130   * This function changes the character sequence `\"` to just `"`. It leaves all other
3131   * slashes alone. The quoting from `preg_replace(//e)` requires this.
3132   *
3133   * @since 1.0.0
3134   *
3135   * @param string $content String to strip slashes from.
3136   * @return string Fixed string with quoted slashes.
3137   */
3138  function wp_kses_stripslashes( $content ) {
3139      return preg_replace( '%\\\\"%', '"', $content );
3140  }
3141  
3142  /**
3143   * Converts the keys of an array to lowercase.
3144   *
3145   * @since 1.0.0
3146   *
3147   * @param array $inarray Unfiltered array.
3148   * @return array Fixed array with all lowercase keys.
3149   */
3150  function wp_kses_array_lc( $inarray ) {
3151      $outarray = array();
3152  
3153      foreach ( (array) $inarray as $inkey => $inval ) {
3154          $outkey              = strtolower( $inkey );
3155          $outarray[ $outkey ] = array();
3156  
3157          foreach ( (array) $inval as $inkey2 => $inval2 ) {
3158              $outkey2                         = strtolower( $inkey2 );
3159              $outarray[ $outkey ][ $outkey2 ] = $inval2;
3160          }
3161      }
3162  
3163      return $outarray;
3164  }
3165  
3166  /**
3167   * Sanitizes content from bad protocols and other characters.
3168   *
3169   * This function searches for URL protocols at the beginning of the string, while
3170   * handling whitespace and HTML entities.
3171   *
3172   * @since 1.0.0
3173   *
3174   * @param string   $content           Content to check for bad protocols.
3175   * @param string[] $allowed_protocols Array of allowed URL protocols.
3176   * @param int      $count             Depth of call recursion to this function.
3177   * @return string Sanitized content.
3178   */
3179  function wp_kses_bad_protocol_once( $content, $allowed_protocols, $count = 1 ) {
3180      $content  = preg_replace( '/(&#0*58(?![;0-9])|&#x0*3a(?![;a-f0-9]))/i', '$1;', $content );
3181      $content2 = preg_split( '/:|&#0*58;|&#x0*3a;|&colon;/i', $content, 2 );
3182  
3183      if ( isset( $content2[1] ) && ! preg_match( '%/\?%', $content2[0] ) ) {
3184          $content  = trim( $content2[1] );
3185          $protocol = wp_kses_bad_protocol_once2( $content2[0], $allowed_protocols );
3186          if ( 'feed:' === $protocol ) {
3187              if ( $count > 2 ) {
3188                  return '';
3189              }
3190              $content = wp_kses_bad_protocol_once( $content, $allowed_protocols, ++$count );
3191              if ( empty( $content ) ) {
3192                  return $content;
3193              }
3194          }
3195          $content = $protocol . $content;
3196      }
3197  
3198      return $content;
3199  }
3200  
3201  /**
3202   * Callback for `wp_kses_bad_protocol_once()` regular expression.
3203   *
3204   * This function processes URL protocols, checks to see if they're in the
3205   * list of allowed protocols or not, and returns different data depending
3206   * on the answer.
3207   *
3208   * @access private
3209   * @ignore
3210   * @since 1.0.0
3211   *
3212   * @param string   $scheme            URI scheme to check against the list of allowed protocols.
3213   * @param string[] $allowed_protocols Array of allowed URL protocols.
3214   * @return string Sanitized content.
3215   */
3216  function wp_kses_bad_protocol_once2( $scheme, $allowed_protocols ) {
3217      $scheme = wp_kses_decode_entities( $scheme );
3218      $scheme = preg_replace( '/\s/', '', $scheme );
3219      $scheme = wp_kses_no_null( $scheme );
3220      $scheme = strtolower( $scheme );
3221  
3222      $allowed = array_any( (array) $allowed_protocols, fn( $protocol ) => strtolower( $protocol ) === $scheme );
3223  
3224      if ( $allowed ) {
3225          return "$scheme:";
3226      } else {
3227          return '';
3228      }
3229  }
3230  
3231  /**
3232   * Converts and fixes HTML entities.
3233   *
3234   * This function normalizes HTML entities. It will convert `AT&T` to the correct
3235   * `AT&amp;T`, `&#00058;` to `&#058;`, `&#XYZZY;` to `&amp;#XYZZY;` and so on.
3236   *
3237   * When `$context` is set to 'xml', HTML entities are converted to their code points.  For
3238   * example, `AT&T&hellip;&#XYZZY;` is converted to `AT&amp;T…&amp;#XYZZY;`.
3239   *
3240   * @since 1.0.0
3241   * @since 5.5.0 Added `$context` parameter.
3242   *
3243   * @param string $content Content to normalize entities.
3244   * @param string $context Context for normalization. Can be either 'html' or 'xml'.
3245   *                        Default 'html'.
3246   * @return string Content with normalized entities.
3247   */
3248  function wp_kses_normalize_entities( $content, $context = 'html' ) {
3249      // Disarm all entities by converting & to &amp;
3250      $content = str_replace( '&', '&amp;', $content );
3251  
3252      /*
3253       * Decode any character references that are now double-encoded.
3254       *
3255       * It's important that the following normalizations happen in the correct order.
3256       *
3257       * At this point, all `&` have been transformed to `&amp;`. Double-encoded named character
3258       * references like `&amp;amp;` will be decoded back to their single-encoded form `&amp;`.
3259       *
3260       * First, numeric (decimal and hexadecimal) character references must be handled so that
3261       * `&amp;#09;` becomes `&#9;`. If the named character references were handled first, there
3262       * would be no way to know whether the double-encoded character reference had been produced
3263       * in this function or was the original input.
3264       *
3265       * Consider the two examples, first with named entity decoding followed by numeric
3266       * entity decoding. We'll use U+002E FULL STOP (.) in our example, this table follows the
3267       * string processing from left to right:
3268       *
3269       * | Input        | &-encoded        | Named ref double-decoded  | Numeric ref double-decoded |
3270       * | ------------ | ---------------- | ------------------------- | -------------------------- |
3271       * | `&#x2E;`     | `&amp;#x2E;`     | `&amp;#x2E;`              | `&#x2E;`                   |
3272       * | `&amp;#x2E;` | `&amp;amp;#x2E;` | `&amp;#x2E;`              | `&#x2E;`                   |
3273       *
3274       * Notice in the example above that different inputs result in the same result. The second case
3275       * was not normalized and produced HTML that is semantically different from the input.
3276       *
3277       * | Input        | &-encoded        |  Numeric ref double-decoded | Named ref double-decoded |
3278       * | ------------ | ---------------- | --------------------------- | ------------------------ |
3279       * | `&#x2E;`     | `&amp;#x2E;`     | `&#x2E;`                    | `&#x2E;`                 |
3280       * | `&amp;#x2E;` | `&amp;amp;#x2E;` | `&amp;amp;#x2E;`            | `&amp;#x2E;`             |
3281       *
3282       * Here, each input is normalized to an appropriate output.
3283       */
3284      $content = preg_replace_callback( '/&amp;#(0*[1-9][0-9]{0,6});/', 'wp_kses_normalize_entities2', $content );
3285      $content = preg_replace_callback( '/&amp;#[Xx](0*[1-9A-Fa-f][0-9A-Fa-f]{0,5});/', 'wp_kses_normalize_entities3', $content );
3286      if ( 'xml' === $context ) {
3287          $content = preg_replace_callback( '/&amp;([A-Za-z]{2,8}[0-9]{0,2});/', 'wp_kses_xml_named_entities', $content );
3288      } else {
3289          $content = preg_replace_callback( '/&amp;([A-Za-z]{2,8}[0-9]{0,2});/', 'wp_kses_named_entities', $content );
3290      }
3291  
3292      return $content;
3293  }
3294  
3295  /**
3296   * Callback for `wp_kses_normalize_entities()` regular expression.
3297   *
3298   * This function only accepts valid named entity references, which are finite,
3299   * case-sensitive, and highly scrutinized by HTML and XML validators.
3300   *
3301   * @since 3.0.0
3302   *
3303   * @global array $allowedentitynames
3304   *
3305   * @param array $matches preg_replace_callback() matches array.
3306   * @return string Correctly encoded entity.
3307   */
3308  function wp_kses_named_entities( $matches ) {
3309      global $allowedentitynames;
3310  
3311      if ( empty( $matches[1] ) ) {
3312          return '';
3313      }
3314  
3315      $i = $matches[1];
3316      return ( ! in_array( $i, $allowedentitynames, true ) ) ? "&amp;$i;" : "&$i;";
3317  }
3318  
3319  /**
3320   * Callback for `wp_kses_normalize_entities()` regular expression.
3321   *
3322   * This function only accepts valid named entity references, which are finite,
3323   * case-sensitive, and highly scrutinized by XML validators.  HTML named entity
3324   * references are converted to their code points.
3325   *
3326   * @since 5.5.0
3327   *
3328   * @global array $allowedentitynames
3329   * @global array $allowedxmlentitynames
3330   *
3331   * @param array $matches preg_replace_callback() matches array.
3332   * @return string Correctly encoded entity.
3333   */
3334  function wp_kses_xml_named_entities( $matches ) {
3335      global $allowedentitynames, $allowedxmlentitynames;
3336  
3337      if ( empty( $matches[1] ) ) {
3338          return '';
3339      }
3340  
3341      $i = $matches[1];
3342  
3343      if ( in_array( $i, $allowedxmlentitynames, true ) ) {
3344          return "&$i;";
3345      } elseif ( in_array( $i, $allowedentitynames, true ) ) {
3346          return html_entity_decode( "&$i;", ENT_HTML5 );
3347      }
3348  
3349      return "&amp;$i;";
3350  }
3351  
3352  /**
3353   * Callback for `wp_kses_normalize_entities()` regular expression.
3354   *
3355   * This function helps `wp_kses_normalize_entities()` to only accept 16-bit
3356   * values and nothing more for `&#number;` entities.
3357   *
3358   * @access private
3359   * @ignore
3360   * @since 1.0.0
3361   *
3362   * @param array $matches `preg_replace_callback()` matches array.
3363   * @return string Correctly encoded entity.
3364   */
3365  function wp_kses_normalize_entities2( $matches ) {
3366      if ( empty( $matches[1] ) ) {
3367          return '';
3368      }
3369  
3370      $i = $matches[1];
3371  
3372      if ( valid_unicode( $i ) ) {
3373          $i = str_pad( ltrim( $i, '0' ), 3, '0', STR_PAD_LEFT );
3374          $i = "&#$i;";
3375      } else {
3376          $i = "&amp;#$i;";
3377      }
3378  
3379      return $i;
3380  }
3381  
3382  /**
3383   * Callback for `wp_kses_normalize_entities()` for regular expression.
3384   *
3385   * This function helps `wp_kses_normalize_entities()` to only accept valid Unicode
3386   * numeric entities in hex form.
3387   *
3388   * @since 2.7.0
3389   * @access private
3390   * @ignore
3391   *
3392   * @param array $matches `preg_replace_callback()` matches array.
3393   * @return string Correctly encoded entity.
3394   */
3395  function wp_kses_normalize_entities3( $matches ) {
3396      if ( empty( $matches[1] ) ) {
3397          return '';
3398      }
3399  
3400      $hexchars = $matches[1];
3401  
3402      return ( ! valid_unicode( hexdec( $hexchars ) ) ) ? "&amp;#x$hexchars;" : '&#x' . ltrim( $hexchars, '0' ) . ';';
3403  }
3404  
3405  /**
3406   * Determines if a Unicode codepoint is valid.
3407   *
3408   * The definition of a valid Unicode codepoint is taken from the XML definition:
3409   *
3410   * > Characters
3411   * >
3412   * > …
3413   * > Legal characters are tab, carriage return, line feed, and the legal characters of
3414   * > Unicode and ISO/IEC 10646.
3415   * > …
3416   * > Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
3417   *
3418   * @since 2.7.0
3419   *
3420   * @link https://www.w3.org/TR/xml/#charsets
3421   *
3422   * @param int $i Unicode codepoint.
3423   * @return bool Whether or not the codepoint is a valid Unicode codepoint.
3424   */
3425  function valid_unicode( $i ) {
3426      $i = (int) $i;
3427  
3428      return (
3429          0x9 === $i || // U+0009 HORIZONTAL TABULATION (HT)
3430          0xA === $i || // U+000A LINE FEED (LF)
3431          0xD === $i || // U+000D CARRIAGE RETURN (CR)
3432          /*
3433           * The valid Unicode characters according to the XML specification:
3434           *
3435           * > any Unicode character, excluding the surrogate blocks, FFFE, and FFFF.
3436           */
3437          ( 0x20 <= $i && $i <= 0xD7FF ) ||
3438          ( 0xE000 <= $i && $i <= 0xFFFD ) ||
3439          ( 0x10000 <= $i && $i <= 0x10FFFF )
3440      );
3441  }
3442  
3443  /**
3444   * Converts all numeric HTML entities to their named counterparts.
3445   *
3446   * This function decodes numeric HTML entities (`&#65;` and `&#x41;`).
3447   * It doesn't do anything with named entities like `&auml;`, but we don't
3448   * need them in the allowed URL protocols system anyway.
3449   *
3450   * @since 1.0.0
3451   *
3452   * @param string $content Content to change entities.
3453   * @return string Content after decoded entities.
3454   */
3455  function wp_kses_decode_entities( $content ) {
3456      $content = preg_replace_callback( '/&#([0-9]+);/', '_wp_kses_decode_entities_chr', $content );
3457      $content = preg_replace_callback( '/&#[Xx]([0-9A-Fa-f]+);/', '_wp_kses_decode_entities_chr_hexdec', $content );
3458  
3459      return $content;
3460  }
3461  
3462  /**
3463   * Regex callback for `wp_kses_decode_entities()`.
3464   *
3465   * @since 2.9.0
3466   * @access private
3467   * @ignore
3468   *
3469   * @param array $matches preg match
3470   * @return string
3471   */
3472  function _wp_kses_decode_entities_chr( $matches ) {
3473      return chr( $matches[1] );
3474  }
3475  
3476  /**
3477   * Regex callback for `wp_kses_decode_entities()`.
3478   *
3479   * @since 2.9.0
3480   * @access private
3481   * @ignore
3482   *
3483   * @param array $matches preg match
3484   * @return string
3485   */
3486  function _wp_kses_decode_entities_chr_hexdec( $matches ) {
3487      return chr( hexdec( $matches[1] ) );
3488  }
3489  
3490  /**
3491   * Sanitize content with allowed HTML KSES rules.
3492   *
3493   * This function expects slashed data.
3494   *
3495   * @since 1.0.0
3496   *
3497   * @param string $data Content to filter, expected to be escaped with slashes.
3498   * @return string Filtered content.
3499   */
3500  function wp_filter_kses( $data ) {
3501      return addslashes( wp_kses( stripslashes( $data ), current_filter() ) );
3502  }
3503  
3504  /**
3505   * Sanitize content with allowed HTML KSES rules.
3506   *
3507   * This function expects unslashed data.
3508   *
3509   * @since 2.9.0
3510   *
3511   * @param string $data Content to filter, expected to not be escaped.
3512   * @return string Filtered content.
3513   */
3514  function wp_kses_data( $data ) {
3515      return wp_kses( $data, current_filter() );
3516  }
3517  
3518  /**
3519   * Sanitizes content for allowed HTML tags for post content.
3520   *
3521   * Post content refers to the page contents of the 'post' type and not `$_POST`
3522   * data from forms.
3523   *
3524   * This function expects slashed data.
3525   *
3526   * @since 2.0.0
3527   *
3528   * @param string $data Post content to filter, expected to be escaped with slashes.
3529   * @return string Filtered post content with allowed HTML tags and attributes intact.
3530   */
3531  function wp_filter_post_kses( $data ) {
3532      return addslashes( wp_kses( stripslashes( $data ), 'post' ) );
3533  }
3534  
3535  /**
3536   * Sanitizes global styles user content removing unsafe rules.
3537   *
3538   * @since 5.9.0
3539   *
3540   * @param string $data Post content to filter.
3541   * @return string Filtered post content with unsafe rules removed.
3542   */
3543  function wp_filter_global_styles_post( $data ) {
3544      $decoded_data        = json_decode( wp_unslash( $data ), true );
3545      $json_decoding_error = json_last_error();
3546      if (
3547          JSON_ERROR_NONE === $json_decoding_error &&
3548          is_array( $decoded_data ) &&
3549          isset( $decoded_data['isGlobalStylesUserThemeJSON'] ) &&
3550          $decoded_data['isGlobalStylesUserThemeJSON']
3551      ) {
3552          unset( $decoded_data['isGlobalStylesUserThemeJSON'] );
3553  
3554          $data_to_encode = WP_Theme_JSON::remove_insecure_properties( $decoded_data, 'custom' );
3555  
3556          $data_to_encode['isGlobalStylesUserThemeJSON'] = true;
3557          /**
3558           * JSON encode the data stored in post content.
3559           * Escape characters that are likely to be mangled by HTML filters: "<>&".
3560           *
3561           * This matches the escaping in {@see WP_REST_Global_Styles_Controller::prepare_item_for_database()}.
3562           */
3563          return wp_slash( wp_json_encode( $data_to_encode, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ) );
3564      }
3565      return $data;
3566  }
3567  
3568  /**
3569   * Sanitizes content for allowed HTML tags for post content.
3570   *
3571   * Post content refers to the page contents of the 'post' type and not `$_POST`
3572   * data from forms.
3573   *
3574   * This function expects unslashed data.
3575   *
3576   * @since 2.9.0
3577   *
3578   * @param string $data Post content to filter.
3579   * @return string Filtered post content with allowed HTML tags and attributes intact.
3580   */
3581  function wp_kses_post( $data ) {
3582      return wp_kses( $data, 'post' );
3583  }
3584  
3585  /**
3586   * Navigates through an array, object, or scalar, and sanitizes content for
3587   * allowed HTML tags for post content.
3588   *
3589   * @since 4.4.2
3590   *
3591   * @see map_deep()
3592   *
3593   * @param mixed $data The array, object, or scalar value to inspect.
3594   * @return mixed The filtered content.
3595   */
3596  function wp_kses_post_deep( $data ) {
3597      return map_deep( $data, 'wp_kses_post' );
3598  }
3599  
3600  /**
3601   * Strips all HTML from a text string.
3602   *
3603   * This function expects slashed data.
3604   *
3605   * @since 2.1.0
3606   *
3607   * @param string $data Content to strip all HTML from.
3608   * @return string Filtered content without any HTML.
3609   */
3610  function wp_filter_nohtml_kses( $data ) {
3611      return addslashes( wp_kses( stripslashes( $data ), 'strip' ) );
3612  }
3613  
3614  /**
3615   * Adds all KSES input form content filters.
3616   *
3617   * All hooks have default priority. The `wp_filter_kses()` function is added to
3618   * the 'pre_comment_content' and 'title_save_pre' hooks.
3619   *
3620   * The `wp_filter_post_kses()` function is added to the 'content_save_pre',
3621   * 'excerpt_save_pre', and 'content_filtered_save_pre' hooks.
3622   *
3623   * @since 2.0.0
3624   */
3625  function kses_init_filters() {
3626      // Normal filtering.
3627      add_filter( 'title_save_pre', 'wp_filter_kses' );
3628  
3629      // Comment filtering.
3630      if ( current_user_can( 'unfiltered_html' ) ) {
3631          add_filter( 'pre_comment_content', 'wp_filter_post_kses' );
3632      } else {
3633          add_filter( 'pre_comment_content', 'wp_filter_kses' );
3634      }
3635  
3636      // Global Styles filtering: Global Styles filters should be executed before normal post_kses HTML filters.
3637      add_filter( 'content_save_pre', 'wp_filter_global_styles_post', 9 );
3638      add_filter( 'content_filtered_save_pre', 'wp_filter_global_styles_post', 9 );
3639  
3640      // Post filtering.
3641      add_filter( 'content_save_pre', 'wp_filter_post_kses' );
3642      add_filter( 'excerpt_save_pre', 'wp_filter_post_kses' );
3643      add_filter( 'content_filtered_save_pre', 'wp_filter_post_kses' );
3644  }
3645  
3646  /**
3647   * Removes all KSES input form content filters.
3648   *
3649   * A quick procedural method to removing all of the filters that KSES uses for
3650   * content in WordPress Loop.
3651   *
3652   * Does not remove the `kses_init()` function from {@see 'init'} hook (priority is
3653   * default). Also does not remove `kses_init()` function from {@see 'set_current_user'}
3654   * hook (priority is also default).
3655   *
3656   * @since 2.0.6
3657   */
3658  function kses_remove_filters() {
3659      // Normal filtering.
3660      remove_filter( 'title_save_pre', 'wp_filter_kses' );
3661  
3662      // Comment filtering.
3663      remove_filter( 'pre_comment_content', 'wp_filter_post_kses' );
3664      remove_filter( 'pre_comment_content', 'wp_filter_kses' );
3665  
3666      // Global Styles filtering.
3667      remove_filter( 'content_save_pre', 'wp_filter_global_styles_post', 9 );
3668      remove_filter( 'content_filtered_save_pre', 'wp_filter_global_styles_post', 9 );
3669  
3670      // Post filtering.
3671      remove_filter( 'content_save_pre', 'wp_filter_post_kses' );
3672      remove_filter( 'excerpt_save_pre', 'wp_filter_post_kses' );
3673      remove_filter( 'content_filtered_save_pre', 'wp_filter_post_kses' );
3674  }
3675  
3676  /**
3677   * Sets up most of the KSES filters for input form content.
3678   *
3679   * First removes all of the KSES filters in case the current user does not need
3680   * to have KSES filter the content. If the user does not have `unfiltered_html`
3681   * capability, then KSES filters are added.
3682   *
3683   * @since 2.0.0
3684   */
3685  function kses_init() {
3686      kses_remove_filters();
3687  
3688      if ( ! current_user_can( 'unfiltered_html' ) ) {
3689          kses_init_filters();
3690      }
3691  }
3692  
3693  /**
3694   * Filters an inline style attribute and removes disallowed rules.
3695   *
3696   * @since 2.8.1
3697   * @since 4.4.0 Added support for `min-height`, `max-height`, `min-width`, and `max-width`.
3698   * @since 4.6.0 Added support for `list-style-type`.
3699   * @since 5.0.0 Added support for `background-image`.
3700   * @since 5.1.0 Added support for `text-transform`.
3701   * @since 5.2.0 Added support for `background-position` and `grid-template-columns`.
3702   * @since 5.3.0 Added support for `grid`, `flex` and `column` layout properties.
3703   *              Extended `background-*` support for individual properties.
3704   * @since 5.3.1 Added support for gradient backgrounds.
3705   * @since 5.7.1 Added support for `object-position`.
3706   * @since 5.8.0 Added support for `calc()` and `var()` values.
3707   * @since 6.1.0 Added support for `min()`, `max()`, `minmax()`, `clamp()`,
3708   *              nested `var()` values, and assigning values to CSS variables.
3709   *              Added support for `object-fit`, `gap`, `column-gap`, `row-gap`, and `flex-wrap`.
3710   *              Extended `margin-*` and `padding-*` support for logical properties.
3711   * @since 6.2.0 Added support for `aspect-ratio`, `position`, `top`, `right`, `bottom`, `left`,
3712   *              and `z-index` CSS properties.
3713   * @since 6.3.0 Extended support for `filter` to accept a URL and added support for repeat().
3714   *              Added support for `box-shadow`.
3715   * @since 6.4.0 Added support for `writing-mode`.
3716   * @since 6.5.0 Added support for `background-repeat`.
3717   * @since 6.6.0 Added support for `grid-column`, `grid-row`, and `container-type`.
3718   * @since 6.9.0 Added support for `white-space`.
3719   * @since 7.1.0 Extended gradient support to allow any single-level nested function.
3720   *              Added support for transform functions, `clip-path` basic shapes,
3721   *              and URLs in the SVG element reference properties.
3722   * @since 7.2.0 Added support for CSS anchor positioning properties and `text-shadow`.
3723   *              Added support for `background-clip`.
3724   *
3725   * @param string $css        A string of CSS rules, decoded from an HTML `style` attribute.
3726   * @param string $deprecated Not used.
3727   * @return string Filtered string of CSS rules, needing HTML escaping before sending back to a `style` attribute.
3728   *
3729   * @phpstan-param '' $deprecated
3730   */
3731  function safecss_filter_attr( $css, $deprecated = '' ) {
3732      if ( ! empty( $deprecated ) ) {
3733          _deprecated_argument( __FUNCTION__, '2.8.1' ); // Never implemented.
3734      }
3735  
3736      $css = wp_kses_no_null( $css );
3737      $css = str_replace( array( "\n", "\r", "\t" ), '', $css );
3738  
3739      $allowed_protocols = wp_allowed_protocols();
3740  
3741      /** @todo Parse enough CSS to split rules without breaking on things like quoted strings. */
3742      $css_array = explode( ';', trim( $css ) );
3743  
3744      /**
3745       * Filters the list of allowed CSS attributes.
3746       *
3747       * @since 2.8.1
3748       * @since 7.1.0 Added support for SVG presentation attributes.
3749       * @since 7.2.0 Added support for `background-clip` and its vendor prefixed companions.
3750       *
3751       * @param string[] $attr Array of allowed CSS attributes.
3752       */
3753      $allowed_attr = apply_filters(
3754          'safe_style_css',
3755          array(
3756              'background',
3757              'background-color',
3758              'background-image',
3759              'background-position',
3760              'background-repeat',
3761              'background-size',
3762              'background-attachment',
3763              'background-blend-mode',
3764              'background-clip',
3765              '-webkit-background-clip',
3766              '-webkit-text-fill-color',
3767  
3768              'border',
3769              'border-radius',
3770              'border-width',
3771              'border-color',
3772              'border-style',
3773              'border-right',
3774              'border-right-color',
3775              'border-right-style',
3776              'border-right-width',
3777              'border-bottom',
3778              'border-bottom-color',
3779              'border-bottom-left-radius',
3780              'border-bottom-right-radius',
3781              'border-bottom-style',
3782              'border-bottom-width',
3783              'border-bottom-right-radius',
3784              'border-bottom-left-radius',
3785              'border-left',
3786              'border-left-color',
3787              'border-left-style',
3788              'border-left-width',
3789              'border-top',
3790              'border-top-color',
3791              'border-top-left-radius',
3792              'border-top-right-radius',
3793              'border-top-style',
3794              'border-top-width',
3795              'border-top-left-radius',
3796              'border-top-right-radius',
3797  
3798              'border-spacing',
3799              'border-collapse',
3800              'caption-side',
3801  
3802              'columns',
3803              'column-count',
3804              'column-fill',
3805              'column-gap',
3806              'column-rule',
3807              'column-span',
3808              'column-width',
3809  
3810              'display',
3811  
3812              'color',
3813              'filter',
3814              'font',
3815              'font-family',
3816              'font-size',
3817              'font-style',
3818              'font-variant',
3819              'font-weight',
3820              'letter-spacing',
3821              'line-height',
3822              'text-align',
3823              'text-decoration',
3824              'text-indent',
3825              'text-shadow',
3826              'text-transform',
3827              'white-space',
3828  
3829              'height',
3830              'min-height',
3831              'max-height',
3832  
3833              'width',
3834              'min-width',
3835              'max-width',
3836  
3837              'margin',
3838              'margin-right',
3839              'margin-bottom',
3840              'margin-left',
3841              'margin-top',
3842              'margin-block-start',
3843              'margin-block-end',
3844              'margin-inline-start',
3845              'margin-inline-end',
3846  
3847              'padding',
3848              'padding-right',
3849              'padding-bottom',
3850              'padding-left',
3851              'padding-top',
3852              'padding-block-start',
3853              'padding-block-end',
3854              'padding-inline-start',
3855              'padding-inline-end',
3856  
3857              'flex',
3858              'flex-basis',
3859              'flex-direction',
3860              'flex-flow',
3861              'flex-grow',
3862              'flex-shrink',
3863              'flex-wrap',
3864  
3865              'gap',
3866              'column-gap',
3867              'row-gap',
3868  
3869              'grid-template-columns',
3870              'grid-auto-columns',
3871              'grid-column-start',
3872              'grid-column-end',
3873              'grid-column',
3874              'grid-column-gap',
3875              'grid-template-rows',
3876              'grid-auto-rows',
3877              'grid-row-start',
3878              'grid-row-end',
3879              'grid-row',
3880              'grid-row-gap',
3881              'grid-gap',
3882  
3883              'justify-content',
3884              'justify-items',
3885              'justify-self',
3886              'align-content',
3887              'align-items',
3888              'align-self',
3889  
3890              'clear',
3891              'cursor',
3892              'direction',
3893              'float',
3894              'list-style-type',
3895              'object-fit',
3896              'object-position',
3897              'opacity',
3898              'overflow',
3899              'vertical-align',
3900              'writing-mode',
3901  
3902              'position',
3903              'top',
3904              'right',
3905              'bottom',
3906              'left',
3907              'z-index',
3908              'box-shadow',
3909              'aspect-ratio',
3910              'container-type',
3911  
3912              'fill',
3913              'fill-opacity',
3914              'fill-rule',
3915  
3916              'stroke',
3917              'stroke-dasharray',
3918              'stroke-dashoffset',
3919              'stroke-linecap',
3920              'stroke-linejoin',
3921              'stroke-miterlimit',
3922              'stroke-opacity',
3923              'stroke-width',
3924  
3925              'color-interpolation',
3926              'color-interpolation-filters',
3927              'paint-order',
3928              'stop-color',
3929              'stop-opacity',
3930              'flood-color',
3931              'flood-opacity',
3932              'lighting-color',
3933  
3934              'marker',
3935              'marker-end',
3936              'marker-mid',
3937              'marker-start',
3938  
3939              'clip-path',
3940              'clip-rule',
3941              'mask',
3942              'mask-type',
3943  
3944              'cx',
3945              'cy',
3946              'r',
3947              'rx',
3948              'ry',
3949              'x',
3950              'y',
3951              'd',
3952  
3953              'alignment-baseline',
3954              'baseline-shift',
3955              'dominant-baseline',
3956              'glyph-orientation-horizontal',
3957              'glyph-orientation-vertical',
3958              'text-anchor',
3959              'unicode-bidi',
3960              'word-spacing',
3961  
3962              'font-size-adjust',
3963              'font-stretch',
3964  
3965              'color-rendering',
3966              'image-rendering',
3967              'shape-rendering',
3968              'text-rendering',
3969              'vector-effect',
3970  
3971              'transform',
3972              'transform-origin',
3973  
3974              'pointer-events',
3975              'visibility',
3976  
3977              'anchor-name',
3978              'anchor-scope',
3979              'position-anchor',
3980              'position-area',
3981              'position-try',
3982              'position-try-fallbacks',
3983              'position-try-order',
3984              'position-visibility',
3985  
3986              // Custom CSS properties.
3987              '--*',
3988          )
3989      );
3990  
3991      /*
3992       * CSS attributes that accept URL data types.
3993       *
3994       * This is in accordance to the CSS spec and unrelated to
3995       * the sub-set of supported attributes above.
3996       *
3997       * See: https://developer.mozilla.org/en-US/docs/Web/CSS/url
3998       */
3999      $css_url_data_types = array(
4000          'background',
4001          'background-image',
4002  
4003          'cursor',
4004          'filter',
4005  
4006          'list-style',
4007          'list-style-image',
4008  
4009          // SVG presentation properties that accept url() references.
4010          'clip-path',
4011          'fill',
4012          'marker',
4013          'marker-end',
4014          'marker-mid',
4015          'marker-start',
4016          'mask',
4017          'stroke',
4018      );
4019  
4020      /*
4021       * CSS attributes that accept gradient data types.
4022       *
4023       */
4024      $css_gradient_data_types = array(
4025          'background',
4026          'background-image',
4027      );
4028  
4029      if ( empty( $allowed_attr ) ) {
4030          return $css;
4031      }
4032  
4033      $css = '';
4034      foreach ( $css_array as $css_item ) {
4035          if ( '' === $css_item ) {
4036              continue;
4037          }
4038  
4039          $css_item        = trim( $css_item );
4040          $css_test_string = $css_item;
4041          $found           = false;
4042          $url_attr        = false;
4043          $gradient_attr   = false;
4044          $is_custom_var   = false;
4045  
4046          if ( ! str_contains( $css_item, ':' ) ) {
4047              $found = true;
4048          } else {
4049              $parts        = explode( ':', $css_item, 2 );
4050              $css_selector = trim( $parts[0] );
4051  
4052              // Allow assigning values to CSS variables.
4053              if ( in_array( '--*', $allowed_attr, true ) && preg_match( '/^--[a-zA-Z0-9-_]+$/', $css_selector ) ) {
4054                  $allowed_attr[] = $css_selector;
4055                  $is_custom_var  = true;
4056              }
4057  
4058              if ( in_array( $css_selector, $allowed_attr, true ) ) {
4059                  $found         = true;
4060                  $url_attr      = in_array( $css_selector, $css_url_data_types, true );
4061                  $gradient_attr = in_array( $css_selector, $css_gradient_data_types, true );
4062              }
4063  
4064              if ( $is_custom_var ) {
4065                  $css_value     = trim( $parts[1] );
4066                  $url_attr      = str_starts_with( $css_value, 'url(' );
4067                  $gradient_attr = str_contains( $css_value, '-gradient(' );
4068              }
4069          }
4070  
4071          if ( $found && $url_attr ) {
4072              // Simplified: matches the sequence `url(*)`.
4073              preg_match_all( '/url\([^)]+\)/', $parts[1], $url_matches );
4074  
4075              foreach ( $url_matches[0] as $url_match ) {
4076                  // Clean up the URL from each of the matches above.
4077                  preg_match( '/^url\(\s*([\'\"]?)(.*)(\g1)\s*\)$/', $url_match, $url_pieces );
4078  
4079                  if ( empty( $url_pieces[2] ) ) {
4080                      $found = false;
4081                      break;
4082                  }
4083  
4084                  $url = trim( $url_pieces[2] );
4085  
4086                  if ( empty( $url ) || wp_kses_bad_protocol( $url, $allowed_protocols ) !== $url ) {
4087                      $found = false;
4088                      break;
4089                  } else {
4090                      // Remove the whole `url(*)` bit that was matched above from the CSS.
4091                      $css_test_string = str_replace( $url_match, '', $css_test_string );
4092                  }
4093              }
4094          }
4095  
4096          if ( $found && $gradient_attr ) {
4097              /*
4098               * Match every `*-gradient()` in the value, allowing one level of nested functions
4099               * (e.g. rgb(), hsl(), var()). Matching each occurrence, rather than requiring the
4100               * whole value to be a single gradient, lets a gradient combine with a url() image.
4101               */
4102              preg_match_all( '/(?:repeating-)?(?:linear|radial|conic)-gradient\((?:[^()]|\([^()]*\))*\)/', $css_test_string, $gradient_matches );
4103  
4104              foreach ( $gradient_matches[0] as $gradient_match ) {
4105                  // Remove each `gradient()` bit that was matched above from the CSS.
4106                  $css_test_string = str_replace( $gradient_match, '', $css_test_string );
4107              }
4108          }
4109  
4110          if ( $found ) {
4111              /*
4112               * Allow CSS functions like var(), calc(), etc. by removing them from the test string.
4113               * Nested functions and parentheses are also removed, so long as the parentheses are balanced.
4114               */
4115              $css_test_string = preg_replace(
4116                  '/\b(?:'
4117                      // General purpose value functions.
4118                      . 'var|calc|min|max|minmax|clamp|repeat'
4119                      // Transform functions.
4120                      . '|matrix|matrix3d|perspective'
4121                      . '|rotate|rotate3d|rotateX|rotateY|rotateZ'
4122                      . '|scale|scale3d|scaleX|scaleY|scaleZ'
4123                      . '|skew|skewX|skewY'
4124                      . '|translate|translate3d|translateX|translateY|translateZ'
4125                      // Basic shape functions, as used by `clip-path`.
4126                      . '|circle|ellipse|inset|path|polygon|rect|shape|xywh'
4127                  . ')(\((?:[^()]|(?1))*\))/',
4128                  '',
4129                  $css_test_string
4130              );
4131  
4132              // Bail if the recursive function stripping hit a PCRE error (e.g. stack/backtrack limit).
4133              if ( null === $css_test_string ) {
4134                  continue;
4135              }
4136  
4137              /*
4138               * Disallow CSS containing \ ( & } = or comments, except for within url(), var(), calc(), etc.
4139               * which were removed from the test string above.
4140               */
4141              $allow_css = 0 === preg_match( '%[\\\(&=}]|/\*%', $css_test_string );
4142  
4143              /**
4144               * Filters the check for unsafe CSS in `safecss_filter_attr`.
4145               *
4146               * Enables developers to determine whether a section of CSS should be allowed or discarded.
4147               * By default, the value will be false if the part contains \ ( & } = or comments.
4148               * Return true to allow the CSS part to be included in the output.
4149               *
4150               * @since 5.5.0
4151               *
4152               * @param bool   $allow_css       Whether the CSS in the test string is considered safe.
4153               * @param string $css_test_string The CSS string to test.
4154               */
4155              $allow_css = apply_filters( 'safecss_filter_attr_allow_css', $allow_css, $css_test_string );
4156  
4157              // Only add the CSS part if it passes the regex check.
4158              if ( $allow_css ) {
4159                  if ( '' !== $css ) {
4160                      $css .= ';';
4161                  }
4162  
4163                  $css .= $css_item;
4164              }
4165          }
4166      }
4167  
4168      return $css;
4169  }
4170  
4171  /**
4172   * Helper function to add global attributes to a tag in the allowed HTML list.
4173   *
4174   * @since 3.5.0
4175   * @since 5.0.0 Added support for `data-*` wildcard attributes.
4176   * @since 6.0.0 Added `dir`, `lang`, and `xml:lang` to global attributes.
4177   * @since 6.3.0 Added `aria-controls`, `aria-current`, and `aria-expanded` attributes.
4178   * @since 6.4.0 Added `aria-live` and `hidden` attributes.
4179   * @since 7.1.0 Added `tabindex` attribute.
4180   *
4181   * @access private
4182   * @ignore
4183   *
4184   * @param array $value An array of attributes.
4185   * @return array The array of attributes with global attributes added.
4186   */
4187  function _wp_add_global_attributes( $value ) {
4188      $global_attributes = array(
4189          'aria-controls'    => true,
4190          'aria-current'     => true,
4191          'aria-describedby' => true,
4192          'aria-details'     => true,
4193          'aria-expanded'    => true,
4194          'aria-hidden'      => true,
4195          'aria-label'       => true,
4196          'aria-labelledby'  => true,
4197          'aria-live'        => true,
4198          'class'            => true,
4199          'data-*'           => true,
4200          'dir'              => true,
4201          'hidden'           => true,
4202          'id'               => true,
4203          'lang'             => true,
4204          'style'            => true,
4205          'tabindex'         => true,
4206          'title'            => true,
4207          'role'             => true,
4208          'xml:lang'         => true,
4209      );
4210  
4211      if ( true === $value ) {
4212          $value = array();
4213      }
4214  
4215      if ( is_array( $value ) ) {
4216          return array_merge( $value, $global_attributes );
4217      }
4218  
4219      return $value;
4220  }
4221  
4222  /**
4223   * Helper function to check if this is a safe PDF URL.
4224   *
4225   * @since 5.9.0
4226   * @access private
4227   * @ignore
4228   *
4229   * @param string $url The URL to check.
4230   * @return bool True if the URL is safe, false otherwise.
4231   */
4232  function _wp_kses_allow_pdf_objects( $url ) {
4233      // We're not interested in URLs that contain query strings or fragments.
4234      if ( str_contains( $url, '?' ) || str_contains( $url, '#' ) ) {
4235          return false;
4236      }
4237  
4238      // If it doesn't have a PDF extension, it's not safe.
4239      if ( ! str_ends_with( $url, '.pdf' ) ) {
4240          return false;
4241      }
4242  
4243      // If the URL host matches the current site's media URL, it's safe.
4244      $upload_info = wp_upload_dir( null, false );
4245      $parsed_url  = wp_parse_url( $upload_info['url'] );
4246      $upload_host = $parsed_url['host'] ?? '';
4247      $upload_port = isset( $parsed_url['port'] ) ? ':' . $parsed_url['port'] : '';
4248  
4249      if ( str_starts_with( $url, "http://$upload_host$upload_port/" )
4250          || str_starts_with( $url, "https://$upload_host$upload_port/" )
4251      ) {
4252          return true;
4253      }
4254  
4255      return false;
4256  }
4257  
4258  /**
4259   * Determines whether KSES restricts the value of any attribute of an element.
4260   *
4261   * An attribute rule other than `required` and `valueless` restricts the value itself,
4262   * for example `values` or `value_callback`.
4263   *
4264   * @since 7.1.4
4265   * @access private
4266   * @ignore
4267   *
4268   * @param array|bool $allowed_attributes The attributes KSES allows on the element.
4269   * @return bool Whether KSES restricts the value of any attribute of the element.
4270   */
4271  function _wp_kses_restricts_attribute_values( $allowed_attributes ): bool {
4272      if ( ! is_array( $allowed_attributes ) ) {
4273          return false;
4274      }
4275  
4276      foreach ( $allowed_attributes as $attribute_rules ) {
4277          if ( ! is_array( $attribute_rules ) ) {
4278              continue;
4279          }
4280          foreach ( array_keys( $attribute_rules ) as $rule ) {
4281              if ( ! in_array( strtolower( (string) $rule ), array( 'required', 'valueless' ), true ) ) {
4282                  return true;
4283              }
4284          }
4285      }
4286  
4287      return false;
4288  }
4289  
4290  /**
4291   * Determines whether a directive binds an attribute whose value KSES restricts.
4292   *
4293   * This only applies to elements for which KSES restricts the value of at least one
4294   * attribute. On those elements, a `data-wp-bind` directive is reported unless it binds
4295   * an attribute which KSES allows there with any value. KSES calls this to keep such a
4296   * directive out of content saved by an author who cannot post unfiltered HTML.
4297   *
4298   * @since 7.1.4
4299   * @access private
4300   * @ignore
4301   *
4302   * @param string     $directive_name     The directive attribute name, in lowercase.
4303   * @param array|bool $allowed_attributes The attributes KSES allows on the element.
4304   * @return bool Whether the directive binds an attribute whose value KSES restricts.
4305   */
4306  function _wp_kses_directive_binds_value_restricted_attribute( string $directive_name, $allowed_attributes ): bool {
4307      if ( ! str_starts_with( $directive_name, 'data-wp-bind--' ) ) {
4308          return false;
4309      }
4310  
4311      if ( ! _wp_kses_restricts_attribute_values( $allowed_attributes ) ) {
4312          return false;
4313      }
4314  
4315      /*
4316       * Both the whole remainder of the name and the part before a unique ID are checked, so
4317       * that the result does not depend on how the unique ID is parsed. A binding with a unique
4318       * ID is not applied at all, so removing one changes nothing.
4319       */
4320      $bound_attribute  = substr( $directive_name, strlen( 'data-wp-bind--' ) );
4321      $bound_attributes = array( $bound_attribute );
4322      $unique_id_index  = strpos( $bound_attribute, '---' );
4323      if ( false !== $unique_id_index ) {
4324          $bound_attributes[] = substr( $bound_attribute, 0, $unique_id_index );
4325      }
4326  
4327      foreach ( $bound_attributes as $attribute_name ) {
4328          if ( isset( $allowed_attributes[ $attribute_name ] ) ) {
4329              $rules = $allowed_attributes[ $attribute_name ];
4330          } elseif ( 1 === preg_match( '/^data-[a-z0-9_-]+$/', $attribute_name ) && isset( $allowed_attributes['data-*'] ) ) {
4331              $rules = $allowed_attributes['data-*'];
4332          } else {
4333              return true;
4334          }
4335  
4336          if ( ! $rules || is_array( $rules ) ) {
4337              return true;
4338          }
4339      }
4340  
4341      return false;
4342  }


Generated : Wed Nov 4 08:20:38 2026 Cross-referenced by PHPXref