Initial commit: WordPress wp-content (themes, plugins, languages)

- Theme: momentry (custom theme with REST API routes)
- Plugins: code-snippets (contains all API proxies)
- Languages: zh_TW translations
- Excludes: cache, backups, uploads, logs
This commit is contained in:
OpenCode
2026-05-29 19:07:56 +08:00
commit 09ef1f000f
6521 changed files with 867163 additions and 0 deletions
+386
View File
@@ -0,0 +1,386 @@
<?php
namespace WPForms\Frontend;
/**
* AMP class.
*
* @since 1.8.1
*/
class Amp {
/**
* Whether the current page is in AMP mode or not.
*
* @since 1.8.1
*
* @var bool
*/
private $is_amp_mode;
/**
* Constructor.
*
* @since 1.8.1
*/
public function __construct() {
$this->hooks();
}
/**
* Register hooks.
*
* @since 1.8.1
*/
private function hooks() {
add_filter( 'amp_skip_post', [ $this, 'skip_post' ] );
add_filter( 'wpforms_frontend_form_atts', [ $this, 'form_atts' ], -PHP_INT_MAX, 2 );
add_action( 'wpforms_frontend_output', [ $this, 'output_state' ], -PHP_INT_MAX, 5 );
}
/**
* Check whether the current page is in AMP mode or not.
*
* @since 1.8.1
*
* @return bool True if the current page is in AMP mode.
*/
public function is_amp(): bool {
if ( is_null( $this->is_amp_mode ) ) {
$this->is_amp_mode = wpforms_is_amp();
}
return $this->is_amp_mode;
}
/**
* Stop AMP output.
*
* @since 1.8.1
*
* @param array $form_data Form data and settings.
*
* @return bool True if we need to stop the output.
*/
public function stop_output( $form_data ): bool {
// We need to stop output processing in case we are on AMP page.
// phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName
if ( ! $this->should_stop_output() ) {
return false;
}
$form_id = ! empty( $form_data['id'] ) ? (int) $form_data['id'] : 0;
$full_page_url = home_url( add_query_arg( 'nonamp', '1' ) . '#wpforms-' . $form_id );
/**
* Allow modifying the text or url for the full page on the AMP pages.
*
* @since 1.4.1.1
* @since 1.7.1 Added $form_id, $full_page_url, and $form_data arguments.
*
* @param string $text Text.
* @param int $form_id Form id.
* @param string $full_page_url Full page url.
* @param array $form_data Form data and settings.
*
* @return string
*/
$text = (string) apply_filters(
'wpforms_frontend_shortcode_amp_text',
sprintf( /* translators: %s - URL to a non-amp version of a page with the form. */
__( '<a href="%s">Go to the full page</a> to view and submit the form.', 'wpforms-lite' ),
esc_url( $full_page_url )
),
$form_id,
$full_page_url,
$form_data
);
printf(
'<p class="wpforms-shortcode-amp-text">%s</p>',
wp_kses_post( $text )
);
return true;
// phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName
}
/**
* Whether output should be stopped.
*
* @since 1.9.0
*
* @return bool
*/
private function should_stop_output(): bool {
if ( ! $this->is_amp() ) {
return false;
}
/**
* Filters PRO status of the plugin.
* Returning `true` means that AMP stop loading.
*
* @since 1.5.4.2
*
* @param bool $pro Pro status.
*/
if ( apply_filters( 'wpforms_amp_pro', wpforms()->is_pro() ) ) { // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
return true;
}
return (
! defined( 'AMP__VERSION' ) ||
version_compare( AMP__VERSION, '1.2', '<' ) ||
! is_ssl()
);
}
/**
* Disable AMP if query param is detected.
*
* This allows the full form to be accessible for Pro users or sites
* that do not have SSL.
*
* @since 1.8.1
*
* @param bool $skip Skip AMP mode, display full post.
*
* @return bool
*/
public function skip_post( $skip ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
return isset( $_GET['nonamp'] ) ? true : $skip;
}
/**
* Form attributes filter.
*
* @since 1.8.1
*
* @param array $form_atts Form attributes.
* @param array $form_data Form data.
*
* @return array
*/
public function form_atts( $form_atts, $form_data ) {
if ( ! $this->is_amp() ) {
return $form_atts;
}
// Set submitting state.
if ( ! isset( $form_atts['atts']['on'] ) ) {
$form_atts['atts']['on'] = '';
} else {
$form_atts['atts']['on'] .= ';';
}
$form_id = ! empty( $form_data['id'] ) ? (int) $form_data['id'] : 0;
$form_atts['atts']['on'] .= sprintf(
'submit:AMP.setState( %1$s ); submit-success:AMP.setState( %2$s ); submit-error:AMP.setState( %2$s );',
wp_json_encode(
[
$this->get_form_amp_state_id( $form_id ) => [ 'submitting' => true ],
]
),
wp_json_encode(
[
$this->get_form_amp_state_id( $form_id ) => [ 'submitting' => false ],
]
)
);
// Upgrade the form to be an amp-form to avoid sanitizer conversion.
if ( isset( $form_atts['atts']['action'] ) ) {
$form_atts['atts']['action-xhr'] = $form_atts['atts']['action'];
$form_atts['atts']['verify-xhr'] = $form_atts['atts']['action-xhr'];
unset( $form_atts['atts']['action'] );
}
return $form_atts;
}
/**
* Get the amp-state ID for a given form.
*
* @since 1.8.1
*
* @param int $form_id Form ID.
*
* @return string State ID.
*/
private function get_form_amp_state_id( $form_id ) {
return sprintf( 'wpforms_form_state_%d', $form_id );
}
/**
* Output AMP state.
*
* @since 1.8.1
*
* @param array $form_data Form data and settings.
* @param null $deprecated Deprecated.
* @param string $title Form title.
* @param string $description Form description.
* @param array $errors Errors.
*
* @noinspection PhpUnusedParameterInspection
*/
public function output_state( $form_data, $deprecated, $title, $description, $errors ) {
if ( ! $this->is_amp() ) {
return;
}
$state = [ 'submitting' => false ];
$form_id = ! empty( $form_data['id'] ) ? (int) $form_data['id'] : 0;
printf(
'<amp-state id="%s"><script type="application/json">%s</script></amp-state>',
$this->get_form_amp_state_id( $form_id ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
wp_json_encode( $state )
);
}
/**
* Output submit success template.
*
* @since 1.8.1
*
* @param array $form_data Form data and settings.
*
* @return bool True if the template was printed.
*/
public function output_success_template( $form_data ) {
if ( ! $this->is_amp() ) {
return false;
}
$frontend = wpforms()->obj( 'frontend' );
if ( ! $frontend ) {
return false;
}
$frontend->assets_confirmation( $form_data );
$class = (int) wpforms_setting( 'disable-css', '1' ) === 1 ?
'wpforms-confirmation-container-full' :
'wpforms-confirmation-container';
printf(
'<div submit-success><template type="amp-mustache"><div class="%s {{#redirecting}}wpforms-redirection-message{{/redirecting}}">{{{message}}}</div></template></div>',
esc_attr( $class )
);
return true;
}
/**
* Output submit error template.
*
* @since 1.8.1
*
* @return bool True if the template was printed.
*/
public function output_error_template() {
if ( ! $this->is_amp() ) {
return false;
}
echo '<div submit-error><template type="amp-mustache"><div class="wpforms-error-container"><p>{{{message}}}</p></div></template></div>';
return true;
}
/**
* Get text attribute.
*
* @since 1.8.1
*
* @param int $form_id Form ID.
* @param array $settings Form settings.
* @param string $submit Submit button text.
*
* @return string
*/
public function get_text_attr( $form_id, $settings, $submit ) {
return sprintf(
'%s.submitting ? %s : %s',
$this->get_form_amp_state_id( $form_id ),
wp_json_encode( $settings['submit_text_processing'], JSON_UNESCAPED_UNICODE ),
wp_json_encode( $submit, JSON_UNESCAPED_UNICODE )
);
}
/**
* Output captcha.
*
* @since 1.8.1
*
* @param bool $is_recaptcha_v3 Whether we use v3.
* @param array $captcha_settings Captcha settings.
* @param array $form_data Form data.
*
* @return bool
*/
public function output_captcha( $is_recaptcha_v3, $captcha_settings, $form_data ) {
if ( ! $this->is_amp() ) {
return false;
}
if ( $is_recaptcha_v3 ) {
printf(
'<amp-recaptcha-input name="wpforms[recaptcha]" data-sitekey="%s" data-action="%s" layout="nodisplay"></amp-recaptcha-input>',
esc_attr( $captcha_settings['site_key'] ),
esc_attr( 'wpforms_' . $form_data['id'] )
);
return true;
}
if ( is_super_admin() ) {
$captcha_provider = $captcha_settings['provider'] === 'hcaptcha' ? esc_html__( 'hCaptcha', 'wpforms-lite' ) : esc_html__( 'Google reCAPTCHA v2', 'wpforms-lite' );
echo '<div class="wpforms-notice wpforms-warning" style="margin: 20px 0;">';
printf(
wp_kses( /* translators: %1$s - CAPTCHA provider name, %2$s - URL to reCAPTCHA documentation. */
__( '%1$s is not supported by AMP and is currently disabled.<br><a href="%2$s" rel="noopener noreferrer" target="_blank">Upgrade to reCAPTCHA v3</a> for full AMP support. <br><em>Please note: this message is only displayed to site administrators.</em>', 'wpforms-lite' ),
[
'a' => [
'href' => [],
'rel' => [],
'target' => [],
],
'br' => [],
'em' => [],
]
),
$captcha_provider, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
'https://wpforms.com/docs/setup-captcha-wpforms/'
);
echo '</div>';
return true;
}
return false;
}
}
@@ -0,0 +1,825 @@
<?php
namespace WPForms\Frontend;
use WPForms\Integrations\Gutenberg\ThemesData;
use WPForms\Lite\Integrations\Gutenberg\ThemesData as LiteThemesData;
use WPForms\Pro\Integrations\Gutenberg\ThemesData as ProThemesData;
use WPForms\Pro\Integrations\Gutenberg\StockPhotos;
/**
* CSS variables class.
*
* @since 1.8.1
*/
class CSSVars {
/**
* Root vars and values.
*
* @since 1.8.1
*
* @var array
*/
public const ROOT_VARS = [
'field-border-radius' => '3px',
'field-border-style' => 'solid',
'field-border-size' => '1px',
'field-background-color' => self::WHITE,
'field-border-color' => 'rgba( 0, 0, 0, 0.25 )',
'field-text-color' => 'rgba( 0, 0, 0, 0.7 )',
'field-menu-color' => self::WHITE,
'label-color' => 'rgba( 0, 0, 0, 0.85 )',
'label-sublabel-color' => 'rgba( 0, 0, 0, 0.55 )',
'label-error-color' => '#d63637',
'button-border-radius' => '3px',
'button-border-style' => 'none',
'button-border-size' => '1px',
'button-background-color' => '#066aab',
'button-border-color' => '#066aab',
'button-text-color' => self::WHITE,
'page-break-color' => '#066aab',
'background-image' => 'none',
'background-position' => 'center center',
'background-repeat' => 'no-repeat',
'background-size' => 'cover',
'background-width' => '100px',
'background-height' => '100px',
'background-color' => 'rgba( 0, 0, 0, 0 )',
'background-url' => 'url()',
'container-padding' => '0px',
'container-border-style' => 'none',
'container-border-width' => '1px',
'container-border-color' => '#000000',
'container-border-radius' => '3px',
];
/**
* Container shadow vars and values.
*
* @since 1.8.8
*
* @var array
*/
public const CONTAINER_SHADOW_SIZE = [
'none' => [
'box-shadow' => 'none',
],
'small' => [
'box-shadow' => '0px 3px 5px 0px rgba(0, 0, 0, 0.1)',
],
'medium' => [
'box-shadow' => '0px 10px 20px 0px rgba(0, 0, 0, 0.1)',
],
'large' => [
'box-shadow' => '0px 30px 50px -10px rgba(0, 0, 0, 0.15)',
],
];
/**
* Field Size vars and values.
*
* @since 1.8.1
*
* @var array
*/
public const FIELD_SIZE = [
'small' => [
'input-height' => '31px',
'input-spacing' => '10px',
'font-size' => '14px',
'line-height' => '17px',
'padding-h' => '9px',
'checkbox-size' => '14px',
'sublabel-spacing' => '5px',
'icon-size' => '0.75',
],
'medium' => [
'input-height' => '43px',
'input-spacing' => '15px',
'font-size' => '16px',
'line-height' => '19px',
'padding-h' => '14px',
'checkbox-size' => '16px',
'sublabel-spacing' => '5px',
'icon-size' => '1',
],
'large' => [
'input-height' => '50px',
'input-spacing' => '20px',
'font-size' => '18px',
'line-height' => '21px',
'padding-h' => '14px',
'checkbox-size' => '18px',
'sublabel-spacing' => '10px',
'icon-size' => '1.25',
],
];
/**
* Label Size vars and values.
*
* @since 1.8.1
*
* @var array
*/
public const LABEL_SIZE = [
'small' => [
'font-size' => '14px',
'line-height' => '17px',
'sublabel-font-size' => '13px',
'sublabel-line-height' => '16px',
],
'medium' => [
'font-size' => '16px',
'line-height' => '19px',
'sublabel-font-size' => '14px',
'sublabel-line-height' => '17px',
],
'large' => [
'font-size' => '18px',
'line-height' => '21px',
'sublabel-font-size' => '16px',
'sublabel-line-height' => '19px',
],
];
/**
* Button Size vars and values.
*
* @since 1.8.1
*
* @var array
*/
public const BUTTON_SIZE = [
'small' => [
'font-size' => '14px',
'height' => '37px',
'padding-h' => '15px',
'margin-top' => '5px',
],
'medium' => [
'font-size' => '17px',
'height' => '41px',
'padding-h' => '15px',
'margin-top' => '10px',
],
'large' => [
'font-size' => '20px',
'height' => '48px',
'padding-h' => '20px',
'margin-top' => '15px',
],
];
/**
* Spare variables.
*
* @since 1.8.8
*
* @var array
*/
private const SPARE_VARS = [ 'field-border-color' ];
/**
* White color.
*
* @since 1.8.8
*
* @var string
*/
private const WHITE = '#ffffff';
/**
* Render engine.
*
* @since 1.8.1
*
* @var string
*/
private $render_engine;
/**
* CSS variables.
*
* @since 1.8.1
*
* @var array
*/
private $css_vars;
/**
* Flag to check if root CSS vars were output.
*
* @since 1.8.1
*
* @var bool
*/
private $is_root_vars_displayed;
/**
* Initialize class.
*
* @since 1.8.1
*/
public function init(): void {
$this->init_vars();
}
/**
* CSS variables data.
*
* @since 1.8.1
*/
private function init_vars(): void {
$vars = [];
$vars[':root'] = array_merge(
self::ROOT_VARS,
$this->get_complex_vars( 'field-size', self::FIELD_SIZE['medium'] ),
$this->get_complex_vars( 'label-size', self::LABEL_SIZE['medium'] ),
$this->get_complex_vars( 'button-size', self::BUTTON_SIZE['medium'] ),
$this->get_complex_vars( 'container-shadow-size', self::CONTAINER_SHADOW_SIZE['none'] )
);
/**
* Allows developers to modify default CSS variables which output on the frontend.
*
* @since 1.8.1
*
* @param array $vars CSS variables two-dimensional array.
* The first level keys is the CSS selector.
* Second level keys is the variable name without the `--wpforms-` prefix.
*/
$this->css_vars = apply_filters( 'wpforms_frontend_css_vars_init_vars', $vars );
}
/**
* Get complex CSS variables data.
*
* @since 1.8.1
*
* @param string $prefix CSS variable prefix.
* @param array $values Values.
*/
public function get_complex_vars( string $prefix, array $values ): array {
$vars = [];
foreach ( $values as $key => $value ) {
$vars[ "{$prefix}-{$key}" ] = $value;
}
return $vars;
}
/**
* Get CSS variables data by selector.
*
* @since 1.8.1
*
* @param string $selector Selector.
*
* @return array
*/
public function get_vars( string $selector = ':root' ): array {
if ( empty( $this->css_vars[ $selector ] ) ) {
return [];
}
return $this->css_vars[ $selector ];
}
/**
* Output root CSS variables.
*
* @since 1.8.1
* @since 1.8.1.2 Added $force argument.
* @deprecated 1.9.3
*
* @param bool $force Force output root variables.
*
* @noinspection PhpMissingParamTypeInspection
*/
public function output_root( $force = false ): void {
_deprecated_function( __METHOD__, '1.9.3 of the WPForms plugin' );
if ( ! empty( $this->is_root_vars_displayed ) && empty( $force ) ) {
return;
}
$this->output_selector_vars( ':root', $this->css_vars[':root'] );
$this->is_root_vars_displayed = true;
}
/**
* Get root variables CSS.
*
* @since 1.9.3
*
* @return string
*/
public function get_root_vars_css(): string {
return $this->get_selector_vars_css( ':root', $this->css_vars[':root'] );
}
/**
* Output selector's CSS variables.
*
* @since 1.8.1
*
* @param string $selector Selector.
* @param array $vars Variables data.
* @param string $style_id Style tag ID attribute. Optional. Default is an empty string.
* @param string|int $form_id Form ID. Optional. Default is an empty string.
*/
public function output_selector_vars( string $selector, array $vars, string $style_id = '', $form_id = '' ): void {
if ( empty( $this->render_engine ) ) {
$this->render_engine = wpforms_get_render_engine();
}
if ( $this->render_engine === 'classic' ) {
return;
}
// If this is not full "Base and Form Theme Styling", skip.
if ( (int) wpforms_setting( 'disable-css', '1' ) !== 1 ) {
return;
}
$style_id = empty( $style_id ) ? 'wpforms-css-vars-' . $selector : $style_id;
printf(
'<style id="%1$s">
%2$s
</style>',
sanitize_key( $style_id ),
$this->get_selector_vars_css( $selector, $vars, $form_id ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
);
}
/**
* Output CSS vars for the form added as a shortcode.
*
* @since 1.9.7
*
* @param array $atts Shortcode attributes.
*/
public function output_css_vars_for_shortcode( array $atts ): void {
if ( empty( $atts['id'] ) ) {
return;
}
$form_handler = wpforms()->obj( 'form' );
if ( ! $form_handler ) {
return;
}
$form_id = (int) $atts['id'];
$form_data = $form_handler->get( $form_id, [ 'content_only' => true ] );
if ( empty( $form_data ) ) {
return;
}
$attr = isset( $form_data['settings']['themes'] ) ? (array) $form_data['settings']['themes'] : [];
$attr = $this->maybe_override_attributes( $attr );
$css_vars = $this->get_customized_css_vars( $attr );
$css_vars = $this->add_css_vars_units( $css_vars );
$selector = "#wpforms-{$form_id}";
$style_id = "wpforms-css-vars-{$form_id}";
$this->output_selector_vars( $selector, $css_vars, $style_id, $form_id );
$this->output_custom_css( $attr, $selector, $style_id );
}
/**
* Output custom CSS.
*
* @since 1.9.7
*
* @param array $attr Attributes.
* @param string $selector Selector.
* @param string $style_id Style ID.
*
* @noinspection PhpMissingParamTypeInspection
*/
private function output_custom_css( array $attr, string $selector, string $style_id ): void {
if ( wpforms_get_render_engine() === 'classic' ) {
return;
}
$custom_css = trim( $attr['customCss'] ?? '' );
if ( empty( $custom_css ) ) {
return;
}
printf(
'<style id="%1$s-custom-css">
%2$s {
%3$s
}
</style>',
sanitize_key( $style_id ),
$selector, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
wp_strip_all_tags( $custom_css ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
);
}
/**
* Maybe override attributes with themes.json settings.
*
* @since 1.9.7
*
* @param array $attr Attributes.
*
* @return array
*/
private function maybe_override_attributes( array $attr ): array {
$theme_slug = (string) ( $attr['wpformsTheme'] ?? '' );
if ( empty( $theme_slug ) ) {
return $attr;
}
$attr = $this->normalize_background_url( $attr );
$theme_data = $this->get_themes_data_object()->get_theme( $theme_slug );
$settings = $theme_data['settings'] ?? [];
return array_merge( $attr, $settings );
}
/**
* Normalize background URL.
*
* Check if the background URL is not wrapped in url() and add it if needed.
*
* @since 1.9.7
*
* @param array $attr Attributes.
*/
private function normalize_background_url( array $attr ): array {
if ( ! isset( $attr['backgroundUrl'] ) ) {
return $attr;
}
if ( strpos( $attr['backgroundUrl'], 'url(' ) === 0 ) {
return $attr;
}
$attr['backgroundUrl'] = 'url(' . $attr['backgroundUrl'] . ')';
return $attr;
}
/**
* Get themes data object.
*
* @since 1.9.7
*
* @return ThemesData
*/
private function get_themes_data_object(): ThemesData {
if ( wpforms()->is_pro() ) {
return new ProThemesData( new StockPhotos() );
}
return new LiteThemesData();
}
/**
* Add CSS vars units.
*
* Form builder saves values without pixels, we need to add them before outputting as CSS vars.
*
* @since 1.9.7
*
* @param array $css_vars CSS vars.
*
* @return array
*/
private function add_css_vars_units( array $css_vars ): array {
$has_pixels = [
'field-border-size',
'field-border-radius',
'button-border-size',
'button-border-radius',
'container-padding',
'container-border-width',
'container-border-radius',
];
foreach ( $has_pixels as $key ) {
if ( isset( $css_vars[ $key ] ) && is_numeric( $css_vars[ $key ] ) && $css_vars[ $key ] > 0 ) {
$css_vars[ $key ] .= 'px';
}
}
return $css_vars;
}
/**
* Get selector variables CSS.
*
* @since 1.9.3
*
* @param string $selector Selector.
* @param array $vars Variables data.
* @param string|int $form_id Form ID. Optional. Default is an empty string.
*
* @return string
*/
private function get_selector_vars_css( string $selector, array $vars, $form_id = '' ): string {
return sprintf(
'%1$s {
%2$s
}',
$selector, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
wp_strip_all_tags( $this->get_vars_css( $vars, $form_id ) )
);
}
/**
* Pre print vars filter.
*
* @since 1.8.8
*
* @param array $vars Variables data.
* @param string|int $form_id Form ID. Optional. Default is an empty string.
*
* @return array
*/
private function get_pre_print_vars( array $vars, $form_id = '' ): array {
// Normalize the `background-url` variable.
if ( isset( $vars['background-url'] ) ) {
$vars['background-url'] = $vars['background-url'] === 'url()' ? 'none' : $vars['background-url'];
}
/**
* Filter CSS variables right before printing the CSS.
*
* @since 1.8.8
*
* @param array $vars CSS variables.
* @param int $form_id Form ID. Optional. Default is an empty string.
*/
return (array) apply_filters( 'wpforms_frontend_css_vars_pre_print_filter', $vars, $form_id );
}
/**
* Generate CSS code from given vars data.
*
* @since 1.8.1
*
* @param array $vars Variables data.
* @param string|int $form_id Form ID. Optional. Default is an empty string.
*/
private function get_vars_css( array $vars, $form_id = '' ): string {
$vars = $this->get_pre_print_vars( $vars, $form_id );
$result = '';
foreach ( $vars as $name => $value ) {
if ( ! is_string( $value ) ) {
continue;
}
if ( $value === '0' ) {
$value = '0px';
}
$result .= "--wpforms-{$name}: {$value};\n";
if ( in_array( $name, self::SPARE_VARS, true ) ) {
$result .= "--wpforms-{$name}-spare: {$value};\n";
}
}
return $result;
}
/**
* Get customized CSS vars.
*
* @since 1.8.3
*
* @param array $attr Attributes passed by integration.
*
* @return array
*/
public function get_customized_css_vars( array $attr ): array { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh
$root_css_vars = $this->get_vars();
$css_vars = [];
foreach ( $attr as $key => $value ) {
$var_name = strtolower( preg_replace( '/[A-Z]/', '-$0', $key ) );
// Skip an attribute that is not the CSS var or has the default value.
if ( empty( $root_css_vars[ $var_name ] ) || $root_css_vars[ $var_name ] === $value ) {
continue;
}
$css_vars[ $var_name ] = $value;
}
// Reset border size in case of border style is `none`.
$css_vars = $this->maybe_reset_border( $css_vars, 'field-border' );
$css_vars = $this->maybe_reset_border( $css_vars, 'button-border' );
// Set the button alternative background color and use border color for accent in case of transparent color.
$button_bg_color = $css_vars['button-background-color'] ?? $root_css_vars['button-background-color'];
if ( $this->is_transparent_color( $button_bg_color ) ) {
$css_vars['button-background-color-alt'] = $button_bg_color;
$border_color = $css_vars['button-border-color'] ?? $root_css_vars['button-border-color'];
$css_vars['button-background-color'] = $this->is_transparent_color( $border_color ) ? $root_css_vars['button-background-color'] : $border_color;
$button_bg_color = $css_vars['button-background-color'];
}
$button_bg_color = strtolower( $button_bg_color );
// Set the button alternative text color in case if the background and text color are identical.
$button_text_color = strtolower( $css_vars['button-text-color'] ?? $root_css_vars['button-text-color'] );
if ( $button_bg_color === $button_text_color || $this->is_transparent_color( $button_text_color ) ) {
$css_vars['button-text-color-alt'] = $this->get_contrast_color( $button_bg_color );
}
$size_css_vars = $this->get_size_css_vars( $attr );
return array_merge( $css_vars, $size_css_vars );
}
/**
* Reset border size in case of border style is `none`.
*
* @since 1.9.7
*
* @param array $css_vars CSS vars.
* @param string $key Key.
*
* @return array
*/
private function maybe_reset_border( array $css_vars, string $key ): array {
$style_key = $key . '-style';
$size_key = $key . '-size';
if ( isset( $css_vars[ $style_key ] ) && $css_vars[ $style_key ] === 'none' ) {
$css_vars[ $size_key ] = '0px';
}
return $css_vars;
}
/**
* Checks if the provided color has transparency.
*
* @since 1.8.8
*
* @param string $color The color to check.
*
* @return bool
*/
private function is_transparent_color( string $color ): bool {
$rgba = $this->get_color_as_rgb_array( $color );
$opacity_threshold = 0.33;
$opacity = $rgba[3] ?? 1;
return $opacity < $opacity_threshold;
}
/**
* Get contrast color relative to a given color.
*
* @since 1.8.8
*
* @param string|array $color The color.
*
* @return string
*/
private function get_contrast_color( $color ): string {
$rgba = is_array( $color ) ? $color : $this->get_color_as_rgb_array( $color );
$avg = (int) ( ( ( array_sum( $rgba ) ) / 3 ) * ( $rgba[3] ?? 1 ) );
return $avg < 128 ? '#ffffff' : '#000000';
}
/**
* Get size CSS vars.
*
* @since 1.8.3
* @since 1.8.8 Removed $css_vars argument.
*
* @param array $attr Attributes passed by integration.
*
* @return array
*/
private function get_size_css_vars( array $attr ): array {
$size_items = [ 'field', 'label', 'button', 'container-shadow' ];
$size_css_vars = [];
foreach ( $size_items as $item ) {
$item_attr = preg_replace_callback(
'/-(\w)/',
static function ( $matches ) {
return strtoupper( $matches[1] );
},
$item
);
$item_attr .= 'Size';
$item_key = $item . '-size';
$item_constant = 'self::' . str_replace( '-', '_', strtoupper( $item ) ) . '_SIZE';
if ( empty( $attr[ $item_attr ] ) ) {
continue;
}
$size_css_vars[] = $this->get_complex_vars( $item_key, constant( $item_constant )[ $attr[ $item_attr ] ] );
}
return empty( $size_css_vars ) ? [] : array_merge( ...$size_css_vars );
}
/**
* Get color as an array of RGB(A) values.
*
* @since 1.8.8
*
* @param string $color Color.
*
* @return array|bool Color as an array of RGBA values. False on error.
*/
private function get_color_as_rgb_array( string $color ) {
// Remove # from the beginning of the string and remove whitespaces.
$color = preg_replace( '/^#/', '', strtolower( trim( $color ) ) );
$color = str_replace( ' ', '', (string) $color );
if ( $color === 'transparent' ) {
$color = 'rgba(0,0,0,0)';
}
$rgba = $color;
$rgb_array = [];
// Check if color is in HEX(A) format.
$is_hex = preg_match( '/[0-9a-f]{6,8}$/', $rgba );
if ( $is_hex ) {
// Search and split HEX(A) color into an array of char couples.
preg_match_all( '/\w\w/', $rgba, $rgb_array );
$rgb_array = array_map(
static function ( $value ) {
return hexdec( '0x' . $value );
},
$rgb_array[0] ?? []
);
$rgb_array[3] = ( $rgb_array[3] ?? 255 ) / 255;
} else {
$rgba = preg_replace( '/[^\d,.]/', '', $rgba );
$rgb_array = explode( ',', $rgba );
}
return $rgb_array;
}
}
@@ -0,0 +1,670 @@
<?php
namespace WPForms\Frontend;
/**
* Captcha class.
*
* @since 1.8.1
*/
class Captcha {
/**
* Initialize class.
*
* @since 1.8.1
*/
public function init() {
$this->hooks();
}
/**
* Register hooks.
*
* @since 1.8.1
*/
private function hooks() {
// Filters.
add_filter( 'script_loader_tag', [ $this, 'set_defer_attribute' ], 10, 3 );
// Actions.
add_action( 'send_headers', [ $this, 'send_headers' ] );
add_action( 'wpforms_frontend_output', [ $this, 'recaptcha' ], 20, 5 );
add_action( 'wp_enqueue_scripts', [ $this, 'recaptcha_noconflict' ], 9999 );
add_action( 'wp_footer', [ $this, 'recaptcha_noconflict' ], 19 );
add_action( 'wpforms_wp_footer', [ $this, 'assets_recaptcha' ] );
}
/**
* Send HTTP headers to prevent warning in the browser console.
*
* @since 1.9.8.3
*/
public function send_headers(): void {
if ( headers_sent() ) {
return;
}
$urls = '"https://www.google.com" "https://www.gstatic.com" "https://recaptcha.net" "https://challenges.cloudflare.com" "https://hcaptcha.com"';
header(
'Permissions-Policy: ' .
"private-state-token-redemption=(self $urls), " .
"private-state-token-issuance=(self $urls)",
false
);
}
/**
* CAPTCHA output if configured.
*
* @since 1.8.1
*
* @param array $form_data Form data and settings.
* @param null $deprecated Deprecated in v1.3.7, previously was $form object.
* @param bool $title Whether to display form title.
* @param bool $description Whether to display form description.
* @param array $errors List of all errors filled in WPForms_Process::process().
*
* @noinspection HtmlUnknownAttribute
* @noinspection PhpUnusedParameterInspection
*/
public function recaptcha( $form_data, $deprecated, $title, $description, $errors ) {
// Check that CAPTCHA is configured in the settings.
$captcha_settings = $this->get_form_captcha_settings( $form_data );
if ( ! $captcha_settings ) {
return;
}
$frontend = wpforms()->obj( 'frontend' );
$container_classes = [ 'wpforms-recaptcha-container', 'wpforms-is-' . $captcha_settings['provider'] ];
if ( $captcha_settings['provider'] === 'recaptcha' ) {
$container_classes[] = 'wpforms-is-recaptcha-type-' . $captcha_settings['recaptcha_type'];
}
printf(
'<div class="%1$s" %2$s>',
wpforms_sanitize_classes( $container_classes, true ),
$frontend->pages ? 'style="display:none;"' : ''
);
$this->print_recaptcha_fields( $captcha_settings, $form_data );
if ( ! empty( $errors['recaptcha'] ) ) {
$frontend->form_error( 'recaptcha', $errors['recaptcha'] );
}
echo '</div>';
}
/**
* Get a provider-specific captcha class.
*
* @since 1.9.8.3
*
* @param string $provider Captcha provider.
*
* @return string
*/
private function get_captcha_class( string $provider ): string {
$classes = [
'recaptcha' => 'g-recaptcha',
'hcaptcha' => 'h-captcha',
'turnstile' => 'wpforms-turnstile',
];
return $classes[ $provider ] ?? 'g-recaptcha';
}
/**
* Get recaptcha data.
*
* @since 1.8.6
*
* @param array $captcha_settings Captcha settings.
* @param array $form_data Form data and settings.
*
* @return array
*/
private function get_recaptcha_data( array $captcha_settings, array $form_data ): array {
/**
* Filters captcha sitekey.
*
* @since 1.7.1
*
* @param array $sitekey Sitekey.
* @param array $form_data Form data and settings.
*/
$data = apply_filters( // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
'wpforms_frontend_recaptcha',
[ 'sitekey' => $captcha_settings['site_key'] ],
$form_data
);
$is_recaptcha = $captcha_settings['provider'] === 'recaptcha';
$is_turnstile = $captcha_settings['provider'] === 'turnstile';
if ( $is_recaptcha && $captcha_settings['recaptcha_type'] === 'invisible' ) {
$data['size'] = 'invisible';
}
if ( ! $is_turnstile ) {
return $data;
}
/**
* Filter Turnstile action value.
*
* @since 1.8.1
*
* @param string $action Action value. Can only contain up to 32 alphanumeric characters including _ and -.
* @param array $form_data Form data and settings.
*/
$data['action'] = apply_filters( // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
'wpforms_frontend_recaptcha_turnstile_action',
sprintf(
'FormID-%d',
$form_data['id']
),
$form_data
);
return $data;
}
/**
* Print recaptcha fields.
*
* @since 1.8.6
*
* @param array $captcha_settings Captcha settings.
* @param array $form_data Form data and settings.
*/
private function print_recaptcha_fields( array $captcha_settings, array $form_data ) {
$data = $this->get_recaptcha_data( $captcha_settings, $form_data );
$is_recaptcha = $captcha_settings['provider'] === 'recaptcha';
$is_recaptcha_v3 = $is_recaptcha && $captcha_settings['recaptcha_type'] === 'v3';
if ( $is_recaptcha_v3 ) {
// The value adds via JS code.
echo '<input type="hidden" name="wpforms[recaptcha]" value="">';
return;
}
$captcha_class = $this->get_captcha_class( $captcha_settings['provider'] );
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<div ' . wpforms_html_attributes( '', [ $captcha_class ], $data ) . '></div>';
if ( $is_recaptcha && $captcha_settings['recaptcha_type'] === 'invisible' ) {
return;
}
printf(
'<input type="text" name="g-recaptcha-hidden" class="wpforms-recaptcha-hidden" style="position:absolute!important;clip:rect(0,0,0,0)!important;height:1px!important;width:1px!important;border:0!important;overflow:hidden!important;padding:0!important;margin:0!important;" data-rule-%1$s="1">',
esc_attr( $captcha_settings['provider'] )
);
}
/**
* Get captcha settings for form output.
* Return null if captcha is disabled.
*
* @since 1.8.1
*
* @param array $form_data Form data and settings.
*
* @return array|null
* @noinspection NullPointerExceptionInspection
*/
private function get_form_captcha_settings( $form_data ) {
$captcha_settings = wpforms_get_captcha_settings();
if (
empty( $captcha_settings['provider'] ) ||
$captcha_settings['provider'] === 'none' ||
empty( $captcha_settings['site_key'] ) ||
empty( $captcha_settings['secret_key'] )
) {
return null;
}
// Check that the CAPTCHA is configured for the specific form.
if (
! isset( $form_data['settings']['recaptcha'] ) ||
$form_data['settings']['recaptcha'] !== '1'
) {
return null;
}
$is_recaptcha_v3 = $captcha_settings['provider'] === 'recaptcha' && $captcha_settings['recaptcha_type'] === 'v3';
if ( wpforms()->obj( 'amp' )->output_captcha( $is_recaptcha_v3, $captcha_settings, $form_data ) ) {
return null;
}
return $captcha_settings;
}
/**
* Google reCAPTCHA no-conflict mode.
*
* When enabled in the WPForms settings, forcefully remove all other
* reCAPTCHA enqueues to prevent conflicts. Filter can be used to target
* specific pages, etc.
*
* @since 1.4.5
* @since 1.6.4 Added hCaptcha support.
*/
public function recaptcha_noconflict() {
$captcha_settings = wpforms_get_captcha_settings();
if (
empty( $captcha_settings['provider'] ) ||
$captcha_settings['provider'] === 'none' ||
empty( wpforms_setting( 'recaptcha-noconflict' ) ) ||
/**
* Filters recaptcha no conflict flag.
*
* @since 1.6.4
*
* @param bool $recaptcha_no_conflict No conflict flag.
*/
! apply_filters( 'wpforms_frontend_recaptcha_noconflict', true ) // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
) {
return;
}
$scripts = wp_scripts();
$urls = [ 'google.com/recaptcha', 'gstatic.com/recaptcha', 'hcaptcha.com/1' ];
foreach ( $scripts->queue as $handle ) {
// Skip the WPForms javascript-assets.
if (
! isset( $scripts->registered[ $handle ] ) ||
false !== strpos( $scripts->registered[ $handle ]->handle, 'wpforms' )
) {
return;
}
foreach ( $urls as $url ) {
if ( false !== strpos( $scripts->registered[ $handle ]->src, $url ) ) {
wp_dequeue_script( $handle );
wp_deregister_script( $handle );
break;
}
}
}
}
/**
* Load the assets needed for the CAPTCHA.
*
* @since 1.6.2
* @since 1.6.4 Added hCaptcha support.
*
* @param array $forms Forms being displayed.
*/
public function assets_recaptcha( $forms ) {
$captcha_settings = $this->get_assets_captcha_settings( $forms );
if ( ! $captcha_settings ) {
return;
}
$is_recaptcha_v3 = $captcha_settings['provider'] === 'recaptcha' && $captcha_settings['recaptcha_type'] === 'v3';
$recaptcha_url = $is_recaptcha_v3 ?
'https://www.google.com/recaptcha/api.js?render=' . $captcha_settings['site_key'] :
/**
* For backward compatibility reason we have to filter only the v2 reCAPTCHA.
*
* @since 1.4.0
*
* @param string $url The reCaptcha v2 URL.
*/
apply_filters( 'wpforms_frontend_recaptcha_url', 'https://www.google.com/recaptcha/api.js?onload=wpformsRecaptchaLoad&render=explicit' ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
$captcha_api_array = [
'hcaptcha' => 'https://hcaptcha.com/1/api.js?onload=wpformsRecaptchaLoad&render=explicit&recaptchacompat=off',
'recaptcha' => $recaptcha_url,
'turnstile' => 'https://challenges.cloudflare.com/turnstile/v0/api.js?onload=wpformsRecaptchaLoad&render=explicit',
];
/**
* Filter the CAPTCHA API URL.
*
* @since 1.6.4
*
* @param string $captcha_api The CAPTCHA API URL.
*/
$captcha_api = apply_filters( 'wpforms_frontend_captcha_api', $captcha_api_array[ $captcha_settings['provider'] ] );
$in_footer = ! wpforms_is_frontend_js_header_force_load();
wp_enqueue_script(
'wpforms-recaptcha',
$captcha_api,
$is_recaptcha_v3 ? [] : [ 'jquery' ],
null, // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
$in_footer
);
/**
* Filter the string containing the CAPTCHA JavaScript to be added.
*
* @since 1.6.4
*
* @param string $captcha_inline The CAPTCHA JavaScript.
*/
$captcha_inline = apply_filters(
'wpforms_frontend_captcha_inline_script',
$this->get_captcha_inline_script( $captcha_settings )
);
wp_add_inline_script( 'wpforms-recaptcha', $captcha_inline );
}
/**
* Get captcha settings for assets output.
* Return null if captcha is disabled.
*
* @since 1.8.1
*
* @param array $forms Forms being displayed.
*
* @return array|null
* @noinspection NullPointerExceptionInspection
*/
private function get_assets_captcha_settings( $forms ) {
/**
* Filters disable captcha switch.
*
* @since 1.6.2
*
* @param bool $is_captcha_disabled Whether captcha is disabled.
*/
if ( apply_filters( 'wpforms_frontend_recaptcha_disable', false ) ) { // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
return null;
}
// Load CAPTCHA support if form supports it.
$captcha_settings = wpforms_get_captcha_settings();
if (
empty( $captcha_settings['provider'] ) ||
$captcha_settings['provider'] === 'none' ||
empty( $captcha_settings['site_key'] ) ||
empty( $captcha_settings['secret_key'] )
) {
return null;
}
// Whether at least 1 form on a page has CAPTCHA enabled.
$captcha = false;
foreach ( $forms as $form ) {
if ( ! empty( $form['settings']['recaptcha'] ) ) {
$captcha = true;
break;
}
}
// Return early.
if ( ! $captcha && ! wpforms()->obj( 'frontend' )->assets_global() ) {
return null;
}
return $captcha_settings;
}
/**
* Retrieve the string containing the CAPTCHA inline javascript.
*
* @since 1.6.4
*
* @param array $captcha_settings The CAPTCHA settings.
*
* @return string
* @noinspection JSUnusedLocalSymbols
* @noinspection UnnecessaryLocalVariableJS
* @noinspection JSUnresolvedVariable
* @noinspection JSDeprecatedSymbols
* @noinspection JSUnresolvedFunction
*/
protected function get_captcha_inline_script( $captcha_settings ) {
// IE11 polyfills for native `matches()` and `closest()` methods.
$polyfills = /** @lang JavaScript */
'if (!Element.prototype.matches) {
Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector;
}
if (!Element.prototype.closest) {
Element.prototype.closest = function (s) {
var el = this;
do {
if (Element.prototype.matches.call(el, s)) { return el; }
el = el.parentElement || el.parentNode;
} while (el !== null && el.nodeType === 1);
return null;
};
}
';
// Native equivalent for jQuery's `trigger()` method.
$dispatch = /** @lang JavaScript */
'var wpformsDispatchEvent = function (el, ev, custom) {
var e = document.createEvent(custom ? "CustomEvent" : "HTMLEvents");
custom ? e.initCustomEvent(ev, true, true, false) : e.initEvent(ev, true, true);
el.dispatchEvent(e);
};
';
// Update container class after changing Turnstile type.
$turnstile_update_class = /** @lang JavaScript */
'var turnstileUpdateContainer = function (el) {
let form = el.closest( "form" ),
iframeWrapperHeight = el.offsetHeight;
parseInt(iframeWrapperHeight) === 0 ?
form.querySelector(".wpforms-is-turnstile").classList.add( "wpforms-is-turnstile-invisible" ) :
form.querySelector(".wpforms-is-turnstile").classList.remove( "wpforms-is-turnstile-invisible" );
};
';
// Captcha callback, used by hCaptcha and checkbox reCaptcha v2.
$callback = /** @lang JavaScript */
'var wpformsRecaptchaCallback = function (el) {
var hdn = el.parentNode.querySelector(".wpforms-recaptcha-hidden");
var err = el.parentNode.querySelector("#g-recaptcha-hidden-error");
hdn.value = "1";
wpformsDispatchEvent(hdn, "change", false);
hdn.classList.remove("wpforms-error");
err && hdn.parentNode.removeChild(err);
};
';
$sync = /** @lang JavaScript */
'const wpformsRecaptchaSync = ( func ) => {
return function() {
const context = this;
const args = arguments;
// Sync with jQuery ready event.
jQuery( document ).ready( function() {
func.apply( context, args );
} );
}
};
';
if ( $captcha_settings['provider'] === 'hcaptcha' ) {
$data = $dispatch;
$data .= $callback;
$data .= /** @lang JavaScript */
'var wpformsRecaptchaLoad = function () {
Array.prototype.forEach.call(document.querySelectorAll(".h-captcha"), function (el) {
var captchaID = hcaptcha.render(el, {
callback: function () {
wpformsRecaptchaCallback(el);
}
});
el.setAttribute("data-recaptcha-id", captchaID);
});
wpformsDispatchEvent(document, "wpformsRecaptchaLoaded", true);
};
';
return $data;
}
if ( $captcha_settings['provider'] === 'turnstile' ) {
$data = $dispatch;
$data .= $callback;
$data .= $turnstile_update_class;
$data .= /** @lang JavaScript */
'var wpformsRecaptchaLoad = function () {
Array.prototype.forEach.call(document.querySelectorAll(".wpforms-turnstile"), function (el) {
let form = el.closest( "form" ),
formId = form.dataset.formid,
captchaID = turnstile.render(el, {
theme: "' . $captcha_settings['theme'] . '",
callback: function () {
turnstileUpdateContainer(el);
wpformsRecaptchaCallback(el);
},
"timeout-callback": function() {
turnstileUpdateContainer(el);
}
});
el.setAttribute("data-recaptcha-id", captchaID);
});
wpformsDispatchEvent( document, "wpformsRecaptchaLoaded", true );
};
';
return $data;
}
if ( $captcha_settings['recaptcha_type'] === 'v3' ) {
$data = $dispatch;
$data .= /** @lang JavaScript */
'var wpformsRecaptchaV3Execute = function ( callback ) {
grecaptcha.execute( "' . $captcha_settings['site_key'] . '", { action: "wpforms" } ).then( function ( token ) {
Array.prototype.forEach.call( document.getElementsByName( "wpforms[recaptcha]" ), function ( el ) {
el.value = token;
} );
if ( typeof callback === "function" ) {
return callback();
}
} );
}
grecaptcha.ready( function () {
wpformsDispatchEvent( document, "wpformsRecaptchaLoaded", true );
} );
';
} elseif ( $captcha_settings['recaptcha_type'] === 'invisible' ) {
$data = $polyfills;
$data .= $dispatch;
$data .= $sync;
$data .= /** @lang JavaScript */
'var wpformsRecaptchaLoad = wpformsRecaptchaSync( function () {
Array.prototype.forEach.call(document.querySelectorAll(".g-recaptcha"), function (el) {
try {
var recaptchaID = grecaptcha.render(el, {
"callback": function () {
wpformsRecaptchaCallback(el);
},
"error-callback": function () {
wpformsRecaptchaErrorCallback(el);
}
}, true);
el.closest("form").querySelector("button[type=submit]").recaptchaID = recaptchaID;
} catch (error) {}
});
wpformsDispatchEvent(document, "wpformsRecaptchaLoaded", true);
} );
var wpformsRecaptchaCallback = function (el) {
var $form = el.closest("form");
if (typeof wpforms.formSubmit === "function") {
wpforms.formSubmit($form);
} else {
$form.querySelector("button[type=submit]").recaptchaID = false;
$form.submit();
}
};
var wpformsRecaptchaErrorCallback = function (el) {
var $form = el.closest("form");
$form.querySelector("button[type=submit]").dataset.captchaInvalid = true;
};
';
} else {
$data = $dispatch;
$data .= $callback;
$data .= /** @lang JavaScript */
'var wpformsRecaptchaLoad = function () {
Array.prototype.forEach.call(document.querySelectorAll(".g-recaptcha"), function (el) {
try {
var recaptchaID = grecaptcha.render(el, {
callback: function () {
wpformsRecaptchaCallback(el);
}
});
el.setAttribute("data-recaptcha-id", recaptchaID);
} catch (error) {}
});
wpformsDispatchEvent(document, "wpformsRecaptchaLoaded", true);
};
';
}
return $data;
}
/**
* Cloudflare Turnstile captcha requires defer attribute.
*
* @since 1.8.1
*
* @param string $tag HTML for the script tag.
* @param string $handle Handle of script.
* @param string $src Src of script.
*
* @return string
*/
public function set_defer_attribute( $tag, $handle, $src ) {
$captcha_settings = wpforms_get_captcha_settings();
if ( $captcha_settings['provider'] !== 'turnstile' ) {
return $tag;
}
if ( $handle !== 'wpforms-recaptcha' ) {
return $tag;
}
return str_replace( ' src', ' defer src', $tag );
}
}
@@ -0,0 +1,406 @@
<?php
namespace WPForms\Frontend;
/**
* Classic render engine class.
*
* @since 1.8.1
*/
class Classic {
/**
* Current form data.
*
* @since 1.8.1
*
* @var array
*/
public $form_data;
/**
* Hooks.
*
* @since 1.8.1
*/
public function hooks() {
}
/**
* Open form container.
*
* @since 1.8.1
*
* @param string|array $classes Form container classes.
* @param array $form_data Form data.
*
* @noinspection PhpUnusedParameterInspection
*/
public function form_container_open( $classes, $form_data ) {
printf(
'<div class="wpforms-container %s" id="wpforms-%d">',
wpforms_sanitize_classes( $classes, true ),
absint( $form_data['id'] )
);
}
/**
* Close form container.
*
* @since 1.8.1
*/
public function form_container_close() {
echo '</div> <!-- .wpforms-container -->';
}
/**
* The form has no fields.
*
* @since 1.8.1
*/
public function form_is_empty() {
echo '<!-- WPForms: no fields, form hidden -->';
}
/**
* Noscript message.
*
* @since 1.8.1
*
* @param string $msg Noscript message.
*/
public function noscript( $msg ) {
printf(
'<noscript class="wpforms-error-noscript">%s</noscript>',
esc_html( $msg )
);
}
/**
* Display form error.
*
* @since 1.8.1
*
* @param string $type Error type.
* @param string $error Error text.
*/
public function form_error( $type, $error ) {
switch ( $type ) {
case 'header':
case 'footer':
// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<div class="wpforms-error-container">' . wpautop( wpforms_sanitize_error( $error ) ) . '</div>';
// phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
break;
case 'header_styled':
case 'footer_styled':
// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<div class="wpforms-error-container wpforms-error-styled-container"><div class="wpforms-error">' . wpautop( wpforms_sanitize_error( $error ) ) . '</div></div>';
// phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
break;
case 'recaptcha':
echo '<label id="wpforms-field_recaptcha-error" class="wpforms-error">' . wpforms_sanitize_error( $error ) . '</label>';
break;
}
}
/**
* Open fields area container.
*
* @since 1.8.1
*/
public function fields_area_open() {
echo '<div class="wpforms-field-container">';
}
/**
* Close fields area container.
*
* @since 1.8.1
*/
public function fields_area_close() {
echo '</div><!-- .wpforms-field-container -->';
}
/**
* Open container for each field.
*
* @since 1.8.1
*
* @param array $field Field data and settings.
* @param array $form_data Form data and settings.
*
* @noinspection HtmlUnknownAttribute
* @noinspection PhpUnusedParameterInspection
*/
public function field_container_open( $field, $form_data ) {
$container = $field['properties']['container'];
$container['data']['field-id'] = wpforms_validate_field_id( $field['id'] );
printf(
'<div %s>',
wpforms_html_attributes( $container['id'], $container['class'], $container['data'], $container['attr'] )
);
}
/**
* Close container markup for each field.
*
* @since 1.8.1
*
* @param array $field Field data and settings.
* @param array $form_data Form data and settings.
*
* @noinspection PhpUnusedParameterInspection
*/
public function field_container_close( $field, $form_data ) {
echo '</div>';
}
/**
* Open fieldset.
*
* @since 1.8.1
*
* @param array $field Field data and settings.
* @param array $form_data Form data and settings.
*
* @noinspection PhpUnusedParameterInspection
*/
public function field_fieldset_open( $field, $form_data ) {
}
/**
* Close fieldset.
*
* @since 1.8.1
*
* @param array $field Field data and settings.
* @param array $form_data Form data and settings.
*
* @noinspection PhpUnusedParameterInspection
*/
public function field_fieldset_close( $field, $form_data ) {
}
/**
* Field label.
*
* @since 1.8.1
*
* @param array $field Field data and settings.
* @param array $form_data Form data and settings.
*
* @noinspection HtmlUnknownAttribute
* @noinspection PhpUnusedParameterInspection
*/
public function field_label( $field, $form_data ) {
if ( empty( $field['properties']['label'] ) ) {
return;
}
$label = $field['properties']['label'];
$required = $label['required'] ? wpforms_get_field_required_label() : '';
printf(
'<label %s>%s%s</label>',
wpforms_html_attributes( $label['id'], $label['class'], $label['data'], $label['attr'] ),
esc_html( $label['value'] ),
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$required
);
}
/**
* Field error.
*
* @since 1.8.1
*
* @param array $field Field data and settings.
* @param array $form_data Form data and settings.
*
* @noinspection HtmlUnknownAttribute
* @noinspection PhpUnusedParameterInspection
*/
public function field_error( $field, $form_data ) {
if ( empty( $field['properties']['error'] ) ) {
return;
}
$error = $field['properties']['error'];
printf(
'<label %s>%s</label>',
wpforms_html_attributes( $error['id'], $error['class'], $error['data'], $error['attr'] ),
esc_html( $error['value'] )
);
}
/**
* Field description.
*
* @since 1.8.1
*
* @param array $field Field data and settings.
* @param array $form_data Form data and settings.
*
* @noinspection HtmlUnknownAttribute
* @noinspection PhpUnusedParameterInspection
*/
public function field_description( $field, $form_data ) {
if ( empty( $field['properties']['description'] ) ) {
return;
}
$description = $field['properties']['description'];
printf(
'<div %s>%s</div>',
wpforms_html_attributes( $description['id'], $description['class'], $description['data'], $description['attr'] ),
do_shortcode( $description['value'] )
);
}
/**
* Confirmation.
*
* @since 1.8.1
*
* @param string $confirmation_message Confirmation message.
* @param string $class CSS class.
* @param array $form_data Form data and settings.
*/
public function confirmation( $confirmation_message, $class, $form_data ) {
$form_id = isset( $form_data['id'] ) ? $form_data['id'] : 0;
printf(
'<div class="%s" id="wpforms-confirmation-%d">%s</div>',
wpforms_sanitize_classes( $class ),
absint( $form_id ),
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$confirmation_message
);
}
/**
* Form head container. Form title and description.
*
* @since 1.8.1
*
* @param bool $title Whether to display form title.
* @param bool $description Whether to display form description.
* @param array $form_data Form data.
*/
public function form_head_container( $title, $description, $form_data ) {
$settings = $form_data['settings'];
echo '<div class="wpforms-head-container">';
if ( $title === true && ! empty( $settings['form_title'] ) ) {
echo '<div class="wpforms-title">' . esc_html( $settings['form_title'] ) . '</div>';
}
if ( $description === true && ! empty( $settings['form_desc'] ) ) {
echo '<div class="wpforms-description">';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo wpforms_process_smart_tags( $settings['form_desc'], $form_data, [], '', 'form-description' );
echo '</div>';
}
echo '</div>';
}
/**
* Open submit container.
*
* @since 1.8.1
*
* @param int $pages Information for multi-page forms.
* @param array $form_data Form data and settings.
*
* @noinspection PhpUnusedParameterInspection
* @noinspection HtmlUnknownAttribute
*/
public function submit_container_open( $pages, $form_data ) {
printf( '<div class="wpforms-submit-container" %s>', $pages ? 'style="display:none;"' : '' );
}
/**
* Submit button.
*
* @since 1.8.1
*
* @param int $form_id Form ID.
* @param string $submit Submit text.
* @param array $classes CSS classes.
* @param array $data_attrs Data attributes.
* @param array $attrs Other attributes.
* @param array $form_data Form data and settings.
*
* @noinspection PhpUnusedParameterInspection
*/
public function submit_button( $form_id, $submit, $classes, $data_attrs, $attrs, $form_data ) {
printf(
'<button type="submit" name="wpforms[submit]" %s>%s</button>',
wpforms_html_attributes(
sprintf( 'wpforms-submit-%d', absint( $form_id ) ),
$classes,
$data_attrs,
$attrs
),
esc_html( $submit )
);
}
/**
* Submit button.
*
* @since 1.8.1
*
* @param string $src Spinner image src attribute.
* @param array $form_data Form data and settings.
*
* @noinspection PhpUnusedParameterInspection
*/
public function submit_spinner( $src, $form_data ) {
printf(
'<img src="%s" class="wpforms-submit-spinner" style="display: none;" width="26" height="26" alt="%s">',
esc_url( $src ),
esc_attr__( 'Loading', 'wpforms-lite' )
);
}
/**
* Open submit container.
*
* @since 1.8.1
*
* @param array $form_data Form data and settings.
*
* @noinspection PhpUnusedParameterInspection
*/
public function submit_container_close( $form_data ) {
echo '</div>';
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,322 @@
<?php
namespace WPForms\Frontend;
/**
* Modern render engine class.
*
* @since 1.8.1
*/
class Modern extends Classic {
/**
* Hooks.
*
* @since 1.8.1
*/
public function hooks() {
add_filter( 'wpforms_field_properties', [ $this, 'field_properties' ], 10, 3 );
add_filter( 'wpforms_get_field_required_label', [ $this, 'get_field_required_label' ], 10 );
add_filter( 'wpforms_frontend_strings', [ $this, 'frontend_strings' ], 10 );
}
/**
* Open form container.
*
* @since 1.8.1
*
* @param string $classes Form container classes.
* @param array $form_data Form data.
*/
public function form_container_open( $classes, $form_data ) {
$classes[] = 'wpforms-render-modern';
parent::form_container_open( $classes, $form_data );
}
/**
* Noscript message.
*
* @since 1.8.1
*
* @param string $msg Noscript message.
*/
public function noscript( $msg ) {
printf(
'<noscript class="wpforms-error-noscript">%1$s</noscript><div id="wpforms-error-noscript" style="display: none;">%1$s</div>',
esc_html( $msg )
);
}
/**
* Display form error.
*
* @since 1.8.1
*
* @param string $type Error type.
* @param string $error Error text.
*/
public function form_error( $type, $error ) {
switch ( $type ) {
case 'header':
case 'footer':
printf(
'<div id="wpforms-%1$s-%2$s-error" class="wpforms-error-container" role="alert">
<span class="wpforms-hidden" aria-hidden="false">%3$s</span>%4$s
</div>',
esc_attr( $this->form_data['id'] ?? 0 ),
esc_attr( $type ),
esc_html__( 'Form error message', 'wpforms-lite' ),
wpautop( wpforms_sanitize_error( $error ) ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
);
break;
case 'header_styled':
case 'footer_styled':
printf(
'<div id="wpforms-%1$s-%2$s-error" class="wpforms-error-container wpforms-error-styled-container" role="alert">
<div class="wpforms-error"><span class="wpforms-hidden" aria-hidden="false">%3$s</span>%4$s</div>
</div>',
esc_attr( $this->form_data['id'] ),
esc_attr( $type ),
esc_html__( 'Form error message', 'wpforms-lite' ),
wpautop( wpforms_sanitize_error( $error ) ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
);
break;
case 'header_styled':
case 'footer_styled':
printf(
'<div id="wpforms-%1$s-%2$s-error" class="wpforms-error-container wpforms-error-styled-container" role="alert">
<div class="wpforms-error"><span class="wpforms-hidden" aria-hidden="false">%3$s</span>%4$s</div>
</div>',
esc_attr( $this->form_data['id'] ),
esc_attr( $type ),
esc_html__( 'Form error message', 'wpforms-lite' ),
wpautop( wpforms_sanitize_error( $error ) ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
);
break;
case 'header_styled':
case 'footer_styled':
printf(
'<div id="wpforms-%1$s-%2$s-error" class="wpforms-error-container wpforms-error-styled-container" role="alert">
<div class="wpforms-error"><span class="wpforms-hidden" aria-hidden="false">%3$s</span>%4$s</div>
</div>',
esc_attr( $this->form_data['id'] ),
esc_attr( $type ),
esc_html__( 'Form error message', 'wpforms-lite' ),
wpautop( wpforms_sanitize_error( $error ) ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
);
break;
case 'recaptcha':
printf(
'<em id="wpforms-field_recaptcha-error" class="wpforms-error" role="alert">
<span class="wpforms-hidden" aria-hidden="false">%1$s</span>%2$s
</em>',
esc_attr__( 'Recaptcha error message', 'wpforms-lite' ),
wpforms_sanitize_error( $error ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
);
break;
}
}
/**
* Field label markup.
*
* @since 1.8.1
*
* @param array $field Field data and settings.
* @param array $form_data Form data and settings.
*/
public function field_label( $field, $form_data ) {
// Do not need to output label if the field requires fieldset.
if ( $this->is_field_requires_fieldset( $field ) ) {
return;
}
if ( ! empty( $field['label_hide'] ) ) {
$field['properties']['label']['attr']['aria-hidden'] = 'false';
}
parent::field_label( $field, $form_data );
}
/**
* Open fieldset markup.
*
* @since 1.8.1
*
* @param array $field Field data and settings.
* @param array $form_data Form data and settings.
*/
public function field_fieldset_open( $field, $form_data ) {
if ( ! $this->is_field_requires_fieldset( $field ) ) {
return;
}
if ( ! empty( $field['label_hide'] ) ) {
$field['properties']['label']['attr']['aria-hidden'] = 'false';
}
$label = $field['properties']['label'];
$required = $label['required'] ? wpforms_get_field_required_label() : '';
unset( $label['attr']['for'] );
printf(
'<fieldset><legend %s>%s%s</legend>',
wpforms_html_attributes( $label['id'], $label['class'], $label['data'], $label['attr'] ),
esc_html( $label['value'] ),
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$required
);
}
/**
* Close fieldset markup.
*
* @since 1.8.1
*
* @param array $field Field data and settings.
* @param array $form_data Form data and settings.
*/
public function field_fieldset_close( $field, $form_data ) {
if ( ! $this->is_field_requires_fieldset( $field ) ) {
return;
}
echo '</fieldset>';
}
/**
* Whether the field requires fieldset markup.
*
* @since 1.8.1
*
* @param array $field Field data and settings.
*/
private function is_field_requires_fieldset( $field ) {
if ( empty( $field['type'] ) ) {
return false;
}
/**
* Determine whether the field is requires fieldset+legend markup on the frontend.
*
* @since 1.8.1
*
* @param bool $requires_fieldset True if requires. Defaults to false.
* @param array $field Field data.
*/
return (bool) apply_filters( "wpforms_frontend_modern_is_field_requires_fieldset_{$field['type']}", false, $field );
}
/**
* Field error.
*
* @since 1.8.1
*
* @param array $field Field data and settings.
* @param array $form_data Form data and settings.
*
* @noinspection HtmlUnknownAttribute
*/
public function field_error( $field, $form_data ) {
if ( empty( $field['properties']['error'] ) ) {
return;
}
$error = $field['properties']['error'];
printf(
'<em %1$s>%2$s</em>',
wpforms_html_attributes( $error['id'], $error['class'], $error['data'], $error['attr'] ),
esc_html( $error['value'] )
);
}
/**
* Define additional field properties.
*
* @since 1.8.1
*
* @param array $properties Field properties.
* @param array $field Field settings.
* @param array $form_data Form data and settings.
*
* @return array
*/
public function field_properties( $properties, $field, $form_data ) {
$field_id = "wpforms-{$form_data['id']}-field_{$field['id']}";
$desc_id = "{$field_id}-description";
// Add `id` to field description.
$properties['description']['id'] = $desc_id;
// Add attributes to error message.
$properties['error']['attr']['role'] = 'alert';
$properties['error']['attr']['aria-label'] = esc_html__( 'Error message', 'wpforms-lite' );
$properties['error']['attr']['id'] = $properties['error']['attr']['for'] . '-error';
$properties['error']['attr']['for'] = '';
foreach ( $properties['inputs'] as $input => $input_data ) {
// Add `aria-errormessage` to inputs (except hidden according to W3C requirements).
if ( ! empty( $input_data['id'] ) && $field['type'] !== 'hidden' ) {
$properties['inputs'][ $input ]['attr']['aria-errormessage'] = "{$input_data['id']}-error";
}
// Add `aria-describedby` to inputs.
if ( ! empty( $field['description'] ) ) {
$properties['inputs'][ $input ]['attr']['aria-describedby'] = $desc_id;
}
}
return $properties;
}
/**
* Required label (asterisk) markup.
*
* @since 1.8.1
*
* @param string $label_html Required label markup.
*
* @return string
*/
public function get_field_required_label( $label_html ) {
return ' <span class="wpforms-required-label" aria-hidden="true">*</span>';
}
/**
* Modify javascript `wpforms_settings` properties on the front end.
*
* @since 1.8.1
*
* @param array $strings Array `wpforms_settings` properties.
*
* @return array
*/
public function frontend_strings( $strings ) {
$strings['isModernMarkupEnabled'] = wpforms_get_render_engine() === 'modern';
$strings['formErrorMessagePrefix'] = esc_html__( 'Form error message', 'wpforms-lite' );
$strings['errorMessagePrefix'] = esc_html__( 'Error message', 'wpforms-lite' );
$strings['submitBtnDisabled'] = esc_html__( 'Submit button is disabled during form submission.', 'wpforms-lite' );
return $strings;
}
}