| @@ -422,8 +422,12 @@ | ||
| 422 | 422 | /* translators: %s: step ID */ |
| 423 | 423 | wp_send_json_error( sprintf( __( 'Invalid step id %1$s.', 'cartflows' ), $new_step_id ) ); |
| 424 | 424 | } |
| 425 | 425 | |
| 426 | + // Insert the new step at the clicked edge position when invoked from a canvas connector. | |
| 427 | + $this->handle_edge_insertion_reorder( $flow_id, $new_step_id ); | |
| 428 | + $this->update_offer_step_source_redirects( $flow_id, $new_step_id ); | |
| 429 | + | |
| 426 | 430 | /** |
| 427 | 431 | * Redirect to the new flow edit screen |
| 428 | 432 | */ |
| 429 | 433 | $response_data = array( |
| @@ -734,11 +738,11 @@ | ||
| 734 | 738 | $cta = ''; |
| 735 | 739 | $btn = ''; |
| 736 | 740 | if ( 'not-installed' === $cf_pro_status ) { |
| 737 | 741 | /* translators: %1$s: link html start, %2$s: link html end*/ |
| 738 | - $btn = sprintf( __( 'CartFlows Pro Required! %1$sUpgrade to CartFlows Pro%2$s', 'cartflows' ), '<a target="_blank" href="https://cartflows.com/?utm_source=dashboard&utm_medium=free-cartflows&utm_campaign=go-pro">', '</a>' ); | |
| 742 | + $btn = sprintf( __( 'CartFlows Pro Required! %1$sUpgrade to CartFlows Pro%2$s', 'cartflows' ), '<a target="_blank" href="' . esc_url( \Cartflows_Helper::get_upgrade_to_pro_link( '', 'https://cartflows.com/' ) ) . '">', '</a>' ); | |
| 739 | 743 | /* translators: %1$s: link html start, %2$s: link html end*/ |
| 740 | - $cta = sprintf( __( 'To import the premium flow %1$supgrade to CartFlows Pro%2$s.', 'cartflows' ), '<a target="_blank" href="https://cartflows.com/?utm_source=dashboard&utm_medium=free-cartflows&utm_campaign=go-pro">', '</a>' ); | |
| 744 | + $cta = sprintf( __( 'To import the premium flow %1$supgrade to CartFlows Pro%2$s.', 'cartflows' ), '<a target="_blank" href="' . esc_url( \Cartflows_Helper::get_upgrade_to_pro_link( '', 'https://cartflows.com/' ) ) . '">', '</a>' ); | |
| 741 | 745 | } elseif ( 'inactive' === $cf_pro_status ) { |
| 742 | 746 | /* translators: %1$s: link html start, %2$s: link html end*/ |
| 743 | 747 | $btn = sprintf( __( 'Activate the CartFlows Pro to import the flow! %1$sActivate CartFlows Pro%2$s', 'cartflows' ), '<a target="_blank" href="' . admin_url( 'plugins.php?plugin_status=search&paged=1&s=CartFlows+Pro' ) . '">', '</a>' ); |
| 744 | 748 | /* translators: %1$s: link html start, %2$s: link html end*/ |
| @@ -912,9 +916,9 @@ | ||
| 912 | 916 | $response = \CartFlows_API::get_instance()->get_flow( $remote_flow_id ); |
| 913 | 917 | |
| 914 | 918 | if ( is_wp_error( $response['data'] ) ) { |
| 915 | 919 | /* translators: %1$s: html tag, %2$s: link html start %3$s: link html end */ |
| 916 | - $btn = sprintf( __( 'Request timeout error. Please check if the firewall or any security plugin is blocking the outgoing HTTP/HTTPS requests to templates.cartflows.com or not. %1$sTo resolve this issue, please check this %2$sarticle%3$s.', 'cartflows' ), '<br><br>', '<a target="_blank" href="https://cartflows.com/docs/request-timeout-error-while-importing-the-flow-step-templates/?utm_source=dashboard&utm_medium=free-cartflows&utm_campaign=docs">', '</a>' ); | |
| 920 | + $btn = sprintf( __( 'Request timeout error. Please check if the firewall or any security plugin is blocking the outgoing HTTP/HTTPS requests to templates.cartflows.com or not. %1$sTo resolve this issue, please check this %2$sarticle%3$s.', 'cartflows' ), '<br><br>', '<a target="_blank" href="' . esc_url( \Cartflows_Helper::get_kb_doc_link( 'https://cartflows.com/docs/request-timeout-error-while-importing-the-flow-step-templates/' ) ) . '">', '</a>' ); | |
| 917 | 921 | |
| 918 | 922 | wp_send_json_error( |
| 919 | 923 | array( |
| 920 | 924 | 'message' => $response['data']->get_error_message(), |
| @@ -934,9 +938,9 @@ | ||
| 934 | 938 | $msg = ''; |
| 935 | 939 | $cta = ''; |
| 936 | 940 | if ( 'not-installed' === $cf_pro_status ) { |
| 937 | 941 | /* translators: %1$s: link html start, %2$s: link html end*/ |
| 938 | - $cta = sprintf( __( '%1$sUpgrade to CartFlows Pro.%2$s', 'cartflows' ), '<a target="_blanks" class="wcf-button wcf-primary-button" href="https://cartflows.com/?utm_source=dashboard&utm_medium=free-cartflows&utm_campaign=go-pro">', '</a>' ); | |
| 942 | + $cta = sprintf( __( '%1$sUpgrade to CartFlows Pro.%2$s', 'cartflows' ), '<a target="_blanks" class="wcf-button wcf-primary-button" href="' . esc_url( \Cartflows_Helper::get_upgrade_to_pro_link( '', 'https://cartflows.com/' ) ) . '">', '</a>' ); | |
| 939 | 943 | $msg = __( 'To import the premium step, please upgrade to CartFlows Pro', 'cartflows' ); |
| 940 | 944 | } elseif ( 'inactive' === $cf_pro_status ) { |
| 941 | 945 | /* translators: %1$s: link html start, %2$s: link html end*/ |
| 942 | 946 | $cta = sprintf( __( '%1$sActivate CartFlows Pro%2$s', 'cartflows' ), '<a target="_blank" class="wcf-button wcf-primary-button" href="' . admin_url( 'plugins.php?plugin_status=search&paged=1&s=CartFlows+Pro' ) . '">', '</a>' ); |
| @@ -1035,9 +1039,9 @@ | ||
| 1035 | 1039 | // Get single step Rest API response. |
| 1036 | 1040 | $response = \CartFlows_API::get_instance()->get_flow( $remote_flow_id ); |
| 1037 | 1041 | if ( is_wp_error( $response['data'] ) ) { |
| 1038 | 1042 | /* translators: %1$s: html tag, %2$s: link html start %3$s: link html end */ |
| 1039 | - $btn = sprintf( __( 'Request timeout error. Please check if the firewall or any security plugin is blocking the outgoing HTTP/HTTPS requests to templates.cartflows.com or not. %1$sTo resolve this issue, please check this %2$s article%3$s.', 'cartflows' ), '<br><br>', '<a target="_blank" href="https://cartflows.com/docs/request-timeout-error-while-importing-the-flow-step-templates/?utm_source=dashboard&utm_medium=free-cartflows&utm_campaign=docs">', '</a>' ); | |
| 1043 | + $btn = sprintf( __( 'Request timeout error. Please check if the firewall or any security plugin is blocking the outgoing HTTP/HTTPS requests to templates.cartflows.com or not. %1$sTo resolve this issue, please check this %2$s article%3$s.', 'cartflows' ), '<br><br>', '<a target="_blank" href="' . esc_url( \Cartflows_Helper::get_kb_doc_link( 'https://cartflows.com/docs/request-timeout-error-while-importing-the-flow-step-templates/' ) ) . '">', '</a>' ); | |
| 1040 | 1044 | |
| 1041 | 1045 | wp_send_json_error( |
| 1042 | 1046 | array( |
| 1043 | 1047 | 'message' => $response['data']->get_error_message(), |
| @@ -1056,9 +1060,9 @@ | ||
| 1056 | 1060 | |
| 1057 | 1061 | $cta = ''; |
| 1058 | 1062 | if ( 'not-installed' === $cf_pro_status ) { |
| 1059 | 1063 | /* translators: %1$s: link html start, %2$s: link html end*/ |
| 1060 | - $cta = sprintf( __( 'Upgrade to %1$sCartFlows Pro.%2$s', 'cartflows' ), '<a target="_blanks" href="https://cartflows.com/?utm_source=dashboard&utm_medium=free-cartflows&utm_campaign=go-pro">', '</a>' ); | |
| 1064 | + $cta = sprintf( __( 'Upgrade to %1$sCartFlows Pro.%2$s', 'cartflows' ), '<a target="_blanks" href="' . esc_url( \Cartflows_Helper::get_upgrade_to_pro_link( '', 'https://cartflows.com/' ) ) . '">', '</a>' ); | |
| 1061 | 1065 | } elseif ( 'inactive' === $cf_pro_status ) { |
| 1062 | 1066 | /* translators: %1$s: link html start, %2$s: link html end*/ |
| 1063 | 1067 | $cta = sprintf( __( '%1$sActivate CartFlows Pro%2$s', 'cartflows' ), '<a target="_blank" href="' . admin_url( 'plugins.php?plugin_status=search&paged=1&s=CartFlows+Pro' ) . '">', '</a>' ); |
| 1064 | 1068 | } elseif ( 'active' === $cf_pro_status ) { |
| @@ -1217,8 +1221,14 @@ | ||
| 1217 | 1221 | /* translators: %s: step ID */ |
| 1218 | 1222 | wp_send_json_error( sprintf( __( 'Invalid step id %1$s or post id %2$s.', 'cartflows' ), $step_id, $new_step_id ) ); |
| 1219 | 1223 | } |
| 1220 | 1224 | |
| 1225 | + // Handle edge insertion - reorder flow steps when inserting on an edge. | |
| 1226 | + $this->handle_edge_insertion_reorder( $flow_id, $new_step_id ); | |
| 1227 | + | |
| 1228 | + // Handle offer edge source update - update source offer step's redirect to point to new step. | |
| 1229 | + $this->update_offer_step_source_redirects( $flow_id, $new_step_id ); | |
| 1230 | + | |
| 1221 | 1231 | wcf()->logger->import_log( 'Remote Step ' . $step_id . ' for local flow "' . get_the_title( $new_step_id ) . '" [' . $new_step_id . ']' ); |
| 1222 | 1232 | |
| 1223 | 1233 | // Get single step Rest API response. |
| 1224 | 1234 | $response = \CartFlows_API::get_instance()->get_template( $step_id ); |
| @@ -1268,8 +1278,11 @@ | ||
| 1268 | 1278 | |
| 1269 | 1279 | // Import Post Meta. |
| 1270 | 1280 | $this->import_post_meta( $new_step_id, $response ); |
| 1271 | 1281 | |
| 1282 | + // Handle user-selected offer redirect settings for upsell/downsell steps. | |
| 1283 | + $this->update_user_selected_offer_redirects( $flow_id, $new_step_id, $step_type ); | |
| 1284 | + | |
| 1272 | 1285 | if ( 'checkout' === $step_type ) { |
| 1273 | 1286 | |
| 1274 | 1287 | $posted_data = array( |
| 1275 | 1288 | 'primary_color' => isset( $_POST['primary_color'] ) ? sanitize_text_field( wp_unslash( $_POST['primary_color'] ) ) : '', |
| @@ -1649,6 +1662,291 @@ | ||
| 1649 | 1662 | } |
| 1650 | 1663 | } |
| 1651 | 1664 | |
| 1652 | 1665 | return false; |
| 1666 | + } | |
| 1667 | + | |
| 1668 | + /** | |
| 1669 | + * Handle edge insertion reorder. | |
| 1670 | + * | |
| 1671 | + * Reorders flow steps when a new step is inserted on an edge between two existing steps. | |
| 1672 | + * Handles both START node edges and regular step edges. | |
| 1673 | + * | |
| 1674 | + * @since 3.1.0 | |
| 1675 | + * @param int $flow_id The flow ID. | |
| 1676 | + * @param int $new_step_id The newly created step ID. | |
| 1677 | + * @return void | |
| 1678 | + */ | |
| 1679 | + private function handle_edge_insertion_reorder( $flow_id, $new_step_id ) { | |
| 1680 | + | |
| 1681 | + // Per-call IDOR guard — re-verify the current user can edit this flow. | |
| 1682 | + if ( ! $this->user_can_edit_flow( $flow_id ) ) { | |
| 1683 | + return; | |
| 1684 | + } | |
| 1685 | + | |
| 1686 | + $is_start_edge = isset( $_POST['is_start_edge'] ) && 'true' === $_POST['is_start_edge']; // phpcs:ignore WordPress.Security.NonceVerification.Missing | |
| 1687 | + $edge_source_step_id = isset( $_POST['edge_source_step_id'] ) ? absint( $_POST['edge_source_step_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing | |
| 1688 | + $edge_target_step_id = isset( $_POST['edge_target_step_id'] ) ? absint( $_POST['edge_target_step_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing | |
| 1689 | + | |
| 1690 | + // Handle edge insertion if we have a target step ID (either from START node or regular step). | |
| 1691 | + if ( ! $edge_target_step_id || ( ! $is_start_edge && ! $edge_source_step_id ) ) { | |
| 1692 | + return; | |
| 1693 | + } | |
| 1694 | + | |
| 1695 | + // IDOR guard — both edge endpoints must belong to this flow. | |
| 1696 | + // The START node is virtual and has no step ID, so skip its check. | |
| 1697 | + if ( ! $is_start_edge && ! $this->is_step_in_flow( $edge_source_step_id, $flow_id ) ) { | |
| 1698 | + return; | |
| 1699 | + } | |
| 1700 | + if ( ! $this->is_step_in_flow( $edge_target_step_id, $flow_id ) ) { | |
| 1701 | + return; | |
| 1702 | + } | |
| 1703 | + | |
| 1704 | + // Get current flow steps. | |
| 1705 | + $flow_steps = get_post_meta( (int) $flow_id, 'wcf-steps', true ); | |
| 1706 | + | |
| 1707 | + if ( ! is_array( $flow_steps ) || empty( $flow_steps ) ) { | |
| 1708 | + return; | |
| 1709 | + } | |
| 1710 | + | |
| 1711 | + // Find indices of source and target steps. | |
| 1712 | + $source_index = -1; // -1 for START node means insert at position 0. | |
| 1713 | + $target_index = -1; | |
| 1714 | + $new_step_index = -1; | |
| 1715 | + | |
| 1716 | + foreach ( $flow_steps as $index => $step_data ) { | |
| 1717 | + if ( isset( $step_data['id'] ) ) { | |
| 1718 | + if ( ! $is_start_edge && $step_data['id'] === $edge_source_step_id ) { | |
| 1719 | + $source_index = $index; | |
| 1720 | + } | |
| 1721 | + if ( $step_data['id'] === $edge_target_step_id ) { | |
| 1722 | + $target_index = $index; | |
| 1723 | + } | |
| 1724 | + if ( $step_data['id'] === $new_step_id ) { | |
| 1725 | + $new_step_index = $index; | |
| 1726 | + } | |
| 1727 | + } | |
| 1728 | + } | |
| 1729 | + | |
| 1730 | + // Determine if we can proceed with reordering. | |
| 1731 | + $can_reorder = -1 !== $target_index && -1 !== $new_step_index; | |
| 1732 | + if ( ! $is_start_edge ) { | |
| 1733 | + $can_reorder = $can_reorder && -1 !== $source_index; | |
| 1734 | + } | |
| 1735 | + | |
| 1736 | + if ( ! $can_reorder ) { | |
| 1737 | + return; | |
| 1738 | + } | |
| 1739 | + | |
| 1740 | + // Remove the new step from its current position (at the end). | |
| 1741 | + $new_step_data = $flow_steps[ $new_step_index ]; | |
| 1742 | + array_splice( $flow_steps, $new_step_index, 1 ); | |
| 1743 | + | |
| 1744 | + // Recalculate indices after removal (if new step was before them). | |
| 1745 | + if ( ! $is_start_edge && $new_step_index < $source_index ) { | |
| 1746 | + --$source_index; | |
| 1747 | + } | |
| 1748 | + if ( $new_step_index < $target_index ) { | |
| 1749 | + --$target_index; | |
| 1750 | + } | |
| 1751 | + | |
| 1752 | + // Insert the new step at the correct position. | |
| 1753 | + if ( $is_start_edge ) { | |
| 1754 | + // START node: insert at position 0 (beginning of flow). | |
| 1755 | + $insert_position = 0; | |
| 1756 | + } else { | |
| 1757 | + // Regular step: insert right after the source step. | |
| 1758 | + $insert_position = $source_index + 1; | |
| 1759 | + } | |
| 1760 | + | |
| 1761 | + array_splice( $flow_steps, (int) $insert_position, 0, array( $new_step_data ) ); | |
| 1762 | + | |
| 1763 | + // Update the flow steps meta. | |
| 1764 | + update_post_meta( (int) $flow_id, 'wcf-steps', $flow_steps ); | |
| 1765 | + } | |
| 1766 | + | |
| 1767 | + /** | |
| 1768 | + * Update offer step source redirects. | |
| 1769 | + * | |
| 1770 | + * When inserting on an Accept/Reject edge, updates the source offer step's | |
| 1771 | + * redirect to point to the new step. | |
| 1772 | + * | |
| 1773 | + * @since 3.1.0 | |
| 1774 | + * @param int $flow_id The flow ID. | |
| 1775 | + * @param int $new_step_id The newly created step ID. | |
| 1776 | + * @return void | |
| 1777 | + */ | |
| 1778 | + private function update_offer_step_source_redirects( $flow_id, $new_step_id ) { | |
| 1779 | + | |
| 1780 | + // Per-call IDOR guard — re-verify the current user can edit this flow. | |
| 1781 | + if ( ! $this->user_can_edit_flow( $flow_id ) ) { | |
| 1782 | + return; | |
| 1783 | + } | |
| 1784 | + | |
| 1785 | + $edge_source_step_id = isset( $_POST['edge_source_step_id'] ) ? absint( $_POST['edge_source_step_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing | |
| 1786 | + $edge_source_handle = isset( $_POST['edge_source_handle'] ) ? sanitize_text_field( wp_unslash( $_POST['edge_source_handle'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing | |
| 1787 | + | |
| 1788 | + if ( $edge_source_step_id <= 0 || empty( $edge_source_handle ) ) { | |
| 1789 | + return; | |
| 1790 | + } | |
| 1791 | + | |
| 1792 | + // IDOR guard — both source and target steps must belong to this flow. | |
| 1793 | + if ( ! $this->is_step_in_flow( $edge_source_step_id, $flow_id ) ) { | |
| 1794 | + return; | |
| 1795 | + } | |
| 1796 | + if ( ! $this->is_step_in_flow( $new_step_id, $flow_id ) ) { | |
| 1797 | + return; | |
| 1798 | + } | |
| 1799 | + | |
| 1800 | + $source_step_type = get_post_meta( $edge_source_step_id, 'wcf-step-type', true ); | |
| 1801 | + | |
| 1802 | + // Only update if source is an offer step (upsell/downsell). | |
| 1803 | + if ( ! in_array( $source_step_type, array( 'upsell', 'downsell' ), true ) ) { | |
| 1804 | + return; | |
| 1805 | + } | |
| 1806 | + | |
| 1807 | + $source_meta_updated = false; | |
| 1808 | + | |
| 1809 | + // Update the appropriate post meta based on handle. | |
| 1810 | + if ( 'a' === $edge_source_handle ) { | |
| 1811 | + // Accept edge - update wcf-yes-next-step. | |
| 1812 | + update_post_meta( $edge_source_step_id, 'wcf-yes-next-step', $new_step_id ); | |
| 1813 | + $source_meta_updated = true; | |
| 1814 | + } elseif ( 'b' === $edge_source_handle ) { | |
| 1815 | + // Reject edge - update wcf-no-next-step. | |
| 1816 | + update_post_meta( $edge_source_step_id, 'wcf-no-next-step', $new_step_id ); | |
| 1817 | + $source_meta_updated = true; | |
| 1818 | + } | |
| 1819 | + | |
| 1820 | + // Also update the flow_steps array to keep it in sync. | |
| 1821 | + if ( $source_meta_updated ) { | |
| 1822 | + $this->sync_offer_redirects_in_flow_steps( $flow_id, $edge_source_step_id, $edge_source_handle, $new_step_id ); | |
| 1823 | + } | |
| 1824 | + } | |
| 1825 | + | |
| 1826 | + /** | |
| 1827 | + * Sync offer redirects in flow_steps array. | |
| 1828 | + * | |
| 1829 | + * Updates the flow_steps array to keep it in sync with post meta. | |
| 1830 | + * Handles both control steps and A/B test variations. | |
| 1831 | + * | |
| 1832 | + * @since 3.1.0 | |
| 1833 | + * @param int $flow_id The flow ID. | |
| 1834 | + * @param int $source_step_id The source offer step ID. | |
| 1835 | + * @param string $handle The edge handle ('a' for accept, 'b' for reject). | |
| 1836 | + * @param int $target_step_id The target step ID to redirect to. | |
| 1837 | + * @return void | |
| 1838 | + */ | |
| 1839 | + private function sync_offer_redirects_in_flow_steps( $flow_id, $source_step_id, $handle, $target_step_id ) { | |
| 1840 | + $updated_flow_steps = get_post_meta( (int) $flow_id, 'wcf-steps', true ); | |
| 1841 | + | |
| 1842 | + if ( ! is_array( $updated_flow_steps ) ) { | |
| 1843 | + return; | |
| 1844 | + } | |
| 1845 | + | |
| 1846 | + $flow_steps_updated = false; | |
| 1847 | + | |
| 1848 | + foreach ( $updated_flow_steps as $idx => $step_data ) { | |
| 1849 | + // Check if this is the control step (direct match). | |
| 1850 | + if ( isset( $step_data['id'] ) && (int) $step_data['id'] === (int) $source_step_id ) { | |
| 1851 | + if ( 'a' === $handle ) { | |
| 1852 | + $updated_flow_steps[ $idx ]['offer_yes_step_id'] = $target_step_id; | |
| 1853 | + } elseif ( 'b' === $handle ) { | |
| 1854 | + $updated_flow_steps[ $idx ]['offer_no_step_id'] = $target_step_id; | |
| 1855 | + } | |
| 1856 | + $flow_steps_updated = true; | |
| 1857 | + break; | |
| 1858 | + } | |
| 1859 | + | |
| 1860 | + // Check if the source is an A/B test variation within this step. | |
| 1861 | + if ( isset( $step_data['ab-test-variations'] ) && is_array( $step_data['ab-test-variations'] ) ) { | |
| 1862 | + foreach ( $step_data['ab-test-variations'] as $var_idx => $variation ) { | |
| 1863 | + if ( isset( $variation['id'] ) && (int) $variation['id'] === (int) $source_step_id ) { | |
| 1864 | + if ( 'a' === $handle ) { | |
| 1865 | + $updated_flow_steps[ $idx ]['ab-test-variations'][ $var_idx ]['offer_yes_step_id'] = $target_step_id; | |
| 1866 | + } elseif ( 'b' === $handle ) { | |
| 1867 | + $updated_flow_steps[ $idx ]['ab-test-variations'][ $var_idx ]['offer_no_step_id'] = $target_step_id; | |
| 1868 | + } | |
| 1869 | + $flow_steps_updated = true; | |
| 1870 | + break 2; // Break out of both loops. | |
| 1871 | + } | |
| 1872 | + } | |
| 1873 | + } | |
| 1874 | + } | |
| 1875 | + | |
| 1876 | + if ( $flow_steps_updated ) { | |
| 1877 | + update_post_meta( (int) $flow_id, 'wcf-steps', $updated_flow_steps ); | |
| 1878 | + } | |
| 1879 | + } | |
| 1880 | + | |
| 1881 | + /** | |
| 1882 | + * Update user-selected offer redirects. | |
| 1883 | + * | |
| 1884 | + * Handles user-selected offer redirect settings for upsell/downsell steps | |
| 1885 | + * when the user explicitly selects Accept/Reject targets via the UI. | |
| 1886 | + * | |
| 1887 | + * @since 3.1.0 | |
| 1888 | + * @param int $flow_id The flow ID. | |
| 1889 | + * @param int $new_step_id The newly created step ID. | |
| 1890 | + * @param string $step_type The step type. | |
| 1891 | + * @return void | |
| 1892 | + */ | |
| 1893 | + private function update_user_selected_offer_redirects( $flow_id, $new_step_id, $step_type ) { | |
| 1894 | + | |
| 1895 | + // Per-call IDOR guard — re-verify the current user can edit this flow. | |
| 1896 | + if ( ! $this->user_can_edit_flow( $flow_id ) ) { | |
| 1897 | + return; | |
| 1898 | + } | |
| 1899 | + | |
| 1900 | + // Only process for offer steps. | |
| 1901 | + if ( ! in_array( $step_type, array( 'upsell', 'downsell' ), true ) ) { | |
| 1902 | + return; | |
| 1903 | + } | |
| 1904 | + | |
| 1905 | + // IDOR guard — the new step itself must belong to this flow. | |
| 1906 | + if ( ! $this->is_step_in_flow( $new_step_id, $flow_id ) ) { | |
| 1907 | + return; | |
| 1908 | + } | |
| 1909 | + | |
| 1910 | + $user_offer_yes_step_id = isset( $_POST['offer_yes_step_id'] ) ? absint( $_POST['offer_yes_step_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing | |
| 1911 | + $user_offer_no_step_id = isset( $_POST['offer_no_step_id'] ) ? absint( $_POST['offer_no_step_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing | |
| 1912 | + | |
| 1913 | + // IDOR guard — redirect targets must belong to this flow if provided. | |
| 1914 | + if ( $user_offer_yes_step_id && ! $this->is_step_in_flow( $user_offer_yes_step_id, $flow_id ) ) { | |
| 1915 | + $user_offer_yes_step_id = 0; | |
| 1916 | + } | |
| 1917 | + if ( $user_offer_no_step_id && ! $this->is_step_in_flow( $user_offer_no_step_id, $flow_id ) ) { | |
| 1918 | + $user_offer_no_step_id = 0; | |
| 1919 | + } | |
| 1920 | + | |
| 1921 | + // Return if user didn't select any custom redirects. | |
| 1922 | + if ( ! $user_offer_yes_step_id && ! $user_offer_no_step_id ) { | |
| 1923 | + return; | |
| 1924 | + } | |
| 1925 | + | |
| 1926 | + $current_flow_steps = get_post_meta( (int) $flow_id, 'wcf-steps', true ); | |
| 1927 | + | |
| 1928 | + if ( ! is_array( $current_flow_steps ) ) { | |
| 1929 | + return; | |
| 1930 | + } | |
| 1931 | + | |
| 1932 | + foreach ( $current_flow_steps as $idx => $step_data ) { | |
| 1933 | + if ( isset( $step_data['id'] ) && $step_data['id'] === $new_step_id ) { | |
| 1934 | + // Update offer_yes_step_id if user selected one. | |
| 1935 | + if ( $user_offer_yes_step_id ) { | |
| 1936 | + $current_flow_steps[ $idx ]['offer_yes_step_id'] = $user_offer_yes_step_id; | |
| 1937 | + update_post_meta( $new_step_id, 'wcf-yes-next-step', $user_offer_yes_step_id ); | |
| 1938 | + } | |
| 1939 | + | |
| 1940 | + // Update offer_no_step_id if user selected one. | |
| 1941 | + if ( $user_offer_no_step_id ) { | |
| 1942 | + $current_flow_steps[ $idx ]['offer_no_step_id'] = $user_offer_no_step_id; | |
| 1943 | + update_post_meta( $new_step_id, 'wcf-no-next-step', $user_offer_no_step_id ); | |
| 1944 | + } | |
| 1945 | + break; | |
| 1946 | + } | |
| 1947 | + } | |
| 1948 | + | |
| 1949 | + // Save the updated flow steps. | |
| 1950 | + update_post_meta( (int) $flow_id, 'wcf-steps', $current_flow_steps ); | |
| 1653 | 1951 | } |
| 1654 | 1952 | } |