[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

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

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


Generated : Thu Jun 13 08:20:01 2024 Cross-referenced by PHPXref