&$value) { if ('attributes' === $key) { $value = self::dropEventHandlerAttributeKeys($value); } $value = self::sanitizeAttributeControlSetting($key, $value); self::sanitizeJsonNode($value); } unset($value); } elseif (is_object($node)) { foreach (get_object_vars($node) as $key => $value) { if ('attributes' === $key) { $value = self::dropEventHandlerAttributeKeys($value); } $value = self::sanitizeAttributeControlSetting($key, $value); self::sanitizeJsonNode($value); $node->{$key} = $value; } } elseif (is_string($node)) { $node = wp_kses_post($node); } } private static function sanitizeAttributeControlSetting($key, $value) { if ('max_repeat_field' === $key) { return is_scalar($value) && '' !== trim((string) $value) ? absint($value) : ''; } if ('display_mode' === $key) { $mode = is_scalar($value) ? sanitize_key((string) $value) : ''; return in_array($mode, ['accordion', 'tabs'], true) ? $mode : 'accordion'; } if ('display_type' === $key) { return is_scalar($value) ? sanitize_html_class((string) $value) : ''; } if ('subscription_options' === $key && is_array($value)) { foreach ($value as &$option) { if (!is_array($option)) { continue; } foreach (['name', 'user_input_label'] as $labelKey) { if (!array_key_exists($labelKey, $option)) { continue; } $label = $option[$labelKey]; $option[$labelKey] = is_scalar($label) ? fluentform_sanitize_html((string) $label) : ''; } } unset($option); return $value; } if ('pricing_options' !== $key || !is_array($value)) { return $value; } foreach ($value as &$option) { if (!is_array($option)) { continue; } if (array_key_exists('label', $option)) { $label = $option['label']; $option['label'] = is_scalar($label) ? fluentform_sanitize_html((string) $label) : ''; } if (array_key_exists('image', $option)) { $image = $option['image']; $option['image'] = is_scalar($image) ? esc_url_raw((string) $image) : ''; } } unset($option); return $value; } /** * kses cleans string values only, so an `onfocus` KEY survives an import untouched. * Shares the Helper rule so the two write paths cannot drift apart. */ private static function dropEventHandlerAttributeKeys($attributes) { if (!is_array($attributes) && !is_object($attributes)) { return $attributes; } $keys = is_object($attributes) ? array_keys(get_object_vars($attributes)) : array_keys($attributes); foreach ($keys as $key) { if (Helper::isSafeAttributeKey($key)) { continue; } if (is_object($attributes)) { unset($attributes->{$key}); } else { unset($attributes[$key]); } } return $attributes; } public static function exportForms($formIds) { $result = Form::with(['formMeta']) ->whereIn('id', $formIds) ->get(); $forms = []; foreach ($result as $item) { $form = json_decode($item); $formMetaFiltered = array_filter($form->form_meta, function ($item) { return ($item->meta_key !== '_total_views'); }); $form->metas = $formMetaFiltered; $form->form_fields = json_decode($form->form_fields); $forms[] = $form; } $fileName = 'fluentform-export-forms-' . count($forms) . '-' . date('d-m-Y') . '.json'; header('Content-disposition: attachment; filename=' . $fileName); header('Content-type: application/json'); echo json_encode(array_values($forms)); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $forms is escaped before being passed in. die(); } /** * Build the notice shown when imported custom JS/CSS or executable date * configuration was skipped because the importer lacks unfiltered_html. Returns an empty string when nothing was skipped. * * @param int $skippedForms * @param int $totalForms * @return string */ protected static function restrictedCodeNotice($skippedForms, $totalForms) { if (!$skippedForms) { return ''; } if ($totalForms < 2) { return __('Custom JS, CSS and advanced date configuration were not imported because your account cannot add custom code. Ask an administrator to add it.', 'fluentform'); } return sprintf( /* translators: 1: number of forms whose custom code was skipped, 2: total number of imported forms */ __('Custom JS, CSS and advanced date configuration were not imported for %1$d of %2$d forms because your account cannot add custom code. Ask an administrator to add it.', 'fluentform'), $skippedForms, $totalForms ); } /** * @param File $file The uploaded JSON file * @param bool $applyDefaultStyle Whether to apply default style settings to imported forms * @throws Exception */ public static function importForms($file, $applyDefaultStyle = false) { if ($file instanceof File) { $forms = \json_decode($file->getContents(), true); $insertedForms = []; $restrictedCodeForms = 0; if ($forms && is_array($forms)) { foreach ($forms as $formItem) { $formFields = json_encode([]); if ($fields = Arr::get($formItem, 'form', '')) { $formFields = json_encode($fields); } elseif ($fields = Arr::get($formItem, 'form_fields', '')) { $formFields = json_encode($fields); } else { throw new Exception(esc_html__('You have a faulty JSON file, please export the Fluent Forms again.', 'fluentform')); } // SECURITY (FINDING-07): the editor save path routes form_fields through // Updater::sanitizeFields (skipped only for unfiltered_html users), but import // stored them verbatim, so an importer without unfiltered_html could plant // stored XSS (e.g. a field label of ). Apply the same // recursive HTML sanitizer used for imported meta values unless the importer // may author raw HTML. $droppedDateConfigs = 0; if (!fluentformCanUnfilteredHTML()) { $decodedFields = json_decode($formFields, true); if (is_array($decodedFields)) { self::sanitizeJsonNode($decodedFields); $decodedFields['fields'] = DateConfigPolicy::dropExecutableConfigs( Arr::get($decodedFields, 'fields', []), $droppedDateConfigs ); $formFields = wp_json_encode($decodedFields) ?: $formFields; } } $formTitle = sanitize_text_field(Arr::get($formItem, 'title')); $form = [ 'title' => $formTitle ?: 'Blank Form', 'form_fields' => $formFields, 'status' => sanitize_text_field(Arr::get($formItem, 'status', 'published')), 'has_payment' => sanitize_text_field(Arr::get($formItem, 'has_payment', 0)), 'type' => sanitize_text_field(Arr::get($formItem, 'type', 'form')), 'created_by' => get_current_user_id(), ]; if (Arr::get($formItem, 'conditions')) { $form['conditions'] = Arr::get($formItem, 'conditions'); } if (isset($formItem['appearance_settings'])) { $form['appearance_settings'] = Arr::get($formItem, 'appearance_settings'); } $formId = Form::insertGetId($form); $insertedForms[$formId] = [ 'title' => $form['title'], 'edit_url' => admin_url('admin.php?page=fluent_forms&route=editor&form_id=' . $formId), ]; $skippedCustomCode = $droppedDateConfigs > 0; if (isset($formItem['metas'])) { foreach ($formItem['metas'] as $metaData) { $metaKey = sanitize_text_field(Arr::get($metaData, 'meta_key')); $metaValue = Arr::get($metaData, 'value'); // SECURITY (FINDING-08): Customizer::store() refuses to save custom // JS/CSS without unfiltered_html; import must honor the same boundary. // Sanitizing _custom_form_js via fluentform_kses_js is insufficient // because the value is JS *code* executed inside a