a(), text → $this->t(), * trusted rich HTML → $this->h(), CSS values → $this->css(). * - Input names use the WP submission scheme: $this->name_attr($field[, $suffix]) → fields[]. * - Never print settings/debug data into the page and never print the reference builder branding. */ require_once __DIR__ . '/class-js.php'; require_once __DIR__ . '/class-schema.php'; require_once __DIR__ . '/class-fp-settings.php'; foreach ( glob( __DIR__ . '/fields/trait-*.php' ) as $wpvfb_fp_trait_file ) { require_once $wpvfb_fp_trait_file; } unset( $wpvfb_fp_trait_file ); class WPVFB_FP_Renderer { use WPVFB_FP_Fields_Text; use WPVFB_FP_Fields_Misc; use WPVFB_FP_Fields_Content; use WPVFB_FP_Fields_Inputs; use WPVFB_FP_Fields_Choice; use WPVFB_FP_Fields_DateTime; use WPVFB_FP_Fields_Choice2; use WPVFB_FP_Assembly; /** Resolved GlobalSettings (see WPVFB_FP_Settings). */ public $gs = array(); /** storefront-shaped form: id, name, fields, settings. */ public $form = array(); public $upload_limit_mb = 3; /** True once a rich text (Quill) paragraph field was rendered. */ public $needs_rich_text = false; const COUNTRY_LIST = array( 'Afghanistan', 'Albania', 'Algeria', 'Andorra', 'Angola', 'Antigua and Barbuda', 'Argentina', 'Armenia', 'Australia', 'Austria', 'Azerbaijan', 'Bahamas', 'Bahrain', 'Bangladesh', 'Barbados', 'Belarus', 'Belgium', 'Belize', 'Benin', 'Bhutan', 'Bolivia', 'Bosnia and Herzegovina', 'Botswana', 'Brazil', 'Brunei', 'Bulgaria', 'Burkina Faso', 'Burundi', 'Cabo Verde', 'Cambodia', 'Cameroon', 'Canada', 'Central African Republic', 'Chad', 'Chile', 'China', 'Colombia', 'Comoros', 'Congo (Congo-Brazzaville)', 'Costa Rica', 'Croatia', 'Cuba', 'Cyprus', 'Czechia (Czech Republic)', 'Democratic Republic of the Congo', 'Denmark', 'Djibouti', 'Dominica', 'Dominican Republic', 'Ecuador', 'Egypt', 'El Salvador', 'Equatorial Guinea', 'Eritrea', 'Estonia', 'Eswatini (Swaziland)', 'Ethiopia', 'Fiji', 'Finland', 'France', 'Gabon', 'Gambia', 'Georgia', 'Germany', 'Ghana', 'Greece', 'Grenada', 'Guatemala', 'Guinea', 'Guinea-Bissau', 'Guyana', 'Haiti', 'Honduras', 'Hungary', 'Iceland', 'India', 'Indonesia', 'Iran', 'Iraq', 'Ireland', 'Israel', 'Italy', 'Ivory Coast', 'Jamaica', 'Japan', 'Jordan', 'Kazakhstan', 'Kenya', 'Kiribati', 'Kuwait', 'Kyrgyzstan', 'Laos', 'Latvia', 'Lebanon', 'Lesotho', 'Liberia', 'Libya', 'Liechtenstein', 'Lithuania', 'Luxembourg', 'Madagascar', 'Malawi', 'Malaysia', 'Maldives', 'Mali', 'Malta', 'Marshall Islands', 'Mauritania', 'Mauritius', 'Mexico', 'Micronesia', 'Moldova', 'Monaco', 'Mongolia', 'Montenegro', 'Morocco', 'Mozambique', 'Myanmar (Burma)', 'Namibia', 'Nauru', 'Nepal', 'Netherlands', 'New Zealand', 'Nicaragua', 'Niger', 'Nigeria', 'North Korea', 'North Macedonia', 'Norway', 'Oman', 'Pakistan', 'Palau', 'Palestine', 'Panama', 'Papua New Guinea', 'Paraguay', 'Peru', 'Philippines', 'Poland', 'Portugal', 'Qatar', 'Romania', 'Russia', 'Rwanda', 'Saint Kitts and Nevis', 'Saint Lucia', 'Saint Vincent and the Grenadines', 'Samoa', 'San Marino', 'Sao Tome and Principe', 'Saudi Arabia', 'Senegal', 'Serbia', 'Seychelles', 'Sierra Leone', 'Singapore', 'Slovakia', 'Slovenia', 'Solomon Islands', 'Somalia', 'South Africa', 'South Korea', 'South Sudan', 'Spain', 'Sri Lanka', 'Sudan', 'Suriname', 'Sweden', 'Switzerland', 'Syria', 'Taiwan', 'Tajikistan', 'Tanzania', 'Thailand', 'Timor-Leste', 'Togo', 'Tonga', 'Trinidad and Tobago', 'Tunisia', 'Turkey', 'Turkmenistan', 'Tuvalu', 'Uganda', 'Ukraine', 'United Arab Emirates', 'United Kingdom', 'United States', 'Uruguay', 'Uzbekistan', 'Vanuatu', 'Vatican City', 'Venezuela', 'Vietnam', 'Yemen', 'Zambia', 'Zimbabwe', ); /** * @param array $form storefront-shaped form (id, name, fields, settings) — run WPVFB_Schema::to_render_form() first. */ public function __construct( $form, $opts = array() ) { $this->form = is_array( $form ) ? $form : array(); $this->gs = WPVFB_FP_Settings::resolve( isset( $this->form['settings'] ) ? $this->form['settings'] : array() ); $this->upload_limit_mb = isset( $opts['upload_limit_mb'] ) ? $opts['upload_limit_mb'] : 3; } // ------------------------------------------------------------------ // Escaping (output must stay byte-compatible with the TS source for // ordinary values; the parity comparator decodes entities). // ------------------------------------------------------------------ /** Attribute value. */ public function a( $v ) { return htmlspecialchars( WPVFB_JS::str( $v ), ENT_QUOTES, 'UTF-8', false ); } /** Text node. */ public function t( $v ) { return htmlspecialchars( WPVFB_JS::str( $v ), ENT_NOQUOTES, 'UTF-8', false ); } /** Trusted rich HTML authored in the builder (static content, HTML block). */ public function h( $v ) { $html = WPVFB_JS::is_nullish( $v ) ? '' : WPVFB_JS::str( $v ); if ( function_exists( 'wp_kses' ) && function_exists( 'wp_kses_allowed_html' ) ) { $allowed = wp_kses_allowed_html( 'post' ); $allowed['iframe'] = array( 'src' => true, 'width' => true, 'height' => true, 'style' => true, 'title' => true, 'allow' => true, 'allowfullscreen' => true, 'loading' => true, 'referrerpolicy' => true, 'frameborder' => true ); foreach ( array( 'span', 'div', 'p', 'a', 'strong', 'em', 'i', 'b', 'u' ) as $tag ) { if ( isset( $allowed[ $tag ] ) ) { $allowed[ $tag ]['style'] = true; $allowed[ $tag ]['class'] = true; } } // Inline SVG icons in builder/template HTML (e.g. newsletter-style-24's envelope badge). wp_kses_post drops // them, the canvas and the reference builder render them. Presentation attributes only: no scripts, handlers or href. $svg_attrs = array_fill_keys( array( 'class', 'style', 'width', 'height', 'viewbox', 'fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', 'stroke-dasharray', 'stroke-opacity', 'fill-opacity', 'fill-rule', 'clip-rule', 'opacity', 'transform', 'xmlns', 'aria-hidden', 'role', 'focusable', 'preserveaspectratio', 'id' ), true ); $shape_attrs = array_fill_keys( array( 'd', 'points', 'cx', 'cy', 'r', 'rx', 'ry', 'x', 'y', 'x1', 'y1', 'x2', 'y2', 'offset', 'stop-color', 'stop-opacity', 'gradientunits', 'gradienttransform' ), true ); foreach ( array( 'svg', 'g', 'path', 'polyline', 'polygon', 'circle', 'ellipse', 'rect', 'line', 'defs', 'lineargradient', 'radialgradient', 'stop', 'title' ) as $tag ) { $allowed[ $tag ] = $svg_attrs + $shape_attrs; } // Gradient text (newsletter-style-28 heading): keep background-clip / text-fill-color, which WordPress' // inline-style allowlist drops (the text then disappears behind its gradient background). $extra_css = function ( $props ) { return array_merge( $props, array( 'background-clip', '-webkit-background-clip', '-webkit-text-fill-color' ) ); }; add_filter( 'safe_style_css', $extra_css ); $out = wp_kses( $html, $allowed ); remove_filter( 'safe_style_css', $extra_css ); return $out; } return $html; } /** Value placed inside a stylesheet or a CSS declaration: cannot close the declaration/rule/element. */ public function css( $v ) { return str_replace( array( '<', '>', '{', '}', ';' ), '', WPVFB_JS::str( $v ) ); } /** name="" for submitted inputs: fields[]. */ public function name_attr( $field, $suffix = '' ) { $id = preg_replace( '/[^A-Za-z0-9_\-]/', '', WPVFB_JS::str( WPVFB_JS::get( $field, 'id' ) ) ); return 'fields[' . $id . ']' . $suffix; } // ------------------------------------------------------------------ // Shared helpers ported from route.ts // ------------------------------------------------------------------ /** * getContrastCheckmarkSvg: the check icon colour for a checkbox background. The icon itself is defined in * assets/css/wpvfb-fp-extensions.css; this returns the data-wpvfb-check="dark|light" attribute that selects it. */ public function contrast_checkmark_attr( $hex_color ) { $c = WPVFB_JS::truthy( $hex_color ) ? str_replace( '#', '', WPVFB_JS::str( $hex_color ) ) : '2563EB'; if ( 3 === strlen( $c ) ) { $c = $c[0] . $c[0] . $c[1] . $c[1] . $c[2] . $c[2]; } $hex = function ( $s ) { return preg_match( '/^[0-9a-fA-F]+/', $s, $m ) ? hexdec( $m[0] ) : 0; }; $yiq = ( $hex( substr( $c, 0, 2 ) ) * 299 + $hex( substr( $c, 2, 2 ) ) * 587 + $hex( substr( $c, 4, 2 ) ) * 114 ) / 1000; return ' data-wpvfb-check="' . ( $yiq >= 130 ? 'dark' : 'light' ) . '"'; } /** renderRequiredAsterisk */ public function required_asterisk( $field, $gs = null ) { if ( ! WPVFB_JS::truthy( WPVFB_JS::get( $field, 'required' ) ) ) { return ''; } $color = WPVFB_JS::or_( WPVFB_JS::get( $field, 'requiredAsteriskColor' ), WPVFB_JS::get( $gs, 'requiredAsteriskColor' ), '#ef4444' ); return '*'; } // ------------------------------------------------------------------ // Dispatch — port of GET's renderField closure (route.ts ~8440-8618) // ------------------------------------------------------------------ public function render_field( $field ) { return $this->tag_field_root( $this->render_field_markup( $field ), $field ); } /** The nested switch GET passes to renderGridField. */ public function render_nested_field( $field ) { return $this->tag_field_root( $this->render_nested_field_markup( $field ), $field ); } /** * WP addition (not in the the reference builder output): marks the field's root element with data-wpvfb-field="" * so the storefront runtime (assets/js/wpvfb-fp-runtime.js) can show/hide it for conditional logic. * Every field renders exactly one root element (optionally preceded by whitespace or comments). * The parity comparator strips the attribute. */ public function tag_field_root( $html, $field ) { $id = WPVFB_JS::get( $field, 'id' ); if ( ! is_string( $id ) && ! is_int( $id ) ) { return $html; } $id = (string) $id; if ( '' === $id || ! is_string( $html ) || '' === $html ) { return $html; } $offset = 0; while ( preg_match( '/\s+|/As', $html, $m, 0, $offset ) ) { $offset += strlen( $m[0] ); } if ( ! preg_match( '/<[a-zA-Z][a-zA-Z0-9-]*/A', $html, $m, 0, $offset ) ) { return $html; } $pos = $offset + strlen( $m[0] ); return substr( $html, 0, $pos ) . ' data-wpvfb-field="' . $this->a( $id ) . '"' . substr( $html, $pos ); } protected function render_field_markup( $field ) { $gs = $this->gs; $type = WPVFB_JS::str( WPVFB_JS::get( $field, 'type' ) ); switch ( $type ) { case 'text': case 'password': return $this->render_text_field( $field, $gs ); case 'textarea': return $this->render_textarea_field( $field, $gs ); case 'dropdown': return $this->render_dropdown_field( $field, $gs ); case 'checkbox': return $this->render_checkbox_field( $field, $gs ); case 'radio': return $this->render_radio_field( $field, $gs ); case 'consent': return $this->render_consent_field( $field, $gs ); case 'switch': return $this->render_switch_field( $field, $gs ); case 'grid': if ( WPVFB_JS::truthy( WPVFB_JS::get( $field, 'repeaterEnabled' ) ) ) { return $this->render_extension_field( $field, false ); } return $this->render_grid_field( $field, $gs, array( $this, 'render_nested_field' ) ); case 'number': return $this->render_number_field( $field, $gs ); case 'hidden': return $this->render_hidden_field( $field ); case 'html': return $this->render_html_field( $field, $gs ); case 'section': return $this->render_section_field( $field, $gs ); case 'name': return $this->render_name_field( $field, $gs ); case 'date': return $this->render_date_field( $field, $gs ); case 'time': return $this->render_time_field( $field, $gs ); case 'tel': return $this->render_tel_field( $field, $gs ); case 'address': return $this->render_address_field( $field, $gs ); case 'url': return $this->render_url_field( $field, $gs ); case 'email': return $this->render_email_field( $field, $gs ); case 'captcha': return $this->render_captcha_field( $field, $gs ); case 'image': return $this->render_image_field( $field, $gs ); case 'static-content': return $this->render_static_content_field( $field, $gs ); case 'promo-banner': return $this->render_promo_banner_field( $field, $gs ); case 'trust-badges': return $this->render_trust_badges_field( $field, $gs ); case 'faq-accordion': return $this->render_faq_accordion_field( $field, $gs ); case 'countdown-banner': return $this->render_countdown_banner_field( $field, $gs ); case 'submit_button': case 'submit': return $this->render_submit_button( $field, $gs ); case '': case 'text': return $this->render_text_field( $field, $gs ); default: return $this->render_extension_field( $field, false ); } } protected function render_nested_field_markup( $field ) { $gs = $this->gs; $type = WPVFB_JS::str( WPVFB_JS::get( $field, 'type' ) ); switch ( $type ) { case 'textarea': return $this->render_textarea_field( $field, $gs ); case 'static-content': return $this->render_static_content_field( $field, $gs ); case 'promo-banner': return $this->render_promo_banner_field( $field, $gs ); case 'trust-badges': return $this->render_trust_badges_field( $field, $gs ); case 'faq-accordion': return $this->render_faq_accordion_field( $field, $gs ); case 'countdown-banner': return $this->render_countdown_banner_field( $field, $gs ); case 'image': return $this->render_image_field( $field, $gs ); case 'submit': return $this->render_submit_button( $field, $gs ); case 'name': return $this->render_name_field( $field, $gs ); case 'email': return $this->render_email_field( $field, $gs ); case 'password': return $this->render_text_field( $field, $gs ); case 'tel': return $this->render_tel_field( $field, $gs ); case 'url': return $this->render_url_field( $field, $gs ); case 'number': return $this->render_number_field( $field, $gs ); case 'date': return $this->render_date_field( $field, $gs ); case 'time': return $this->render_time_field( $field, $gs ); case 'address': return $this->render_address_field( $field, $gs ); case 'dropdown': return $this->render_dropdown_field( $field, $gs ); case 'checkbox': return $this->render_checkbox_field( $field, $gs ); case 'radio': return $this->render_radio_field( $field, $gs ); case 'consent': return $this->render_consent_field( $field, $gs ); case 'switch': return $this->render_switch_field( $field, $gs ); case 'html': return $this->render_html_field( $field, $gs ); case 'section': return $this->render_section_field( $field, $gs ); case 'captcha': return $this->render_captcha_field( $field, $gs ); case 'hidden': return $this->render_hidden_field( $field ); case 'grid': if ( WPVFB_JS::truthy( WPVFB_JS::get( $field, 'repeaterEnabled' ) ) ) { return $this->render_extension_field( $field, true ); } return $this->render_grid_field( $field, $gs, array( $this, 'render_field' ) ); case '': case 'text': return $this->render_text_field( $field, $gs ); default: return $this->render_extension_field( $field, true ); } } /** * Field types this plugin does not render itself. Add-ons render them through the "wpvfb_render_field" * filter or by overriding this method in a subclass (see the "wpvfb_fp_renderer_class" filter). * Unknown types render nothing. * * @param array $field Field definition. * @param bool $nested True inside a grid column. * @return string */ protected function render_extension_field( $field, $nested ) { $html = apply_filters( 'wpvfb_render_field', null, $field, $this, $nested ); return is_string( $html ) ? $html : ''; } // ------------------------------------------------------------------ // Stylesheet text collected while rendering. Nothing is printed // inline: WPVFB_Renderer escapes it and hands it to wp_add_inline_style(). // ------------------------------------------------------------------ /** @var string[] */ protected $inline_css = array(); /** Queue stylesheet text for this form. */ public function add_inline_css( $css ) { $css = trim( (string) $css ); if ( '' !== $css ) { $this->inline_css[] = $css; } return ''; } /** @return string Collected stylesheet text. */ public function get_inline_css() { return implode( "\n", array_unique( $this->inline_css ) ); } }