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
← All changes | admin-core/ajax/steps.php +152 -1 3.0.1 → 3.3.0 View file →
@@ -329,8 +329,12 @@
329 329 if ( ! is_array( $flow_steps ) ) {
330 330 wp_send_json( $result );
331 331 }
332 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 +
333 337 $is_ab_test = get_post_meta( $step_id, 'wcf-ab-test', true );
334 338
335 339 if ( ! $is_ab_test ) {
336 340
@@ -429,9 +433,9 @@
429 433 $step_type = wcf()->utils->get_step_type( $step_id );
430 434
431 435 $post_meta = AdminHelper::get_step_default_meta( $step_type, $step_id );
432 436
433 - MetaOps::save_meta_fields( $step_id, $post_meta, 'cartflows_save_meta_settings' );
437 + $skipped_fields = MetaOps::save_meta_fields( $step_id, $post_meta, 'cartflows_save_meta_settings' );
434 438
435 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.
436 440 delete_post_meta( $step_id, 'wcf-dynamic-css' );
437 441
@@ -450,10 +454,157 @@
450 454 /* translators: %s flow id */
451 455 'text' => sprintf( __( 'Data saved successfully for step id %s', 'cartflows' ), $step_id ),
452 456 );
453 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 +
454 464 wp_send_json( $result );
465 + }
455 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;
456 517 }
457 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 + }
458 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 + }
459 610 }