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-legacy-core/ajax/importer.php +304 -6 3.0.1 → 3.3.0 View file →
@@ -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 }