PluginProbe
CartFlows – Funnel Builder & Checkout Plugin for WooCommerce / 3.2.1
CartFlows – Funnel Builder & Checkout Plugin for WooCommerce v3.2.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.2.1, at admin-core/ajax/importer.php

2,025 lines 69.3 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 do_action( 'cartflows_generate_analytics_lead', $flow, false );
731
732 wp_send_json_error(
733 array(
734 'error_code' => $is_error['error_code'],
735 'call_to_action' => $is_error['call_to_action'],
736 'message' => $is_error['error_message'],
737 'data' => $response,
738 )
739 );
740 }
741
742 $license_status = isset( $response['data']['licence_status'] ) ? $response['data']['licence_status'] : '';
743
744 // If license is invalid then.
745 if ( 'valid' !== $license_status ) {
746
747 $cf_pro_status = AdminMenu::get_instance()->get_plugin_status( 'cartflows-pro/cartflows-pro.php' );
748
749 $cta = '';
750 $btn = '';
751 if ( 'not-installed' === $cf_pro_status ) {
752 /* translators: %1$s: link html start, %2$s: link html end*/
753 $btn = sprintf( __( 'CartFlows Pro Required! %1$sUpgrade to CartFlows Pro%2$s', 'cartflows' ), '<a target="_blank" href="' . esc_url( \Cartflows_Helper::get_upgrade_to_pro_link( '', 'https://cartflows.com/' ) ) . '">', '</a>' );
754 /* translators: %1$s: link html start, %2$s: link html end*/
755 $cta = sprintf( __( 'To import the premium flow %1$supgrade to CartFlows Pro%2$s.', 'cartflows' ), '<a target="_blank" href="' . esc_url( \Cartflows_Helper::get_upgrade_to_pro_link( '', 'https://cartflows.com/' ) ) . '">', '</a>' );
756 } elseif ( 'inactive' === $cf_pro_status ) {
757 /* translators: %1$s: link html start, %2$s: link html end*/
758 $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>' );
759 /* translators: %1$s: link html start, %2$s: link html end*/
760 $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>' );
761 } elseif ( 'active' === $cf_pro_status ) {
762 /* translators: %1$s: link html start, %2$s: link html end*/
763 $btn = sprintf( __( 'Invalid License Key! %1$sActivate CartFlows Pro%2$s', 'cartflows' ), '<a target="_blank" href="' . admin_url( 'plugins.php?cartflows-license-popup' ) . '">', '</a>' );
764 /* translators: %1$s: link html start, %2$s: link html end*/
765 $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>' );
766 }
767
768 wp_send_json_error(
769 array(
770 'message' => \ucfirst( $license_status ) . ' license key!',
771 'call_to_action' => $btn,
772 'data' => $response,
773 )
774 );
775 }
776
777 if ( empty( $flow ) ) {
778 $response_data = array( 'message' => __( 'Funnel data not found.', 'cartflows' ) );
779 wp_send_json_error( $response_data );
780 }
781
782 // Capture creation method before insert so the meta is in place
783 // when transition_post_status fires on publish.
784 $creation_method = isset( $_POST['creation_method'] ) ? sanitize_text_field( wp_unslash( $_POST['creation_method'] ) ) : 'ready_made_template';
785
786 /**
787 * Create Flow
788 */
789 $new_flow_post = array(
790 'post_title' => isset( $_POST['flow_name'] ) ? sanitize_text_field( wp_unslash( $_POST['flow_name'] ) ) : '',
791 'post_content' => '',
792 'post_status' => 'publish',
793 'post_type' => CARTFLOWS_FLOW_POST_TYPE,
794 'meta_input' => array(
795 'wcf-flow-creation-method' => $creation_method,
796 ),
797 );
798
799 // Insert the post into the database.
800 $new_flow_id = wp_insert_post( $new_flow_post );
801
802 if ( is_wp_error( $new_flow_id ) ) {
803 do_action( 'cartflows_generate_analytics_lead', $flow, false );
804 wp_send_json_error( $new_flow_id->get_error_message() );
805 }
806
807 $store_checkout = isset( $_POST['store_checkout'] ) ? sanitize_text_field( wp_unslash( $_POST['store_checkout'] ) ) : '';
808
809 // If is global checkout update store_checkout options.
810 if ( 'true' === $store_checkout ) {
811 update_option( '_cartflows_store_checkout', $new_flow_id );
812
813 // reset global checkout on store checkout creation.
814 $common_settings = \Cartflows_Helper::get_common_settings();
815 $common_settings['global_checkout'] = '';
816 $common_settings['override_global_checkout'] = 'disable';
817
818 update_option( '_cartflows_common', $common_settings );
819 }
820
821 // Import the Global Colors Data if exists.
822 $this->import_funnel_gcp_vars_data( $response, $new_flow_id );
823
824 wcf()->logger->import_log( '✓ Flow Created! Flow ID: ' . $new_flow_id . ' - Remote Flow ID - ' . $flow['ID'] );
825
826 /**
827 * All Import Steps
828 */
829 $steps = isset( $flow['steps'] ) ? $flow['steps'] : array();
830
831 // Return of no steps are found in the imported flow.
832 if ( empty( $steps ) ) {
833 do_action( 'cartflows_generate_analytics_lead', $flow, false );
834 $response_data = array( 'message' => __( 'Steps not found.', 'cartflows' ) );
835 wp_send_json_error( $response_data );
836 }
837
838 // Set the flag as true to check for the import process is started for the CartFlows. So as to import/upload the required files.
839 \CartFlows_Batch_Process::set_is_wcf_template_import( true );
840
841 foreach ( $steps as $key => $step ) {
842
843 if ( in_array( $step['type'], array( 'upsell', 'downsell' ), true ) && ( ! _is_cartflows_pro() || is_wcf_starter_plan() ) ) {
844 continue;
845 }
846
847 $this->import_single_step(
848 array(
849 'step' => array(
850 'id' => $step['ID'],
851 'title' => $step['title'],
852 'type' => $step['type'],
853 ),
854 'flow' => array(
855 'id' => $new_flow_id,
856 ),
857 'is_store_checkout' => isset( $_POST['store_checkout'] ) ? sanitize_text_field( wp_unslash( $_POST['store_checkout'] ) ) : '',
858 ),
859 'cartflows_import_flow'
860 );
861 }
862
863 /**
864 * Redirect to the new flow edit screen
865 */
866 $response_data = array(
867 'message' => __( 'Successfully imported the Flow!', 'cartflows' ),
868 'items' => $flow,
869 'redirect_url' => admin_url( 'post.php?action=edit&post=' . $new_flow_id ),
870 'new_flow_id' => $new_flow_id,
871 );
872
873 // Set the flag as false once the template import is complete.
874 \CartFlows_Batch_Process::set_is_wcf_template_import( false );
875
876 // Check if the user has imported their first flow.
877 $first_flow_imported = get_option( 'wcf_first_flow_imported', false );
878
879 if ( ! $first_flow_imported ) {
880 update_option( 'wcf_first_flow_imported', true );
881 }
882
883 // Track first template imported event.
884 \Cartflows_Helper::set_analytics_flag( 'first_template_imported' );
885
886 // Track funnel creation method for analytics.
887 // $creation_method was captured and persisted as post meta before wp_insert_post above.
888 AdminHelper::track_funnel_creation_method( $creation_method );
889
890 wcf()->logger->import_log( 'COMPLETE! Importing Flow' );
891
892 // Report the completed template import to the CartFlows server.
893 do_action( 'cartflows_generate_analytics_lead', $flow, true );
894
895 wp_send_json_success( $response_data );
896 }
897
898 /**
899 * Import Step
900 *
901 * @return void
902 */
903 public function import_step() {
904
905 wcf()->logger->import_log( 'STARTED! Importing Step' );
906
907 $response_data = array( 'message' => $this->get_error_msg( 'permission' ) );
908
909 if ( ! current_user_can( 'cartflows_manage_flows_steps' ) ) {
910 wp_send_json_error( $response_data );
911 }
912
913 /**
914 * Nonce verification
915 */
916 if ( ! check_ajax_referer( 'cartflows_import_step', 'security', false ) ) {
917 $response_data = array( 'message' => $this->get_error_msg( 'nonce' ) );
918 wp_send_json_error( $response_data );
919 }
920
921 // $_POST['step'] is the JSON, There is nothing to sanitize JSON as it is data format not data type. Hence sanitizing decoded array below.
922 $step = isset( $_POST['step'] ) ? json_decode( stripslashes( $_POST['step'] ), true ) : array(); //phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
923
924 // Return if step data not found in the import request.
925 if ( empty( $step ) ) {
926 wp_send_json_error( array( 'message' => __( 'Step data ID not found for import.', 'cartflows' ) ) );
927 }
928
929 // Sanitizing decoded array.
930 $step = array_map( 'sanitize_text_field', $step );
931
932 $flow_id = isset( $_POST['flow_id'] ) ? absint( $_POST['flow_id'] ) : 0;
933
934 $remote_flow_id = isset( $_POST['remote_flow_id'] ) ? absint( $_POST['remote_flow_id'] ) : 0;
935
936 // Return if the remote flow ID is blank or not found in the request.
937 if ( empty( $remote_flow_id ) || empty( $flow_id ) ) {
938 wp_send_json_error( array( 'message' => __( 'Funnel ID not found in the request.', 'cartflows' ) ) );
939 }
940
941 // Get single step Rest API response.
942 $response = \CartFlows_API::get_instance()->get_flow( $remote_flow_id );
943
944 if ( is_wp_error( $response['data'] ) ) {
945 /* translators: %1$s: html tag, %2$s: link html start %3$s: link html end */
946 $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="' . esc_url( \Cartflows_Helper::get_kb_doc_link( 'https://cartflows.com/docs/request-timeout-error-while-importing-the-flow-step-templates/' ) ) . '">', '</a>' );
947
948 wp_send_json_error(
949 array(
950 'message' => $response['data']->get_error_message(),
951 'call_to_action' => $btn,
952 'data' => $response,
953 )
954 );
955 }
956
957 $license_status = isset( $response['data']['licence_status'] ) ? $response['data']['licence_status'] : '';
958
959 // If license is invalid then.
960 if ( 'valid' !== $license_status ) {
961
962 $cf_pro_status = AdminMenu::get_instance()->get_plugin_status( 'cartflows-pro/cartflows-pro.php' );
963
964 $msg = '';
965 $cta = '';
966 if ( 'not-installed' === $cf_pro_status ) {
967 /* translators: %1$s: link html start, %2$s: link html end*/
968 $cta = sprintf( __( '%1$sUpgrade to CartFlows Pro.%2$s', 'cartflows' ), '<a target="_blanks" class="wcf-button wcf-primary-button" href="' . esc_url( \Cartflows_Helper::get_upgrade_to_pro_link( '', 'https://cartflows.com/' ) ) . '">', '</a>' );
969 $msg = __( 'To import the premium step, please upgrade to CartFlows Pro', 'cartflows' );
970 } elseif ( 'inactive' === $cf_pro_status ) {
971 /* translators: %1$s: link html start, %2$s: link html end*/
972 $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>' );
973 $msg = __( 'To import the premium step activate Cartflows Pro and validate the license key.', 'cartflows' );
974 } elseif ( 'active' === $cf_pro_status ) {
975 /* translators: %1$s: link html start, %2$s: link html end*/
976 $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>' );
977 $msg = __( 'To import the premium step activate the CartFlows Pro.', 'cartflows' );
978 }
979
980 wp_send_json_error(
981 array(
982 'message' => \ucfirst( $license_status ) . ' license key! ' . $msg,
983 'call_to_action' => $cta,
984 'data' => $response,
985 )
986 );
987 }
988
989 $step['title'] = isset( $_POST['step_name'] ) && ! empty( $_POST['step_name'] ) ? sanitize_text_field( wp_unslash( $_POST['step_name'] ) ) : $step['title'];
990
991 // Set the flag as true to check for the import process is started for the CartFlows. So as to import/upload the required files.
992 \CartFlows_Batch_Process::set_is_wcf_template_import( true );
993
994 // Create steps.
995 $this->import_single_step(
996 array(
997 'step' => array(
998 'id' => $step['ID'],
999 'title' => $step['title'],
1000 'type' => $step['type'],
1001 ),
1002 'flow' => array(
1003 'id' => $flow_id,
1004 ),
1005 ),
1006 'cartflows_import_step'
1007 );
1008
1009 wcf()->logger->import_log( 'COMPLETE! Importing Step' );
1010
1011 if ( empty( $step ) ) {
1012 $response_data = array( 'message' => __( 'Step data not found.', 'cartflows' ) );
1013 wp_send_json_error( $response_data );
1014 }
1015
1016 /**
1017 * Redirect to the new step edit screen
1018 */
1019 $response_data = array(
1020 'message' => __( 'Successfully imported the Step!', 'cartflows' ),
1021 );
1022
1023 // Set the flag as false once the template import is complete.
1024 \CartFlows_Batch_Process::set_is_wcf_template_import( false );
1025
1026 // Track first template imported event.
1027 \Cartflows_Helper::set_analytics_flag( 'first_template_imported' );
1028
1029 wcf()->logger->import_log( 'COMPLETE! Importing Step' );
1030
1031 wp_send_json_success( $response_data );
1032 }
1033
1034 /**
1035 * Updates post content of chosen template.
1036 * working only for Store Checkout.
1037 *
1038 * @return void
1039 * @since X.X.X
1040 */
1041 public function update_step() {
1042
1043 wcf()->logger->import_log( 'STARTED! Updating Step' );
1044
1045 $response_data = array( 'message' => $this->get_error_msg( 'permission' ) );
1046
1047 if ( ! current_user_can( 'cartflows_manage_flows_steps' ) ) {
1048 wp_send_json_error( $response_data );
1049 }
1050
1051 /**
1052 * Nonce verification
1053 */
1054 if ( ! check_ajax_referer( 'cartflows_update_step', 'security', false ) ) {
1055 $response_data = array( 'message' => $this->get_error_msg( 'nonce' ) );
1056 wp_send_json_error( $response_data );
1057 }
1058
1059 // $_POST['step'] is the JSON, There is nothing to sanitize JSON as it is data format not data type. Hence sanitizing decoded array below.
1060 $step = isset( $_POST['step'] ) ? json_decode( stripslashes( $_POST['step'] ), true ) : array(); //phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1061 // Sanitizing decoded array.
1062 $step = array_map( 'sanitize_text_field', $step );
1063 $flow_id = isset( $_POST['flow_id'] ) ? absint( $_POST['flow_id'] ) : 0;
1064 $step_id = isset( $_POST['step_id'] ) ? absint( $_POST['step_id'] ) : 0;
1065
1066 $remote_flow_id = isset( $_POST['remote_flow_id'] ) ? absint( $_POST['remote_flow_id'] ) : 0;
1067
1068 // Get single step Rest API response.
1069 $response = \CartFlows_API::get_instance()->get_flow( $remote_flow_id );
1070 if ( is_wp_error( $response['data'] ) ) {
1071 /* translators: %1$s: html tag, %2$s: link html start %3$s: link html end */
1072 $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="' . esc_url( \Cartflows_Helper::get_kb_doc_link( 'https://cartflows.com/docs/request-timeout-error-while-importing-the-flow-step-templates/' ) ) . '">', '</a>' );
1073
1074 wp_send_json_error(
1075 array(
1076 'message' => $response['data']->get_error_message(),
1077 'call_to_action' => $btn,
1078 'data' => $response,
1079 )
1080 );
1081 }
1082
1083 $license_status = isset( $response['data']['licence_status'] ) ? $response['data']['licence_status'] : '';
1084
1085 // If license is invalid then.
1086 if ( 'valid' !== $license_status ) {
1087
1088 $cf_pro_status = AdminMenu::get_instance()->get_plugin_status( 'cartflows-pro/cartflows-pro.php' );
1089
1090 $cta = '';
1091 if ( 'not-installed' === $cf_pro_status ) {
1092 /* translators: %1$s: link html start, %2$s: link html end*/
1093 $cta = sprintf( __( 'Upgrade to %1$sCartFlows Pro.%2$s', 'cartflows' ), '<a target="_blanks" href="' . esc_url( \Cartflows_Helper::get_upgrade_to_pro_link( '', 'https://cartflows.com/' ) ) . '">', '</a>' );
1094 } elseif ( 'inactive' === $cf_pro_status ) {
1095 /* translators: %1$s: link html start, %2$s: link html end*/
1096 $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>' );
1097 } elseif ( 'active' === $cf_pro_status ) {
1098 /* translators: %1$s: link html start, %2$s: link html end*/
1099 $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>' );
1100 }
1101
1102 wp_send_json_error(
1103 array(
1104 'message' => \ucfirst( $license_status ) . ' license key! ' . $cta,
1105 'data' => $response,
1106 )
1107 );
1108 }
1109
1110 if ( empty( $remote_flow_id ) ) {
1111 $response_data = array( 'message' => __( 'Funnel data not found.', 'cartflows' ) );
1112 wp_send_json_error( $response_data );
1113 }
1114 $step['title'] = isset( $_POST['step_name'] ) && ! empty( $_POST['step_name'] ) ? sanitize_text_field( wp_unslash( $_POST['step_name'] ) ) : $step['title'];
1115 // Create steps.
1116 $this->update_single_step(
1117 array(
1118 'step' => array(
1119 'id' => $step['ID'],
1120 'title' => $step['title'],
1121 'type' => $step['type'],
1122 ),
1123 'flow' => array(
1124 'id' => $flow_id,
1125 ),
1126 'current_step' => array(
1127 'id' => $step_id,
1128 ),
1129 )
1130 );
1131
1132 wcf()->logger->import_log( 'COMPLETE! Importing Step' );
1133
1134 if ( empty( $step ) ) {
1135 $response_data = array( 'message' => __( 'Step data not found.', 'cartflows' ) );
1136 wp_send_json_error( $response_data );
1137 }
1138
1139 /**
1140 * Redirect to the new step edit screen
1141 */
1142 $response_data = array(
1143 'message' => __( 'Successfully imported the Step!', 'cartflows' ),
1144 );
1145
1146 wcf()->logger->import_log( 'COMPLETE! Importing Step' );
1147
1148 wp_send_json_success( $response_data );
1149 }
1150
1151 /**
1152 * Update Sinple Step
1153 *
1154 * @param array $args Rest API Arguments.
1155 * @return void
1156 */
1157 public function update_single_step( $args = array() ) {
1158
1159 wcf()->logger->import_log( 'STARTED! Updating Step' );
1160
1161 $step_id = isset( $args['step']['id'] ) ? absint( $args['step']['id'] ) : 0;
1162 $new_step_id = isset( $args['current_step']['id'] ) ? absint( $args['current_step']['id'] ) : '';
1163
1164 if ( empty( $step_id ) || empty( $new_step_id ) ) {
1165 /* translators: %s: step ID */
1166 wp_send_json_error( sprintf( __( 'Invalid step id %1$s or post id %2$s.', 'cartflows' ), $step_id, $new_step_id ) );
1167 }
1168
1169 wcf()->logger->import_log( 'Remote Step ' . $step_id . ' for local flow "' . get_the_title( $new_step_id ) . '" [' . $new_step_id . ']' );
1170
1171 // Get single step Rest API response.
1172 $response = \CartFlows_API::get_instance()->get_template( $step_id );
1173 wcf()->logger->import_log( wp_json_encode( $response ) );
1174
1175 if ( 'divi' === \Cartflows_Helper::get_common_setting( 'default_page_builder' ) ) {
1176 if ( isset( $response['data']['divi_content'] ) && ! empty( $response['data']['divi_content'] ) ) {
1177
1178 update_post_meta( $new_step_id, 'divi_content', $response['data']['divi_content'] );
1179
1180 wp_update_post(
1181 array(
1182 'ID' => $new_step_id,
1183 'post_content' => $response['data']['divi_content'],
1184 )
1185 );
1186 }
1187 }
1188
1189 if ( 'gutenberg' === \Cartflows_Helper::get_common_setting( 'default_page_builder' ) ) {
1190 if ( isset( $response['data']['divi_content'] ) && ! empty( $response['data']['divi_content'] ) ) {
1191 wp_update_post(
1192 array(
1193 'ID' => $new_step_id,
1194 'post_content' => $response['data']['divi_content'],
1195 )
1196 );
1197 }
1198 }
1199
1200 // Import Post Meta.
1201 $this->import_post_meta( $new_step_id, $response );
1202
1203 /* Imported Step */
1204 update_post_meta( $new_step_id, 'cartflows_imported_step', 'yes' );
1205
1206 do_action( 'cartflows_import_complete' );
1207
1208 // Batch Process.
1209 do_action( 'cartflows_after_template_import', $new_step_id, $response );
1210
1211 wcf()->logger->import_log( 'COMPLETE! Importing Step' );
1212 }
1213
1214 /**
1215 * Create Simple Step
1216 *
1217 * @param array $args Rest API Arguments.
1218 * @param string $action action name to check nonce.
1219 *
1220 * @return void
1221 */
1222 public function import_single_step( $args, $action ) {
1223
1224 /**
1225 * Nonce verification
1226 */
1227 if ( ! check_ajax_referer( $action, 'security', false ) ) {
1228 $response_data = array( 'message' => __( 'Nonce verification failed.', 'cartflows' ) );
1229 wp_send_json_error( $response_data );
1230 }
1231
1232 wcf()->logger->import_log( 'STARTED! Importing Step' );
1233
1234 $step_id = isset( $args['step']['id'] ) ? absint( $args['step']['id'] ) : 0;
1235 $step_title = isset( $args['step']['title'] ) ? $args['step']['title'] : '';
1236 $step_type = isset( $args['step']['type'] ) ? $args['step']['type'] : '';
1237 $flow_id = isset( $args['flow']['id'] ) ? absint( $args['flow']['id'] ) : 0;
1238 $is_store_checkout = isset( $args['is_store_checkout'] ) ? $args['is_store_checkout'] : '';
1239
1240 // create steps only for checkout and thankyou if store checkout.
1241 // This logic will be removed once we have store checkout templates on server.
1242 if ( 'true' === $is_store_checkout && ! in_array( $step_type, array( 'checkout', 'thankyou' ), true ) ) {
1243 return;
1244 }
1245
1246 // Create new step.
1247 $new_step_id = \CartFlows_Importer::get_instance()->create_step( $flow_id, $step_type, $step_title );
1248
1249 if ( empty( $step_id ) || empty( $new_step_id ) ) {
1250 /* translators: %s: step ID */
1251 wp_send_json_error( sprintf( __( 'Invalid step id %1$s or post id %2$s.', 'cartflows' ), $step_id, $new_step_id ) );
1252 }
1253
1254 // Handle edge insertion - reorder flow steps when inserting on an edge.
1255 $this->handle_edge_insertion_reorder( $flow_id, $new_step_id );
1256
1257 // Handle offer edge source update - update source offer step's redirect to point to new step.
1258 $this->update_offer_step_source_redirects( $flow_id, $new_step_id );
1259
1260 wcf()->logger->import_log( 'Remote Step ' . $step_id . ' for local flow "' . get_the_title( $new_step_id ) . '" [' . $new_step_id . ']' );
1261
1262 // Get single step Rest API response.
1263 $response = \CartFlows_API::get_instance()->get_template( $step_id );
1264
1265 wcf()->logger->import_log( wp_json_encode( $response ) );
1266
1267 // Return if there is an error while importing the step template.
1268 if ( ! $response['success'] ) {
1269 $response_data = $response['data'];
1270 $error_code = wp_remote_retrieve_response_code( $response_data );
1271 $error_msge = json_decode( wp_remote_retrieve_body( $response_data ) );
1272
1273 wp_send_json_error(
1274 array(
1275 'error_code' => $error_code,
1276 'message' => ! empty( $error_msge->message ) ? $error_msge->message : '',
1277 'data' => $response['data'],
1278 'success' => $response['success'],
1279 )
1280 );
1281 }
1282
1283 if ( 'divi' === \Cartflows_Helper::get_common_setting( 'default_page_builder' ) ) {
1284 if ( isset( $response['data']['divi_content'] ) && ! empty( $response['data']['divi_content'] ) ) {
1285
1286 update_post_meta( $new_step_id, 'divi_content', $response['data']['divi_content'] );
1287
1288 wp_update_post(
1289 array(
1290 'ID' => $new_step_id,
1291 'post_content' => $response['data']['divi_content'],
1292 )
1293 );
1294 }
1295 }
1296
1297 if ( 'gutenberg' === \Cartflows_Helper::get_common_setting( 'default_page_builder' ) ) {
1298 if ( isset( $response['data']['divi_content'] ) && ! empty( $response['data']['divi_content'] ) ) {
1299 wp_update_post(
1300 array(
1301 'ID' => $new_step_id,
1302 'post_content' => $response['data']['divi_content'],
1303 )
1304 );
1305 }
1306 }
1307
1308 // Import Post Meta.
1309 $this->import_post_meta( $new_step_id, $response );
1310
1311 // Handle user-selected offer redirect settings for upsell/downsell steps.
1312 $this->update_user_selected_offer_redirects( $flow_id, $new_step_id, $step_type );
1313
1314 if ( 'checkout' === $step_type ) {
1315
1316 $posted_data = array(
1317 'primary_color' => isset( $_POST['primary_color'] ) ? sanitize_text_field( wp_unslash( $_POST['primary_color'] ) ) : '',
1318 'site_logo' => isset( $_POST['site_logo'] ) ? array_map( 'sanitize_text_field', wp_unslash( $_POST['site_logo'] ) ) : '',
1319 );
1320
1321 $this->update_store_checkout_template_data( $new_step_id, $response, $posted_data );
1322 }
1323
1324 /* Imported Step */
1325 update_post_meta( $new_step_id, 'cartflows_imported_step', 'yes' );
1326
1327 do_action( 'cartflows_import_complete' );
1328
1329 // Batch Process.
1330 do_action( 'cartflows_after_template_import', $new_step_id, $response );
1331
1332 wcf()->logger->import_log( 'COMPLETE! Importing Step' );
1333 }
1334
1335 /**
1336 * Import Post Meta
1337 *
1338 * @since 1.0.0
1339 *
1340 * @param integer $post_id Post ID.
1341 * @param array $response Post meta.
1342 * @return void
1343 */
1344 public function import_post_meta( $post_id, $response ) {
1345
1346 $metadata = apply_filters( 'cartflows_template_import_meta_data', (array) $response['post_meta'] );
1347
1348 $exclude_meta_keys = \Cartflows_Helper::get_instance()->get_meta_keys_to_exclude_from_import( $post_id );
1349
1350 foreach ( $metadata as $meta_key => $meta_value ) {
1351
1352 if ( in_array( $meta_key, $exclude_meta_keys, true ) ) {
1353 continue;
1354 }
1355
1356 // Security: Only allow meta keys matching known prefixes to prevent arbitrary DB writes.
1357 if ( ! \Cartflows_Helper::get_instance()->is_meta_key_allowed_for_import( $meta_key ) ) {
1358 continue;
1359 }
1360
1361 $meta_value = isset( $meta_value[0] ) ? $meta_value[0] : '';
1362
1363 if ( $meta_value ) {
1364
1365 // Beaver Builder layout keys are handled separately from the generic security
1366 // path below. BB stores rows/columns/modules as serialized arrays of stdClass
1367 // objects. We allow only stdClass — no other classes are expected or permitted.
1368 // Passing the raw serialized string directly to update_post_meta would cause
1369 // WordPress to double-serialize it, breaking BB's get_post_meta readback.
1370 $bb_meta_keys = array( '_fl_builder_data', '_fl_builder_draft', '_fl_builder_data_settings' );
1371 if ( in_array( $meta_key, $bb_meta_keys, true ) ) {
1372 $raw_data = $this->unserialize_bb_meta( $meta_key, $meta_value );
1373 if ( false === $raw_data ) {
1374 continue;
1375 }
1376 } elseif ( is_serialized( $meta_value, true ) ) {
1377 // Security: Using unserialize with allowed_classes=>false to prevent object injection.
1378 $raw_data = unserialize( stripslashes( $meta_value ), array( 'allowed_classes' => false ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize, PHPCompatibility.FunctionUse.NewFunctionParameters.unserialize_optionsFound
1379 // Drop malicious payloads completely to prevent fatal errors.
1380 // Security: Block serialized meta that contains nested PHP objects.
1381 // Even with allowed_classes => false, unserialized payloads may contain
1382 // __PHP_Incomplete_Class instances which WordPress attempts to mutate
1383 // during wp_unslash(), causing fatal errors and violating object safety.
1384 if (
1385 false === $raw_data ||
1386 $this->has_nested_object_payload( $raw_data )
1387 ) {
1388 $raw_data = '';
1389 continue;
1390 }
1391 } elseif ( is_array( $meta_value ) ) {
1392 $raw_data = json_decode( stripslashes( $meta_value ), true );
1393 } else {
1394 $raw_data = $meta_value;
1395 }
1396
1397 if ( '_elementor_data' === $meta_key ) {
1398
1399 if ( is_array( $raw_data ) ) {
1400 $raw_data = wp_slash( wp_json_encode( $raw_data ) );
1401 } else {
1402 $raw_data = wp_slash( $raw_data );
1403 }
1404 }
1405
1406 if ( '_elementor_data' !== $meta_key && '_elementor_draft' !== $meta_key && '_fl_builder_data' !== $meta_key && '_fl_builder_draft' !== $meta_key ) {
1407 if ( is_array( $raw_data ) ) {
1408 wcf()->logger->import_log( '✓ Added post meta ' . $meta_key /* . ' | ' . wp_json_encode( $raw_data ) */ );
1409 } elseif ( ! is_object( $raw_data ) ) {
1410 wcf()->logger->import_log( '✓ Added post meta ' . $meta_key /* . ' | ' . $raw_data */ );
1411 }
1412 }
1413 update_post_meta( $post_id, $meta_key, $raw_data );
1414 }
1415 }
1416 }
1417
1418 /**
1419 * Find the Checkout block and set the Primary color and site logo provided by the user.
1420 *
1421 * @since 1.10.0
1422 *
1423 * @param int $post_id newly created steps ID.
1424 * @param array $response data received from from the imported step.
1425 * @param array $posted_data post data.
1426 *
1427 * @return void
1428 */
1429 public function update_store_checkout_template_data( $post_id, $response, $posted_data ) {
1430
1431 $store_checkout_id = get_option( '_cartflows_store_checkout', false );
1432 $current_flow_id = (int) wcf()->utils->get_flow_id_from_step_id( $post_id );
1433 $default_page_bulder = \Cartflows_Helper::get_common_setting( 'default_page_builder' );
1434
1435 if ( empty( $posted_data['primary_color'] ) && empty( $posted_data['site_logo'] ) ) {
1436 return;
1437 }
1438
1439 if ( $store_checkout_id !== $current_flow_id ) {
1440 return;
1441 }
1442
1443 if ( 'elementor' === $default_page_bulder ) {
1444
1445 $metadata = (array) $response['post_meta'];
1446
1447 foreach ( $metadata as $meta_key => $meta_value ) {
1448
1449 $meta_value = isset( $meta_value[0] ) ? $meta_value[0] : '';
1450
1451 if ( $meta_value ) {
1452
1453 if ( is_serialized( $meta_value, true ) ) {
1454 // Security: Using unserialize with allowed_classes=>false to prevent object injection.
1455 $raw_data = unserialize( stripslashes( $meta_value ), array( 'allowed_classes' => false ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize, PHPCompatibility.FunctionUse.NewFunctionParameters.unserialize_optionsFound
1456 if ( false === $raw_data || is_object( $raw_data ) ) {
1457 continue;
1458 }
1459 } elseif ( is_array( $meta_value ) ) {
1460 $raw_data = json_decode( stripslashes( $meta_value ), true );
1461 } else {
1462 $raw_data = $meta_value;
1463 }
1464
1465 if ( '_elementor_data' === $meta_key ) {
1466
1467 $raw_data = json_decode( $raw_data, true );
1468
1469 // Find the checkout-form and update the primary color.
1470 $this->elementor_find_and_replace_template_data( $raw_data, $posted_data );
1471
1472 if ( is_array( $raw_data ) ) {
1473 $raw_data = wp_slash( wp_json_encode( $raw_data ) );
1474 } else {
1475 $raw_data = wp_slash( $raw_data );
1476 }
1477
1478 update_post_meta( $post_id, $meta_key, $raw_data );
1479
1480 }
1481 }
1482 }
1483 } elseif ( 'gutenberg' === $default_page_bulder ) {
1484
1485 $post = get_post( $post_id );
1486 $blocks = parse_blocks( $post->post_content );
1487
1488 if ( is_array( $blocks ) && ! empty( $blocks ) ) {
1489
1490 $this->gutenberg_find_and_replace_template_data( $blocks, $posted_data );
1491
1492 if ( ! empty( $blocks ) ) {
1493 $serialized_blocks = serialize_blocks( $blocks );
1494
1495 wp_update_post(
1496 array(
1497 'ID' => $post_id,
1498 'post_content' => $serialized_blocks,
1499 )
1500 );
1501 }
1502 }
1503 } elseif ( 'beaver-builder' === $default_page_bulder ) {
1504 $data = \FLBuilderModel::get_layout_data( 'published', $post_id );
1505
1506 if ( ! empty( $data ) ) {
1507
1508 $this->beaver_builder_find_and_replace_template_data( $data, $posted_data );
1509
1510 // Update page builder data.
1511 update_post_meta( $post_id, '_fl_builder_data', $data );
1512 update_post_meta( $post_id, '_fl_builder_draft', $data );
1513 }
1514 }
1515 }
1516
1517
1518 /**
1519 * Get flows list for preview
1520 *
1521 * @return void
1522 */
1523 public function get_flows_list() {
1524
1525 $response_data = array( 'message' => $this->get_error_msg( 'permission' ) );
1526
1527 if ( ! current_user_can( 'cartflows_manage_flows_steps' ) ) {
1528 wp_send_json_error( $response_data );
1529 }
1530
1531 /**
1532 * Nonce verification
1533 */
1534 if ( ! check_ajax_referer( 'cartflows_get_flows_list', 'security', false ) ) {
1535 $response_data = array( 'message' => $this->get_error_msg( 'nonce' ) );
1536 wp_send_json_error( $response_data );
1537 }
1538
1539 $flows_list = \Cartflows_Helper::get_instance()->get_flows_and_steps();
1540
1541 /**
1542 * Redirect to the new step edit screen
1543 */
1544 $response_data = array(
1545 'message' => __( 'Successful!', 'cartflows' ),
1546 'flows' => $flows_list,
1547 );
1548
1549 wp_send_json_success( $response_data );
1550 }
1551
1552 /**
1553 * Get the elementor widget data.
1554 *
1555 * @param array $elements elements data.
1556 * @param array $posted_data posted data.
1557 */
1558 public function elementor_find_and_replace_template_data( &$elements, $posted_data ) {
1559
1560 foreach ( $elements as &$element ) {
1561
1562 if ( 'widget' === $element['elType'] && 'checkout-form' === $element['widgetType'] ) {
1563 $element['settings']['global_primary_color'] = ! empty( $posted_data['primary_color'] ) ? $posted_data['primary_color'] : $element['settings']['global_primary_color'];
1564 }
1565
1566 if ( 'widget' === $element['elType'] && 'image' === $element['widgetType'] && isset( $element['settings']['_css_classes'] ) ) {
1567
1568 if ( str_contains( $element['settings']['_css_classes'], 'cartflows-store-checkout-logo-field' ) ) {
1569 $element['settings']['image']['url'] = ! empty( $posted_data['site_logo'] ) ? $posted_data['site_logo']['url'] : $element['settings']['image']['url'];
1570 $element['settings']['image']['id'] = ! empty( $posted_data['site_logo'] ) ? $posted_data['site_logo']['id'] : $element['settings']['image']['id'];
1571 }
1572 }
1573
1574 if ( ! empty( $element['elements'] ) ) {
1575 $this->elementor_find_and_replace_template_data( $element['elements'], $posted_data );
1576 }
1577 }
1578 }
1579
1580 /**
1581 * Get the block data.
1582 *
1583 * @param array $elements elements data.
1584 * @param array $posted_data posted data.
1585 */
1586 public function gutenberg_find_and_replace_template_data( &$elements, $posted_data ) {
1587
1588 foreach ( $elements as &$element ) {
1589 if ( 'wcfb/checkout-form' === $element['blockName'] ) {
1590 // Update the element with the data.
1591 $element['attrs']['globalbgColor'] = ! empty( $posted_data['primary_color'] ) ? $posted_data['primary_color'] : $element['attrs']['globalbgColor'];
1592 }
1593
1594 if ( 'uagb/info-box' === $element['blockName'] && isset( $element['attrs']['className'] ) ) {
1595
1596 if ( str_contains( $element['attrs']['className'], 'cartflows-store-checkout-logo-field' ) ) {
1597 $element['attrs']['iconImage']['id'] = ! empty( $posted_data['site_logo'] ) ? $posted_data['site_logo']['id'] : $element['attrs']['iconImage']['id'];
1598 $element['attrs']['iconImage']['url'] = ! empty( $posted_data['site_logo'] ) ? $posted_data['site_logo']['url'] : $element['attrs']['iconImage']['url'];
1599 $element['attrs']['iconImage']['link'] = ! empty( $posted_data['site_logo'] ) ? $posted_data['site_logo']['url'] : $element['attrs']['iconImage']['link'];
1600 $element['attrs']['iconImage']['sizes']['full']['url'] = ! empty( $posted_data['site_logo'] ) ? $posted_data['site_logo']['url'] : $element['attrs']['iconImage']['link'];
1601 }
1602 }
1603
1604 if ( ! empty( $element['innerBlocks'] ) ) {
1605 $this->gutenberg_find_and_replace_template_data( $element['innerBlocks'], $posted_data );
1606 }
1607 }
1608 }
1609
1610 /**
1611 * Replace the logo and color in the BB template while importing.
1612 *
1613 * @param array $elements elements data.
1614 * @param array $posted_data posted data.
1615 */
1616 public function beaver_builder_find_and_replace_template_data( &$elements, $posted_data ) {
1617
1618 foreach ( $elements as $node => &$element ) {
1619 if ( ! empty( $element->type ) && 'module' === $element->type ) {
1620
1621 if ( ! empty( $element->settings->type ) && 'cartflows-bb-checkout-form' === $element->settings->type ) {
1622 // Update the logo in the template.
1623 $element->settings->global_primary_color = ! empty( $posted_data['primary_color'] ) ? $posted_data['primary_color'] : $element->settings->global_primary_color;
1624 }
1625
1626 if ( ! empty( $element->settings->type ) && 'photo' === $element->settings->type && ! empty( $element->settings->class ) && 'cartflows-store-checkout-logo-field' === $element->settings->class ) {
1627 $module_setting = $element->settings;
1628 // Update the logo in the template.
1629 $element->settings->photo_src = ! empty( $posted_data['site_logo'] ) ? $posted_data['site_logo']['url'] : $element->settings->photo_src;
1630
1631 }
1632 }
1633 }
1634 }
1635
1636 /**
1637 * Imports the Global Colors and Patterns (GCP) variables data for a given flow.
1638 *
1639 * This function updates the post meta for a flow with the GCP variables data received in the response.
1640 * It logs the import process and updates the post meta with the GCP data.
1641 *
1642 * @param array $response The response data containing the GCP variables.
1643 * @param int $flow_id The ID of the flow for which the GCP data is being imported.
1644 *
1645 * @return void
1646 */
1647 public function import_funnel_gcp_vars_data( $response, $flow_id ) {
1648
1649 wcf()->logger->import_log( 'Start: ' . __CLASS__ . ' :: ' . __FUNCTION__ );
1650
1651 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
1652
1653 if ( isset( $response['data']['flow_gcp_meta'] ) && ! empty( $response['data']['flow_gcp_meta'] ) && is_object( $response['data']['flow_gcp_meta'] ) ) {
1654
1655 wcf()->logger->import_log( 'Before Importing:' . print_r( $response['data']['flow_gcp_meta'], true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
1656
1657 $gcp_data = (object) array_map( 'sanitize_text_field', (array) $response['data']['flow_gcp_meta'] );
1658
1659 wcf()->logger->import_log( 'After Importing: ' . print_r( $gcp_data, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
1660
1661 $gcp_meta_keys = array(
1662 'wcf-enable-gcp-styling' => 'yes',
1663 'wcf-gcp-primary-color' => ! empty( $gcp_data->gcp_primary_color ) ? $gcp_data->gcp_primary_color : '',
1664 'wcf-gcp-secondary-color' => ! empty( $gcp_data->gcp_secondary_color ) ? $gcp_data->gcp_secondary_color : '',
1665 'wcf-gcp-text-color' => ! empty( $gcp_data->gcp_text_color ) ? $gcp_data->gcp_text_color : '',
1666 'wcf-gcp-accent-color' => ! empty( $gcp_data->gcp_accent_color ) ? $gcp_data->gcp_accent_color : '',
1667 );
1668
1669 foreach ( $gcp_meta_keys as $key => $value ) {
1670 update_post_meta( $flow_id, $key, $value );
1671 }
1672 }
1673
1674 wcf()->logger->import_log( 'End: ' . __CLASS__ . ' :: ' . __FUNCTION__ );
1675 }
1676
1677 /**
1678 * Deserialize a Beaver Builder layout meta value.
1679 *
1680 * BB layout keys (_fl_builder_data, _fl_builder_draft, _fl_builder_data_settings)
1681 * store arrays of stdClass nodes. Only stdClass is allowed during unserialize —
1682 * no other classes are expected in BB layout data and none should be permitted.
1683 *
1684 * Returns false if the value is serialized but cannot be unserialized, so the
1685 * caller can skip the key rather than storing corrupt data silently.
1686 *
1687 * @since 3.0.2
1688 *
1689 * @param string $meta_key The meta key being processed (used for error logging).
1690 * @param mixed $meta_value The raw meta value from the template API response.
1691 * @return mixed Unserialized value, original value if not serialized, or false on failure.
1692 */
1693 private function unserialize_bb_meta( $meta_key, $meta_value ) {
1694 if ( ! is_serialized( $meta_value ) ) {
1695 return $meta_value;
1696 }
1697
1698 // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize, PHPCompatibility.FunctionUse.NewFunctionParameters.unserialize_optionsFound -- BB layout data uses only stdClass; no arbitrary classes are permitted.
1699 $result = unserialize( $meta_value, array( 'allowed_classes' => array( 'stdClass' ) ) );
1700
1701 if ( false === $result ) {
1702 wcf()->logger->import_log( 'Failed to unserialize BB meta key: ' . $meta_key );
1703 return false;
1704 }
1705
1706 return $result;
1707 }
1708
1709 /**
1710 * Detect whether the given value contains objects at any depth.
1711 *
1712 * This is used as a hard security guard to prevent storing
1713 * unserialized payloads that contain PHP objects (including
1714 * __PHP_Incomplete_Class), which can cause fatal errors and
1715 * violate object injection protections.
1716 *
1717 * @since 2.2.1
1718 *
1719 * @param mixed $value The value to inspect recursively.
1720 *
1721 * @return bool True if an object is found anywhere in the payload.
1722 */
1723 private function has_nested_object_payload( $value ) {
1724
1725 if ( is_object( $value ) ) {
1726 return true;
1727 }
1728
1729 if ( is_array( $value ) ) {
1730 foreach ( $value as $v ) {
1731 if ( $this->has_nested_object_payload( $v ) ) {
1732 return true;
1733 }
1734 }
1735 }
1736
1737 return false;
1738 }
1739
1740 /**
1741 * Handle edge insertion reorder.
1742 *
1743 * Reorders flow steps when a new step is inserted on an edge between two existing steps.
1744 * Handles both START node edges and regular step edges.
1745 *
1746 * @since 3.1.0
1747 * @param int $flow_id The flow ID.
1748 * @param int $new_step_id The newly created step ID.
1749 * @return void
1750 */
1751 private function handle_edge_insertion_reorder( $flow_id, $new_step_id ) {
1752
1753 // Per-call IDOR guard — re-verify the current user can edit this flow.
1754 if ( ! $this->user_can_edit_flow( $flow_id ) ) {
1755 return;
1756 }
1757
1758 $is_start_edge = isset( $_POST['is_start_edge'] ) && 'true' === $_POST['is_start_edge']; // phpcs:ignore WordPress.Security.NonceVerification.Missing
1759 $edge_source_step_id = isset( $_POST['edge_source_step_id'] ) ? absint( $_POST['edge_source_step_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing
1760 $edge_target_step_id = isset( $_POST['edge_target_step_id'] ) ? absint( $_POST['edge_target_step_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing
1761
1762 // Handle edge insertion if we have a target step ID (either from START node or regular step).
1763 if ( ! $edge_target_step_id || ( ! $is_start_edge && ! $edge_source_step_id ) ) {
1764 return;
1765 }
1766
1767 // IDOR guard — both edge endpoints must belong to this flow.
1768 // The START node is virtual and has no step ID, so skip its check.
1769 if ( ! $is_start_edge && ! $this->is_step_in_flow( $edge_source_step_id, $flow_id ) ) {
1770 return;
1771 }
1772 if ( ! $this->is_step_in_flow( $edge_target_step_id, $flow_id ) ) {
1773 return;
1774 }
1775
1776 // Get current flow steps.
1777 $flow_steps = get_post_meta( (int) $flow_id, 'wcf-steps', true );
1778
1779 if ( ! is_array( $flow_steps ) || empty( $flow_steps ) ) {
1780 return;
1781 }
1782
1783 // Find indices of source and target steps.
1784 $source_index = -1; // -1 for START node means insert at position 0.
1785 $target_index = -1;
1786 $new_step_index = -1;
1787
1788 foreach ( $flow_steps as $index => $step_data ) {
1789 if ( isset( $step_data['id'] ) ) {
1790 if ( ! $is_start_edge && $step_data['id'] === $edge_source_step_id ) {
1791 $source_index = $index;
1792 }
1793 if ( $step_data['id'] === $edge_target_step_id ) {
1794 $target_index = $index;
1795 }
1796 if ( $step_data['id'] === $new_step_id ) {
1797 $new_step_index = $index;
1798 }
1799 }
1800 }
1801
1802 // Determine if we can proceed with reordering.
1803 $can_reorder = -1 !== $target_index && -1 !== $new_step_index;
1804 if ( ! $is_start_edge ) {
1805 $can_reorder = $can_reorder && -1 !== $source_index;
1806 }
1807
1808 if ( ! $can_reorder ) {
1809 return;
1810 }
1811
1812 // Remove the new step from its current position (at the end).
1813 $new_step_data = $flow_steps[ $new_step_index ];
1814 array_splice( $flow_steps, $new_step_index, 1 );
1815
1816 // Recalculate indices after removal (if new step was before them).
1817 if ( ! $is_start_edge && $new_step_index < $source_index ) {
1818 --$source_index;
1819 }
1820 if ( $new_step_index < $target_index ) {
1821 --$target_index;
1822 }
1823
1824 // Insert the new step at the correct position.
1825 if ( $is_start_edge ) {
1826 // START node: insert at position 0 (beginning of flow).
1827 $insert_position = 0;
1828 } else {
1829 // Regular step: insert right after the source step.
1830 $insert_position = $source_index + 1;
1831 }
1832
1833 array_splice( $flow_steps, (int) $insert_position, 0, array( $new_step_data ) );
1834
1835 // Update the flow steps meta.
1836 update_post_meta( (int) $flow_id, 'wcf-steps', $flow_steps );
1837 }
1838
1839 /**
1840 * Update offer step source redirects.
1841 *
1842 * When inserting on an Accept/Reject edge, updates the source offer step's
1843 * redirect to point to the new step.
1844 *
1845 * @since 3.1.0
1846 * @param int $flow_id The flow ID.
1847 * @param int $new_step_id The newly created step ID.
1848 * @return void
1849 */
1850 private function update_offer_step_source_redirects( $flow_id, $new_step_id ) {
1851
1852 // Per-call IDOR guard — re-verify the current user can edit this flow.
1853 if ( ! $this->user_can_edit_flow( $flow_id ) ) {
1854 return;
1855 }
1856
1857 $edge_source_step_id = isset( $_POST['edge_source_step_id'] ) ? absint( $_POST['edge_source_step_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing
1858 $edge_source_handle = isset( $_POST['edge_source_handle'] ) ? sanitize_text_field( wp_unslash( $_POST['edge_source_handle'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing
1859
1860 if ( $edge_source_step_id <= 0 || empty( $edge_source_handle ) ) {
1861 return;
1862 }
1863
1864 // IDOR guard — both source and target steps must belong to this flow.
1865 if ( ! $this->is_step_in_flow( $edge_source_step_id, $flow_id ) ) {
1866 return;
1867 }
1868 if ( ! $this->is_step_in_flow( $new_step_id, $flow_id ) ) {
1869 return;
1870 }
1871
1872 $source_step_type = get_post_meta( $edge_source_step_id, 'wcf-step-type', true );
1873
1874 // Only update if source is an offer step (upsell/downsell).
1875 if ( ! in_array( $source_step_type, array( 'upsell', 'downsell' ), true ) ) {
1876 return;
1877 }
1878
1879 $source_meta_updated = false;
1880
1881 // Update the appropriate post meta based on handle.
1882 if ( 'a' === $edge_source_handle ) {
1883 // Accept edge - update wcf-yes-next-step.
1884 update_post_meta( $edge_source_step_id, 'wcf-yes-next-step', $new_step_id );
1885 $source_meta_updated = true;
1886 } elseif ( 'b' === $edge_source_handle ) {
1887 // Reject edge - update wcf-no-next-step.
1888 update_post_meta( $edge_source_step_id, 'wcf-no-next-step', $new_step_id );
1889 $source_meta_updated = true;
1890 }
1891
1892 // Also update the flow_steps array to keep it in sync.
1893 if ( $source_meta_updated ) {
1894 $this->sync_offer_redirects_in_flow_steps( $flow_id, $edge_source_step_id, $edge_source_handle, $new_step_id );
1895 }
1896 }
1897
1898 /**
1899 * Sync offer redirects in flow_steps array.
1900 *
1901 * Updates the flow_steps array to keep it in sync with post meta.
1902 * Handles both control steps and A/B test variations.
1903 *
1904 * @since 3.1.0
1905 * @param int $flow_id The flow ID.
1906 * @param int $source_step_id The source offer step ID.
1907 * @param string $handle The edge handle ('a' for accept, 'b' for reject).
1908 * @param int $target_step_id The target step ID to redirect to.
1909 * @return void
1910 */
1911 private function sync_offer_redirects_in_flow_steps( $flow_id, $source_step_id, $handle, $target_step_id ) {
1912 $updated_flow_steps = get_post_meta( (int) $flow_id, 'wcf-steps', true );
1913
1914 if ( ! is_array( $updated_flow_steps ) ) {
1915 return;
1916 }
1917
1918 $flow_steps_updated = false;
1919
1920 foreach ( $updated_flow_steps as $idx => $step_data ) {
1921 // Check if this is the control step (direct match).
1922 if ( isset( $step_data['id'] ) && (int) $step_data['id'] === (int) $source_step_id ) {
1923 if ( 'a' === $handle ) {
1924 $updated_flow_steps[ $idx ]['offer_yes_step_id'] = $target_step_id;
1925 } elseif ( 'b' === $handle ) {
1926 $updated_flow_steps[ $idx ]['offer_no_step_id'] = $target_step_id;
1927 }
1928 $flow_steps_updated = true;
1929 break;
1930 }
1931
1932 // Check if the source is an A/B test variation within this step.
1933 if ( isset( $step_data['ab-test-variations'] ) && is_array( $step_data['ab-test-variations'] ) ) {
1934 foreach ( $step_data['ab-test-variations'] as $var_idx => $variation ) {
1935 if ( isset( $variation['id'] ) && (int) $variation['id'] === (int) $source_step_id ) {
1936 if ( 'a' === $handle ) {
1937 $updated_flow_steps[ $idx ]['ab-test-variations'][ $var_idx ]['offer_yes_step_id'] = $target_step_id;
1938 } elseif ( 'b' === $handle ) {
1939 $updated_flow_steps[ $idx ]['ab-test-variations'][ $var_idx ]['offer_no_step_id'] = $target_step_id;
1940 }
1941 $flow_steps_updated = true;
1942 break 2; // Break out of both loops.
1943 }
1944 }
1945 }
1946 }
1947
1948 if ( $flow_steps_updated ) {
1949 update_post_meta( (int) $flow_id, 'wcf-steps', $updated_flow_steps );
1950 }
1951 }
1952
1953 /**
1954 * Update user-selected offer redirects.
1955 *
1956 * Handles user-selected offer redirect settings for upsell/downsell steps
1957 * when the user explicitly selects Accept/Reject targets via the UI.
1958 *
1959 * @since 3.1.0
1960 * @param int $flow_id The flow ID.
1961 * @param int $new_step_id The newly created step ID.
1962 * @param string $step_type The step type.
1963 * @return void
1964 */
1965 private function update_user_selected_offer_redirects( $flow_id, $new_step_id, $step_type ) {
1966
1967 // Per-call IDOR guard — re-verify the current user can edit this flow.
1968 if ( ! $this->user_can_edit_flow( $flow_id ) ) {
1969 return;
1970 }
1971
1972 // Only process for offer steps.
1973 if ( ! in_array( $step_type, array( 'upsell', 'downsell' ), true ) ) {
1974 return;
1975 }
1976
1977 // IDOR guard — the new step itself must belong to this flow.
1978 if ( ! $this->is_step_in_flow( $new_step_id, $flow_id ) ) {
1979 return;
1980 }
1981
1982 $user_offer_yes_step_id = isset( $_POST['offer_yes_step_id'] ) ? absint( $_POST['offer_yes_step_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing
1983 $user_offer_no_step_id = isset( $_POST['offer_no_step_id'] ) ? absint( $_POST['offer_no_step_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing
1984
1985 // IDOR guard — redirect targets must belong to this flow if provided.
1986 if ( $user_offer_yes_step_id && ! $this->is_step_in_flow( $user_offer_yes_step_id, $flow_id ) ) {
1987 $user_offer_yes_step_id = 0;
1988 }
1989 if ( $user_offer_no_step_id && ! $this->is_step_in_flow( $user_offer_no_step_id, $flow_id ) ) {
1990 $user_offer_no_step_id = 0;
1991 }
1992
1993 // Return if user didn't select any custom redirects.
1994 if ( ! $user_offer_yes_step_id && ! $user_offer_no_step_id ) {
1995 return;
1996 }
1997
1998 $current_flow_steps = get_post_meta( (int) $flow_id, 'wcf-steps', true );
1999
2000 if ( ! is_array( $current_flow_steps ) ) {
2001 return;
2002 }
2003
2004 foreach ( $current_flow_steps as $idx => $step_data ) {
2005 if ( isset( $step_data['id'] ) && $step_data['id'] === $new_step_id ) {
2006 // Update offer_yes_step_id if user selected one.
2007 if ( $user_offer_yes_step_id ) {
2008 $current_flow_steps[ $idx ]['offer_yes_step_id'] = $user_offer_yes_step_id;
2009 update_post_meta( $new_step_id, 'wcf-yes-next-step', $user_offer_yes_step_id );
2010 }
2011
2012 // Update offer_no_step_id if user selected one.
2013 if ( $user_offer_no_step_id ) {
2014 $current_flow_steps[ $idx ]['offer_no_step_id'] = $user_offer_no_step_id;
2015 update_post_meta( $new_step_id, 'wcf-no-next-step', $user_offer_no_step_id );
2016 }
2017 break;
2018 }
2019 }
2020
2021 // Save the updated flow steps.
2022 update_post_meta( (int) $flow_id, 'wcf-steps', $current_flow_steps );
2023 }
2024 }
2025