&$value) { // Local var: mutating $attribute here would collapse every sibling // after the first onto a bare key, resolving nested inputs to the wrong element. $childAttribute = $attribute ? $attribute . '[' . $key . ']' : $key; $value = static::sanitizer($value, $childAttribute, $fields); } unset($value); } return $input; } /** * Flatten a request value into a plain, printable string. * * Request values are string or array. Walks nested arrays so a crafted * param[][] cannot raise an "Array to string conversion" notice, and the * caller can escape the result in one pass instead of branching on shape. * * @param mixed $value * * @return string */ public static function flattenRequestValue($value) { if (!is_array($value)) { return is_scalar($value) ? (string) $value : ''; } $flat = []; array_walk_recursive($value, function ($item) use (&$flat) { if (is_scalar($item)) { $flat[] = (string) $item; } }); return implode(', ', $flat); } public static function isOptionGroup($option) { return is_array($option) && ArrayHelper::get($option, 'type') === 'group' && is_array(ArrayHelper::get($option, 'options')); } /* * Int or nothing. Persisting free text here would hand the one user class * this sanitizer exists to contain an arbitrary string in form_fields, for a * value with no PHP consumer at all — it only keys the editor's Vue list. * A non-numeric id collapses to 0, and ensureUniqueIds() regenerates it on * load. Numerics must stay numeric for the same reason: as the string "0" it * would be JS-truthy, and ensureUniqueIds() only regenerates ids that are * falsy or already seen. */ public static function sanitizeOptionId($value) { return is_numeric($value) ? (int) $value : 0; } /* * sanitize_title, not sanitize_key: Pro writes inventory slugs with * sanitize_title (InventoryController), which percent-encodes non-ASCII. * sanitize_key strips '%', producing a slug that no longer exists — and a * truthy-but-missing slug resolves to quantity 0 in InventoryValidation, so * the option fails closed as permanently stocked out. sanitize_title is * idempotent over its own output and still neutralises markup. * * The is_scalar guard is ours: unlike sanitize_key, sanitize_title has no * guard of its own and raises a TypeError on PHP 8 for an array value. */ public static function sanitizeOptionSlug($value) { return is_scalar($value) ? sanitize_title($value) : ''; } /** * A key is unsafe when it opens a handler, or when it carries a character that * ENDS an attribute name in the HTML tokeniser -- whitespace, quote, slash, * equals or angle bracket. `esc_attr()` leaves those intact, so `x onclick` * renders as two attributes and the second one is live. * * Deny those characters rather than allow-list a charset: an allow-list also * rejects the legal-but-unusual keys real sites carry (leading underscore, * non-Latin names, framework prefixes) and silently drops working markup. * * @param string|int $key * @return bool */ public static function isSafeAttributeKey($key) { $key = (string) $key; if ('' === $key || preg_match('/^on[a-z]/i', $key)) { return false; } return !preg_match('/[\s"\'\/=<>`]|[\x00-\x1F\x7F]/', $key); } /* * Keys the whitelist above drops but the editor and Pro Inventory need back. * They are sanitized rather than passed through, since preserving unknown * keys verbatim would defeat the whitelist for exactly the users this path * protects. Absent keys stay absent: an invented `quantity => 0` reads as * "stock out" to InventoryValidation. */ protected static function optionPassthroughMap() { return [ 'id' => [self::class, 'sanitizeOptionId'], 'quantity' => 'intval', 'global_inventory' => [self::class, 'sanitizeOptionSlug'], ]; } public static function sanitizeAdvancedOptions($options, $depth = 0) { if (!is_array($options)) { return []; } $sanitized = []; foreach ($options as $option) { if (!is_array($option)) { continue; } if (self::isOptionGroup($option)) { $groupOptions = self::sanitizeAdvancedOptions( ArrayHelper::get($option, 'options', []), $depth + 1 ); if ($depth > 0) { $sanitized = array_merge($sanitized, $groupOptions); continue; } $groupLabel = ArrayHelper::get($option, 'label', ''); $group = [ 'type' => 'group', 'label' => wp_kses_post(is_scalar($groupLabel) ? $groupLabel : ''), 'options' => $groupOptions, ]; // Groups are keyed on group.id in the editor exactly like leaf // options, and is_open is user-visible collapse state. if (array_key_exists('id', $option)) { $group['id'] = self::sanitizeOptionId($option['id']); } if (array_key_exists('is_open', $option)) { $group['is_open'] = (bool) $option['is_open']; } $sanitized[] = $group; continue; } /* * Non-scalars are flattened to '' before the WP sanitisers see them: * wp_kses_post() and sanitize_url() have no guard of their own and * raise a TypeError on PHP 8 for an array, which would be an uncaught * 500 on save for exactly the users this whitelist protects. */ $scalar = function ($key) use ($option) { $value = ArrayHelper::get($option, $key, ''); return is_scalar($value) ? $value : ''; }; $clean = [ 'label' => wp_kses_post($scalar('label')), 'value' => sanitize_text_field($scalar('value')), 'image' => sanitize_url($scalar('image')), 'calc_value' => sanitize_text_field($scalar('calc_value')), 'disabled' => ArrayHelper::isTrue($option, 'disabled'), ]; foreach (self::optionPassthroughMap() as $key => $sanitizer) { if (array_key_exists($key, $option)) { $clean[$key] = call_user_func($sanitizer, $option[$key]); } } $sanitized[] = $clean; } return $sanitized; } public static function flattenAdvancedOptions($options) { if (!is_array($options)) { return []; } $flattened = []; foreach ($options as $option) { if (self::isOptionGroup($option)) { $flattened = array_merge( $flattened, self::flattenAdvancedOptions(ArrayHelper::get($option, 'options', [])) ); continue; } if (!is_array($option)) { continue; } $flattened[] = $option; } return $flattened; } public static function advancedOptionsValueLabelMap($options) { $formatted = []; foreach (self::flattenAdvancedOptions($options) as $option) { $formatted[ArrayHelper::get($option, 'value')] = ArrayHelper::get($option, 'label'); } return $formatted; } public static function makeMenuUrl($page = 'fluent_forms_settings', $component = null) { $baseUrl = admin_url('admin.php?page=' . $page); $hash = ArrayHelper::get($component, 'hash', ''); if ($hash) { $baseUrl = $baseUrl . '#' . $hash; } $query = ArrayHelper::get($component, 'query'); if ($query) { $paramString = http_build_query($query); if ($hash) { $baseUrl .= '?' . $paramString; } else { $baseUrl .= '&' . $paramString; } } return $baseUrl; } public static function getHtmlElementClass($value1, $value2, $class = 'active', $default = '') { return $value1 === $value2 ? $class : $default; } /** * Determines if the given string is a valid json. * * @param $string * * @return bool */ public static function isJson($string) { json_decode($string); return JSON_ERROR_NONE === json_last_error(); } public static function isSlackEnabled() { $globalModules = get_option('fluentform_global_modules_status'); return $globalModules && isset($globalModules['slack']) && 'yes' == $globalModules['slack']; } public static function getEntryStatuses($form_id = false) { $statuses = [ 'unread' => __('Unread', 'fluentform'), 'read' => __('Read', 'fluentform'), 'favorites' => __('Favorites', 'fluentform'), ]; $statuses = apply_filters_deprecated( 'fluentform_entry_statuses_core', [ $statuses, $form_id, ], FLUENTFORM_FRAMEWORK_UPGRADE, 'fluentform/entry_statuses_core', 'Use fluentform/entry_statuses_core instead of fluentform_entry_statuses_core.' ); $statuses = apply_filters('fluentform/entry_statuses_core', $statuses, $form_id); $statuses['spam'] = __('Spam', 'fluentform'); $statuses['trashed'] = __('Trashed', 'fluentform'); return $statuses; } // Statuses a caller may write by hand; add-ons withhold the ones they own as workflow steps. public static function getMutableEntryStatuses($form_id = false, $submission_id = null) { return apply_filters('fluentform/entry_statuses_for_mutation', static::getEntryStatuses($form_id), $form_id, $submission_id); } public static function getReportableInputs() { $data = [ 'select', 'input_radio', 'input_checkbox', 'ratings', 'net_promoter', 'select_country', 'net_promoter_score', ]; $data = apply_filters_deprecated( 'fluentform_reportable_inputs', [ $data, ], FLUENTFORM_FRAMEWORK_UPGRADE, 'fluentform/reportable_inputs', 'Use fluentform/reportable_inputs instead of fluentform_reportable_inputs.' ); return apply_filters('fluentform/reportable_inputs', $data); } public static function getSubFieldReportableInputs() { $grid = apply_filters_deprecated( 'fluentform_subfield_reportable_inputs', [ ['tabular_grid'], ], FLUENTFORM_FRAMEWORK_UPGRADE, 'fluentform/subfield_reportable_inputs', 'Use fluentform/subfield_reportable_inputs instead of fluentform_subfield_reportable_inputs.' ); return apply_filters('fluentform/subfield_reportable_inputs', $grid); } public static function getFormMeta($formId, $metaKey, $default = '', $forced = false) { $formattedValues = self::$formMetaCache[$formId] ?? []; if (!isset(self::$formMetaCache[$formId]) || $forced) { $formMetas = FormMeta::where('form_id', $formId) ->get(); $formattedValues = []; foreach ($formMetas as $formMeta) { $value = $formMeta->value; $decoded = json_decode($value ?? '', true); if (is_array($decoded)) { $value = $decoded; } $formattedValues[$formMeta->meta_key] = $value; } self::$formMetaCache[$formId] = $formattedValues; } return Arr::get($formattedValues, $metaKey, $default); } public static function setFormMeta($formId, $metaKey, $value) { if ($meta = FormMeta::persist($formId, $metaKey, $value)) { // Update the cache with the new value if (!isset(self::$formMetaCache[$formId])) { self::$formMetaCache[$formId] = []; } self::$formMetaCache[$formId][$metaKey] = $value; return $meta->id; } return null; } public static function deleteFormMeta($formId, $metaKey) { try { FormMeta::remove($formId, $metaKey); return true; } catch (\Exception $ex) { return null; } } /** * Resolve an entry's column => value map regardless of whether the entry is * a stdClass DB row (columns are real properties) or a WPFluent Model * (columns live in an internal attribute bag reached via __get). * * @param object|array $entry * @return array */ public static function getEntryColumns($entry) { if (is_object($entry) && method_exists($entry, 'getAttributes')) { return $entry->getAttributes(); } return (array) $entry; } public static function getSubmissionMeta($submissionId, $metaKey, $default = false) { return SubmissionMeta::retrieve($metaKey, $submissionId, $default); } public static function setSubmissionMeta($submissionId, $metaKey, $value, $formId = false) { if ($meta = SubmissionMeta::persist($submissionId, $metaKey, $value, $formId)) { return $meta->id; } return null; } public static function setSubmissionMetaAsArrayPush($submissionId, $metaKey, $value, $formId = false) { if ($meta = SubmissionMeta::persistArray($submissionId, $metaKey, $value, $formId)) { return $meta->id; } return null; } public static function isEntryAutoDeleteEnabled($formId) { if ( 'yes' == ArrayHelper::get(static::getFormMeta($formId, 'formSettings', []), 'delete_entry_on_submission', '') ) { return true; } return false; } public static function formExtraCssClass($form) { if (!$form->settings) { $formSettings = static::getFormMeta($form->id, 'formSettings'); } else { $formSettings = $form->settings; } if (!$formSettings) { return ''; } if ($extraClass = ArrayHelper::get($formSettings, 'form_extra_css_class')) { return esc_attr($extraClass); } return ''; } public static function getNextTabIndex($increment = 1) { if (static::isTabIndexEnabled()) { static::$tabIndex += $increment; return static::$tabIndex; } return ''; } public static function getFormInstaceClass($formId) { static::$formInstance++; return 'ff_form_instance_' . $formId . '_' . static::$formInstance; } public static function resetTabIndex() { static::$tabIndex = 0; } public static function isFluentAdminPage() { $fluentPages = [ 'fluent_forms', 'fluent_forms_all_entries', 'fluent_forms_transfer', 'fluent_forms_settings', 'fluent_forms_add_ons', 'fluent_forms_docs', 'fluent_forms_payment_entries', 'fluent_forms_reports', ]; $status = true; $page = wpFluentForm('request')->get('page'); if (!$page || !in_array($page, $fluentPages)) { $status = false; } $status = apply_filters_deprecated( 'fluentform_is_admin_page', [ $status, ], FLUENTFORM_FRAMEWORK_UPGRADE, 'fluentform/is_admin_page', 'Use fluentform/is_admin_page instead of fluentform_is_admin_page.' ); return apply_filters('fluentform/is_admin_page', $status); } public static function getShortCodeIds($content, $tag = 'fluentform', $selector = 'id') { if (false === strpos($content, '[')) { return []; } preg_match_all('/' . get_shortcode_regex() . '/', $content, $matches, PREG_SET_ORDER); if (empty($matches)) { return []; } $ids = []; $attributes = []; foreach ($matches as $shortcode) { if (count($shortcode) >= 2 && $tag === $shortcode[2]) { // Replace braces with empty string. $parsedCode = str_replace(['[', ']', '[', ']'], '', $shortcode[0]); $result = shortcode_parse_atts($parsedCode); if (!empty($result[$selector])) { if ('fluentform' == $tag && !empty($result['type']) && 'conversational' == $result['type']) { continue; } $ids[$result[$selector]] = $result[$selector]; $theme = ArrayHelper::get($result, 'theme'); if ($theme) { $attributes[] = [ 'formId' => $result[$selector], 'theme' => $theme, ]; } } } } if ($attributes) { $ids['attributes'] = $attributes; } return $ids; } public static function getFormsIdsFromBlocks($content) { $ids = []; $attributes = []; if (!function_exists('parse_blocks')) { return $ids; } $has_block = false !== strpos($content, '