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