PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 6.32
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v6.32
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.32, at classes/controllers/FrmFormActionsController.php

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