diff --git a/backport-changelog/7.1/TODO.md b/backport-changelog/7.1/TODO.md new file mode 100644 index 00000000000000..bd6ca721e6248b --- /dev/null +++ b/backport-changelog/7.1/TODO.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/TODO + +* https://github.com/WordPress/gutenberg/pull/75550 diff --git a/lib/compat/wordpress-7.1/class-gutenberg-icons-registry-7-1.php b/lib/compat/wordpress-7.1/class-gutenberg-icons-registry-7-1.php index e827056064e7b1..48a4ccb6abd9ad 100644 --- a/lib/compat/wordpress-7.1/class-gutenberg-icons-registry-7-1.php +++ b/lib/compat/wordpress-7.1/class-gutenberg-icons-registry-7-1.php @@ -50,6 +50,671 @@ protected function __construct() { } } + /** + * Sanitizes the icon SVG content. + * + * Uses WP_HTML_Processor to extract the SVG element in its entirety before + * applying wp_kses. This avoids issues where HTML tags like
inside the + * content would terminate the SVG element when parsed as HTML, and ensures + * proper handling of SVG structure including self-closing tags. + * + * @param string $icon_content The icon SVG content to sanitize. + * @return string The sanitized icon SVG content. + */ + protected function sanitize_icon_content( $icon_content ) { + // Core attributes applicable to most elements. + $core_attributes = array( + 'id' => true, + 'class' => true, + 'style' => true, + ); + + // ARIA and accessibility attributes. + $aria_attributes = array( + 'aria-hidden' => true, + 'aria-label' => true, + 'aria-labelledby' => true, + 'aria-describedby' => true, + 'role' => true, + 'focusable' => true, + 'tabindex' => true, + ); + + // Presentation attributes for graphics elements (shapes, text, use, image). + $presentation_attributes = array( + 'fill' => true, + 'fill-opacity' => true, + 'fill-rule' => true, + 'stroke' => true, + 'stroke-width' => true, + 'stroke-linecap' => true, + 'stroke-linejoin' => true, + 'stroke-miterlimit' => true, + 'stroke-dasharray' => true, + 'stroke-dashoffset' => true, + 'stroke-opacity' => true, + 'opacity' => true, + 'transform' => true, + 'clip-path' => true, + 'clip-rule' => true, + 'mask' => true, + 'filter' => true, + 'visibility' => true, + 'display' => true, + 'color' => true, + 'color-interpolation' => true, + 'color-rendering' => true, + 'vector-effect' => true, + 'paint-order' => true, + ); + + // Marker attributes (only for shape elements). + $marker_attributes = array( + 'marker-start' => true, + 'marker-mid' => true, + 'marker-end' => true, + ); + + // Container attributes for grouping elements. + $container_attributes = array( + 'transform' => true, + 'clip-path' => true, + 'mask' => true, + 'filter' => true, + 'visibility' => true, + 'display' => true, + 'opacity' => true, + ); + + /* + * Allowed tags for wp_kses(). WP_HTML_Processor::normalize() with + * constraints (similar structure to this array) is proposed to improve + * HTML/SVG sanitization in the future. + * + * @see https://github.com/dmsnell/wordpress-develop/pull/20 + */ + $allowed_tags = array( + // Root SVG element. + 'svg' => array_merge( + $core_attributes, + $aria_attributes, + $presentation_attributes, + array( + 'xmlns' => true, + 'xmlns:xlink' => true, + 'width' => true, + 'height' => true, + 'viewbox' => true, + 'preserveaspectratio' => true, + 'x' => true, + 'y' => true, + ) + ), + // Basic shape elements (with markers). + 'path' => array_merge( + $core_attributes, + $aria_attributes, + $presentation_attributes, + $marker_attributes, + array( + 'd' => true, + 'pathlength' => true, + ) + ), + 'circle' => array_merge( + $core_attributes, + $aria_attributes, + $presentation_attributes, + $marker_attributes, + array( + 'cx' => true, + 'cy' => true, + 'r' => true, + ) + ), + 'ellipse' => array_merge( + $core_attributes, + $aria_attributes, + $presentation_attributes, + $marker_attributes, + array( + 'cx' => true, + 'cy' => true, + 'rx' => true, + 'ry' => true, + ) + ), + 'line' => array_merge( + $core_attributes, + $aria_attributes, + $presentation_attributes, + $marker_attributes, + array( + 'x1' => true, + 'x2' => true, + 'y1' => true, + 'y2' => true, + ) + ), + 'polygon' => array_merge( + $core_attributes, + $aria_attributes, + $presentation_attributes, + $marker_attributes, + array( + 'points' => true, + ) + ), + 'polyline' => array_merge( + $core_attributes, + $aria_attributes, + $presentation_attributes, + $marker_attributes, + array( + 'points' => true, + ) + ), + 'rect' => array_merge( + $core_attributes, + $aria_attributes, + $presentation_attributes, + $marker_attributes, + array( + 'x' => true, + 'y' => true, + 'width' => true, + 'height' => true, + 'rx' => true, + 'ry' => true, + ) + ), + // Grouping and structural elements. + 'g' => array_merge( + $core_attributes, + $aria_attributes, + $container_attributes + ), + 'defs' => $core_attributes, + 'view' => array_merge( + $core_attributes, + array( + 'viewbox' => true, + 'preserveaspectratio' => true, + 'zoomandpan' => true, + 'viewtarget' => true, + ) + ), + 'symbol' => array_merge( + $core_attributes, + $aria_attributes, + $container_attributes, + array( + 'viewbox' => true, + 'preserveaspectratio' => true, + 'x' => true, + 'y' => true, + 'width' => true, + 'height' => true, + ) + ), + 'use' => array_merge( + $core_attributes, + $aria_attributes, + $presentation_attributes, + array( + 'href' => true, + 'xlink:href' => true, + 'x' => true, + 'y' => true, + 'width' => true, + 'height' => true, + ) + ), + 'switch' => array_merge( + $core_attributes, + $aria_attributes, + $container_attributes + ), + // Linking element. + 'a' => array_merge( + $core_attributes, + $aria_attributes, + $presentation_attributes, + $container_attributes, + array( + 'href' => true, + 'xlink:href' => true, + 'target' => true, + 'rel' => true, + 'type' => true, + ) + ), + 'clippath' => array_merge( + $core_attributes, + array( + 'clippathunits' => true, + 'transform' => true, + ) + ), + 'mask' => array_merge( + $core_attributes, + array( + 'x' => true, + 'y' => true, + 'width' => true, + 'height' => true, + 'maskunits' => true, + 'maskcontentunits' => true, + ) + ), + // Gradient elements. + 'lineargradient' => array_merge( + $core_attributes, + array( + 'x1' => true, + 'x2' => true, + 'y1' => true, + 'y2' => true, + 'gradientunits' => true, + 'gradienttransform' => true, + 'spreadmethod' => true, + 'href' => true, + 'xlink:href' => true, + ) + ), + 'radialgradient' => array_merge( + $core_attributes, + array( + 'cx' => true, + 'cy' => true, + 'r' => true, + 'fx' => true, + 'fy' => true, + 'fr' => true, + 'gradientunits' => true, + 'gradienttransform' => true, + 'spreadmethod' => true, + 'href' => true, + 'xlink:href' => true, + ) + ), + 'stop' => array_merge( + $core_attributes, + array( + 'offset' => true, + 'stop-color' => true, + 'stop-opacity' => true, + ) + ), + // Pattern element. + 'pattern' => array_merge( + $core_attributes, + array( + 'x' => true, + 'y' => true, + 'width' => true, + 'height' => true, + 'patternunits' => true, + 'patterncontentunits' => true, + 'patterntransform' => true, + 'viewbox' => true, + 'preserveaspectratio' => true, + 'href' => true, + 'xlink:href' => true, + ) + ), + // Filter elements. + 'filter' => array_merge( + $core_attributes, + array( + 'x' => true, + 'y' => true, + 'width' => true, + 'height' => true, + 'filterunits' => true, + 'primitiveunits' => true, + ) + ), + 'feblend' => array( + 'in' => true, + 'in2' => true, + 'mode' => true, + 'result' => true, + ), + 'fecolormatrix' => array( + 'in' => true, + 'type' => true, + 'values' => true, + 'result' => true, + ), + 'fecomponenttransfer' => array( + 'in' => true, + 'result' => true, + ), + 'fecomposite' => array( + 'in' => true, + 'in2' => true, + 'operator' => true, + 'k1' => true, + 'k2' => true, + 'k3' => true, + 'k4' => true, + 'result' => true, + ), + 'feconvolvematrix' => array( + 'in' => true, + 'order' => true, + 'kernelmatrix' => true, + 'divisor' => true, + 'bias' => true, + 'targetx' => true, + 'targety' => true, + 'edgemode' => true, + 'preservealpha' => true, + 'result' => true, + ), + 'fediffuselighting' => array( + 'in' => true, + 'surfacescale' => true, + 'diffuseconstant' => true, + 'result' => true, + ), + 'fedisplacementmap' => array( + 'in' => true, + 'in2' => true, + 'scale' => true, + 'xchannelselector' => true, + 'ychannelselector' => true, + 'result' => true, + ), + 'fedistantlight' => array( + 'azimuth' => true, + 'elevation' => true, + ), + 'feflood' => array( + 'flood-color' => true, + 'flood-opacity' => true, + 'result' => true, + ), + 'fegaussianblur' => array( + 'in' => true, + 'stddeviation' => true, + 'edgemode' => true, + 'result' => true, + ), + 'feimage' => array( + 'href' => true, + 'xlink:href' => true, + 'preserveaspectratio' => true, + 'result' => true, + ), + 'femerge' => array( + 'result' => true, + ), + 'femergenode' => array( + 'in' => true, + ), + 'femorphology' => array( + 'in' => true, + 'operator' => true, + 'radius' => true, + 'result' => true, + ), + 'feoffset' => array( + 'in' => true, + 'dx' => true, + 'dy' => true, + 'result' => true, + ), + 'fepointlight' => array( + 'x' => true, + 'y' => true, + 'z' => true, + ), + 'fespecularlighting' => array( + 'in' => true, + 'surfacescale' => true, + 'specularconstant' => true, + 'specularexponent' => true, + 'result' => true, + ), + 'fespotlight' => array( + 'x' => true, + 'y' => true, + 'z' => true, + 'pointsatx' => true, + 'pointsaty' => true, + 'pointsatz' => true, + 'specularexponent' => true, + 'limitingconeangle' => true, + ), + 'fetile' => array( + 'in' => true, + 'result' => true, + ), + 'feturbulence' => array( + 'basefrequency' => true, + 'numoctaves' => true, + 'seed' => true, + 'stitchtiles' => true, + 'type' => true, + 'result' => true, + ), + 'fefunca' => array( + 'type' => true, + 'tablevalues' => true, + 'slope' => true, + 'intercept' => true, + 'amplitude' => true, + 'exponent' => true, + 'offset' => true, + ), + 'fefuncb' => array( + 'type' => true, + 'tablevalues' => true, + 'slope' => true, + 'intercept' => true, + 'amplitude' => true, + 'exponent' => true, + 'offset' => true, + ), + 'fefuncg' => array( + 'type' => true, + 'tablevalues' => true, + 'slope' => true, + 'intercept' => true, + 'amplitude' => true, + 'exponent' => true, + 'offset' => true, + ), + 'fefuncr' => array( + 'type' => true, + 'tablevalues' => true, + 'slope' => true, + 'intercept' => true, + 'amplitude' => true, + 'exponent' => true, + 'offset' => true, + ), + // Text elements. + 'text' => array_merge( + $core_attributes, + $aria_attributes, + $presentation_attributes, + array( + 'x' => true, + 'y' => true, + 'dx' => true, + 'dy' => true, + 'rotate' => true, + 'textlength' => true, + 'lengthadjust' => true, + 'text-anchor' => true, + 'font-family' => true, + 'font-size' => true, + 'font-weight' => true, + 'font-style' => true, + 'font-variant' => true, + 'text-decoration' => true, + 'writing-mode' => true, + 'letter-spacing' => true, + 'word-spacing' => true, + 'dominant-baseline' => true, + 'alignment-baseline' => true, + 'baseline-shift' => true, + ) + ), + 'tspan' => array_merge( + $core_attributes, + $aria_attributes, + $presentation_attributes, + array( + 'x' => true, + 'y' => true, + 'dx' => true, + 'dy' => true, + 'rotate' => true, + 'textlength' => true, + 'lengthadjust' => true, + 'text-anchor' => true, + 'font-family' => true, + 'font-size' => true, + 'font-weight' => true, + 'font-style' => true, + 'text-decoration' => true, + ) + ), + 'textpath' => array_merge( + $core_attributes, + $aria_attributes, + $presentation_attributes, + array( + 'href' => true, + 'xlink:href' => true, + 'startoffset' => true, + 'method' => true, + 'spacing' => true, + 'text-anchor' => true, + ) + ), + // Descriptive elements. + 'title' => array(), + 'desc' => array(), + 'metadata' => array(), + // Image element. + 'image' => array_merge( + $core_attributes, + $aria_attributes, + $presentation_attributes, + array( + 'x' => true, + 'y' => true, + 'width' => true, + 'height' => true, + 'href' => true, + 'xlink:href' => true, + 'preserveaspectratio' => true, + ) + ), + // Marker element. + 'marker' => array_merge( + $core_attributes, + array( + 'markerunits' => true, + 'refx' => true, + 'refy' => true, + 'markerwidth' => true, + 'markerheight' => true, + 'orient' => true, + 'preserveaspectratio' => true, + 'viewbox' => true, + ) + ), + // Animation elements. + 'animate' => array_merge( + $core_attributes, + array( + 'attributename' => true, + 'from' => true, + 'to' => true, + 'dur' => true, + 'repeatcount' => true, + 'begin' => true, + 'end' => true, + 'values' => true, + 'keytimes' => true, + 'keysplines' => true, + 'calcmode' => true, + 'additive' => true, + 'accumulate' => true, + ) + ), + 'animatemotion' => array_merge( + $core_attributes, + array( + 'path' => true, + 'keypoints' => true, + 'rotate' => true, + 'keytimes' => true, + 'keysplines' => true, + 'calcmode' => true, + 'from' => true, + 'to' => true, + 'values' => true, + 'dur' => true, + 'repeatcount' => true, + 'begin' => true, + 'end' => true, + 'additive' => true, + 'accumulate' => true, + ) + ), + 'animatetransform' => array_merge( + $core_attributes, + array( + 'attributename' => true, + 'type' => true, + 'from' => true, + 'to' => true, + 'dur' => true, + 'repeatcount' => true, + 'begin' => true, + 'end' => true, + 'values' => true, + 'keytimes' => true, + 'keysplines' => true, + 'calcmode' => true, + 'additive' => true, + 'accumulate' => true, + ) + ), + 'set' => array_merge( + $core_attributes, + array( + 'attributename' => true, + 'to' => true, + 'begin' => true, + 'dur' => true, + 'end' => true, + 'repeatcount' => true, + ) + ), + ); + + $processor = WP_HTML_Processor::create_fragment( $icon_content ); + if ( ! $processor || ! $processor->next_token() || 'SVG' !== $processor->get_tag() ) { + return ''; + } + + $svg = $processor->serialize_token(); + $depth = $processor->get_current_depth(); + while ( $processor->next_token() && $processor->get_current_depth() >= $depth ) { + $svg .= $processor->serialize_token(); + } + $svg .= ''; + return wp_kses( $svg, $allowed_tags ); + } + /** * Modified to also search in icon labels */ diff --git a/phpunit/experimental/class-gutenberg-icons-registry-7-1-test.php b/phpunit/experimental/class-gutenberg-icons-registry-7-1-test.php new file mode 100644 index 00000000000000..66b21bf7dc53fe --- /dev/null +++ b/phpunit/experimental/class-gutenberg-icons-registry-7-1-test.php @@ -0,0 +1,198 @@ +registry = Gutenberg_Icons_Registry_7_1::get_instance(); + } + + /** + * Invokes the Gutenberg_Icons_Registry_7_1::register method on the registry instance. + * + * @param string $icon_name Icon name including namespace. + * @param array $icon_properties Icon properties (label, content, filePath). + * @return bool True if the icon was registered successfully. + */ + private function register( $icon_name, $icon_properties ) { + $method = new ReflectionMethod( $this->registry, 'register' ); + $method->setAccessible( true ); + return $method->invoke( $this->registry, $icon_name, $icon_properties ); + } + + /** + * Invokes the Gutenberg_Icons_Registry_7_1::sanitize_icon_content method on the registry instance. + * + * @param string $icon_content The icon SVG content to sanitize. + * @return string The sanitized icon SVG content. + */ + private function sanitize_icon_content( $icon_content ) { + $method = new ReflectionMethod( $this->registry, 'sanitize_icon_content' ); + $method->setAccessible( true ); + return $method->invoke( $this->registry, $icon_content ); + } + + /** + * @dataProvider data_sanitize_icon_content + * @covers Gutenberg_Icons_Registry_7_1::sanitize_icon_content + * + * @param string $input The icon content to sanitize. + * @param string $expected The expected sanitized output. + */ + public function test_sanitize_icon_content( $input, $expected ) { + $sanitized = $this->sanitize_icon_content( $input ); + $this->assertSame( $expected, $sanitized ); + } + + /** + * Data provider for test_sanitize_icon_content. + * + * @return array[] Array of arrays with input and expected sanitized output. + */ + public function data_sanitize_icon_content() { + return array( + 'extracts only first svg when multiple present' => array( + '', + '', + ), + 'returns empty svg when html-like tags present' => array( + '', + '', + ), + 'strips namespace attributes' => array( + '', + '', + ), + + // Dangerous content is stripped (wp_kses). + 'strips foreignObject but keeps text content' => array( + '', + '', + ), + 'strips script tags' => array( + '', + '', + ), + 'strips event handlers' => array( + '', + '', + ), + 'strips javascript protocol in href' => array( + '', + '', + ), + 'strips data protocol in href' => array( + '', + '', + ), + 'strips disallowed tags' => array( + '', + '', + ), + + // Returns empty string when input is not SVG. + 'returns empty for empty string' => array( + '', + '', + ), + 'returns empty for whitespace only' => array( + " \n\t ", + '', + ), + 'returns empty for plain text' => array( + 'plain text without svg', + '', + ), + 'returns empty for html without svg' => array( + '
content
', + '', + ), + 'returns empty when svg is not first element' => array( + 'before
', + '', + ), + + // Root SVG element. + 'preserves root svg element' => array( + '', + '', + ), + // Basic shape elements. + 'preserves basic shape elements' => array( + '', + '', + ), + // Grouping and structural elements. + 'preserves grouping and structural elements' => array( + '', + '', + ), + 'preserves switch element' => array( + '', + '', + ), + 'preserves view element' => array( + '', + '', + ), + 'preserves linking element' => array( + '', + '', + ), + // Gradient elements. + 'preserves gradient elements' => array( + '', + '', + ), + // Pattern element. + 'preserves pattern element' => array( + '', + '', + ), + // Filter elements. + 'preserves filter elements' => array( + '', + '', + ), + // Text elements. + 'preserves text elements' => array( + '', + '', + ), + // Descriptive elements. + 'preserves descriptive elements' => array( + '', + '', + ), + // Image element. + 'preserves image element' => array( + '', + '', + ), + // Marker element. + 'preserves marker element' => array( + '', + '', + ), + // Animation elements. + 'preserves animation elements' => array( + '', + '', + ), + ); + } +}