Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/fluentform/app/Helpers/Helper.php
+++ b/fluentform/app/Helpers/Helper.php
@@ -206,7 +206,7 @@
'fluentform_entry_statuses_core',
[
$statuses,
- $form_id
+ $form_id,
],
FLUENTFORM_FRAMEWORK_UPGRADE,
'fluentform/entry_statuses_core',
@@ -235,7 +235,7 @@
$data = apply_filters_deprecated(
'fluentform_reportable_inputs',
[
- $data
+ $data,
],
FLUENTFORM_FRAMEWORK_UPGRADE,
'fluentform/reportable_inputs',
@@ -250,7 +250,7 @@
$grid = apply_filters_deprecated(
'fluentform_subfield_reportable_inputs',
[
- ['tabular_grid']
+ ['tabular_grid'],
],
FLUENTFORM_FRAMEWORK_UPGRADE,
'fluentform/subfield_reportable_inputs',
@@ -263,7 +263,7 @@
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();
@@ -271,7 +271,7 @@
$formattedValues = [];
foreach ($formMetas as $formMeta) {
$value = $formMeta->value;
-
+
$decoded = json_decode($value ?? '', true);
if (is_array($decoded)) {
$value = $decoded;
@@ -391,7 +391,7 @@
public static function getFormInstaceClass($formId)
{
- static::$formInstance += 1;
+ static::$formInstance++;
return 'ff_form_instance_' . $formId . '_' . static::$formInstance;
}
@@ -411,7 +411,7 @@
'fluent_forms_add_ons',
'fluent_forms_docs',
'fluent_forms_payment_entries',
- 'fluent_forms_reports'
+ 'fluent_forms_reports',
];
$status = true;
@@ -425,7 +425,7 @@
$status = apply_filters_deprecated(
'fluentform_is_admin_page',
[
- $status
+ $status,
],
FLUENTFORM_FRAMEWORK_UPGRADE,
'fluentform/is_admin_page',
@@ -457,7 +457,7 @@
$result = shortcode_parse_atts($parsedCode);
if (!empty($result[$selector])) {
- if ($tag == 'fluentform' && !empty($result['type']) && $result['type'] == 'conversational') {
+ if ('fluentform' == $tag && !empty($result['type']) && 'conversational' == $result['type']) {
continue;
}
@@ -468,7 +468,7 @@
if ($theme) {
$attributes[] = [
'formId' => $result[$selector],
- 'theme' => $theme
+ 'theme' => $theme,
];
}
}
@@ -491,7 +491,7 @@
return $ids;
}
- $has_block = false !== strpos($content, '<!-- wp:fluentfom/guten-block' . ' ');
+ $has_block = false !== strpos($content, '<!-- wp:fluentfom/guten-block ');
if (!$has_block) {
return $ids;
@@ -505,7 +505,7 @@
$hasBlock = strpos($block['blockName'], 'fluentfom/guten-block') === 0;
if ($hasBlock) {
- $formId = (int)$block['attrs']['formId'];
+ $formId = (int) $block['attrs']['formId'];
$ids[] = $formId;
@@ -514,7 +514,7 @@
if ($theme) {
$attributes[] = [
'formId' => $formId,
- 'theme' => $theme
+ 'theme' => $theme,
];
}
}
@@ -550,8 +550,8 @@
return false;
}
- $fieldsJson = (string)($form->form_fields ?? '');
- if ($fieldsJson === '') {
+ $fieldsJson = (string) ($form->form_fields ?? '');
+ if ('' === $fieldsJson) {
return false;
}
@@ -560,7 +560,7 @@
return false;
}
- return (bool)ArrayHelper::get($fields, 'stepsWrapper');
+ return (bool) ArrayHelper::get($fields, 'stepsWrapper');
}
public static function hasFormElement($formId, $elementName)
@@ -584,8 +584,8 @@
// if form has pending payment then the value doesn't exist in EntryDetails table
// further checking on Submission table if the value exists
if (!$exist && $form->has_payment) {
- $escapedKey = json_encode($fieldName);
- $escapedValue = json_encode($inputValue);
+ $escapedKey = wp_json_encode($fieldName);
+ $escapedValue = wp_json_encode($inputValue);
$searchPattern = trim($escapedKey, '"') . '":' . $escapedValue;
$searchPattern = addcslashes($searchPattern, '%_');
@@ -672,7 +672,7 @@
$data = apply_filters_deprecated(
'fluentform_numeric_styles',
[
- $data
+ $data,
],
FLUENTFORM_FRAMEWORK_UPGRADE,
'fluentform/numeric_styles',
@@ -753,10 +753,8 @@
$columnInputs = static::getFieldNamesStatuses(ArrayHelper::get($column, 'fields', []));
$names = array_merge($names, $columnInputs);
}
- } else {
- if ($name = ArrayHelper::get($field, 'attributes.name')) {
+ } elseif ($name = ArrayHelper::get($field, 'attributes.name')) {
$names[] = $name;
- }
}
}
@@ -807,14 +805,18 @@
'sanitize_text_field',
array_column(static::flattenAdvancedOptions($formattedOptions), 'value')
), function ($value) {
- return $value !== '';
+ return '' !== $value;
}));
if (count($optionValues) !== count(array_unique($optionValues))) {
- $duplicates[] = ArrayHelper::get($field, 'settings.admin_field_label')
- ?: ArrayHelper::get($field, 'settings.label')
- ?: ArrayHelper::get($field, 'attributes.name')
- ?: __('Ranking Field', 'fluentform');
+ $fieldLabel = ArrayHelper::get($field, 'settings.admin_field_label');
+ if (!$fieldLabel) {
+ $fieldLabel = ArrayHelper::get($field, 'settings.label');
+ }
+ if (!$fieldLabel) {
+ $fieldLabel = ArrayHelper::get($field, 'attributes.name');
+ }
+ $duplicates[] = $fieldLabel ? $fieldLabel : __('Ranking Field', 'fluentform');
}
}
@@ -839,10 +841,8 @@
return static::getConversionUrl($formId);
} elseif ('classic' == $type) {
return site_url('?fluent_forms_pages=1&design_mode=1&preview_id=' . $formId) . '#ff_preview';
- } else {
- if (static::isConversionForm($formId)) {
+ } elseif (static::isConversionForm($formId)) {
return static::getConversionUrl($formId);
- }
}
return site_url('?fluent_forms_pages=1&design_mode=1&preview_id=' . $formId) . '#ff_preview';
@@ -870,7 +870,7 @@
$slug = apply_filters_deprecated(
'fluentform_conversational_url_slug',
[
- 'fluent-form'
+ 'fluent-form',
],
FLUENTFORM_FRAMEWORK_UPGRADE,
'fluentform/conversational_url_slug',
@@ -904,7 +904,7 @@
$locations = apply_filters_deprecated(
'fluentform_file_upload_options',
[
- $locations
+ $locations,
],
FLUENTFORM_FRAMEWORK_UPGRADE,
'fluentform/file_upload_options',
@@ -989,7 +989,7 @@
'fluentform_truncate_password_values',
[
true,
- $formId
+ $formId,
],
FLUENTFORM_FRAMEWORK_UPGRADE,
'fluentform/truncate_password_values',
@@ -1011,8 +1011,7 @@
$rowJoiner = '<br />',
$colJoiner = ', ',
$type = ''
- )
- {
+ ) {
if (!$girdData || !$field) {
return '';
}
@@ -1039,7 +1038,7 @@
if ($girdCols && isset($girdCols[$item])) {
$item = $girdCols[$item];
}
- if ($index == (count($column) - 1)) {
+ if ((count($column) - 1) == $index) {
$_colJoiner = '';
}
$value .= $item . $_colJoiner;
@@ -1116,7 +1115,7 @@
public static function isAutosaveEnabled()
{
$autosaveEnabled = ArrayHelper::get(get_option('_fluentform_global_form_settings'), 'misc.autosave_enabled', 'no');
- return $autosaveEnabled === 'yes';
+ return 'yes' === $autosaveEnabled;
}
public static function maybeDecryptUrl($url)
@@ -1144,7 +1143,7 @@
public static function isBlockEditor()
{
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Checking REST API context
- return defined('REST_REQUEST') && REST_REQUEST && !empty($_REQUEST['context']) && $_REQUEST['context'] === 'edit';
+ return defined('REST_REQUEST') && REST_REQUEST && !empty($_REQUEST['context']) && 'edit' === $_REQUEST['context'];
}
public static function resolveValidationRulesGlobalOption(&$field)
@@ -1153,14 +1152,12 @@
foreach ($field['fields'] as &$subField) {
static::resolveValidationRulesGlobalOption($subField);
}
- } else {
- if (ArrayHelper::get($field, 'settings.validation_rules')) {
- foreach ($field['settings']['validation_rules'] as $key => &$rule) {
- if (!isset($rule['global'])) {
- $rule['global'] = false;
- }
- $rule['global_message'] = static::getGlobalDefaultMessage($key);
+ } elseif (ArrayHelper::get($field, 'settings.validation_rules')) {
+ foreach ($field['settings']['validation_rules'] as $key => &$rule) {
+ if (!isset($rule['global'])) {
+ $rule['global'] = false;
}
+ $rule['global_message'] = static::getGlobalDefaultMessage($key);
}
}
}
@@ -1197,7 +1194,8 @@
$fieldType = ArrayHelper::get($rawField, 'element');
$rawField = apply_filters('fluentform/rendering_field_data_' . $fieldType, $rawField, $form);
$options = [];
- if ("net_promoter_score" === $fieldType) {
+ $otherPrefix = '';
+ if ('net_promoter_score' === $fieldType) {
$options = array_flip(ArrayHelper::get($rawField, 'options', []));
} elseif ('ratings' == $fieldType) {
$options = array_keys(ArrayHelper::get($rawField, 'options', []));
@@ -1222,14 +1220,15 @@
}
$options = array_column(self::flattenAdvancedOptions($formattedOptions), 'value');
-
+
// Add field-specific __ff_other__ to options if "Other" option is enabled
if (in_array($fieldType, ['input_checkbox', 'input_radio']) &&
ArrayHelper::get($rawField, 'settings.enable_other_option') === 'yes') {
$fieldName = sanitize_key(str_replace(['[', ']'], '', ArrayHelper::get($rawField, 'attributes.name', '')));
$options[] = '__ff_other_' . $fieldName . '__';
+ $otherPrefix = static::getOtherOptionValuePrefix($rawField);
}
- } elseif ("dynamic_field" == $fieldType) {
+ } elseif ('dynamic_field' == $fieldType) {
$dynamicFetchValue = 'yes' == ArrayHelper::get($rawField, 'settings.dynamic_fetch');
if ($dynamicFetchValue) {
$rawField = apply_filters('fluentform/dynamic_field_re_fetch_result_and_resolve_value', $rawField);
@@ -1262,11 +1261,11 @@
}
$filteredValues = array_values(array_filter(array_map('sanitize_text_field', $inputValue), function ($value) {
- return $value !== '';
+ return '' !== $value;
}));
$normalizedOptions = array_values(array_filter(array_map('sanitize_text_field', $options), function ($value) {
- return $value !== '';
+ return '' !== $value;
}));
sort($filteredValues);
@@ -1285,28 +1284,26 @@
case 'input_checkbox':
case 'multi_select':
case 'dynamic_field_options':
-
$skipValidationInputsWithOptions = apply_filters('fluentform/skip_validation_inputs_with_options', false, $fieldType, $form, $formData);
if ($skipValidationInputsWithOptions) {
break;
}
if (is_array($inputValue)) {
- // Handle field-specific "Other" options for checkboxes
- $filteredValues = array_filter($inputValue, function($value) {
- // Skip field-specific other values and processed other values
+ // Skip "Other" values — raw, localized or legacy English prefix
+ $filteredValues = array_filter($inputValue, function ($value) use ($otherPrefix) {
return !preg_match('/^__ff_other_.*__$/', $value) &&
- !preg_match('/^Other:s/', $value);
+ !preg_match('/^Other:s/', $value) &&
+ !($otherPrefix && 0 === strpos($value, $otherPrefix));
});
$isValid = array_diff($filteredValues, $options);
$isValid = empty($isValid);
+ } elseif (preg_match('/^__ff_other_.*__$/', $inputValue) ||
+ preg_match('/^Other:s/', $inputValue) ||
+ ($otherPrefix && 0 === strpos($inputValue, $otherPrefix))) {
+ // Accept "Other" values — raw, localized or legacy English prefix
+ $isValid = true;
} else {
- // Handle field-specific "Other" option for single values
- if (preg_match('/^__ff_other_.*__$/', $inputValue) ||
- preg_match('/^Other:s/', $inputValue)) {
- $isValid = true;
- } else {
- $isValid = in_array($inputValue, $options);
- }
+ $isValid = in_array($inputValue, $options);
}
break;
case 'input_number':
@@ -1370,6 +1367,32 @@
return $error;
}
+ /**
+ * Prefix used to store a checkable field's "Other" option value,
+ * built from the field's own (translated) label. Pass $form to run
+ * the field through the rendering filter (translation plugins) first.
+ *
+ * @param array $rawField
+ * @param object|null $form
+ * @return string
+ */
+ public static function getOtherOptionValuePrefix($rawField, $form = null)
+ {
+ $fieldType = ArrayHelper::get($rawField, 'element');
+ if ($form && $fieldType) {
+ $rawField = apply_filters('fluentform/rendering_field_data_' . $fieldType, $rawField, $form);
+ }
+
+ $label = trim((string) ArrayHelper::get($rawField, 'settings.other_option_label'));
+
+ if ('' === $label) {
+ $label = __('Other', 'fluentform');
+ }
+
+ // Avoid "::" when the label already ends with a colon
+ return ':' === substr($label, -1) ? $label . ' ' : $label . ': ';
+ }
+
public static function getWhiteListedFields($formId)
{
$whiteListedFields = [
@@ -1385,7 +1408,7 @@
'__square_payment_method_id',
'__square_verify_buyer_id',
'ct_bot_detector_event_token',
- 'ff_ct_form_load_time'
+ 'ff_ct_form_load_time',
];
return apply_filters('fluentform/white_listed_fields', $whiteListedFields, $formId);
@@ -1393,6 +1416,7 @@
/**
* Shortcode parse on validation message
+ *
* @param string $message
* @param object $form
* @param string $fieldName
@@ -1401,7 +1425,7 @@
public static function shortCodeParseOnValidationMessage($message, $form, $fieldName)
{
// Return early if form is null to prevent errors
- if ($form === null) {
+ if (null === $form) {
return $message;
}
@@ -1409,7 +1433,7 @@
// Add 'current_field' name as data array to resolve {labels.current_field} shortcode if it has
return ShortCodeParser::parse(
$message,
- (object)['response' => "", 'form_id' => $form->id],
+ (object) ['response' => '', 'form_id' => $form->id],
['current_field' => $fieldName],
$form
);
@@ -1565,7 +1589,7 @@
'HTTP_X_COUNTRY',
'X-Country',
'HTTP_X_COUNTRY_ISO',
- 'X-Country-ISO'
+ 'X-Country-ISO',
];
foreach ($headers as $header) {
@@ -1573,8 +1597,8 @@
if (isset($_SERVER[$header])) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- Country code from CDN/proxy header, validated below
$code = trim(sanitize_text_field(wp_unslash($_SERVER[$header])));
- } // Try with HTTP_ prefix if not already present
- elseif (strpos($header, 'HTTP_') !== 0) {
+ } elseif (strpos($header, 'HTTP_') !== 0) {
+ // Try with HTTP_ prefix if not already present
$httpHeader = 'HTTP_' . str_replace('-', '_', strtoupper($header));
if (isset($_SERVER[$httpHeader])) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- Country code from CDN/proxy header, validated below
@@ -1587,7 +1611,7 @@
}
// Basic validation - should be 2-letter country code
- if (!empty($code) && is_string($code) && strlen($code) === 2 && ctype_alpha($code) && $code !== 'XX') {
+ if (!empty($code) && is_string($code) && 2 === strlen($code) && ctype_alpha($code) && 'XX' !== $code) {
return strtoupper($code);
}
}
@@ -1597,6 +1621,7 @@
/**
* Fixes PHP Object Injection Vulnerability
+ *
* @param $data
* @return mixed
*/
@@ -1608,10 +1633,11 @@
return $data;
}
- /**
- * If elementor editor is open
- * @return bool
- */
+ /**
+ * If elementor editor is open
+ *
+ * @return bool
+ */
public static function isElementorEditor()
{
return defined('ELEMENTOR_VERSION') &&
@@ -1623,6 +1649,7 @@
/**
* Check if we're in block editor context (Site Editor, Template Editor, or Post/Page Editor)
* Covers all Gutenberg block editor contexts including mobile/tablet preview iframes
+ *
* @return bool
*/
public static function isSiteEditor()
@@ -1653,8 +1680,8 @@
$request_uri = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : '';
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Checking block editor context
return isset( $_GET['_wp-find-template'] ) ||
- strpos( $request_uri, 'site-editor.php' ) !== false ||
+ strpos( $request_uri, 'site-editor.php' ) !== false ||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Checking REST API context
- (defined('REST_REQUEST') && REST_REQUEST && !empty($_REQUEST['context']) && $_REQUEST['context'] === 'edit');
+ (defined('REST_REQUEST') && REST_REQUEST && !empty($_REQUEST['context']) && 'edit' === $_REQUEST['context']);
}
}
--- a/fluentform/app/Modules/Acl/Acl.php
+++ b/fluentform/app/Modules/Acl/Acl.php
@@ -13,7 +13,7 @@
public static function normalizeFormId($formId)
{
- if ($formId === null || $formId === false || $formId === '') {
+ if (null === $formId || false === $formId || '' === $formId) {
return null;
}
@@ -62,11 +62,11 @@
'fluentform_settings_manager',
'fluentform_full_access',
];
-
+
$data = apply_filters_deprecated(
'fluentform_permission_set',
[
- $data
+ $data,
],
FLUENTFORM_FRAMEWORK_UPGRADE,
'fluentform/permission_set',
@@ -165,7 +165,8 @@
return true;
}
- $grantedRole = static::getCurrentUserCapability();
+ // Skip the role fallback for explicit managers, else a limited manager escalates.
+ $grantedRole = self::isExplicitManager() ? false : static::getCurrentUserCapability();
foreach ((array) $permissions as $permission) {
$allowed = current_user_can($permission);
@@ -190,13 +191,40 @@
return current_user_can('fluentform_full_access') || current_user_can('manage_options');
}
+ // Is the CURRENT user a Manager added by name (per-user), not just someone riding a delegated role?
+ private static function isExplicitManager()
+ {
+ $userId = get_current_user_id();
+
+ return (bool) ($userId && self::userHasDirectGrant($userId, wp_get_current_user()));
+ }
+
+ // "Direct grant" = permissions attached to the USER themselves (per-user Manager),
+ // as opposed to access inherited from a delegated WordPress role.
+ private static function userHasDirectGrant($userId, $user)
+ {
+ // Flag set when an admin adds the user via Settings -> Managers.
+ if (get_user_meta($userId, '_fluent_forms_has_role', true)) {
+ return true;
+ }
+
+ // Legacy fallback: caps stored on the user itself ($user->caps), not merged in from a role.
+ foreach (static::getPermissionSet() as $permission) {
+ if ($user && !empty($user->caps[$permission])) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
private static function filterPermissionCheck($permission, $allowed, $formId)
{
$allowed = apply_filters_deprecated(
'fluentform_verify_user_permission_' . $permission,
[
$allowed,
- $formId
+ $formId,
],
FLUENTFORM_FRAMEWORK_UPGRADE,
'fluentform/verify_user_permission_' . $permission,
@@ -351,16 +379,20 @@
$isSuperMan = static::isSuperMan($user);
$capability = static::findUserCapability($user);
- if ($isSuperMan || $capability) {
- if ($isSuperMan) {
- // $permissionSet[] = 'administrator';
- }
+ $isManager = self::userHasDirectGrant($user->ID, $user);
+ if ($isSuperMan) {
return $permissionSet;
}
$userPermissions = array_values(array_intersect(array_keys($user->allcaps), $permissionSet));
+ // Delegated-role users still return before the filter (unchanged boundary);
+ // a manager just reports their own scoped caps instead of the full set.
+ if ($capability) {
+ return $isManager ? $userPermissions : $permissionSet;
+ }
+
return apply_filters('fluentform/current_user_permissions', $userPermissions);
}
--- a/fluentform/app/Modules/Ai/AiFormBuilder.php
+++ b/fluentform/app/Modules/Ai/AiFormBuilder.php
@@ -109,7 +109,8 @@
'site_locale' => determine_locale(),
'has_pro' => Helper::hasPro(),
'has_payment' => $paymentSetting['status'] == 'yes',
- 'request_id' => uniqid('ff_ai_')
+ 'request_id' => uniqid('ff_ai_'),
+ 'save_usage' => apply_filters('fluentform/ai_save_usage', true),
];
$result = (new FluentFormAIAPI())->makeRequest($queryArgs);
--- a/fluentform/app/Modules/Payments/PaymentHandler.php
+++ b/fluentform/app/Modules/Payments/PaymentHandler.php
@@ -396,14 +396,18 @@
{
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Nonce verified by Acl::verify()
$route = isset($_REQUEST['route']) ? sanitize_text_field(wp_unslash($_REQUEST['route'])) : '';
- $formScopedRoutes = [
- 'get_form_settings',
- 'save_form_settings',
+ $paymentMutationRoutes = [
'update_transaction',
'cancel_subscription'
];
+ $formSettingRoutes = [
+ 'get_form_settings',
+ 'save_form_settings'
+ ];
- if (in_array($route, $formScopedRoutes, true)) {
+ if (in_array($route, $paymentMutationRoutes, true)) {
+ Acl::verify('fluentform_manage_payments', $this->resolveRouteFormId($route));
+ } elseif (in_array($route, $formSettingRoutes, true)) {
Acl::verify('fluentform_forms_manager', $this->resolveRouteFormId($route));
} else {
Acl::verify('fluentform_settings_manager');
--- a/fluentform/app/Services/Form/SubmissionHandlerService.php
+++ b/fluentform/app/Services/Form/SubmissionHandlerService.php
@@ -35,6 +35,7 @@
/**
* Form Submission
+ *
* @param $formDataRaw
* @param $formId
* @return array
@@ -53,6 +54,8 @@
}
/**
+ * Prepare the form and validated form data for submission handling.
+ *
* @throws ValidationException
*/
protected function prepareHandler($formId, $formDataRaw)
@@ -72,13 +75,16 @@
foreach ($formDataRaw as $name => $input) {
if (is_array($input)) {
$formDataRaw[$name] = array_filter($input, function ($value) {
- return $value !== null && $value !== false && $value !== '';
+ return null !== $value && false !== $value && '' !== $value;
});
}
// Process "Other" options for checkboxes and radio fields
if (strpos($name, '__ff_other_input__') !== false && !empty($input)) {
$fieldName = str_replace('__ff_other_input__', '', $name);
+ // Store with the field's own (translated) "Other" label as prefix
+ $rawField = Arr::get(FormFieldsParser::getInputs($this->form, ['raw']), $fieldName . '.raw', []);
+ $otherPrefix = Helper::getOtherOptionValuePrefix($rawField, $this->form);
// Handle checkbox fields (array values)
if (isset($formDataRaw[$fieldName]) && is_array($formDataRaw[$fieldName])) {
@@ -89,13 +95,13 @@
$key = array_search($otherValue, $selectedValues);
- if ($key !== false) {
- $selectedValues[$key] = 'Other: ' . sanitize_text_field($input);
+ if (false !== $key) {
+ $selectedValues[$key] = $otherPrefix . sanitize_text_field($input);
$formDataRaw[$fieldName] = $selectedValues;
}
- } // Handle radio fields (single value)
- elseif (isset($formDataRaw[$fieldName]) && $formDataRaw[$fieldName] === '__ff_other_' . $fieldName . '__') {
- $formDataRaw[$fieldName] = 'Other: ' . sanitize_text_field($input);
+ } elseif (isset($formDataRaw[$fieldName]) && '__ff_other_' . $fieldName . '__' === $formDataRaw[$fieldName]) {
+ // Handle radio fields (single value)
+ $formDataRaw[$fieldName] = $otherPrefix . sanitize_text_field($input);
}
unset($formDataRaw[$name]);
@@ -124,6 +130,7 @@
/**
* Prepare the data to be inserted to the database.
+ *
* @param boolean $formData
* @return array
*/
@@ -146,7 +153,7 @@
[
$formData,
$formId,
- $inputConfigs
+ $inputConfigs,
],
FLUENTFORM_FRAMEWORK_UPGRADE,
'fluentform/insert_response_data',
@@ -160,7 +167,7 @@
'fluentform_disable_ip_logging',
[
false,
- $formId
+ $formId,
],
FLUENTFORM_FRAMEWORK_UPGRADE,
'fluentform/disable_ip_logging',
@@ -175,7 +182,7 @@
$response = [
'form_id' => $formId,
'serial_number' => $serialNumber,
- 'response' => json_encode($this->formData, JSON_UNESCAPED_UNICODE),
+ 'response' => wp_json_encode($this->formData, JSON_UNESCAPED_UNICODE),
'source_url' => site_url(Arr::get($formData, '_wp_http_referer')),
'user_id' => get_current_user_id(),
'browser' => $browser->getBrowser(),
@@ -189,7 +196,7 @@
$response = apply_filters_deprecated(
'fluentform_filter_insert_data',
[
- $response
+ $response,
],
FLUENTFORM_FRAMEWORK_UPGRADE,
'fluentform/filter_insert_data',
@@ -205,10 +212,10 @@
$formData = isset($this->formData) ? $this->formData : $formData;
do_action_deprecated(
'fluentform_before_form_actions_processing', [
- $insertId,
- $this->formData,
- $form
- ],
+ $insertId,
+ $this->formData,
+ $form,
+ ],
FLUENTFORM_FRAMEWORK_UPGRADE,
'fluentform/before_form_actions_processing',
'Use fluentform/before_form_actions_processing instead of fluentform_before_form_actions_processing.'
@@ -237,10 +244,10 @@
do_action_deprecated(
'fluentform_submission_inserted_' . $form->type . '_form', [
- $insertId,
- $formData,
- $form
- ],
+ $insertId,
+ $formData,
+ $form,
+ ],
FLUENTFORM_FRAMEWORK_UPGRADE,
'fluentform/submission_inserted_' . $form->type . '_form',
'Use fluentform/submission_inserted_' . $form->type . '_form instead of fluentform_submission_inserted_' . $form->type . '_form'
@@ -261,10 +268,10 @@
do_action_deprecated(
'fluentform_before_submission_confirmation', [
- $insertId,
- $formData,
- $form
- ],
+ $insertId,
+ $formData,
+ $form,
+ ],
FLUENTFORM_FRAMEWORK_UPGRADE,
'fluentform/before_submission_confirmation',
'Use fluentform/before_submission_confirmation instead of fluentform_before_submission_confirmation.'
@@ -281,6 +288,7 @@
/**
* Return Formatted Response Data
+ *
* @param $insertId
* @param $form
* @param $formData
@@ -298,7 +306,7 @@
[
$confirmation,
$formData,
- $form
+ $form,
],
FLUENTFORM_FRAMEWORK_UPGRADE,
'fluentform/form_submission_confirmation',
@@ -321,7 +329,7 @@
$confirmation['messageToShow'],
$insertId,
$formData,
- $form
+ $form,
],
FLUENTFORM_FRAMEWORK_UPGRADE,
'fluentform/submission_message_parse',
@@ -361,7 +369,7 @@
}
$parseUrl = apply_filters_deprecated('fluentform_will_parse_url_value', [
true,
- $form
+ $form,
],
FLUENTFORM_FRAMEWORK_UPGRADE,
'fluentform/will_parse_url_value',
@@ -431,7 +439,7 @@
$form,
$confirmation,
$insertId,
- $formData
+ $formData,
],
FLUENTFORM_FRAMEWORK_UPGRADE,
'fluentform/submission_confirmation',
@@ -512,6 +520,7 @@
/**
* Validates Submission
+ *
* @throws ValidationException
*/
private function handleValidation()
@@ -559,7 +568,7 @@
[
$insertData,
$formDataRaw,
- $this->form
+ $this->form,
],
FLUENTFORM_FRAMEWORK_UPGRADE,
'fluentform/before_insert_submission',
@@ -574,7 +583,7 @@
[
$insertData,
$formDataRaw,
- $this->form
+ $this->form,
],
FLUENTFORM_FRAMEWORK_UPGRADE,
'fluentform/before_insert_payment_form',
@@ -616,8 +625,7 @@
'component' => $type . ' Integration',
'status' => 'info',
'title' => __('Skip Submission Processing', 'fluentform'),
- 'description' => __('Submission marked as spammed. And skip all actions processing', 'fluentform')
+ 'description' => __('Submission marked as spammed. And skip all actions processing', 'fluentform'),
]);
}
}
-
--- a/fluentform/app/Services/Form/Updater.php
+++ b/fluentform/app/Services/Form/Updater.php
@@ -14,11 +14,11 @@
{
public function update($attributes = [])
{
- $formId = (int)Arr::get($attributes, 'form_id');
+ $formId = (int) Arr::get($attributes, 'form_id');
$formFields = Arr::get($attributes, 'formFields');
$status = sanitize_text_field(Arr::get($attributes, 'status', 'published'));
$title = sanitize_text_field(Arr::get($attributes, 'title'));
-
+
$this->validate([
'title' => $title,
'formFields' => $formFields,
@@ -41,7 +41,7 @@
'fluentform_form_fields_update',
[
$formFields,
- $formId
+ $formId,
],
FLUENTFORM_FRAMEWORK_UPGRADE,
'fluentform/form_fields_update',
@@ -52,10 +52,11 @@
$data['form_fields'] = $formFields;
/**
* Fires before a Form is updated.
+ *
* @since 5.2.1
*/
do_action('fluentform/before_updating_form', $form, $data);
-
+
$form->fill($data);
if (FormFieldsParser::hasPaymentFields($form)) {
@@ -65,7 +66,7 @@
}
$this->updatePrimaryEmail($form);
-
+
}
$form->fill($data)->save();
@@ -93,7 +94,8 @@
throw new Exception(
sprintf(
- __('Ranking field %s has duplicate option values. Please make each option value unique.', 'fluentform'),
+ // translators: %s is the ranking field name(s) with duplicate option values.
+ esc_html__('Ranking field %s has duplicate option values. Please make each option value unique.', 'fluentform'),
esc_html($duplicateRankingFields)
)
);
@@ -110,7 +112,7 @@
if (fluentformCanUnfilteredHTML()) {
return $formFields;
}
-
+
$fieldsArray = json_decode($formFields, true);
if (isset($fieldsArray['submitButton'])) {
@@ -148,7 +150,7 @@
];
$attributesKeys = array_keys($attributesMap);
-
+
$settingsMap = [
'container_class' => 'sanitize_text_field',
'label' => 'fluentform_sanitize_html',
@@ -169,6 +171,7 @@
'description' => 'fluentform_sanitize_html',
'grid_columns' => [Helper::class, 'sanitizeArrayKeysAndValues'],
'grid_rows' => [Helper::class, 'sanitizeArrayKeysAndValues'],
+ 'date_config' => 'fluentform_sanitize_json_object',
'enable_crop' => 'sanitize_text_field',
'crop_mode' => 'sanitize_text_field',
'crop_ratio' => 'sanitize_text_field',
@@ -176,7 +179,6 @@
'crop_height' => 'absint',
'enforce_image_dimensions' => 'sanitize_text_field',
];
-
$settingsKeys = array_keys($settingsMap);
@@ -186,10 +188,10 @@
'alt_text' => 'sanitize_text_field',
];
$stylePrefKeys = array_keys($stylePrefMap);
-
+
foreach ($fields as $fieldIndex => &$field) {
$element = Arr::get($field, 'element');
-
+
if ('container' == $element) {
$columns = $field['columns'];
foreach ($columns as $columnIndex => $column) {
@@ -245,7 +247,6 @@
}
}
}
-
if (!empty($field['attributes'])) {
$attributes = array_filter(Arr::only($field['attributes'], $attributesKeys));
@@ -268,7 +269,7 @@
$fields[$fieldIndex]['fields'] = $this->sanitizeFieldMaps($field['fields']);
continue;
}
-
+
if (!empty($field['style_pref'])) {
$settings = array_filter(Arr::only($field['style_pref'], $stylePrefKeys));
@@ -276,7 +277,7 @@
$fields[$fieldIndex]['style_pref'][$key] = call_user_func($stylePrefMap[$key], $value);
}
}
-
+
$validationRules = Arr::get($field, 'settings.validation_rules');
if (!empty($validationRules)) {
foreach ($validationRules as $key => $rule) {
@@ -288,7 +289,7 @@
}
}
}
-
+
return $fields;
}
@@ -305,7 +306,7 @@
FormMeta::persist($form->id, '_primary_email_field', $emailInputName);
}
-
+
private function sanitizeCustomSubmit($fields)
{
$customSubmitSanitizationMap = [
@@ -314,14 +315,14 @@
'borderColor' => [$this, 'sanitizeRgbColor'],
'color' => [$this, 'sanitizeRgbColor'],
'borderRadius' => 'sanitize_text_field',
- 'minWidth' => [$this, 'sanitizeMinWidth']
+ 'minWidth' => [$this, 'sanitizeMinWidth'],
],
'normal_styles' => [
'backgroundColor' => [$this, 'sanitizeRgbColor'],
'borderColor' => [$this, 'sanitizeRgbColor'],
'color' => [$this, 'sanitizeRgbColor'],
'borderRadius' => 'sanitize_text_field',
- 'minWidth' => [$this, 'sanitizeMinWidth']
+ 'minWidth' => [$this, 'sanitizeMinWidth'],
],
'button_ui' => [
'type' => 'sanitize_text_field',
@@ -331,7 +332,7 @@
];
foreach ($fields as $fieldIndex => $field) {
$element = Arr::get($field, 'element');
-
+
if ('custom_submit_button' == $element) {
$styleAttr = ['hover_styles', 'normal_styles', 'button_ui'];
foreach ($styleAttr as $attr) {
@@ -344,8 +345,7 @@
}
}
}
- }
- elseif ('container' == $element) {
+ } elseif ('container' == $element) {
$columns = $field['columns'];
foreach ($columns as $columnIndex => $column) {
$fields[$fieldIndex]['columns'][$columnIndex]['fields'] = $this->sanitizeCustomSubmit($column['fields']);
@@ -369,7 +369,7 @@
foreach ($stepWrapper as $fieldIndex => $field) {
$element = Arr::get($field, 'element');
- if ($element === 'step_start' || $element === 'step_end') {
+ if ('step_start' === $element || 'step_end' === $element) {
if (!empty($field['settings']['step_titles']) && is_array($field['settings']['step_titles'])) {
foreach ($field['settings']['step_titles'] as $index => $title) {
$field['settings']['step_titles'][$index] = fluentform_sanitize_html($title);
@@ -393,7 +393,7 @@
}
}
}
-
+
if (!empty($field['attributes']['class'])) {
$field['attributes']['class'] = sanitize_text_field($field['attributes']['class']);
}
@@ -402,7 +402,7 @@
}
}
- if ($element === 'step_start' && isset($field['fields'])) {
+ if ('step_start' === $element && isset($field['fields'])) {
$field['fields'] = $this->sanitizeStepsWrapper($field['fields']);
}
@@ -419,7 +419,7 @@
}
return '';
}
-
+
public function sanitizeRgbColor($value) {
if (preg_match('/^rgba?((d{1,3}s*,s*){2,3}(0|1|0?.d+))$/', $value)) {
return $value;
--- a/fluentform/app/Services/FormBuilder/Components/DateTime.php
+++ b/fluentform/app/Services/FormBuilder/Components/DateTime.php
@@ -2,7 +2,7 @@
namespace FluentFormAppServicesFormBuilderComponents;
-defined('ABSPATH') or die;
+defined('ABSPATH') || die;
use FluentFormAppHelpersHelper;
use FluentFormFrameworkHelpersArrayHelper;
@@ -24,7 +24,7 @@
'fluentform_rendering_field_data_' . $elementName,
[
$data,
- $form
+ $form,
],
FLUENTFORM_FRAMEWORK_UPGRADE,
'fluentform/rendering_field_data_' . $elementName,
@@ -54,11 +54,11 @@
}
$id = $data['attributes']['id'];
- $ariaLabel = esc_html__(' Use arrow keys to navigate dates. Press enter to select a date.', 'fluentform') ;
- $label = ArrayHelper::get($data,'settings.label');
- $elMarkup = "<input aria-label='".$label.$ariaLabel."' aria-haspopup='dialog' data-type-datepicker data-format='" . esc_attr($dateFormat) . "' " . $atts . " aria-invalid='false' aria-required={$ariaRequired}>"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $atts is escaped before being passed in.
+ $ariaLabel = esc_html__(' Use arrow keys to navigate dates. Press enter to select a date.', 'fluentform');
+ $label = ArrayHelper::get($data, 'settings.label');
+ $elMarkup = "<input aria-label='" . $label . $ariaLabel . "' aria-haspopup='dialog' data-type-datepicker data-format='" . esc_attr($dateFormat) . "' " . $atts . " aria-invalid='false' aria-required={$ariaRequired}>"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $atts is escaped before being passed in.
$config = $this->getDateFormatConfigJSON($data['settings'], $form);
- $customConfig = $this->getCustomConfig($data['settings']);
+ $customConfig = $this->getCustomConfig($data['settings'], $form);
$this->loadToFooter($config, $customConfig, $form, $id);
$html = $this->buildElementMarkup($elMarkup, $data, $form);
@@ -67,7 +67,7 @@
[
$html,
$data,
- $form
+ $form,
],
FLUENTFORM_FRAMEWORK_UPGRADE,
'fluentform/rendering_field_html_' . $elementName,
@@ -126,7 +126,7 @@
$config = apply_filters('fluentform/frontend_date_format', [
'dateFormat' => $dateFormat,
- 'ariaDateFormat' =>"F j, Y",
+ 'ariaDateFormat' =>'F j, Y',
'enableTime' => $hasTime,
'noCalendar' => ! $this->hasDate($dateFormat),
'disableMobile' => true,
@@ -136,15 +136,19 @@
return json_encode($config, JSON_FORCE_OBJECT);
}
- public function getCustomConfig($settings)
+ public function getCustomConfig($settings, $form = null)
{
- $customConfigObject = trim(ArrayHelper::get($settings, 'date_config'));
+ $customConfigObject = fluentform_sanitize_json_object(
+ (string) ArrayHelper::get($settings, 'date_config')
+ );
- if (! $customConfigObject || '{' != substr($customConfigObject, 0, 1) || '}' != substr($customConfigObject, -1)) {
- $customConfigObject = '{}';
- }
+ $customConfigObject = '' !== $customConfigObject ? $customConfigObject : '{}';
- return $customConfigObject;
+ // The stored field value is always sanitised to a data-only JSON object
+ // (functions/expressions are stripped — they are the XSS vector for
+ // lower-privilege editors). Developers who need flatpickr callbacks
+ // supply them here from trusted server-side code, never via the setting.
+ return apply_filters('fluentform/date_time_custom_config', $customConfigObject, $settings, $form);
}
private function loadToFooter($config, $customConfigObject, $form, $id)
--- a/fluentform/app/Services/Manager/ManagerService.php
+++ b/fluentform/app/Services/Manager/ManagerService.php
@@ -14,7 +14,7 @@
{
$limit = Arr::get($attributes, 'per_page', 10);
$page = Arr::get($attributes, 'page', 1);
- $offset = $page == 1 ? 0 : ($page - 1) * $limit;
+ $offset = 1 == $page ? 0 : ($page - 1) * $limit;
$query = new WP_User_Query([
'meta_key' => '_fluent_forms_has_role',
@@ -55,25 +55,37 @@
'forms' => $forms,
]);
}
-
+
public function addManager($attributes = [])
{
$manager = Arr::get($attributes, 'manager');
-
+
$this->validate($manager);
-
+
$permissions = Arr::get($manager, 'permissions', []);
-
+
+ // Defense in depth: only a full WordPress administrator may grant the
+ // crown-jewel `fluentform_full_access` permission. A holder of that ACL
+ // permission is not necessarily a WP admin (managers can be delegated),
+ // so without this a compromised full-access session — e.g. a script
+ // running via XSS in a privileged session — could mint new full-access
+ // managers and escalate. Lesser permission grants are unaffected.
+ if (in_array('fluentform_full_access', (array) $permissions, true) && !current_user_can('manage_options')) {
+ $permissions = array_values(array_filter((array) $permissions, function ($permission) {
+ return 'fluentform_full_access' !== $permission;
+ }));
+ }
+
$user = get_user_by('email', $manager['email']);
-
+
if (!$user) {
throw new ValidationException('', 0, null, ['message' => 'Please Provide Valid Email']);
}
-
+
Acl::attachPermissions($user, $permissions);
-
+
update_user_meta($user->ID, '_fluent_forms_has_role', 1);
-
+
$hasSpecificFormsPermission = 'yes' === Arr::get($manager, 'has_specific_forms_permission');
$allowedForms = array_values(array_filter(array_map('intval', (array) Arr::get($manager, 'forms', []))));
@@ -92,83 +104,83 @@
'first_name' => $user->first_name,
'last_name' => $user->last_name,
'email' => $user->user_email,
- 'permissions' => Acl::getUserPermissions($user)
+ 'permissions' => Acl::getUserPermissions($user),
];
-
+
return ([
'message' => __('Manager has been saved.', 'fluentform'),
- 'manager' => $updatedUser
+ 'manager' => $updatedUser,
]);
}
-
+
public function removeManager($attributes = [])
{
$userID = intval(Arr::get($attributes, 'id'));
$user = get_user_by('ID', $userID);
-
+
if (!$user) {
return ([
'message' => __('Associate user could not be found', 'fluentform'),
]);
}
-
+
Acl::attachPermissions($user, []);
-
+
delete_user_meta($user->ID, '_fluent_forms_has_role');
-
+
$deletedUser = [
'id' => $user->ID,
'first_name' => $user->first_name,
'last_name' => $user->last_name,
'email' => $user->user_email,
- 'permissions' => Acl::getUserPermissions($user)
+ 'permissions' => Acl::getUserPermissions($user),
];
-
+
return ([
'message' => __('Manager has been removed.', 'fluentform'),
- 'manager' => $deletedUser
+ 'manager' => $deletedUser,
]);
}
-
+
private function validate($manager)
{
$rules = [
'permissions' => 'required',
'email' => 'required|email',
];
-
+
$validatorInstance = new Validator();
$validator = $validatorInstance->make($manager, $rules);
-
+
$errors = null;
-
+
if ($validator->validate()->fails()) {
$errors = $validator->errors();
}
-
+
if (!isset($errors['email'])) {
$user = get_user_by('email', $manager['email']);
-
+
if (!$user) {
$errors['email'] = [
'no_user' => __('We could not found any user with this email.', 'fluentform'),
];
}
}
-
+
if (!isset($errors['permissions'])) {
$message = $this->dependencyValidate($manager['permissions']);
-
+
if ($message) {
$errors['permissions'] = [
'dependency' => $message,
];
}
}
-
+
if ($errors) {
// Escape all error messages before throwing
- $escapedErrors = array_map(function($errorMessages) {
+ $escapedErrors = array_map(function ($errorMessages) {
if (is_array($errorMessages)) {
return array_map('esc_html', $errorMessages);
}
@@ -181,22 +193,22 @@
]);
}
}
-
+
private function dependencyValidate($permissions)
{
$allPermissions = Acl::getReadablePermissions();
-
+
foreach ($permissions as $permission) {
$depends = Arr::get($allPermissions, $permission . '.depends', []);
-
+
if ($depends && $more = array_values(array_diff($depends, $permissions))) {
$message = $allPermissions[$permission]['title'] . ' requires permission: ';
-
+
foreach ($more as $i => $p) {
$joiner = $i ? ', ' : '';
$message = $message . $joiner . $allPermissions[$p]['title'];
}
-
+
return $message;
}
}
--- a/fluentform/boot/globals.php
+++ b/fluentform/boot/globals.php
@@ -440,6 +440,64 @@
return preg_replace('/</?script[^>]*>/is', '', $content);
}
+function fluentform_sanitize_json_object($value)
+{
+ if (!is_string($value) || '' === trim($value)) {
+ return '';
+ }
+
+ $value = trim($value);
+
+ $decoded = json_decode($value, true);
+
+ // Best-effort recovery of a pure-data JS-object literal (unquoted keys,
+ // single quotes, trailing commas) — the documented "JS object" format for
+ // the Date/Time field's advanced config. json_decode below is the security
+ // gate: any function or expression that survives normalisation is still not
+ // valid JSON, so it is rejected. Normalisation can only recover data, never
+ // execute or emit code.
+ if (JSON_ERROR_NONE !== json_last_error() || !is_array($decoded)) {
+ $decoded = json_decode(fluentform_js_object_to_json($value), true);
+ }
+
+ if (JSON_ERROR_NONE !== json_last_error() || !is_array($decoded)) {
+ return '';
+ }
+
+ if ([] === $decoded) {
+ return '{}';
+ }
+
+ // date_config must be an object; reject a top-level JSON array. Nested
+ // arrays (e.g. flatpickr `disable: [...]`) are preserved by not forcing
+ // JSON_FORCE_OBJECT recursively.
+ if (array_keys($decoded) === range(0, count($decoded) - 1)) {
+ return '';
+ }
+
+ return wp_json_encode($decoded);
+}
+
+function fluentform_js_object_to_json($value)
+{
+ // Single-quoted strings -> double-quoted (respecting escapes).
+ $value = preg_replace_callback(
+ "/'((?:\\.|[^'\\])*)'/s",
+ function ($m) {
+ return '"' . str_replace(['\'', '"'], [''', '\"'], $m[1]) . '"';
+ },
+ $value
+ );
+
+ // Quote unquoted object keys: `{ key:` / `, key:` -> `{ "key":`.
+ $value = preg_replace('/([{,]s*)([A-Za-z_$][A-Za-z0-9_$]*)(s*:)/', '$1"$2"$3', $value);
+
+ // Drop trailing commas before a closing brace/bracket.
+ $value = preg_replace('/,s*([}]])/', '$1', $value);
+
+ return $value;
+}
+
/**
* Sanitize inputs recursively.
*
--- a/fluentform/fluentform.php
+++ b/fluentform/fluentform.php
@@ -4,7 +4,7 @@
/**
* Plugin Name: Fluent Forms
* Description: Contact Form By Fluent Forms is the advanced Contact form plugin with drag and drop, multi column supported form builder plugin
- * Version: 6.2.5
+ * Version: 6.2.6
* Author: Contact Form - WPManageNinja LLC
* Author URI: https://fluentforms.com
* Plugin URI: https://wpmanageninja.com/wp-fluent-form/
@@ -17,7 +17,7 @@
defined('FLUENTFORM') or define('FLUENTFORM', true);
define('FLUENTFORM_DIR_PATH', plugin_dir_path(__FILE__));
define('FLUENTFORM_FRAMEWORK_UPGRADE', '4.3.22');
-defined('FLUENTFORM_VERSION') or define('FLUENTFORM_VERSION', '6.2.5');
+defined('FLUENTFORM_VERSION') or define('FLUENTFORM_VERSION', '6.2.6');
defined('FLUENTFORM_MINIMUM_PRO_VERSION') or define('FLUENTFORM_MINIMUM_PRO_VERSION', '6.0.0');
if (!defined('FLUENTFORM_HAS_NIA')) {