[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

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

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


Generated : Thu Nov 21 08:20:01 2024 Cross-referenced by PHPXref