PluginProbe
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell / 3.10.6
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell v3.10.6
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 2.8.16 All 261 releases
wpfunnels / admin / modules / template-library / sources / base.php

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

671 lines 19.8 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 return [
151 'success' => true,
152 'redirectLink' => $redirect_link,
153 ];
154 }
155
156
157 /**
158 * Update funnel metadata with newly created steps
159 *
160 * @param $funnel_id
161 * @param $imported_steps
162 * @since 3.1.2
163 */
164 private function save_steps_meta_data( $funnel_id, $imported_steps ) {
165 if ( !$funnel_id || empty( $imported_steps ) ) {
166 return;
167 }
168 $steps = [];
169 $identifier = [];
170 foreach ( $imported_steps as $step_id ) {
171 $step_title = get_the_title( $step_id );
172 $step_type = get_post_meta( $step_id, '_step_type', true );
173 $identifier[] = $step_id;
174 $steps[] = array(
175 'id' => $step_id,
176 'step_type' => $step_type,
177 'name' => $step_title
178 );
179 }
180 $first_step_id = $this->get_first_step_id( $funnel_id, $steps );
181 update_post_meta( $funnel_id, '_steps_order', $steps );
182 update_post_meta( $funnel_id, '_steps', $steps );
183 update_post_meta( $funnel_id, '_first_step', $first_step_id );
184
185 $this->update_funnel_identifier( $funnel_id, $identifier );
186 }
187
188
189 /**
190 * Filter and reinitialize the drawflow data for freemium
191 * template. This reinitialization takes place if and only if
192 * free user import the freemium template.
193 *
194 * @param $steps_array
195 * @return array
196 * @since 3.1.2
197 */
198 private function filter_node_data( $steps_array ) {
199
200 if ( empty($steps_array) ) {
201 return $steps_array;
202 }
203 $filtered_array = [];
204 $thank_you_found = false;
205 foreach ($steps_array as $key => $step) {
206 // Skip downsell steps
207 if ( $step['data']['step_type'] === 'downsell') {
208 continue;
209 }
210
211 // If it's a thankyou step and hasn't been found before, add it
212 if ($step['data']['step_type'] === 'thankyou' && !$thank_you_found) {
213 $filtered_array[$key] = $step;
214 $thank_you_found = true;
215 }
216
217 // If it's not an downsell, or the first thankyou, add it
218 if ( ( $step['data']['step_type'] !== 'downsell' && !$thank_you_found) || in_array($step['data']['step_type'], array('landing', 'checkout', 'upsell' )) ) {
219 $filtered_array[$key] = $step;
220 }
221 }
222
223 // Find the checkout and thankyou steps
224 $checkout_id = null;
225 $thank_you_id = null;
226 $upsell_id = null;
227 $upsell_step_id = null;
228
229 foreach ($filtered_array as $key => $step) {
230 if ($step['data']['step_type'] === 'checkout') {
231 $checkout_id = $key;
232 } elseif ($step['data']['step_type'] === 'thankyou') {
233 $thank_you_id = $key;
234 } elseif ($step['data']['step_type'] === 'upsell') {
235 $upsell_id = $key;
236 $upsell_step_id = $step['data']['step_id'];
237 }
238 }
239
240 // Only rebuild connections if a checkout step exists.
241 // For lead funnels (landing → thankyou), there is no checkout step,
242 // so $checkout_id would be null. In that case, original connections
243 // are already correct since no downsell nodes were removed.
244 if ( $checkout_id !== null ) {
245 if( $upsell_id === null ) {
246 // Update inputs of thankyou and outputs of checkout
247 $filtered_array[$thank_you_id]['inputs'] = [
248 'input_1' => [
249 'connections' => [
250 [
251 'node' => $checkout_id,
252 'input' => 'output_1'
253 ]
254 ]
255 ]
256 ];
257
258 $filtered_array[$checkout_id]['outputs'] = [
259 'output_1' => [
260 'connections' => [
261 [
262 'node' => $thank_you_id,
263 'output' => 'input_1'
264 ]
265 ]
266 ]
267 ];
268 }else{
269 $filtered_array[$upsell_id]['inputs'] = [
270 'input_1' => [
271 'connections' => [
272 [
273 'node' => $checkout_id,
274 'input' => 'output_1'
275 ]
276 ]
277 ]
278 ];
279
280 $filtered_array[$upsell_id]['outputs'] = [
281 'output_1' => [
282 'connections' => [
283 [
284 'node' => $thank_you_id,
285 'output' => 'input_1'
286 ]
287 ]
288 ]
289 ];
290
291 $filtered_array[$checkout_id]['outputs'] = [
292 'output_1' => [
293 'connections' => [
294 [
295 'node' => $upsell_id,
296 'output' => 'input_1'
297 ]
298 ]
299 ]
300 ];
301
302 // Update inputs of thankyou and outputs of checkout
303 $filtered_array[$thank_you_id]['inputs'] = [
304 'input_1' => [
305 'connections' => [
306 [
307 'node' => $upsell_id,
308 'input' => 'output_1'
309 ]
310 ]
311 ]
312 ];
313
314 delete_post_meta( $upsell_step_id, '_wpfnl_maybe_enable_condition' );
315 }
316 }
317
318
319 return $filtered_array;
320 }
321
322
323 /**
324 * Get the first step id of the funnel
325 *
326 * @param $funnel_id
327 * @param $steps
328 * @return mixed|void|null
329 * @since 3.1.2
330 */
331 private function get_first_step_id( $funnel_id, $steps ) {
332 if ( !$funnel_id || empty( $steps ) ) {
333 return;
334 }
335 foreach ($steps as $step) {
336 if ( !isset($step['step_id']) ) {
337 continue;
338 }
339 if ($step['step_type'] === 'landing') {
340 return $step['step_id'];
341 } elseif ($step['step_type'] === 'checkout') {
342 return $step['step_id'];
343 }
344 }
345
346 // If neither landing nor checkout exists
347 return null;
348 }
349
350
351 /**
352 * Update the funnel identifier data
353 *
354 * @param $funnel_id
355 * @param $identifier
356 * @since 3.1.2
357 */
358 private function update_funnel_identifier( $funnel_id, $identifier ) {
359 if ( !$funnel_id || empty( $identifier ) ) {
360 return;
361 }
362 $identifier_json = get_post_meta($funnel_id, 'funnel_identifier', true);
363 $identifier_json = preg_replace('/\: *([0-9]+\.?[0-9e+\-]*)/', ':"\\1"', $identifier_json);
364 $identifier_data = json_decode( $identifier_json, true );
365
366 if (is_array($identifier_data)) {
367 foreach ($identifier_data as $key => $data) {
368 if ( !in_array( $data, $identifier ) ) {
369 unset($identifier_data[$key]);
370 }
371 }
372 $funnel_identifier_json = json_encode($identifier_data, JSON_UNESCAPED_SLASHES);
373 update_post_meta($funnel_id, 'funnel_identifier', $funnel_identifier_json);
374 }
375 }
376
377
378 /**
379 * Filter drawflow nodes for Store Checkout - only keep checkout and thankyou
380 *
381 * @param $steps_array
382 * @param $imported_steps
383 * @return array
384 * @since 3.5.0
385 */
386 private function filter_store_checkout_nodes( $steps_array, $imported_steps ) {
387 if ( empty($steps_array) ) {
388 return $steps_array;
389 }
390
391 $filtered_array = [];
392 $checkout_id = null;
393 $thankyou_id = null;
394
395 // Only keep checkout and thankyou nodes
396 // NOTE: Don't filter by step_id match because canvas has old template IDs, not new DB IDs
397 // Just filter by step_type - the step IDs will be updated by update_step_id_in_funnel_data_and_identifier
398 foreach ($steps_array as $key => $step) {
399 $step_type = isset($step['data']['step_type']) ? $step['data']['step_type'] : '';
400 $step_id = isset($step['data']['step_id']) ? $step['data']['step_id'] : 0;
401
402 // Keep only checkout and thankyou regardless of step_id
403 if (in_array($step_type, ['checkout', 'thankyou'])) {
404 $filtered_array[$key] = $step;
405
406 if ($step_type === 'checkout') {
407 $checkout_id = $key;
408 } elseif ($step_type === 'thankyou') {
409 $thankyou_id = $key;
410 }
411 }
412 }
413
414 // Update connections: checkout -> thankyou
415 if ($checkout_id !== null && $thankyou_id !== null) {
416 $filtered_array[$checkout_id]['outputs'] = [
417 'output_1' => [
418 'connections' => [
419 [
420 'node' => $thankyou_id,
421 'output' => 'input_1'
422 ]
423 ]
424 ]
425 ];
426
427 $filtered_array[$thankyou_id]['inputs'] = [
428 'input_1' => [
429 'connections' => [
430 [
431 'node' => $checkout_id,
432 'input' => 'output_1'
433 ]
434 ]
435 ]
436 ];
437 }
438
439 return $filtered_array;
440 }
441
442 /**
443 * Filter drawflow nodes for sales funnels to only include landing, checkout, and thankyou
444 * Remove upsell and downsell nodes
445 *
446 * @param array $steps_array
447 * @param array $imported_steps
448 * @return array
449 * @since 3.7.0
450 */
451 private function filter_sales_funnel_nodes( $steps_array, $imported_steps ) {
452 if ( empty($steps_array) ) {
453 return $steps_array;
454 }
455
456 $filtered_array = [];
457 $landing_id = null;
458 $checkout_id = null;
459 $thankyou_id = null;
460
461 // Only keep landing, checkout, and thankyou nodes
462 foreach ($steps_array as $key => $step) {
463 $step_type = isset($step['data']['step_type']) ? $step['data']['step_type'] : '';
464
465 // Keep only landing, checkout, and thankyou - exclude upsell and downsell
466 if (in_array($step_type, ['landing', 'checkout', 'thankyou'])) {
467 $filtered_array[$key] = $step;
468
469 if ($step_type === 'landing') {
470 $landing_id = $key;
471 } elseif ($step_type === 'checkout') {
472 $checkout_id = $key;
473 } elseif ($step_type === 'thankyou') {
474 $thankyou_id = $key;
475 }
476 }
477 }
478
479 // Update connections: landing -> checkout -> thankyou
480 if ($landing_id !== null && $checkout_id !== null && $thankyou_id !== null) {
481 // Landing page outputs to checkout
482 $filtered_array[$landing_id]['outputs'] = [
483 'output_1' => [
484 'connections' => [
485 [
486 'node' => $checkout_id,
487 'output' => 'input_1'
488 ]
489 ]
490 ]
491 ];
492
493 // Checkout receives from landing and outputs to thankyou
494 $filtered_array[$checkout_id]['inputs'] = [
495 'input_1' => [
496 'connections' => [
497 [
498 'node' => $landing_id,
499 'input' => 'output_1'
500 ]
501 ]
502 ]
503 ];
504 $filtered_array[$checkout_id]['outputs'] = [
505 'output_1' => [
506 'connections' => [
507 [
508 'node' => $thankyou_id,
509 'output' => 'input_1'
510 ]
511 ]
512 ]
513 ];
514
515 // Thankyou receives from checkout
516 $filtered_array[$thankyou_id]['inputs'] = [
517 'input_1' => [
518 'connections' => [
519 [
520 'node' => $checkout_id,
521 'input' => 'output_1'
522 ]
523 ]
524 ]
525 ];
526 }
527
528 return $filtered_array;
529 }
530
531
532 /**
533 * After funnel import
534 * redirect to new funnel edit url
535 *
536 * @param $payload
537 *
538 * @return array
539 * @since 1.0.0
540 */
541 public function after_step_creation($payload)
542 {
543 $redirect_link = add_query_arg(
544 [
545 'page' => WPFNL_EDIT_FUNNEL_SLUG,
546 'id' => $payload['funnelID'],
547 'step_id' => $payload['stepID'],
548 ],
549 admin_url('admin.php')
550 );
551 return [
552 'success' => true,
553 'redirectLink' => $redirect_link,
554 ];
555 }
556
557
558 /**
559 * Get steps order
560 *
561 * @param $funnel_flow_data
562 *
563 * @return array
564 *
565 * @since 2.2.6
566 */
567 private function get_steps_order( $funnel_flow_data ) {
568 $drawflow = $funnel_flow_data['drawflow'];
569 $nodes = array();
570 $step_order = array();
571 $first_node_id = '';
572 $start_node = array();
573
574
575 if( isset( $drawflow['Home']['data'] ) ) {
576 $drawflow_data = $drawflow['Home']['data'];
577
578 /**
579 * If has only one step, that only step will be the first step, no conditions should be checked.
580 * just return the step order
581 */
582 if( 1 === count( $drawflow_data ) ) {
583 $node_id = array_keys($drawflow_data)[0];
584 $data = $drawflow_data[$node_id];
585 $step_data = $data['data'];
586 $step_id = $step_data['step_id'];
587 $step_type = $step_data['step_type'];
588 $step_order[] = array(
589 'id' => $step_id,
590 'step_type' => $step_type,
591 'name' => sanitize_text_field( get_the_title( $step_id ) ),
592 );
593 return $step_order;
594
595 }
596
597 /**
598 * 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
599 * Step information includes output connection and input connection and it will be stored on $nodes
600 */
601 foreach ( $drawflow_data as $key => $data ) {
602 $step_data = $data['data'];
603 $step_type = $step_data['step_type'];
604 $step_id = $step_type !== 'conditional' ? $step_data['step_id'] : 0;
605 if(
606 (isset( $data['outputs']['output_1']['connections'] ) && count( $data['outputs']['output_1']['connections'] ) ) ||
607 (isset( $data['inputs']['input_1']['connections'] ) && count($data['inputs']['input_1']['connections']) )
608 ) {
609
610 if('conditional' === $step_type) {
611 continue;
612 }
613
614 /**
615 * A starting node is a node which has only output connection but not any input connection.
616 * 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.
617 * For other step types (checkout, offer, thankyou), we will check if the step has any output connection and no input connection.
618 */
619 if( 'landing' === $step_type ) {
620 if (
621 isset($data['outputs']['output_1']['connections']) && count($data['outputs']['output_1']['connections']) &&
622 (isset( $data['inputs'] ) && count($data['inputs']) == 0 )
623 ) {
624 $start_node = array(
625 'id' => $step_id,
626 'step_type' => $step_type,
627 'name' => sanitize_text_field(get_the_title($step_id)),
628 );
629 }
630 } else {
631 if (
632 isset($data['outputs']['output_1']['connections']) && count($data['outputs']['output_1']['connections']) &&
633 (isset($data['inputs']['input_1']['connections']) && count($data['inputs']['input_1']['connections']) === 0)
634 ) {
635 $start_node = array(
636 'id' => $step_id,
637 'step_type' => $step_type,
638 'name' => sanitize_text_field(get_the_title($step_id)),
639 );
640 } else {
641 $step_order[] = array(
642 'id' => $step_id,
643 'step_type' => $step_type,
644 'name' => sanitize_text_field(get_the_title($step_id)),
645 );
646 }
647 }
648 }
649 }
650 $step_order = $this->array_insert($step_order, $start_node, 0);
651 }
652 return $step_order;
653 }
654
655
656 /**
657 * Array insert element on position
658 *
659 * @param $original
660 * @param $inserted
661 * @param int $position
662 *
663 * @return mixed
664 */
665 private function array_insert(&$original, $inserted, $position) {
666 array_splice($original, $position, 0, array($inserted));
667 return $original;
668 }
669
670 }
671