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

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

605 lines 17.7 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 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 wp_send_json( $result );
459 }
460
461 /**
462 * Process redirect updates when a step is deleted.
463 *
464 * Handles updating redirects for source steps (offer steps pointing to the deleted step).
465 * This includes both control steps and A/B testing variations.
466 *
467 * @since 3.1.0
468 * @param array<int, array<string, mixed>> $flow_steps The current flow steps array.
469 * @param int $flow_id The flow post ID — used to enforce per-flow ownership on each ID in the payload.
470 * @return array<int, array<string, mixed>> The updated flow steps array.
471 */
472 private function process_redirect_updates_on_step_deletion( $flow_steps, $flow_id ) {
473
474 // Per-call IDOR guard — re-verify the current user can edit this flow.
475 if ( ! $this->user_can_edit_flow( $flow_id ) ) {
476 return $flow_steps;
477 }
478
479 $redirect_updates_json = isset( $_POST['redirect_updates'] ) ? sanitize_text_field( wp_unslash( $_POST['redirect_updates'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing
480
481 if ( empty( $redirect_updates_json ) ) {
482 return $flow_steps;
483 }
484
485 $redirect_updates = json_decode( $redirect_updates_json, true );
486
487 if ( ! is_array( $redirect_updates ) ) {
488 return $flow_steps;
489 }
490
491 foreach ( $redirect_updates as $source_step_id => $redirects ) {
492 $source_step_id = absint( $source_step_id );
493
494 // IDOR guard — source step must belong to this flow.
495 if ( ! $this->is_step_in_flow( $source_step_id, $flow_id ) ) {
496 continue;
497 }
498
499 // IDOR guard — accept/reject targets must belong to this flow if provided.
500 if ( ! empty( $redirects['accept'] ) && ! $this->is_step_in_flow( absint( $redirects['accept'] ), $flow_id ) ) {
501 unset( $redirects['accept'] );
502 }
503 if ( ! empty( $redirects['reject'] ) && ! $this->is_step_in_flow( absint( $redirects['reject'] ), $flow_id ) ) {
504 unset( $redirects['reject'] );
505 }
506
507 $flow_steps = $this->update_single_step_redirects( $flow_steps, $source_step_id, $redirects );
508 }
509
510 return $flow_steps;
511 }
512
513 /**
514 * Update redirects for a single source step.
515 *
516 * Determines if the source is a control step or a variation and delegates
517 * to the appropriate handler.
518 *
519 * @since 3.1.0
520 * @param array<int, array<string, mixed>> $flow_steps The current flow steps array.
521 * @param int $source_step_id The source step ID.
522 * @param array<string, mixed> $redirects The redirect targets (accept/reject).
523 * @return array<int, array<string, mixed>> The updated flow steps array.
524 */
525 private function update_single_step_redirects( $flow_steps, $source_step_id, $redirects ) {
526 if ( $source_step_id <= 0 ) {
527 return $flow_steps;
528 }
529
530 $accept_target = isset( $redirects['accept'] ) && ! empty( $redirects['accept'] ) ? absint( $redirects['accept'] ) : 0;
531 $reject_target = isset( $redirects['reject'] ) && ! empty( $redirects['reject'] ) ? absint( $redirects['reject'] ) : 0;
532
533 // First, check if it's a control step.
534 $flow_steps = $this->update_control_step_redirects( $flow_steps, $source_step_id, $accept_target, $reject_target );
535
536 // If not found as control step, check if it's a variation.
537 $flow_steps = $this->update_variation_step_redirects( $flow_steps, $source_step_id, $accept_target, $reject_target );
538
539 return $flow_steps;
540 }
541
542 /**
543 * Update redirects for a control step (direct match in flow_steps).
544 *
545 * @since 3.1.0
546 * @param array<int, array<string, mixed>> $flow_steps The current flow steps array.
547 * @param int $source_step_id The source step ID.
548 * @param int $accept_target The accept redirect target step ID.
549 * @param int $reject_target The reject redirect target step ID.
550 * @return array<int, array<string, mixed>> The updated flow steps array.
551 */
552 private function update_control_step_redirects( $flow_steps, $source_step_id, $accept_target, $reject_target ) {
553 foreach ( $flow_steps as $idx => $step_data ) {
554 if ( isset( $step_data['id'] ) && (int) $step_data['id'] === $source_step_id ) {
555 // Update post meta for the control step.
556 if ( $accept_target > 0 ) {
557 update_post_meta( $source_step_id, 'wcf-yes-next-step', $accept_target );
558 $flow_steps[ $idx ]['offer_yes_step_id'] = $accept_target;
559 }
560 if ( $reject_target > 0 ) {
561 update_post_meta( $source_step_id, 'wcf-no-next-step', $reject_target );
562 $flow_steps[ $idx ]['offer_no_step_id'] = $reject_target;
563 }
564 break;
565 }
566 }
567
568 return $flow_steps;
569 }
570
571 /**
572 * Update redirects for an A/B test variation step.
573 *
574 * Variations don't have separate post meta, they're stored in the flow_steps array.
575 *
576 * @since 3.1.0
577 * @param array<int, array<string, mixed>> $flow_steps The current flow steps array.
578 * @param int $source_step_id The source step ID (variation ID).
579 * @param int $accept_target The accept redirect target step ID.
580 * @param int $reject_target The reject redirect target step ID.
581 * @return array<int, array<string, mixed>> The updated flow steps array.
582 */
583 private function update_variation_step_redirects( $flow_steps, $source_step_id, $accept_target, $reject_target ) {
584 foreach ( $flow_steps as $idx => $step_data ) {
585 // Check if this step has A/B test variations.
586 if ( isset( $step_data['ab-test-variations'] ) && is_array( $step_data['ab-test-variations'] ) ) {
587 foreach ( $step_data['ab-test-variations'] as $var_idx => $variation ) {
588 if ( isset( $variation['id'] ) && (int) $variation['id'] === $source_step_id ) {
589 // Update the variation's redirects in the flow_steps array.
590 if ( $accept_target > 0 ) {
591 $flow_steps[ $idx ]['ab-test-variations'][ $var_idx ]['offer_yes_step_id'] = $accept_target;
592 }
593 if ( $reject_target > 0 ) {
594 $flow_steps[ $idx ]['ab-test-variations'][ $var_idx ]['offer_no_step_id'] = $reject_target;
595 }
596 return $flow_steps; // Found and updated, exit early.
597 }
598 }
599 }
600 }
601
602 return $flow_steps;
603 }
604 }
605