[ 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                                  $raw_value = $this->get_attribute( $name );
1863                                  $value     = is_string( $raw_value ) ? $raw_value : '';
1864  
1865                                  // Process the style attribute through CSS sanitization.
1866                                  if ( 'style' === $name ) {
1867                                      if ( ! is_string( $raw_value ) ) {
1868                                          continue;
1869                                      }
1870  
1871                                      $value = safecss_filter_attr( $value );
1872                                      if ( '' === trim( $value ) ) {
1873                                          continue;
1874                                      }
1875                                  }
1876  
1877                                  $is_url_ish = in_array( strtolower( $name ), $this->uri_attributes, true );
1878                                  if ( $is_url_ish ) {
1879                                      $value = wp_kses_bad_protocol( $value, $this->allowed_protocols );
1880                                  }
1881  
1882                                  /*
1883                                   * Process the remaining attributes according to their policies.
1884                                   *
1885                                   * Non-array values for the attribute specification are assumed
1886                                   * to be `true`, thus permitting the attribute.
1887                                   */
1888                                  if ( is_array( $spec ) ) {
1889                                      foreach ( $spec as $property => $constraint ) {
1890                                          $vless = true === $raw_value ? 'y' : 'n';
1891  
1892                                          if ( ! wp_kses_check_attr_val( $value, $vless, $property, $constraint ) ) {
1893                                              continue 2;
1894                                          }
1895                                      }
1896                                  }
1897  
1898                                  $did_set = ( true === $raw_value && '' === $value )
1899                                      ? $tag_maker->set_attribute( $name, true )
1900                                      : $tag_maker->set_attribute( $name, $value );
1901  
1902                                  if ( $did_set ) {
1903                                      unset( $required_attributes[ $name ] );
1904                                  }
1905                              }
1906                          }
1907  
1908                          $needs_special_newline = 'html' === $namespace && ( 'PRE' === $token_name || 'LISTING' === $token_name );
1909  
1910                          if ( ! empty( $required_attributes ) ) {
1911                              if ( ! $expects_closer ) {
1912                                  break;
1913                              }
1914  
1915                              /*
1916                               * Since this processor cannot track nesting of HTML elements
1917                               * generally, leave opening tags when required attributes are
1918                               * missing, but strip them of their attributes.
1919                               */
1920                              $output .= "<{$tag_name}>";
1921                              if ( $needs_special_newline ) {
1922                                  $special_newline_at = strlen( $output );
1923                              }
1924                              break;
1925                          }
1926  
1927                          if ( $is_special_atomic_element ) {
1928                              if ( 'TITLE' === $token_name || 'TEXTAREA' === $token_name ) {
1929                                  /*
1930                                   * RCDATA nodes can be safely escaped, but this must be
1931                                   * done after enqueuing the update to avoid double-escaping.
1932                                   */
1933                                  $tag_maker->set_modifiable_text( $text );
1934                                  $tag_maker->lexical_updates['modifiable text']->text = strtr(
1935                                      $tag_maker->lexical_updates['modifiable text']->text,
1936                                      array(
1937                                          "\x00" => "\u{FFFD}",
1938                                          "\r"   => '&#xD;',
1939                                      )
1940                                  );
1941                              } elseif ( ! self::contains_a_block_delimiter( $text ) ) {
1942                                  // Other nodes not containing a block delimiter are safe.
1943                                  $tag_maker->set_modifiable_text( $text );
1944                              } else {
1945                                  /*
1946                                   * But RAWTEXT and SCRIPT cannot be generally escaped, so reject
1947                                   * updates which would include something that could be misparsed
1948                                   * as a block comment delimiter.
1949                                   */
1950                                  $tag_maker->set_modifiable_text( '' );
1951                              }
1952                          }
1953  
1954                          $output .= $tag_maker->get_updated_html();
1955                          if ( $needs_special_newline ) {
1956                              $special_newline_at = strlen( $output );
1957                          }
1958  
1959                          break;
1960                  }
1961  
1962                  // Re-enter the HTML namespace.
1963                  if ( 'html' !== $namespace ) {
1964                      if ( $has_self_closing_flag ) {
1965                          array_pop( $this->foreign_content_stack );
1966  
1967                          if ( 'math' === $namespace && 'ANNOTATION-XML' === $token_name ) {
1968                              --$this->math_annotation_xml_depth;
1969                          }
1970                      }
1971  
1972                      if ( empty( $this->foreign_content_stack ) ) {
1973                          $is_in_mathml_text_integration_point = false;
1974                          $is_in_svg_html_integration_point    = false;
1975                          $this->change_parsing_namespace( 'html' );
1976                          $foreign_content_starts_at = PHP_INT_MAX;
1977                          $foreign_closed_blocks     = array();
1978                      } elseif ( '#tag' === $token_type && ! $has_self_closing_flag ) {
1979                          switch ( $token_name ) {
1980                              case 'MI':
1981                              case 'MN':
1982                              case 'MO':
1983                              case 'MS':
1984                              case 'MTEXT':
1985                                  $is_in_mathml_text_integration_point = ! $is_closer && 'math' === $namespace;
1986                                  break;
1987  
1988                              case 'DESC':
1989                              case 'FOREIGNOBJECT':
1990                              case 'TITLE':
1991                                  $is_in_svg_html_integration_point = ! $is_closer && 'svg' === $namespace;
1992                                  break;
1993                          }
1994                      }
1995                  }
1996  
1997                  if ( 'TEMPLATE' === $token_name && $template_depth > 0 && $is_closer && 'html' === $namespace ) {
1998                      --$template_depth;
1999                  }
2000              }
2001  
2002              /*
2003               * While there might have been an incomplete token in the output stream,
2004               * there is no need to render it to the output. They would disappear on
2005               * their own in a browser if they ended the document, but here they do
2006               * not end the document; instead, they are likely being inserted into an
2007               * existing document, where the incomplete token might mess with the rest
2008               * of the page’s HTML structure.
2009               */
2010  
2011              $sanitized = substr( $output, 0, $foreign_content_starts_at );
2012  
2013              // Templates should be contained within blocks, so close them first.
2014              if ( $template_depth > 0 && isset( $this->allowed_html['template'] ) ) {
2015                  $sanitized .= str_repeat( '</template>', $template_depth );
2016              }
2017  
2018              // Close any remaining-open blocks ensure isolation of block content.
2019              foreach ( $foreign_closed_blocks as $block_name ) {
2020                  $sanitized .= "<!-- /wp:{$block_name} -->";
2021              }
2022  
2023              for ( $i = count( $open_blocks ) - 1; $i >= 0; $i-- ) {
2024                  // Skip blocks that were opened when inside truncated foreign content.
2025                  if ( $open_blocks_at[ $i ] >= $foreign_content_starts_at ) {
2026                      continue;
2027                  }
2028  
2029                  $block_name = $open_blocks[ $i ];
2030                  $sanitized .= "<!-- /wp:{$block_name} -->";
2031              }
2032  
2033              return $sanitized;
2034          }
2035      };
2036  
2037      return $processor->sanitize();
2038  }
2039  
2040  /**
2041   * Filters one HTML attribute and ensures its value is allowed.
2042   *
2043   * This function can escape data in some situations where `wp_kses()` must strip the whole attribute.
2044   *
2045   * @since 4.2.3
2046   *
2047   * @param string $attr    The 'whole' attribute, including name and value.
2048   * @param string $element The HTML element name to which the attribute belongs.
2049   * @return string Filtered attribute.
2050   */
2051  function wp_kses_one_attr( $attr, $element ) {
2052      $uris              = wp_kses_uri_attributes();
2053      $allowed_html      = wp_kses_allowed_html( 'post' );
2054      $allowed_protocols = wp_allowed_protocols();
2055      $attr              = wp_kses_no_null( $attr, array( 'slash_zero' => 'keep' ) );
2056  
2057      // Preserve leading and trailing whitespace.
2058      $matches = array();
2059      preg_match( '/^\s*/', $attr, $matches );
2060      $lead = $matches[0];
2061      preg_match( '/\s*$/', $attr, $matches );
2062      $trail = $matches[0];
2063      if ( empty( $trail ) ) {
2064          $attr = substr( $attr, strlen( $lead ) );
2065      } else {
2066          $attr = substr( $attr, strlen( $lead ), -strlen( $trail ) );
2067      }
2068  
2069      // Parse attribute name and value from input.
2070      $split = preg_split( '/\s*=\s*/', $attr, 2 );
2071      $name  = $split[0];
2072      if ( count( $split ) === 2 ) {
2073          $value = $split[1];
2074  
2075          /*
2076           * Remove quotes surrounding $value.
2077           * Also guarantee correct quoting in $attr for this one attribute.
2078           */
2079          if ( '' === $value ) {
2080              $quote = '';
2081          } else {
2082              $quote = $value[0];
2083          }
2084          if ( '"' === $quote || "'" === $quote ) {
2085              if ( ! str_ends_with( $value, $quote ) ) {
2086                  return '';
2087              }
2088              $value = substr( $value, 1, -1 );
2089          } else {
2090              $quote = '"';
2091          }
2092  
2093          // Sanitize quotes, angle braces, and entities.
2094          $value = esc_attr( $value );
2095  
2096          // Sanitize URI values.
2097          if ( in_array( strtolower( $name ), $uris, true ) ) {
2098              $value = wp_kses_bad_protocol( $value, $allowed_protocols );
2099          }
2100  
2101          $attr  = "$name=$quote$value$quote";
2102          $vless = 'n';
2103      } else {
2104          $value = '';
2105          $vless = 'y';
2106      }
2107  
2108      // Sanitize attribute by name.
2109      wp_kses_attr_check( $name, $value, $attr, $vless, $element, $allowed_html );
2110  
2111      // Restore whitespace.
2112      return $lead . $attr . $trail;
2113  }
2114  
2115  /**
2116   * Returns an array of allowed HTML tags and attributes for a given context.
2117   *
2118   * @since 3.5.0
2119   * @since 5.0.1 `form` removed as allowable HTML tag.
2120   *
2121   * @global array $allowedposttags
2122   * @global array $allowedtags
2123   * @global array $allowedentitynames
2124   *
2125   * @param string|array $context The context for which to retrieve tags. Allowed values are 'post',
2126   *                              'strip', 'data', 'entities', or the name of a field filter such as
2127   *                              'pre_user_description', or an array of allowed HTML elements and attributes.
2128   * @return array Array of allowed HTML tags and their allowed attributes.
2129   */
2130  function wp_kses_allowed_html( $context = '' ) {
2131      global $allowedposttags, $allowedtags, $allowedentitynames;
2132  
2133      if ( is_array( $context ) ) {
2134          // When `$context` is an array it's actually an array of allowed HTML elements and attributes.
2135          $html    = $context;
2136          $context = 'explicit';
2137  
2138          /**
2139           * Filters the HTML tags that are allowed for a given context.
2140           *
2141           * HTML tags and attribute names are case-insensitive in HTML but must be
2142           * added to the KSES allow list in lowercase. An item added to the allow list
2143           * in upper or mixed case will not recognized as permitted by KSES.
2144           *
2145           * @since 3.5.0
2146           *
2147           * @param array[] $html    Allowed HTML tags.
2148           * @param string  $context Context name.
2149           */
2150          return apply_filters( 'wp_kses_allowed_html', $html, $context );
2151      }
2152  
2153      switch ( $context ) {
2154          case 'post':
2155              /** This filter is documented in wp-includes/kses.php */
2156              $tags = apply_filters( 'wp_kses_allowed_html', $allowedposttags, $context );
2157  
2158              // 5.0.1 removed the `<form>` tag, allow it if a filter is allowing it's sub-elements `<input>` or `<select>`.
2159              if ( ! CUSTOM_TAGS && ! isset( $tags['form'] ) && ( isset( $tags['input'] ) || isset( $tags['select'] ) ) ) {
2160                  $tags = $allowedposttags;
2161  
2162                  $tags['form'] = array(
2163                      'action'         => true,
2164                      'accept'         => true,
2165                      'accept-charset' => true,
2166                      'enctype'        => true,
2167                      'method'         => true,
2168                      'name'           => true,
2169                      'target'         => true,
2170                  );
2171  
2172                  /** This filter is documented in wp-includes/kses.php */
2173                  $tags = apply_filters( 'wp_kses_allowed_html', $tags, $context );
2174              }
2175  
2176              return $tags;
2177  
2178          case 'user_description':
2179          case 'pre_term_description':
2180          case 'pre_user_description':
2181              $tags                = $allowedtags;
2182              $tags['a']['rel']    = true;
2183              $tags['a']['target'] = true;
2184              /** This filter is documented in wp-includes/kses.php */
2185              return apply_filters( 'wp_kses_allowed_html', $tags, $context );
2186  
2187          case 'strip':
2188              /** This filter is documented in wp-includes/kses.php */
2189              return apply_filters( 'wp_kses_allowed_html', array(), $context );
2190  
2191          case 'entities':
2192              /** This filter is documented in wp-includes/kses.php */
2193              return apply_filters( 'wp_kses_allowed_html', $allowedentitynames, $context );
2194  
2195          case 'data':
2196          default:
2197              /** This filter is documented in wp-includes/kses.php */
2198              return apply_filters( 'wp_kses_allowed_html', $allowedtags, $context );
2199      }
2200  }
2201  
2202  /**
2203   * Allows the note mention chip markup in comment content.
2204   *
2205   * The notes `@` mention completer stores a mention as a chip carrying the
2206   * mentioned user's ID in a class token:
2207   * `<span class="wp-note-mention user-N">@Name</span>`. The default comment
2208   * allowlist does not allow `span` at all, so for users without
2209   * `unfiltered_html` the mention would be stripped on save.
2210   *
2211   * The allowance is deliberately narrow and always on: `span` is a
2212   * semantics-free element and _wp_kses_sanitize_note_mention_classes()
2213   * reduces its `class` to the two mention tokens right after kses runs, so
2214   * regular (including anonymous) commenters gain nothing beyond the inert
2215   * mention markup itself.
2216   *
2217   * @since 7.1.0
2218   * @access private
2219   *
2220   * @param array<string, array<string, bool>> $allowed The allowed tags structure for the context.
2221   * @param string                             $context The kses context.
2222   * @return array<string, array<string, bool>> Modified allowed tags structure.
2223   */
2224  function _wp_kses_allow_note_mention_span( $allowed, $context ): array {
2225      if ( ! is_array( $allowed ) ) {
2226          $allowed = array();
2227      }
2228      if ( 'pre_comment_content' !== $context ) {
2229          return $allowed;
2230      }
2231  
2232      if ( ! isset( $allowed['span'] ) || ! is_array( $allowed['span'] ) ) {
2233          $allowed['span'] = array();
2234      }
2235  
2236      $allowed['span']['class'] = true;
2237  
2238      return $allowed;
2239  }
2240  
2241  /**
2242   * Reduces `span` classes in comment content to the note mention tokens.
2243   *
2244   * _wp_kses_allow_note_mention_span() lets `class` through kses on `span` so
2245   * the mention chip survives, but `class` is an open-ended styling and
2246   * scripting hook, so this companion pass - running right after
2247   * `wp_filter_kses` at priority 10 - strips every class token except the two
2248   * the mention markup uses: `wp-note-mention` and `user-N`. `span` is the only
2249   * comment tag allowed to carry `class` at all, so walking `span` tags covers
2250   * the entire allowance.
2251   *
2252   * The pass only applies while the restrictive comment allowlist is active:
2253   * users with `unfiltered_html` are filtered through `wp_filter_post_kses`
2254   * (or not at all), where arbitrary classes are already permitted, and
2255   * narrowing their markup here would restrict what core allows them to post.
2256   *
2257   * @since 7.1.0
2258   * @access private
2259   *
2260   * @param string $content Slashed comment content, already filtered by kses.
2261   * @return string Slashed comment content with span classes reduced.
2262   */
2263  function _wp_kses_sanitize_note_mention_classes( $content ): string {
2264      if ( ! is_string( $content ) ) {
2265          $content = '';
2266      }
2267      if ( false === has_filter( 'pre_comment_content', 'wp_filter_kses' ) ) {
2268          return $content;
2269      }
2270  
2271      $processor = new WP_HTML_Tag_Processor( wp_unslash( $content ) );
2272  
2273      while ( $processor->next_tag( 'SPAN' ) ) {
2274          foreach ( $processor->class_list() as $token ) {
2275              if ( 'wp-note-mention' !== $token && ! preg_match( '/^user-[1-9][0-9]*$/', $token ) ) {
2276                  // Removing the last class also removes the attribute itself.
2277                  $processor->remove_class( $token );
2278              }
2279          }
2280      }
2281  
2282      return wp_slash( $processor->get_updated_html() );
2283  }
2284  
2285  /**
2286   * You add any KSES hooks here.
2287   *
2288   * There is currently only one KSES WordPress hook, {@see 'pre_kses'}, and it is called here.
2289   * All parameters are passed to the hooks and expected to receive a string.
2290   *
2291   * @since 1.0.0
2292   *
2293   * @param string         $content           Content to filter through KSES.
2294   * @param array[]|string $allowed_html      An array of allowed HTML elements and attributes,
2295   *                                          or a context name such as 'post'. See wp_kses_allowed_html()
2296   *                                          for the list of accepted context names.
2297   * @param string[]       $allowed_protocols Array of allowed URL protocols.
2298   * @return string Filtered content through {@see 'pre_kses'} hook.
2299   */
2300  function wp_kses_hook( $content, $allowed_html, $allowed_protocols ) {
2301      /**
2302       * Filters content to be run through KSES.
2303       *
2304       * @since 2.3.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       */
2312      return apply_filters( 'pre_kses', $content, $allowed_html, $allowed_protocols );
2313  }
2314  
2315  /**
2316   * Returns the version number of KSES.
2317   *
2318   * @since 1.0.0
2319   *
2320   * @return string KSES version number.
2321   */
2322  function wp_kses_version() {
2323      return '0.2.2';
2324  }
2325  
2326  /**
2327   * Searches for HTML tags, no matter how malformed.
2328   *
2329   * It also matches stray `>` characters.
2330   *
2331   * @since 1.0.0
2332   * @since 6.6.0 Recognize additional forms of invalid HTML which convert into comments.
2333   *
2334   * @global array[]|string $pass_allowed_html      An array of allowed HTML elements and attributes,
2335   *                                                or a context name such as 'post'.
2336   * @global string[]       $pass_allowed_protocols Array of allowed URL protocols.
2337   *
2338   * @param string         $content           Content to filter.
2339   * @param array[]|string $allowed_html      An array of allowed HTML elements and attributes,
2340   *                                          or a context name such as 'post'. See wp_kses_allowed_html()
2341   *                                          for the list of accepted context names.
2342   * @param string[]       $allowed_protocols Array of allowed URL protocols.
2343   * @return string Content with fixed HTML tags
2344   */
2345  function wp_kses_split( $content, $allowed_html, $allowed_protocols ) {
2346      global $pass_allowed_html, $pass_allowed_protocols;
2347  
2348      $pass_allowed_html      = $allowed_html;
2349      $pass_allowed_protocols = $allowed_protocols;
2350  
2351      $token_pattern = <<<REGEX
2352  ~
2353      (                      # Detect comments of various flavors before attempting to find tags.
2354          (<!--.*?(-->|$))   #  - Normative HTML comments.
2355          |
2356          </[^a-zA-Z][^>]*>  #  - Closing tags with invalid tag names.
2357          |
2358          <![^>]*>           #  - Invalid markup declaration nodes. Not all invalid nodes
2359                             #    are matched so as to avoid breaking legacy behaviors.
2360      )
2361      |
2362      (<[^>]*(>|$)|>)        # Tag-like spans of text.
2363  ~x
2364  REGEX;
2365      return preg_replace_callback( $token_pattern, '_wp_kses_split_callback', $content );
2366  }
2367  
2368  /**
2369   * Returns an array of HTML attribute names whose value contains a URL.
2370   *
2371   * This function returns a list of all HTML attributes that must contain
2372   * a URL according to the HTML specification.
2373   *
2374   * This list includes URI attributes both allowed and disallowed by KSES.
2375   *
2376   * @link https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes
2377   *
2378   * @since 5.0.1
2379   *
2380   * @return string[] HTML attribute names whose value contains a URL.
2381   */
2382  function wp_kses_uri_attributes() {
2383      $uri_attributes = array(
2384          'action',
2385          'archive',
2386          'background',
2387          'cite',
2388          'classid',
2389          'codebase',
2390          'data',
2391          'formaction',
2392          'href',
2393          'icon',
2394          'longdesc',
2395          'manifest',
2396          'poster',
2397          'profile',
2398          'src',
2399          'usemap',
2400          'xmlns',
2401      );
2402  
2403      /**
2404       * Filters the list of attributes that are required to contain a URL.
2405       *
2406       * Use this filter to add any `data-` attributes that are required to be
2407       * validated as a URL.
2408       *
2409       * @since 5.0.1
2410       *
2411       * @param string[] $uri_attributes HTML attribute names whose value contains a URL.
2412       */
2413      $uri_attributes = apply_filters( 'wp_kses_uri_attributes', $uri_attributes );
2414  
2415      return $uri_attributes;
2416  }
2417  
2418  /**
2419   * Callback for `wp_kses_split()`.
2420   *
2421   * @since 3.1.0
2422   * @access private
2423   * @ignore
2424   *
2425   * @global array[]|string $pass_allowed_html      An array of allowed HTML elements and attributes,
2426   *                                                or a context name such as 'post'.
2427   * @global string[]       $pass_allowed_protocols Array of allowed URL protocols.
2428   *
2429   * @param array $matches preg_replace regexp matches
2430   * @return string
2431   */
2432  function _wp_kses_split_callback( $matches ) {
2433      global $pass_allowed_html, $pass_allowed_protocols;
2434  
2435      return wp_kses_split2( $matches[0], $pass_allowed_html, $pass_allowed_protocols );
2436  }
2437  
2438  /**
2439   * Callback for `wp_kses_split()` for fixing malformed HTML tags.
2440   *
2441   * This function does a lot of work. It rejects some very malformed things like
2442   * `<:::>`. It returns an empty string, if the element isn't allowed (look ma, no
2443   * `strip_tags()`!). Otherwise it splits the tag into an element and an attribute
2444   * list.
2445   *
2446   * After the tag is split into an element and an attribute list, it is run
2447   * through another filter which will remove illegal attributes and once that is
2448   * completed, will be returned.
2449   *
2450   * @access private
2451   * @ignore
2452   * @since 1.0.0
2453   * @since 6.6.0 Recognize additional forms of invalid HTML which convert into comments.
2454   *
2455   * @param string         $content           Content to filter.
2456   * @param array[]|string $allowed_html      An array of allowed HTML elements and attributes,
2457   *                                          or a context name such as 'post'. See wp_kses_allowed_html()
2458   *                                          for the list of accepted context names.
2459   * @param string[]       $allowed_protocols Array of allowed URL protocols.
2460   * @return string Fixed HTML element
2461   */
2462  function wp_kses_split2( $content, $allowed_html, $allowed_protocols ) {
2463      $content = wp_kses_stripslashes( $content );
2464  
2465      /*
2466       * The regex pattern used to split HTML into chunks attempts
2467       * to split on HTML token boundaries. This function should
2468       * thus receive chunks that _either_ start with meaningful
2469       * syntax tokens, like a tag `<div>` or a comment `<!-- ... -->`.
2470       *
2471       * If the first character of the `$content` chunk _isn't_ one
2472       * of these syntax elements, which always starts with `<`, then
2473       * the match had to be for the final alternation of `>`. In such
2474       * case, it's probably standing on its own and could be encoded
2475       * with a character reference to remove ambiguity.
2476       *
2477       * In other words, if this chunk isn't from a match of a syntax
2478       * token, it's just a plaintext greater-than (`>`) sign.
2479       */
2480      if ( ! str_starts_with( $content, '<' ) ) {
2481          return '&gt;';
2482      }
2483  
2484      /*
2485       * When certain invalid syntax constructs appear, the HTML parser
2486       * shifts into what's called the "bogus comment state." This is a
2487       * plaintext state that consumes everything until the nearest `>`
2488       * and then transforms the entire span into an HTML comment.
2489       *
2490       * Preserve these comments and do not treat them like tags.
2491       *
2492       * @link https://html.spec.whatwg.org/#bogus-comment-state
2493       */
2494      if ( 1 === preg_match( '~^(?:</[^a-zA-Z][^>]*>|<![a-z][^>]*>)$~', $content ) ) {
2495          /**
2496           * Since the pattern matches `</…>` and also `<!…>`, this will
2497           * preserve the type of the cleaned-up token in the output.
2498           */
2499          $opener  = $content[1];
2500          $content = substr( $content, 2, -1 );
2501  
2502          do {
2503              $prev    = $content;
2504              $content = wp_kses( $content, $allowed_html, $allowed_protocols );
2505          } while ( $prev !== $content );
2506  
2507          // Recombine the modified inner content with the original token structure.
2508          return "<{$opener}{$content}>";
2509      }
2510  
2511      /*
2512       * Normative HTML comments should be handled separately as their
2513       * parsing rules differ from those for tags and text nodes.
2514       */
2515      if ( str_starts_with( $content, '<!--' ) ) {
2516          $content = str_replace( array( '<!--', '-->' ), '', $content );
2517  
2518          while ( ( $newstring = wp_kses( $content, $allowed_html, $allowed_protocols ) ) !== $content ) {
2519              $content = $newstring;
2520          }
2521  
2522          if ( '' === $content ) {
2523              return '';
2524          }
2525  
2526          // Prevent multiple dashes in comments.
2527          $content = preg_replace( '/--+/', '-', $content );
2528          // Prevent three dashes closing a comment.
2529          $content = preg_replace( '/-$/', '', $content );
2530  
2531          return "<!--{$content}-->";
2532      }
2533  
2534      // It's seriously malformed.
2535      if ( ! preg_match( '%^<\s*(/\s*)?([a-zA-Z0-9-]+)([^>]*)>?$%', $content, $matches ) ) {
2536          return '';
2537      }
2538  
2539      $slash    = trim( $matches[1] );
2540      $elem     = $matches[2];
2541      $attrlist = $matches[3];
2542  
2543      if ( ! is_array( $allowed_html ) ) {
2544          $allowed_html = wp_kses_allowed_html( $allowed_html );
2545      }
2546  
2547      // They are using a not allowed HTML element.
2548      if ( ! isset( $allowed_html[ strtolower( $elem ) ] ) ) {
2549          return '';
2550      }
2551  
2552      // No attributes are allowed for closing elements.
2553      if ( '' !== $slash ) {
2554          return "</$elem>";
2555      }
2556  
2557      return wp_kses_attr( $elem, $attrlist, $allowed_html, $allowed_protocols );
2558  }
2559  
2560  /**
2561   * Removes all attributes, if none are allowed for this element.
2562   *
2563   * If some are allowed it calls `wp_kses_hair()` to split them further, and then
2564   * it builds up new HTML code from the data that `wp_kses_hair()` returns. It also
2565   * removes `<` and `>` characters, if there are any left. One more thing it does
2566   * is to check if the tag has a closing XHTML slash, and if it does, it puts one
2567   * in the returned code as well.
2568   *
2569   * An array of allowed values can be defined for attributes. If the attribute value
2570   * doesn't fall into the list, the attribute will be removed from the tag.
2571   *
2572   * Attributes can be marked as required. If a required attribute is not present,
2573   * KSES will remove all attributes from the tag. As KSES doesn't match opening and
2574   * closing tags, it's not possible to safely remove the tag itself, the safest
2575   * fallback is to strip all attributes from the tag, instead.
2576   *
2577   * @since 1.0.0
2578   * @since 5.9.0 Added support for an array of allowed values for attributes.
2579   *              Added support for required attributes.
2580   *
2581   * @param string         $element           HTML element/tag.
2582   * @param string         $attr              HTML attributes from HTML element to closing HTML element tag.
2583   * @param array[]|string $allowed_html      An array of allowed HTML elements and attributes,
2584   *                                          or a context name such as 'post'. See wp_kses_allowed_html()
2585   *                                          for the list of accepted context names.
2586   * @param string[]       $allowed_protocols Array of allowed URL protocols.
2587   * @return string Sanitized HTML element.
2588   */
2589  function wp_kses_attr( $element, $attr, $allowed_html, $allowed_protocols ) {
2590      if ( ! is_array( $allowed_html ) ) {
2591          $allowed_html = wp_kses_allowed_html( $allowed_html );
2592      }
2593  
2594      // Is there a closing XHTML slash at the end of the attributes?
2595      $xhtml_slash = '';
2596      if ( preg_match( '%\s*/\s*$%', $attr ) ) {
2597          $xhtml_slash = ' /';
2598      }
2599  
2600      // Are any attributes allowed at all for this element?
2601      $element_low = strtolower( $element );
2602      if ( empty( $allowed_html[ $element_low ] ) || true === $allowed_html[ $element_low ] ) {
2603          return "<$element$xhtml_slash>";
2604      }
2605  
2606      // Split it.
2607      $attrarr = wp_kses_hair( $attr, $allowed_protocols );
2608  
2609      // Check if there are attributes that are required.
2610      $required_attrs = array_filter(
2611          $allowed_html[ $element_low ],
2612          static function ( $required_attr_limits ) {
2613              return isset( $required_attr_limits['required'] ) && true === $required_attr_limits['required'];
2614          }
2615      );
2616  
2617      /*
2618       * If a required attribute check fails, we can return nothing for a self-closing tag,
2619       * but for a non-self-closing tag the best option is to return the element with attributes,
2620       * as KSES doesn't handle matching the relevant closing tag.
2621       */
2622      $stripped_tag = '';
2623      if ( empty( $xhtml_slash ) ) {
2624          $stripped_tag = "<$element>";
2625      }
2626  
2627      // Go through $attrarr, and save the allowed attributes for this element in $attr2.
2628      $attr2 = '';
2629      foreach ( $attrarr as $arreach ) {
2630          // Check if this attribute is required.
2631          $required = isset( $required_attrs[ strtolower( $arreach['name'] ) ] );
2632  
2633          if ( wp_kses_attr_check( $arreach['name'], $arreach['value'], $arreach['whole'], $arreach['vless'], $element, $allowed_html ) ) {
2634              $attr2 .= ' ' . $arreach['whole'];
2635  
2636              // If this was a required attribute, we can mark it as found.
2637              if ( $required ) {
2638                  unset( $required_attrs[ strtolower( $arreach['name'] ) ] );
2639              }
2640          } elseif ( $required ) {
2641              // This attribute was required, but didn't pass the check. The entire tag is not allowed.
2642              return $stripped_tag;
2643          }
2644      }
2645  
2646      // If some required attributes weren't set, the entire tag is not allowed.
2647      if ( ! empty( $required_attrs ) ) {
2648          return $stripped_tag;
2649      }
2650  
2651      // Remove any "<" or ">" characters.
2652      $attr2 = preg_replace( '/[<>]/', '', $attr2 );
2653  
2654      return "<$element$attr2$xhtml_slash>";
2655  }
2656  
2657  /**
2658   * Determines whether an attribute is allowed.
2659   *
2660   * @since 4.2.3
2661   * @since 5.0.0 Added support for `data-*` wildcard attributes.
2662   *
2663   * @param string $name         The attribute name. Passed by reference. Returns empty string when not allowed.
2664   * @param string $value        The attribute value. Passed by reference. Returns a filtered value.
2665   * @param string $whole        The `name=value` input. Passed by reference. Returns filtered input.
2666   * @param string $vless        Whether the attribute is valueless. Use 'y' or 'n'.
2667   * @param string $element      The name of the element to which this attribute belongs.
2668   * @param array  $allowed_html The full list of allowed elements and attributes.
2669   * @return bool Whether or not the attribute is allowed.
2670   */
2671  function wp_kses_attr_check( &$name, &$value, &$whole, $vless, $element, $allowed_html ) {
2672      $name_low    = strtolower( $name );
2673      $element_low = strtolower( $element );
2674  
2675      if ( ! isset( $allowed_html[ $element_low ] ) ) {
2676          $name  = '';
2677          $value = '';
2678          $whole = '';
2679          return false;
2680      }
2681  
2682      $allowed_attr = $allowed_html[ $element_low ];
2683  
2684      if ( ! isset( $allowed_attr[ $name_low ] ) || '' === $allowed_attr[ $name_low ] ) {
2685          /*
2686           * Allow `data-*` attributes.
2687           *
2688           * When specifying `$allowed_html`, the attribute name should be set as
2689           * `data-*` (not to be mixed with the HTML 4.0 `data` attribute, see
2690           * https://www.w3.org/TR/html40/struct/objects.html#adef-data).
2691           *
2692           * Note: the attribute name should only contain `A-Za-z0-9_-` chars.
2693           */
2694          if ( str_starts_with( $name_low, 'data-' ) && ! empty( $allowed_attr['data-*'] )
2695              && preg_match( '/^data-[a-z0-9_-]+$/', $name_low, $match )
2696          ) {
2697              /*
2698               * Add the whole attribute name to the allowed attributes and set any restrictions
2699               * for the `data-*` attribute values for the current element.
2700               */
2701              $allowed_attr[ $match[0] ] = $allowed_attr['data-*'];
2702          } else {
2703              $name  = '';
2704              $value = '';
2705              $whole = '';
2706              return false;
2707          }
2708      }
2709  
2710      if ( 'style' === $name_low ) {
2711          $decoded_value = WP_HTML_Decoder::decode_attribute( $value );
2712          $new_value     = safecss_filter_attr( $decoded_value );
2713  
2714          if ( empty( $new_value ) ) {
2715              $name  = '';
2716              $value = '';
2717              $whole = '';
2718              return false;
2719          }
2720  
2721          if ( $new_value !== $decoded_value ) {
2722              $encoded_value = esc_attr( $new_value );
2723              $whole         = str_replace( $value, $encoded_value, $whole );
2724              $value         = $encoded_value;
2725          }
2726      }
2727  
2728      if ( is_array( $allowed_attr[ $name_low ] ) ) {
2729          // There are some checks.
2730          foreach ( $allowed_attr[ $name_low ] as $currkey => $currval ) {
2731              if ( ! wp_kses_check_attr_val( $value, $vless, $currkey, $currval ) ) {
2732                  $name  = '';
2733                  $value = '';
2734                  $whole = '';
2735                  return false;
2736              }
2737          }
2738      }
2739  
2740      return true;
2741  }
2742  
2743  /**
2744   * Given a string of HTML attributes and values, parse into a structured attribute list.
2745   *
2746   * This function performs a number of transformations while parsing attribute strings:
2747   *  - It normalizes attribute values and surrounds them with double quotes.
2748   *  - It normalizes HTML character references inside attribute values.
2749   *  - It removes “bad” URL protocols from attribute values.
2750   *
2751   * Otherwise this reads the attributes as if they were part of an HTML tag. It performs
2752   * these transformations to lower the risk of mis-parsing down the line and to perform
2753   * URL sanitization in line with the rest of the `kses` subsystem. Importantly, it does
2754   * not decode the attribute values, meaning that special HTML syntax characters will
2755   * be left with character references in the `value` property.
2756   *
2757   * Example:
2758   *
2759   *     $attrs = wp_kses_hair( 'class="is-wide" inert data-lazy=\'&lt;img&#00062\' =/🐮=/' );
2760   *     $attrs === array(
2761   *         'class'     => array( 'name' => 'class', 'value' => 'is-wide', 'whole' => 'class="is-wide"', 'vless' => 'n' ),
2762   *         'inert'     => array( 'name' => 'inert', 'value' => '', 'whole' => 'inert', 'vless' => 'y' ),
2763   *         'data-lazy' => array( 'name' => 'data-lazy', 'value' => '&lt;img&gt;', 'whole' => 'data-lazy="&lt;img&gt;"', 'vless' => 'n' ),
2764   *         '='         => array( 'name' => '=', 'value' => '', 'whole' => '=', 'vless' => 'y' ),
2765   *         '🐮'        => array( 'name' => '🐮', 'value' => '/', 'whole' => '🐮="/"', 'vless' => 'n' ),
2766   *     );
2767   *
2768   * @since 1.0.0
2769   * @since 7.0.0 Reliably parses HTML via the HTML API.
2770   *
2771   * @param string   $attr              Attribute list from HTML element to closing HTML element tag.
2772   * @param string[] $allowed_protocols Array of allowed URL protocols.
2773   * @return array<string, array{name: string, value: string, whole: string, vless: 'y'|'n'}> Array of attribute information after parsing.
2774   */
2775  function wp_kses_hair( $attr, $allowed_protocols ) {
2776      $attributes = array();
2777      $uris       = wp_kses_uri_attributes();
2778  
2779      $processor = new WP_HTML_Tag_Processor( "<wp {$attr}>" );
2780      $processor->next_token();
2781  
2782      $attribute_names = $processor->get_attribute_names_with_prefix( '' );
2783      if ( null === $attribute_names || 0 === count( $attribute_names ) ) {
2784          return $attributes;
2785      }
2786  
2787      $syntax_characters = array(
2788          '&' => '&amp;',
2789          '<' => '&lt;',
2790          '>' => '&gt;',
2791          "'" => '&apos;',
2792          '"' => '&quot;',
2793      );
2794  
2795      foreach ( $attribute_names as $name ) {
2796          $value   = $processor->get_attribute( $name );
2797          $is_bool = true === $value;
2798          if ( is_string( $value ) && in_array( $name, $uris, true ) ) {
2799              $value = wp_kses_bad_protocol( $value, $allowed_protocols );
2800          }
2801  
2802          // Reconstruct and normalize the attribute value.
2803          $recoded = $is_bool ? '' : strtr( $value, $syntax_characters );
2804          $whole   = $is_bool ? $name : "{$name}=\"{$recoded}\"";
2805  
2806          $attributes[ $name ] = array(
2807              'name'  => $name,
2808              'value' => $recoded,
2809              'whole' => $whole,
2810              'vless' => $is_bool ? 'y' : 'n',
2811          );
2812      }
2813  
2814      return $attributes;
2815  }
2816  
2817  /**
2818   * Finds all attributes of an HTML element.
2819   *
2820   * Does not modify input.  May return "evil" output.
2821   *
2822   * Based on `wp_kses_split2()` and `wp_kses_attr()`.
2823   *
2824   * @since 4.2.3
2825   *
2826   * @param string $element HTML element.
2827   * @return array|false List of attributes found in the element. Returns false on failure.
2828   */
2829  function wp_kses_attr_parse( $element ) {
2830      $valid = preg_match( '%^(<\s*)(/\s*)?([a-zA-Z0-9]+\s*)([^>]*)(>?)$%', $element, $matches );
2831      if ( 1 !== $valid ) {
2832          return false;
2833      }
2834  
2835      $begin  = $matches[1];
2836      $slash  = $matches[2];
2837      $elname = $matches[3];
2838      $attr   = $matches[4];
2839      $end    = $matches[5];
2840  
2841      if ( '' !== $slash ) {
2842          // Closing elements do not get parsed.
2843          return false;
2844      }
2845  
2846      // Is there a closing XHTML slash at the end of the attributes?
2847      if ( 1 === preg_match( '%\s*/\s*$%', $attr, $matches ) ) {
2848          $xhtml_slash = $matches[0];
2849          $attr        = substr( $attr, 0, -strlen( $xhtml_slash ) );
2850      } else {
2851          $xhtml_slash = '';
2852      }
2853  
2854      // Split it.
2855      $attrarr = wp_kses_hair_parse( $attr );
2856      if ( false === $attrarr ) {
2857          return false;
2858      }
2859  
2860      // Make sure all input is returned by adding front and back matter.
2861      array_unshift( $attrarr, $begin . $slash . $elname );
2862      array_push( $attrarr, $xhtml_slash . $end );
2863  
2864      return $attrarr;
2865  }
2866  
2867  /**
2868   * Builds an attribute list from string containing attributes.
2869   *
2870   * Does not modify input.  May return "evil" output.
2871   * In case of unexpected input, returns false instead of stripping things.
2872   *
2873   * Based on `wp_kses_hair()` but does not return a multi-dimensional array.
2874   *
2875   * @since 4.2.3
2876   *
2877   * @param string $attr Attribute list from HTML element to closing HTML element tag.
2878   * @return array|false List of attributes found in $attr. Returns false on failure.
2879   */
2880  function wp_kses_hair_parse( $attr ) {
2881      if ( '' === $attr ) {
2882          return array();
2883      }
2884  
2885      $regex =
2886          '(?:
2887                  [_a-zA-Z][-_a-zA-Z0-9:.]* # Attribute name.
2888              |
2889                  \[\[?[^\[\]]+\]\]?        # Shortcode in the name position implies unfiltered_html.
2890          )
2891          (?:                               # Attribute value.
2892              \s*=\s*                       # All values begin with "=".
2893              (?:
2894                  "[^"]*"                   # Double-quoted.
2895              |
2896                  \'[^\']*\'                # Single-quoted.
2897              |
2898                  [^\s"\']+                 # Non-quoted.
2899                  (?:\s|$)                  # Must have a space.
2900              )
2901          |
2902              (?:\s|$)                      # If attribute has no value, space is required.
2903          )
2904          \s*                               # Trailing space is optional except as mentioned above.
2905          ';
2906  
2907      /*
2908       * Although it is possible to reduce this procedure to a single regexp,
2909       * we must run that regexp twice to get exactly the expected result.
2910       *
2911       * Note: do NOT remove the `x` modifiers as they are essential for the above regex!
2912       */
2913  
2914      $validation = "/^($regex)+$/x";
2915      $extraction = "/$regex/x";
2916  
2917      if ( 1 === preg_match( $validation, $attr ) ) {
2918          preg_match_all( $extraction, $attr, $attrarr );
2919          return $attrarr[0];
2920      } else {
2921          return false;
2922      }
2923  }
2924  
2925  /**
2926   * Performs different checks for attribute values.
2927   *
2928   * The currently implemented checks are "maxlen", "minlen", "maxval", "minval",
2929   * and "valueless".
2930   *
2931   * @since 1.0.0
2932   *
2933   * @param string $value      Attribute value.
2934   * @param string $vless      Whether the attribute is valueless. Use 'y' or 'n'.
2935   * @param string $checkname  What $checkvalue is checking for.
2936   * @param mixed  $checkvalue What constraint the value should pass.
2937   * @return bool Whether check passes.
2938   */
2939  function wp_kses_check_attr_val( $value, $vless, $checkname, $checkvalue ) {
2940      $ok = true;
2941  
2942      switch ( strtolower( $checkname ) ) {
2943          case 'maxlen':
2944              /*
2945               * The maxlen check makes sure that the attribute value has a length not
2946               * greater than the given value. This can be used to avoid Buffer Overflows
2947               * in WWW clients and various Internet servers.
2948               */
2949  
2950              if ( strlen( $value ) > $checkvalue ) {
2951                  $ok = false;
2952              }
2953              break;
2954  
2955          case 'minlen':
2956              /*
2957               * The minlen check makes sure that the attribute value has a length not
2958               * smaller than the given value.
2959               */
2960  
2961              if ( strlen( $value ) < $checkvalue ) {
2962                  $ok = false;
2963              }
2964              break;
2965  
2966          case 'maxval':
2967              /*
2968               * The maxval check does two things: it checks that the attribute value is
2969               * an integer from 0 and up, without an excessive amount of zeroes or
2970               * whitespace (to avoid Buffer Overflows). It also checks that the attribute
2971               * value is not greater than the given value.
2972               * This check can be used to avoid Denial of Service attacks.
2973               */
2974  
2975              if ( ! preg_match( '/^\s{0,6}[0-9]{1,6}\s{0,6}$/', $value ) ) {
2976                  $ok = false;
2977              }
2978              if ( $value > $checkvalue ) {
2979                  $ok = false;
2980              }
2981              break;
2982  
2983          case 'minval':
2984              /*
2985               * The minval check makes sure that the attribute value is a positive integer,
2986               * and that it is not smaller than the given value.
2987               */
2988  
2989              if ( ! preg_match( '/^\s{0,6}[0-9]{1,6}\s{0,6}$/', $value ) ) {
2990                  $ok = false;
2991              }
2992              if ( $value < $checkvalue ) {
2993                  $ok = false;
2994              }
2995              break;
2996  
2997          case 'valueless':
2998              /*
2999               * The valueless check makes sure if the attribute has a value
3000               * (like `<a href="blah">`) or not (`<option selected>`). If the given value
3001               * is a "y" or a "Y", the attribute must not have a value.
3002               * If the given value is an "n" or an "N", the attribute must have a value.
3003               */
3004  
3005              if ( strtolower( $checkvalue ) !== $vless ) {
3006                  $ok = false;
3007              }
3008              break;
3009  
3010          case 'values':
3011              /*
3012               * The values check is used when you want to make sure that the attribute
3013               * has one of the given values.
3014               */
3015  
3016              if ( ! in_array( strtolower( $value ), $checkvalue, true ) ) {
3017                  $ok = false;
3018              }
3019              break;
3020  
3021          case 'value_callback':
3022              /*
3023               * The value_callback check is used when you want to make sure that the attribute
3024               * value is accepted by the callback function.
3025               */
3026  
3027              if ( ! call_user_func( $checkvalue, $value ) ) {
3028                  $ok = false;
3029              }
3030              break;
3031      } // End switch.
3032  
3033      return $ok;
3034  }
3035  
3036  /**
3037   * Sanitizes a string and removed disallowed URL protocols.
3038   *
3039   * This function removes all non-allowed protocols from the beginning of the
3040   * string. It ignores whitespace and the case of the letters, and it does
3041   * understand HTML entities. It does its work recursively, so it won't be
3042   * fooled by a string like `javascript:javascript:alert(57)`.
3043   *
3044   * @since 1.0.0
3045   *
3046   * @param string   $content           Content to filter bad protocols from.
3047   * @param string[] $allowed_protocols Array of allowed URL protocols.
3048   * @return string Filtered content.
3049   */
3050  function wp_kses_bad_protocol( $content, $allowed_protocols ) {
3051      $content = wp_kses_no_null( $content );
3052  
3053      // Short-circuit if the string starts with `https://` or `http://`. Most common cases.
3054      if (
3055          ( str_starts_with( $content, 'https://' ) && in_array( 'https', $allowed_protocols, true ) ) ||
3056          ( str_starts_with( $content, 'http://' ) && in_array( 'http', $allowed_protocols, true ) )
3057      ) {
3058          return $content;
3059      }
3060  
3061      $iterations = 0;
3062  
3063      do {
3064          $original_content = $content;
3065          $content          = wp_kses_bad_protocol_once( $content, $allowed_protocols );
3066      } while ( $original_content !== $content && ++$iterations < 6 );
3067  
3068      if ( $original_content !== $content ) {
3069          return '';
3070      }
3071  
3072      return $content;
3073  }
3074  
3075  /**
3076   * Removes any invalid control characters in a text string.
3077   *
3078   * Also removes any instance of the `\0` string.
3079   *
3080   * @since 1.0.0
3081   *
3082   * @param string $content Content to filter null characters from.
3083   * @param array  $options Set 'slash_zero' => 'keep' when '\0' is allowed. Default is 'remove'.
3084   * @return string Filtered content.
3085   */
3086  function wp_kses_no_null( $content, $options = null ) {
3087      if ( ! isset( $options['slash_zero'] ) ) {
3088          $options = array( 'slash_zero' => 'remove' );
3089      }
3090  
3091      $content = preg_replace( '/[\x00-\x08\x0B\x0C\x0E-\x1F]/', '', $content );
3092      if ( 'remove' === $options['slash_zero'] ) {
3093          $content = preg_replace( '/\\\\+0+/', '', $content );
3094      }
3095  
3096      return $content;
3097  }
3098  
3099  /**
3100   * Strips slashes from in front of quotes.
3101   *
3102   * This function changes the character sequence `\"` to just `"`. It leaves all other
3103   * slashes alone. The quoting from `preg_replace(//e)` requires this.
3104   *
3105   * @since 1.0.0
3106   *
3107   * @param string $content String to strip slashes from.
3108   * @return string Fixed string with quoted slashes.
3109   */
3110  function wp_kses_stripslashes( $content ) {
3111      return preg_replace( '%\\\\"%', '"', $content );
3112  }
3113  
3114  /**
3115   * Converts the keys of an array to lowercase.
3116   *
3117   * @since 1.0.0
3118   *
3119   * @param array $inarray Unfiltered array.
3120   * @return array Fixed array with all lowercase keys.
3121   */
3122  function wp_kses_array_lc( $inarray ) {
3123      $outarray = array();
3124  
3125      foreach ( (array) $inarray as $inkey => $inval ) {
3126          $outkey              = strtolower( $inkey );
3127          $outarray[ $outkey ] = array();
3128  
3129          foreach ( (array) $inval as $inkey2 => $inval2 ) {
3130              $outkey2                         = strtolower( $inkey2 );
3131              $outarray[ $outkey ][ $outkey2 ] = $inval2;
3132          }
3133      }
3134  
3135      return $outarray;
3136  }
3137  
3138  /**
3139   * Sanitizes content from bad protocols and other characters.
3140   *
3141   * This function searches for URL protocols at the beginning of the string, while
3142   * handling whitespace and HTML entities.
3143   *
3144   * @since 1.0.0
3145   *
3146   * @param string   $content           Content to check for bad protocols.
3147   * @param string[] $allowed_protocols Array of allowed URL protocols.
3148   * @param int      $count             Depth of call recursion to this function.
3149   * @return string Sanitized content.
3150   */
3151  function wp_kses_bad_protocol_once( $content, $allowed_protocols, $count = 1 ) {
3152      $content  = preg_replace( '/(&#0*58(?![;0-9])|&#x0*3a(?![;a-f0-9]))/i', '$1;', $content );
3153      $content2 = preg_split( '/:|&#0*58;|&#x0*3a;|&colon;/i', $content, 2 );
3154  
3155      if ( isset( $content2[1] ) && ! preg_match( '%/\?%', $content2[0] ) ) {
3156          $content  = trim( $content2[1] );
3157          $protocol = wp_kses_bad_protocol_once2( $content2[0], $allowed_protocols );
3158          if ( 'feed:' === $protocol ) {
3159              if ( $count > 2 ) {
3160                  return '';
3161              }
3162              $content = wp_kses_bad_protocol_once( $content, $allowed_protocols, ++$count );
3163              if ( empty( $content ) ) {
3164                  return $content;
3165              }
3166          }
3167          $content = $protocol . $content;
3168      }
3169  
3170      return $content;
3171  }
3172  
3173  /**
3174   * Callback for `wp_kses_bad_protocol_once()` regular expression.
3175   *
3176   * This function processes URL protocols, checks to see if they're in the
3177   * list of allowed protocols or not, and returns different data depending
3178   * on the answer.
3179   *
3180   * @access private
3181   * @ignore
3182   * @since 1.0.0
3183   *
3184   * @param string   $scheme            URI scheme to check against the list of allowed protocols.
3185   * @param string[] $allowed_protocols Array of allowed URL protocols.
3186   * @return string Sanitized content.
3187   */
3188  function wp_kses_bad_protocol_once2( $scheme, $allowed_protocols ) {
3189      $scheme = wp_kses_decode_entities( $scheme );
3190      $scheme = preg_replace( '/\s/', '', $scheme );
3191      $scheme = wp_kses_no_null( $scheme );
3192      $scheme = strtolower( $scheme );
3193  
3194      $allowed = array_any( (array) $allowed_protocols, fn( $protocol ) => strtolower( $protocol ) === $scheme );
3195  
3196      if ( $allowed ) {
3197          return "$scheme:";
3198      } else {
3199          return '';
3200      }
3201  }
3202  
3203  /**
3204   * Converts and fixes HTML entities.
3205   *
3206   * This function normalizes HTML entities. It will convert `AT&T` to the correct
3207   * `AT&amp;T`, `&#00058;` to `&#058;`, `&#XYZZY;` to `&amp;#XYZZY;` and so on.
3208   *
3209   * When `$context` is set to 'xml', HTML entities are converted to their code points.  For
3210   * example, `AT&T&hellip;&#XYZZY;` is converted to `AT&amp;T…&amp;#XYZZY;`.
3211   *
3212   * @since 1.0.0
3213   * @since 5.5.0 Added `$context` parameter.
3214   *
3215   * @param string $content Content to normalize entities.
3216   * @param string $context Context for normalization. Can be either 'html' or 'xml'.
3217   *                        Default 'html'.
3218   * @return string Content with normalized entities.
3219   */
3220  function wp_kses_normalize_entities( $content, $context = 'html' ) {
3221      // Disarm all entities by converting & to &amp;
3222      $content = str_replace( '&', '&amp;', $content );
3223  
3224      /*
3225       * Decode any character references that are now double-encoded.
3226       *
3227       * It's important that the following normalizations happen in the correct order.
3228       *
3229       * At this point, all `&` have been transformed to `&amp;`. Double-encoded named character
3230       * references like `&amp;amp;` will be decoded back to their single-encoded form `&amp;`.
3231       *
3232       * First, numeric (decimal and hexadecimal) character references must be handled so that
3233       * `&amp;#09;` becomes `&#9;`. If the named character references were handled first, there
3234       * would be no way to know whether the double-encoded character reference had been produced
3235       * in this function or was the original input.
3236       *
3237       * Consider the two examples, first with named entity decoding followed by numeric
3238       * entity decoding. We'll use U+002E FULL STOP (.) in our example, this table follows the
3239       * string processing from left to right:
3240       *
3241       * | Input        | &-encoded        | Named ref double-decoded  | Numeric ref double-decoded |
3242       * | ------------ | ---------------- | ------------------------- | -------------------------- |
3243       * | `&#x2E;`     | `&amp;#x2E;`     | `&amp;#x2E;`              | `&#x2E;`                   |
3244       * | `&amp;#x2E;` | `&amp;amp;#x2E;` | `&amp;#x2E;`              | `&#x2E;`                   |
3245       *
3246       * Notice in the example above that different inputs result in the same result. The second case
3247       * was not normalized and produced HTML that is semantically different from the input.
3248       *
3249       * | Input        | &-encoded        |  Numeric ref double-decoded | Named ref double-decoded |
3250       * | ------------ | ---------------- | --------------------------- | ------------------------ |
3251       * | `&#x2E;`     | `&amp;#x2E;`     | `&#x2E;`                    | `&#x2E;`                 |
3252       * | `&amp;#x2E;` | `&amp;amp;#x2E;` | `&amp;amp;#x2E;`            | `&amp;#x2E;`             |
3253       *
3254       * Here, each input is normalized to an appropriate output.
3255       */
3256      $content = preg_replace_callback( '/&amp;#(0*[1-9][0-9]{0,6});/', 'wp_kses_normalize_entities2', $content );
3257      $content = preg_replace_callback( '/&amp;#[Xx](0*[1-9A-Fa-f][0-9A-Fa-f]{0,5});/', 'wp_kses_normalize_entities3', $content );
3258      if ( 'xml' === $context ) {
3259          $content = preg_replace_callback( '/&amp;([A-Za-z]{2,8}[0-9]{0,2});/', 'wp_kses_xml_named_entities', $content );
3260      } else {
3261          $content = preg_replace_callback( '/&amp;([A-Za-z]{2,8}[0-9]{0,2});/', 'wp_kses_named_entities', $content );
3262      }
3263  
3264      return $content;
3265  }
3266  
3267  /**
3268   * Callback for `wp_kses_normalize_entities()` regular expression.
3269   *
3270   * This function only accepts valid named entity references, which are finite,
3271   * case-sensitive, and highly scrutinized by HTML and XML validators.
3272   *
3273   * @since 3.0.0
3274   *
3275   * @global array $allowedentitynames
3276   *
3277   * @param array $matches preg_replace_callback() matches array.
3278   * @return string Correctly encoded entity.
3279   */
3280  function wp_kses_named_entities( $matches ) {
3281      global $allowedentitynames;
3282  
3283      if ( empty( $matches[1] ) ) {
3284          return '';
3285      }
3286  
3287      $i = $matches[1];
3288      return ( ! in_array( $i, $allowedentitynames, true ) ) ? "&amp;$i;" : "&$i;";
3289  }
3290  
3291  /**
3292   * Callback for `wp_kses_normalize_entities()` regular expression.
3293   *
3294   * This function only accepts valid named entity references, which are finite,
3295   * case-sensitive, and highly scrutinized by XML validators.  HTML named entity
3296   * references are converted to their code points.
3297   *
3298   * @since 5.5.0
3299   *
3300   * @global array $allowedentitynames
3301   * @global array $allowedxmlentitynames
3302   *
3303   * @param array $matches preg_replace_callback() matches array.
3304   * @return string Correctly encoded entity.
3305   */
3306  function wp_kses_xml_named_entities( $matches ) {
3307      global $allowedentitynames, $allowedxmlentitynames;
3308  
3309      if ( empty( $matches[1] ) ) {
3310          return '';
3311      }
3312  
3313      $i = $matches[1];
3314  
3315      if ( in_array( $i, $allowedxmlentitynames, true ) ) {
3316          return "&$i;";
3317      } elseif ( in_array( $i, $allowedentitynames, true ) ) {
3318          return html_entity_decode( "&$i;", ENT_HTML5 );
3319      }
3320  
3321      return "&amp;$i;";
3322  }
3323  
3324  /**
3325   * Callback for `wp_kses_normalize_entities()` regular expression.
3326   *
3327   * This function helps `wp_kses_normalize_entities()` to only accept 16-bit
3328   * values and nothing more for `&#number;` entities.
3329   *
3330   * @access private
3331   * @ignore
3332   * @since 1.0.0
3333   *
3334   * @param array $matches `preg_replace_callback()` matches array.
3335   * @return string Correctly encoded entity.
3336   */
3337  function wp_kses_normalize_entities2( $matches ) {
3338      if ( empty( $matches[1] ) ) {
3339          return '';
3340      }
3341  
3342      $i = $matches[1];
3343  
3344      if ( valid_unicode( $i ) ) {
3345          $i = str_pad( ltrim( $i, '0' ), 3, '0', STR_PAD_LEFT );
3346          $i = "&#$i;";
3347      } else {
3348          $i = "&amp;#$i;";
3349      }
3350  
3351      return $i;
3352  }
3353  
3354  /**
3355   * Callback for `wp_kses_normalize_entities()` for regular expression.
3356   *
3357   * This function helps `wp_kses_normalize_entities()` to only accept valid Unicode
3358   * numeric entities in hex form.
3359   *
3360   * @since 2.7.0
3361   * @access private
3362   * @ignore
3363   *
3364   * @param array $matches `preg_replace_callback()` matches array.
3365   * @return string Correctly encoded entity.
3366   */
3367  function wp_kses_normalize_entities3( $matches ) {
3368      if ( empty( $matches[1] ) ) {
3369          return '';
3370      }
3371  
3372      $hexchars = $matches[1];
3373  
3374      return ( ! valid_unicode( hexdec( $hexchars ) ) ) ? "&amp;#x$hexchars;" : '&#x' . ltrim( $hexchars, '0' ) . ';';
3375  }
3376  
3377  /**
3378   * Determines if a Unicode codepoint is valid.
3379   *
3380   * The definition of a valid Unicode codepoint is taken from the XML definition:
3381   *
3382   * > Characters
3383   * >
3384   * > …
3385   * > Legal characters are tab, carriage return, line feed, and the legal characters of
3386   * > Unicode and ISO/IEC 10646.
3387   * > …
3388   * > Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
3389   *
3390   * @since 2.7.0
3391   *
3392   * @link https://www.w3.org/TR/xml/#charsets
3393   *
3394   * @param int $i Unicode codepoint.
3395   * @return bool Whether or not the codepoint is a valid Unicode codepoint.
3396   */
3397  function valid_unicode( $i ) {
3398      $i = (int) $i;
3399  
3400      return (
3401          0x9 === $i || // U+0009 HORIZONTAL TABULATION (HT)
3402          0xA === $i || // U+000A LINE FEED (LF)
3403          0xD === $i || // U+000D CARRIAGE RETURN (CR)
3404          /*
3405           * The valid Unicode characters according to the XML specification:
3406           *
3407           * > any Unicode character, excluding the surrogate blocks, FFFE, and FFFF.
3408           */
3409          ( 0x20 <= $i && $i <= 0xD7FF ) ||
3410          ( 0xE000 <= $i && $i <= 0xFFFD ) ||
3411          ( 0x10000 <= $i && $i <= 0x10FFFF )
3412      );
3413  }
3414  
3415  /**
3416   * Converts all numeric HTML entities to their named counterparts.
3417   *
3418   * This function decodes numeric HTML entities (`&#65;` and `&#x41;`).
3419   * It doesn't do anything with named entities like `&auml;`, but we don't
3420   * need them in the allowed URL protocols system anyway.
3421   *
3422   * @since 1.0.0
3423   *
3424   * @param string $content Content to change entities.
3425   * @return string Content after decoded entities.
3426   */
3427  function wp_kses_decode_entities( $content ) {
3428      $content = preg_replace_callback( '/&#([0-9]+);/', '_wp_kses_decode_entities_chr', $content );
3429      $content = preg_replace_callback( '/&#[Xx]([0-9A-Fa-f]+);/', '_wp_kses_decode_entities_chr_hexdec', $content );
3430  
3431      return $content;
3432  }
3433  
3434  /**
3435   * Regex callback for `wp_kses_decode_entities()`.
3436   *
3437   * @since 2.9.0
3438   * @access private
3439   * @ignore
3440   *
3441   * @param array $matches preg match
3442   * @return string
3443   */
3444  function _wp_kses_decode_entities_chr( $matches ) {
3445      return chr( $matches[1] );
3446  }
3447  
3448  /**
3449   * Regex callback for `wp_kses_decode_entities()`.
3450   *
3451   * @since 2.9.0
3452   * @access private
3453   * @ignore
3454   *
3455   * @param array $matches preg match
3456   * @return string
3457   */
3458  function _wp_kses_decode_entities_chr_hexdec( $matches ) {
3459      return chr( hexdec( $matches[1] ) );
3460  }
3461  
3462  /**
3463   * Sanitize content with allowed HTML KSES rules.
3464   *
3465   * This function expects slashed data.
3466   *
3467   * @since 1.0.0
3468   *
3469   * @param string $data Content to filter, expected to be escaped with slashes.
3470   * @return string Filtered content.
3471   */
3472  function wp_filter_kses( $data ) {
3473      return addslashes( wp_kses( stripslashes( $data ), current_filter() ) );
3474  }
3475  
3476  /**
3477   * Sanitize content with allowed HTML KSES rules.
3478   *
3479   * This function expects unslashed data.
3480   *
3481   * @since 2.9.0
3482   *
3483   * @param string $data Content to filter, expected to not be escaped.
3484   * @return string Filtered content.
3485   */
3486  function wp_kses_data( $data ) {
3487      return wp_kses( $data, current_filter() );
3488  }
3489  
3490  /**
3491   * Sanitizes content for allowed HTML tags for post content.
3492   *
3493   * Post content refers to the page contents of the 'post' type and not `$_POST`
3494   * data from forms.
3495   *
3496   * This function expects slashed data.
3497   *
3498   * @since 2.0.0
3499   *
3500   * @param string $data Post content to filter, expected to be escaped with slashes.
3501   * @return string Filtered post content with allowed HTML tags and attributes intact.
3502   */
3503  function wp_filter_post_kses( $data ) {
3504      return addslashes( wp_kses( stripslashes( $data ), 'post' ) );
3505  }
3506  
3507  /**
3508   * Sanitizes global styles user content removing unsafe rules.
3509   *
3510   * @since 5.9.0
3511   *
3512   * @param string $data Post content to filter.
3513   * @return string Filtered post content with unsafe rules removed.
3514   */
3515  function wp_filter_global_styles_post( $data ) {
3516      $decoded_data        = json_decode( wp_unslash( $data ), true );
3517      $json_decoding_error = json_last_error();
3518      if (
3519          JSON_ERROR_NONE === $json_decoding_error &&
3520          is_array( $decoded_data ) &&
3521          isset( $decoded_data['isGlobalStylesUserThemeJSON'] ) &&
3522          $decoded_data['isGlobalStylesUserThemeJSON']
3523      ) {
3524          unset( $decoded_data['isGlobalStylesUserThemeJSON'] );
3525  
3526          $data_to_encode = WP_Theme_JSON::remove_insecure_properties( $decoded_data, 'custom' );
3527  
3528          $data_to_encode['isGlobalStylesUserThemeJSON'] = true;
3529          /**
3530           * JSON encode the data stored in post content.
3531           * Escape characters that are likely to be mangled by HTML filters: "<>&".
3532           *
3533           * This matches the escaping in {@see WP_REST_Global_Styles_Controller::prepare_item_for_database()}.
3534           */
3535          return wp_slash( wp_json_encode( $data_to_encode, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ) );
3536      }
3537      return $data;
3538  }
3539  
3540  /**
3541   * Sanitizes content for allowed HTML tags for post content.
3542   *
3543   * Post content refers to the page contents of the 'post' type and not `$_POST`
3544   * data from forms.
3545   *
3546   * This function expects unslashed data.
3547   *
3548   * @since 2.9.0
3549   *
3550   * @param string $data Post content to filter.
3551   * @return string Filtered post content with allowed HTML tags and attributes intact.
3552   */
3553  function wp_kses_post( $data ) {
3554      return wp_kses( $data, 'post' );
3555  }
3556  
3557  /**
3558   * Navigates through an array, object, or scalar, and sanitizes content for
3559   * allowed HTML tags for post content.
3560   *
3561   * @since 4.4.2
3562   *
3563   * @see map_deep()
3564   *
3565   * @param mixed $data The array, object, or scalar value to inspect.
3566   * @return mixed The filtered content.
3567   */
3568  function wp_kses_post_deep( $data ) {
3569      return map_deep( $data, 'wp_kses_post' );
3570  }
3571  
3572  /**
3573   * Strips all HTML from a text string.
3574   *
3575   * This function expects slashed data.
3576   *
3577   * @since 2.1.0
3578   *
3579   * @param string $data Content to strip all HTML from.
3580   * @return string Filtered content without any HTML.
3581   */
3582  function wp_filter_nohtml_kses( $data ) {
3583      return addslashes( wp_kses( stripslashes( $data ), 'strip' ) );
3584  }
3585  
3586  /**
3587   * Adds all KSES input form content filters.
3588   *
3589   * All hooks have default priority. The `wp_filter_kses()` function is added to
3590   * the 'pre_comment_content' and 'title_save_pre' hooks.
3591   *
3592   * The `wp_filter_post_kses()` function is added to the 'content_save_pre',
3593   * 'excerpt_save_pre', and 'content_filtered_save_pre' hooks.
3594   *
3595   * @since 2.0.0
3596   */
3597  function kses_init_filters() {
3598      // Normal filtering.
3599      add_filter( 'title_save_pre', 'wp_filter_kses' );
3600  
3601      // Comment filtering.
3602      if ( current_user_can( 'unfiltered_html' ) ) {
3603          add_filter( 'pre_comment_content', 'wp_filter_post_kses' );
3604      } else {
3605          add_filter( 'pre_comment_content', 'wp_filter_kses' );
3606      }
3607  
3608      // Global Styles filtering: Global Styles filters should be executed before normal post_kses HTML filters.
3609      add_filter( 'content_save_pre', 'wp_filter_global_styles_post', 9 );
3610      add_filter( 'content_filtered_save_pre', 'wp_filter_global_styles_post', 9 );
3611  
3612      // Post filtering.
3613      add_filter( 'content_save_pre', 'wp_filter_post_kses' );
3614      add_filter( 'excerpt_save_pre', 'wp_filter_post_kses' );
3615      add_filter( 'content_filtered_save_pre', 'wp_filter_post_kses' );
3616  }
3617  
3618  /**
3619   * Removes all KSES input form content filters.
3620   *
3621   * A quick procedural method to removing all of the filters that KSES uses for
3622   * content in WordPress Loop.
3623   *
3624   * Does not remove the `kses_init()` function from {@see 'init'} hook (priority is
3625   * default). Also does not remove `kses_init()` function from {@see 'set_current_user'}
3626   * hook (priority is also default).
3627   *
3628   * @since 2.0.6
3629   */
3630  function kses_remove_filters() {
3631      // Normal filtering.
3632      remove_filter( 'title_save_pre', 'wp_filter_kses' );
3633  
3634      // Comment filtering.
3635      remove_filter( 'pre_comment_content', 'wp_filter_post_kses' );
3636      remove_filter( 'pre_comment_content', 'wp_filter_kses' );
3637  
3638      // Global Styles filtering.
3639      remove_filter( 'content_save_pre', 'wp_filter_global_styles_post', 9 );
3640      remove_filter( 'content_filtered_save_pre', 'wp_filter_global_styles_post', 9 );
3641  
3642      // Post filtering.
3643      remove_filter( 'content_save_pre', 'wp_filter_post_kses' );
3644      remove_filter( 'excerpt_save_pre', 'wp_filter_post_kses' );
3645      remove_filter( 'content_filtered_save_pre', 'wp_filter_post_kses' );
3646  }
3647  
3648  /**
3649   * Sets up most of the KSES filters for input form content.
3650   *
3651   * First removes all of the KSES filters in case the current user does not need
3652   * to have KSES filter the content. If the user does not have `unfiltered_html`
3653   * capability, then KSES filters are added.
3654   *
3655   * @since 2.0.0
3656   */
3657  function kses_init() {
3658      kses_remove_filters();
3659  
3660      if ( ! current_user_can( 'unfiltered_html' ) ) {
3661          kses_init_filters();
3662      }
3663  }
3664  
3665  /**
3666   * Filters an inline style attribute and removes disallowed rules.
3667   *
3668   * @since 2.8.1
3669   * @since 4.4.0 Added support for `min-height`, `max-height`, `min-width`, and `max-width`.
3670   * @since 4.6.0 Added support for `list-style-type`.
3671   * @since 5.0.0 Added support for `background-image`.
3672   * @since 5.1.0 Added support for `text-transform`.
3673   * @since 5.2.0 Added support for `background-position` and `grid-template-columns`.
3674   * @since 5.3.0 Added support for `grid`, `flex` and `column` layout properties.
3675   *              Extended `background-*` support for individual properties.
3676   * @since 5.3.1 Added support for gradient backgrounds.
3677   * @since 5.7.1 Added support for `object-position`.
3678   * @since 5.8.0 Added support for `calc()` and `var()` values.
3679   * @since 6.1.0 Added support for `min()`, `max()`, `minmax()`, `clamp()`,
3680   *              nested `var()` values, and assigning values to CSS variables.
3681   *              Added support for `object-fit`, `gap`, `column-gap`, `row-gap`, and `flex-wrap`.
3682   *              Extended `margin-*` and `padding-*` support for logical properties.
3683   * @since 6.2.0 Added support for `aspect-ratio`, `position`, `top`, `right`, `bottom`, `left`,
3684   *              and `z-index` CSS properties.
3685   * @since 6.3.0 Extended support for `filter` to accept a URL and added support for repeat().
3686   *              Added support for `box-shadow`.
3687   * @since 6.4.0 Added support for `writing-mode`.
3688   * @since 6.5.0 Added support for `background-repeat`.
3689   * @since 6.6.0 Added support for `grid-column`, `grid-row`, and `container-type`.
3690   * @since 6.9.0 Added support for `white-space`.
3691   * @since 7.1.0 Extended gradient support to allow any single-level nested function.
3692   *              Added support for transform functions, `clip-path` basic shapes,
3693   *              and URLs in the SVG element reference properties.
3694   * @since 7.2.0 Added support for CSS anchor positioning properties and `text-shadow`.
3695   *              Added support for `background-clip`.
3696   *
3697   * @param string $css        A string of CSS rules, decoded from an HTML `style` attribute.
3698   * @param string $deprecated Not used.
3699   * @return string Filtered string of CSS rules, needing HTML escaping before sending back to a `style` attribute.
3700   *
3701   * @phpstan-param '' $deprecated
3702   */
3703  function safecss_filter_attr( $css, $deprecated = '' ) {
3704      if ( ! empty( $deprecated ) ) {
3705          _deprecated_argument( __FUNCTION__, '2.8.1' ); // Never implemented.
3706      }
3707  
3708      $css = wp_kses_no_null( $css );
3709      $css = str_replace( array( "\n", "\r", "\t" ), '', $css );
3710  
3711      $allowed_protocols = wp_allowed_protocols();
3712  
3713      /** @todo Parse enough CSS to split rules without breaking on things like quoted strings. */
3714      $css_array = explode( ';', trim( $css ) );
3715  
3716      /**
3717       * Filters the list of allowed CSS attributes.
3718       *
3719       * @since 2.8.1
3720       * @since 7.1.0 Added support for SVG presentation attributes.
3721       * @since 7.2.0 Added support for `background-clip` and its vendor prefixed companions.
3722       *
3723       * @param string[] $attr Array of allowed CSS attributes.
3724       */
3725      $allowed_attr = apply_filters(
3726          'safe_style_css',
3727          array(
3728              'background',
3729              'background-color',
3730              'background-image',
3731              'background-position',
3732              'background-repeat',
3733              'background-size',
3734              'background-attachment',
3735              'background-blend-mode',
3736              'background-clip',
3737              '-webkit-background-clip',
3738              '-webkit-text-fill-color',
3739  
3740              'border',
3741              'border-radius',
3742              'border-width',
3743              'border-color',
3744              'border-style',
3745              'border-right',
3746              'border-right-color',
3747              'border-right-style',
3748              'border-right-width',
3749              'border-bottom',
3750              'border-bottom-color',
3751              'border-bottom-left-radius',
3752              'border-bottom-right-radius',
3753              'border-bottom-style',
3754              'border-bottom-width',
3755              'border-bottom-right-radius',
3756              'border-bottom-left-radius',
3757              'border-left',
3758              'border-left-color',
3759              'border-left-style',
3760              'border-left-width',
3761              'border-top',
3762              'border-top-color',
3763              'border-top-left-radius',
3764              'border-top-right-radius',
3765              'border-top-style',
3766              'border-top-width',
3767              'border-top-left-radius',
3768              'border-top-right-radius',
3769  
3770              'border-spacing',
3771              'border-collapse',
3772              'caption-side',
3773  
3774              'columns',
3775              'column-count',
3776              'column-fill',
3777              'column-gap',
3778              'column-rule',
3779              'column-span',
3780              'column-width',
3781  
3782              'display',
3783  
3784              'color',
3785              'filter',
3786              'font',
3787              'font-family',
3788              'font-size',
3789              'font-style',
3790              'font-variant',
3791              'font-weight',
3792              'letter-spacing',
3793              'line-height',
3794              'text-align',
3795              'text-decoration',
3796              'text-indent',
3797              'text-shadow',
3798              'text-transform',
3799              'white-space',
3800  
3801              'height',
3802              'min-height',
3803              'max-height',
3804  
3805              'width',
3806              'min-width',
3807              'max-width',
3808  
3809              'margin',
3810              'margin-right',
3811              'margin-bottom',
3812              'margin-left',
3813              'margin-top',
3814              'margin-block-start',
3815              'margin-block-end',
3816              'margin-inline-start',
3817              'margin-inline-end',
3818  
3819              'padding',
3820              'padding-right',
3821              'padding-bottom',
3822              'padding-left',
3823              'padding-top',
3824              'padding-block-start',
3825              'padding-block-end',
3826              'padding-inline-start',
3827              'padding-inline-end',
3828  
3829              'flex',
3830              'flex-basis',
3831              'flex-direction',
3832              'flex-flow',
3833              'flex-grow',
3834              'flex-shrink',
3835              'flex-wrap',
3836  
3837              'gap',
3838              'column-gap',
3839              'row-gap',
3840  
3841              'grid-template-columns',
3842              'grid-auto-columns',
3843              'grid-column-start',
3844              'grid-column-end',
3845              'grid-column',
3846              'grid-column-gap',
3847              'grid-template-rows',
3848              'grid-auto-rows',
3849              'grid-row-start',
3850              'grid-row-end',
3851              'grid-row',
3852              'grid-row-gap',
3853              'grid-gap',
3854  
3855              'justify-content',
3856              'justify-items',
3857              'justify-self',
3858              'align-content',
3859              'align-items',
3860              'align-self',
3861  
3862              'clear',
3863              'cursor',
3864              'direction',
3865              'float',
3866              'list-style-type',
3867              'object-fit',
3868              'object-position',
3869              'opacity',
3870              'overflow',
3871              'vertical-align',
3872              'writing-mode',
3873  
3874              'position',
3875              'top',
3876              'right',
3877              'bottom',
3878              'left',
3879              'z-index',
3880              'box-shadow',
3881              'aspect-ratio',
3882              'container-type',
3883  
3884              'fill',
3885              'fill-opacity',
3886              'fill-rule',
3887  
3888              'stroke',
3889              'stroke-dasharray',
3890              'stroke-dashoffset',
3891              'stroke-linecap',
3892              'stroke-linejoin',
3893              'stroke-miterlimit',
3894              'stroke-opacity',
3895              'stroke-width',
3896  
3897              'color-interpolation',
3898              'color-interpolation-filters',
3899              'paint-order',
3900              'stop-color',
3901              'stop-opacity',
3902              'flood-color',
3903              'flood-opacity',
3904              'lighting-color',
3905  
3906              'marker',
3907              'marker-end',
3908              'marker-mid',
3909              'marker-start',
3910  
3911              'clip-path',
3912              'clip-rule',
3913              'mask',
3914              'mask-type',
3915  
3916              'cx',
3917              'cy',
3918              'r',
3919              'rx',
3920              'ry',
3921              'x',
3922              'y',
3923              'd',
3924  
3925              'alignment-baseline',
3926              'baseline-shift',
3927              'dominant-baseline',
3928              'glyph-orientation-horizontal',
3929              'glyph-orientation-vertical',
3930              'text-anchor',
3931              'unicode-bidi',
3932              'word-spacing',
3933  
3934              'font-size-adjust',
3935              'font-stretch',
3936  
3937              'color-rendering',
3938              'image-rendering',
3939              'shape-rendering',
3940              'text-rendering',
3941              'vector-effect',
3942  
3943              'transform',
3944              'transform-origin',
3945  
3946              'pointer-events',
3947              'visibility',
3948  
3949              'anchor-name',
3950              'anchor-scope',
3951              'position-anchor',
3952              'position-area',
3953              'position-try',
3954              'position-try-fallbacks',
3955              'position-try-order',
3956              'position-visibility',
3957  
3958              // Custom CSS properties.
3959              '--*',
3960          )
3961      );
3962  
3963      /*
3964       * CSS attributes that accept URL data types.
3965       *
3966       * This is in accordance to the CSS spec and unrelated to
3967       * the sub-set of supported attributes above.
3968       *
3969       * See: https://developer.mozilla.org/en-US/docs/Web/CSS/url
3970       */
3971      $css_url_data_types = array(
3972          'background',
3973          'background-image',
3974  
3975          'cursor',
3976          'filter',
3977  
3978          'list-style',
3979          'list-style-image',
3980  
3981          // SVG presentation properties that accept url() references.
3982          'clip-path',
3983          'fill',
3984          'marker',
3985          'marker-end',
3986          'marker-mid',
3987          'marker-start',
3988          'mask',
3989          'stroke',
3990      );
3991  
3992      /*
3993       * CSS attributes that accept gradient data types.
3994       *
3995       */
3996      $css_gradient_data_types = array(
3997          'background',
3998          'background-image',
3999      );
4000  
4001      if ( empty( $allowed_attr ) ) {
4002          return $css;
4003      }
4004  
4005      $css = '';
4006      foreach ( $css_array as $css_item ) {
4007          if ( '' === $css_item ) {
4008              continue;
4009          }
4010  
4011          $css_item        = trim( $css_item );
4012          $css_test_string = $css_item;
4013          $found           = false;
4014          $url_attr        = false;
4015          $gradient_attr   = false;
4016          $is_custom_var   = false;
4017  
4018          if ( ! str_contains( $css_item, ':' ) ) {
4019              $found = true;
4020          } else {
4021              $parts        = explode( ':', $css_item, 2 );
4022              $css_selector = trim( $parts[0] );
4023  
4024              // Allow assigning values to CSS variables.
4025              if ( in_array( '--*', $allowed_attr, true ) && preg_match( '/^--[a-zA-Z0-9-_]+$/', $css_selector ) ) {
4026                  $allowed_attr[] = $css_selector;
4027                  $is_custom_var  = true;
4028              }
4029  
4030              if ( in_array( $css_selector, $allowed_attr, true ) ) {
4031                  $found         = true;
4032                  $url_attr      = in_array( $css_selector, $css_url_data_types, true );
4033                  $gradient_attr = in_array( $css_selector, $css_gradient_data_types, true );
4034              }
4035  
4036              if ( $is_custom_var ) {
4037                  $css_value     = trim( $parts[1] );
4038                  $url_attr      = str_starts_with( $css_value, 'url(' );
4039                  $gradient_attr = str_contains( $css_value, '-gradient(' );
4040              }
4041          }
4042  
4043          if ( $found && $url_attr ) {
4044              // Simplified: matches the sequence `url(*)`.
4045              preg_match_all( '/url\([^)]+\)/', $parts[1], $url_matches );
4046  
4047              foreach ( $url_matches[0] as $url_match ) {
4048                  // Clean up the URL from each of the matches above.
4049                  preg_match( '/^url\(\s*([\'\"]?)(.*)(\g1)\s*\)$/', $url_match, $url_pieces );
4050  
4051                  if ( empty( $url_pieces[2] ) ) {
4052                      $found = false;
4053                      break;
4054                  }
4055  
4056                  $url = trim( $url_pieces[2] );
4057  
4058                  if ( empty( $url ) || wp_kses_bad_protocol( $url, $allowed_protocols ) !== $url ) {
4059                      $found = false;
4060                      break;
4061                  } else {
4062                      // Remove the whole `url(*)` bit that was matched above from the CSS.
4063                      $css_test_string = str_replace( $url_match, '', $css_test_string );
4064                  }
4065              }
4066          }
4067  
4068          if ( $found && $gradient_attr ) {
4069              /*
4070               * Match every `*-gradient()` in the value, allowing one level of nested functions
4071               * (e.g. rgb(), hsl(), var()). Matching each occurrence, rather than requiring the
4072               * whole value to be a single gradient, lets a gradient combine with a url() image.
4073               */
4074              preg_match_all( '/(?:repeating-)?(?:linear|radial|conic)-gradient\((?:[^()]|\([^()]*\))*\)/', $css_test_string, $gradient_matches );
4075  
4076              foreach ( $gradient_matches[0] as $gradient_match ) {
4077                  // Remove each `gradient()` bit that was matched above from the CSS.
4078                  $css_test_string = str_replace( $gradient_match, '', $css_test_string );
4079              }
4080          }
4081  
4082          if ( $found ) {
4083              /*
4084               * Allow CSS functions like var(), calc(), etc. by removing them from the test string.
4085               * Nested functions and parentheses are also removed, so long as the parentheses are balanced.
4086               */
4087              $css_test_string = preg_replace(
4088                  '/\b(?:'
4089                      // General purpose value functions.
4090                      . 'var|calc|min|max|minmax|clamp|repeat'
4091                      // Transform functions.
4092                      . '|matrix|matrix3d|perspective'
4093                      . '|rotate|rotate3d|rotateX|rotateY|rotateZ'
4094                      . '|scale|scale3d|scaleX|scaleY|scaleZ'
4095                      . '|skew|skewX|skewY'
4096                      . '|translate|translate3d|translateX|translateY|translateZ'
4097                      // Basic shape functions, as used by `clip-path`.
4098                      . '|circle|ellipse|inset|path|polygon|rect|shape|xywh'
4099                  . ')(\((?:[^()]|(?1))*\))/',
4100                  '',
4101                  $css_test_string
4102              );
4103  
4104              // Bail if the recursive function stripping hit a PCRE error (e.g. stack/backtrack limit).
4105              if ( null === $css_test_string ) {
4106                  continue;
4107              }
4108  
4109              /*
4110               * Disallow CSS containing \ ( & } = or comments, except for within url(), var(), calc(), etc.
4111               * which were removed from the test string above.
4112               */
4113              $allow_css = 0 === preg_match( '%[\\\(&=}]|/\*%', $css_test_string );
4114  
4115              /**
4116               * Filters the check for unsafe CSS in `safecss_filter_attr`.
4117               *
4118               * Enables developers to determine whether a section of CSS should be allowed or discarded.
4119               * By default, the value will be false if the part contains \ ( & } = or comments.
4120               * Return true to allow the CSS part to be included in the output.
4121               *
4122               * @since 5.5.0
4123               *
4124               * @param bool   $allow_css       Whether the CSS in the test string is considered safe.
4125               * @param string $css_test_string The CSS string to test.
4126               */
4127              $allow_css = apply_filters( 'safecss_filter_attr_allow_css', $allow_css, $css_test_string );
4128  
4129              // Only add the CSS part if it passes the regex check.
4130              if ( $allow_css ) {
4131                  if ( '' !== $css ) {
4132                      $css .= ';';
4133                  }
4134  
4135                  $css .= $css_item;
4136              }
4137          }
4138      }
4139  
4140      return $css;
4141  }
4142  
4143  /**
4144   * Helper function to add global attributes to a tag in the allowed HTML list.
4145   *
4146   * @since 3.5.0
4147   * @since 5.0.0 Added support for `data-*` wildcard attributes.
4148   * @since 6.0.0 Added `dir`, `lang`, and `xml:lang` to global attributes.
4149   * @since 6.3.0 Added `aria-controls`, `aria-current`, and `aria-expanded` attributes.
4150   * @since 6.4.0 Added `aria-live` and `hidden` attributes.
4151   * @since 7.1.0 Added `tabindex` attribute.
4152   *
4153   * @access private
4154   * @ignore
4155   *
4156   * @param array $value An array of attributes.
4157   * @return array The array of attributes with global attributes added.
4158   */
4159  function _wp_add_global_attributes( $value ) {
4160      $global_attributes = array(
4161          'aria-controls'    => true,
4162          'aria-current'     => true,
4163          'aria-describedby' => true,
4164          'aria-details'     => true,
4165          'aria-expanded'    => true,
4166          'aria-hidden'      => true,
4167          'aria-label'       => true,
4168          'aria-labelledby'  => true,
4169          'aria-live'        => true,
4170          'class'            => true,
4171          'data-*'           => true,
4172          'dir'              => true,
4173          'hidden'           => true,
4174          'id'               => true,
4175          'lang'             => true,
4176          'style'            => true,
4177          'tabindex'         => true,
4178          'title'            => true,
4179          'role'             => true,
4180          'xml:lang'         => true,
4181      );
4182  
4183      if ( true === $value ) {
4184          $value = array();
4185      }
4186  
4187      if ( is_array( $value ) ) {
4188          return array_merge( $value, $global_attributes );
4189      }
4190  
4191      return $value;
4192  }
4193  
4194  /**
4195   * Helper function to check if this is a safe PDF URL.
4196   *
4197   * @since 5.9.0
4198   * @access private
4199   * @ignore
4200   *
4201   * @param string $url The URL to check.
4202   * @return bool True if the URL is safe, false otherwise.
4203   */
4204  function _wp_kses_allow_pdf_objects( $url ) {
4205      // We're not interested in URLs that contain query strings or fragments.
4206      if ( str_contains( $url, '?' ) || str_contains( $url, '#' ) ) {
4207          return false;
4208      }
4209  
4210      // If it doesn't have a PDF extension, it's not safe.
4211      if ( ! str_ends_with( $url, '.pdf' ) ) {
4212          return false;
4213      }
4214  
4215      // If the URL host matches the current site's media URL, it's safe.
4216      $upload_info = wp_upload_dir( null, false );
4217      $parsed_url  = wp_parse_url( $upload_info['url'] );
4218      $upload_host = $parsed_url['host'] ?? '';
4219      $upload_port = isset( $parsed_url['port'] ) ? ':' . $parsed_url['port'] : '';
4220  
4221      if ( str_starts_with( $url, "http://$upload_host$upload_port/" )
4222          || str_starts_with( $url, "https://$upload_host$upload_port/" )
4223      ) {
4224          return true;
4225      }
4226  
4227      return false;
4228  }


Generated : Mon Oct 26 08:20:34 2026 Cross-referenced by PHPXref