*/ private static $printed_styles = array(); /** * Register the front-end behaviour script and the stylesheet handle used to carry * per-product CSS via wp_add_inline_style(). * * Safe to call more than once, and safe to call from render time: a block's render * callback can run before wp_enqueue_scripts depending on the theme and how the page is * assembled, so registration cannot assume that hook has already fired. */ public static function register_assets(): void { if ( ! wp_script_is( self::SCRIPT_HANDLE, 'registered' ) ) { wp_register_script( self::SCRIPT_HANDLE, AFFILIATEBOXES_PLUGIN_URL . 'public/js/affiliateboxes-boxes.js', array(), defined( 'AFFILIATEBOXES_VERSION' ) ? AFFILIATEBOXES_VERSION : '1.0.0', true ); } // No src: this handle exists only to carry per-product CSS via wp_add_inline_style(). if ( ! wp_style_is( self::STYLE_HANDLE, 'registered' ) ) { wp_register_style( self::STYLE_HANDLE, false, array(), defined( 'AFFILIATEBOXES_VERSION' ) ? AFFILIATEBOXES_VERSION : '1.0.0' ); } } /** * Build the list of HTML elements and attributes allowed in a product payload. * * @return array> A wp_kses() compatible allow list. */ public static function allowed_html(): array { // Attributes every element may carry. $common = array( 'class' => true, 'id' => true, 'style' => true, 'title' => true, 'role' => true, 'lang' => true, 'dir' => true, 'hidden' => true, // Behaviour hooks for public/js/affiliateboxes-boxes.js. wp_kses() treats // 'data-*' as a wildcard, so the server can add hooks without waiting on a // plugin release. Data attributes carry no script or URL surface of their own. 'data-*' => true, // Makes a non-native control keyboard reachable. Pinned to exactly "0" so a // payload cannot reorder the whole page's tab sequence with a positive value. 'tabindex' => array( 'minval' => 0, 'maxval' => 0, ), ); /* * wp_kses() supports a data-* wildcard but has no aria-* equivalent, so every ARIA * state and property has to be named. The complete ARIA 1.2 set is listed rather * than the handful in use today: they are inert attributes with no script or URL * surface, and a partial list silently drops accessibility markup the service adds * later. Deprecated aria-dropeffect and aria-grabbed are excluded. */ $aria_attributes = array( 'activedescendant', 'atomic', 'autocomplete', 'braillelabel', 'brailleroledescription', 'busy', 'checked', 'colcount', 'colindex', 'colindextext', 'colspan', 'controls', 'current', 'describedby', 'description', 'details', 'disabled', 'errormessage', 'expanded', 'flowto', 'haspopup', 'hidden', 'invalid', 'keyshortcuts', 'label', 'labelledby', 'level', 'live', 'modal', 'multiline', 'multiselectable', 'orientation', 'owns', 'placeholder', 'posinset', 'pressed', 'readonly', 'relevant', 'required', 'roledescription', 'rowcount', 'rowindex', 'rowindextext', 'rowspan', 'selected', 'setsize', 'sort', 'valuemax', 'valuemin', 'valuenow', 'valuetext', ); foreach ( $aria_attributes as $aria_attribute ) { $common[ 'aria-' . $aria_attribute ] = true; } $text_elements = array( 'div', 'span', 'section', 'article', 'header', 'footer', 'aside', 'nav', 'main', 'p', 'br', 'hr', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'strong', 'b', 'em', 'i', 'u', 's', 'small', 'sup', 'sub', 'mark', 'del', 'ins', 'code', 'pre', 'blockquote', 'cite', 'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'figure', 'figcaption', 'details', 'summary', 'table', 'thead', 'tbody', 'tfoot', 'tr', 'caption', 'colgroup', ); $allowed = array(); foreach ( $text_elements as $element ) { $allowed[ $element ] = $common; } $allowed['a'] = array_merge( $common, array( 'href' => true, 'target' => true, 'rel' => true, 'download' => true, ) ); $allowed['img'] = array_merge( $common, array( 'src' => true, 'srcset' => true, 'sizes' => true, 'alt' => true, 'width' => true, 'height' => true, 'loading' => true, 'decoding' => true, ) ); $allowed['picture'] = $common; $allowed['source'] = array_merge( $common, array( 'src' => true, 'srcset' => true, 'sizes' => true, 'type' => true, 'media' => true, ) ); $allowed['th'] = array_merge( $common, array( 'colspan' => true, 'rowspan' => true, 'scope' => true, ) ); $allowed['td'] = array_merge( $common, array( 'colspan' => true, 'rowspan' => true, ) ); $allowed['col'] = array_merge( $common, array( 'span' => true ) ); // Checkbox-driven disclosure widgets. Only type="checkbox" survives; see filter_inputs(). $allowed['input'] = array_merge( $common, array( 'type' => true, 'checked' => true, 'name' => true, ) ); $allowed['label'] = array_merge( $common, array( 'for' => true ) ); // Inline SVG icons. Element keys must be lower case: wp_kses() looks the element up // with strtolower() but preserves the original casing on output, so camelCase SVG // elements and attributes such as viewBox still render correctly. $svg_common = array_merge( $common, array( 'fill' => true, 'fill-rule' => true, 'fill-opacity' => true, 'stroke' => true, 'stroke-width' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, 'stroke-dasharray' => true, 'stroke-opacity' => true, 'opacity' => true, 'transform' => true, 'clip-path' => true, 'clip-rule' => true, ) ); $allowed['svg'] = array_merge( $svg_common, array( 'xmlns' => true, 'xmlns:xlink' => true, 'viewbox' => true, 'width' => true, 'height' => true, 'preserveaspectratio' => true, 'focusable' => true, ) ); $allowed['path'] = array_merge( $svg_common, array( 'd' => true ) ); $allowed['g'] = $svg_common; $allowed['defs'] = $svg_common; $allowed['use'] = array_merge( $svg_common, array( 'href' => true, 'xlink:href' => true, 'x' => true, 'y' => true, ) ); $allowed['circle'] = array_merge( $svg_common, array( 'cx' => true, 'cy' => true, 'r' => true, ) ); $allowed['ellipse'] = array_merge( $svg_common, array( 'cx' => true, 'cy' => true, 'rx' => true, 'ry' => true, ) ); $allowed['rect'] = array_merge( $svg_common, array( 'x' => true, 'y' => true, 'width' => true, 'height' => true, 'rx' => true, 'ry' => true, ) ); $allowed['line'] = array_merge( $svg_common, array( 'x1' => true, 'y1' => true, 'x2' => true, 'y2' => true, ) ); $allowed['polygon'] = array_merge( $svg_common, array( 'points' => true ) ); $allowed['polyline'] = array_merge( $svg_common, array( 'points' => true ) ); $allowed['text'] = array_merge( $svg_common, array( 'x' => true, 'y' => true, 'dx' => true, 'dy' => true, 'text-anchor' => true, 'font-size' => true, 'font-family' => true, 'font-weight' => true, ) ); $allowed['tspan'] = $allowed['text']; $allowed['clippath'] = array_merge( $svg_common, array( 'clippathunits' => true ) ); $allowed['mask'] = $svg_common; $allowed['lineargradient'] = array_merge( $svg_common, array( 'x1' => true, 'y1' => true, 'x2' => true, 'y2' => true, 'gradientunits' => true, 'gradienttransform' => true, ) ); $allowed['radialgradient'] = array_merge( $svg_common, array( 'cx' => true, 'cy' => true, 'r' => true, 'fx' => true, 'fy' => true, 'gradientunits' => true, ) ); $allowed['stop'] = array_merge( $svg_common, array( 'offset' => true, 'stop-color' => true, 'stop-opacity' => true, ) ); $allowed['title'] = $common; $allowed['desc'] = $common; /** * Filters the HTML elements and attributes allowed in a synced product payload. * * Anything not listed here is removed by wp_kses() before the payload reaches the page. * * @param array> $allowed A wp_kses() compatible allow list. */ return apply_filters( 'affiliateboxes_allowed_html', $allowed ); } /** * Sanitize a product payload's markup. * * @param string $html Raw markup from affiliateboxes.com. * @return string Markup safe to output. */ public static function sanitize_html( string $html ): string { $html = self::strip_raw_text_elements( $html ); $html = wp_kses( $html, self::allowed_html(), array( 'http', 'https', 'mailto', 'tel' ) ); return self::filter_inputs( $html ); } /** * Remove elements whose text content must not survive as visible copy. * * Note that wp_kses() drops disallowed tags but keeps the text between them, so a stray *