PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / 6.2.15
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder v6.2.15
6.2.15 6.2.14 6.2.13 6.2.12 6.2.10 6.2.11 6.2.9 6.2.8 6.2.7 6.2.6 6.2.5 6.2.4 6.2.3 6.2.2 3.6.22 3.6.31 3.6.40 3.6.41 3.6.42 3.6.50 3.6.51 3.6.60 3.6.61 3.6.62 3.6.64 All 197 releases
← All changes | app/Hooks/actions.php +131 -47 6.2.3 → 6.2.15 View file →
@@ -1,11 +1,12 @@
1 1 <?php
2 2
3 -defined('ABSPATH') or die;
3 +defined('ABSPATH') || die;
4 4
5 5 use FluentForm\App\Modules\Component\Component;
6 6 use FluentForm\App\Modules\Acl\Acl;
7 7 use FluentForm\App\Helpers\Helper;
8 +use FluentForm\App\Services\FormBuilder\LegacyDateConfigDecoder;
8 9 use FluentForm\Framework\Helpers\ArrayHelper;
9 10
10 11 /**
11 12 * All registered action's handlers should be in app\Hooks\Handlers,
@@ -10,14 +11,16 @@
10 11 /**
11 12 * All registered action's handlers should be in app\Hooks\Handlers,
12 13 * addAction is similar to add_action and addCustomAction is just a
13 14 * wrapper over add_action which will add a prefix to the hook name
14 - * using the plugin slug to make it unique in all wordpress plugins,
15 + * using the plugin slug to make it unique in all WordPress plugins,
15 16 * ex: $app->addCustomAction('foo', ['FooHandler', 'handleFoo']) is
16 17 * equivalent to add_action('slug-foo', ['FooHandler', 'handleFoo']).
17 18 */
18 19
19 20 /**
21 + * Application instance.
22 + *
20 23 * @var $app FluentForm\Framework\Foundation\Application
21 24 */
22 25
23 26 // From MenuProvider.php
@@ -69,8 +72,23 @@
69 72 (new \FluentForm\App\Modules\Renderer\GlobalSettings\Settings($app))->render();
70 73 }
71 74 );
72 75
76 +/**
77 + * Pro 6.2.13+ supplies the REST API used by Free's Vue license screen. Older
78 + * Pro versions keep rendering their PHP page through this component action.
79 + */
80 +$app->addAction(
81 + 'fluentform/global_settings_component_license_page',
82 + function () use ($app) {
83 + if (!defined('FLUENTFORMPRO_VERSION') || version_compare(FLUENTFORMPRO_VERSION, '6.2.13', '<')) {
84 + return;
85 + }
86 +
87 + (new \FluentForm\App\Modules\Renderer\GlobalSettings\Settings($app))->render('license');
88 + }
89 +);
90 +
73 91 // Register DefaultStyleApplicator on init so it works for REST API requests too
74 92 add_action('init', function () {
75 93 new \FluentForm\App\Modules\Form\DefaultStyleApplicator();
76 94 }, 9);
@@ -150,9 +168,9 @@
150 168 'fluent_forms_docs',
151 169 'fluent_forms_all_entries',
152 170 'msformentries',
153 171 'fluent_forms_payment_entries',
154 - 'fluent_forms_reports'
172 + 'fluent_forms_reports',
155 173 ];
156 174
157 175 $page = wpFluentForm('request')->get('page');
158 176
@@ -159,8 +177,10 @@
159 177 if ($page && in_array($page, $disablePages)) {
160 178 remove_all_actions('admin_notices');
161 179 \FluentForm\App\Modules\Registerer\ReviewQuery::register();
162 180 \FluentForm\App\Modules\Registerer\MigrationNotice::register();
181 + \FluentForm\App\Modules\Registerer\StripeKeyNotice::register();
182 + \FluentForm\App\Modules\Registerer\CaptchaKeyNotice::register();
163 183 }
164 184 });
165 185
166 186 add_action('wp_print_scripts', function () {
@@ -171,9 +191,9 @@
171 191
172 192 $isSkip = apply_filters_deprecated(
173 193 'fluentform_skip_no_conflict',
174 194 [
175 - $isSkip
195 + $isSkip,
176 196 ],
177 197 FLUENTFORM_FRAMEWORK_UPGRADE,
178 198 'fluentform/skip_no_conflict',
179 199 'Use fluentform/skip_no_conflict instead of fluentform_skip_no_conflict.'
@@ -278,8 +298,50 @@
278 298 if (!isset($element['settings']['dynamic_default_value'])) {
279 299 $element['settings']['dynamic_default_value'] = '';
280 300 }
281 301
302 + // The editor only renders a rule the field already carries, so forms
303 + // built before selection limits existed need the keys backfilled.
304 + $isMultiSelect = 'select' == $upgradeElement
305 + && \FluentForm\Framework\Helpers\ArrayHelper::get($element, 'attributes.multiple');
306 +
307 + if ('input_checkbox' == $upgradeElement || $isMultiSelect) {
308 + $rules = \FluentForm\Framework\Helpers\ArrayHelper::get($element, 'settings.validation_rules', []);
309 +
310 + foreach (['max_selection', 'min_selection'] as $selectionRule) {
311 + if (isset($rules[$selectionRule])) {
312 + continue;
313 + }
314 +
315 + $globalMessage = \FluentForm\App\Helpers\Helper::getGlobalDefaultMessage($selectionRule);
316 +
317 + // Carry the legacy ceiling across, or the editor would show
318 + // "no limit" on a form that has one and drop it on save.
319 + $value = '';
320 + if ('max_selection' === $selectionRule && $isMultiSelect) {
321 + $value = \FluentForm\Framework\Helpers\ArrayHelper::get($element, 'settings.max_selection', '');
322 + }
323 +
324 + $rules[$selectionRule] = [
325 + 'value' => $value,
326 + 'message' => $globalMessage,
327 + 'global_message' => $globalMessage,
328 + 'global' => true,
329 + ];
330 + }
331 +
332 + // Key order is the panel's layout order. Rebuilt rather than
333 + // appended, so forms saved by an earlier build get it too.
334 + $ordered = [];
335 + foreach (['required', 'max_selection', 'min_selection'] as $key) {
336 + if (isset($rules[$key])) {
337 + $ordered[$key] = $rules[$key];
338 + }
339 + }
340 +
341 + $element['settings']['validation_rules'] = $ordered + $rules;
342 + }
343 +
282 344 if ('select_country' != $upgradeElement && !isset($element['settings']['randomize_options'])) {
283 345 $element['settings']['randomize_options'] = 'no';
284 346 }
285 347
@@ -312,10 +374,8 @@
312 374 if ('select' == $upgradeElement && !isset($element['settings']['enable_option_groups'])) {
313 375 $element['settings']['enable_option_groups'] = 'no';
314 376 }
315 377
316 -
317 -
318 378 return $element;
319 379 });
320 380 }
321 381
@@ -337,9 +397,9 @@
337 397 }
338 398 if (!isset($element['settings']['crop_mode'])) {
339 399 $element['settings']['crop_mode'] = (
340 400 isset($element['settings']['enforce_image_dimensions']) &&
341 - $element['settings']['enforce_image_dimensions'] === 'yes'
401 + 'yes' === $element['settings']['enforce_image_dimensions']
342 402 ) ? 'dimensions' : 'ratio';
343 403 }
344 404 if (!isset($element['settings']['crop_ratio'])) {
345 405 $element['settings']['crop_ratio'] = 'free';
@@ -356,15 +416,15 @@
356 416 }
357 417 return $element;
358 418 });
359 419 }
360 -
420 +
361 421 $prefixSuffixInputs = [
362 422 'textarea',
363 423 'input_url',
364 424 'input_password',
365 425 ];
366 -
426 +
367 427 foreach ($prefixSuffixInputs as $inputType) {
368 428 add_filter('fluentform/editor_init_element_' . $inputType, function ($item) {
369 429 if (!isset($item['settings']['prefix_label'])) {
370 430 $item['settings']['prefix_label'] = '';
@@ -374,9 +434,9 @@
374 434 }
375 435 return $item;
376 436 });
377 437 }
378 -
438 +
379 439 add_filter('fluentform/editor_init_element_gdpr_agreement', function ($element) {
380 440 if (!isset($element['settings']['required_field_message'])) {
381 441 $element['settings']['required_field_message'] = '';
382 442 }
@@ -400,8 +460,13 @@
400 460 add_filter('fluentform/editor_init_element_input_date', function ($item) {
401 461 if (!isset($item['settings']['date_config'])) {
402 462 $item['settings']['date_config'] = '';
403 463 }
464 + // Show the executable form of any legacy 6.2.7-6.2.12 tokens; only a trusted author's verbatim save persists it (restricted saves keep the stored value).
465 + $decoded = LegacyDateConfigDecoder::decode((string) $item['settings']['date_config']);
466 + if (null !== $decoded) {
467 + $item['settings']['date_config'] = $decoded;
468 + }
404 469 return $item;
405 470 });
406 471
407 472 add_filter('fluentform/editor_init_element_ratings', function ($item) {
@@ -427,9 +492,8 @@
427 492
428 493 return $item;
429 494 });
430 495
431 -
432 496 add_filter('fluentform/editor_init_element_container', function ($item) {
433 497 if (!isset($item['settings']['conditional_logics'])) {
434 498 $item['settings']['conditional_logics'] = [];
435 499 }
@@ -478,8 +542,17 @@
478 542
479 543 return $item;
480 544 });
481 545
546 + foreach (['input_text', 'input_email', 'textarea', 'input_number', 'select', 'input_url', 'input_password', 'input_date', 'input_name', 'address', 'phone'] as $autocompleteElement) {
547 + add_filter('fluentform/editor_init_element_' . $autocompleteElement, function ($item) {
548 + if (!isset($item['attributes']['autocomplete'])) {
549 + $item['attributes']['autocomplete'] = '';
550 + }
551 + return $item;
552 + });
553 + }
554 +
482 555 add_filter('fluentform/editor_init_element_input_mask', function ($item) {
483 556 if (!isset($item['settings']['mobile_keyboard_type'])) {
484 557 $item['settings']['mobile_keyboard_type'] = '';
485 558 }
@@ -501,9 +574,8 @@
501 574 }
502 575 return $item;
503 576 });
504 577
505 -
506 578 add_filter('fluentform/editor_init_element_input_text', function ($item) {
507 579 if (isset($item['attributes']['data-mask'])) {
508 580 if (!isset($item['settings']['data-mask-reverse'])) {
509 581 $item['settings']['data-mask-reverse'] = 'no';
@@ -530,9 +602,9 @@
530 602 });
531 603
532 604 if ($inputs = \FluentForm\App\Modules\Form\FormFieldsParser::getInputs($form, ['element'])) {
533 605 foreach ($inputs as $input) {
534 - add_filter('fluentform/editor_init_element_'. $input['element'], function ($field) {
606 + add_filter('fluentform/editor_init_element_' . $input['element'], function ($field) {
535 607 Helper::resolveValidationRulesGlobalOption($field);
536 608 return $field;
537 609 });
538 610 }
@@ -608,19 +680,19 @@
608 680 });
609 681
610 682 add_filter('fluentform/editor_init_element_gdpr_agreement', function ($item, $form) {
611 683 $isConversationalForm = Helper::isConversionForm($form->id);
612 -
684 +
613 685 if ($isConversationalForm) {
614 686 $item['settings']['tc_agree_text'] = __('I accept', 'fluentform');
615 687 }
616 -
688 +
617 689 return $item;
618 690 }, 10, 2);
619 691
620 692 add_filter('fluentform/editor_init_element_terms_and_condition', function ($item, $form) {
621 693 $isConversationalForm = Helper::isConversionForm($form->id);
622 -
694 +
623 695 if ($isConversationalForm) {
624 696 $item['settings']['hide_disagree'] = false;
625 697 }
626 698
@@ -669,11 +741,9 @@
669 741 }
670 742
671 743 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified by WordPress save_post action
672 744 $post_content = isset($_REQUEST['post_content']) ? wp_kses_post(wp_unslash($_REQUEST['post_content'])) : false;
673 - if ($post_content && is_string($post_content)) {
674 - // Already sanitized above
675 - } else {
745 + if (!$post_content || !is_string($post_content)) {
676 746 $post = get_post($post_id);
677 747 $post_content = $post->post_content;
678 748 }
679 749
@@ -759,9 +829,9 @@
759 829 );
760 830 $globalVars = [
761 831 'ajaxurl' => Helper::getAjaxUrl(),
762 832 'global_search_active' => apply_filters('fluentform/global_search_active', 'yes'),
763 - 'rest' => Helper::getRestInfo()
833 + 'rest' => Helper::getRestInfo(),
764 834 ];
765 835 if (Acl::hasAnyFormPermission()) {
766 836 $globalVars['fluent_forms_admin_nonce'] = wp_create_nonce('fluent_forms_admin_nonce');
767 837 }
@@ -767,9 +837,9 @@
767 837 }
768 838 wp_localize_script('fluent_forms_global', 'fluent_forms_global_var', $globalVars);
769 839 wp_enqueue_style('fluent-form-styles');
770 840 $form = wpFluent()->table('fluentform_forms')->find(intval($app->request->get('preview_id')));
771 - $postId = get_the_ID() ?: 0;
841 + $postId = get_the_ID() ? get_the_ID() : 0;
772 842
773 843 $loadPublicStyle = apply_filters_deprecated(
774 844 'fluentform_load_default_public',
775 845 [
@@ -774,9 +844,9 @@
774 844 'fluentform_load_default_public',
775 845 [
776 846 true,
777 847 $form,
778 - $postId
848 + $postId,
779 849 ],
780 850 FLUENTFORM_FRAMEWORK_UPGRADE,
781 851 'fluentform/load_default_public',
782 852 'Use fluentform/load_default_public instead of fluentform_load_default_public.'
@@ -796,9 +866,9 @@
796 866 true
797 867 );
798 868
799 869 wp_localize_script('fluentform-preview_app', 'fluent_preview_var', [
800 - 'i18n' => \FluentForm\App\Modules\Registerer\TranslationString::getPreviewI18n()
870 + 'i18n' => \FluentForm\App\Modules\Registerer\TranslationString::getPreviewI18n(),
801 871 ]);
802 872 }
803 873 });
804 874
@@ -834,9 +904,9 @@
834 904 'fluentform_api_success_log',
835 905 [
836 906 $isDev,
837 907 $form,
838 - $feed
908 + $feed,
839 909 ],
840 910 FLUENTFORM_FRAMEWORK_UPGRADE,
841 911 'fluentform/api_success_log',
842 912 'Use fluentform/api_success_log instead of fluentform_api_success_log.'
@@ -874,9 +944,9 @@
874 944 'fluentform_api_failed_log',
875 945 [
876 946 $isDev,
877 947 $form,
878 - $feed
948 + $feed,
879 949 ],
880 950 FLUENTFORM_FRAMEWORK_UPGRADE,
881 951 'fluentform/api_failed_log',
882 952 'Use fluentform/api_failed_log instead of fluentform_api_failed_log.'
@@ -971,11 +1041,23 @@
971 1041 $tokenBasedSpamProtection = new \FluentForm\App\Modules\Form\TokenBasedSpamProtection($app);
972 1042 $tokenBasedSpamProtection->verify($insertData, $requestData, $form->id);
973 1043 }, 9, 3);
974 1044
1045 +// The token-based spam check (FINDING-25) enforces on conversational forms too, but its ~1h TTL
1046 +// token cannot be refreshed by the conversational JS app — it bakes hidden inputs statically at
1047 +// render, so behind a full-page cache the token expires and rejects every legitimate submission.
1048 +// Disable ONLY the token for conversational forms, resolved from server-side form meta (never the
1049 +// client-supplied isFFConversational flag, which was the original bypass). The honeypot still applies.
1050 +$app->addFilter('fluentform/token_based_spam_protection_status', function ($status, $formId) {
1051 + if ($status && \FluentForm\App\Helpers\Helper::isConversionForm($formId)) {
1052 + return false;
1053 + }
1054 + return $status;
1055 +}, 10, 2);
1056 +
975 1057 // Maybe update current user allowed form ids,
976 1058 // if current user has specific form permission and capable to create form
977 -$app->addAction('fluentform/inserted_new_form', function ($formId){
1059 +$app->addAction('fluentform/inserted_new_form', function ($formId) {
978 1060 \FluentForm\App\Services\Manager\FormManagerService::maybeAddUserAllowedFormIds($formId);
979 1061 });
980 1062
981 1063 add_action('fluentform/log_data', function ($data) use ($app) {
@@ -1041,8 +1123,12 @@
1041 1123 if (!$note) {
1042 1124 $note = $status;
1043 1125 }
1044 1126
1127 + $note = is_scalar($note)
1128 + ? sanitize_text_field(wp_unslash((string) $note))
1129 + : sanitize_text_field((string) wp_json_encode($note));
1130 +
1045 1131 if (strlen($note) > 255) {
1046 1132 if (function_exists('mb_substr')) {
1047 1133 $note = mb_substr($note, 0, 251) . '...';
1048 1134 } else {
@@ -1072,8 +1158,12 @@
1072 1158 if (!$note) {
1073 1159 $note = $status;
1074 1160 }
1075 1161
1162 + $note = is_scalar($note)
1163 + ? sanitize_text_field(wp_unslash((string) $note))
1164 + : sanitize_text_field((string) wp_json_encode($note));
1165 +
1076 1166 if (strlen($note) > 255) {
1077 1167 if (function_exists('mb_substr')) {
1078 1168 $note = mb_substr($note, 0, 251) . '...';
1079 1169 } else {
@@ -1094,9 +1184,9 @@
1094 1184 $isTruncate = apply_filters_deprecated(
1095 1185 'fluentform_truncate_password_values',
1096 1186 [
1097 1187 true,
1098 - $form->id
1188 + $form->id,
1099 1189 ],
1100 1190 FLUENTFORM_FRAMEWORK_UPGRADE,
1101 1191 'fluentform/truncate_password_values',
1102 1192 'Use fluentform/truncate_password_values instead of fluentform_truncate_password_values.'
@@ -1150,9 +1240,8 @@
1150 1240 ['wp-edit-blocks'],
1151 1241 FLUENTFORM_VERSION
1152 1242 );
1153 1243
1154 -
1155 1244 $forms = wpFluent()->table('fluentform_forms')
1156 1245 ->select(['id', 'title'])
1157 1246 ->orderBy('id', 'DESC')
1158 1247 ->get()
@@ -1169,9 +1258,9 @@
1169 1258 'value' => '',
1170 1259 ],
1171 1260 [
1172 1261 'label' => __('Inherit Theme Style', 'fluentform'),
1173 - 'value' => 'ffs_inherit_theme'
1262 + 'value' => 'ffs_inherit_theme',
1174 1263 ],
1175 1264 ];
1176 1265
1177 1266 $presets = apply_filters('fluentform/block_editor_style_presets', $presets);
@@ -1181,9 +1270,9 @@
1181 1270 'forms' => $forms,
1182 1271 'style_presets' => $presets,
1183 1272 'theme_style' => apply_filters('fluentform/load_theme_style', false) ? 'ffs_inherit_theme' : '',
1184 1273 'conversational_demo_img' => fluentFormMix('img/conversational-form-demo.png'),
1185 - 'rest' => Helper::getRestInfo()
1274 + 'rest' => Helper::getRestInfo(),
1186 1275 ]);
1187 1276
1188 1277 wp_enqueue_style(
1189 1278 'fluentform-gutenberg-block',
@@ -1206,15 +1295,15 @@
1206 1295 FLUENTFORM_VERSION
1207 1296 );
1208 1297
1209 1298 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Checking post ID in admin context
1210 - $post_id = isset($_GET['post']) ? (int)$_GET['post'] : 0;
1299 + $post_id = isset($_GET['post']) ? (int) $_GET['post'] : 0;
1211 1300 $loadPublicStyle = apply_filters_deprecated(
1212 1301 'fluentform_load_default_public',
1213 1302 [
1214 1303 true,
1215 - (object)[],
1216 - $post_id
1304 + (object) [],
1305 + $post_id,
1217 1306 ],
1218 1307 FLUENTFORM_FRAMEWORK_UPGRADE,
1219 1308 'fluentform/load_default_public',
1220 1309 'Use fluentform/load_default_public instead of fluentform_load_default_public.'
@@ -1219,9 +1308,9 @@
1219 1308 'fluentform/load_default_public',
1220 1309 'Use fluentform/load_default_public instead of fluentform_load_default_public.'
1221 1310 );
1222 1311
1223 - if (apply_filters('fluentform/load_default_public', $loadPublicStyle, (object)[], $post_id)) {
1312 + if (apply_filters('fluentform/load_default_public', $loadPublicStyle, (object) [], $post_id)) {
1224 1313 wp_enqueue_style(
1225 1314 'fluentform-public-default',
1226 1315 $fluentFormPublicDefaultCss,
1227 1316 [],
@@ -1238,29 +1327,25 @@
1238 1327 });
1239 1328 }
1240 1329
1241 1330
1242 -add_action('fluentform/before_updating_form',function ($form, $postData){
1331 +add_action('fluentform/before_updating_form', function ($form, $postData) {
1243 1332 (new FluentForm\App\Services\Form\HistoryService())->init($form, $postData);
1244 -},10,2);
1333 +}, 10, 2);
1245 1334
1246 1335
1247 1336 // WordPress 6.3+ uses iframes for block editor preview
1248 1337 // Official WordPress solution: Use enqueue_block_assets with proper context checking
1249 1338 // See: https://make.wordpress.org/core/2023/07/18/miscellaneous-editor-changes-in-wordpress-6-3/
1250 -add_action('enqueue_block_assets', function() {
1251 - // Check if we're in the block editor context (not frontend)
1252 - // This works for both the editor UI and the iframe preview in WordPress 6.3+
1253 - if (!is_admin() && !wp_is_block_theme()) {
1339 +add_action('enqueue_block_assets', function () {
1340 + // enqueue_block_assets also fires on the front end, where wp_enqueue_scripts already loads these conditionally.
1341 + if (!is_admin()) {
1254 1342 return;
1255 1343 }
1256 1344
1257 - // Additional check: Only load if we're actually in a block editor screen
1258 - if (is_admin()) {
1259 - $current_screen = function_exists('get_current_screen') ? get_current_screen() : null;
1260 - if ($current_screen && !$current_screen->is_block_editor()) {
1261 - return;
1262 - }
1345 + $current_screen = function_exists('get_current_screen') ? get_current_screen() : null;
1346 + if ($current_screen && !$current_screen->is_block_editor()) {
1347 + return;
1263 1348 }
1264 1349
1265 1350 // Enqueue Fluent Forms CSS for block editor iframe preview
1266 1351 // These styles are necessary for the live form preview in the block editor
@@ -1265,9 +1350,8 @@
1265 1350 // Enqueue Fluent Forms CSS for block editor iframe preview
1266 1351 // These styles are necessary for the live form preview in the block editor
1267 1352 wp_enqueue_style('fluent-forms-public', fluentFormMix('css/fluent-forms-public.css'), [], FLUENTFORM_VERSION);
1268 1353 wp_enqueue_style('fluentform-public-default', fluentFormMix('css/fluentform-public-default.css'), [], FLUENTFORM_VERSION);
1269 -
1270 1354 });
1271 1355
1272 1356
1273 1357