PluginProbe
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell / 3.13.4
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell v3.13.4
3.13.4 3.13.3 3.13.2 3.13.1 3.13.0 3.12.13 3.12.12 3.12.11 3.12.10 3.12.9 3.12.8 3.12.7 3.12.6 3.12.5 3.12.4 3.12.3 3.12.1 3.12.2 3.12.0 3.11.1 3.11.0 3.10.9 3.10.8 3.10.7 3.10.6 All 262 releases
← All changes | includes/core/rest-api/Controllers/class-funnel-controller.php +102 -4 3.12.13 → 3.13.4 View file →
@@ -749,14 +749,85 @@
749 749 $funnel_data = get_post_meta($funnel_id, 'funnel_data', true) ? get_post_meta($funnel_id, 'funnel_data', true) : get_post_meta($funnel_id, '_funnel_data', true);
750 750 $migration->update_funnel_data($funnel_id, $funnel_data);
751 751 update_post_meta($funnel_id, 'wpfnls_is_newui_migrated', 'yes');
752 752 }
753 + $this->reconcile_derived_step_meta($funnel_id);
753 754 $response = $this->prepare_funnel_data_response($funnel_id);
755 + // What saveFunnel checks before it overwrites the graph.
756 + $response['revision'] = self::get_funnel_revision($funnel_id);
754 757 return $this->prepare_item_for_response($response, $request);
755 758 }
756 759
757 760
758 761 /**
762 + * Revision of a funnel's stored graph: a hash of `_funnel_data`.
763 + *
764 + * Every save posts the whole graph, so two editors (two tabs, two people,
765 + * or a canvas left open while an import, MCP call or A/B swap rewrites the
766 + * graph) would overwrite each other's steps. A hash rather than a counter
767 + * changes whoever writes the meta, so no writer has to remember to bump it.
768 + *
769 + * @param int $funnel_id Funnel ID.
770 + *
771 + * @return string Revision.
772 + * @since 3.13.8
773 + */
774 + public static function get_funnel_revision($funnel_id)
775 + {
776 + return md5(maybe_serialize(get_post_meta($funnel_id, '_funnel_data', true)));
777 + }
778 +
779 +
780 + /**
781 + * Rebuild `_steps`, `_steps_order` and the first step from the stored graph
782 + * when they have drifted from it.
783 + *
784 + * The classic canvas saved the graph every time it opened, which rebuilt
785 + * these; the React canvas only saves after an edit. Funnels changed by an
786 + * import, MCP or an A/B swap would otherwise keep a stale `_steps_order`,
787 + * which gateways and A/B testing read on the front end. Writes only when
788 + * something differs, so an unchanged funnel costs no write.
789 + *
790 + * @param int $funnel_id Funnel ID.
791 + *
792 + * @return bool Whether anything was rewritten.
793 + * @since 3.13.8
794 + */
795 + private function reconcile_derived_step_meta($funnel_id)
796 + {
797 + if ('yes' !== get_post_meta($funnel_id, 'wpfnls_is_newui_migrated', true)) {
798 + return false;
799 + }
800 +
801 + $funnel_data = get_post_meta($funnel_id, '_funnel_data', true);
802 +
803 + // An empty or scratch funnel has nothing to derive from; leave it alone.
804 + if (empty($funnel_data['drawflow']['Home']['data']) || !is_array($funnel_data['drawflow']['Home']['data'])) {
805 + return false;
806 + }
807 +
808 + $changed = false;
809 + $steps = $this->get_steps($funnel_data);
810 +
811 + if ($steps != get_post_meta($funnel_id, '_steps', true)) {
812 + update_post_meta($funnel_id, '_steps', $steps);
813 + Wpfnl_functions::generate_first_step($funnel_id, $steps);
814 + $changed = true;
815 + }
816 +
817 + // As save_funnel_data stores it: empty entries dropped.
818 + $steps_order = array_values(array_filter($this->get_steps_order($funnel_data), 'count'));
819 +
820 + if (count($steps_order) && $steps_order != get_post_meta($funnel_id, '_steps_order', true)) {
821 + update_post_meta($funnel_id, '_steps_order', $steps_order);
822 + $changed = true;
823 + }
824 +
825 + return $changed;
826 + }
827 +
828 +
829 + /**
759 830 * Retrieves visit/conversion figures for a funnel's steps plus funnel totals.
760 831 *
761 832 * Deliberately kept out of get_funnel_data() so that opening a funnel on the
762 833 * canvas never blocks on analytics. On sites with large amounts of tracked
@@ -1180,8 +1251,10 @@
1180 1251 }
1181 1252 }
1182 1253
1183 1254 $_temp_step['should_assign_product'] = $should_assign_product;
1255 + // The Mail Mint automation linked to this step, so the canvas can open it.
1256 + $_temp_step['mint_automation_id'] = (int) get_post_meta( $step_id, 'wpfnl_mint_automation_id', true );
1184 1257
1185 1258 /**
1186 1259 * Fires to add Mail Mint automation data
1187 1260 *
@@ -1459,8 +1532,27 @@
1459 1532 $funnel_data = [];
1460 1533 $_steps = [];
1461 1534 $response = ['success' => true, 'link' => home_url()];
1462 1535
1536 + /*
1537 + * The revision the graph was read at (getFunnel / the last save). When
1538 + * the stored graph has changed since, this save would drop whatever
1539 + * changed it, so it is refused; the editor reloads and redoes its edit.
1540 + * Without one (the classic canvas, older callers) nothing is checked.
1541 + */
1542 + $base_revision = isset($request['revision']) ? sanitize_text_field($request['revision']) : '';
1543 +
1544 + if ($funnel_id && '' !== $base_revision && self::get_funnel_revision($funnel_id) !== $base_revision) {
1545 + return new WP_Error(
1546 + 'wpfunnels_funnel_conflict',
1547 + __('This funnel was changed somewhere else (another tab, another user or an import) since it was opened. The canvas now shows the latest version; please make your last change again.', 'wpfnl'),
1548 + array(
1549 + 'status' => 409,
1550 + 'revision' => self::get_funnel_revision($funnel_id),
1551 + )
1552 + );
1553 + }
1554 +
1463 1555 if ($funnel_json) {
1464 1556 $funnel_data = $funnel_json;
1465 1557 $steps = $funnel_data['drawflow']['Home']['data'];
1466 1558
@@ -1562,8 +1654,11 @@
1562 1654 Wpfnl_functions::generate_first_step($funnel_id);
1563 1655
1564 1656 do_action( 'wpfunnels_canvas_saved', absint( $funnel_id ) );
1565 1657
1658 + // Read after the hooks above, which may rewrite the graph themselves.
1659 + $response['revision'] = self::get_funnel_revision($funnel_id);
1660 +
1566 1661 return rest_ensure_response($response);
1567 1662 }
1568 1663
1569 1664
@@ -1841,12 +1936,12 @@
1841 1936 );
1842 1937 return rest_ensure_response($response);
1843 1938 }
1844 1939
1845 - $is_gbf = get_post_meta($funnel_id, 'is_global_funnel', true);
1940 + $is_gbf = get_post_meta($funnel_id, 'is_global_funnel', true);
1941 + $step_ids = array();
1846 1942 if ('yes' === $is_gbf) {
1847 1943 $start_condition = get_post_meta($funnel_id, 'global_funnel_start_condition', true);
1848 - $step_ids = array();
1849 1944 foreach ($steps as $step) {
1850 1945 if ($step['step_type'] == 'checkout') {
1851 1946 if (!empty($start_condition)) {
1852 1947 array_push($step_ids, $step['id']);
@@ -1863,11 +1958,14 @@
1863 1958 }
1864 1959 }
1865 1960 }
1866 1961 }
1962 + // The steps whose Global Funnel rules are set — the ones the canvas tags
1963 + // as having products. Fixed: the ids were collected and then dropped,
1964 + // so every Global Funnel card read "Add Product".
1867 1965 $response = array(
1868 - 'success' => false,
1869 - 'data' => []
1966 + 'success' => 'yes' === $is_gbf,
1967 + 'data' => array_map('absint', $step_ids),
1870 1968 );
1871 1969 return rest_ensure_response($response);
1872 1970 }
1873 1971