&$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