$attributes Attribute values keyed by name. * * @return array Attribute values keyed by clean name. */ public static function attribute_map( array $attributes ): array { $clean = array(); foreach ( $attributes as $name => $value ) { if ( null === $value || false === $value || ! is_scalar( $value ) ) { continue; } $name = sanitize_key( (string) $name ); if ( '' === $name || str_starts_with( $name, 'on' ) ) { continue; } $clean[ $name ] = true === $value ? true : (string) $value; } return $clean; } /** * Filters HTML an administrator wrote, such as a notification body. * * Formatting survives; scripts, styles, iframes and event handlers do not. * * @param string $html Raw HTML. */ public static function rich_text( string $html ): string { return wp_kses( $html, self::allowed_tags() ); } /** * Filters the markup of a rendered form before it reaches the page. * * The templates escape every value as they print it, so for the plugin's own * output this changes nothing. It is here for what arrives afterwards: the * `formpilot_rendered_form` and `formpilot_shortcode_output` filters and a * theme's template overrides can all hand back arbitrary HTML, and the * shortcode is the last point the plugin sees it. Scripts, styles, frames, * event handlers, `style` attributes and URLs with a disallowed protocol do * not survive; the form controls, ARIA and the `data-*` hooks the scripts * read do. * * @param string $html Rendered form markup. */ public static function form_markup( string $html ): string { return wp_kses( $html, self::form_tags() ); } /** * Returns the tags and attributes a rendered form may contain. * * @return array> Allowed tags and attributes. */ public static function form_tags(): array { // Accepted on every tag below. `data-*` is a wildcard kses understands. $global = array( 'id' => true, 'class' => true, 'role' => true, 'hidden' => true, 'tabindex' => true, 'title' => true, 'lang' => true, 'dir' => true, 'aria-controls' => true, 'aria-current' => true, 'aria-describedby' => true, 'aria-expanded' => true, 'aria-hidden' => true, 'aria-invalid' => true, 'aria-label' => true, 'aria-labelledby' => true, 'aria-live' => true, 'aria-required' => true, 'aria-valuemax' => true, 'aria-valuemin' => true, 'aria-valuenow' => true, 'data-*' => true, ); $control = array( 'name' => true, 'value' => true, 'disabled' => true, 'required' => true, 'autocomplete' => true, ); $specific = array( 'form' => array( 'action' => true, 'method' => true, 'enctype' => true, 'novalidate' => true, ), 'input' => $control + array( 'type' => true, 'checked' => true, 'placeholder' => true, 'readonly' => true, 'min' => true, 'max' => true, 'step' => true, 'minlength' => true, 'maxlength' => true, 'pattern' => true, 'accept' => true, 'multiple' => true, 'inputmode' => true, ), 'textarea' => $control + array( 'rows' => true, 'cols' => true, 'placeholder' => true, 'readonly' => true, 'minlength' => true, 'maxlength' => true, ), 'select' => $control + array( 'multiple' => true, 'size' => true, ), 'option' => array( 'value' => true, 'selected' => true, 'disabled' => true, ), 'optgroup' => array( 'label' => true, 'disabled' => true, ), 'button' => array( 'type' => true, 'name' => true, 'value' => true, 'disabled' => true, ), 'label' => array( 'for' => true ), 'fieldset' => array( 'disabled' => true ), 'legend' => array(), 'div' => array(), 'span' => array(), 'p' => array(), 'ul' => array(), 'ol' => array(), 'li' => array(), 'h2' => array(), 'h3' => array(), 'h4' => array(), 'noscript' => array(), 'a' => array( 'href' => true, 'target' => true, 'rel' => true, ), 'strong' => array(), 'em' => array(), 'b' => array(), 'i' => array(), 'small' => array(), 'br' => array(), ); $tags = array(); foreach ( $specific as $tag => $attributes ) { $tags[ $tag ] = $attributes + $global; } /** * Filters the tags and attributes allowed in a rendered form. * * An add-on or theme override that prints markup outside this list adds * it here, deliberately, rather than having the shortcode pass anything. * * @since 1.0.0 * * @param array> $tags Allowed tags and attributes. */ return (array) apply_filters( 'formpilot_form_allowed_html', $tags ); } /** * Returns the tags allowed in administrator written HTML. * * @return array> Allowed tags and attributes. */ public static function allowed_tags(): array { $tags = array( 'a' => array( 'href' => true, 'title' => true, 'target' => true, 'rel' => true, ), 'b' => array(), 'blockquote' => array(), 'br' => array(), 'em' => array(), 'h1' => array(), 'h2' => array(), 'h3' => array(), 'h4' => array(), 'h5' => array(), 'h6' => array(), 'hr' => array(), 'i' => array(), 'li' => array(), 'ol' => array(), 'p' => array(), 'strong' => array(), 'ul' => array(), /* * Present because this policy also governs notification bodies, which are * HTML emails. Those were saved under `wp_kses_post()` until the two save * paths were brought into line, so a stored body may already contain a logo, * a layout table or a heading level — and narrowing the list without these * would have stripped them the first time an administrator saved the form * for any reason at all, with no warning and nothing to restore from. * * They are also what a plain HTML email legitimately needs: mail clients * have no flexbox, so a table is how you lay one out. Nothing here can * execute: no script, style, iframe, object or event handler. */ 'img' => array( 'src' => true, 'alt' => true, 'width' => true, 'height' => true, ), 'div' => array( 'class' => true ), 'span' => array( 'class' => true ), 'table' => array( 'width' => true, 'border' => true, 'cellpadding' => true, 'cellspacing' => true ), 'thead' => array(), 'tbody' => array(), 'tr' => array(), 'th' => array( 'align' => true, 'width' => true, 'colspan' => true ), 'td' => array( 'align' => true, 'width' => true, 'colspan' => true ), 'code' => array(), 'pre' => array(), 'small' => array(), ); /** * Filters the tags allowed in administrator written HTML. * * @since 1.0.0 * * @param array> $tags Allowed tags and attributes. */ return (array) apply_filters( 'formpilot_allowed_html', $tags ); } }