PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / trunk
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More vtrunk
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
← All changes | classes/controllers/FrmFormActionsController.php +568 -93 6.25 → trunk View file →
@@ -3,12 +3,16 @@
3 3 die( 'You are not allowed to call this page directly.' );
4 4 }
5 5
6 6 class FrmFormActionsController {
7 +
8 + /**
9 + * @var string
10 + */
7 11 public static $action_post_type = 'frm_form_actions';
8 12
9 13 /**
10 - * @var array|null
14 + * @var Frm_Form_Action_Factory|null
11 15 */
12 16 public static $registered_actions;
13 17
14 18 /**
@@ -33,8 +37,9 @@
33 37 'has_archive' => false,
34 38 )
35 39 );
36 40
41 + self::maybe_setup_unlicensed_action_gate();
37 42 self::actions_init();
38 43 }
39 44
40 45 public static function actions_init() {
@@ -49,28 +54,33 @@
49 54 public static function register_actions() {
50 55 $action_classes = array(
51 56 'on_submit' => 'FrmOnSubmitAction',
52 57 'email' => 'FrmEmailAction',
58 + 'gated_content' => 'FrmGatedContentAction',
53 59 'wppost' => 'FrmDefPostAction',
54 60 'register' => 'FrmDefRegAction',
55 - 'paypal' => 'FrmDefPayPalAction',
61 + 'stripe' => 'FrmStripeLiteAction',
62 + 'square' => 'FrmSquareAction',
63 + 'paypal' => 'FrmPayPalLiteAction',
64 + 'paypal-legacy' => 'FrmDefPayPalLegacyAction',
56 65 'payment' => 'FrmTransLiteAction',
57 66 'quiz' => 'FrmDefQuizAction',
58 67 'quiz_outcome' => 'FrmDefQuizOutcomeAction',
68 + 'api' => 'FrmDefApiAction',
59 69 'mailchimp' => 'FrmDefMlcmpAction',
60 - 'api' => 'FrmDefApiAction',
61 - 'salesforce' => 'FrmDefSalesforceAction',
62 70 'activecampaign' => 'FrmDefActiveCampaignAction',
63 71 'constantcontact' => 'FrmDefConstContactAction',
64 72 'getresponse' => 'FrmDefGetResponseAction',
73 + 'mailpoet' => 'FrmDefMailpoetAction',
74 + 'convertkit' => 'FrmDefConvertKitAction',
75 + 'aweber' => 'FrmDefAweberAction',
76 + 'twilio' => 'FrmDefTwilioAction',
77 + 'salesforce' => 'FrmDefSalesforceAction',
65 78 'hubspot' => 'FrmDefHubspotAction',
79 + 'highrise' => 'FrmDefHighriseAction',
66 80 'zapier' => 'FrmDefZapierAction',
67 - 'twilio' => 'FrmDefTwilioAction',
68 - 'highrise' => 'FrmDefHighriseAction',
69 - 'mailpoet' => 'FrmDefMailpoetAction',
70 - 'aweber' => 'FrmDefAweberAction',
71 - 'convertkit' => 'FrmDefConvertKitAction',
72 81 'googlespreadsheet' => 'FrmDefGoogleSpreadsheetAction',
82 + 'n8n' => 'FrmDefN8NAction',
73 83 );
74 84
75 85 $action_classes = apply_filters( 'frm_registered_form_actions', $action_classes );
76 86 $action_classes = self::maybe_unset_highrise( $action_classes );
@@ -77,19 +87,72 @@
77 87
78 88 include_once FrmAppHelper::plugin_path() . '/classes/views/frm-form-actions/email_action.php';
79 89 include_once FrmAppHelper::plugin_path() . '/classes/views/frm-form-actions/default_actions.php';
80 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 +
81 96 foreach ( $action_classes as $action_class ) {
82 97 self::$registered_actions->register( $action_class );
83 98 }
99 +
100 + self::apply_default_action_descriptions();
84 101 }
85 102
86 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 + /**
87 149 * Remove the Highrise action if it is not registered.
88 150 *
89 151 * @since 6.23
90 152 *
91 153 * @param array $action_classes
154 + *
92 155 * @return array
93 156 */
94 157 private static function maybe_unset_highrise( $action_classes ) {
95 158 if ( 'FrmDefHighriseAction' === ( $action_classes['highrise'] ?? '' ) ) {
@@ -103,11 +166,10 @@
103 166 *
104 167 * @param array $values
105 168 */
106 169 public static function email_settings( $values ) {
107 - $form = FrmForm::getOne( $values['id'] );
108 - $groups = self::form_action_groups();
109 -
170 + $form = FrmForm::getOne( $values['id'] );
171 + $groups = self::form_action_groups();
110 172 $action_controls = self::get_form_actions();
111 173 self::maybe_add_action_to_group( $action_controls, $groups );
112 174
113 175 $allowed = self::active_actions( $action_controls );
@@ -118,23 +180,41 @@
118 180 /**
119 181 * Add unknown actions to a group.
120 182 *
121 183 * @since 4.0
184 + *
185 + * @param array $action_controls
186 + * @param array $groups
187 + *
188 + * @return void
122 189 */
123 190 private static function maybe_add_action_to_group( $action_controls, &$groups ) {
124 191 $grouped = array();
192 +
125 193 foreach ( $groups as $group ) {
126 194 if ( isset( $group['actions'] ) ) {
127 195 $grouped = array_merge( $grouped, $group['actions'] );
128 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 + }
129 208 }
130 209
131 210 foreach ( $action_controls as $action ) {
132 - if ( isset( $groups[ $action->id_base ] ) || in_array( $action->id_base, $grouped ) ) {
211 + if ( isset( $groups[ $action->id_base ] ) || in_array( $action->id_base, $grouped, true ) ) {
133 212 continue;
134 213 }
135 214
136 215 $this_group = $action->action_options['group'];
216 +
137 217 if ( ! isset( $groups[ $this_group ] ) ) {
138 218 $this_group = 'misc';
139 219 }
140 220
@@ -152,32 +232,48 @@
152 232 *
153 233 * @return array
154 234 */
155 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 +
156 253 $groups = array(
157 - 'misc' => array(
158 - 'name' => '',
159 - 'icon' => 'frm_icon_font frm_shuffle_icon',
160 - 'actions' => array(
161 - 'email',
162 - 'wppost',
163 - 'register',
164 - 'quiz',
165 - 'quiz_outcome',
166 - 'twilio',
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 + ),
167 266 ),
168 267 ),
169 - 'payment' => array(
170 - 'name' => __( 'eCommerce', 'formidable' ),
171 - 'icon' => 'frm_icon_font frm_credit_card_alt_icon',
172 - 'actions' => array(
173 - 'paypal',
174 - 'payment',
175 - ),
268 + 'payment' => array(
269 + 'name' => __( 'E-Commerce', 'formidable' ),
270 + 'icon' => 'frmfont frm_credit_card_alt_icon',
271 + 'actions' => self::get_payment_actions( $action_controls ),
176 272 ),
177 - 'marketing' => array(
178 - 'name' => __( 'Email Marketing', 'formidable' ),
179 - 'icon' => 'frm_icon_font frm_mail_bulk_icon',
273 + 'marketing' => array(
274 + 'name' => __( 'Marketing', 'formidable' ),
275 + 'icon' => 'frmfont frm_mail_bulk_icon',
180 276 'actions' => array(
181 277 'mailchimp',
182 278 'activecampaign',
183 279 'constantcontact',
@@ -184,15 +280,21 @@
184 280 'getresponse',
185 281 'aweber',
186 282 'mailpoet',
187 283 'convertkit',
284 + 'twilio',
188 285 ),
189 286 ),
190 - 'crm' => array(
287 + 'crm' => array(
191 288 'name' => __( 'CRM', 'formidable' ),
192 - 'icon' => 'frm_icon_font frm_address_card_icon',
289 + 'icon' => 'frmfont frm_address_card_icon',
193 290 'actions' => self::get_crm_actions(),
194 291 ),
292 + 'misc' => array(
293 + 'name' => __( 'Misc', 'formidable' ),
294 + 'icon' => 'frmfont frm_shuffle_icon',
295 + 'actions' => self::get_misc_actions( $action_controls ),
296 + ),
195 297 );
196 298
197 299 return apply_filters( 'frm_action_groups', $groups );
198 300 }
@@ -197,8 +299,102 @@
197 299 return apply_filters( 'frm_action_groups', $groups );
198 300 }
199 301
200 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 + /**
201 397 * Get the actions to include in the CRM section.
202 398 *
203 399 * @since 6.23
204 400 *
@@ -207,8 +403,9 @@
207 403 private static function get_crm_actions() {
208 404 $crm_actions = array(
209 405 'salesforce',
210 406 'hubspot',
407 + 'zapier',
211 408 );
212 409
213 410 // Only include Highrise when the add-on is active.
214 411 // This is because Highrise is deprecated. We don't want to show it in Lite.
@@ -223,17 +420,21 @@
223 420 * Get the number of currently active form actions.
224 421 *
225 422 * @since 4.0
226 423 *
424 + * @param array $action_controls
425 + *
227 426 * @return array
228 427 */
229 428 private static function active_actions( $action_controls ) {
230 429 $allowed = array();
430 +
231 431 foreach ( $action_controls as $action_control ) {
232 - if ( isset( $action_control->action_options['active'] ) && $action_control->action_options['active'] ) {
432 + if ( ! empty( $action_control->action_options['active'] ) ) {
233 433 $allowed[] = $action_control->id_base;
234 434 }
235 435 }
436 +
236 437 return $allowed;
237 438 }
238 439
239 440 /**
@@ -239,15 +440,15 @@
239 440 /**
240 441 * For each add-on, add an li, class, and javascript function. If active, add an additional class.
241 442 *
242 443 * @since 4.0
444 + *
243 445 * @param object $action_control
244 446 * @param array $allowed
245 447 */
246 448 public static function show_action_icon_link( $action_control, $allowed ) {
247 - $data = array();
248 - $classes = ' frm_' . $action_control->id_base . '_action frm_single_action';
249 -
449 + $data = array();
450 + $classes = ' frm_' . $action_control->id_base . '_action frm_single_action';
250 451 $group_class = ' frm-group-' . $action_control->action_options['group'];
251 452
252 453 /* translators: %s: Name of form action */
253 454 $upgrade_label = sprintf( esc_html__( '%s form actions', 'formidable' ), $action_control->action_options['tooltip'] );
@@ -253,15 +454,20 @@
253 454 $upgrade_label = sprintf( esc_html__( '%s form actions', 'formidable' ), $action_control->action_options['tooltip'] );
254 455
255 456 $default_shown = array( 'wppost', 'register', 'payment', 'quiz', 'hubspot' );
256 457 $default_shown = array_values( array_diff( $default_shown, $allowed ) );
257 - $default_position = array_search( $action_control->id_base, $default_shown );
458 + $default_position = array_search( $action_control->id_base, $default_shown, true );
258 459 $allowed_count = count( $allowed );
259 460
260 - if ( isset( $action_control->action_options['active'] ) && $action_control->action_options['active'] ) {
461 + if ( ! empty( $action_control->action_options['active'] ) ) {
261 462 $classes .= ' frm_active_action';
262 463 } else {
263 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 +
264 470 if ( $default_position !== false && ( $allowed_count + $default_position ) < 6 ) {
265 471 $group_class .= ' frm-default-show';
266 472 }
267 473
@@ -268,8 +474,9 @@
268 474 $data['data-upgrade'] = $upgrade_label;
269 475 $data['data-medium'] = 'settings-' . $action_control->id_base;
270 476
271 477 $upgrading = FrmAddonsController::install_link( $action_control->action_options['plugin'] );
478 +
272 479 if ( isset( $upgrading['url'] ) ) {
273 480 $data['data-oneclick'] = json_encode( $upgrading );
274 481 }
275 482
@@ -277,41 +484,67 @@
277 484 $data['data-message'] = $action_control->action_options['message'];
278 485 }
279 486
280 487 $requires = FrmFormsHelper::get_plan_required( $upgrading );
488 +
281 489 if ( $requires && 'free' !== $requires ) {
282 490 $data['data-requires'] = $requires;
283 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 + }
284 504 }//end if
285 505
286 - // HTML to include on the icon.
287 - $icon_atts = array();
288 - if ( $action_control->action_options['color'] !== 'var(--primary-700)' ) {
289 - $icon_atts = array(
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(
290 521 'style' => '--primary-700:' . $action_control->action_options['color'],
291 522 );
292 523 }
293 524
294 - include FrmAppHelper::plugin_path() . '/classes/views/frm-form-actions/_action_icon.php';
525 + return array();
295 526 }
296 527
297 528 /**
298 529 * @param string $action
530 + *
299 531 * @return array|FrmFormAction A single form action is returned when a specific $action value is requested.
300 532 */
301 533 public static function get_form_actions( $action = 'all' ) {
302 534 $temp_actions = self::$registered_actions;
303 - if ( empty( $temp_actions ) ) {
535 +
536 + if ( $temp_actions ) {
537 + $temp_actions = $temp_actions->actions;
538 + } else {
304 539 self::actions_init();
305 540 $temp_actions = self::$registered_actions->actions;
306 - } else {
307 - $temp_actions = $temp_actions->actions;
308 541 }
309 542
310 543 $actions = array();
311 544
312 545 foreach ( $temp_actions as $a ) {
313 - if ( 'all' !== $action && $a->id_base == $action ) {
546 + if ( 'all' !== $action && $a->id_base === $action ) {
314 547 return $a;
315 548 }
316 549
317 550 $actions[ $a->id_base ] = $a;
@@ -321,11 +554,16 @@
321 554 }
322 555
323 556 /**
324 557 * @since 2.0
558 + *
559 + * @param object $form
560 + * @param array $values
561 + *
562 + * @return void
325 563 */
326 564 public static function list_actions( $form, $values ) {
327 - if ( empty( $form ) ) {
565 + if ( ! $form ) {
328 566 return;
329 567 }
330 568
331 569 /**
@@ -352,11 +590,15 @@
352 590 }
353 591
354 592 self::maybe_show_limit_warning( $form->id, $form_actions );
355 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 +
356 598 foreach ( $form_actions as $action ) {
357 599 if ( ! isset( $action_map[ $action->post_excerpt ] ) ) {
358 - // don't try and show settings if action no longer exists
600 + // Don't try and show settings if action no longer exists
359 601 continue;
360 602 }
361 603
362 604 self::action_control( $action, $form, $action->ID, $action_controls[ $action_map[ $action->post_excerpt ] ], $values );
@@ -370,17 +612,20 @@
370 612 * @since 6.17
371 613 *
372 614 * @param int|string $form_id
373 615 * @param array $form_actions
616 + *
374 617 * @return void
375 618 */
376 619 private static function maybe_show_limit_warning( $form_id, $form_actions ) {
377 620 $count = count( $form_actions );
621 +
378 622 if ( $count < 99 ) {
379 623 return;
380 624 }
381 625
382 626 $limit = FrmFormAction::get_action_limit( $form_id );
627 +
383 628 if ( $limit < 99 || $count < $limit ) {
384 629 return;
385 630 }
386 631
@@ -386,9 +631,9 @@
386 631
387 632 $documentation_url = 'https://formidableforms.com/knowledgebase/frm_form_action_limit/#kb-increase-limit-of-form-actions';
388 633
389 634 echo '<div class="frm_warning_style">';
390 - FrmAppHelper::icon_by_class( 'frm_icon_font frm_alert_icon' );
635 + FrmAppHelper::icon_by_class( 'frmfont frm_alert_icon' );
391 636 echo '&nbsp;';
392 637 printf(
393 638 // translators: %s: URL to documentation
394 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' ),
@@ -396,8 +641,17 @@
396 641 );
397 642 echo '</div>';
398 643 }
399 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 + */
400 654 public static function action_control( $form_action, $form, $action_key, $action_control, $values ) {
401 655 $action_control->_set( $action_key );
402 656
403 657 $use_logging = self::should_show_log_message( $form_action->post_excerpt );
@@ -408,13 +662,18 @@
408 662 public static function add_form_action() {
409 663 FrmAppHelper::permission_check( 'frm_edit_forms' );
410 664 check_ajax_referer( 'frm_ajax', 'nonce' );
411 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 +
412 674 global $frm_vars;
413 675
414 - $action_key = FrmAppHelper::get_param( 'list_id', '', 'post', 'absint' );
415 - $action_type = FrmAppHelper::get_param( 'type', '', 'post', 'sanitize_text_field' );
416 -
417 676 /**
418 677 * @var FrmFormAction
419 678 */
420 679 $action_control = self::get_form_actions( $action_type );
@@ -419,37 +678,60 @@
419 678 */
420 679 $action_control = self::get_form_actions( $action_type );
421 680 $action_control->_set( $action_key );
422 681
423 - $form_id = FrmAppHelper::get_param( 'form_id', '', 'post', 'absint' );
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' );
424 685
425 - $form_action = $action_control->prepare_new( $form_id );
686 + if ( $existing_titles ) {
687 + $form_action->post_title = self::get_unique_action_title( $form_action->post_title, $existing_titles );
688 + }
689 +
426 690 $use_logging = self::should_show_log_message( $action_type );
691 + $values = array();
692 + $form = self::fields_to_values( $form_id, $values );
427 693
428 - $values = array();
429 - $form = self::fields_to_values( $form_id, $values );
430 -
431 694 include FrmAppHelper::plugin_path() . '/classes/views/frm-form-actions/form_action.php';
432 695 wp_die();
433 696 }
434 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 +
435 719 public static function fill_action() {
436 720 FrmAppHelper::permission_check( 'frm_edit_forms' );
437 721 check_ajax_referer( 'frm_ajax', 'nonce' );
438 722
439 - $action_key = FrmAppHelper::get_param( 'action_id', '', 'post', 'absint' );
440 - $action_type = FrmAppHelper::get_param( 'action_type', '', 'post', 'sanitize_text_field' );
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 );
441 726
442 - $action_control = self::get_form_actions( $action_type );
443 - if ( empty( $action_control ) ) {
727 + if ( ! $action_control ) {
444 728 wp_die();
445 729 }
446 730
447 731 $form_action = $action_control->get_single_action( $action_key );
448 -
449 - $values = array();
450 - $form = self::fields_to_values( $form_action->menu_order, $values );
451 -
732 + $values = array();
733 + $form = self::fields_to_values( $form_action->menu_order, $values );
452 734 $use_logging = self::should_show_log_message( $action_type );
453 735
454 736 include FrmAppHelper::plugin_path() . '/classes/views/frm-form-actions/_action_inside.php';
455 737 wp_die();
@@ -456,15 +738,40 @@
456 738 }
457 739
458 740 /**
459 741 * @since 3.06.04
742 + *
743 + * @param string $action_type
744 + *
460 745 * @return bool
461 746 */
462 747 private static function should_show_log_message( $action_type ) {
463 - $logging = array( 'api', 'salesforce', 'constantcontact', 'activecampaign' );
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 + );
464 765 return in_array( $action_type, $logging, true ) && ! function_exists( 'frm_log_autoloader' );
465 766 }
466 767
768 + /**
769 + * @param int|string $form_id
770 + * @param array $values
771 + *
772 + * @return object
773 + */
467 774 private static function fields_to_values( $form_id, array &$values ) {
468 775 $form = FrmForm::getOne( $form_id );
469 776
470 777 $values = array(
@@ -472,12 +779,14 @@
472 779 'id' => $form->id,
473 780 );
474 781
475 782 $fields = FrmField::get_all_for_form( $form->id );
783 +
476 784 foreach ( $fields as $k => $f ) {
477 785 $f = (array) $f;
478 786 $opts = (array) $f['field_options'];
479 787 $f = array_merge( $opts, $f );
788 +
480 789 if ( ! isset( $f['post_field'] ) ) {
481 790 $f['post_field'] = '';
482 791 }
483 792 $values['fields'][] = $f;
@@ -488,13 +797,15 @@
488 797 }
489 798
490 799 /**
491 800 * @param int $form_id
801 + *
492 802 * @return void
493 803 */
494 804 public static function update_settings( $form_id ) {
495 805 FrmAppHelper::permission_check( 'frm_edit_forms' );
496 806 $process_form = FrmAppHelper::get_post_param( 'process_form', '', 'sanitize_text_field' );
807 +
497 808 if ( ! wp_verify_nonce( $process_form, 'process_form_nonce' ) ) {
498 809 $frm_settings = FrmAppHelper::get_settings();
499 810 $error_args = array(
500 811 'title' => __( 'Verification failed', 'formidable' ),
@@ -527,17 +838,19 @@
527 838 $new_actions = array();
528 839
529 840 foreach ( $registered_actions as $registered_action ) {
530 841 $action_ids = $registered_action->update_callback( $form_id );
531 - if ( ! empty( $action_ids ) ) {
842 +
843 + if ( $action_ids ) {
532 844 $new_actions[] = $action_ids;
533 845 }
534 846 }
535 847
536 848 // Only use array_merge if there are new actions.
537 - if ( ! empty( $new_actions ) ) {
849 + if ( $new_actions ) {
538 850 $new_actions = call_user_func_array( 'array_merge', $new_actions );
539 851 }
852 +
540 853 $old_actions = array_diff( $old_actions, $new_actions );
541 854
542 855 self::delete_missing_actions( $old_actions );
543 856
@@ -543,17 +856,32 @@
543 856
544 857 FrmOnSubmitHelper::save_on_submit_settings( $form_id );
545 858 }
546 859
860 + /**
861 + * @param array $old_actions
862 + *
863 + * @return void
864 + */
547 865 public static function delete_missing_actions( $old_actions ) {
548 - if ( ! empty( $old_actions ) ) {
549 - foreach ( $old_actions as $old_id ) {
550 - wp_delete_post( $old_id );
551 - }
552 - FrmDb::cache_delete_group( 'frm_actions' );
866 + if ( ! $old_actions ) {
867 + return;
553 868 }
869 +
870 + foreach ( $old_actions as $old_id ) {
871 + wp_delete_post( $old_id );
872 + }
873 +
874 + FrmDb::cache_delete_group( 'frm_actions' );
554 875 }
555 876
877 + /**
878 + * @param int|string $entry_id
879 + * @param int|string $form_id
880 + * @param array $args
881 + *
882 + * @return void
883 + */
556 884 public static function trigger_create_actions( $entry_id, $form_id, $args = array() ) {
557 885 $filter_args = $args;
558 886 $filter_args['entry_id'] = $entry_id;
559 887 $filter_args['form_id'] = $form_id;
@@ -570,26 +898,34 @@
570 898 self::trigger_actions( $event, $form_id, $entry_id, 'all', $args );
571 899 }
572 900
573 901 /**
574 - * @param string $event
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
575 909 */
576 - public static function trigger_actions( $event, $form, $entry, $type = 'all', $args = array() ) {
910 + public static function trigger_actions( $event, $form, $entry, $type = 'all', $args = array() ) { // phpcs:ignore SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh
577 911 $action_status = array(
578 912 'post_status' => 'publish',
579 913 );
580 - $form_actions = FrmFormAction::get_action_for_form( ( is_object( $form ) ? $form->id : $form ), $type, $action_status );
914 + $form_actions = FrmFormAction::get_action_for_form( is_object( $form ) ? $form->id : $form, $type, $action_status );
581 915
582 - if ( empty( $form_actions ) ) {
916 + if ( ! $form_actions ) {
583 917 return;
584 918 }
585 919
586 920 FrmForm::maybe_get_form( $form );
921 +
587 922 if ( ! is_object( $form ) ) {
588 923 return;
589 924 }
590 925
591 926 $link_settings = self::get_form_actions( $type );
927 +
592 928 if ( 'all' !== $type ) {
593 929 $link_settings = array( $type => $link_settings );
594 930 }
595 931
@@ -602,11 +938,11 @@
602 938 $this_event = $event;
603 939 }
604 940
605 941 foreach ( $form_actions as $action ) {
606 -
607 942 $skip_this_action = ! in_array( $this_event, $action->post_content['event'], true ) || FrmOnSubmitAction::$slug === $action->post_excerpt;
608 943 $skip_this_action = apply_filters( 'frm_skip_form_action', $skip_this_action, compact( 'action', 'entry', 'form', 'event' ) );
944 +
609 945 if ( $skip_this_action ) {
610 946 continue;
611 947 }
612 948
@@ -613,17 +949,18 @@
613 949 if ( ! is_object( $entry ) ) {
614 950 $entry = FrmEntry::getOne( $entry, true );
615 951 }
616 952
617 - if ( empty( $entry ) || ( FrmEntriesHelper::DRAFT_ENTRY_STATUS === (int) $entry->is_draft && 'draft' !== $event ) ) {
953 + if ( ! $entry || ( FrmEntriesHelper::DRAFT_ENTRY_STATUS === (int) $entry->is_draft && 'draft' !== $event ) ) {
618 954 continue;
619 955 }
620 956
621 - $child_entry = ( is_numeric( $form->parent_form_id ) && $form->parent_form_id ) || ( $entry && ( $entry->form_id != $form->id || $entry->parent_item_id ) ) || ! empty( $args['is_child'] );
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
622 958
623 959 if ( $child_entry ) {
624 - // maybe trigger actions for sub forms
960 + // Maybe trigger actions for sub forms
625 961 $trigger_children = apply_filters( 'frm_use_embedded_form_actions', false, compact( 'form', 'entry' ) );
962 +
626 963 if ( ! $trigger_children ) {
627 964 continue;
628 965 }
629 966 }
@@ -629,8 +966,9 @@
629 966 }
630 967
631 968 // Check conditional logic.
632 969 $stop = FrmFormAction::action_conditions_met( $action, $entry );
970 +
633 971 if ( $stop ) {
634 972 continue;
635 973 }
636 974
@@ -640,9 +978,9 @@
640 978
641 979 unset( $action );
642 980 }//end foreach
643 981
644 - if ( ! empty( $stored_actions ) ) {
982 + if ( $stored_actions ) {
645 983 asort( $action_priority );
646 984
647 985 // Make sure hooks are loaded.
648 986 new FrmNotification();
@@ -673,8 +1011,15 @@
673 1011 }//end foreach
674 1012 }//end if
675 1013 }
676 1014
1015 + /**
1016 + * @param int|string $form_id
1017 + * @param array $values
1018 + * @param array $args
1019 + *
1020 + * @return void
1021 + */
677 1022 public static function duplicate_form_actions( $form_id, $values, $args = array() ) {
678 1023 if ( empty( $args['old_id'] ) ) {
679 1024 // Continue if we know which actions to copy.
680 1025 return;
@@ -690,8 +1035,13 @@
690 1035 unset( $action_control );
691 1036 }
692 1037 }
693 1038
1039 + /**
1040 + * @param string $where
1041 + *
1042 + * @return string
1043 + */
694 1044 public static function limit_by_type( $where ) {
695 1045 global $frm_vars, $wpdb;
696 1046
697 1047 if ( ! isset( $frm_vars['action_type'] ) ) {
@@ -697,11 +1047,9 @@
697 1047 if ( ! isset( $frm_vars['action_type'] ) ) {
698 1048 return $where;
699 1049 }
700 1050
701 - $where .= $wpdb->prepare( ' AND post_excerpt = %s ', $frm_vars['action_type'] );
702 -
703 - return $where;
1051 + return $where . $wpdb->prepare( ' AND post_excerpt = %s ', $frm_vars['action_type'] );
704 1052 }
705 1053
706 1054 /**
707 1055 * Prevent WPML from filtering form actions based on the active language.
@@ -709,19 +1057,136 @@
709 1057 * @since 6.20
710 1058 *
711 1059 * @param bool|null $null
712 1060 * @param string $post_type
1061 + *
713 1062 * @return bool|null
714 1063 */
715 1064 public static function prevent_wpml_translations( $null, $post_type ) {
716 - if ( self::$action_post_type === $post_type ) {
717 - return false;
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;
718 1079 }
719 - return $null;
1080 +
1081 + add_filter( 'frm_registered_form_actions', array( self::class, 'disable_unlicensed_actions' ), 100 );
720 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 + }
721 1182 }
722 1183
723 1184 class Frm_Form_Action_Factory {
1185 +
1186 + /**
1187 + * @var array
1188 + */
724 1189 public $actions = array();
725 1190
726 1191 public function __construct() {
727 1192 add_action( 'frm_form_actions_init', array( $this, '_register_actions' ), 100 );
@@ -726,12 +1191,22 @@
726 1191 public function __construct() {
727 1192 add_action( 'frm_form_actions_init', array( $this, '_register_actions' ), 100 );
728 1193 }
729 1194
1195 + /**
1196 + * @param string $action_class
1197 + *
1198 + * @return void
1199 + */
730 1200 public function register( $action_class ) {
731 1201 $this->actions[ $action_class ] = new $action_class();
732 1202 }
733 1203
1204 + /**
1205 + * @param string $action_class
1206 + *
1207 + * @return void
1208 + */
734 1209 public function unregister( $action_class ) {
735 1210 if ( isset( $this->actions[ $action_class ] ) ) {
736 1211 unset( $this->actions[ $action_class ] );
737 1212 }
@@ -740,9 +1215,9 @@
740 1215 public function _register_actions() {
741 1216 $keys = array_keys( $this->actions );
742 1217
743 1218 foreach ( $keys as $key ) {
744 - // don't register new action if old action with the same id is already registered
1219 + // Don't register new action if old action with the same id is already registered
745 1220 if ( ! isset( $this->actions[ $key ] ) ) {
746 1221 $this->actions[ $key ]->_register();
747 1222 }
748 1223 }