| @@ -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 | |
| @@ -451,9 +455,150 @@ | ||
| 451 | 455 | 'text' => sprintf( __( 'Data saved successfully for step id %s', 'cartflows' ), $step_id ), |
| 452 | 456 | ); |
| 453 | 457 | |
| 454 | 458 | wp_send_json( $result ); |
| 459 | + } | |
| 455 | 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; | |
| 456 | 511 | } |
| 457 | 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 $flow_steps The current flow steps array. | |
| 521 | + * @param int $source_step_id The source step ID. | |
| 522 | + * @param array $redirects The redirect targets (accept/reject). | |
| 523 | + * @return array 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 | + } | |
| 458 | 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 $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 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 $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 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 | + } | |
| 459 | 604 | } |