PluginProbe
CartFlows – Funnel Builder & Checkout Plugin for WooCommerce / 3.1.1
CartFlows – Funnel Builder & Checkout Plugin for WooCommerce v3.1.1
3.3.0 3.2.1 3.2.0 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.1 trunk 1.0.4 1.1.0 1.1.0.1 1.1.1 1.1.10 1.1.11 1.1.12 1.1.13 1.1.14 1.1.15 1.1.16 1.1.17 1.1.18 1.1.19 1.1.2 All 162 releases
cartflows / admin-core / ajax / importer.php

importer.php in CartFlows – Funnel Builder & Checkout Plugin for WooCommerce 3.1.1, at admin-core/ajax/importer.php

2,018 lines 69.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Importer
4 *
5 * @package CartFlows
6 */
7
8 namespace CartflowsAdmin\AdminCore\Ajax;
9
10 // Exit if accessed directly.
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit;
13 }
14
15 use CartflowsAdmin\AdminCore\Inc\AdminMenu;
16 use CartflowsAdmin\AdminCore\Ajax\AjaxBase;
17 use CartflowsAdmin\AdminCore\Inc\AdminHelper;
18
19 /**
20 * Importer.
21 */
22 class Importer extends AjaxBase {
23
24 /**
25 * Instance
26 *
27 * @access private
28 * @var object Class object.
29 * @since 1.0.0
30 */
31 private static $instance;
32
33 /**
34 * Initiator
35 *
36 * @since 1.0.0
37 * @return object initialized object of class.
38 */
39 public static function get_instance() {
40 if ( ! isset( self::$instance ) ) {
41 self::$instance = new self();
42 }
43 return self::$instance;
44 }
45
46 /**
47 * Register AJAX Events.
48 *
49 * @since 1.0.0
50 * @return void
51 */
52 public function register_ajax_events() {
53
54 $ajax_events = array(
55 'create_flow',
56 'import_flow',
57
58 'create_step',
59
60 'import_step',
61
62 'activate_plugin',
63 'activate_theme',
64
65 'sync_library',
66 'request_count',
67 'import_sites',
68 'update_library_complete',
69 'export_flow',
70
71 'get_flows_list',
72
73 'import_json_flow',
74 'export_all_flows',
75 'update_step',
76 );
77
78 $this->init_ajax_events( $ajax_events );
79
80 add_action( 'admin_footer', array( $this, 'json_importer_popup_wrapper' ) );
81 add_action( 'wp_ajax_cartflows_install_plugin', 'wp_ajax_install_plugin' );
82 }
83
84 /**
85 * Export Flows.
86 *
87 * @since 1.0.0
88 * @return void
89 */
90 public function export_all_flows() {
91
92 $response_data = array( 'message' => $this->get_error_msg( 'permission' ) );
93
94 if ( ! current_user_can( 'cartflows_manage_flows_steps' ) ) {
95 wp_send_json_error( $response_data );
96 }
97
98 if ( ! check_ajax_referer( 'cartflows_export_all_flows', 'security', false ) ) {
99 $response_data = array( 'message' => $this->get_error_msg( 'nonce' ) );
100 wp_send_json_error( $response_data );
101 }
102
103 $export = \CartFlows_Importer::get_instance();
104 $flows = $export->get_all_flow_export_data();
105
106 if ( ! empty( $flows ) && is_array( $flows ) && count( $flows ) > 0 ) {
107
108 $response_data = array(
109 'message' => __( 'Funnel exported successfully', 'cartflows' ),
110 'flows' => wp_json_encode( $flows ),
111 'export' => true,
112 );
113
114 } else {
115 $response_data = array(
116 'message' => __( 'No Funnels to export', 'cartflows' ),
117 'flows' => $flows,
118 'export' => false,
119 );
120 }
121
122 wp_send_json_success( $response_data );
123 }
124
125 /**
126 * Import the Flow.
127 *
128 * @since 1.0.0
129 * @return void
130 */
131 public function import_json_flow() {
132
133 $response_data = array( 'message' => $this->get_error_msg( 'permission' ) );
134
135 if ( ! current_user_can( 'cartflows_manage_flows_steps' ) ) {
136 wp_send_json_error( $response_data );
137 }
138
139 /**
140 * Nonce verification
141 */
142 if ( ! check_ajax_referer( 'cartflows_import_json_flow', 'security', false ) ) {
143 $response_data = array( 'message' => $this->get_error_msg( 'nonce' ) );
144 wp_send_json_error( $response_data );
145 }
146
147 // $_POST['flow_data'] is the JSON, There is nothing to sanitize JSON as it is data format not data type.
148 $flow_data = ( isset( $_POST['flow_data'] ) ) ? json_decode( stripslashes( $_POST['flow_data'] ), true ) : array(); //phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
149 $check_store_checkout = isset( $_POST['check_store_checkout'] ) ? sanitize_text_field( wp_unslash( $_POST['check_store_checkout'] ) ) : 'no'; //phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
150 $force_import = isset( $_POST['force_import'] ) ? sanitize_text_field( wp_unslash( $_POST['force_import'] ) ) : 'no'; //phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
151
152 $response_data = array(
153 'message' => 'Error occured. Funnel not imported.',
154 'flow_data' => $flow_data,
155 'redirect_url' => admin_url( 'admin.php?page=' . CARTFLOWS_SLUG ),
156 );
157
158 // Check if this is a Store Checkout flow and if one already exists.
159 if ( 'yes' === $check_store_checkout && 'yes' !== $force_import ) {
160 $existing_store_checkout = \Cartflows_Helper::get_global_setting( '_cartflows_store_checkout' );
161
162 if ( $existing_store_checkout ) {
163 // Send confirmation prompt to the user.
164 wp_send_json_success(
165 array(
166 'requires_confirmation' => true,
167 'message' => __( 'A Store Checkout funnel already exists. Importing this funnel will replace the current Store Checkout funnel.', 'cartflows' ),
168 'flow_data' => $flow_data,
169 )
170 );
171 }
172 }
173
174 if ( is_array( $flow_data ) ) {
175 // Set the flag as true to check for the import process is started for the CartFlows. So as to import/upload the required files.
176 \CartFlows_Batch_Process::set_is_wcf_template_import( true );
177
178 $imported_flow = \CartFlows_Importer::get_instance()->import_from_json_data( $flow_data );
179
180 // Set the flag as false once the template import is complete.
181 \CartFlows_Batch_Process::set_is_wcf_template_import( false );
182
183 $response_data['message'] = 'Funnel Imported successfully';
184 $response_data['redirect_url'] = admin_url( 'admin.php?page=' . CARTFLOWS_SLUG . '&path=flows' );
185
186 }
187
188 wp_send_json_success( $response_data );
189 }
190
191 /**
192 * Import Wrapper.
193 *
194 * @since 1.0.0
195 * @return void
196 */
197 public function json_importer_popup_wrapper() {
198 echo '<div id="wcf-json-importer"></div>';
199 }
200
201 /**
202 * Export Step
203 */
204 public function export_flow() {
205
206 $response_data = array( 'message' => $this->get_error_msg( 'permission' ) );
207
208 if ( ! current_user_can( 'cartflows_manage_flows_steps' ) ) {
209 wp_send_json_error( $response_data );
210 }
211
212 /**
213 * Nonce verification
214 */
215 if ( ! check_ajax_referer( 'cartflows_export_flow', 'security', false ) ) {
216 $response_data = array( 'message' => $this->get_error_msg( 'nonce' ) );
217 wp_send_json_error( $response_data );
218 }
219 $flow_id = ( isset( $_POST['flow_id'] ) ) ? absint( $_POST['flow_id'] ) : '';
220
221 if ( ! $flow_id ) {
222 $response_data = array( 'message' => __( 'Invalid flow ID.', 'cartflows' ) );
223 wp_send_json_error( $response_data );
224 }
225
226 $flows[] = \CartFlows_Importer::get_instance()->get_flow_export_data( $flow_id );
227
228 $response_data = array(
229 'message' => __( 'Funnel exported successfully', 'cartflows' ),
230 'flow_name' => sanitize_title( get_the_title( $flow_id ) ),
231 'flows' => wp_json_encode( $flows ),
232 );
233 wp_send_json_success( $response_data );
234 }
235
236 /**
237 * Update library complete
238 */
239 public function update_library_complete() {
240 $response_data = array( 'message' => $this->get_error_msg( 'permission' ) );
241 if ( ! current_user_can( 'cartflows_manage_flows_steps' ) ) {
242 wp_send_json_error( $response_data );
243 }
244
245 /**
246 * Nonce verification
247 */
248 if ( ! check_ajax_referer( 'cartflows_update_library_complete', 'security', false ) ) {
249 $response_data = array( 'message' => $this->get_error_msg( 'nonce' ) );
250 wp_send_json_error( $response_data );
251 }
252
253 $templates = ! empty( $_POST['template'] ) ? sanitize_text_field( wp_unslash( $_POST['template'] ) ) : '';
254
255 \CartFlows_Batch_Process::get_instance()->update_latest_checksums( $templates );
256
257 update_site_option( 'cartflows-batch-is-complete', 'no' );
258 update_site_option( 'cartflows-manual-sync-complete', 'yes' );
259
260 $response_data = array( 'message' => 'SUCCESS: cartflows_import_sites' );
261 wp_send_json_success( $response_data );
262 }
263
264 /**
265 * Import Sites
266 */
267 public function import_sites() {
268 $response_data = array( 'message' => $this->get_error_msg( 'permission' ) );
269 if ( ! current_user_can( 'cartflows_manage_flows_steps' ) ) {
270 wp_send_json_error( $response_data );
271 }
272
273 /**
274 * Nonce verification
275 */
276 if ( ! check_ajax_referer( 'cartflows_import_sites', 'security', false ) ) {
277 $response_data = array( 'message' => $this->get_error_msg( 'nonce' ) );
278 wp_send_json_error( $response_data );
279 }
280
281 $page_no = isset( $_POST['page_no'] ) ? absint( $_POST['page_no'] ) : '';
282 $template = isset( $_POST['template'] ) ? sanitize_text_field( wp_unslash( $_POST['template'] ) ) : '';
283 if ( $page_no ) {
284 $sites_and_pages = \Cartflows_Batch_Processing_Sync_Library::get_instance()->import_sites( $page_no, $template );
285 wp_send_json_success(
286 array(
287 'message' => 'SUCCESS: cartflows_import_sites',
288 'sites_and_pages' => $sites_and_pages,
289 )
290 );
291 }
292
293 wp_send_json_error(
294 array(
295 'message' => 'SUCCESS: cartflows_import_sites',
296 )
297 );
298 }
299
300 /**
301 * Sync Library
302 */
303 public function sync_library() {
304 $response_data = array( 'message' => $this->get_error_msg( 'permission' ) );
305 if ( ! current_user_can( 'cartflows_manage_flows_steps' ) ) {
306 wp_send_json_error( $response_data );
307 }
308
309 /**
310 * Nonce verification
311 */
312 if ( ! check_ajax_referer( 'cartflows_sync_library', 'security', false ) ) {
313 $response_data = array( 'message' => $this->get_error_msg( 'nonce' ) );
314 wp_send_json_error( $response_data );
315 }
316
317 $templates = ! empty( $_POST['template'] ) ? sanitize_text_field( wp_unslash( $_POST['template'] ) ) : '';
318
319 /**
320 * LOGIC
321 */
322 if ( 'no' === \CartFlows_Batch_Process::get_instance()->get_last_export_checksums( $templates ) ) {
323 wp_send_json_success( 'updated' );
324 }
325
326 $status = \CartFlows_Batch_Process::get_instance()->test_cron();
327 if ( is_wp_error( $status ) ) {
328 $import_with = 'ajax';
329 } else {
330 $import_with = 'batch';
331 // Process import.
332 \CartFlows_Batch_Process::get_instance()->process_batch( $templates );
333 }
334
335 $response_data = array(
336 'message' => 'SUCCESS: cartflows_sync_library',
337 'status' => $import_with,
338 );
339
340 wp_send_json_success( $response_data );
341 }
342
343 /**
344 * Request Count
345 */
346 public function request_count() {
347 $response_data = array( 'message' => $this->get_error_msg( 'permission' ) );
348 if ( ! current_user_can( 'cartflows_manage_flows_steps' ) ) {
349 wp_send_json_error( $response_data );
350 }
351
352 /**
353 * Nonce verification
354 */
355 if ( ! check_ajax_referer( 'cartflows_request_count', 'security', false ) ) {
356 $response_data = array( 'message' => $this->get_error_msg( 'nonce' ) );
357 wp_send_json_error( $response_data );
358 }
359
360 $templates = ! empty( $_POST['template'] ) ? sanitize_text_field( wp_unslash( $_POST['template'] ) ) : '';
361
362 $total_requests = \CartFlows_Batch_Process::get_instance()->get_total_requests( '', $templates );
363 if ( $total_requests ) {
364 wp_send_json_success(
365 array(
366 'message' => 'SUCCESS: cartflows_request_count',
367 'count' => $total_requests,
368 )
369 );
370 }
371
372 wp_send_json_error(
373 array(
374 'message' => 'ERROR: cartflows_request_count',
375 'count' => $total_requests,
376 )
377 );
378 }
379
380 /**
381 * Create Step
382 */
383 public function create_step() {
384
385 $response_data = array( 'message' => $this->get_error_msg( 'permission' ) );
386
387 if ( ! current_user_can( 'cartflows_manage_flows_steps' ) ) {
388 wp_send_json_error( $response_data );
389 }
390
391 /**
392 * Nonce verification
393 */
394 if ( ! check_ajax_referer( 'cartflows_create_step', 'security', false ) ) {
395 $response_data = array( 'message' => $this->get_error_msg( 'nonce' ) );
396 wp_send_json_error( $response_data );
397 }
398
399 wcf()->logger->import_log( 'STARTED! Importing Step' );
400
401 $flow_id = ( isset( $_POST['flow_id'] ) ) ? absint( $_POST['flow_id'] ) : 0;
402
403 if ( CARTFLOWS_FLOW_POST_TYPE !== get_post_type( $flow_id ) ) {
404 wp_send_json_error(
405 array(
406 array(
407 'status' => false,
408 'message' => __( 'Invalid Funnel Id has been provided.', 'cartflows' ),
409 ),
410 )
411 );
412 }
413
414 $step_type = ( isset( $_POST['step_type'] ) ) ? sanitize_text_field( $_POST['step_type'] ) : '';
415 $step_title = ( isset( $_POST['step_title'] ) ) ? sanitize_text_field( $_POST['step_title'] ) : '';
416 $step_title = isset( $_POST['step_name'] ) && ! empty( $_POST['step_name'] ) ? sanitize_text_field( wp_unslash( $_POST['step_name'] ) ) : $step_title;
417
418 // Create new step.
419 $new_step_id = \CartFlows_Importer::get_instance()->create_step( $flow_id, $step_type, $step_title );
420
421 if ( empty( $new_step_id ) ) {
422 /* translators: %s: step ID */
423 wp_send_json_error( sprintf( __( 'Invalid step id %1$s.', 'cartflows' ), $new_step_id ) );
424 }
425
426 // Insert the new step at the clicked edge position when invoked from a canvas connector.
427 $this->handle_edge_insertion_reorder( $flow_id, $new_step_id );
428 $this->update_offer_step_source_redirects( $flow_id, $new_step_id );
429
430 /**
431 * Redirect to the new flow edit screen
432 */
433 $response_data = array(
434 'message' => __( 'Successfully created the step!', 'cartflows' ),
435 'redirect_url' => admin_url( 'post.php?action=edit&post=' . $new_step_id ),
436 );
437 wp_send_json_success( $response_data );
438 }
439
440 /**
441 * Active Plugin
442 */
443 public function activate_plugin() {
444 $response_data = array( 'message' => $this->get_error_msg( 'permission' ) );
445
446 if ( ! current_user_can( 'cartflows_manage_flows_steps' ) || ! current_user_can( 'activate_plugins' ) ) {
447 wp_send_json_error( $response_data );
448 }
449
450 /**
451 * Nonce verification
452 */
453 if ( ! check_ajax_referer( 'cartflows_activate_plugin', 'security', false ) ) {
454 $response_data = array( 'message' => $this->get_error_msg( 'nonce' ) );
455 wp_send_json_error( $response_data );
456 }
457
458 \wp_clean_plugins_cache();
459
460 $plugin_init = ( isset( $_POST['init'] ) ) ? sanitize_text_field( $_POST['init'] ) : '';
461
462 $do_sliently = true;
463
464 $exclude_do_silently = array(
465 'woo-cart-abandonment-recovery/woo-cart-abandonment-recovery.php',
466 );
467
468 if ( in_array( $plugin_init, $exclude_do_silently, true ) ) {
469 $do_sliently = false;
470 }
471
472 $activate = \activate_plugin( $plugin_init, '', false, $do_sliently );
473
474 if ( is_wp_error( $activate ) ) {
475 wp_send_json_error(
476 array(
477 'success' => false,
478 'message' => $activate->get_error_message(),
479 )
480 );
481 }
482
483
484 if ( class_exists( '\BSF_UTM_Analytics' ) && is_callable( '\BSF_UTM_Analytics::update_referer' ) ) {
485 $plugin_slug = pathinfo( $plugin_init, PATHINFO_FILENAME ); // Retrives the plugin slug from the init.
486 \BSF_UTM_Analytics::update_referer( 'cartflows', $plugin_slug );
487 }
488
489 wp_send_json_success(
490 array(
491 'success' => true,
492 'message' => 'Plugin activated successfully.',
493 )
494 );
495 }
496
497 /**
498 * Activate theme
499 *
500 * @since 2.0.12
501 * @return void
502 */
503 public function activate_theme() {
504
505 // Verify Nonce.
506 $response_data = array( 'message' => $this->get_error_msg( 'permission' ) );
507
508 if ( ! current_user_can( 'cartflows_manage_flows_steps' ) || ! current_user_can( 'switch_themes' ) ) {
509 wp_send_json_error( $response_data );
510 }
511
512 /**
513 * Nonce verification
514 */
515 if ( ! check_ajax_referer( 'cartflows_activate_theme', 'security', false ) ) {
516 $response_data = array( 'message' => $this->get_error_msg( 'nonce' ) );
517 wp_send_json_error( $response_data );
518 }
519
520 // Check the theme slug is available or not.
521 $theme_slug = ( isset( $_POST['theme_slug'] ) ) ? sanitize_text_field( $_POST['theme_slug'] ) : '';
522
523 // If the theme slug is not available then bail.
524 if ( empty( $theme_slug ) ) {
525 $response_data = array( 'message' => $this->get_error_msg( 'parameter' ) );
526 wp_send_json_error( $response_data );
527 }
528
529 // Pass the theme slug and switch the theme and activate it.
530 switch_theme( $theme_slug );
531
532 wp_send_json_success(
533 array(
534 'success' => true,
535 'message' => __( 'Theme Activated', 'cartflows' ),
536 )
537 );
538 }
539
540 /**
541 * Create the Flow.
542 */
543 public function create_flow() {
544
545 $response_data = array( 'message' => $this->get_error_msg( 'permission' ) );
546
547 if ( ! current_user_can( 'cartflows_manage_flows_steps' ) ) {
548 wp_send_json_error( $response_data );
549 }
550
551 /**
552 * Nonce verification
553 */
554 if ( ! check_ajax_referer( 'cartflows_create_flow', 'security', false ) ) {
555 $response_data = array( 'message' => $this->get_error_msg( 'nonce' ) );
556 wp_send_json_error( $response_data );
557 }
558
559 // Capture creation method before insert so the meta is in place
560 // when transition_post_status fires on publish.
561 $creation_method = isset( $_POST['creation_method'] ) ? sanitize_text_field( wp_unslash( $_POST['creation_method'] ) ) : 'scratch';
562
563 // Create post object.
564 $new_flow_post = array(
565 'post_title' => isset( $_POST['flow_name'] ) ? sanitize_text_field( wp_unslash( $_POST['flow_name'] ) ) : '',
566 'post_content' => '',
567 'post_status' => 'publish',
568 'post_type' => CARTFLOWS_FLOW_POST_TYPE,
569 'meta_input' => array(
570 'wcf-flow-creation-method' => $creation_method,
571 ),
572 );
573
574 // Insert the post into the database.
575 $flow_id = wp_insert_post( $new_flow_post );
576
577 if ( is_wp_error( $flow_id ) ) {
578 wp_send_json_error( $flow_id->get_error_message() );
579 }
580
581 $store_checkout = isset( $_POST['store_checkout'] ) ? sanitize_text_field( wp_unslash( $_POST['store_checkout'] ) ) : '';
582 $instant_layout = isset( $_POST['instant_layout'] ) ? sanitize_text_field( wp_unslash( $_POST['instant_layout'] ) ) : '';
583
584 // If is store checkout update store_checkout options.
585 if ( 'true' === $store_checkout ) {
586 update_option( '_cartflows_store_checkout', $flow_id );
587
588 // reset global checkout on store checkout creation.
589 $common_settings = \Cartflows_Helper::get_common_settings();
590 $common_settings['global_checkout'] = '';
591 $common_settings['override_global_checkout'] = 'disable';
592
593 update_option( '_cartflows_common', $common_settings );
594 }
595
596 $flow_steps = array();
597
598 if ( wcf()->is_woo_active ) {
599 if ( 'true' === $store_checkout ) {
600 $steps_data = array(
601 'order-form' => array(
602 'title' => __( 'Checkout', 'cartflows' ),
603 'type' => 'checkout',
604 ),
605 'order-confirmation' => array(
606 'title' => __( 'Thank You', 'cartflows' ),
607 'type' => 'thankyou',
608 ),
609 );
610 } else {
611 $steps_data = array(
612 'sales' => array(
613 'title' => __( 'Sales Landing', 'cartflows' ),
614 'type' => 'landing',
615 ),
616 'order-form' => array(
617 'title' => __( 'Checkout', 'cartflows' ),
618 'type' => 'checkout',
619 ),
620 'order-confirmation' => array(
621 'title' => __( 'Thank You', 'cartflows' ),
622 'type' => 'thankyou',
623 ),
624 );
625 }
626 } else {
627 $steps_data = array(
628 'landing' => array(
629 'title' => __( 'Landing', 'cartflows' ),
630 'type' => 'landing',
631 ),
632 'thankyou' => array(
633 'title' => __( 'Thank You', 'cartflows' ),
634 'type' => 'landing',
635 ),
636 );
637 }
638
639 foreach ( $steps_data as $slug => $data ) {
640
641 $post_content = '';
642 $step_type = trim( $data['type'] );
643
644 // Create new step.
645 $step_id = wp_insert_post(
646 array(
647 'post_type' => CARTFLOWS_STEP_POST_TYPE,
648 'post_title' => $data['title'],
649 'post_content' => $post_content,
650 'post_status' => 'publish',
651 )
652 );
653
654 // Return the error.
655 if ( is_wp_error( $step_id ) ) {
656 wp_send_json_error( $step_id->get_error_message() );
657 }
658
659 if ( $step_id ) {
660
661 $flow_steps[] = array(
662 'id' => $step_id,
663 'title' => $data['title'],
664 'type' => $step_type,
665 );
666
667 // Insert post meta.
668 update_post_meta( $step_id, 'wcf-flow-id', $flow_id );
669 update_post_meta( $step_id, 'wcf-step-type', $step_type );
670
671 // Set taxonomies.
672 wp_set_object_terms( $step_id, $step_type, CARTFLOWS_TAXONOMY_STEP_TYPE );
673 wp_set_object_terms( $step_id, 'flow-' . $flow_id, CARTFLOWS_TAXONOMY_STEP_FLOW );
674
675 update_post_meta( $step_id, '_wp_page_template', 'cartflows-default' );
676 }
677 }
678
679 update_post_meta( $flow_id, 'wcf-steps', $flow_steps );
680
681 // Enable the Instant Layout for the flow for all page builders if the funnel is created from scratch.
682 update_post_meta( $flow_id, 'instant-layout-style', 'yes' );
683 \Cartflows_Helper::set_analytics_flag( 'first_instant_layout_enabled' );
684
685 // Track funnel creation method for analytics.
686 // $creation_method was captured and persisted as post meta before wp_insert_post above.
687 AdminHelper::track_funnel_creation_method( $creation_method );
688
689 /**
690 * Redirect to the new flow edit screen
691 */
692 $response_data = array(
693 'message' => __( 'Successfully created the Funnel!', 'cartflows' ),
694 'redirect_url' => admin_url( 'post.php?action=edit&post=' . $flow_id ),
695 'flow_id' => $flow_id,
696 );
697 wp_send_json_success( $response_data );
698 }
699
700 /**
701 * Create the Flow.
702 */
703 public function import_flow() {
704
705 wcf()->logger->import_log( 'STARTED! Importing Flow' );
706
707 $response_data = array( 'message' => $this->get_error_msg( 'permission' ) );
708
709 if ( ! current_user_can( 'cartflows_manage_flows_steps' ) ) {
710 wp_send_json_error( $response_data );
711 }
712
713 /**
714 * Nonce verification
715 */
716 if ( ! check_ajax_referer( 'cartflows_import_flow', 'security', false ) ) {
717 $response_data = array( 'message' => $this->get_error_msg( 'nonce' ) );
718 wp_send_json_error( $response_data );
719 }
720 // $_POST['flow'] is the JSON, There is nothing to sanitize JSON as it is data format not data type.
721 $flow = isset( $_POST['flow'] ) ? json_decode( stripslashes( $_POST['flow'] ), true ) : array(); //phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
722
723 // Get single step Rest API response.
724 $response = \CartFlows_API::get_instance()->get_flow( $flow['ID'] );
725
726 $is_error = AdminHelper::has_api_error( $response['data'] );
727
728 if ( $is_error['error'] ) {
729
730 wp_send_json_error(
731 array(
732 'error_code' => $is_error['error_code'],
733 'call_to_action' => $is_error['call_to_action'],
734 'message' => $is_error['error_message'],
735 'data' => $response,
736 )
737 );
738 }
739
740 $license_status = isset( $response['data']['licence_status'] ) ? $response['data']['licence_status'] : '';
741
742 // If license is invalid then.
743 if ( 'valid' !== $license_status ) {
744
745 $cf_pro_status = AdminMenu::get_instance()->get_plugin_status( 'cartflows-pro/cartflows-pro.php' );
746
747 $cta = '';
748 $btn = '';
749 if ( 'not-installed' === $cf_pro_status ) {
750 /* translators: %1$s: link html start, %2$s: link html end*/
751 $btn = sprintf( __( 'CartFlows Pro Required! %1$sUpgrade to CartFlows Pro%2$s', 'cartflows' ), '<a target="_blank" href="https://cartflows.com/?utm_source=dashboard&utm_medium=free-cartflows&utm_campaign=go-pro">', '</a>' );
752 /* translators: %1$s: link html start, %2$s: link html end*/
753 $cta = sprintf( __( 'To import the premium flow %1$supgrade to CartFlows Pro%2$s.', 'cartflows' ), '<a target="_blank" href="https://cartflows.com/?utm_source=dashboard&utm_medium=free-cartflows&utm_campaign=go-pro">', '</a>' );
754 } elseif ( 'inactive' === $cf_pro_status ) {
755 /* translators: %1$s: link html start, %2$s: link html end*/
756 $btn = sprintf( __( 'Activate the CartFlows Pro to import the flow! %1$sActivate CartFlows Pro%2$s', 'cartflows' ), '<a target="_blank" href="' . admin_url( 'plugins.php?plugin_status=search&paged=1&s=CartFlows+Pro' ) . '">', '</a>' );
757 /* translators: %1$s: link html start, %2$s: link html end*/
758 $cta = sprintf( __( 'To import the premium flow %1$sactivate Cartflows Pro%2$s and validate the license key.', 'cartflows' ), '<a target="_blank" href="' . admin_url( 'plugins.php?plugin_status=search&paged=1&s=CartFlows+Pro' ) . '">', '</a>' );
759 } elseif ( 'active' === $cf_pro_status ) {
760 /* translators: %1$s: link html start, %2$s: link html end*/
761 $btn = sprintf( __( 'Invalid License Key! %1$sActivate CartFlows Pro%2$s', 'cartflows' ), '<a target="_blank" href="' . admin_url( 'plugins.php?cartflows-license-popup' ) . '">', '</a>' );
762 /* translators: %1$s: link html start, %2$s: link html end*/
763 $cta = sprintf( __( 'To import the premium flow %1$sactivate CartFlows Pro%2$s.', 'cartflows' ), '<a target="_blank" href="' . admin_url( 'plugins.php?cartflows-license-popup' ) . '">', '</a>' );
764 }
765
766 wp_send_json_error(
767 array(
768 'message' => \ucfirst( $license_status ) . ' license key!',
769 'call_to_action' => $btn,
770 'data' => $response,
771 )
772 );
773 }
774
775 if ( empty( $flow ) ) {
776 $response_data = array( 'message' => __( 'Funnel data not found.', 'cartflows' ) );
777 wp_send_json_error( $response_data );
778 }
779
780 // Capture creation method before insert so the meta is in place
781 // when transition_post_status fires on publish.
782 $creation_method = isset( $_POST['creation_method'] ) ? sanitize_text_field( wp_unslash( $_POST['creation_method'] ) ) : 'ready_made_template';
783
784 /**
785 * Create Flow
786 */
787 $new_flow_post = array(
788 'post_title' => isset( $_POST['flow_name'] ) ? sanitize_text_field( wp_unslash( $_POST['flow_name'] ) ) : '',
789 'post_content' => '',
790 'post_status' => 'publish',
791 'post_type' => CARTFLOWS_FLOW_POST_TYPE,
792 'meta_input' => array(
793 'wcf-flow-creation-method' => $creation_method,
794 ),
795 );
796
797 // Insert the post into the database.
798 $new_flow_id = wp_insert_post( $new_flow_post );
799
800 if ( is_wp_error( $new_flow_id ) ) {
801 wp_send_json_error( $new_flow_id->get_error_message() );
802 }
803
804 $store_checkout = isset( $_POST['store_checkout'] ) ? sanitize_text_field( wp_unslash( $_POST['store_checkout'] ) ) : '';
805
806 // If is global checkout update store_checkout options.
807 if ( 'true' === $store_checkout ) {
808 update_option( '_cartflows_store_checkout', $new_flow_id );
809
810 // reset global checkout on store checkout creation.
811 $common_settings = \Cartflows_Helper::get_common_settings();
812 $common_settings['global_checkout'] = '';
813 $common_settings['override_global_checkout'] = 'disable';
814
815 update_option( '_cartflows_common', $common_settings );
816 }
817
818 // Import the Global Colors Data if exists.
819 $this->import_funnel_gcp_vars_data( $response, $new_flow_id );
820
821 wcf()->logger->import_log( '✓ Flow Created! Flow ID: ' . $new_flow_id . ' - Remote Flow ID - ' . $flow['ID'] );
822
823 /**
824 * All Import Steps
825 */
826 $steps = isset( $flow['steps'] ) ? $flow['steps'] : array();
827
828 // Return of no steps are found in the imported flow.
829 if ( empty( $steps ) ) {
830 $response_data = array( 'message' => __( 'Steps not found.', 'cartflows' ) );
831 wp_send_json_error( $response_data );
832 }
833
834 // Set the flag as true to check for the import process is started for the CartFlows. So as to import/upload the required files.
835 \CartFlows_Batch_Process::set_is_wcf_template_import( true );
836
837 foreach ( $steps as $key => $step ) {
838
839 if ( in_array( $step['type'], array( 'upsell', 'downsell' ), true ) && ( ! _is_cartflows_pro() || is_wcf_starter_plan() ) ) {
840 continue;
841 }
842
843 $this->import_single_step(
844 array(
845 'step' => array(
846 'id' => $step['ID'],
847 'title' => $step['title'],
848 'type' => $step['type'],
849 ),
850 'flow' => array(
851 'id' => $new_flow_id,
852 ),
853 'is_store_checkout' => isset( $_POST['store_checkout'] ) ? sanitize_text_field( wp_unslash( $_POST['store_checkout'] ) ) : '',
854 ),
855 'cartflows_import_flow'
856 );
857 }
858
859 /**
860 * Redirect to the new flow edit screen
861 */
862 $response_data = array(
863 'message' => __( 'Successfully imported the Flow!', 'cartflows' ),
864 'items' => $flow,
865 'redirect_url' => admin_url( 'post.php?action=edit&post=' . $new_flow_id ),
866 'new_flow_id' => $new_flow_id,
867 );
868
869 // Set the flag as false once the template import is complete.
870 \CartFlows_Batch_Process::set_is_wcf_template_import( false );
871
872 // Check if the user has imported their first flow.
873 $first_flow_imported = get_option( 'wcf_first_flow_imported', false );
874
875 if ( ! $first_flow_imported ) {
876 update_option( 'wcf_first_flow_imported', true );
877 }
878
879 // Track first template imported event.
880 \Cartflows_Helper::set_analytics_flag( 'first_template_imported' );
881
882 // Track funnel creation method for analytics.
883 // $creation_method was captured and persisted as post meta before wp_insert_post above.
884 AdminHelper::track_funnel_creation_method( $creation_method );
885
886 wcf()->logger->import_log( 'COMPLETE! Importing Flow' );
887
888 wp_send_json_success( $response_data );
889 }
890
891 /**
892 * Import Step
893 *
894 * @return void
895 */
896 public function import_step() {
897
898 wcf()->logger->import_log( 'STARTED! Importing Step' );
899
900 $response_data = array( 'message' => $this->get_error_msg( 'permission' ) );
901
902 if ( ! current_user_can( 'cartflows_manage_flows_steps' ) ) {
903 wp_send_json_error( $response_data );
904 }
905
906 /**
907 * Nonce verification
908 */
909 if ( ! check_ajax_referer( 'cartflows_import_step', 'security', false ) ) {
910 $response_data = array( 'message' => $this->get_error_msg( 'nonce' ) );
911 wp_send_json_error( $response_data );
912 }
913
914 // $_POST['step'] is the JSON, There is nothing to sanitize JSON as it is data format not data type. Hence sanitizing decoded array below.
915 $step = isset( $_POST['step'] ) ? json_decode( stripslashes( $_POST['step'] ), true ) : array(); //phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
916
917 // Return if step data not found in the import request.
918 if ( empty( $step ) ) {
919 wp_send_json_error( array( 'message' => __( 'Step data ID not found for import.', 'cartflows' ) ) );
920 }
921
922 // Sanitizing decoded array.
923 $step = array_map( 'sanitize_text_field', $step );
924
925 $flow_id = isset( $_POST['flow_id'] ) ? absint( $_POST['flow_id'] ) : 0;
926
927 $remote_flow_id = isset( $_POST['remote_flow_id'] ) ? absint( $_POST['remote_flow_id'] ) : 0;
928
929 // Return if the remote flow ID is blank or not found in the request.
930 if ( empty( $remote_flow_id ) || empty( $flow_id ) ) {
931 wp_send_json_error( array( 'message' => __( 'Funnel ID not found in the request.', 'cartflows' ) ) );
932 }
933
934 // Get single step Rest API response.
935 $response = \CartFlows_API::get_instance()->get_flow( $remote_flow_id );
936
937 if ( is_wp_error( $response['data'] ) ) {
938 /* translators: %1$s: html tag, %2$s: link html start %3$s: link html end */
939 $btn = sprintf( __( 'Request timeout error. Please check if the firewall or any security plugin is blocking the outgoing HTTP/HTTPS requests to templates.cartflows.com or not. %1$sTo resolve this issue, please check this %2$sarticle%3$s.', 'cartflows' ), '<br><br>', '<a target="_blank" href="https://cartflows.com/docs/request-timeout-error-while-importing-the-flow-step-templates/?utm_source=dashboard&utm_medium=free-cartflows&utm_campaign=docs">', '</a>' );
940
941 wp_send_json_error(
942 array(
943 'message' => $response['data']->get_error_message(),
944 'call_to_action' => $btn,
945 'data' => $response,
946 )
947 );
948 }
949
950 $license_status = isset( $response['data']['licence_status'] ) ? $response['data']['licence_status'] : '';
951
952 // If license is invalid then.
953 if ( 'valid' !== $license_status ) {
954
955 $cf_pro_status = AdminMenu::get_instance()->get_plugin_status( 'cartflows-pro/cartflows-pro.php' );
956
957 $msg = '';
958 $cta = '';
959 if ( 'not-installed' === $cf_pro_status ) {
960 /* translators: %1$s: link html start, %2$s: link html end*/
961 $cta = sprintf( __( '%1$sUpgrade to CartFlows Pro.%2$s', 'cartflows' ), '<a target="_blanks" class="wcf-button wcf-primary-button" href="https://cartflows.com/?utm_source=dashboard&utm_medium=free-cartflows&utm_campaign=go-pro">', '</a>' );
962 $msg = __( 'To import the premium step, please upgrade to CartFlows Pro', 'cartflows' );
963 } elseif ( 'inactive' === $cf_pro_status ) {
964 /* translators: %1$s: link html start, %2$s: link html end*/
965 $cta = sprintf( __( '%1$sActivate CartFlows Pro%2$s', 'cartflows' ), '<a target="_blank" class="wcf-button wcf-primary-button" href="' . admin_url( 'plugins.php?plugin_status=search&paged=1&s=CartFlows+Pro' ) . '">', '</a>' );
966 $msg = __( 'To import the premium step activate Cartflows Pro and validate the license key.', 'cartflows' );
967 } elseif ( 'active' === $cf_pro_status ) {
968 /* translators: %1$s: link html start, %2$s: link html end*/
969 $cta = sprintf( __( '%1$sActivate CartFlows Pro License %2$s', 'cartflows' ), '<a target="_blank" class="wcf-button wcf-primary-button" href="' . admin_url( 'plugins.php?cartflows-license-popup' ) . '">', '</a>' );
970 $msg = __( 'To import the premium step activate the CartFlows Pro.', 'cartflows' );
971 }
972
973 wp_send_json_error(
974 array(
975 'message' => \ucfirst( $license_status ) . ' license key! ' . $msg,
976 'call_to_action' => $cta,
977 'data' => $response,
978 )
979 );
980 }
981
982 $step['title'] = isset( $_POST['step_name'] ) && ! empty( $_POST['step_name'] ) ? sanitize_text_field( wp_unslash( $_POST['step_name'] ) ) : $step['title'];
983
984 // Set the flag as true to check for the import process is started for the CartFlows. So as to import/upload the required files.
985 \CartFlows_Batch_Process::set_is_wcf_template_import( true );
986
987 // Create steps.
988 $this->import_single_step(
989 array(
990 'step' => array(
991 'id' => $step['ID'],
992 'title' => $step['title'],
993 'type' => $step['type'],
994 ),
995 'flow' => array(
996 'id' => $flow_id,
997 ),
998 ),
999 'cartflows_import_step'
1000 );
1001
1002 wcf()->logger->import_log( 'COMPLETE! Importing Step' );
1003
1004 if ( empty( $step ) ) {
1005 $response_data = array( 'message' => __( 'Step data not found.', 'cartflows' ) );
1006 wp_send_json_error( $response_data );
1007 }
1008
1009 /**
1010 * Redirect to the new step edit screen
1011 */
1012 $response_data = array(
1013 'message' => __( 'Successfully imported the Step!', 'cartflows' ),
1014 );
1015
1016 // Set the flag as false once the template import is complete.
1017 \CartFlows_Batch_Process::set_is_wcf_template_import( false );
1018
1019 // Track first template imported event.
1020 \Cartflows_Helper::set_analytics_flag( 'first_template_imported' );
1021
1022 wcf()->logger->import_log( 'COMPLETE! Importing Step' );
1023
1024 wp_send_json_success( $response_data );
1025 }
1026
1027 /**
1028 * Updates post content of chosen template.
1029 * working only for Store Checkout.
1030 *
1031 * @return void
1032 * @since X.X.X
1033 */
1034 public function update_step() {
1035
1036 wcf()->logger->import_log( 'STARTED! Updating Step' );
1037
1038 $response_data = array( 'message' => $this->get_error_msg( 'permission' ) );
1039
1040 if ( ! current_user_can( 'cartflows_manage_flows_steps' ) ) {
1041 wp_send_json_error( $response_data );
1042 }
1043
1044 /**
1045 * Nonce verification
1046 */
1047 if ( ! check_ajax_referer( 'cartflows_update_step', 'security', false ) ) {
1048 $response_data = array( 'message' => $this->get_error_msg( 'nonce' ) );
1049 wp_send_json_error( $response_data );
1050 }
1051
1052 // $_POST['step'] is the JSON, There is nothing to sanitize JSON as it is data format not data type. Hence sanitizing decoded array below.
1053 $step = isset( $_POST['step'] ) ? json_decode( stripslashes( $_POST['step'] ), true ) : array(); //phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1054 // Sanitizing decoded array.
1055 $step = array_map( 'sanitize_text_field', $step );
1056 $flow_id = isset( $_POST['flow_id'] ) ? absint( $_POST['flow_id'] ) : 0;
1057 $step_id = isset( $_POST['step_id'] ) ? absint( $_POST['step_id'] ) : 0;
1058
1059 $remote_flow_id = isset( $_POST['remote_flow_id'] ) ? absint( $_POST['remote_flow_id'] ) : 0;
1060
1061 // Get single step Rest API response.
1062 $response = \CartFlows_API::get_instance()->get_flow( $remote_flow_id );
1063 if ( is_wp_error( $response['data'] ) ) {
1064 /* translators: %1$s: html tag, %2$s: link html start %3$s: link html end */
1065 $btn = sprintf( __( 'Request timeout error. Please check if the firewall or any security plugin is blocking the outgoing HTTP/HTTPS requests to templates.cartflows.com or not. %1$sTo resolve this issue, please check this %2$s article%3$s.', 'cartflows' ), '<br><br>', '<a target="_blank" href="https://cartflows.com/docs/request-timeout-error-while-importing-the-flow-step-templates/?utm_source=dashboard&utm_medium=free-cartflows&utm_campaign=docs">', '</a>' );
1066
1067 wp_send_json_error(
1068 array(
1069 'message' => $response['data']->get_error_message(),
1070 'call_to_action' => $btn,
1071 'data' => $response,
1072 )
1073 );
1074 }
1075
1076 $license_status = isset( $response['data']['licence_status'] ) ? $response['data']['licence_status'] : '';
1077
1078 // If license is invalid then.
1079 if ( 'valid' !== $license_status ) {
1080
1081 $cf_pro_status = AdminMenu::get_instance()->get_plugin_status( 'cartflows-pro/cartflows-pro.php' );
1082
1083 $cta = '';
1084 if ( 'not-installed' === $cf_pro_status ) {
1085 /* translators: %1$s: link html start, %2$s: link html end*/
1086 $cta = sprintf( __( 'Upgrade to %1$sCartFlows Pro.%2$s', 'cartflows' ), '<a target="_blanks" href="https://cartflows.com/?utm_source=dashboard&utm_medium=free-cartflows&utm_campaign=go-pro">', '</a>' );
1087 } elseif ( 'inactive' === $cf_pro_status ) {
1088 /* translators: %1$s: link html start, %2$s: link html end*/
1089 $cta = sprintf( __( '%1$sActivate CartFlows Pro%2$s', 'cartflows' ), '<a target="_blank" href="' . admin_url( 'plugins.php?plugin_status=search&paged=1&s=CartFlows+Pro' ) . '">', '</a>' );
1090 } elseif ( 'active' === $cf_pro_status ) {
1091 /* translators: %1$s: link html start, %2$s: link html end*/
1092 $cta = sprintf( __( 'CartFlows Pro license is not active. Activate %1$sCartFlows Pro License %2$s', 'cartflows' ), '<a target="_blank" href="' . admin_url( 'plugins.php?cartflows-license-popup' ) . '">', '</a>' );
1093 }
1094
1095 wp_send_json_error(
1096 array(
1097 'message' => \ucfirst( $license_status ) . ' license key! ' . $cta,
1098 'data' => $response,
1099 )
1100 );
1101 }
1102
1103 if ( empty( $remote_flow_id ) ) {
1104 $response_data = array( 'message' => __( 'Funnel data not found.', 'cartflows' ) );
1105 wp_send_json_error( $response_data );
1106 }
1107 $step['title'] = isset( $_POST['step_name'] ) && ! empty( $_POST['step_name'] ) ? sanitize_text_field( wp_unslash( $_POST['step_name'] ) ) : $step['title'];
1108 // Create steps.
1109 $this->update_single_step(
1110 array(
1111 'step' => array(
1112 'id' => $step['ID'],
1113 'title' => $step['title'],
1114 'type' => $step['type'],
1115 ),
1116 'flow' => array(
1117 'id' => $flow_id,
1118 ),
1119 'current_step' => array(
1120 'id' => $step_id,
1121 ),
1122 )
1123 );
1124
1125 wcf()->logger->import_log( 'COMPLETE! Importing Step' );
1126
1127 if ( empty( $step ) ) {
1128 $response_data = array( 'message' => __( 'Step data not found.', 'cartflows' ) );
1129 wp_send_json_error( $response_data );
1130 }
1131
1132 /**
1133 * Redirect to the new step edit screen
1134 */
1135 $response_data = array(
1136 'message' => __( 'Successfully imported the Step!', 'cartflows' ),
1137 );
1138
1139 wcf()->logger->import_log( 'COMPLETE! Importing Step' );
1140
1141 wp_send_json_success( $response_data );
1142 }
1143
1144 /**
1145 * Update Sinple Step
1146 *
1147 * @param array $args Rest API Arguments.
1148 * @return void
1149 */
1150 public function update_single_step( $args = array() ) {
1151
1152 wcf()->logger->import_log( 'STARTED! Updating Step' );
1153
1154 $step_id = isset( $args['step']['id'] ) ? absint( $args['step']['id'] ) : 0;
1155 $new_step_id = isset( $args['current_step']['id'] ) ? absint( $args['current_step']['id'] ) : '';
1156
1157 if ( empty( $step_id ) || empty( $new_step_id ) ) {
1158 /* translators: %s: step ID */
1159 wp_send_json_error( sprintf( __( 'Invalid step id %1$s or post id %2$s.', 'cartflows' ), $step_id, $new_step_id ) );
1160 }
1161
1162 wcf()->logger->import_log( 'Remote Step ' . $step_id . ' for local flow "' . get_the_title( $new_step_id ) . '" [' . $new_step_id . ']' );
1163
1164 // Get single step Rest API response.
1165 $response = \CartFlows_API::get_instance()->get_template( $step_id );
1166 wcf()->logger->import_log( wp_json_encode( $response ) );
1167
1168 if ( 'divi' === \Cartflows_Helper::get_common_setting( 'default_page_builder' ) ) {
1169 if ( isset( $response['data']['divi_content'] ) && ! empty( $response['data']['divi_content'] ) ) {
1170
1171 update_post_meta( $new_step_id, 'divi_content', $response['data']['divi_content'] );
1172
1173 wp_update_post(
1174 array(
1175 'ID' => $new_step_id,
1176 'post_content' => $response['data']['divi_content'],
1177 )
1178 );
1179 }
1180 }
1181
1182 if ( 'gutenberg' === \Cartflows_Helper::get_common_setting( 'default_page_builder' ) ) {
1183 if ( isset( $response['data']['divi_content'] ) && ! empty( $response['data']['divi_content'] ) ) {
1184 wp_update_post(
1185 array(
1186 'ID' => $new_step_id,
1187 'post_content' => $response['data']['divi_content'],
1188 )
1189 );
1190 }
1191 }
1192
1193 // Import Post Meta.
1194 $this->import_post_meta( $new_step_id, $response );
1195
1196 /* Imported Step */
1197 update_post_meta( $new_step_id, 'cartflows_imported_step', 'yes' );
1198
1199 do_action( 'cartflows_import_complete' );
1200
1201 // Batch Process.
1202 do_action( 'cartflows_after_template_import', $new_step_id, $response );
1203
1204 wcf()->logger->import_log( 'COMPLETE! Importing Step' );
1205 }
1206
1207 /**
1208 * Create Simple Step
1209 *
1210 * @param array $args Rest API Arguments.
1211 * @param string $action action name to check nonce.
1212 *
1213 * @return void
1214 */
1215 public function import_single_step( $args, $action ) {
1216
1217 /**
1218 * Nonce verification
1219 */
1220 if ( ! check_ajax_referer( $action, 'security', false ) ) {
1221 $response_data = array( 'message' => __( 'Nonce verification failed.', 'cartflows' ) );
1222 wp_send_json_error( $response_data );
1223 }
1224
1225 wcf()->logger->import_log( 'STARTED! Importing Step' );
1226
1227 $step_id = isset( $args['step']['id'] ) ? absint( $args['step']['id'] ) : 0;
1228 $step_title = isset( $args['step']['title'] ) ? $args['step']['title'] : '';
1229 $step_type = isset( $args['step']['type'] ) ? $args['step']['type'] : '';
1230 $flow_id = isset( $args['flow']['id'] ) ? absint( $args['flow']['id'] ) : 0;
1231 $is_store_checkout = isset( $args['is_store_checkout'] ) ? $args['is_store_checkout'] : '';
1232
1233 // create steps only for checkout and thankyou if store checkout.
1234 // This logic will be removed once we have store checkout templates on server.
1235 if ( 'true' === $is_store_checkout && ! in_array( $step_type, array( 'checkout', 'thankyou' ), true ) ) {
1236 return;
1237 }
1238
1239 // Create new step.
1240 $new_step_id = \CartFlows_Importer::get_instance()->create_step( $flow_id, $step_type, $step_title );
1241
1242 if ( empty( $step_id ) || empty( $new_step_id ) ) {
1243 /* translators: %s: step ID */
1244 wp_send_json_error( sprintf( __( 'Invalid step id %1$s or post id %2$s.', 'cartflows' ), $step_id, $new_step_id ) );
1245 }
1246
1247 // Handle edge insertion - reorder flow steps when inserting on an edge.
1248 $this->handle_edge_insertion_reorder( $flow_id, $new_step_id );
1249
1250 // Handle offer edge source update - update source offer step's redirect to point to new step.
1251 $this->update_offer_step_source_redirects( $flow_id, $new_step_id );
1252
1253 wcf()->logger->import_log( 'Remote Step ' . $step_id . ' for local flow "' . get_the_title( $new_step_id ) . '" [' . $new_step_id . ']' );
1254
1255 // Get single step Rest API response.
1256 $response = \CartFlows_API::get_instance()->get_template( $step_id );
1257
1258 wcf()->logger->import_log( wp_json_encode( $response ) );
1259
1260 // Return if there is an error while importing the step template.
1261 if ( ! $response['success'] ) {
1262 $response_data = $response['data'];
1263 $error_code = wp_remote_retrieve_response_code( $response_data );
1264 $error_msge = json_decode( wp_remote_retrieve_body( $response_data ) );
1265
1266 wp_send_json_error(
1267 array(
1268 'error_code' => $error_code,
1269 'message' => ! empty( $error_msge->message ) ? $error_msge->message : '',
1270 'data' => $response['data'],
1271 'success' => $response['success'],
1272 )
1273 );
1274 }
1275
1276 if ( 'divi' === \Cartflows_Helper::get_common_setting( 'default_page_builder' ) ) {
1277 if ( isset( $response['data']['divi_content'] ) && ! empty( $response['data']['divi_content'] ) ) {
1278
1279 update_post_meta( $new_step_id, 'divi_content', $response['data']['divi_content'] );
1280
1281 wp_update_post(
1282 array(
1283 'ID' => $new_step_id,
1284 'post_content' => $response['data']['divi_content'],
1285 )
1286 );
1287 }
1288 }
1289
1290 if ( 'gutenberg' === \Cartflows_Helper::get_common_setting( 'default_page_builder' ) ) {
1291 if ( isset( $response['data']['divi_content'] ) && ! empty( $response['data']['divi_content'] ) ) {
1292 wp_update_post(
1293 array(
1294 'ID' => $new_step_id,
1295 'post_content' => $response['data']['divi_content'],
1296 )
1297 );
1298 }
1299 }
1300
1301 // Import Post Meta.
1302 $this->import_post_meta( $new_step_id, $response );
1303
1304 // Handle user-selected offer redirect settings for upsell/downsell steps.
1305 $this->update_user_selected_offer_redirects( $flow_id, $new_step_id, $step_type );
1306
1307 if ( 'checkout' === $step_type ) {
1308
1309 $posted_data = array(
1310 'primary_color' => isset( $_POST['primary_color'] ) ? sanitize_text_field( wp_unslash( $_POST['primary_color'] ) ) : '',
1311 'site_logo' => isset( $_POST['site_logo'] ) ? array_map( 'sanitize_text_field', wp_unslash( $_POST['site_logo'] ) ) : '',
1312 );
1313
1314 $this->update_store_checkout_template_data( $new_step_id, $response, $posted_data );
1315 }
1316
1317 /* Imported Step */
1318 update_post_meta( $new_step_id, 'cartflows_imported_step', 'yes' );
1319
1320 do_action( 'cartflows_import_complete' );
1321
1322 // Batch Process.
1323 do_action( 'cartflows_after_template_import', $new_step_id, $response );
1324
1325 wcf()->logger->import_log( 'COMPLETE! Importing Step' );
1326 }
1327
1328 /**
1329 * Import Post Meta
1330 *
1331 * @since 1.0.0
1332 *
1333 * @param integer $post_id Post ID.
1334 * @param array $response Post meta.
1335 * @return void
1336 */
1337 public function import_post_meta( $post_id, $response ) {
1338
1339 $metadata = apply_filters( 'cartflows_template_import_meta_data', (array) $response['post_meta'] );
1340
1341 $exclude_meta_keys = \Cartflows_Helper::get_instance()->get_meta_keys_to_exclude_from_import( $post_id );
1342
1343 foreach ( $metadata as $meta_key => $meta_value ) {
1344
1345 if ( in_array( $meta_key, $exclude_meta_keys, true ) ) {
1346 continue;
1347 }
1348
1349 // Security: Only allow meta keys matching known prefixes to prevent arbitrary DB writes.
1350 if ( ! \Cartflows_Helper::get_instance()->is_meta_key_allowed_for_import( $meta_key ) ) {
1351 continue;
1352 }
1353
1354 $meta_value = isset( $meta_value[0] ) ? $meta_value[0] : '';
1355
1356 if ( $meta_value ) {
1357
1358 // Beaver Builder layout keys are handled separately from the generic security
1359 // path below. BB stores rows/columns/modules as serialized arrays of stdClass
1360 // objects. We allow only stdClass — no other classes are expected or permitted.
1361 // Passing the raw serialized string directly to update_post_meta would cause
1362 // WordPress to double-serialize it, breaking BB's get_post_meta readback.
1363 $bb_meta_keys = array( '_fl_builder_data', '_fl_builder_draft', '_fl_builder_data_settings' );
1364 if ( in_array( $meta_key, $bb_meta_keys, true ) ) {
1365 $raw_data = $this->unserialize_bb_meta( $meta_key, $meta_value );
1366 if ( false === $raw_data ) {
1367 continue;
1368 }
1369 } elseif ( is_serialized( $meta_value, true ) ) {
1370 // Security: Using unserialize with allowed_classes=>false to prevent object injection.
1371 $raw_data = unserialize( stripslashes( $meta_value ), array( 'allowed_classes' => false ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize, PHPCompatibility.FunctionUse.NewFunctionParameters.unserialize_optionsFound
1372 // Drop malicious payloads completely to prevent fatal errors.
1373 // Security: Block serialized meta that contains nested PHP objects.
1374 // Even with allowed_classes => false, unserialized payloads may contain
1375 // __PHP_Incomplete_Class instances which WordPress attempts to mutate
1376 // during wp_unslash(), causing fatal errors and violating object safety.
1377 if (
1378 false === $raw_data ||
1379 $this->has_nested_object_payload( $raw_data )
1380 ) {
1381 $raw_data = '';
1382 continue;
1383 }
1384 } elseif ( is_array( $meta_value ) ) {
1385 $raw_data = json_decode( stripslashes( $meta_value ), true );
1386 } else {
1387 $raw_data = $meta_value;
1388 }
1389
1390 if ( '_elementor_data' === $meta_key ) {
1391
1392 if ( is_array( $raw_data ) ) {
1393 $raw_data = wp_slash( wp_json_encode( $raw_data ) );
1394 } else {
1395 $raw_data = wp_slash( $raw_data );
1396 }
1397 }
1398
1399 if ( '_elementor_data' !== $meta_key && '_elementor_draft' !== $meta_key && '_fl_builder_data' !== $meta_key && '_fl_builder_draft' !== $meta_key ) {
1400 if ( is_array( $raw_data ) ) {
1401 wcf()->logger->import_log( '✓ Added post meta ' . $meta_key /* . ' | ' . wp_json_encode( $raw_data ) */ );
1402 } elseif ( ! is_object( $raw_data ) ) {
1403 wcf()->logger->import_log( '✓ Added post meta ' . $meta_key /* . ' | ' . $raw_data */ );
1404 }
1405 }
1406 update_post_meta( $post_id, $meta_key, $raw_data );
1407 }
1408 }
1409 }
1410
1411 /**
1412 * Find the Checkout block and set the Primary color and site logo provided by the user.
1413 *
1414 * @since 1.10.0
1415 *
1416 * @param int $post_id newly created steps ID.
1417 * @param array $response data received from from the imported step.
1418 * @param array $posted_data post data.
1419 *
1420 * @return void
1421 */
1422 public function update_store_checkout_template_data( $post_id, $response, $posted_data ) {
1423
1424 $store_checkout_id = get_option( '_cartflows_store_checkout', false );
1425 $current_flow_id = (int) wcf()->utils->get_flow_id_from_step_id( $post_id );
1426 $default_page_bulder = \Cartflows_Helper::get_common_setting( 'default_page_builder' );
1427
1428 if ( empty( $posted_data['primary_color'] ) && empty( $posted_data['site_logo'] ) ) {
1429 return;
1430 }
1431
1432 if ( $store_checkout_id !== $current_flow_id ) {
1433 return;
1434 }
1435
1436 if ( 'elementor' === $default_page_bulder ) {
1437
1438 $metadata = (array) $response['post_meta'];
1439
1440 foreach ( $metadata as $meta_key => $meta_value ) {
1441
1442 $meta_value = isset( $meta_value[0] ) ? $meta_value[0] : '';
1443
1444 if ( $meta_value ) {
1445
1446 if ( is_serialized( $meta_value, true ) ) {
1447 // Security: Using unserialize with allowed_classes=>false to prevent object injection.
1448 $raw_data = unserialize( stripslashes( $meta_value ), array( 'allowed_classes' => false ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize, PHPCompatibility.FunctionUse.NewFunctionParameters.unserialize_optionsFound
1449 if ( false === $raw_data || is_object( $raw_data ) ) {
1450 continue;
1451 }
1452 } elseif ( is_array( $meta_value ) ) {
1453 $raw_data = json_decode( stripslashes( $meta_value ), true );
1454 } else {
1455 $raw_data = $meta_value;
1456 }
1457
1458 if ( '_elementor_data' === $meta_key ) {
1459
1460 $raw_data = json_decode( $raw_data, true );
1461
1462 // Find the checkout-form and update the primary color.
1463 $this->elementor_find_and_replace_template_data( $raw_data, $posted_data );
1464
1465 if ( is_array( $raw_data ) ) {
1466 $raw_data = wp_slash( wp_json_encode( $raw_data ) );
1467 } else {
1468 $raw_data = wp_slash( $raw_data );
1469 }
1470
1471 update_post_meta( $post_id, $meta_key, $raw_data );
1472
1473 }
1474 }
1475 }
1476 } elseif ( 'gutenberg' === $default_page_bulder ) {
1477
1478 $post = get_post( $post_id );
1479 $blocks = parse_blocks( $post->post_content );
1480
1481 if ( is_array( $blocks ) && ! empty( $blocks ) ) {
1482
1483 $this->gutenberg_find_and_replace_template_data( $blocks, $posted_data );
1484
1485 if ( ! empty( $blocks ) ) {
1486 $serialized_blocks = serialize_blocks( $blocks );
1487
1488 wp_update_post(
1489 array(
1490 'ID' => $post_id,
1491 'post_content' => $serialized_blocks,
1492 )
1493 );
1494 }
1495 }
1496 } elseif ( 'beaver-builder' === $default_page_bulder ) {
1497 $data = \FLBuilderModel::get_layout_data( 'published', $post_id );
1498
1499 if ( ! empty( $data ) ) {
1500
1501 $this->beaver_builder_find_and_replace_template_data( $data, $posted_data );
1502
1503 // Update page builder data.
1504 update_post_meta( $post_id, '_fl_builder_data', $data );
1505 update_post_meta( $post_id, '_fl_builder_draft', $data );
1506 }
1507 }
1508 }
1509
1510
1511 /**
1512 * Get flows list for preview
1513 *
1514 * @return void
1515 */
1516 public function get_flows_list() {
1517
1518 $response_data = array( 'message' => $this->get_error_msg( 'permission' ) );
1519
1520 if ( ! current_user_can( 'cartflows_manage_flows_steps' ) ) {
1521 wp_send_json_error( $response_data );
1522 }
1523
1524 /**
1525 * Nonce verification
1526 */
1527 if ( ! check_ajax_referer( 'cartflows_get_flows_list', 'security', false ) ) {
1528 $response_data = array( 'message' => $this->get_error_msg( 'nonce' ) );
1529 wp_send_json_error( $response_data );
1530 }
1531
1532 $flows_list = \Cartflows_Helper::get_instance()->get_flows_and_steps();
1533
1534 /**
1535 * Redirect to the new step edit screen
1536 */
1537 $response_data = array(
1538 'message' => __( 'Successful!', 'cartflows' ),
1539 'flows' => $flows_list,
1540 );
1541
1542 wp_send_json_success( $response_data );
1543 }
1544
1545 /**
1546 * Get the elementor widget data.
1547 *
1548 * @param array $elements elements data.
1549 * @param array $posted_data posted data.
1550 */
1551 public function elementor_find_and_replace_template_data( &$elements, $posted_data ) {
1552
1553 foreach ( $elements as &$element ) {
1554
1555 if ( 'widget' === $element['elType'] && 'checkout-form' === $element['widgetType'] ) {
1556 $element['settings']['global_primary_color'] = ! empty( $posted_data['primary_color'] ) ? $posted_data['primary_color'] : $element['settings']['global_primary_color'];
1557 }
1558
1559 if ( 'widget' === $element['elType'] && 'image' === $element['widgetType'] && isset( $element['settings']['_css_classes'] ) ) {
1560
1561 if ( str_contains( $element['settings']['_css_classes'], 'cartflows-store-checkout-logo-field' ) ) {
1562 $element['settings']['image']['url'] = ! empty( $posted_data['site_logo'] ) ? $posted_data['site_logo']['url'] : $element['settings']['image']['url'];
1563 $element['settings']['image']['id'] = ! empty( $posted_data['site_logo'] ) ? $posted_data['site_logo']['id'] : $element['settings']['image']['id'];
1564 }
1565 }
1566
1567 if ( ! empty( $element['elements'] ) ) {
1568 $this->elementor_find_and_replace_template_data( $element['elements'], $posted_data );
1569 }
1570 }
1571 }
1572
1573 /**
1574 * Get the block data.
1575 *
1576 * @param array $elements elements data.
1577 * @param array $posted_data posted data.
1578 */
1579 public function gutenberg_find_and_replace_template_data( &$elements, $posted_data ) {
1580
1581 foreach ( $elements as &$element ) {
1582 if ( 'wcfb/checkout-form' === $element['blockName'] ) {
1583 // Update the element with the data.
1584 $element['attrs']['globalbgColor'] = ! empty( $posted_data['primary_color'] ) ? $posted_data['primary_color'] : $element['attrs']['globalbgColor'];
1585 }
1586
1587 if ( 'uagb/info-box' === $element['blockName'] && isset( $element['attrs']['className'] ) ) {
1588
1589 if ( str_contains( $element['attrs']['className'], 'cartflows-store-checkout-logo-field' ) ) {
1590 $element['attrs']['iconImage']['id'] = ! empty( $posted_data['site_logo'] ) ? $posted_data['site_logo']['id'] : $element['attrs']['iconImage']['id'];
1591 $element['attrs']['iconImage']['url'] = ! empty( $posted_data['site_logo'] ) ? $posted_data['site_logo']['url'] : $element['attrs']['iconImage']['url'];
1592 $element['attrs']['iconImage']['link'] = ! empty( $posted_data['site_logo'] ) ? $posted_data['site_logo']['url'] : $element['attrs']['iconImage']['link'];
1593 $element['attrs']['iconImage']['sizes']['full']['url'] = ! empty( $posted_data['site_logo'] ) ? $posted_data['site_logo']['url'] : $element['attrs']['iconImage']['link'];
1594 }
1595 }
1596
1597 if ( ! empty( $element['innerBlocks'] ) ) {
1598 $this->gutenberg_find_and_replace_template_data( $element['innerBlocks'], $posted_data );
1599 }
1600 }
1601 }
1602
1603 /**
1604 * Replace the logo and color in the BB template while importing.
1605 *
1606 * @param array $elements elements data.
1607 * @param array $posted_data posted data.
1608 */
1609 public function beaver_builder_find_and_replace_template_data( &$elements, $posted_data ) {
1610
1611 foreach ( $elements as $node => &$element ) {
1612 if ( ! empty( $element->type ) && 'module' === $element->type ) {
1613
1614 if ( ! empty( $element->settings->type ) && 'cartflows-bb-checkout-form' === $element->settings->type ) {
1615 // Update the logo in the template.
1616 $element->settings->global_primary_color = ! empty( $posted_data['primary_color'] ) ? $posted_data['primary_color'] : $element->settings->global_primary_color;
1617 }
1618
1619 if ( ! empty( $element->settings->type ) && 'photo' === $element->settings->type && ! empty( $element->settings->class ) && 'cartflows-store-checkout-logo-field' === $element->settings->class ) {
1620 $module_setting = $element->settings;
1621 // Update the logo in the template.
1622 $element->settings->photo_src = ! empty( $posted_data['site_logo'] ) ? $posted_data['site_logo']['url'] : $element->settings->photo_src;
1623
1624 }
1625 }
1626 }
1627 }
1628
1629 /**
1630 * Imports the Global Colors and Patterns (GCP) variables data for a given flow.
1631 *
1632 * This function updates the post meta for a flow with the GCP variables data received in the response.
1633 * It logs the import process and updates the post meta with the GCP data.
1634 *
1635 * @param array $response The response data containing the GCP variables.
1636 * @param int $flow_id The ID of the flow for which the GCP data is being imported.
1637 *
1638 * @return void
1639 */
1640 public function import_funnel_gcp_vars_data( $response, $flow_id ) {
1641
1642 wcf()->logger->import_log( 'Start: ' . __CLASS__ . ' :: ' . __FUNCTION__ );
1643
1644 wcf()->logger->import_log( 'Newly Imported Flow ID: ' . $flow_id . PHP_EOL . ' Response ' . print_r( $response, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
1645
1646 if ( isset( $response['data']['flow_gcp_meta'] ) && ! empty( $response['data']['flow_gcp_meta'] ) && is_object( $response['data']['flow_gcp_meta'] ) ) {
1647
1648 wcf()->logger->import_log( 'Before Importing:' . print_r( $response['data']['flow_gcp_meta'], true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
1649
1650 $gcp_data = (object) array_map( 'sanitize_text_field', (array) $response['data']['flow_gcp_meta'] );
1651
1652 wcf()->logger->import_log( 'After Importing: ' . print_r( $gcp_data, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
1653
1654 $gcp_meta_keys = array(
1655 'wcf-enable-gcp-styling' => 'yes',
1656 'wcf-gcp-primary-color' => ! empty( $gcp_data->gcp_primary_color ) ? $gcp_data->gcp_primary_color : '',
1657 'wcf-gcp-secondary-color' => ! empty( $gcp_data->gcp_secondary_color ) ? $gcp_data->gcp_secondary_color : '',
1658 'wcf-gcp-text-color' => ! empty( $gcp_data->gcp_text_color ) ? $gcp_data->gcp_text_color : '',
1659 'wcf-gcp-accent-color' => ! empty( $gcp_data->gcp_accent_color ) ? $gcp_data->gcp_accent_color : '',
1660 );
1661
1662 foreach ( $gcp_meta_keys as $key => $value ) {
1663 update_post_meta( $flow_id, $key, $value );
1664 }
1665 }
1666
1667 wcf()->logger->import_log( 'End: ' . __CLASS__ . ' :: ' . __FUNCTION__ );
1668 }
1669
1670 /**
1671 * Deserialize a Beaver Builder layout meta value.
1672 *
1673 * BB layout keys (_fl_builder_data, _fl_builder_draft, _fl_builder_data_settings)
1674 * store arrays of stdClass nodes. Only stdClass is allowed during unserialize —
1675 * no other classes are expected in BB layout data and none should be permitted.
1676 *
1677 * Returns false if the value is serialized but cannot be unserialized, so the
1678 * caller can skip the key rather than storing corrupt data silently.
1679 *
1680 * @since 3.0.2
1681 *
1682 * @param string $meta_key The meta key being processed (used for error logging).
1683 * @param mixed $meta_value The raw meta value from the template API response.
1684 * @return mixed Unserialized value, original value if not serialized, or false on failure.
1685 */
1686 private function unserialize_bb_meta( $meta_key, $meta_value ) {
1687 if ( ! is_serialized( $meta_value ) ) {
1688 return $meta_value;
1689 }
1690
1691 // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize, PHPCompatibility.FunctionUse.NewFunctionParameters.unserialize_optionsFound -- BB layout data uses only stdClass; no arbitrary classes are permitted.
1692 $result = unserialize( $meta_value, array( 'allowed_classes' => array( 'stdClass' ) ) );
1693
1694 if ( false === $result ) {
1695 wcf()->logger->import_log( 'Failed to unserialize BB meta key: ' . $meta_key );
1696 return false;
1697 }
1698
1699 return $result;
1700 }
1701
1702 /**
1703 * Detect whether the given value contains objects at any depth.
1704 *
1705 * This is used as a hard security guard to prevent storing
1706 * unserialized payloads that contain PHP objects (including
1707 * __PHP_Incomplete_Class), which can cause fatal errors and
1708 * violate object injection protections.
1709 *
1710 * @since 2.2.1
1711 *
1712 * @param mixed $value The value to inspect recursively.
1713 *
1714 * @return bool True if an object is found anywhere in the payload.
1715 */
1716 private function has_nested_object_payload( $value ) {
1717
1718 if ( is_object( $value ) ) {
1719 return true;
1720 }
1721
1722 if ( is_array( $value ) ) {
1723 foreach ( $value as $v ) {
1724 if ( $this->has_nested_object_payload( $v ) ) {
1725 return true;
1726 }
1727 }
1728 }
1729
1730 return false;
1731 }
1732
1733 /**
1734 * Handle edge insertion reorder.
1735 *
1736 * Reorders flow steps when a new step is inserted on an edge between two existing steps.
1737 * Handles both START node edges and regular step edges.
1738 *
1739 * @since 3.1.0
1740 * @param int $flow_id The flow ID.
1741 * @param int $new_step_id The newly created step ID.
1742 * @return void
1743 */
1744 private function handle_edge_insertion_reorder( $flow_id, $new_step_id ) {
1745
1746 // Per-call IDOR guard — re-verify the current user can edit this flow.
1747 if ( ! $this->user_can_edit_flow( $flow_id ) ) {
1748 return;
1749 }
1750
1751 $is_start_edge = isset( $_POST['is_start_edge'] ) && 'true' === $_POST['is_start_edge']; // phpcs:ignore WordPress.Security.NonceVerification.Missing
1752 $edge_source_step_id = isset( $_POST['edge_source_step_id'] ) ? absint( $_POST['edge_source_step_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing
1753 $edge_target_step_id = isset( $_POST['edge_target_step_id'] ) ? absint( $_POST['edge_target_step_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing
1754
1755 // Handle edge insertion if we have a target step ID (either from START node or regular step).
1756 if ( ! $edge_target_step_id || ( ! $is_start_edge && ! $edge_source_step_id ) ) {
1757 return;
1758 }
1759
1760 // IDOR guard — both edge endpoints must belong to this flow.
1761 // The START node is virtual and has no step ID, so skip its check.
1762 if ( ! $is_start_edge && ! $this->is_step_in_flow( $edge_source_step_id, $flow_id ) ) {
1763 return;
1764 }
1765 if ( ! $this->is_step_in_flow( $edge_target_step_id, $flow_id ) ) {
1766 return;
1767 }
1768
1769 // Get current flow steps.
1770 $flow_steps = get_post_meta( (int) $flow_id, 'wcf-steps', true );
1771
1772 if ( ! is_array( $flow_steps ) || empty( $flow_steps ) ) {
1773 return;
1774 }
1775
1776 // Find indices of source and target steps.
1777 $source_index = -1; // -1 for START node means insert at position 0.
1778 $target_index = -1;
1779 $new_step_index = -1;
1780
1781 foreach ( $flow_steps as $index => $step_data ) {
1782 if ( isset( $step_data['id'] ) ) {
1783 if ( ! $is_start_edge && $step_data['id'] === $edge_source_step_id ) {
1784 $source_index = $index;
1785 }
1786 if ( $step_data['id'] === $edge_target_step_id ) {
1787 $target_index = $index;
1788 }
1789 if ( $step_data['id'] === $new_step_id ) {
1790 $new_step_index = $index;
1791 }
1792 }
1793 }
1794
1795 // Determine if we can proceed with reordering.
1796 $can_reorder = -1 !== $target_index && -1 !== $new_step_index;
1797 if ( ! $is_start_edge ) {
1798 $can_reorder = $can_reorder && -1 !== $source_index;
1799 }
1800
1801 if ( ! $can_reorder ) {
1802 return;
1803 }
1804
1805 // Remove the new step from its current position (at the end).
1806 $new_step_data = $flow_steps[ $new_step_index ];
1807 array_splice( $flow_steps, $new_step_index, 1 );
1808
1809 // Recalculate indices after removal (if new step was before them).
1810 if ( ! $is_start_edge && $new_step_index < $source_index ) {
1811 --$source_index;
1812 }
1813 if ( $new_step_index < $target_index ) {
1814 --$target_index;
1815 }
1816
1817 // Insert the new step at the correct position.
1818 if ( $is_start_edge ) {
1819 // START node: insert at position 0 (beginning of flow).
1820 $insert_position = 0;
1821 } else {
1822 // Regular step: insert right after the source step.
1823 $insert_position = $source_index + 1;
1824 }
1825
1826 array_splice( $flow_steps, (int) $insert_position, 0, array( $new_step_data ) );
1827
1828 // Update the flow steps meta.
1829 update_post_meta( (int) $flow_id, 'wcf-steps', $flow_steps );
1830 }
1831
1832 /**
1833 * Update offer step source redirects.
1834 *
1835 * When inserting on an Accept/Reject edge, updates the source offer step's
1836 * redirect to point to the new step.
1837 *
1838 * @since 3.1.0
1839 * @param int $flow_id The flow ID.
1840 * @param int $new_step_id The newly created step ID.
1841 * @return void
1842 */
1843 private function update_offer_step_source_redirects( $flow_id, $new_step_id ) {
1844
1845 // Per-call IDOR guard — re-verify the current user can edit this flow.
1846 if ( ! $this->user_can_edit_flow( $flow_id ) ) {
1847 return;
1848 }
1849
1850 $edge_source_step_id = isset( $_POST['edge_source_step_id'] ) ? absint( $_POST['edge_source_step_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing
1851 $edge_source_handle = isset( $_POST['edge_source_handle'] ) ? sanitize_text_field( wp_unslash( $_POST['edge_source_handle'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing
1852
1853 if ( $edge_source_step_id <= 0 || empty( $edge_source_handle ) ) {
1854 return;
1855 }
1856
1857 // IDOR guard — both source and target steps must belong to this flow.
1858 if ( ! $this->is_step_in_flow( $edge_source_step_id, $flow_id ) ) {
1859 return;
1860 }
1861 if ( ! $this->is_step_in_flow( $new_step_id, $flow_id ) ) {
1862 return;
1863 }
1864
1865 $source_step_type = get_post_meta( $edge_source_step_id, 'wcf-step-type', true );
1866
1867 // Only update if source is an offer step (upsell/downsell).
1868 if ( ! in_array( $source_step_type, array( 'upsell', 'downsell' ), true ) ) {
1869 return;
1870 }
1871
1872 $source_meta_updated = false;
1873
1874 // Update the appropriate post meta based on handle.
1875 if ( 'a' === $edge_source_handle ) {
1876 // Accept edge - update wcf-yes-next-step.
1877 update_post_meta( $edge_source_step_id, 'wcf-yes-next-step', $new_step_id );
1878 $source_meta_updated = true;
1879 } elseif ( 'b' === $edge_source_handle ) {
1880 // Reject edge - update wcf-no-next-step.
1881 update_post_meta( $edge_source_step_id, 'wcf-no-next-step', $new_step_id );
1882 $source_meta_updated = true;
1883 }
1884
1885 // Also update the flow_steps array to keep it in sync.
1886 if ( $source_meta_updated ) {
1887 $this->sync_offer_redirects_in_flow_steps( $flow_id, $edge_source_step_id, $edge_source_handle, $new_step_id );
1888 }
1889 }
1890
1891 /**
1892 * Sync offer redirects in flow_steps array.
1893 *
1894 * Updates the flow_steps array to keep it in sync with post meta.
1895 * Handles both control steps and A/B test variations.
1896 *
1897 * @since 3.1.0
1898 * @param int $flow_id The flow ID.
1899 * @param int $source_step_id The source offer step ID.
1900 * @param string $handle The edge handle ('a' for accept, 'b' for reject).
1901 * @param int $target_step_id The target step ID to redirect to.
1902 * @return void
1903 */
1904 private function sync_offer_redirects_in_flow_steps( $flow_id, $source_step_id, $handle, $target_step_id ) {
1905 $updated_flow_steps = get_post_meta( (int) $flow_id, 'wcf-steps', true );
1906
1907 if ( ! is_array( $updated_flow_steps ) ) {
1908 return;
1909 }
1910
1911 $flow_steps_updated = false;
1912
1913 foreach ( $updated_flow_steps as $idx => $step_data ) {
1914 // Check if this is the control step (direct match).
1915 if ( isset( $step_data['id'] ) && (int) $step_data['id'] === (int) $source_step_id ) {
1916 if ( 'a' === $handle ) {
1917 $updated_flow_steps[ $idx ]['offer_yes_step_id'] = $target_step_id;
1918 } elseif ( 'b' === $handle ) {
1919 $updated_flow_steps[ $idx ]['offer_no_step_id'] = $target_step_id;
1920 }
1921 $flow_steps_updated = true;
1922 break;
1923 }
1924
1925 // Check if the source is an A/B test variation within this step.
1926 if ( isset( $step_data['ab-test-variations'] ) && is_array( $step_data['ab-test-variations'] ) ) {
1927 foreach ( $step_data['ab-test-variations'] as $var_idx => $variation ) {
1928 if ( isset( $variation['id'] ) && (int) $variation['id'] === (int) $source_step_id ) {
1929 if ( 'a' === $handle ) {
1930 $updated_flow_steps[ $idx ]['ab-test-variations'][ $var_idx ]['offer_yes_step_id'] = $target_step_id;
1931 } elseif ( 'b' === $handle ) {
1932 $updated_flow_steps[ $idx ]['ab-test-variations'][ $var_idx ]['offer_no_step_id'] = $target_step_id;
1933 }
1934 $flow_steps_updated = true;
1935 break 2; // Break out of both loops.
1936 }
1937 }
1938 }
1939 }
1940
1941 if ( $flow_steps_updated ) {
1942 update_post_meta( (int) $flow_id, 'wcf-steps', $updated_flow_steps );
1943 }
1944 }
1945
1946 /**
1947 * Update user-selected offer redirects.
1948 *
1949 * Handles user-selected offer redirect settings for upsell/downsell steps
1950 * when the user explicitly selects Accept/Reject targets via the UI.
1951 *
1952 * @since 3.1.0
1953 * @param int $flow_id The flow ID.
1954 * @param int $new_step_id The newly created step ID.
1955 * @param string $step_type The step type.
1956 * @return void
1957 */
1958 private function update_user_selected_offer_redirects( $flow_id, $new_step_id, $step_type ) {
1959
1960 // Per-call IDOR guard — re-verify the current user can edit this flow.
1961 if ( ! $this->user_can_edit_flow( $flow_id ) ) {
1962 return;
1963 }
1964
1965 // Only process for offer steps.
1966 if ( ! in_array( $step_type, array( 'upsell', 'downsell' ), true ) ) {
1967 return;
1968 }
1969
1970 // IDOR guard — the new step itself must belong to this flow.
1971 if ( ! $this->is_step_in_flow( $new_step_id, $flow_id ) ) {
1972 return;
1973 }
1974
1975 $user_offer_yes_step_id = isset( $_POST['offer_yes_step_id'] ) ? absint( $_POST['offer_yes_step_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing
1976 $user_offer_no_step_id = isset( $_POST['offer_no_step_id'] ) ? absint( $_POST['offer_no_step_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing
1977
1978 // IDOR guard — redirect targets must belong to this flow if provided.
1979 if ( $user_offer_yes_step_id && ! $this->is_step_in_flow( $user_offer_yes_step_id, $flow_id ) ) {
1980 $user_offer_yes_step_id = 0;
1981 }
1982 if ( $user_offer_no_step_id && ! $this->is_step_in_flow( $user_offer_no_step_id, $flow_id ) ) {
1983 $user_offer_no_step_id = 0;
1984 }
1985
1986 // Return if user didn't select any custom redirects.
1987 if ( ! $user_offer_yes_step_id && ! $user_offer_no_step_id ) {
1988 return;
1989 }
1990
1991 $current_flow_steps = get_post_meta( (int) $flow_id, 'wcf-steps', true );
1992
1993 if ( ! is_array( $current_flow_steps ) ) {
1994 return;
1995 }
1996
1997 foreach ( $current_flow_steps as $idx => $step_data ) {
1998 if ( isset( $step_data['id'] ) && $step_data['id'] === $new_step_id ) {
1999 // Update offer_yes_step_id if user selected one.
2000 if ( $user_offer_yes_step_id ) {
2001 $current_flow_steps[ $idx ]['offer_yes_step_id'] = $user_offer_yes_step_id;
2002 update_post_meta( $new_step_id, 'wcf-yes-next-step', $user_offer_yes_step_id );
2003 }
2004
2005 // Update offer_no_step_id if user selected one.
2006 if ( $user_offer_no_step_id ) {
2007 $current_flow_steps[ $idx ]['offer_no_step_id'] = $user_offer_no_step_id;
2008 update_post_meta( $new_step_id, 'wcf-no-next-step', $user_offer_no_step_id );
2009 }
2010 break;
2011 }
2012 }
2013
2014 // Save the updated flow steps.
2015 update_post_meta( (int) $flow_id, 'wcf-steps', $current_flow_steps );
2016 }
2017 }
2018