PluginProbe
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell / trunk
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell vtrunk
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 2.8.16 2.8.17 2.8.18 All 259 releases
wpfunnels / admin / modules / template-library / sources / base.php

base.php in WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell trunk, at admin/modules/template-library/sources/base.php

675 lines 19.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Abstract class of source base
4 *
5 * @package
6 */
7 namespace WPFunnels\TemplateLibrary;
8
9 use WPFunnels\Wpfnl;
10 use WPFunnels\Wpfnl_functions;
11
12 abstract class Wpfnl_Source_Base
13 {
14 abstract public function get_source();
15
16 abstract public function get_funnels($arg = []);
17
18 abstract public function get_funnel($template_id);
19
20 abstract public function get_data(array $args);
21
22 abstract public function import_funnel($args = []);
23
24 abstract public function import_step($args = []);
25
26
27 /**
28 * After funnel import
29 * redirect to new funnel edit url
30 *
31 * @param $payload
32 *
33 * @return array
34 * @since 1.0.0
35 */
36 public function after_funnel_creation($payload)
37 {
38 $funnel = Wpfnl::$instance->funnel_store;
39 $funnel->set_id($payload['funnelID']);
40 $funnel->set_steps_order();
41
42 // rearrange steps order
43 $funnel_data = get_post_meta( $payload['funnelID'], 'funnel_data', true );
44 $funnel_data = $funnel_data ? $funnel_data : get_post_meta( $payload['funnelID'], '_funnel_data', true );
45
46 // Handle if funnel_data is JSON string
47 if (is_string($funnel_data)) {
48 $funnel_data = json_decode($funnel_data, true);
49 }
50
51 $imported_steps = isset($payload['importedSteps']) ? $payload['importedSteps'] : [];
52
53 // Check if this is a Store Checkout funnel - check both payload flag AND funnel meta
54 $is_store_checkout_flag = isset($payload['is_store_checkout']) && $payload['is_store_checkout'] == 'true';
55 $funnel_type = get_post_meta($payload['funnelID'], '_wpfnl_funnel_type', true);
56 $is_store_checkout = $is_store_checkout_flag && ($funnel_type === 'store_checkout');
57 // For Store Checkout, get all steps and filter to only checkout and thankyou
58 // This handles cases where frontend sends all steps or some steps were created before validation
59 if ($is_store_checkout) {
60 // Get all steps actually created for this funnel
61 $all_funnel_steps = get_posts([
62 'post_type' => WPFNL_STEPS_POST_TYPE,
63 'post_parent' => $payload['funnelID'],
64 'posts_per_page' => -1,
65 'post_status' => 'any',
66 'fields' => 'ids',
67 ]);
68
69
70 $filtered_steps = [];
71 foreach ($all_funnel_steps as $step_id) {
72 $step_type = get_post_meta($step_id, '_step_type', true);
73 if (in_array($step_type, ['checkout', 'thankyou'])) {
74 $filtered_steps[] = $step_id;
75 } else {
76 wp_delete_post($step_id, true);
77 }
78 }
79
80 // If query found no steps, use the imported_steps from payload and filter those
81 if (empty($all_funnel_steps) && !empty($imported_steps)) {
82 foreach ($imported_steps as $step_id) {
83 if ($step_id > 0) {
84 $step_type = get_post_meta($step_id, '_step_type', true);
85 if (in_array($step_type, ['checkout', 'thankyou'])) {
86 $filtered_steps[] = $step_id;
87 } else {
88 // Delete steps that shouldn't be in Store Checkout
89 wp_delete_post($step_id, true);
90 }
91 }
92 }
93 }
94
95 $imported_steps = $filtered_steps;
96 }
97
98 // update the funnel metadata with newly created step
99 $this->save_steps_meta_data( $payload['funnelID'], $imported_steps );
100
101 // Persist the store checkout step ID so the public-facing override can use it
102 if ( $is_store_checkout ) {
103 require_once WPFNL_DIR . 'includes/core/woocommerce/class-wpfnl-store-checkout-override.php';
104 \WPFunnels\WooCommerce\Wpfnl_Store_Checkout_Override::save_checkout_step_id( $payload['funnelID'] );
105 }
106
107 // Filter drawflow data for Store Checkout to remove non-checkout/thankyou nodes
108 if ($is_store_checkout) {
109
110 if ($funnel_data && is_array($funnel_data) && isset($funnel_data['drawflow']['Home']['data'])) {
111 $node_data = $funnel_data['drawflow']['Home']['data'];
112 $filtered_nodes = $this->filter_store_checkout_nodes($node_data, $imported_steps);
113 $funnel_data['drawflow']['Home']['data'] = $filtered_nodes;
114 update_post_meta($payload['funnelID'], 'funnel_data', $funnel_data);
115 update_post_meta($payload['funnelID'], '_funnel_data', $funnel_data);
116 }
117 }
118
119 // Filter drawflow data for Sales funnels to remove upsell/downsell nodes
120 $is_sales_funnel = isset($payload['goal']) && $payload['goal'] === 'sales';
121 if ($is_sales_funnel && !$is_store_checkout) {
122 if ($funnel_data && is_array($funnel_data) && isset($funnel_data['drawflow']['Home']['data'])) {
123 $node_data = $funnel_data['drawflow']['Home']['data'];
124 $filtered_nodes = $this->filter_sales_funnel_nodes($node_data, $imported_steps);
125 $funnel_data['drawflow']['Home']['data'] = $filtered_nodes;
126 update_post_meta($payload['funnelID'], 'funnel_data', $funnel_data);
127 update_post_meta($payload['funnelID'], '_funnel_data', $funnel_data);
128 }
129 }
130
131 // Reinitialize the funnel data if Pro is not active and template contains downsell
132 // This removes downsell nodes from the canvas for free users
133 $is_pro_active = apply_filters( 'wpfunnels/is_pro_license_activated', false );
134 if ( !$is_pro_active && !$is_store_checkout && $funnel_data && is_array($funnel_data) && isset($funnel_data['drawflow']['Home']['data']) ) {
135 $node_data = $funnel_data['drawflow']['Home']['data'];
136 $filter_data = $this->filter_node_data($node_data);
137 $funnel_data['drawflow']['Home']['data'] = $filter_data;
138 update_post_meta($payload['funnelID'],'funnel_data', $funnel_data);
139 update_post_meta($payload['funnelID'],'_funnel_data', $funnel_data);
140 }
141
142 $redirect_link = add_query_arg(
143 [
144 'page' => WPFNL_EDIT_FUNNEL_SLUG,
145 'id' => $payload['funnelID'],
146 'step_id' => $funnel->get_first_step_id(),
147 ],
148 admin_url('admin.php')
149 );
150
151 // Flush rewrite rules so newly created funnel/step permalinks resolve immediately.
152 flush_rewrite_rules();
153
154 return [
155 'success' => true,
156 'redirectLink' => $redirect_link,
157 ];
158 }
159
160
161 /**
162 * Update funnel metadata with newly created steps
163 *
164 * @param $funnel_id
165 * @param $imported_steps
166 * @since 3.1.2
167 */
168 private function save_steps_meta_data( $funnel_id, $imported_steps ) {
169 if ( !$funnel_id || empty( $imported_steps ) ) {
170 return;
171 }
172 $steps = [];
173 $identifier = [];
174 foreach ( $imported_steps as $step_id ) {
175 $step_title = get_the_title( $step_id );
176 $step_type = get_post_meta( $step_id, '_step_type', true );
177 $identifier[] = $step_id;
178 $steps[] = array(
179 'id' => $step_id,
180 'step_type' => $step_type,
181 'name' => $step_title
182 );
183 }
184 $first_step_id = $this->get_first_step_id( $funnel_id, $steps );
185 update_post_meta( $funnel_id, '_steps_order', $steps );
186 update_post_meta( $funnel_id, '_steps', $steps );
187 update_post_meta( $funnel_id, '_first_step', $first_step_id );
188
189 $this->update_funnel_identifier( $funnel_id, $identifier );
190 }
191
192
193 /**
194 * Filter and reinitialize the drawflow data for freemium
195 * template. This reinitialization takes place if and only if
196 * free user import the freemium template.
197 *
198 * @param $steps_array
199 * @return array
200 * @since 3.1.2
201 */
202 private function filter_node_data( $steps_array ) {
203
204 if ( empty($steps_array) ) {
205 return $steps_array;
206 }
207 $filtered_array = [];
208 $thank_you_found = false;
209 foreach ($steps_array as $key => $step) {
210 // Skip downsell steps
211 if ( $step['data']['step_type'] === 'downsell') {
212 continue;
213 }
214
215 // If it's a thankyou step and hasn't been found before, add it
216 if ($step['data']['step_type'] === 'thankyou' && !$thank_you_found) {
217 $filtered_array[$key] = $step;
218 $thank_you_found = true;
219 }
220
221 // If it's not an downsell, or the first thankyou, add it
222 if ( ( $step['data']['step_type'] !== 'downsell' && !$thank_you_found) || in_array($step['data']['step_type'], array('landing', 'checkout', 'upsell' )) ) {
223 $filtered_array[$key] = $step;
224 }
225 }
226
227 // Find the checkout and thankyou steps
228 $checkout_id = null;
229 $thank_you_id = null;
230 $upsell_id = null;
231 $upsell_step_id = null;
232
233 foreach ($filtered_array as $key => $step) {
234 if ($step['data']['step_type'] === 'checkout') {
235 $checkout_id = $key;
236 } elseif ($step['data']['step_type'] === 'thankyou') {
237 $thank_you_id = $key;
238 } elseif ($step['data']['step_type'] === 'upsell') {
239 $upsell_id = $key;
240 $upsell_step_id = $step['data']['step_id'];
241 }
242 }
243
244 // Only rebuild connections if a checkout step exists.
245 // For lead funnels (landing → thankyou), there is no checkout step,
246 // so $checkout_id would be null. In that case, original connections
247 // are already correct since no downsell nodes were removed.
248 if ( $checkout_id !== null ) {
249 if( $upsell_id === null ) {
250 // Update inputs of thankyou and outputs of checkout
251 $filtered_array[$thank_you_id]['inputs'] = [
252 'input_1' => [
253 'connections' => [
254 [
255 'node' => $checkout_id,
256 'input' => 'output_1'
257 ]
258 ]
259 ]
260 ];
261
262 $filtered_array[$checkout_id]['outputs'] = [
263 'output_1' => [
264 'connections' => [
265 [
266 'node' => $thank_you_id,
267 'output' => 'input_1'
268 ]
269 ]
270 ]
271 ];
272 }else{
273 $filtered_array[$upsell_id]['inputs'] = [
274 'input_1' => [
275 'connections' => [
276 [
277 'node' => $checkout_id,
278 'input' => 'output_1'
279 ]
280 ]
281 ]
282 ];
283
284 $filtered_array[$upsell_id]['outputs'] = [
285 'output_1' => [
286 'connections' => [
287 [
288 'node' => $thank_you_id,
289 'output' => 'input_1'
290 ]
291 ]
292 ]
293 ];
294
295 $filtered_array[$checkout_id]['outputs'] = [
296 'output_1' => [
297 'connections' => [
298 [
299 'node' => $upsell_id,
300 'output' => 'input_1'
301 ]
302 ]
303 ]
304 ];
305
306 // Update inputs of thankyou and outputs of checkout
307 $filtered_array[$thank_you_id]['inputs'] = [
308 'input_1' => [
309 'connections' => [
310 [
311 'node' => $upsell_id,
312 'input' => 'output_1'
313 ]
314 ]
315 ]
316 ];
317
318 delete_post_meta( $upsell_step_id, '_wpfnl_maybe_enable_condition' );
319 }
320 }
321
322
323 return $filtered_array;
324 }
325
326
327 /**
328 * Get the first step id of the funnel
329 *
330 * @param $funnel_id
331 * @param $steps
332 * @return mixed|void|null
333 * @since 3.1.2
334 */
335 private function get_first_step_id( $funnel_id, $steps ) {
336 if ( !$funnel_id || empty( $steps ) ) {
337 return;
338 }
339 foreach ($steps as $step) {
340 if ( !isset($step['step_id']) ) {
341 continue;
342 }
343 if ($step['step_type'] === 'landing') {
344 return $step['step_id'];
345 } elseif ($step['step_type'] === 'checkout') {
346 return $step['step_id'];
347 }
348 }
349
350 // If neither landing nor checkout exists
351 return null;
352 }
353
354
355 /**
356 * Update the funnel identifier data
357 *
358 * @param $funnel_id
359 * @param $identifier
360 * @since 3.1.2
361 */
362 private function update_funnel_identifier( $funnel_id, $identifier ) {
363 if ( !$funnel_id || empty( $identifier ) ) {
364 return;
365 }
366 $identifier_json = get_post_meta($funnel_id, 'funnel_identifier', true);
367 $identifier_json = preg_replace('/\: *([0-9]+\.?[0-9e+\-]*)/', ':"\\1"', $identifier_json);
368 $identifier_data = json_decode( $identifier_json, true );
369
370 if (is_array($identifier_data)) {
371 foreach ($identifier_data as $key => $data) {
372 if ( !in_array( $data, $identifier ) ) {
373 unset($identifier_data[$key]);
374 }
375 }
376 $funnel_identifier_json = json_encode($identifier_data, JSON_UNESCAPED_SLASHES);
377 update_post_meta($funnel_id, 'funnel_identifier', $funnel_identifier_json);
378 }
379 }
380
381
382 /**
383 * Filter drawflow nodes for Store Checkout - only keep checkout and thankyou
384 *
385 * @param $steps_array
386 * @param $imported_steps
387 * @return array
388 * @since 3.5.0
389 */
390 private function filter_store_checkout_nodes( $steps_array, $imported_steps ) {
391 if ( empty($steps_array) ) {
392 return $steps_array;
393 }
394
395 $filtered_array = [];
396 $checkout_id = null;
397 $thankyou_id = null;
398
399 // Only keep checkout and thankyou nodes
400 // NOTE: Don't filter by step_id match because canvas has old template IDs, not new DB IDs
401 // Just filter by step_type - the step IDs will be updated by update_step_id_in_funnel_data_and_identifier
402 foreach ($steps_array as $key => $step) {
403 $step_type = isset($step['data']['step_type']) ? $step['data']['step_type'] : '';
404 $step_id = isset($step['data']['step_id']) ? $step['data']['step_id'] : 0;
405
406 // Keep only checkout and thankyou regardless of step_id
407 if (in_array($step_type, ['checkout', 'thankyou'])) {
408 $filtered_array[$key] = $step;
409
410 if ($step_type === 'checkout') {
411 $checkout_id = $key;
412 } elseif ($step_type === 'thankyou') {
413 $thankyou_id = $key;
414 }
415 }
416 }
417
418 // Update connections: checkout -> thankyou
419 if ($checkout_id !== null && $thankyou_id !== null) {
420 $filtered_array[$checkout_id]['outputs'] = [
421 'output_1' => [
422 'connections' => [
423 [
424 'node' => $thankyou_id,
425 'output' => 'input_1'
426 ]
427 ]
428 ]
429 ];
430
431 $filtered_array[$thankyou_id]['inputs'] = [
432 'input_1' => [
433 'connections' => [
434 [
435 'node' => $checkout_id,
436 'input' => 'output_1'
437 ]
438 ]
439 ]
440 ];
441 }
442
443 return $filtered_array;
444 }
445
446 /**
447 * Filter drawflow nodes for sales funnels to only include landing, checkout, and thankyou
448 * Remove upsell and downsell nodes
449 *
450 * @param array $steps_array
451 * @param array $imported_steps
452 * @return array
453 * @since 3.7.0
454 */
455 private function filter_sales_funnel_nodes( $steps_array, $imported_steps ) {
456 if ( empty($steps_array) ) {
457 return $steps_array;
458 }
459
460 $filtered_array = [];
461 $landing_id = null;
462 $checkout_id = null;
463 $thankyou_id = null;
464
465 // Only keep landing, checkout, and thankyou nodes
466 foreach ($steps_array as $key => $step) {
467 $step_type = isset($step['data']['step_type']) ? $step['data']['step_type'] : '';
468
469 // Keep only landing, checkout, and thankyou - exclude upsell and downsell
470 if (in_array($step_type, ['landing', 'checkout', 'thankyou'])) {
471 $filtered_array[$key] = $step;
472
473 if ($step_type === 'landing') {
474 $landing_id = $key;
475 } elseif ($step_type === 'checkout') {
476 $checkout_id = $key;
477 } elseif ($step_type === 'thankyou') {
478 $thankyou_id = $key;
479 }
480 }
481 }
482
483 // Update connections: landing -> checkout -> thankyou
484 if ($landing_id !== null && $checkout_id !== null && $thankyou_id !== null) {
485 // Landing page outputs to checkout
486 $filtered_array[$landing_id]['outputs'] = [
487 'output_1' => [
488 'connections' => [
489 [
490 'node' => $checkout_id,
491 'output' => 'input_1'
492 ]
493 ]
494 ]
495 ];
496
497 // Checkout receives from landing and outputs to thankyou
498 $filtered_array[$checkout_id]['inputs'] = [
499 'input_1' => [
500 'connections' => [
501 [
502 'node' => $landing_id,
503 'input' => 'output_1'
504 ]
505 ]
506 ]
507 ];
508 $filtered_array[$checkout_id]['outputs'] = [
509 'output_1' => [
510 'connections' => [
511 [
512 'node' => $thankyou_id,
513 'output' => 'input_1'
514 ]
515 ]
516 ]
517 ];
518
519 // Thankyou receives from checkout
520 $filtered_array[$thankyou_id]['inputs'] = [
521 'input_1' => [
522 'connections' => [
523 [
524 'node' => $checkout_id,
525 'input' => 'output_1'
526 ]
527 ]
528 ]
529 ];
530 }
531
532 return $filtered_array;
533 }
534
535
536 /**
537 * After funnel import
538 * redirect to new funnel edit url
539 *
540 * @param $payload
541 *
542 * @return array
543 * @since 1.0.0
544 */
545 public function after_step_creation($payload)
546 {
547 $redirect_link = add_query_arg(
548 [
549 'page' => WPFNL_EDIT_FUNNEL_SLUG,
550 'id' => $payload['funnelID'],
551 'step_id' => $payload['stepID'],
552 ],
553 admin_url('admin.php')
554 );
555 return [
556 'success' => true,
557 'redirectLink' => $redirect_link,
558 ];
559 }
560
561
562 /**
563 * Get steps order
564 *
565 * @param $funnel_flow_data
566 *
567 * @return array
568 *
569 * @since 2.2.6
570 */
571 private function get_steps_order( $funnel_flow_data ) {
572 $drawflow = $funnel_flow_data['drawflow'];
573 $nodes = array();
574 $step_order = array();
575 $first_node_id = '';
576 $start_node = array();
577
578
579 if( isset( $drawflow['Home']['data'] ) ) {
580 $drawflow_data = $drawflow['Home']['data'];
581
582 /**
583 * If has only one step, that only step will be the first step, no conditions should be checked.
584 * just return the step order
585 */
586 if( 1 === count( $drawflow_data ) ) {
587 $node_id = array_keys($drawflow_data)[0];
588 $data = $drawflow_data[$node_id];
589 $step_data = $data['data'];
590 $step_id = $step_data['step_id'];
591 $step_type = $step_data['step_type'];
592 $step_order[] = array(
593 'id' => $step_id,
594 'step_type' => $step_type,
595 'name' => sanitize_text_field( get_the_title( $step_id ) ),
596 );
597 return $step_order;
598
599 }
600
601 /**
602 * First we will find the first node (the node which has only output connection but no input connection will be considered as first node) and the list of nodes array which has the
603 * Step information includes output connection and input connection and it will be stored on $nodes
604 */
605 foreach ( $drawflow_data as $key => $data ) {
606 $step_data = $data['data'];
607 $step_type = $step_data['step_type'];
608 $step_id = $step_type !== 'conditional' ? $step_data['step_id'] : 0;
609 if(
610 (isset( $data['outputs']['output_1']['connections'] ) && count( $data['outputs']['output_1']['connections'] ) ) ||
611 (isset( $data['inputs']['input_1']['connections'] ) && count($data['inputs']['input_1']['connections']) )
612 ) {
613
614 if('conditional' === $step_type) {
615 continue;
616 }
617
618 /**
619 * A starting node is a node which has only output connection but not any input connection.
620 * If the step is landing, then there should not be any input connection for this step. so we will only consider the output connection for landing only.
621 * For other step types (checkout, offer, thankyou), we will check if the step has any output connection and no input connection.
622 */
623 if( 'landing' === $step_type ) {
624 if (
625 isset($data['outputs']['output_1']['connections']) && count($data['outputs']['output_1']['connections']) &&
626 (isset( $data['inputs'] ) && count($data['inputs']) == 0 )
627 ) {
628 $start_node = array(
629 'id' => $step_id,
630 'step_type' => $step_type,
631 'name' => sanitize_text_field(get_the_title($step_id)),
632 );
633 }
634 } else {
635 if (
636 isset($data['outputs']['output_1']['connections']) && count($data['outputs']['output_1']['connections']) &&
637 (isset($data['inputs']['input_1']['connections']) && count($data['inputs']['input_1']['connections']) === 0)
638 ) {
639 $start_node = array(
640 'id' => $step_id,
641 'step_type' => $step_type,
642 'name' => sanitize_text_field(get_the_title($step_id)),
643 );
644 } else {
645 $step_order[] = array(
646 'id' => $step_id,
647 'step_type' => $step_type,
648 'name' => sanitize_text_field(get_the_title($step_id)),
649 );
650 }
651 }
652 }
653 }
654 $step_order = $this->array_insert($step_order, $start_node, 0);
655 }
656 return $step_order;
657 }
658
659
660 /**
661 * Array insert element on position
662 *
663 * @param $original
664 * @param $inserted
665 * @param int $position
666 *
667 * @return mixed
668 */
669 private function array_insert(&$original, $inserted, $position) {
670 array_splice($original, $position, 0, array($inserted));
671 return $original;
672 }
673
674 }
675