array( 'type' => 'text', 'label' => __( 'Label', 'mammoth-forms' ), ), 'key' => array( 'type' => 'text', 'label' => __( 'Field key', 'mammoth-forms' ), 'help' => __( 'Use this in formulas as {key}. Letters, numbers and underscores only.', 'mammoth-forms' ), ), 'description' => array( 'type' => 'textarea', 'label' => __( 'Description', 'mammoth-forms' ), 'help' => __( 'Shown under the field.', 'mammoth-forms' ), ), 'placeholder' => array( 'type' => 'text', 'label' => __( 'Placeholder', 'mammoth-forms' ), ), 'default' => array( 'type' => 'text', 'label' => __( 'Default value', 'mammoth-forms' ), ), 'required' => array( 'type' => 'toggle', 'label' => __( 'Required', 'mammoth-forms' ), ), 'min' => array( 'type' => 'text', 'label' => __( 'Minimum', 'mammoth-forms' ), ), 'max' => array( 'type' => 'text', 'label' => __( 'Maximum', 'mammoth-forms' ), ), 'step' => array( 'type' => 'text', 'label' => __( 'Step', 'mammoth-forms' ), 'help' => __( 'Use "any" to allow any decimal.', 'mammoth-forms' ), ), 'rows' => array( 'type' => 'number', 'label' => __( 'Rows', 'mammoth-forms' ), ), 'options' => array( 'type' => 'options', 'label' => __( 'Choices', 'mammoth-forms' ), 'help' => __( 'The value is what formulas use — keep it numeric for calculations.', 'mammoth-forms' ), ), 'choice_style' => array( 'type' => 'select', 'label' => __( 'Look', 'mammoth-forms' ), 'choices' => array( 'list' => __( 'List', 'mammoth-forms' ), 'buttons' => __( 'Buttons', 'mammoth-forms' ), ), ), 'multiple' => array( 'type' => 'toggle', 'label' => __( 'Allow multiple selections', 'mammoth-forms' ), ), 'formula' => array( 'type' => 'formula', 'label' => __( 'Formula', 'mammoth-forms' ), ), 'decimals' => array( 'type' => 'number', 'label' => __( 'Decimal places', 'mammoth-forms' ), 'default' => 2, ), 'prefix' => array( 'type' => 'text', 'label' => __( 'Prefix', 'mammoth-forms' ), 'help' => __( 'For example: €', 'mammoth-forms' ), ), 'suffix' => array( 'type' => 'text', 'label' => __( 'Suffix', 'mammoth-forms' ), 'help' => __( 'For example: kg, %, or " total"', 'mammoth-forms' ), ), 'thousands' => array( 'type' => 'toggle', 'label' => __( 'Group thousands', 'mammoth-forms' ), ), 'editable' => array( 'type' => 'toggle', 'label' => __( 'Let visitors override the result', 'mammoth-forms' ), 'help' => __( 'Off = read-only display (recommended).', 'mammoth-forms' ), ), 'hide_field' => array( 'type' => 'toggle', 'label' => __( 'Hide from the form', 'mammoth-forms' ), 'help' => __( 'Still calculated, still stored — useful for intermediate steps.', 'mammoth-forms' ), ), 'html' => array( 'type' => 'textarea', 'label' => __( 'Content', 'mammoth-forms' ), ), 'width' => array( 'type' => 'select', 'label' => __( 'Width', 'mammoth-forms' ), 'choices' => array( '100' => __( 'Full width', 'mammoth-forms' ), '50' => __( 'One half', 'mammoth-forms' ), '33' => __( 'One third', 'mammoth-forms' ), '66' => __( 'Two thirds', 'mammoth-forms' ), '25' => __( 'One quarter', 'mammoth-forms' ), '75' => __( 'Three quarters', 'mammoth-forms' ), ), ), 'css_class' => array( 'type' => 'text', 'label' => __( 'CSS class', 'mammoth-forms' ), ), 'image' => array( 'type' => 'image', 'label' => __( 'Image', 'mammoth-forms' ), ), 'image_size' => array( 'type' => 'select', 'label' => __( 'Image size', 'mammoth-forms' ), 'choices' => array( 'medium' => __( 'Medium', 'mammoth-forms' ), 'large' => __( 'Large', 'mammoth-forms' ), 'full' => __( 'Original', 'mammoth-forms' ), ), ), 'alt' => array( 'type' => 'text', 'label' => __( 'Alternative text', 'mammoth-forms' ), 'help' => __( 'Describes the image to people who cannot see it. Leave empty if it is only decoration.', 'mammoth-forms' ), ), 'caption' => array( 'type' => 'text', 'label' => __( 'Caption', 'mammoth-forms' ), ), 'link_url' => array( 'type' => 'text', 'label' => __( 'Link', 'mammoth-forms' ), 'help' => __( 'Optional: the address the image opens.', 'mammoth-forms' ), ), 'align' => array( 'type' => 'select', 'label' => __( 'Alignment', 'mammoth-forms' ), 'choices' => array( 'center' => __( 'Centre', 'mammoth-forms' ), 'left' => __( 'Left', 'mammoth-forms' ), 'right' => __( 'Right', 'mammoth-forms' ), 'wide' => __( 'Fill the width', 'mammoth-forms' ), ), ), 'table' => array( 'type' => 'table', 'label' => __( 'Cells', 'mammoth-forms' ), 'help' => __( 'Type any text. {key} shows the current value of a field, {key:label} the choice the visitor picked, and a cell that starts with = is a formula, such as ={quantity} * {price}. Values update as the visitor types.', 'mammoth-forms' ), ), 'consent_text' => array( 'type' => 'textarea', 'label' => __( 'Consent text', 'mammoth-forms' ), 'help' => __( 'Shown next to the checkbox. {privacy_policy} becomes a link to your privacy policy page (Settings > Privacy). Links, bold and italics are allowed.', 'mammoth-forms' ), ), 'table_header' => array( 'type' => 'toggle', 'label' => __( 'First row is a header', 'mammoth-forms' ), ), 'table_footer' => array( 'type' => 'toggle', 'label' => __( 'Last row is a total', 'mammoth-forms' ), ), 'country_first' => array( 'type' => 'text', 'label' => __( 'Countries listed first', 'mammoth-forms' ), 'help' => __( 'Two-letter codes, separated by commas, such as GR, CY. The rest follow in alphabetical order.', 'mammoth-forms' ), ), 'address_line2' => array( 'type' => 'toggle', 'label' => __( 'Second address line', 'mammoth-forms' ), ), 'address_region' => array( 'type' => 'toggle', 'label' => __( 'State / region', 'mammoth-forms' ), ), 'address_country' => array( 'type' => 'toggle', 'label' => __( 'Country', 'mammoth-forms' ), ), 'pattern' => array( 'type' => 'text', 'label' => __( 'Allowed format (regular expression)', 'mammoth-forms' ), 'help' => __( 'The whole answer has to match, for example [A-Z]{2}[0-9]{6} or \d{5}. Empty = any text.', 'mammoth-forms' ), ), 'validation_rule' => array( 'type' => 'text', 'label' => __( 'Must be true', 'mammoth-forms' ), 'help' => __( 'A formula that has to be true for the form to be sent, such as {end} > {start}, {guests} <= 40 or {total} >= 50. It can read every field and result.', 'mammoth-forms' ), ), 'error_message' => array( 'type' => 'text', 'label' => __( 'Error message', 'mammoth-forms' ), 'help' => __( 'Shown when the answer does not match the format or the rule. Empty = a general message.', 'mammoth-forms' ), ), 'conditions' => array( 'type' => 'conditions', 'label' => __( 'Conditional logic', 'mammoth-forms' ), ), ); return apply_filters( 'mammoth_setting_defs', $defs ); } public static function types() { $common = array( 'label', 'key', 'description', 'required', 'width', 'css_class', 'conditions' ); $display = array( 'width', 'css_class', 'conditions' ); // Checks of an answer: a format (text answers) and a rule (any answer or result). $rule = array( 'validation_rule', 'error_message' ); $check = array( 'pattern', 'validation_rule', 'error_message' ); $types = array( 'number' => array( 'label' => __( 'Number', 'mammoth-forms' ), 'icon' => '123', 'group' => 'basic', 'numeric' => true, 'supports' => array_merge( $common, array( 'placeholder', 'default', 'min', 'max', 'step', 'prefix', 'suffix' ), $rule ), ), 'calculated' => array( 'label' => __( 'Calculated', 'mammoth-forms' ), 'icon' => '=', 'group' => 'calc', 'numeric' => true, 'supports' => array( 'label', 'key', 'description', 'formula', 'decimals', 'prefix', 'suffix', 'thousands', 'editable', 'hide_field', 'width', 'css_class', 'conditions', 'validation_rule', 'error_message' ), ), 'slider' => array( 'label' => __( 'Slider', 'mammoth-forms' ), 'icon' => '⇹', 'group' => 'calc', 'numeric' => true, 'supports' => array_merge( $common, array( 'default', 'min', 'max', 'step', 'prefix', 'suffix' ), $rule ), ), 'select' => array( 'label' => __( 'Dropdown', 'mammoth-forms' ), 'icon' => '▾', 'group' => 'choice', 'numeric' => true, 'supports' => array_merge( $common, array( 'options', 'placeholder', 'multiple' ), $rule ), ), 'radio' => array( 'label' => __( 'Radio buttons', 'mammoth-forms' ), 'icon' => '◉', 'group' => 'choice', 'numeric' => true, 'supports' => array_merge( $common, array( 'options', 'choice_style' ), $rule ), ), 'checkboxes' => array( 'label' => __( 'Checkbox group', 'mammoth-forms' ), 'icon' => '☑', 'group' => 'choice', 'numeric' => true, 'supports' => array_merge( $common, array( 'options', 'choice_style' ), $rule ), ), 'checkbox' => array( 'label' => __( 'Single checkbox', 'mammoth-forms' ), 'icon' => '✓', 'group' => 'choice', 'numeric' => true, 'supports' => array_merge( $common, array( 'default' ) ), ), 'text' => array( 'label' => __( 'Text', 'mammoth-forms' ), 'icon' => 'Ab', 'group' => 'basic', 'supports' => array_merge( $common, array( 'placeholder', 'default' ), $check ), ), 'textarea' => array( 'label' => __( 'Paragraph', 'mammoth-forms' ), 'icon' => '¶', 'group' => 'basic', 'supports' => array_merge( $common, array( 'placeholder', 'default', 'rows' ), $check ), ), 'email' => array( 'label' => __( 'Email', 'mammoth-forms' ), 'icon' => '@', 'group' => 'basic', 'supports' => array_merge( $common, array( 'placeholder', 'default' ), $check ), ), 'tel' => array( 'label' => __( 'Phone', 'mammoth-forms' ), 'icon' => '☎', 'group' => 'basic', 'supports' => array_merge( $common, array( 'placeholder', 'default' ), $check ), ), 'date' => array( 'label' => __( 'Date', 'mammoth-forms' ), 'icon' => '▦', 'group' => 'basic', 'numeric' => true, 'help' => __( 'In formulas a date counts as days since 1 Jan 1970, so {end} - {start} gives a number of days.', 'mammoth-forms' ), 'supports' => array_merge( $common, array( 'default', 'min', 'max' ), $rule ), ), 'datetime' => array( 'label' => __( 'Date and time', 'mammoth-forms' ), 'icon' => '◴', 'group' => 'basic', 'numeric' => true, 'help' => __( 'In formulas it counts as days since 1 Jan 1970, the time as a fraction of a day, so ({end} - {start}) * 24 gives hours. Minimum and maximum look like 2026-12-01T09:00.', 'mammoth-forms' ), 'supports' => array_merge( $common, array( 'default', 'min', 'max' ), $rule ), ), 'range' => array( 'label' => __( 'Number range', 'mammoth-forms' ), 'icon' => '↔', 'group' => 'calc', 'numeric' => true, 'help' => __( 'Two numbers, from and to: a budget, an age range, square metres. In formulas {key} is the difference (to minus from), {key__from} and {key__to} each end.', 'mammoth-forms' ), 'supports' => array_merge( $common, array( 'min', 'max', 'step', 'prefix', 'suffix' ), $rule ), ), 'country' => array( 'label' => __( 'Country', 'mammoth-forms' ), 'icon' => '⚑', 'group' => 'choice', 'numeric' => true, 'help' => __( 'A list of all countries. Formulas read the two-letter code, such as GR, so conditions and price tables can use it; entries and emails show the name.', 'mammoth-forms' ), 'supports' => array_merge( $common, array( 'placeholder', 'default', 'country_first' ), $rule ), ), 'address' => array( 'label' => __( 'Address', 'mammoth-forms' ), 'icon' => '⌂', 'group' => 'basic', 'help' => __( 'Street, city and postal code, with an optional second line, region and country. When required, every part except the second line and the region has to be filled in.', 'mammoth-forms' ), 'supports' => array_merge( $common, array( 'address_line2', 'address_region', 'address_country', 'country_first' ) ), ), 'hidden' => array( 'label' => __( 'Hidden', 'mammoth-forms' ), 'icon' => '◌', 'group' => 'advanced', 'numeric' => true, 'supports' => array( 'label', 'key', 'default' ), ), 'heading' => array( 'label' => __( 'Heading', 'mammoth-forms' ), 'icon' => 'H', 'group' => 'layout', 'display' => true, 'supports' => array_merge( array( 'label', 'description' ), $display ), ), 'html' => array( 'label' => __( 'HTML', 'mammoth-forms' ), 'icon' => '', 'group' => 'layout', 'display' => true, 'supports' => array_merge( array( 'label', 'html' ), $display ), ), 'image' => array( 'label' => __( 'Image', 'mammoth-forms' ), 'icon' => '▣', 'group' => 'layout', 'display' => true, 'help' => __( 'Show a picture anywhere in the form. With conditional logic it can change with the answers.', 'mammoth-forms' ), 'supports' => array_merge( array( 'image', 'image_size', 'alt', 'caption', 'link_url', 'align' ), $display ), ), 'table' => array( 'label' => __( 'Table', 'mammoth-forms' ), 'icon' => '▤', 'group' => 'layout', 'display' => true, 'help' => __( 'A table of your own text, live field values and formulas — a price breakdown, a comparison, a summary.', 'mammoth-forms' ), 'supports' => array_merge( array( 'label', 'description', 'table', 'table_header', 'table_footer', 'decimals', 'prefix', 'suffix', 'thousands' ), $display ), ), 'consent' => array( 'label' => __( 'GDPR consent', 'mammoth-forms' ), 'icon' => '⚖', 'group' => 'choice', 'numeric' => true, 'help' => __( 'A checkbox the visitor must tick to agree to your privacy policy. The entry records that they agreed.', 'mammoth-forms' ), 'supports' => array( 'label', 'key', 'consent_text', 'required', 'width', 'css_class', 'conditions' ), ), 'divider' => array( 'label' => __( 'Divider', 'mammoth-forms' ), 'icon' => '—', 'group' => 'layout', 'display' => true, 'supports' => array_merge( array( 'label' ), $display ), ), ); return apply_filters( 'mammoth_field_types', $types ); } public static function type( $type ) { $types = self::types(); return isset( $types[ $type ] ) ? $types[ $type ] : null; } public static function supports( $field, $setting ) { $def = self::type( $field['type'] ); return $def && in_array( $setting, $def['supports'], true ); } /** * Display-only fields hold no value and are never validated or stored. */ public static function is_display( $type ) { $def = self::type( $type ); return $def && ! empty( $def['display'] ); } public static function is_numeric_type( $type ) { $def = self::type( $type ); return $def && ! empty( $def['numeric'] ); } public static function field_defaults() { /** Add-on field types add the settings they keep (Pro: rating_max, file_types…). */ return apply_filters( 'mammoth_field_defaults', array( 'id' => '', 'type' => 'text', 'key' => '', 'label' => '', 'description' => '', 'placeholder' => '', 'default' => '', 'required' => 'no', 'min' => '', 'max' => '', 'step' => '', 'rows' => 4, 'options' => array(), 'multiple' => 'no', 'formula' => '', 'decimals' => 2, 'prefix' => '', 'suffix' => '', 'thousands' => 'yes', 'editable' => 'no', 'hide_field' => 'no', 'html' => '', 'width' => '100', 'css_class' => '', 'image_id' => 0, 'image_url' => '', 'image_size' => 'large', 'alt' => '', 'caption' => '', 'link_url' => '', 'align' => 'center', 'table' => array(), 'consent_text' => '', 'choice_style' => 'list', 'table_header' => 'yes', 'table_footer' => 'no', 'country_first' => '', 'address_line2' => 'yes', 'address_region' => 'no', 'address_country' => 'yes', 'pattern' => '', 'validation_rule' => '', 'error_message' => '', 'conditions' => array( 'action' => 'show', 'match' => 'all', 'rules' => array(), ), ) ); } /** * Sanitises one field coming from the builder. */ public static function normalize_field( $field, $index = 1 ) { $out = array_merge( self::field_defaults(), array_intersect_key( $field, self::field_defaults() ) ); $out['type'] = sanitize_key( $out['type'] ); // A type this site does not know (an add-on that is switched off) is kept as it is // and shown as a text box, so switching the add-on back on brings the field back. if ( '' === $out['type'] ) { $out['type'] = 'text'; } $out['id'] = $out['id'] ? preg_replace( '/[^a-zA-Z0-9_-]/', '', (string) $out['id'] ) : 'f' . $index; $out['label'] = sanitize_text_field( (string) $out['label'] ); $out['description'] = wp_kses_post( (string) $out['description'] ); $out['placeholder'] = sanitize_text_field( (string) $out['placeholder'] ); $out['default'] = sanitize_text_field( (string) $out['default'] ); $out['min'] = sanitize_text_field( (string) $out['min'] ); $out['max'] = sanitize_text_field( (string) $out['max'] ); $out['step'] = sanitize_text_field( (string) $out['step'] ); $out['rows'] = max( 2, (int) $out['rows'] ); $out['formula'] = self::sanitize_formula( (string) $out['formula'] ); $out['decimals'] = max( 0, min( 8, (int) $out['decimals'] ) ); $out['prefix'] = self::sanitize_affix( $out['prefix'] ); $out['suffix'] = self::sanitize_affix( $out['suffix'] ); $out['html'] = wp_kses_post( (string) $out['html'] ); $out['css_class'] = sanitize_text_field( (string) $out['css_class'] ); $out['width'] = in_array( (string) $out['width'], array( '100', '75', '66', '50', '33', '25' ), true ) ? (string) $out['width'] : '100'; $out['image_id'] = absint( $out['image_id'] ); $out['image_url'] = esc_url_raw( (string) $out['image_url'] ); if ( $out['image_id'] && '' === $out['image_url'] ) { $found = wp_get_attachment_image_url( $out['image_id'], 'large' ); $out['image_url'] = $found ? esc_url_raw( $found ) : ''; } $out['image_size'] = in_array( $out['image_size'], array( 'medium', 'large', 'full' ), true ) ? $out['image_size'] : 'large'; $out['alt'] = sanitize_text_field( (string) $out['alt'] ); $out['caption'] = sanitize_text_field( (string) $out['caption'] ); $out['link_url'] = esc_url_raw( (string) $out['link_url'] ); $out['align'] = in_array( $out['align'], array( 'center', 'left', 'right', 'wide' ), true ) ? $out['align'] : 'center'; $out['table'] = self::sanitize_table( $out['table'] ); $out['consent_text'] = wp_kses( (string) $out['consent_text'], self::inline_html() ); // An unknown look (from a switched-off add-on) is kept; it displays as a list. $out['choice_style'] = sanitize_key( (string) $out['choice_style'] ); $out['choice_style'] = '' !== $out['choice_style'] ? $out['choice_style'] : 'list'; $out['country_first'] = implode( ', ', array_filter( array_map( 'trim', explode( ',', strtoupper( (string) preg_replace( '/[^A-Za-z,]/', '', (string) $out['country_first'] ) ) ) ) ) ); $out['pattern'] = self::sanitize_pattern( $out['pattern'] ); $out['validation_rule'] = self::sanitize_formula( (string) $out['validation_rule'] ); $out['error_message'] = sanitize_text_field( (string) $out['error_message'] ); foreach ( array( 'required', 'multiple', 'thousands', 'editable', 'hide_field', 'table_header', 'table_footer', 'address_line2', 'address_region', 'address_country' ) as $bool ) { $out[ $bool ] = self::yes_no( $out[ $bool ] ); } $out['key'] = self::sanitize_key_name( $out['key'], $out['label'], $index ); $options = array(); if ( is_array( $out['options'] ) ) { foreach ( $out['options'] as $option ) { if ( ! is_array( $option ) ) { continue; } $one = array( 'label' => sanitize_text_field( isset( $option['label'] ) ? (string) $option['label'] : '' ), 'value' => sanitize_text_field( isset( $option['value'] ) ? (string) $option['value'] : '' ), 'selected' => self::yes_no( isset( $option['selected'] ) ? $option['selected'] : false ), ); // Kept for add-ons (a card picture), cleaned. if ( ! empty( $option['image'] ) ) { $one['image'] = esc_url_raw( (string) $option['image'] ); $one['image_id'] = isset( $option['image_id'] ) ? absint( $option['image_id'] ) : 0; } // Extra numbers of a choice that an add-on reads (v_weight, v_days…), kept as text. foreach ( $option as $name => $extra ) { if ( is_string( $name ) && preg_match( '/^v_[a-z0-9_]{1,30}$/', $name ) && is_scalar( $extra ) ) { $one[ $name ] = sanitize_text_field( (string) $extra ); } } $options[] = $one; } } $out['options'] = $options; $out['conditions'] = Mammoth_Conditions::normalize( isset( $field['conditions'] ) ? $field['conditions'] : array() ); // Settings of a switched-off add-on (a rating's number of stars…) are kept as plain text. foreach ( $field as $name => $value ) { if ( ! array_key_exists( $name, $out ) && is_scalar( $value ) && preg_match( '/^[a-z][a-z0-9_]{0,39}$/', (string) $name ) && 'keyLocked' !== $name ) { $out[ $name ] = sanitize_text_field( (string) $value ); } } return apply_filters( 'mammoth_normalize_field', $out, $field ); } /** * A regular expression typed by the site owner: one line, no tags, at most * 200 characters. One that does not compile is dropped, so it can never * block a form. */ public static function sanitize_pattern( $pattern ) { $pattern = trim( str_replace( array( "\r", "\n", "\t" ), '', wp_strip_all_tags( (string) $pattern ) ) ); $pattern = substr( $pattern, 0, 200 ); if ( '' === $pattern || false === self::match_pattern( $pattern, '' ) ) { return ''; } return $pattern; } /** * Whether a whole answer matches a pattern, as the browser checks it * (form.js wraps it in ^(?:…)$ too). * * @return int|false 1 match, 0 no match, false for a pattern that does not compile. */ public static function match_pattern( $pattern, $value ) { $regex = '#^(?:' . preg_replace( '/(? name, in alphabetical order of the (translated) * names, with the codes in $first on top. * * @param string $first Codes separated by commas. */ public static function countries( $first = '' ) { static $all = null; if ( null === $all ) { $all = (array) apply_filters( 'mammoth_countries', require MAMMOTH_PLUGIN_DIR . 'includes/data/countries.php' ); $key = function ( $name ) { $name = function_exists( 'mb_strtolower' ) ? mb_strtolower( (string) $name, 'UTF-8' ) : strtolower( (string) $name ); return remove_accents( strtr( $name, array( 'ά' => 'α', 'έ' => 'ε', 'ή' => 'η', 'ί' => 'ι', 'ϊ' => 'ι', 'ΐ' => 'ι', 'ό' => 'ο', 'ύ' => 'υ', 'ϋ' => 'υ', 'ΰ' => 'υ', 'ώ' => 'ω' ) ) ); }; uasort( $all, function ( $a, $b ) use ( $key ) { return strcmp( $key( $a ), $key( $b ) ); } ); } $top = array(); foreach ( explode( ',', (string) $first ) as $code ) { $code = strtoupper( trim( $code ) ); if ( isset( $all[ $code ] ) ) { $top[ $code ] = $all[ $code ]; } } return $top + $all; } /** The name of a country code, or the code itself. */ public static function country_name( $code ) { $all = self::countries(); $code = strtoupper( trim( (string) $code ) ); return isset( $all[ $code ] ) ? $all[ $code ] : (string) $code; } /** * The boxes of an Address field, part => label, in order. * * @return array */ public static function address_parts( $field ) { $parts = array( 'street' => __( 'Street address', 'mammoth-forms' ) ); if ( ! isset( $field['address_line2'] ) || 'no' !== $field['address_line2'] ) { $parts['street2'] = __( 'Address line 2', 'mammoth-forms' ); } $parts['city'] = __( 'City', 'mammoth-forms' ); $parts['postcode'] = __( 'Postal code', 'mammoth-forms' ); if ( isset( $field['address_region'] ) && 'yes' === $field['address_region'] ) { $parts['region'] = __( 'State / region', 'mammoth-forms' ); } if ( ! isset( $field['address_country'] ) || 'no' !== $field['address_country'] ) { $parts['country'] = __( 'Country', 'mammoth-forms' ); } return $parts; } /** The parts a required Address field cannot leave empty. */ public static function address_required_parts( $field ) { return array_values( array_diff( array_keys( self::address_parts( $field ) ), array( 'street2', 'region' ) ) ); } /** Looks a choice field can take (add-ons may add their own). */ public static function choice_styles() { // Used by add-ons and the builder. static $styles = null; if ( null === $styles ) { $defs = self::setting_defs(); $styles = isset( $defs['choice_style']['choices'] ) ? array_map( 'strval', array_keys( $defs['choice_style']['choices'] ) ) : array( 'list' ); } return $styles; } /** The little HTML a consent sentence may hold. */ public static function inline_html() { return array( 'a' => array( 'href' => array(), 'target' => array(), 'rel' => array(), ), 'strong' => array(), 'b' => array(), 'em' => array(), 'i' => array(), ); } /** True for the one-box types: checkbox and consent. */ public static function is_single_box( $type ) { return in_array( $type, array( 'checkbox', 'consent' ), true ); } /** * A table is rows of plain-text cells. A cell starting with "=" is a * formula and keeps every character the formula parser accepts ("<" too, * which sanitize_text_field() would turn into an entity). */ public static function sanitize_table( $table ) { $out = array(); if ( ! is_array( $table ) ) { return $out; } $cols = 0; foreach ( array_slice( array_values( $table ), 0, 50 ) as $row ) { if ( ! is_array( $row ) ) { continue; } $cells = array(); foreach ( array_slice( array_values( $row ), 0, 10 ) as $cell ) { $cell = is_scalar( $cell ) ? trim( (string) $cell ) : ''; if ( '=' === substr( $cell, 0, 1 ) ) { $cells[] = '=' . self::sanitize_formula( substr( $cell, 1 ) ); } else { $cells[] = sanitize_text_field( $cell ); } } $cols = max( $cols, count( $cells ) ); $out[] = $cells; } // Every row the same length. foreach ( $out as &$cells ) { $cells = array_pad( $cells, $cols, '' ); } return $out; } /** * Normalising has to be idempotent: a config is re-normalised on every save * and every read, and plain empty() would read the string 'no' as true. */ public static function yes_no( $value ) { return in_array( $value, array( true, 1, '1', 'yes', 'on', 'true' ), true ) ? 'yes' : 'no'; } /** * Prefixes and suffixes keep their spaces — " €" and "%" are meant to sit * exactly where the user typed them, so this cannot go through * sanitize_text_field(), which trims. */ public static function sanitize_affix( $text ) { // Not wp_strip_all_tags(): that trims, which is the whole problem. $text = preg_replace( '@<(script|style)[^>]*?>.*?@si', '', (string) $text ); $text = strip_tags( (string) $text ); // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags -- wp_strip_all_tags() trims, see above. return str_replace( array( "\r", "\n", "\t" ), ' ', $text ); } /** * Formulas are stored raw-ish: only characters the parser can handle. */ public static function sanitize_formula( $formula ) { $formula = wp_unslash( $formula ); // \p{L}\p{M}: letters of any script, so LOOKUP('prices', 'Ελλάδα') keeps its // text; the parser still only accepts ASCII names outside quotes. $formula = preg_replace( '/[^0-9\p{L}\p{M}_{}\.\,\+\-\*\/\^\%\(\)\<\>\=\!\&\|\s\'\"\:]/u', '', $formula ); return trim( (string) $formula ); } /** * Same table as slugify() in admin/js/builder.js: "Τιμή μονάδας" -> "timi monadas". */ public static function greek_to_latin( $text ) { $text = function_exists( 'mb_strtolower' ) ? mb_strtolower( (string) $text, 'UTF-8' ) : strtolower( (string) $text ); $text = strtr( $text, array( 'ά' => 'α', 'έ' => 'ε', 'ή' => 'η', 'ί' => 'ι', 'ϊ' => 'ι', 'ΐ' => 'ι', 'ό' => 'ο', 'ύ' => 'υ', 'ϋ' => 'υ', 'ΰ' => 'υ', 'ώ' => 'ω', ) ); $text = strtr( $text, array( 'θ' => 'th', 'χ' => 'ch', 'ψ' => 'ps', 'α' => 'a', 'β' => 'v', 'γ' => 'g', 'δ' => 'd', 'ε' => 'e', 'ζ' => 'z', 'η' => 'i', 'ι' => 'i', 'κ' => 'k', 'λ' => 'l', 'μ' => 'm', 'ν' => 'n', 'ξ' => 'x', 'ο' => 'o', 'π' => 'p', 'ρ' => 'r', 'σ' => 's', 'ς' => 's', 'τ' => 't', 'υ' => 'y', 'φ' => 'f', 'ω' => 'o', ) ); return remove_accents( $text ); } public static function sanitize_key_name( $key, $fallback_label = '', $index = 1 ) { $key = strtolower( (string) $key ); $key = preg_replace( '/[^a-z0-9_]/', '_', $key ); $key = trim( (string) $key, '_' ); if ( '' === $key ) { $key = strtolower( self::greek_to_latin( (string) $fallback_label ) ); // One underscore per gap, as slugify() in builder.js does. $key = preg_replace( '/[^a-z0-9_]+/', '_', $key ); $key = trim( (string) $key, '_' ); } if ( '' === $key ) { $key = 'field_' . $index; } if ( preg_match( '/^[0-9]/', $key ) ) { $key = 'f_' . $key; } return $key; } }