PluginProbe
CartFlows – Funnel Builder & Checkout Plugin for WooCommerce / 3.3.0
CartFlows – Funnel Builder & Checkout Plugin for WooCommerce v3.3.0
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 / steps.php

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

611 lines 18.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * CartFlows Flows ajax actions.
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\Ajax\AjaxBase;
16 use CartflowsAdmin\AdminCore\Inc\AdminHelper;
17 use CartflowsAdmin\AdminCore\Inc\MetaOps;
18
19 /**
20 * Class Steps.
21 */
22 class Steps 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 'clone_step',
56 'delete_step',
57 'save_meta_settings',
58 'update_step_title',
59 );
60
61 $this->init_ajax_events( $ajax_events );
62 }
63
64 /**
65 * Update step title.
66 *
67 * @since 1.0.0
68 * @return void
69 */
70 public function update_step_title() {
71
72 $response_data = array( 'messsage' => $this->get_error_msg( 'permission' ) );
73
74 if ( ! current_user_can( 'cartflows_manage_flows_steps' ) ) {
75 wp_send_json_error( $response_data );
76 }
77
78 /**
79 * Nonce verification
80 */
81 if ( ! check_ajax_referer( 'cartflows_update_step_title', 'security', false ) ) {
82 $response_data = array( 'message' => $this->get_error_msg( 'nonce' ) );
83 wp_send_json_error( $response_data );
84 }
85
86 $step_id = isset( $_POST['step_id'] ) ? intval( $_POST['step_id'] ) : 0;
87 $new_step_title = isset( $_POST['new_step_title'] ) ? sanitize_text_field( $_POST['new_step_title'] ) : '';
88
89 $result = array(
90 'status' => false,
91 'text' => __( 'Can\'t update the step title', 'cartflows' ),
92 );
93
94 /* Check if CartFlows Post type */
95 if ( CARTFLOWS_STEP_POST_TYPE !== get_post_type( $step_id ) ) {
96 wp_send_json( $result );
97 }
98
99 if ( empty( $step_id ) || empty( $new_step_title ) ) {
100 wp_send_json( $result );
101 }
102
103 $new_step_data = array(
104 'ID' => $step_id,
105 'post_title' => $new_step_title,
106 );
107 wp_update_post( $new_step_data );
108
109 $result = array(
110 'status' => true,
111 /* translators: %s flow id */
112 'text' => sprintf( __( 'Step title updated - %s', 'cartflows' ), $step_id ),
113 );
114
115 wp_send_json( $result );
116 }
117
118 /**
119 * Clone step with its meta.
120 */
121 public function clone_step() {
122
123 global $wpdb;
124
125 $response_data = array( 'messsage' => $this->get_error_msg( 'permission' ) );
126
127 if ( ! current_user_can( 'cartflows_manage_flows_steps' ) ) {
128 wp_send_json_error( $response_data );
129 }
130
131 /**
132 * Nonce verification
133 */
134 if ( ! check_ajax_referer( 'cartflows_clone_step', 'security', false ) ) {
135 $response_data = array( 'message' => $this->get_error_msg( 'nonce' ) );
136 wp_send_json_error( $response_data );
137 }
138
139 if ( isset( $_POST['post_id'] ) && isset( $_POST['step_id'] ) ) {
140 $flow_id = intval( $_POST['post_id'] );
141 $step_id = intval( $_POST['step_id'] );
142 }
143
144 $result = array(
145 'status' => false,
146 'reload' => true,
147 /* translators: %s flow id */
148 'text' => sprintf( __( 'Can\'t clone this step - %1$s. Flow - %2$s', 'cartflows' ), $step_id, $flow_id ),
149 );
150
151 if ( ! $flow_id || ! $step_id ) {
152 wp_send_json( $result );
153 }
154
155 /* Check if CartFlows Post type */
156 if ( CARTFLOWS_FLOW_POST_TYPE !== get_post_type( $flow_id ) || CARTFLOWS_STEP_POST_TYPE !== get_post_type( $step_id ) ) {
157 wp_send_json( $result );
158 }
159
160 /**
161 * And all the original post data then
162 */
163 $post = get_post( $step_id );
164
165 /**
166 * Assign current user to be the new post author
167 */
168 $current_user = wp_get_current_user();
169 $new_post_author = $current_user->ID;
170
171 /**
172 * If post data exists, create the post duplicate
173 */
174 if ( isset( $post ) && null !== $post ) {
175
176 /**
177 * New post data array
178 */
179 $args = array(
180 'comment_status' => $post->comment_status,
181 'ping_status' => $post->ping_status,
182 'post_author' => $new_post_author,
183 'post_content' => $post->post_content,
184 'post_excerpt' => $post->post_excerpt,
185 'post_name' => $post->post_name,
186 'post_parent' => $post->post_parent,
187 'post_password' => $post->post_password,
188 'post_status' => $post->post_status,
189 'post_title' => $post->post_title . ' Clone',
190 'post_type' => $post->post_type,
191 'to_ping' => $post->to_ping,
192 'menu_order' => $post->menu_order,
193 );
194
195 /**
196 * Insert the post
197 */
198 $new_step_id = wp_insert_post( $args );
199
200 /**
201 * Get all current post terms ad set them to the new post
202 */
203 // returns array of taxonomy names for post type, ex array("category", "post_tag");.
204 $taxonomies = get_object_taxonomies( $post->post_type );
205
206 foreach ( $taxonomies as $taxonomy ) {
207
208 $post_terms = wp_get_object_terms( $step_id, $taxonomy, array( 'fields' => 'slugs' ) );
209
210 wp_set_object_terms( $new_step_id, $post_terms, $taxonomy, false );
211 }
212
213 /**
214 * Duplicate all post meta just in two SQL queries
215 */
216 $post_meta_infos = $wpdb->get_results( //phpcs:ignore WordPress.DB.DirectDatabaseQuery
217 $wpdb->prepare( "SELECT meta_key, meta_value FROM $wpdb->postmeta WHERE post_id=%d", $step_id )
218 );
219
220 if ( ! empty( $post_meta_infos ) ) {
221
222 $sql_query = "INSERT INTO $wpdb->postmeta (post_id, meta_key, meta_value) VALUES ";
223
224 $sql_query_sel = array();
225
226 foreach ( $post_meta_infos as $meta_info ) {
227
228 $meta_key = $meta_info->meta_key;
229
230 if ( '_wp_old_slug' === $meta_key ) {
231 continue;
232 }
233
234 $sql_query_sel[] = $wpdb->prepare( '( %d, %s, %s )', $new_step_id, $meta_key, $meta_info->meta_value );
235
236 }
237
238 $sql_query .= implode( ',', $sql_query_sel );
239
240 $wpdb->query( $sql_query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery
241 }
242
243 $flow_steps = get_post_meta( $flow_id, 'wcf-steps', true );
244 $step_type = get_post_meta( $step_id, 'wcf-step-type', true );
245
246 if ( ! is_array( $flow_steps ) ) {
247 $flow_steps = array();
248 }
249
250 $flow_steps[] = array(
251 'id' => $new_step_id,
252 'title' => $post->post_title,
253 'type' => $step_type,
254 );
255
256 $flow_steps = \Cartflows_Helper::get_instance()->maybe_update_flow_steps( $flow_id, $flow_steps );
257
258 update_post_meta( $flow_id, 'wcf-steps', $flow_steps );
259
260 /* Clear Page Builder Cache */
261 $this->clear_cache();
262
263 $result = array(
264 'status' => true,
265 'reload' => true,
266 /* translators: %s flow id */
267 'text' => sprintf( __( 'Step - %1$s cloned. Flow - %2$s', 'cartflows' ), $step_id, $flow_id ),
268 );
269 }
270
271 wp_send_json( $result );
272 }
273
274 /**
275 * Clear Page Builder Cache
276 */
277 public function clear_cache() {
278
279 // Clear 'Elementor' file cache.
280 if ( class_exists( '\Elementor\Plugin' ) ) {
281 \Elementor\Plugin::$instance->files_manager->clear_cache();
282 }
283 }
284
285 /**
286 * Delete step for flow
287 *
288 * @since 1.0.0
289 *
290 * @return void
291 */
292 public function delete_step() {
293
294 $response_data = array( 'messsage' => $this->get_error_msg( 'permission' ) );
295
296 if ( ! current_user_can( 'cartflows_manage_flows_steps' ) ) {
297 wp_send_json_error( $response_data );
298 }
299
300 /**
301 * Nonce verification
302 */
303 if ( ! check_ajax_referer( 'cartflows_delete_step', 'security', false ) ) {
304 $response_data = array( 'message' => $this->get_error_msg( 'nonce' ) );
305 wp_send_json_error( $response_data );
306 }
307
308 $flow_id = isset( $_POST['post_id'] ) ? intval( $_POST['post_id'] ) : 0;
309 $step_id = isset( $_POST['step_id'] ) ? intval( $_POST['step_id'] ) : 0;
310
311 $result = array(
312 'status' => false,
313 'reload' => false,
314 /* translators: %s flow id */
315 'text' => sprintf( __( 'Step not deleted for flow - %s', 'cartflows' ), $flow_id ),
316 );
317
318 if ( ! $flow_id || ! $step_id ) {
319 wp_send_json( $result );
320 }
321
322 /* Check if CartFlows Post type */
323 if ( CARTFLOWS_FLOW_POST_TYPE !== get_post_type( $flow_id ) || CARTFLOWS_STEP_POST_TYPE !== get_post_type( $step_id ) ) {
324 wp_send_json( $result );
325 }
326
327 $flow_steps = get_post_meta( $flow_id, 'wcf-steps', true );
328
329 if ( ! is_array( $flow_steps ) ) {
330 wp_send_json( $result );
331 }
332
333 // Handle redirect updates for source steps (offer steps pointing to the deleted step).
334 // This includes both control steps and A/B testing variations.
335 $flow_steps = $this->process_redirect_updates_on_step_deletion( $flow_steps, $flow_id );
336
337 $is_ab_test = get_post_meta( $step_id, 'wcf-ab-test', true );
338
339 if ( ! $is_ab_test ) {
340
341 foreach ( $flow_steps as $index => $data ) {
342
343 if ( intval( $data['id'] ) === $step_id ) {
344 unset( $flow_steps[ $index ] );
345 break;
346 }
347 }
348
349 /* Set index order properly */
350 $flow_steps = array_merge( $flow_steps );
351
352 /* Update latest data */
353 update_post_meta( $flow_id, 'wcf-steps', $flow_steps );
354
355 /* Delete step */
356 wp_delete_post( $step_id, true );
357
358 $result = array(
359 'status' => true,
360 'reload' => false,
361 /* translators: %s flow id */
362 'text' => sprintf( __( 'Step deleted for flow - %s', 'cartflows' ), $flow_id ),
363 );
364
365 } else {
366
367 $result = array(
368 'status' => false,
369 'reload' => false,
370 /* translators: %s flow id */
371 'text' => sprintf( __( 'This step can not be deleted.', 'cartflows' ), $flow_id ),
372 );
373 /**
374 Action do_action( 'cartflows_step_delete_ab_test', $step_id, $flow_id, $flow_steps );
375 */
376 }
377
378 wp_send_json( $result );
379 }
380
381 /**
382 * Save meta settings for steps.
383 */
384 public function save_meta_settings() {
385
386 $response_data = array( 'messsage' => $this->get_error_msg( 'permission' ) );
387
388 if ( ! current_user_can( 'cartflows_manage_flows_steps' ) ) {
389 wp_send_json_error( $response_data );
390 }
391
392 /**
393 * Nonce verification
394 */
395 if ( ! check_ajax_referer( 'cartflows_save_meta_settings', 'security', false ) ) {
396 $response_data = array( 'message' => $this->get_error_msg( 'nonce' ) );
397 wp_send_json_error( $response_data );
398 }
399
400 $result = array(
401 'status' => false,
402 'reload' => false,
403 /* translators: %s flow id */
404 'text' => sprintf( __( 'Invalid Step Id has been provided.', 'cartflows' ) ),
405 );
406
407 if ( ! isset( $_POST['post_id'] ) || ! isset( $_POST['step_id'] ) ) {
408 wp_send_json( $result );
409 }
410
411 $step_id = isset( $_POST['step_id'] ) ? intval( $_POST['step_id'] ) : 0;
412
413 if ( CARTFLOWS_STEP_POST_TYPE !== get_post_type( $step_id ) ) {
414 wp_send_json_error( $result );
415 }
416
417 $step_tile = isset( $_POST['post_title'] ) ? sanitize_text_field( wp_unslash( $_POST['post_title'] ) ) : get_the_title( $step_id );
418 if ( '' === $step_tile ) {
419 $step_tile = __( '(no title)', 'cartflows' );
420 }
421 $step_slug = isset( $_POST['step_post_name'] ) ? sanitize_text_field( wp_unslash( $_POST['step_post_name'] ) ) : get_post_field( 'post_name', $step_id );
422
423 $step_data = array(
424 'ID' => $step_id,
425 'post_title' => $step_tile,
426 'post_name' => $step_slug,
427 );
428
429 wp_update_post( $step_data );
430
431 $post_meta = '';
432
433 $step_type = wcf()->utils->get_step_type( $step_id );
434
435 $post_meta = AdminHelper::get_step_default_meta( $step_type, $step_id );
436
437 $skipped_fields = MetaOps::save_meta_fields( $step_id, $post_meta, 'cartflows_save_meta_settings' );
438
439 // We are storing the step dynamic css in the post meta and deleting when any setting changes. Once deleted it will be regenerated with first page load.
440 delete_post_meta( $step_id, 'wcf-dynamic-css' );
441
442 do_action( 'cartflows_admin_save_step_meta', $step_id );
443
444 $meta_options = AdminHelper::get_step_meta_options( $step_id );
445
446 // Delete the Google font URL and generate it while loading the page first time.
447 delete_post_meta( $step_id, 'wcf-field-google-font-url' );
448
449 $result = array(
450 'status' => true,
451 'reload' => false,
452 'options' => $meta_options,
453 'step_view_url' => get_the_permalink( $step_id ),
454 /* translators: %s flow id */
455 'text' => sprintf( __( 'Data saved successfully for step id %s', 'cartflows' ), $step_id ),
456 );
457
458 // Name anything the save dropped rather than reporting a clean save the user did not get.
459 if ( ! empty( $skipped_fields ) ) {
460 $result['skipped_fields'] = $skipped_fields;
461 $result['text'] = __( 'Saved, but the custom script fields were not updated because your account cannot edit scripts.', 'cartflows' );
462 }
463
464 wp_send_json( $result );
465 }
466
467 /**
468 * Process redirect updates when a step is deleted.
469 *
470 * Handles updating redirects for source steps (offer steps pointing to the deleted step).
471 * This includes both control steps and A/B testing variations.
472 *
473 * @since 3.1.0
474 * @param array<int, array<string, mixed>> $flow_steps The current flow steps array.
475 * @param int $flow_id The flow post ID — used to enforce per-flow ownership on each ID in the payload.
476 * @return array<int, array<string, mixed>> The updated flow steps array.
477 */
478 private function process_redirect_updates_on_step_deletion( $flow_steps, $flow_id ) {
479
480 // Per-call IDOR guard — re-verify the current user can edit this flow.
481 if ( ! $this->user_can_edit_flow( $flow_id ) ) {
482 return $flow_steps;
483 }
484
485 $redirect_updates_json = isset( $_POST['redirect_updates'] ) ? sanitize_text_field( wp_unslash( $_POST['redirect_updates'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing
486
487 if ( empty( $redirect_updates_json ) ) {
488 return $flow_steps;
489 }
490
491 $redirect_updates = json_decode( $redirect_updates_json, true );
492
493 if ( ! is_array( $redirect_updates ) ) {
494 return $flow_steps;
495 }
496
497 foreach ( $redirect_updates as $source_step_id => $redirects ) {
498 $source_step_id = absint( $source_step_id );
499
500 // IDOR guard — source step must belong to this flow.
501 if ( ! $this->is_step_in_flow( $source_step_id, $flow_id ) ) {
502 continue;
503 }
504
505 // IDOR guard — accept/reject targets must belong to this flow if provided.
506 if ( ! empty( $redirects['accept'] ) && ! $this->is_step_in_flow( absint( $redirects['accept'] ), $flow_id ) ) {
507 unset( $redirects['accept'] );
508 }
509 if ( ! empty( $redirects['reject'] ) && ! $this->is_step_in_flow( absint( $redirects['reject'] ), $flow_id ) ) {
510 unset( $redirects['reject'] );
511 }
512
513 $flow_steps = $this->update_single_step_redirects( $flow_steps, $source_step_id, $redirects );
514 }
515
516 return $flow_steps;
517 }
518
519 /**
520 * Update redirects for a single source step.
521 *
522 * Determines if the source is a control step or a variation and delegates
523 * to the appropriate handler.
524 *
525 * @since 3.1.0
526 * @param array<int, array<string, mixed>> $flow_steps The current flow steps array.
527 * @param int $source_step_id The source step ID.
528 * @param array<string, mixed> $redirects The redirect targets (accept/reject).
529 * @return array<int, array<string, mixed>> The updated flow steps array.
530 */
531 private function update_single_step_redirects( $flow_steps, $source_step_id, $redirects ) {
532 if ( $source_step_id <= 0 ) {
533 return $flow_steps;
534 }
535
536 $accept_target = isset( $redirects['accept'] ) && ! empty( $redirects['accept'] ) ? absint( $redirects['accept'] ) : 0;
537 $reject_target = isset( $redirects['reject'] ) && ! empty( $redirects['reject'] ) ? absint( $redirects['reject'] ) : 0;
538
539 // First, check if it's a control step.
540 $flow_steps = $this->update_control_step_redirects( $flow_steps, $source_step_id, $accept_target, $reject_target );
541
542 // If not found as control step, check if it's a variation.
543 $flow_steps = $this->update_variation_step_redirects( $flow_steps, $source_step_id, $accept_target, $reject_target );
544
545 return $flow_steps;
546 }
547
548 /**
549 * Update redirects for a control step (direct match in flow_steps).
550 *
551 * @since 3.1.0
552 * @param array<int, array<string, mixed>> $flow_steps The current flow steps array.
553 * @param int $source_step_id The source step ID.
554 * @param int $accept_target The accept redirect target step ID.
555 * @param int $reject_target The reject redirect target step ID.
556 * @return array<int, array<string, mixed>> The updated flow steps array.
557 */
558 private function update_control_step_redirects( $flow_steps, $source_step_id, $accept_target, $reject_target ) {
559 foreach ( $flow_steps as $idx => $step_data ) {
560 if ( isset( $step_data['id'] ) && (int) $step_data['id'] === $source_step_id ) {
561 // Update post meta for the control step.
562 if ( $accept_target > 0 ) {
563 update_post_meta( $source_step_id, 'wcf-yes-next-step', $accept_target );
564 $flow_steps[ $idx ]['offer_yes_step_id'] = $accept_target;
565 }
566 if ( $reject_target > 0 ) {
567 update_post_meta( $source_step_id, 'wcf-no-next-step', $reject_target );
568 $flow_steps[ $idx ]['offer_no_step_id'] = $reject_target;
569 }
570 break;
571 }
572 }
573
574 return $flow_steps;
575 }
576
577 /**
578 * Update redirects for an A/B test variation step.
579 *
580 * Variations don't have separate post meta, they're stored in the flow_steps array.
581 *
582 * @since 3.1.0
583 * @param array<int, array<string, mixed>> $flow_steps The current flow steps array.
584 * @param int $source_step_id The source step ID (variation ID).
585 * @param int $accept_target The accept redirect target step ID.
586 * @param int $reject_target The reject redirect target step ID.
587 * @return array<int, array<string, mixed>> The updated flow steps array.
588 */
589 private function update_variation_step_redirects( $flow_steps, $source_step_id, $accept_target, $reject_target ) {
590 foreach ( $flow_steps as $idx => $step_data ) {
591 // Check if this step has A/B test variations.
592 if ( isset( $step_data['ab-test-variations'] ) && is_array( $step_data['ab-test-variations'] ) ) {
593 foreach ( $step_data['ab-test-variations'] as $var_idx => $variation ) {
594 if ( isset( $variation['id'] ) && (int) $variation['id'] === $source_step_id ) {
595 // Update the variation's redirects in the flow_steps array.
596 if ( $accept_target > 0 ) {
597 $flow_steps[ $idx ]['ab-test-variations'][ $var_idx ]['offer_yes_step_id'] = $accept_target;
598 }
599 if ( $reject_target > 0 ) {
600 $flow_steps[ $idx ]['ab-test-variations'][ $var_idx ]['offer_no_step_id'] = $reject_target;
601 }
602 return $flow_steps; // Found and updated, exit early.
603 }
604 }
605 }
606 }
607
608 return $flow_steps;
609 }
610 }
611