true, 'id' => true, 'style' => true, 'title' => true, 'role' => true, 'tabindex' => true, 'hidden' => true, 'lang' => true, 'dir' => true, 'data-*' => true, 'aria-hidden' => true, 'aria-label' => true, 'aria-labelledby' => true, 'aria-describedby' => true, 'aria-expanded' => true, 'aria-controls' => true, 'aria-current' => true, 'aria-live' => true, 'aria-haspopup' => true, 'aria-selected' => true, 'aria-pressed' => true, 'aria-disabled' => true, 'aria-valuenow' => true, 'aria-valuemin' => true, 'aria-valuemax' => true, 'aria-roledescription' => true, ]; /** Attributes the elements added above need beyond the global set. */ private const TAG_ATTRS = [ 'svg' => ['viewbox' => true, 'xmlns' => true, 'width' => true, 'height' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, 'stroke-dasharray' => true, 'stroke-dashoffset' => true, 'preserveaspectratio' => true, 'focusable' => true, 'overflow' => true], 'button' => ['type' => true, 'value' => true, 'name' => true, 'disabled' => true, 'form' => true, 'formaction' => true, 'autofocus' => true], 'input' => ['type' => true, 'name' => true, 'value' => true, 'placeholder' => true, 'checked' => true, 'disabled' => true, 'readonly' => true, 'required' => true, 'min' => true, 'max' => true, 'step' => true, 'pattern' => true, 'size' => true, 'maxlength' => true, 'minlength' => true, 'autocomplete' => true, 'multiple' => true, 'accept' => true, 'list' => true], 'select' => ['name' => true, 'multiple' => true, 'required' => true, 'disabled' => true, 'size' => true, 'autocomplete' => true], 'option' => ['value' => true, 'selected' => true, 'disabled' => true, 'label' => true], 'optgroup' => ['label' => true, 'disabled' => true], 'textarea' => ['name' => true, 'rows' => true, 'cols' => true, 'placeholder' => true, 'required' => true, 'disabled' => true, 'readonly' => true, 'maxlength' => true, 'minlength' => true, 'autocomplete' => true, 'wrap' => true], 'label' => ['for' => true], 'form' => ['action' => true, 'method' => true, 'enctype' => true, 'target' => true, 'novalidate' => true, 'accept-charset' => true, 'autocomplete' => true], 'canvas' => ['width' => true, 'height' => true], 'source' => ['src' => true, 'srcset' => true, 'type' => true, 'media' => true, 'sizes' => true], 'track' => ['src' => true, 'kind' => true, 'srclang' => true, 'label' => true, 'default' => true], 'use' => ['href' => true, 'x' => true, 'y' => true, 'width' => true, 'height' => true], ]; /** Geometry and presentation attributes shared by the svg children. */ private const SVG_ATTRS = [ 'd' => true, 'fill' => true, 'fill-rule' => true, 'fill-opacity' => true, 'stroke' => true, 'stroke-width' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, 'stroke-dasharray' => true, 'stroke-dashoffset' => true, 'stroke-opacity' => true, 'stroke-miterlimit' => true, 'cx' => true, 'cy' => true, 'r' => true, 'rx' => true, 'ry' => true, 'x' => true, 'y' => true, 'x1' => true, 'y1' => true, 'x2' => true, 'y2' => true, 'width' => true, 'height' => true, 'points' => true, 'transform' => true, 'opacity' => true, 'offset' => true, 'stop-color' => true, 'stop-opacity' => true, 'gradientunits' => true, 'gradienttransform' => true, 'clip-path' => true, 'clip-rule' => true, 'mask' => true, 'patternunits' => true, 'viewbox' => true, 'preserveaspectratio' => true, 'text-anchor' => true, 'font-size' => true, 'font-family' => true, 'font-weight' => true, 'dx' => true, 'dy' => true, 'vector-effect' => true, 'paint-order' => true, 'marker-end' => true, 'marker-start' => true, 'xmlns' => true, ]; /** @var array>|null */ private static $allowed = null; /** * The allow-list, built once per request. * * @return array> */ private static function allowed(): array { if (self::$allowed !== null) { return self::$allowed; } $allowed = wp_kses_allowed_html('post'); /* * `script` and `style` are not in core's `post` list and must not be added here. An * inline `style` element would carry the same arbitrary CSS this layer exists to * bound; the compiler emits stylesheet text separately, through `css()` below. */ unset($allowed['script'], $allowed['style']); foreach (self::EXTRA_TAGS as $tag) { if (!isset($allowed[$tag])) { $allowed[$tag] = []; } } $svg_children = ['g', 'path', 'circle', 'ellipse', 'rect', 'line', 'polyline', 'polygon', 'defs', 'use', 'symbol', 'clippath', 'mask', 'pattern', 'lineargradient', 'radialgradient', 'stop', 'text', 'tspan', 'title', 'desc', 'marker', 'filter', 'feblend', 'fegaussianblur', 'feoffset', 'femerge', 'femergenode', 'fecolormatrix', 'animate', 'animatetransform', 'foreignobject']; foreach ($allowed as $tag => $attrs) { if (!is_array($attrs)) { $attrs = []; } $attrs = array_merge($attrs, self::GLOBAL_ATTRS); if (isset(self::TAG_ATTRS[$tag])) { $attrs = array_merge($attrs, self::TAG_ATTRS[$tag]); } if ($tag === 'svg' || in_array($tag, $svg_children, true)) { $attrs = array_merge($attrs, self::SVG_ATTRS); } $allowed[$tag] = $attrs; } self::$allowed = $allowed; return $allowed; } /** * Sanitises compiled HTML. * * kses removes any element or attribute outside the allow-list, which covers event-handler * attributes and script elements, and it rejects URL schemes outside * `wp_allowed_protocols()`, which covers `javascript:`. */ public static function html(string $html): string { if ($html === '') { return ''; } return wp_kses($html, self::allowed()); } /** * Sanitises compiled CSS. * * The stored text is printed inside a `style` element, so the one thing it must never * contain is `<`: it is not valid anywhere in CSS and it is the only way out of that * element. `@import` would pull in a stylesheet from anywhere, and `expression()` executes * script in old engines; neither is emitted by the compiler. * * The real corpus - 652 compiled stylesheets across 24 kits, 12.1 MB - contains none of * these constructs, so this function returns that input unchanged. */ /** * The stylesheet for a scene. * * CSS is never taken from a request. It is generated here from the scene's own * structured values: `Css\Compiler` reads the node tree, emits one rule per node and * builds every selector itself from an identifier it has validated. * * @param array $scene A decoded scene. */ public static function css_from_scene($scene): string { return Css\Compiler::scene($scene); } /** * The stylesheet for one library component. * * A component is compiled as a scene of its own. The breakpoints are not part of its * definition, so the caller supplies them and `Css\Scene::breakpoints()` validates them. * * @param array $def A decoded component definition: `root` and `nodes`. * @param mixed $breakpoints The breakpoint list, as sent or imported. */ public static function css_from_component($def, $breakpoints): string { if (!is_array($def) || empty($def['root']) || empty($def['nodes'])) { return ''; } return Css\Compiler::scene([ 'root' => $def['root'], 'nodes' => $def['nodes'], 'breakpoints' => Css\Scene::breakpoints($breakpoints), ]); } }