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-legacy-core / ajax / importer.php

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

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