PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 6.33.1
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v6.33.1
6.35 6.34 6.33.1 6.33 6.32.1 6.32 6.31 6.25 6.25.1 6.26 6.26.1 6.27 6.28 6.29 6.3 6.3.1 6.3.2 6.30 6.4 6.4.1 6.4.2 6.5 6.5.1 6.5.2 6.5.3 All 141 releases
formidable / classes / controllers / FrmFormActionsController.php

FrmFormActionsController.php in Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More 6.33.1, at classes/controllers/FrmFormActionsController.php

1,226 lines 34.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if ( ! defined( 'ABSPATH' ) ) {
3 die( 'You are not allowed to call this page directly.' );
4 }
5
6 class FrmFormActionsController {
7
8 /**
9 * @var string
10 */
11 public static $action_post_type = 'frm_form_actions';
12
13 /**
14 * @var Frm_Form_Action_Factory|null
15 */
16 public static $registered_actions;
17
18 /**
19 * Variables saved in the post:
20 * post_content: json settings
21 * menu_order: form id
22 * post_excerpt: action type
23 */
24 public static function register_post_types() {
25 register_post_type(
26 self::$action_post_type,
27 array(
28 'label' => __( 'Form Actions', 'formidable' ),
29 'description' => '',
30 'public' => false,
31 'show_ui' => false,
32 'exclude_from_search' => true,
33 'show_in_nav_menus' => false,
34 'show_in_menu' => true,
35 'capability_type' => 'page',
36 'supports' => array( 'title', 'editor', 'excerpt', 'custom-fields', 'page-attributes' ),
37 'has_archive' => false,
38 )
39 );
40
41 self::maybe_setup_unlicensed_action_gate();
42 self::actions_init();
43 }
44
45 public static function actions_init() {
46 self::$registered_actions = new Frm_Form_Action_Factory();
47 self::register_actions();
48 do_action( 'frm_form_actions_init' );
49 }
50
51 /**
52 * @return void
53 */
54 public static function register_actions() {
55 $action_classes = array(
56 'on_submit' => 'FrmOnSubmitAction',
57 'email' => 'FrmEmailAction',
58 'gated_content' => 'FrmGatedContentAction',
59 'wppost' => 'FrmDefPostAction',
60 'register' => 'FrmDefRegAction',
61 'stripe' => 'FrmStripeLiteAction',
62 'square' => 'FrmSquareAction',
63 'paypal' => 'FrmPayPalLiteAction',
64 'paypal-legacy' => 'FrmDefPayPalLegacyAction',
65 'payment' => 'FrmTransLiteAction',
66 'quiz' => 'FrmDefQuizAction',
67 'quiz_outcome' => 'FrmDefQuizOutcomeAction',
68 'api' => 'FrmDefApiAction',
69 'mailchimp' => 'FrmDefMlcmpAction',
70 'activecampaign' => 'FrmDefActiveCampaignAction',
71 'constantcontact' => 'FrmDefConstContactAction',
72 'getresponse' => 'FrmDefGetResponseAction',
73 'mailpoet' => 'FrmDefMailpoetAction',
74 'convertkit' => 'FrmDefConvertKitAction',
75 'aweber' => 'FrmDefAweberAction',
76 'twilio' => 'FrmDefTwilioAction',
77 'salesforce' => 'FrmDefSalesforceAction',
78 'hubspot' => 'FrmDefHubspotAction',
79 'highrise' => 'FrmDefHighriseAction',
80 'zapier' => 'FrmDefZapierAction',
81 'googlespreadsheet' => 'FrmDefGoogleSpreadsheetAction',
82 'n8n' => 'FrmDefN8NAction',
83 );
84
85 $action_classes = apply_filters( 'frm_registered_form_actions', $action_classes );
86 $action_classes = self::maybe_unset_highrise( $action_classes );
87
88 include_once FrmAppHelper::plugin_path() . '/classes/views/frm-form-actions/email_action.php';
89 include_once FrmAppHelper::plugin_path() . '/classes/views/frm-form-actions/default_actions.php';
90
91 // This needs to be called after we include default_actions.php or FrmDefPayPalLegacyAction will never exist.
92 if ( 'FrmPayPalLiteAction' === $action_classes['paypal'] || ! class_exists( 'FrmPaymentAction' ) || ! class_exists( 'FrmDefPayPalLegacyAction' ) ) {
93 unset( $action_classes['paypal-legacy'] );
94 }
95
96 foreach ( $action_classes as $action_class ) {
97 self::$registered_actions->register( $action_class );
98 }
99
100 self::apply_default_action_descriptions();
101 }
102
103 /**
104 * Sets default descriptions on registered actions from a central list.
105 *
106 * Keeps the description when an add-on replaces a base action class without its own.
107 *
108 * @since 6.31
109 *
110 * @return void
111 */
112 private static function apply_default_action_descriptions() {
113 $descriptions = array(
114 'on_submit' => __( 'Success messages', 'formidable' ),
115 'email' => __( 'Autoresponder alerts', 'formidable' ),
116 'wppost' => __( 'Content publishing', 'formidable' ),
117 'register' => __( 'Account creation', 'formidable' ),
118 'payment' => __( 'Transaction alerts', 'formidable' ),
119 'stripe' => __( 'Payment gateway', 'formidable' ),
120 'square' => __( 'Payment gateway', 'formidable' ),
121 'paypal' => __( 'Payment gateway', 'formidable' ),
122 'quiz' => __( 'Automated grading', 'formidable' ),
123 'quiz_outcome' => __( 'Result logic', 'formidable' ),
124 'aweber' => __( 'List triggers', 'formidable' ),
125 'mailchimp' => __( 'Subscription confirmation', 'formidable' ),
126 'zapier' => __( 'App automation', 'formidable' ),
127 'n8n' => __( 'Workflow automation', 'formidable' ),
128 'twilio' => __( 'Text notifications', 'formidable' ),
129 'activecampaign' => __( 'Contact automation', 'formidable' ),
130 'salesforce' => __( 'Lead automation', 'formidable' ),
131 'constantcontact' => __( 'Content distribution', 'formidable' ),
132 'getresponse' => __( 'Success notifications', 'formidable' ),
133 'hubspot' => __( 'CRM alerts', 'formidable' ),
134 'mailpoet' => __( 'Plugin automation', 'formidable' ),
135 'api' => __( 'System integration', 'formidable' ),
136 'googlespreadsheet' => __( 'Spreadsheet sync', 'formidable' ),
137 'convertkit' => __( 'Broadcast publishing', 'formidable' ),
138 'gated_content' => __( 'Access control', 'formidable' ),
139 );
140
141 foreach ( self::$registered_actions->actions as $action ) {
142 if ( $action->action_options['description'] === '' && isset( $descriptions[ $action->id_base ] ) ) {
143 $action->action_options['description'] = $descriptions[ $action->id_base ];
144 }
145 }
146 }
147
148 /**
149 * Remove the Highrise action if it is not registered.
150 *
151 * @since 6.23
152 *
153 * @param array $action_classes
154 *
155 * @return array
156 */
157 private static function maybe_unset_highrise( $action_classes ) {
158 if ( 'FrmDefHighriseAction' === ( $action_classes['highrise'] ?? '' ) ) {
159 unset( $action_classes['highrise'] );
160 }
161 return $action_classes;
162 }
163
164 /**
165 * @since 4.0
166 *
167 * @param array $values
168 */
169 public static function email_settings( $values ) {
170 $form = FrmForm::getOne( $values['id'] );
171 $groups = self::form_action_groups();
172 $action_controls = self::get_form_actions();
173 self::maybe_add_action_to_group( $action_controls, $groups );
174
175 $allowed = self::active_actions( $action_controls );
176
177 include FrmAppHelper::plugin_path() . '/classes/views/frm-form-actions/settings.php';
178 }
179
180 /**
181 * Add unknown actions to a group.
182 *
183 * @since 4.0
184 *
185 * @param array $action_controls
186 * @param array $groups
187 *
188 * @return void
189 */
190 private static function maybe_add_action_to_group( $action_controls, &$groups ) {
191 $grouped = array();
192
193 foreach ( $groups as $group ) {
194 if ( isset( $group['actions'] ) ) {
195 $grouped = array_merge( $grouped, $group['actions'] );
196 }
197
198 // Also collect actions from sections
199 if ( ! isset( $group['sections'] ) ) {
200 continue;
201 }
202
203 foreach ( $group['sections'] as $section ) {
204 if ( isset( $section['actions'] ) ) {
205 $grouped = array_merge( $grouped, $section['actions'] );
206 }
207 }
208 }
209
210 foreach ( $action_controls as $action ) {
211 if ( isset( $groups[ $action->id_base ] ) || in_array( $action->id_base, $grouped, true ) ) {
212 continue;
213 }
214
215 $this_group = $action->action_options['group'];
216
217 if ( ! isset( $groups[ $this_group ] ) ) {
218 $this_group = 'misc';
219 }
220
221 if ( ! isset( $groups[ $this_group ]['actions'] ) ) {
222 $groups[ $this_group ]['actions'] = array();
223 }
224 $groups[ $this_group ]['actions'][] = $action->id_base;
225
226 unset( $action );
227 }
228 }
229
230 /**
231 * @since 4.0
232 *
233 * @return array
234 */
235 public static function form_action_groups() {
236 // Get all action controls to check which are active
237 $action_controls = self::get_form_actions();
238
239 // Determine which actions are currently available
240 $available_actions = self::get_available_my_actions( $action_controls );
241
242 // Featured actions are add-ons that are NOT currently available
243 $all_addon_actions = array(
244 'api',
245 'register',
246 'n8n',
247 'quiz',
248 'quiz_outcome',
249 'googlespreadsheet',
250 );
251 $featured_actions = array_diff( $all_addon_actions, $available_actions );
252
253 $groups = array(
254 'my_actions' => array(
255 'name' => __( 'My Actions', 'formidable' ),
256 'icon' => 'frmfont frm_shuffle_icon',
257 'sections' => array(
258 'active' => array(
259 'name' => '',
260 'actions' => $available_actions,
261 ),
262 'featured' => array(
263 'name' => __( 'Featured', 'formidable' ),
264 'actions' => array_values( $featured_actions ),
265 ),
266 ),
267 ),
268 'payment' => array(
269 'name' => __( 'E-Commerce', 'formidable' ),
270 'icon' => 'frmfont frm_credit_card_alt_icon',
271 'actions' => self::get_payment_actions( $action_controls ),
272 ),
273 'marketing' => array(
274 'name' => __( 'Marketing', 'formidable' ),
275 'icon' => 'frmfont frm_mail_bulk_icon',
276 'actions' => array(
277 'mailchimp',
278 'activecampaign',
279 'constantcontact',
280 'getresponse',
281 'aweber',
282 'mailpoet',
283 'convertkit',
284 'twilio',
285 ),
286 ),
287 'crm' => array(
288 'name' => __( 'CRM', 'formidable' ),
289 'icon' => 'frmfont frm_address_card_icon',
290 'actions' => self::get_crm_actions(),
291 ),
292 'misc' => array(
293 'name' => __( 'Misc', 'formidable' ),
294 'icon' => 'frmfont frm_shuffle_icon',
295 'actions' => self::get_misc_actions( $action_controls ),
296 ),
297 );
298
299 return apply_filters( 'frm_action_groups', $groups );
300 }
301
302 /**
303 * Get the actions that are currently available (active) for My Actions section.
304 *
305 * @since 6.31
306 *
307 * @param array $action_controls The registered action controls.
308 *
309 * @return array
310 */
311 private static function get_available_my_actions( $action_controls ) {
312 $available = array(
313 'on_submit',
314 'email',
315 'stripe',
316 'square',
317 'paypal',
318 );
319
320 // Include all actions that are marked as active, including custom actions.
321 // This ensures custom actions appear in "My Actions" when enabled.
322 foreach ( $action_controls as $action_id => $action_control ) {
323 if ( ! in_array( $action_id, $available, true ) && ! empty( $action_control->action_options['active'] ) ) {
324 $available[] = $action_id;
325 }
326 }
327
328 return $available;
329 }
330
331 /**
332 * Get the actions to include in the Misc section.
333 *
334 * @since 6.31
335 *
336 * @param array $action_controls The registered action controls.
337 *
338 * @return array
339 */
340 private static function get_misc_actions( $action_controls ) {
341 $misc_actions = array(
342 'on_submit',
343 'email',
344 'wppost',
345 'register',
346 'api',
347 'n8n',
348 'quiz',
349 'quiz_outcome',
350 'googlespreadsheet',
351 );
352
353 // Include all active actions that aren't in specific groups (payment, marketing, crm).
354 // This ensures custom actions appear in "Misc" when enabled.
355 $payment_actions = self::get_payment_actions( $action_controls );
356 $marketing_actions = array( 'mailchimp', 'activecampaign', 'constantcontact', 'getresponse', 'aweber', 'mailpoet', 'convertkit', 'twilio' );
357
358 $excluded_actions = array_merge( $payment_actions, $marketing_actions, self::get_crm_actions() );
359
360 foreach ( $action_controls as $action_id => $action_control ) {
361 if ( ! in_array( $action_id, $misc_actions, true ) && ! in_array( $action_id, $excluded_actions, true ) && ! empty( $action_control->action_options['active'] ) ) {
362 $misc_actions[] = $action_id;
363 }
364 }
365
366 return $misc_actions;
367 }
368
369 /**
370 * Get the actions to include in the E-Commerce section.
371 *
372 * @since 6.33
373 *
374 * @param array $action_controls
375 *
376 * @return array
377 */
378 private static function get_payment_actions( $action_controls ) {
379 $payment_actions = array(
380 'paypal',
381 'stripe',
382 'square',
383 );
384
385 if ( isset( $action_controls['paypal-legacy'] ) ) {
386 $payment_actions[] = 'paypal-legacy';
387 }
388
389 if ( isset( $action_controls['payment'] ) ) {
390 $payment_actions[] = 'payment';
391 }
392
393 return $payment_actions;
394 }
395
396 /**
397 * Get the actions to include in the CRM section.
398 *
399 * @since 6.23
400 *
401 * @return array
402 */
403 private static function get_crm_actions() {
404 $crm_actions = array(
405 'salesforce',
406 'hubspot',
407 'zapier',
408 );
409
410 // Only include Highrise when the add-on is active.
411 // This is because Highrise is deprecated. We don't want to show it in Lite.
412 if ( class_exists( 'FrmHrsSettings' ) ) {
413 $crm_actions[] = 'highrise';
414 }
415
416 return $crm_actions;
417 }
418
419 /**
420 * Get the number of currently active form actions.
421 *
422 * @since 4.0
423 *
424 * @param array $action_controls
425 *
426 * @return array
427 */
428 private static function active_actions( $action_controls ) {
429 $allowed = array();
430
431 foreach ( $action_controls as $action_control ) {
432 if ( ! empty( $action_control->action_options['active'] ) ) {
433 $allowed[] = $action_control->id_base;
434 }
435 }
436
437 return $allowed;
438 }
439
440 /**
441 * For each add-on, add an li, class, and javascript function. If active, add an additional class.
442 *
443 * @since 4.0
444 *
445 * @param object $action_control
446 * @param array $allowed
447 */
448 public static function show_action_icon_link( $action_control, $allowed ) {
449 $data = array();
450 $classes = ' frm_' . $action_control->id_base . '_action frm_single_action';
451 $group_class = ' frm-group-' . $action_control->action_options['group'];
452
453 /* translators: %s: Name of form action */
454 $upgrade_label = sprintf( esc_html__( '%s form actions', 'formidable' ), $action_control->action_options['tooltip'] );
455
456 $default_shown = array( 'wppost', 'register', 'payment', 'quiz', 'hubspot' );
457 $default_shown = array_values( array_diff( $default_shown, $allowed ) );
458 $default_position = array_search( $action_control->id_base, $default_shown, true );
459 $allowed_count = count( $allowed );
460
461 if ( ! empty( $action_control->action_options['active'] ) ) {
462 $classes .= ' frm_active_action';
463 } else {
464 $classes .= ' frm_inactive_action';
465
466 if ( str_contains( $action_control->action_options['classes'], 'frm_show_expired_modal' ) ) {
467 $classes .= ' frm_show_expired_modal';
468 }
469
470 if ( $default_position !== false && ( $allowed_count + $default_position ) < 6 ) {
471 $group_class .= ' frm-default-show';
472 }
473
474 $data['data-upgrade'] = $upgrade_label;
475 $data['data-medium'] = 'settings-' . $action_control->id_base;
476
477 $upgrading = FrmAddonsController::install_link( $action_control->action_options['plugin'] );
478
479 if ( isset( $upgrading['url'] ) ) {
480 $data['data-oneclick'] = json_encode( $upgrading );
481 }
482
483 if ( isset( $action_control->action_options['message'] ) ) {
484 $data['data-message'] = $action_control->action_options['message'];
485 }
486
487 $requires = FrmFormsHelper::get_plan_required( $upgrading );
488
489 if ( $requires && 'free' !== $requires ) {
490 $data['data-requires'] = $requires;
491 }
492
493 $learn_more_slug = ! empty( $action_control->action_options['learn-more'] )
494 ? $action_control->action_options['learn-more']
495 : self::get_learn_more_slug( $action_control->id_base );
496
497 if ( $learn_more_slug ) {
498 $data['data-learn-more'] = FrmAppHelper::get_doc_url(
499 $learn_more_slug,
500 'settings-' . $action_control->id_base,
501 ! str_contains( $learn_more_slug, '/' )
502 );
503 }
504 }//end if
505
506 include FrmAppHelper::plugin_path() . '/classes/views/frm-form-actions/_action_icon.php';
507 }
508
509 /**
510 * Get the HTML attributes for the action icon.
511 *
512 * @since 6.31
513 *
514 * @param object $action_control
515 *
516 * @return array
517 */
518 public static function get_action_icon_atts( $action_control ) {
519 if ( 'var(--primary-700)' !== $action_control->action_options['color'] ) {
520 return array(
521 'style' => '--primary-700:' . $action_control->action_options['color'],
522 );
523 }
524
525 return array();
526 }
527
528 /**
529 * @param string $action
530 *
531 * @return array|FrmFormAction A single form action is returned when a specific $action value is requested.
532 */
533 public static function get_form_actions( $action = 'all' ) {
534 $temp_actions = self::$registered_actions;
535
536 if ( $temp_actions ) {
537 $temp_actions = $temp_actions->actions;
538 } else {
539 self::actions_init();
540 $temp_actions = self::$registered_actions->actions;
541 }
542
543 $actions = array();
544
545 foreach ( $temp_actions as $a ) {
546 if ( 'all' !== $action && $a->id_base === $action ) {
547 return $a;
548 }
549
550 $actions[ $a->id_base ] = $a;
551 }
552
553 return $actions;
554 }
555
556 /**
557 * @since 2.0
558 *
559 * @param object $form
560 * @param array $values
561 *
562 * @return void
563 */
564 public static function list_actions( $form, $values ) {
565 if ( ! $form ) {
566 return;
567 }
568
569 /**
570 * Use this hook to migrate old settings into a new action
571 *
572 * @since 2.0
573 */
574 do_action( 'frm_before_list_actions', $form );
575
576 $filters = array(
577 'post_status' => 'all',
578 );
579 $form_actions = FrmFormAction::get_action_for_form( $form->id, 'all', $filters );
580
581 /**
582 * @var array
583 */
584 $action_controls = self::get_form_actions();
585
586 $action_map = array();
587
588 foreach ( $action_controls as $key => $control ) {
589 $action_map[ $control->id_base ] = $key;
590 }
591
592 self::maybe_show_limit_warning( $form->id, $form_actions );
593
594 echo '<p class="frm-mb-lg frm-no-actions-message' . ( $form_actions ? ' frm_hidden' : '' ) . '"> '
595 . esc_html__( 'No actions have been added yet. Select an action above to get started.', 'formidable' )
596 . '</p>';
597
598 foreach ( $form_actions as $action ) {
599 if ( ! isset( $action_map[ $action->post_excerpt ] ) ) {
600 // Don't try and show settings if action no longer exists
601 continue;
602 }
603
604 self::action_control( $action, $form, $action->ID, $action_controls[ $action_map[ $action->post_excerpt ] ], $values );
605 }
606 }
607
608 /**
609 * Show a warning before the form actions list if there are 99 actions, and the limit is set to 99.
610 * If it is filtered, the warning is still shown when applicable, just using the new limit.
611 *
612 * @since 6.17
613 *
614 * @param int|string $form_id
615 * @param array $form_actions
616 *
617 * @return void
618 */
619 private static function maybe_show_limit_warning( $form_id, $form_actions ) {
620 $count = count( $form_actions );
621
622 if ( $count < 99 ) {
623 return;
624 }
625
626 $limit = FrmFormAction::get_action_limit( $form_id );
627
628 if ( $limit < 99 || $count < $limit ) {
629 return;
630 }
631
632 $documentation_url = 'https://formidableforms.com/knowledgebase/frm_form_action_limit/#kb-increase-limit-of-form-actions';
633
634 echo '<div class="frm_warning_style">';
635 FrmAppHelper::icon_by_class( 'frmfont frm_alert_icon' );
636 echo '&nbsp;';
637 printf(
638 // translators: %s: URL to documentation
639 esc_html__( 'You have reached your form action limit. To increase this limit, you will require additional code. Visit our documentation at %s.', 'formidable' ),
640 '<a href="' . esc_url( $documentation_url ) . '" target="_blank">' . esc_html( $documentation_url ) . '</a>'
641 );
642 echo '</div>';
643 }
644
645 /**
646 * @param WP_Post $form_action
647 * @param object $form
648 * @param int $action_key Action ID.
649 * @param FrmFormAction $action_control
650 * @param array $values
651 *
652 * @return void
653 */
654 public static function action_control( $form_action, $form, $action_key, $action_control, $values ) {
655 $action_control->_set( $action_key );
656
657 $use_logging = self::should_show_log_message( $form_action->post_excerpt );
658
659 include FrmAppHelper::plugin_path() . '/classes/views/frm-form-actions/form_action.php';
660 }
661
662 public static function add_form_action() {
663 FrmAppHelper::permission_check( 'frm_edit_forms' );
664 check_ajax_referer( 'frm_ajax', 'nonce' );
665
666 $action_key = FrmAppHelper::get_param( 'list_id', '', 'post', 'absint' );
667 $action_type = FrmAppHelper::get_param( 'type', '', 'post', 'sanitize_text_field' );
668 $lite_actions = array_fill_keys( self::get_lite_actions(), true );
669
670 if ( ! FrmAppHelper::pro_is_connected() && ! isset( $lite_actions[ $action_type ] ) ) {
671 wp_die();
672 }
673
674 global $frm_vars;
675
676 /**
677 * @var FrmFormAction
678 */
679 $action_control = self::get_form_actions( $action_type );
680 $action_control->_set( $action_key );
681
682 $form_id = FrmAppHelper::get_param( 'form_id', '', 'post', 'absint' );
683 $form_action = $action_control->prepare_new( $form_id );
684 $existing_titles = (array) FrmAppHelper::get_post_param( 'existing_titles', array(), 'sanitize_text_field' );
685
686 if ( $existing_titles ) {
687 $form_action->post_title = self::get_unique_action_title( $form_action->post_title, $existing_titles );
688 }
689
690 $use_logging = self::should_show_log_message( $action_type );
691 $values = array();
692 $form = self::fields_to_values( $form_id, $values );
693
694 include FrmAppHelper::plugin_path() . '/classes/views/frm-form-actions/form_action.php';
695 wp_die();
696 }
697
698 /**
699 * Returns the first available title not in $existing_titles, appending " (2)", " (3)", etc. if needed.
700 *
701 * @since 6.31
702 *
703 * @param string $base_title Default action title from the action type.
704 * @param string[] $existing_titles Titles currently visible in the form editor.
705 *
706 * @return string
707 */
708 private static function get_unique_action_title( $base_title, array $existing_titles ) {
709 $taken = array_flip( $existing_titles );
710 $title = $base_title;
711
712 for ( $n = 2; isset( $taken[ $title ] ); $n++ ) {
713 $title = $base_title . ' (' . $n . ')';
714 }
715
716 return $title;
717 }
718
719 public static function fill_action() {
720 FrmAppHelper::permission_check( 'frm_edit_forms' );
721 check_ajax_referer( 'frm_ajax', 'nonce' );
722
723 $action_key = FrmAppHelper::get_param( 'action_id', '', 'post', 'absint' );
724 $action_type = FrmAppHelper::get_param( 'action_type', '', 'post', 'sanitize_text_field' );
725 $action_control = self::get_form_actions( $action_type );
726
727 if ( ! $action_control ) {
728 wp_die();
729 }
730
731 $form_action = $action_control->get_single_action( $action_key );
732 $values = array();
733 $form = self::fields_to_values( $form_action->menu_order, $values );
734 $use_logging = self::should_show_log_message( $action_type );
735
736 include FrmAppHelper::plugin_path() . '/classes/views/frm-form-actions/_action_inside.php';
737 wp_die();
738 }
739
740 /**
741 * @since 3.06.04
742 *
743 * @param string $action_type
744 *
745 * @return bool
746 */
747 private static function should_show_log_message( $action_type ) {
748 $logging = array(
749 'activecampaign',
750 'api',
751 'aweber',
752 'campaignmonitor',
753 'constantcontact',
754 'convertkit',
755 'getresponse',
756 'googlespreadsheet',
757 'hubspot',
758 'mailchimp',
759 'mailpoet',
760 'n8n',
761 'salesforce',
762 'twilio',
763 'zapier',
764 );
765 return in_array( $action_type, $logging, true ) && ! function_exists( 'frm_log_autoloader' );
766 }
767
768 /**
769 * @param int|string $form_id
770 * @param array $values
771 *
772 * @return object
773 */
774 private static function fields_to_values( $form_id, array &$values ) {
775 $form = FrmForm::getOne( $form_id );
776
777 $values = array(
778 'fields' => array(),
779 'id' => $form->id,
780 );
781
782 $fields = FrmField::get_all_for_form( $form->id );
783
784 foreach ( $fields as $k => $f ) {
785 $f = (array) $f;
786 $opts = (array) $f['field_options'];
787 $f = array_merge( $opts, $f );
788
789 if ( ! isset( $f['post_field'] ) ) {
790 $f['post_field'] = '';
791 }
792 $values['fields'][] = $f;
793 unset( $k, $f );
794 }
795
796 return $form;
797 }
798
799 /**
800 * @param int $form_id
801 *
802 * @return void
803 */
804 public static function update_settings( $form_id ) {
805 FrmAppHelper::permission_check( 'frm_edit_forms' );
806 $process_form = FrmAppHelper::get_post_param( 'process_form', '', 'sanitize_text_field' );
807
808 if ( ! wp_verify_nonce( $process_form, 'process_form_nonce' ) ) {
809 $frm_settings = FrmAppHelper::get_settings();
810 $error_args = array(
811 'title' => __( 'Verification failed', 'formidable' ),
812 'body' => $frm_settings->admin_permission,
813 'cancel_url' => add_query_arg(
814 array(
815 'page' => 'formidable',
816 'frm_action' => 'settings',
817 'id' => $form_id,
818 ),
819 admin_url( 'admin.php?' )
820 ),
821 );
822 FrmAppController::show_error_modal( $error_args );
823 return;
824 }
825
826 global $wpdb;
827
828 $registered_actions = self::$registered_actions->actions;
829
830 $old_actions = FrmDb::get_col(
831 $wpdb->posts,
832 array(
833 'post_type' => self::$action_post_type,
834 'menu_order' => $form_id,
835 ),
836 'ID'
837 );
838 $new_actions = array();
839
840 foreach ( $registered_actions as $registered_action ) {
841 $action_ids = $registered_action->update_callback( $form_id );
842
843 if ( $action_ids ) {
844 $new_actions[] = $action_ids;
845 }
846 }
847
848 // Only use array_merge if there are new actions.
849 if ( $new_actions ) {
850 $new_actions = call_user_func_array( 'array_merge', $new_actions );
851 }
852
853 $old_actions = array_diff( $old_actions, $new_actions );
854
855 self::delete_missing_actions( $old_actions );
856
857 FrmOnSubmitHelper::save_on_submit_settings( $form_id );
858 }
859
860 /**
861 * @param array $old_actions
862 *
863 * @return void
864 */
865 public static function delete_missing_actions( $old_actions ) {
866 if ( ! $old_actions ) {
867 return;
868 }
869
870 foreach ( $old_actions as $old_id ) {
871 wp_delete_post( $old_id );
872 }
873
874 FrmDb::cache_delete_group( 'frm_actions' );
875 }
876
877 /**
878 * @param int|string $entry_id
879 * @param int|string $form_id
880 * @param array $args
881 *
882 * @return void
883 */
884 public static function trigger_create_actions( $entry_id, $form_id, $args = array() ) {
885 $filter_args = $args;
886 $filter_args['entry_id'] = $entry_id;
887 $filter_args['form_id'] = $form_id;
888
889 /**
890 * @since 2.0.23
891 * @since 6.11.2 $filter_args is now passed instead of $args. It includes additional ID data.
892 *
893 * @param string $event 'create' by default. Pro may filter this value to 'draft' instead.
894 * @param array $filter_args
895 */
896 $event = apply_filters( 'frm_trigger_create_action', 'create', $filter_args );
897
898 self::trigger_actions( $event, $form_id, $entry_id, 'all', $args );
899 }
900
901 /**
902 * @param string $event
903 * @param int|object|string $form
904 * @param int|object|string $entry
905 * @param string $type
906 * @param array $args
907 *
908 * @return void
909 */
910 public static function trigger_actions( $event, $form, $entry, $type = 'all', $args = array() ) { // phpcs:ignore SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh
911 $action_status = array(
912 'post_status' => 'publish',
913 );
914 $form_actions = FrmFormAction::get_action_for_form( is_object( $form ) ? $form->id : $form, $type, $action_status );
915
916 if ( ! $form_actions ) {
917 return;
918 }
919
920 FrmForm::maybe_get_form( $form );
921
922 if ( ! is_object( $form ) ) {
923 return;
924 }
925
926 $link_settings = self::get_form_actions( $type );
927
928 if ( 'all' !== $type ) {
929 $link_settings = array( $type => $link_settings );
930 }
931
932 $stored_actions = array();
933 $action_priority = array();
934
935 if ( in_array( $event, array( 'create', 'update' ), true ) && defined( 'WP_IMPORTING' ) && WP_IMPORTING ) {
936 $this_event = 'import';
937 } else {
938 $this_event = $event;
939 }
940
941 foreach ( $form_actions as $action ) {
942 $skip_this_action = ! in_array( $this_event, $action->post_content['event'], true ) || FrmOnSubmitAction::$slug === $action->post_excerpt;
943 $skip_this_action = apply_filters( 'frm_skip_form_action', $skip_this_action, compact( 'action', 'entry', 'form', 'event' ) );
944
945 if ( $skip_this_action ) {
946 continue;
947 }
948
949 if ( ! is_object( $entry ) ) {
950 $entry = FrmEntry::getOne( $entry, true );
951 }
952
953 if ( ! $entry || ( FrmEntriesHelper::DRAFT_ENTRY_STATUS === (int) $entry->is_draft && 'draft' !== $event ) ) {
954 continue;
955 }
956
957 $child_entry = ( is_numeric( $form->parent_form_id ) && $form->parent_form_id ) || ( $entry && ( (int) $entry->form_id !== (int) $form->id || $entry->parent_item_id ) ) || ! empty( $args['is_child'] ); // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
958
959 if ( $child_entry ) {
960 // Maybe trigger actions for sub forms
961 $trigger_children = apply_filters( 'frm_use_embedded_form_actions', false, compact( 'form', 'entry' ) );
962
963 if ( ! $trigger_children ) {
964 continue;
965 }
966 }
967
968 // Check conditional logic.
969 $stop = FrmFormAction::action_conditions_met( $action, $entry );
970
971 if ( $stop ) {
972 continue;
973 }
974
975 // Store actions so they can be triggered with the correct priority.
976 $stored_actions[ $action->ID ] = $action;
977 $action_priority[ $action->ID ] = $link_settings[ $action->post_excerpt ]->action_options['priority'];
978
979 unset( $action );
980 }//end foreach
981
982 if ( $stored_actions ) {
983 asort( $action_priority );
984
985 // Make sure hooks are loaded.
986 new FrmNotification();
987
988 foreach ( $action_priority as $action_id => $priority ) {
989 $action = $stored_actions[ $action_id ];
990
991 /**
992 * Allows custom form action trigger.
993 *
994 * @since 6.10
995 *
996 * @param bool $skip Skip default trigger.
997 * @param object $action Action object.
998 * @param object $entry Entry object.
999 * @param object $form Form object.
1000 * @param string $event Event ('create' or 'update').
1001 */
1002 if ( false === apply_filters( 'frm_custom_trigger_action', false, $action, $entry, $form, $event ) ) {
1003 do_action( 'frm_trigger_' . $action->post_excerpt . '_action', $action, $entry, $form, $event );
1004 do_action( 'frm_trigger_' . $action->post_excerpt . '_' . $event . '_action', $action, $entry, $form );
1005 }
1006
1007 // If post is created, get updated $entry object.
1008 if ( $action->post_excerpt === 'wppost' && $event === 'create' ) {
1009 $entry = FrmEntry::getOne( $entry->id, true );
1010 }
1011 }//end foreach
1012 }//end if
1013 }
1014
1015 /**
1016 * @param int|string $form_id
1017 * @param array $values
1018 * @param array $args
1019 *
1020 * @return void
1021 */
1022 public static function duplicate_form_actions( $form_id, $values, $args = array() ) {
1023 if ( empty( $args['old_id'] ) ) {
1024 // Continue if we know which actions to copy.
1025 return;
1026 }
1027
1028 /**
1029 * @var array
1030 */
1031 $action_controls = self::get_form_actions();
1032
1033 foreach ( $action_controls as $action_control ) {
1034 $action_control->duplicate_form_actions( $form_id, $args['old_id'] );
1035 unset( $action_control );
1036 }
1037 }
1038
1039 /**
1040 * @param string $where
1041 *
1042 * @return string
1043 */
1044 public static function limit_by_type( $where ) {
1045 global $frm_vars, $wpdb;
1046
1047 if ( ! isset( $frm_vars['action_type'] ) ) {
1048 return $where;
1049 }
1050
1051 return $where . $wpdb->prepare( ' AND post_excerpt = %s ', $frm_vars['action_type'] );
1052 }
1053
1054 /**
1055 * Prevent WPML from filtering form actions based on the active language.
1056 *
1057 * @since 6.20
1058 *
1059 * @param bool|null $null
1060 * @param string $post_type
1061 *
1062 * @return bool|null
1063 */
1064 public static function prevent_wpml_translations( $null, $post_type ) {
1065 return self::$action_post_type === $post_type ? false : $null;
1066 }
1067
1068 /**
1069 * If Pro is not connected, hook a filter that will force all non-Lite
1070 * actions to inactive so the upgrade popup is shown instead.
1071 *
1072 * @since 6.31
1073 *
1074 * @return void
1075 */
1076 private static function maybe_setup_unlicensed_action_gate() {
1077 if ( FrmAppHelper::pro_is_connected() ) {
1078 return;
1079 }
1080
1081 add_filter( 'frm_registered_form_actions', array( self::class, 'disable_unlicensed_actions' ), 100 );
1082 }
1083
1084 /**
1085 * For every registered action that is not a Lite action, add a per-action
1086 * options filter that forces it to inactive with the upgrade class.
1087 *
1088 * Runs inside apply_filters('frm_registered_form_actions') at priority 100,
1089 * so the per-key option filters are in place before the class constructors
1090 * run in the foreach loop that follows.
1091 *
1092 * @since 6.31
1093 *
1094 * @param array $actions Map of action_key => class_name.
1095 *
1096 * @return array
1097 */
1098 public static function disable_unlicensed_actions( $actions ) {
1099 $lite_actions = array_fill_keys( self::get_lite_actions(), true );
1100
1101 foreach ( array_keys( $actions ) as $key ) {
1102 if ( isset( $lite_actions[ $key ] ) ) {
1103 continue;
1104 }
1105
1106 add_filter(
1107 'frm_' . $key . '_action_options',
1108 function ( $options ) {
1109 $options['active'] = false;
1110
1111 if ( ! str_contains( $options['classes'], 'frm_show_upgrade' ) ) {
1112 $options['classes'] .= ' frm_show_upgrade';
1113 }
1114
1115 return $options;
1116 }
1117 );
1118 }//end foreach
1119
1120 return $actions;
1121 }
1122
1123 /**
1124 * Get action keys that are available in Lite without a Pro license.
1125 *
1126 * @since 6.31
1127 *
1128 * @return string[]
1129 */
1130 public static function get_lite_actions() {
1131 return apply_filters( 'frm_lite_form_actions', array( 'on_submit', 'email', 'payment', 'stripe', 'square', 'paypal', 'gated_content' ) );
1132 }
1133
1134 /**
1135 * Single source of truth for learn-more URL slugs used in
1136 * upgrade modals for non-Lite form actions.
1137 *
1138 * Slugs without '/' are KB doc slugs (knowledgebase/ prefix is added).
1139 * Slugs with '/' are direct paths (e.g. features/) used as-is.
1140 *
1141 * @since 6.31
1142 *
1143 * @return array<string,string> Map of action_key => URL slug.
1144 */
1145 public static function get_action_learn_more_links() {
1146 return array(
1147 'wppost' => 'features/user-submitted-posts-wordpress-forms',
1148 'register' => 'user-registration',
1149 'paypal' => 'features/paypal-wordpress-payments',
1150 'quiz' => 'quiz-maker-forms',
1151 'quiz_outcome' => 'quiz-maker-forms',
1152 'aweber' => 'features/aweber-addon',
1153 'mailchimp' => 'features/mailchimp-addon',
1154 'zapier' => 'features/form-entry-routing-with-zapier',
1155 'twilio' => 'features/twilio-sms-form-notifications',
1156 'activecampaign' => 'features/entries-to-activecampaign',
1157 'salesforce' => 'features/form-entries-to-salesforce',
1158 'constantcontact' => 'features/entries-to-constant-contact',
1159 'getresponse' => 'features/form-entries-to-getresponse',
1160 'hubspot' => 'features/form-entries-to-hubspot',
1161 'mailpoet' => 'features/mailpoet-newsletters-addon',
1162 'api' => 'features/wordpress-form-api',
1163 'googlespreadsheet' => 'features/google-sheets',
1164 'n8n' => 'features/connect-your-forms-to-any-app-with-n8n',
1165 'convertkit' => 'features/convertkit',
1166 );
1167 }
1168
1169 /**
1170 * Look up the learn-more doc slug for a given action key.
1171 *
1172 * @since 6.31
1173 *
1174 * @param string $action_key Action identifier (e.g. 'register').
1175 *
1176 * @return string Doc slug or empty string.
1177 */
1178 private static function get_learn_more_slug( $action_key ) {
1179 $links = self::get_action_learn_more_links();
1180 return $links[ $action_key ] ?? '';
1181 }
1182 }
1183
1184 class Frm_Form_Action_Factory {
1185
1186 /**
1187 * @var array
1188 */
1189 public $actions = array();
1190
1191 public function __construct() {
1192 add_action( 'frm_form_actions_init', array( $this, '_register_actions' ), 100 );
1193 }
1194
1195 /**
1196 * @param string $action_class
1197 *
1198 * @return void
1199 */
1200 public function register( $action_class ) {
1201 $this->actions[ $action_class ] = new $action_class();
1202 }
1203
1204 /**
1205 * @param string $action_class
1206 *
1207 * @return void
1208 */
1209 public function unregister( $action_class ) {
1210 if ( isset( $this->actions[ $action_class ] ) ) {
1211 unset( $this->actions[ $action_class ] );
1212 }
1213 }
1214
1215 public function _register_actions() {
1216 $keys = array_keys( $this->actions );
1217
1218 foreach ( $keys as $key ) {
1219 // Don't register new action if old action with the same id is already registered
1220 if ( ! isset( $this->actions[ $key ] ) ) {
1221 $this->actions[ $key ]->_register();
1222 }
1223 }
1224 }
1225 }
1226