| @@ -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 | } |