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