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

1,204 lines 34.1 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 ( $default_position !== false && ( $allowed_count + $default_position ) < 6 ) {
465 $group_class .= ' frm-default-show';
466 }
467
468 $data['data-upgrade'] = $upgrade_label;
469 $data['data-medium'] = 'settings-' . $action_control->id_base;
470
471 $upgrading = FrmAddonsController::install_link( $action_control->action_options['plugin'] );
472
473 if ( isset( $upgrading['url'] ) ) {
474 $data['data-oneclick'] = json_encode( $upgrading );
475 }
476
477 if ( isset( $action_control->action_options['message'] ) ) {
478 $data['data-message'] = $action_control->action_options['message'];
479 }
480
481 $requires = FrmFormsHelper::get_plan_required( $upgrading );
482
483 if ( $requires && 'free' !== $requires ) {
484 $data['data-requires'] = $requires;
485 }
486
487 $learn_more_slug = ! empty( $action_control->action_options['learn-more'] )
488 ? $action_control->action_options['learn-more']
489 : self::get_learn_more_slug( $action_control->id_base );
490
491 if ( $learn_more_slug ) {
492 $data['data-learn-more'] = FrmAppHelper::get_doc_url(
493 $learn_more_slug,
494 'settings-' . $action_control->id_base,
495 ! str_contains( $learn_more_slug, '/' )
496 );
497 }
498 }//end if
499
500 include FrmAppHelper::plugin_path() . '/classes/views/frm-form-actions/_action_icon.php';
501 }
502
503 /**
504 * Get the HTML attributes for the action icon.
505 *
506 * @since 6.31
507 *
508 * @param object $action_control
509 *
510 * @return array
511 */
512 public static function get_action_icon_atts( $action_control ) {
513 if ( 'var(--primary-700)' !== $action_control->action_options['color'] ) {
514 return array(
515 'style' => '--primary-700:' . $action_control->action_options['color'],
516 );
517 }
518
519 return array();
520 }
521
522 /**
523 * @param string $action
524 *
525 * @return array|FrmFormAction A single form action is returned when a specific $action value is requested.
526 */
527 public static function get_form_actions( $action = 'all' ) {
528 $temp_actions = self::$registered_actions;
529
530 if ( $temp_actions ) {
531 $temp_actions = $temp_actions->actions;
532 } else {
533 self::actions_init();
534 $temp_actions = self::$registered_actions->actions;
535 }
536
537 $actions = array();
538
539 foreach ( $temp_actions as $a ) {
540 if ( 'all' !== $action && $a->id_base === $action ) {
541 return $a;
542 }
543
544 $actions[ $a->id_base ] = $a;
545 }
546
547 return $actions;
548 }
549
550 /**
551 * @since 2.0
552 *
553 * @param object $form
554 * @param array $values
555 *
556 * @return void
557 */
558 public static function list_actions( $form, $values ) {
559 if ( ! $form ) {
560 return;
561 }
562
563 /**
564 * Use this hook to migrate old settings into a new action
565 *
566 * @since 2.0
567 */
568 do_action( 'frm_before_list_actions', $form );
569
570 $filters = array(
571 'post_status' => 'all',
572 );
573 $form_actions = FrmFormAction::get_action_for_form( $form->id, 'all', $filters );
574
575 /**
576 * @var array
577 */
578 $action_controls = self::get_form_actions();
579
580 $action_map = array();
581
582 foreach ( $action_controls as $key => $control ) {
583 $action_map[ $control->id_base ] = $key;
584 }
585
586 self::maybe_show_limit_warning( $form->id, $form_actions );
587
588 echo '<p class="frm-mb-lg frm-no-actions-message' . ( $form_actions ? ' frm_hidden' : '' ) . '"> '
589 . esc_html__( 'No actions have been added yet. Select an action above to get started.', 'formidable' )
590 . '</p>';
591
592 foreach ( $form_actions as $action ) {
593 if ( ! isset( $action_map[ $action->post_excerpt ] ) ) {
594 // Don't try and show settings if action no longer exists
595 continue;
596 }
597
598 self::action_control( $action, $form, $action->ID, $action_controls[ $action_map[ $action->post_excerpt ] ], $values );
599 }
600 }
601
602 /**
603 * Show a warning before the form actions list if there are 99 actions, and the limit is set to 99.
604 * If it is filtered, the warning is still shown when applicable, just using the new limit.
605 *
606 * @since 6.17
607 *
608 * @param int|string $form_id
609 * @param array $form_actions
610 *
611 * @return void
612 */
613 private static function maybe_show_limit_warning( $form_id, $form_actions ) {
614 $count = count( $form_actions );
615
616 if ( $count < 99 ) {
617 return;
618 }
619
620 $limit = FrmFormAction::get_action_limit( $form_id );
621
622 if ( $limit < 99 || $count < $limit ) {
623 return;
624 }
625
626 $documentation_url = 'https://formidableforms.com/knowledgebase/frm_form_action_limit/#kb-increase-limit-of-form-actions';
627
628 echo '<div class="frm_warning_style">';
629 FrmAppHelper::icon_by_class( 'frmfont frm_alert_icon' );
630 echo '&nbsp;';
631 printf(
632 // translators: %s: URL to documentation
633 esc_html__( 'You have reached your form action limit. To increase this limit, you will require additional code. Visit our documentation at %s.', 'formidable' ),
634 '<a href="' . esc_url( $documentation_url ) . '" target="_blank">' . esc_html( $documentation_url ) . '</a>'
635 );
636 echo '</div>';
637 }
638
639 /**
640 * @param WP_Post $form_action
641 * @param object $form
642 * @param int $action_key Action ID.
643 * @param FrmFormAction $action_control
644 * @param array $values
645 *
646 * @return void
647 */
648 public static function action_control( $form_action, $form, $action_key, $action_control, $values ) {
649 $action_control->_set( $action_key );
650
651 $use_logging = self::should_show_log_message( $form_action->post_excerpt );
652
653 include FrmAppHelper::plugin_path() . '/classes/views/frm-form-actions/form_action.php';
654 }
655
656 public static function add_form_action() {
657 FrmAppHelper::permission_check( 'frm_edit_forms' );
658 check_ajax_referer( 'frm_ajax', 'nonce' );
659
660 $action_key = FrmAppHelper::get_param( 'list_id', '', 'post', 'absint' );
661 $action_type = FrmAppHelper::get_param( 'type', '', 'post', 'sanitize_text_field' );
662 $lite_actions = array_fill_keys( self::get_lite_actions(), true );
663
664 if ( ! FrmAppHelper::pro_is_connected() && ! isset( $lite_actions[ $action_type ] ) ) {
665 wp_die();
666 }
667
668 global $frm_vars;
669
670 /**
671 * @var FrmFormAction
672 */
673 $action_control = self::get_form_actions( $action_type );
674 $action_control->_set( $action_key );
675
676 $form_id = FrmAppHelper::get_param( 'form_id', '', 'post', 'absint' );
677 $form_action = $action_control->prepare_new( $form_id );
678 $existing_titles = (array) FrmAppHelper::get_post_param( 'existing_titles', array(), 'sanitize_text_field' );
679
680 if ( $existing_titles ) {
681 $form_action->post_title = self::get_unique_action_title( $form_action->post_title, $existing_titles );
682 }
683
684 $use_logging = self::should_show_log_message( $action_type );
685 $values = array();
686 $form = self::fields_to_values( $form_id, $values );
687
688 include FrmAppHelper::plugin_path() . '/classes/views/frm-form-actions/form_action.php';
689 wp_die();
690 }
691
692 /**
693 * Returns the first available title not in $existing_titles, appending " (2)", " (3)", etc. if needed.
694 *
695 * @since 6.31
696 *
697 * @param string $base_title Default action title from the action type.
698 * @param string[] $existing_titles Titles currently visible in the form editor.
699 *
700 * @return string
701 */
702 private static function get_unique_action_title( $base_title, array $existing_titles ) {
703 $taken = array_flip( $existing_titles );
704 $title = $base_title;
705
706 for ( $n = 2; isset( $taken[ $title ] ); $n++ ) {
707 $title = $base_title . ' (' . $n . ')';
708 }
709
710 return $title;
711 }
712
713 public static function fill_action() {
714 FrmAppHelper::permission_check( 'frm_edit_forms' );
715 check_ajax_referer( 'frm_ajax', 'nonce' );
716
717 $action_key = FrmAppHelper::get_param( 'action_id', '', 'post', 'absint' );
718 $action_type = FrmAppHelper::get_param( 'action_type', '', 'post', 'sanitize_text_field' );
719 $action_control = self::get_form_actions( $action_type );
720
721 if ( ! $action_control ) {
722 wp_die();
723 }
724
725 $form_action = $action_control->get_single_action( $action_key );
726 $values = array();
727 $form = self::fields_to_values( $form_action->menu_order, $values );
728 $use_logging = self::should_show_log_message( $action_type );
729
730 include FrmAppHelper::plugin_path() . '/classes/views/frm-form-actions/_action_inside.php';
731 wp_die();
732 }
733
734 /**
735 * @since 3.06.04
736 *
737 * @param string $action_type
738 *
739 * @return bool
740 */
741 private static function should_show_log_message( $action_type ) {
742 $logging = array( 'api', 'salesforce', 'constantcontact', 'activecampaign' );
743 return in_array( $action_type, $logging, true ) && ! function_exists( 'frm_log_autoloader' );
744 }
745
746 /**
747 * @param int|string $form_id
748 * @param array $values
749 *
750 * @return object
751 */
752 private static function fields_to_values( $form_id, array &$values ) {
753 $form = FrmForm::getOne( $form_id );
754
755 $values = array(
756 'fields' => array(),
757 'id' => $form->id,
758 );
759
760 $fields = FrmField::get_all_for_form( $form->id );
761
762 foreach ( $fields as $k => $f ) {
763 $f = (array) $f;
764 $opts = (array) $f['field_options'];
765 $f = array_merge( $opts, $f );
766
767 if ( ! isset( $f['post_field'] ) ) {
768 $f['post_field'] = '';
769 }
770 $values['fields'][] = $f;
771 unset( $k, $f );
772 }
773
774 return $form;
775 }
776
777 /**
778 * @param int $form_id
779 *
780 * @return void
781 */
782 public static function update_settings( $form_id ) {
783 FrmAppHelper::permission_check( 'frm_edit_forms' );
784 $process_form = FrmAppHelper::get_post_param( 'process_form', '', 'sanitize_text_field' );
785
786 if ( ! wp_verify_nonce( $process_form, 'process_form_nonce' ) ) {
787 $frm_settings = FrmAppHelper::get_settings();
788 $error_args = array(
789 'title' => __( 'Verification failed', 'formidable' ),
790 'body' => $frm_settings->admin_permission,
791 'cancel_url' => add_query_arg(
792 array(
793 'page' => 'formidable',
794 'frm_action' => 'settings',
795 'id' => $form_id,
796 ),
797 admin_url( 'admin.php?' )
798 ),
799 );
800 FrmAppController::show_error_modal( $error_args );
801 return;
802 }
803
804 global $wpdb;
805
806 $registered_actions = self::$registered_actions->actions;
807
808 $old_actions = FrmDb::get_col(
809 $wpdb->posts,
810 array(
811 'post_type' => self::$action_post_type,
812 'menu_order' => $form_id,
813 ),
814 'ID'
815 );
816 $new_actions = array();
817
818 foreach ( $registered_actions as $registered_action ) {
819 $action_ids = $registered_action->update_callback( $form_id );
820
821 if ( $action_ids ) {
822 $new_actions[] = $action_ids;
823 }
824 }
825
826 // Only use array_merge if there are new actions.
827 if ( $new_actions ) {
828 $new_actions = call_user_func_array( 'array_merge', $new_actions );
829 }
830
831 $old_actions = array_diff( $old_actions, $new_actions );
832
833 self::delete_missing_actions( $old_actions );
834
835 FrmOnSubmitHelper::save_on_submit_settings( $form_id );
836 }
837
838 /**
839 * @param array $old_actions
840 *
841 * @return void
842 */
843 public static function delete_missing_actions( $old_actions ) {
844 if ( ! $old_actions ) {
845 return;
846 }
847
848 foreach ( $old_actions as $old_id ) {
849 wp_delete_post( $old_id );
850 }
851
852 FrmDb::cache_delete_group( 'frm_actions' );
853 }
854
855 /**
856 * @param int|string $entry_id
857 * @param int|string $form_id
858 * @param array $args
859 *
860 * @return void
861 */
862 public static function trigger_create_actions( $entry_id, $form_id, $args = array() ) {
863 $filter_args = $args;
864 $filter_args['entry_id'] = $entry_id;
865 $filter_args['form_id'] = $form_id;
866
867 /**
868 * @since 2.0.23
869 * @since 6.11.2 $filter_args is now passed instead of $args. It includes additional ID data.
870 *
871 * @param string $event 'create' by default. Pro may filter this value to 'draft' instead.
872 * @param array $filter_args
873 */
874 $event = apply_filters( 'frm_trigger_create_action', 'create', $filter_args );
875
876 self::trigger_actions( $event, $form_id, $entry_id, 'all', $args );
877 }
878
879 /**
880 * @param string $event
881 * @param int|object|string $form
882 * @param int|object|string $entry
883 * @param string $type
884 * @param array $args
885 *
886 * @return void
887 */
888 public static function trigger_actions( $event, $form, $entry, $type = 'all', $args = array() ) { // phpcs:ignore SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh
889 $action_status = array(
890 'post_status' => 'publish',
891 );
892 $form_actions = FrmFormAction::get_action_for_form( is_object( $form ) ? $form->id : $form, $type, $action_status );
893
894 if ( ! $form_actions ) {
895 return;
896 }
897
898 FrmForm::maybe_get_form( $form );
899
900 if ( ! is_object( $form ) ) {
901 return;
902 }
903
904 $link_settings = self::get_form_actions( $type );
905
906 if ( 'all' !== $type ) {
907 $link_settings = array( $type => $link_settings );
908 }
909
910 $stored_actions = array();
911 $action_priority = array();
912
913 if ( in_array( $event, array( 'create', 'update' ), true ) && defined( 'WP_IMPORTING' ) && WP_IMPORTING ) {
914 $this_event = 'import';
915 } else {
916 $this_event = $event;
917 }
918
919 foreach ( $form_actions as $action ) {
920 $skip_this_action = ! in_array( $this_event, $action->post_content['event'], true ) || FrmOnSubmitAction::$slug === $action->post_excerpt;
921 $skip_this_action = apply_filters( 'frm_skip_form_action', $skip_this_action, compact( 'action', 'entry', 'form', 'event' ) );
922
923 if ( $skip_this_action ) {
924 continue;
925 }
926
927 if ( ! is_object( $entry ) ) {
928 $entry = FrmEntry::getOne( $entry, true );
929 }
930
931 if ( ! $entry || ( FrmEntriesHelper::DRAFT_ENTRY_STATUS === (int) $entry->is_draft && 'draft' !== $event ) ) {
932 continue;
933 }
934
935 $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
936
937 if ( $child_entry ) {
938 // Maybe trigger actions for sub forms
939 $trigger_children = apply_filters( 'frm_use_embedded_form_actions', false, compact( 'form', 'entry' ) );
940
941 if ( ! $trigger_children ) {
942 continue;
943 }
944 }
945
946 // Check conditional logic.
947 $stop = FrmFormAction::action_conditions_met( $action, $entry );
948
949 if ( $stop ) {
950 continue;
951 }
952
953 // Store actions so they can be triggered with the correct priority.
954 $stored_actions[ $action->ID ] = $action;
955 $action_priority[ $action->ID ] = $link_settings[ $action->post_excerpt ]->action_options['priority'];
956
957 unset( $action );
958 }//end foreach
959
960 if ( $stored_actions ) {
961 asort( $action_priority );
962
963 // Make sure hooks are loaded.
964 new FrmNotification();
965
966 foreach ( $action_priority as $action_id => $priority ) {
967 $action = $stored_actions[ $action_id ];
968
969 /**
970 * Allows custom form action trigger.
971 *
972 * @since 6.10
973 *
974 * @param bool $skip Skip default trigger.
975 * @param object $action Action object.
976 * @param object $entry Entry object.
977 * @param object $form Form object.
978 * @param string $event Event ('create' or 'update').
979 */
980 if ( false === apply_filters( 'frm_custom_trigger_action', false, $action, $entry, $form, $event ) ) {
981 do_action( 'frm_trigger_' . $action->post_excerpt . '_action', $action, $entry, $form, $event );
982 do_action( 'frm_trigger_' . $action->post_excerpt . '_' . $event . '_action', $action, $entry, $form );
983 }
984
985 // If post is created, get updated $entry object.
986 if ( $action->post_excerpt === 'wppost' && $event === 'create' ) {
987 $entry = FrmEntry::getOne( $entry->id, true );
988 }
989 }//end foreach
990 }//end if
991 }
992
993 /**
994 * @param int|string $form_id
995 * @param array $values
996 * @param array $args
997 *
998 * @return void
999 */
1000 public static function duplicate_form_actions( $form_id, $values, $args = array() ) {
1001 if ( empty( $args['old_id'] ) ) {
1002 // Continue if we know which actions to copy.
1003 return;
1004 }
1005
1006 /**
1007 * @var array
1008 */
1009 $action_controls = self::get_form_actions();
1010
1011 foreach ( $action_controls as $action_control ) {
1012 $action_control->duplicate_form_actions( $form_id, $args['old_id'] );
1013 unset( $action_control );
1014 }
1015 }
1016
1017 /**
1018 * @param string $where
1019 *
1020 * @return string
1021 */
1022 public static function limit_by_type( $where ) {
1023 global $frm_vars, $wpdb;
1024
1025 if ( ! isset( $frm_vars['action_type'] ) ) {
1026 return $where;
1027 }
1028
1029 return $where . $wpdb->prepare( ' AND post_excerpt = %s ', $frm_vars['action_type'] );
1030 }
1031
1032 /**
1033 * Prevent WPML from filtering form actions based on the active language.
1034 *
1035 * @since 6.20
1036 *
1037 * @param bool|null $null
1038 * @param string $post_type
1039 *
1040 * @return bool|null
1041 */
1042 public static function prevent_wpml_translations( $null, $post_type ) {
1043 return self::$action_post_type === $post_type ? false : $null;
1044 }
1045
1046 /**
1047 * If Pro is not connected, hook a filter that will force all non-Lite
1048 * actions to inactive so the upgrade popup is shown instead.
1049 *
1050 * @since 6.31
1051 *
1052 * @return void
1053 */
1054 private static function maybe_setup_unlicensed_action_gate() {
1055 if ( FrmAppHelper::pro_is_connected() ) {
1056 return;
1057 }
1058
1059 add_filter( 'frm_registered_form_actions', array( self::class, 'disable_unlicensed_actions' ), 100 );
1060 }
1061
1062 /**
1063 * For every registered action that is not a Lite action, add a per-action
1064 * options filter that forces it to inactive with the upgrade class.
1065 *
1066 * Runs inside apply_filters('frm_registered_form_actions') at priority 100,
1067 * so the per-key option filters are in place before the class constructors
1068 * run in the foreach loop that follows.
1069 *
1070 * @since 6.31
1071 *
1072 * @param array $actions Map of action_key => class_name.
1073 *
1074 * @return array
1075 */
1076 public static function disable_unlicensed_actions( $actions ) {
1077 $lite_actions = array_fill_keys( self::get_lite_actions(), true );
1078
1079 foreach ( array_keys( $actions ) as $key ) {
1080 if ( isset( $lite_actions[ $key ] ) ) {
1081 continue;
1082 }
1083
1084 add_filter(
1085 'frm_' . $key . '_action_options',
1086 function ( $options ) {
1087 $options['active'] = false;
1088
1089 if ( ! str_contains( $options['classes'], 'frm_show_upgrade' ) ) {
1090 $options['classes'] .= ' frm_show_upgrade';
1091 }
1092
1093 return $options;
1094 }
1095 );
1096 }//end foreach
1097
1098 return $actions;
1099 }
1100
1101 /**
1102 * Get action keys that are available in Lite without a Pro license.
1103 *
1104 * @since 6.31
1105 *
1106 * @return string[]
1107 */
1108 public static function get_lite_actions() {
1109 return apply_filters( 'frm_lite_form_actions', array( 'on_submit', 'email', 'payment', 'stripe', 'square', 'paypal' ) );
1110 }
1111
1112 /**
1113 * Single source of truth for learn-more URL slugs used in
1114 * upgrade modals for non-Lite form actions.
1115 *
1116 * Slugs without '/' are KB doc slugs (knowledgebase/ prefix is added).
1117 * Slugs with '/' are direct paths (e.g. features/) used as-is.
1118 *
1119 * @since 6.31
1120 *
1121 * @return array<string,string> Map of action_key => URL slug.
1122 */
1123 public static function get_action_learn_more_links() {
1124 return array(
1125 'wppost' => 'features/user-submitted-posts-wordpress-forms',
1126 'register' => 'user-registration',
1127 'paypal' => 'features/paypal-wordpress-payments',
1128 'quiz' => 'quiz-maker-forms',
1129 'quiz_outcome' => 'quiz-maker-forms',
1130 'aweber' => 'features/aweber-addon',
1131 'mailchimp' => 'features/mailchimp-addon',
1132 'zapier' => 'features/form-entry-routing-with-zapier',
1133 'twilio' => 'features/twilio-sms-form-notifications',
1134 'activecampaign' => 'features/entries-to-activecampaign',
1135 'salesforce' => 'features/form-entries-to-salesforce',
1136 'constantcontact' => 'features/entries-to-constant-contact',
1137 'getresponse' => 'features/form-entries-to-getresponse',
1138 'hubspot' => 'features/form-entries-to-hubspot',
1139 'mailpoet' => 'features/mailpoet-newsletters-addon',
1140 'api' => 'features/wordpress-form-api',
1141 'googlespreadsheet' => 'features/google-sheets',
1142 'n8n' => 'features/connect-your-forms-to-any-app-with-n8n',
1143 'convertkit' => 'features/convertkit',
1144 );
1145 }
1146
1147 /**
1148 * Look up the learn-more doc slug for a given action key.
1149 *
1150 * @since 6.31
1151 *
1152 * @param string $action_key Action identifier (e.g. 'register').
1153 *
1154 * @return string Doc slug or empty string.
1155 */
1156 private static function get_learn_more_slug( $action_key ) {
1157 $links = self::get_action_learn_more_links();
1158 return $links[ $action_key ] ?? '';
1159 }
1160 }
1161
1162 class Frm_Form_Action_Factory {
1163
1164 /**
1165 * @var array
1166 */
1167 public $actions = array();
1168
1169 public function __construct() {
1170 add_action( 'frm_form_actions_init', array( $this, '_register_actions' ), 100 );
1171 }
1172
1173 /**
1174 * @param string $action_class
1175 *
1176 * @return void
1177 */
1178 public function register( $action_class ) {
1179 $this->actions[ $action_class ] = new $action_class();
1180 }
1181
1182 /**
1183 * @param string $action_class
1184 *
1185 * @return void
1186 */
1187 public function unregister( $action_class ) {
1188 if ( isset( $this->actions[ $action_class ] ) ) {
1189 unset( $this->actions[ $action_class ] );
1190 }
1191 }
1192
1193 public function _register_actions() {
1194 $keys = array_keys( $this->actions );
1195
1196 foreach ( $keys as $key ) {
1197 // Don't register new action if old action with the same id is already registered
1198 if ( ! isset( $this->actions[ $key ] ) ) {
1199 $this->actions[ $key ]->_register();
1200 }
1201 }
1202 }
1203 }
1204