'dropdown', 'phone' => 'tel', 'submit_button' => 'submit' ); private static function has( $arr, $key ) { return is_array( $arr ) && array_key_exists( $key, $arr ) && null !== $arr[ $key ] && '' !== $arr[ $key ]; } private static function to_sides( $value ) { if ( is_array( $value ) ) { return $value; } if ( ! is_numeric( $value ) ) { return null; } $n = $value + 0; return array( 'top' => $n, 'right' => $n, 'bottom' => $n, 'left' => $n ); } // Card shadow presets of the WP-only `formShadow` setting, exactly as the builder canvas prints them (App.jsx form container). private static $form_shadows = array( 'none' => 'none', 'md' => '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)', 'xl' => '0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)', 'default' => '0 4px 6px -1px rgba(15, 23, 42, 0.05), 0 12px 28px -4px rgba(15, 23, 42, 0.10), 0 0 0 1px rgba(15, 23, 42, 0.05)', ); private static function num_or_zero( $v ) { return is_numeric( $v ) ? WPVFB_JS::num( $v + 0 ) : '0'; } public static function to_render_settings( $settings ) { $s = is_array( $settings ) ? $settings : array(); $out = $s; $map = function ( $target, $sources ) use ( $s, &$out ) { foreach ( (array) $sources as $key ) { if ( self::has( $s, $key ) ) { $out[ $target ] = $s[ $key ]; return; } } }; $map( 'backgroundColor', array( 'formBgColor', 'backgroundColor' ) ); $map( 'backgroundImageSize', array( 'backgroundImageSize', 'bgSize' ) ); $map( 'backgroundImagePosition', array( 'backgroundImagePosition', 'bgPosition' ) ); $map( 'wrapperMaxWidth', array( 'formWidth', 'wrapperMaxWidth' ) ); if ( self::has( $s, 'formPadding' ) ) { $out['formPadding'] = self::to_sides( $s['formPadding'] ); } $map( 'formBorderRadius', array( 'formBorderRadius', 'borderRadius' ) ); $map( 'labelTextColor', array( 'labelColor', 'labelTextColor' ) ); $map( 'inputBackgroundColor', array( 'inputBgColor', 'inputBackgroundColor' ) ); $map( 'placeholderTextColor', array( 'placeholderTextColor', 'placeholderColor' ) ); $map( 'submitBackgroundColor', array( 'submitButtonBgColor', 'submitBackgroundColor' ) ); $map( 'submitTextColor', array( 'submitButtonTextColor', 'submitTextColor' ) ); $map( 'primaryColor', array( 'primaryColor', 'submitButtonBgColor' ) ); // The import stores a gradient submit button (buttonGradient) in submitButtonBgColor; the renderer uses // submitBackgroundColor/primaryColor as solid accents (slider, focus) — take the gradient's first colour there. foreach ( array( 'submitBackgroundColor', 'primaryColor' ) as $key ) { if ( isset( $out[ $key ] ) && is_string( $out[ $key ] ) && false !== strpos( $out[ $key ], 'gradient' ) && preg_match( '/#[0-9a-fA-F]{3,8}\b|rgba?\([^)]*\)/', $out[ $key ], $m ) ) { $out[ $key ] = $m[0]; } } // The renderer prints formBoxShadow as CSS. Template import stores the original previewShadow object there; without a // value (and no the reference builder previewShadow) the canvas shows the formShadow preset (default: soft shadow). $fbs = isset( $s['formBoxShadow'] ) ? $s['formBoxShadow'] : null; if ( is_array( $fbs ) && ! WPVFB_JS::is_list( $fbs ) ) { $out['formBoxShadow'] = self::num_or_zero( isset( $fbs['offsetX'] ) ? $fbs['offsetX'] : 0 ) . 'px ' . self::num_or_zero( isset( $fbs['offsetY'] ) ? $fbs['offsetY'] : 0 ) . 'px ' . self::num_or_zero( isset( $fbs['blur'] ) ? $fbs['blur'] : 0 ) . 'px ' . ( ! empty( $fbs['color'] ) && is_string( $fbs['color'] ) ? $fbs['color'] : 'transparent' ); } elseif ( ! self::has( $s, 'formBoxShadow' ) && ! self::has( $s, 'previewShadow' ) ) { $key = isset( $s['formShadow'] ) && is_string( $s['formShadow'] ) && isset( self::$form_shadows[ $s['formShadow'] ] ) ? $s['formShadow'] : 'default'; $out['formBoxShadow'] = self::$form_shadows[ $key ]; } if ( self::has( $s, 'inputBorderWidth' ) || self::has( $s, 'inputBorderRadius' ) ) { $input = isset( $s['inputSettings'] ) && is_array( $s['inputSettings'] ) ? $s['inputSettings'] : array(); if ( self::has( $s, 'inputBorderWidth' ) ) { $input['borderWidth'] = $s['inputBorderWidth']; } if ( self::has( $s, 'inputBorderRadius' ) ) { $input['borderRadius'] = $s['inputBorderRadius']; } $out['inputSettings'] = $input; } $heading_keys = array( 'h1' => 'headingH1Size', 'h2' => 'headingH2Size', 'h3' => 'headingH3Size', 'h4' => 'headingH4Size' ); $headings = isset( $s['headingFontSizes'] ) && is_array( $s['headingFontSizes'] ) ? $s['headingFontSizes'] : array(); $has_heading = false; foreach ( $heading_keys as $h => $key ) { if ( self::has( $s, $key ) ) { $headings[ $h ] = $s[ $key ]; $has_heading = true; } } if ( $has_heading ) { $out['headingFontSizes'] = $headings; } $out['schemaVersion'] = self::VERSION; // Remove arbitrary CSS from older forms: it is never output. unset( $out['customCss'], $out['customCSS'] ); return apply_filters( 'wpvfb_schema_settings', $out, $s ); } private static function normalize_column_key( $key ) { return preg_match( '/^col-(\d+)$/', (string) $key, $m ) ? 'column' . ( (int) $m[1] + 1 ) : $key; } private static function normalize_logic( $field ) { $legacy = isset( $field['conditionalLogic'] ) ? $field['conditionalLogic'] : null; if ( array_key_exists( 'enableConditionalLogic', $field ) || ! is_array( $legacy ) ) { return $field; } $rules = array(); foreach ( ( isset( $legacy['rules'] ) && is_array( $legacy['rules'] ) ? $legacy['rules'] : array() ) as $r ) { $rules[] = array( 'field' => isset( $r['fieldId'] ) ? $r['fieldId'] : ( isset( $r['field'] ) ? $r['field'] : '' ), 'operator' => ! empty( $r['operator'] ) ? $r['operator'] : 'is', 'value' => isset( $r['value'] ) ? $r['value'] : '', ); } $field['enableConditionalLogic'] = ! empty( $legacy['enabled'] ); $field['logicAction'] = ! empty( $legacy['action'] ) ? $legacy['action'] : 'show'; $field['logicMatch'] = ! empty( $legacy['match'] ) ? $legacy['match'] : 'all'; $field['logicConditions'] = $rules; return $field; } private static function blank( $arr, $key ) { return ! is_array( $arr ) || ! array_key_exists( $key, $arr ) || null === $arr[ $key ] || '' === $arr[ $key ]; } /** Copy each old key into its the reference builder key when the the reference builder key is blank. */ private static function fill( $f, $pairs ) { foreach ( $pairs as $to => $from ) { if ( self::blank( $f, $to ) && ! self::blank( $f, $from ) ) { $f[ $to ] = $f[ $from ]; } } return $f; } private static function numeric( $v ) { return ( ( is_int( $v ) || is_float( $v ) ) && is_finite( $v ) ) || ( is_string( $v ) && '' !== trim( $v ) && is_numeric( trim( $v ) ) ); } private static function is_assoc( $v ) { return is_array( $v ) && ! WPVFB_JS::is_list( $v ); } // Trust badge ids/icons of the WP settings panel → the badge types the renderer has icons for (same as the canvas). private static $badge_types = array( 'ssl' => 'ssl', 'secure' => 'ssl', 'shield' => 'ssl', 'shipping' => 'shipping', 'truck' => 'shipping', 'support' => 'support', 'headphone' => 'support', 'guarantee' => 'guarantee', 'badge' => 'guarantee' ); private static $legacy_badge_labels = array( 'secure' => '256-Bit SSL', 'shipping' => 'Fast Delivery', 'support' => '24/7 Support', 'guarantee' => '30-Day Guarantee' ); private static function opt( $f, $key, $default ) { return self::blank( $f, $key ) ? $default : $f[ $key ]; } /** Older WP builder field shapes → the keys the the reference builder renderer reads. Existing the reference builder keys always win. */ private static function normalize_legacy_shape( $f, $keep_name = false ) { // The canvas shows label (falling back to name); the renderer prints name. Layouts that show name as the // title (add-ons, see "wpvfb_schema_keep_field_name") keep it. if ( ! self::blank( $f, 'label' ) && is_scalar( $f['label'] ) && ! $keep_name ) { $f['name'] = $f['label']; } $type = isset( $f['type'] ) ? $f['type'] : ''; $has_sub_object = isset( $f['subfields'] ) && is_array( $f['subfields'] ) && ! WPVFB_JS::is_list( $f['subfields'] ); if ( 'name' === $type && ! $has_sub_object ) { $f['subfields'] = array( 'prefix' => array( 'isVisible' => ! empty( $f['showPrefix'] ) ), // Same labels/placeholders as the canvas (FormField.jsx case 'name'). 'first' => array( 'isVisible' => true, 'label' => in_array( self::opt( $f, 'firstNameLabel', 'First Name' ), array( 'First Name' ), true ) ? 'First' : $f['firstNameLabel'], 'placeholder' => self::opt( $f, 'firstNamePlaceholder', 'First Name' ) ), 'middle' => array( 'isVisible' => ! empty( $f['showMiddle'] ), 'label' => self::opt( $f, 'middleNameLabel', 'Middle' ), 'placeholder' => self::opt( $f, 'middleNamePlaceholder', 'M.I.' ) ), 'last' => array( 'isVisible' => true, 'label' => in_array( self::opt( $f, 'lastNameLabel', 'Last Name' ), array( 'Last Name' ), true ) ? 'Last' : $f['lastNameLabel'], 'placeholder' => self::opt( $f, 'lastNamePlaceholder', 'Last Name' ) ), 'suffix' => array( 'isVisible' => ! empty( $f['showSuffix'] ), 'placeholder' => 'Jr.' ), ); } if ( 'address' === $type && ! $has_sub_object ) { // Same labels/placeholders as the canvas (FormField.jsx case 'address'). $f['subfields'] = array( 'street' => array( 'isVisible' => true, 'label' => self::opt( $f, 'streetLabel', 'Street Address' ), 'placeholder' => self::opt( $f, 'streetPlaceholder', '123 Main St' ) ), 'address2' => array( 'isVisible' => ! isset( $f['showAddress2'] ) || false !== $f['showAddress2'], 'label' => self::opt( $f, 'street2Label', 'Address Line 2' ), 'placeholder' => self::opt( $f, 'street2Placeholder', 'Apartment, suite, unit, etc.' ) ), 'city' => array( 'isVisible' => true, 'label' => self::opt( $f, 'cityLabel', 'City' ), 'placeholder' => 'City' ), 'state' => array( 'isVisible' => true, 'label' => self::opt( $f, 'stateLabel', 'State / Province' ), 'placeholder' => 'State / Province' ), 'zip' => array( 'isVisible' => true, 'label' => self::opt( $f, 'zipLabel', 'Postal / Zip Code' ), 'placeholder' => 'ZIP Code' ), 'country' => array( 'isVisible' => true, 'label' => self::opt( $f, 'countryLabel', 'Country' ) ), ); } // Switch: older builders saved onValue/offValue/onColor/offColor/defaultState; the renderer reads the the reference builder keys. if ( 'switch' === $type ) { foreach ( array( 'switchOnValue' => 'onValue', 'switchOffValue' => 'offValue', 'switchOnColor' => 'onColor', 'switchOffColor' => 'offColor' ) as $to => $from ) { if ( self::blank( $f, $to ) && ! self::blank( $f, $from ) ) { $f[ $to ] = $f[ $from ]; } } if ( self::blank( $f, 'defaultValue' ) && isset( $f['defaultState'] ) && true === $f['defaultState'] ) { $f['defaultValue'] = true; } } // Switch "on by default": the builder stores a boolean; the renderer compares defaultValue with 'true' / 'on' / the on-value. if ( 'switch' === $type && isset( $f['defaultValue'] ) && true === $f['defaultValue'] ) { $f['defaultValue'] = 'true'; } // Dropdown "Enable Multiple Selection": older builders saved multiSelect. if ( 'dropdown' === $type && ( ! array_key_exists( 'isMulti', $f ) || null === $f['isMulti'] ) && array_key_exists( 'multiSelect', $f ) ) { $f['isMulti'] = (bool) $f['multiSelect']; } // Choice columns: older builders saved choiceColumns (1-4 or 'inline'); the canvas reads it before the the reference builder key. if ( ( 'checkbox' === $type || 'radio' === $type ) && ! self::blank( $f, 'choiceColumns' ) ) { $cols = $f['choiceColumns']; if ( 'inline' === $cols ) { $cols = max( 1, isset( $f['options'] ) && WPVFB_JS::is_list( $f['options'] ) ? count( $f['options'] ) : 1 ); } if ( is_numeric( $cols ) && (int) $cols >= 1 ) { $f[ 'checkbox' === $type ? 'checkboxRowColumns' : 'radioRowColumns' ] = (int) $cols; } } // Column backgrounds: the builder's column editor saved columnSettings[col].bgColor/bgImage; the canvas lets them win. if ( 'grid' === $type && isset( $f['columnSettings'] ) && is_array( $f['columnSettings'] ) && ! WPVFB_JS::is_list( $f['columnSettings'] ) ) { $backgrounds = isset( $f['columnBackgrounds'] ) && is_array( $f['columnBackgrounds'] ) && ! WPVFB_JS::is_list( $f['columnBackgrounds'] ) ? $f['columnBackgrounds'] : array(); foreach ( $f['columnSettings'] as $key => $cs ) { if ( ! is_array( $cs ) ) { continue; } $target = self::normalize_column_key( $key ); $bg = isset( $backgrounds[ $target ] ) && is_array( $backgrounds[ $target ] ) ? $backgrounds[ $target ] : ( isset( $backgrounds[ $key ] ) && is_array( $backgrounds[ $key ] ) ? $backgrounds[ $key ] : array() ); $changed = false; foreach ( array( 'bgColor' => 'backgroundColor', 'bgImage' => 'backgroundImageUrl', 'bgSize' => 'backgroundImageSize', 'bgPosition' => 'backgroundImagePosition' ) as $from => $to ) { if ( ! self::blank( $cs, $from ) ) { $bg[ $to ] = $cs[ $from ]; $changed = true; } } if ( $changed ) { $backgrounds[ $target ] = $bg; } } if ( $backgrounds ) { $f['columnBackgrounds'] = $backgrounds; } } // Label colour: older builders saved labelColor per field; the canvas reads it after labelTextColor. if ( self::blank( $f, 'labelTextColor' ) && ! self::blank( $f, 'labelColor' ) ) { $f['labelTextColor'] = $f['labelColor']; } // Label size / line height: the canvas sizes the label with labelFontSize / labelLineHeight (field.fontSize and // field.lineHeight style the input there). the reference builder ignores field.labelFontSize (its templates carry it unused), so // the renderer reads these WP-only keys, set here, before field.fontSize / field.lineHeight. if ( isset( $f['labelFontSize'] ) && self::numeric( $f['labelFontSize'] ) ) { $f['wpvfbLabelFontSize'] = $f['labelFontSize']; } if ( isset( $f['labelLineHeight'] ) && self::numeric( $f['labelLineHeight'] ) ) { $f['wpvfbLabelLineHeight'] = $f['labelLineHeight']; } // Static content: older builders saved content* keys; the canvas reads them after the the reference builder keys. if ( 'static-content' === $type ) { $f = self::fill( $f, array( 'fontSize' => 'contentFontSize', 'textColor' => 'contentColor', 'textAlign' => 'contentAlign', 'fontFamily' => 'contentFontFamily' ) ); $ts = isset( $f['textStyle'] ) && is_array( $f['textStyle'] ) && ! WPVFB_JS::is_list( $f['textStyle'] ) ? $f['textStyle'] : array(); $add = array(); $weight = isset( $f['contentFontWeight'] ) ? $f['contentFontWeight'] : null; if ( ! ( isset( $ts['bold'] ) && is_bool( $ts['bold'] ) ) && ( 'bold' === $weight || ( self::numeric( $weight ) && (float) $weight >= 600 ) ) ) { $add['bold'] = true; } if ( ! ( isset( $ts['italic'] ) && is_bool( $ts['italic'] ) ) && isset( $f['contentFontStyle'] ) && 'italic' === $f['contentFontStyle'] ) { $add['italic'] = true; } if ( ! ( isset( $ts['underline'] ) && is_bool( $ts['underline'] ) ) && isset( $f['contentDecoration'] ) && 'underline' === $f['contentDecoration'] ) { $add['underline'] = true; } if ( $add ) { $f['textStyle'] = array_merge( $ts, $add ); } } // Utility fields: the WP settings panel used its own keys before it wrote the the reference builder ones. if ( 'promo-banner' === $type ) { $f = self::fill( $f, array( 'bannerMessage' => 'bannerTitle', 'backgroundColor' => 'bannerBgColor', 'textColor' => 'bannerTextColor' ) ); if ( ( ! array_key_exists( 'allowClose', $f ) || null === $f['allowClose'] ) && array_key_exists( 'dismissible', $f ) && null !== $f['dismissible'] ) { $f['allowClose'] = false !== $f['dismissible']; } } if ( 'trust-badges' === $type ) { $f = self::fill( $f, array( 'badgeColor' => 'iconColor' ) ); $use_old = ! ( isset( $f['trustBadges'] ) && WPVFB_JS::is_list( $f['trustBadges'] ) && count( $f['trustBadges'] ) > 0 ) && isset( $f['badges'] ) && WPVFB_JS::is_list( $f['badges'] ) && count( $f['badges'] ) > 0; $source = $use_old ? $f['badges'] : ( isset( $f['trustBadges'] ) ? $f['trustBadges'] : null ); $has_disabled = false; if ( WPVFB_JS::is_list( $source ) ) { foreach ( $source as $b ) { if ( self::is_assoc( $b ) && array_key_exists( 'enabled', $b ) && false === $b['enabled'] ) { $has_disabled = true; } } } if ( WPVFB_JS::is_list( $source ) && ( $use_old || $has_disabled ) ) { $list = array(); foreach ( $source as $b ) { if ( self::is_assoc( $b ) && array_key_exists( 'enabled', $b ) && false === $b['enabled'] ) { continue; } if ( is_string( $b ) ) { $list[] = array( 'type' => isset( self::$badge_types[ $b ] ) ? self::$badge_types[ $b ] : $b, 'label' => isset( self::$legacy_badge_labels[ $b ] ) ? self::$legacy_badge_labels[ $b ] : $b ); continue; } if ( ! self::is_assoc( $b ) ) { $list[] = $b; continue; } $badge_type = null; foreach ( array( 'type', 'id', 'icon' ) as $k ) { if ( isset( $b[ $k ] ) && is_string( $b[ $k ] ) && isset( self::$badge_types[ $b[ $k ] ] ) ) { $badge_type = self::$badge_types[ $b[ $k ] ]; break; } } if ( null === $badge_type ) { foreach ( array( 'type', 'id', 'icon' ) as $k ) { if ( isset( $b[ $k ] ) && WPVFB_JS::truthy( $b[ $k ] ) ) { $badge_type = $b[ $k ]; break; } } } if ( null === $badge_type && array_key_exists( 'icon', $b ) && null !== $b['icon'] ) { $badge_type = $b['icon']; } if ( null === $badge_type ) { unset( $b['type'] ); } else { $b['type'] = $badge_type; } $list[] = $b; } $f['trustBadges'] = $list; } } if ( 'countdown-banner' === $type ) { $f = self::fill( $f, array( 'countdownText' => 'countdownTitle', 'backgroundColor' => 'timerBgColor', 'expiryText' => 'expiryMessage' ) ); if ( isset( $f['expiryAction'] ) && 'show_message' === $f['expiryAction'] ) { $f['expiryAction'] = 'text'; } if ( isset( $f['expiryAction'] ) && 'hide_form' === $f['expiryAction'] ) { $f['expiryAction'] = 'hide'; } // targetDate (end date) → seconds left at render time. 0 would fall back to the renderer's 300 s, so an ended // countdown gets 1 (the storefront script shows the expiry text after one second; below 1 it throws). if ( self::blank( $f, 'countdownTime' ) && isset( $f['targetDate'] ) && is_string( $f['targetDate'] ) && '' !== $f['targetDate'] ) { try { $end = new DateTime( $f['targetDate'], function_exists( 'wp_timezone' ) ? wp_timezone() : new DateTimeZone( 'UTC' ) ); $left = $end->getTimestamp() - time(); $f['countdownTime'] = $left > 0 ? $left : 1; } catch ( Exception $e ) { // Not a date: leave the renderer default. } } } if ( 'section' === $type ) { $f = self::fill( $f, array( 'headingFormat' => 'headingLevel' ) ); } // The builder canvas reads numbers above 3 as pixels; the renderer would emit a unitless multiplier. if ( 'static-content' === $type && isset( $f['lineHeight'] ) && ( is_int( $f['lineHeight'] ) || is_float( $f['lineHeight'] ) ) && $f['lineHeight'] > 3 ) { $f['lineHeight'] = WPVFB_JS::num( $f['lineHeight'] ) . 'px'; } // Without its own value the canvas uses contentLineHeight (px) or the browser default; the renderer would fall // back to the global input line-height printed without a unit (20 → 20× the font size). if ( 'static-content' === $type && self::blank( $f, 'lineHeight' ) ) { $f['lineHeight'] = isset( $f['contentLineHeight'] ) && ( is_int( $f['contentLineHeight'] ) || is_float( $f['contentLineHeight'] ) ) ? WPVFB_JS::num( $f['contentLineHeight'] ) . 'px' : 'normal'; } return apply_filters( 'wpvfb_normalize_field', $f, $type ); } public static function normalize_field( $field, $keep_name = false ) { if ( ! is_array( $field ) ) { return $field; } if ( isset( $field['type'] ) && isset( self::$type_aliases[ $field['type'] ] ) ) { $field['type'] = self::$type_aliases[ $field['type'] ]; } $field = self::normalize_legacy_shape( $field, $keep_name ); $field = self::normalize_logic( $field ); $type = isset( $field['type'] ) ? $field['type'] : ''; if ( in_array( $type, (array) apply_filters( 'wpvfb_column_container_types', array( 'grid' ) ), true ) && isset( $field['columns'] ) && is_array( $field['columns'] ) && ! WPVFB_JS::is_list( $field['columns'] ) ) { $cols = array(); foreach ( $field['columns'] as $key => $list ) { $cols[ self::normalize_column_key( $key ) ] = WPVFB_JS::is_list( $list ) ? self::normalize_list( $list, $keep_name ) : $list; } $field['columns'] = $cols; } if ( isset( $field['subfields'] ) && WPVFB_JS::is_list( $field['subfields'] ) ) { $field['subfields'] = self::normalize_list( $field['subfields'], $keep_name ); } return $field; } private static function normalize_list( $list, $keep_name ) { $out = array(); foreach ( $list as $k => $item ) { $out[ $k ] = self::normalize_field( $item, $keep_name ); } return $out; } /** Keys whose values end up inside generated CSS (matched at the end of the key, case-insensitive). */ const STYLE_KEY_PATTERN = '/(colou?r|family|font|size|width|height|radius|padding|margin|shadow|spacing|weight|gap|opacity|align|alignment|position|border|background|bg|gradient|decoration|transform|offset|blur|spread|inset|repeat|overflow|display|cursor|transition|animation|lineheight|zindex|style|direction|wrap|justify|indent|transparency)s?$/i'; /** Keys whose values end up inside url() in generated CSS. */ const URL_KEY_PATTERN = '/(bgimage|backgroundimage|backgroundimageurl|bgimageurl)$/i'; /** * Validates every style value of a form (fields and settings) before it reaches the renderer, so that a saved * value can never close a CSS declaration, rule or the stylesheet: characters that end a declaration/rule/tag, * comments and code-like constructs are removed; url() values must be http(s), protocol-relative, relative or a * data: image. Text content keys (labels, HTML content, messages) are left to the output escaping. * * @param mixed $value Value. * @param string $key Key of the value (or of its parent array). * @param bool $style True when an ancestor key already marks the value as style data. * @return mixed */ public static function sanitize_style_values( $value, $key = '', $style = false ) { $key = (string) $key; if ( is_array( $value ) ) { $is_style = $style || ( '' !== $key && ! is_numeric( $key ) && preg_match( self::STYLE_KEY_PATTERN, $key ) ); foreach ( $value as $k => $v ) { $value[ $k ] = self::sanitize_style_values( $v, is_int( $k ) ? $key : (string) $k, (bool) $is_style ); } return $value; } if ( ! is_string( $value ) ) { return $value; } if ( '' !== $key && ! is_numeric( $key ) && preg_match( self::URL_KEY_PATTERN, $key ) ) { return self::sanitize_css_url_value( $value ); } if ( $style || ( '' !== $key && ! is_numeric( $key ) && preg_match( self::STYLE_KEY_PATTERN, $key ) ) ) { return self::sanitize_css_value( $value ); } return $value; } /** A CSS value: no declaration/rule/tag terminators, escapes, comments or code. */ public static function sanitize_css_value( $value ) { $value = wp_strip_all_tags( (string) $value ); $value = str_replace( array( '<', '>', '{', '}', ';', '\\', '/*', '*/', '`' ), '', $value ); if ( preg_match( '/expression\s*\(|javascript:|vbscript:|@import|behavior\s*:|-moz-binding/i', $value ) ) { return ''; } // url() inside a style value (gradients with images, backgrounds): same rule as image keys. if ( preg_match_all( '/url\(\s*([\'"]?)(.*?)\1\s*\)/i', $value, $m ) ) { foreach ( $m[2] as $u ) { if ( '' === self::sanitize_css_url_value( $u ) ) { return ''; } } } return $value; } /** A value used as url(...): http(s), protocol-relative or relative URL, or a data: image; otherwise ''. */ public static function sanitize_css_url_value( $value ) { $value = trim( wp_strip_all_tags( (string) $value ) ); if ( '' === $value ) { return ''; } if ( preg_match( '#^data:image/(png|jpe?g|gif|webp|svg\+xml)[;,]#i', $value ) ) { return str_replace( array( '<', '>', '"', "'", '(', ')', '\\', '{', '}', ';' ), '', $value ) === $value ? $value : ''; } // Gradients stored in image keys. if ( preg_match( '/^(repeating-)?(linear|radial|conic)-gradient\(/i', $value ) ) { return self::sanitize_css_value( $value ); } if ( preg_match( '#^(https?:)?//#i', $value ) || 0 === strpos( $value, '/' ) || preg_match( '#^[\w\-./%]+$#', $value ) ) { $url = esc_url_raw( $value, array( 'http', 'https' ) ); if ( '' === $url && ( 0 === strpos( $value, '/' ) || preg_match( '#^[\w\-./%]+$#', $value ) ) ) { $url = preg_replace( '#[^\w\-./%?=&]#', '', $value ); } return str_replace( array( '"', "'", '(', ')', '\\', ' ' ), array( '%22', '%27', '%28', '%29', '', '%20' ), $url ); } return ''; } public static function to_render_form( $fields, $settings ) { $fields = self::sanitize_style_values( is_array( $fields ) ? $fields : array() ); $settings = self::sanitize_style_values( is_array( $settings ) ? $settings : array() ); $keep_name = (bool) apply_filters( 'wpvfb_schema_keep_field_name', false, is_array( $settings ) ? $settings : array() ); $list = self::normalize_list( is_array( $fields ) ? $fields : array(), $keep_name ); $list = apply_filters( 'wpvfb_schema_fields', $list, is_array( $settings ) ? $settings : array() ); return array( 'fields' => $list, 'settings' => self::to_render_settings( $settings ), ); } }