PluginProbe
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell / 3.13.1
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell v3.13.1
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 / includes / core / rest-api / Controllers / class-funnel-controller.php

class-funnel-controller.php in WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell 3.13.1, at includes/core/rest-api/Controllers/class-funnel-controller.php

2,204 lines 67.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Funnel controller
4 *
5 * @package WPFunnels\Rest\Controllers
6 */
7
8 namespace WPFunnels\Rest\Controllers;
9
10 use Error;
11 use WP_Error;
12 use WP_REST_Request;
13 use WP_REST_Response;
14 use WPFunnels\Wpfnl_functions;
15 use Wpfnl_Controller_Type_Factory;
16 use WPFunnels\Modules\Admin\Funnel\Module;
17 use WPFunnels\Wpfnl;
18 use WPFunnels\Migration\Migration;
19
20 class FunnelController extends Wpfnl_REST_Controller
21 {
22
23 /**
24 * Endpoint namespace.
25 *
26 * @var string
27 */
28 protected $namespace = 'wpfunnels/v1';
29
30 /**
31 * Route base.
32 *
33 * @var string
34 */
35 protected $rest_base = 'funnel-control';
36
37 /**
38 * Check if user has valid permission
39 *
40 * @param $request
41 *
42 * @return bool|WP_Error
43 * @since 1.0.0
44 */
45 public function update_items_permissions_check($request)
46 {
47 if (!Wpfnl_functions::wpfnl_rest_check_manager_permissions('steps', 'edit')) {
48 return new WP_Error('wpfunnels_rest_cannot_edit', __('Sorry, you cannot edit this resource.', 'wpfnl'), ['status' => rest_authorization_required_code()]);
49 }
50 return true;
51 }
52
53 /**
54 * Makes sure the current user has access to READ the settings APIs.
55 *
56 * @param WP_REST_Request $request Full data about the request.
57 *
58 * @return WP_Error|boolean
59 * @since 3.0.0
60 */
61 public function get_items_permissions_check($request)
62 {
63 if (!Wpfnl_functions::wpfnl_rest_check_manager_permissions('settings')) {
64 return new WP_Error('wpfunnels_rest_cannot_view', __('Sorry, you cannot list resources.', 'wpfnl'), ['status' => rest_authorization_required_code()]);
65 }
66 return true;
67 }
68
69
70 /**
71 * Register rest routes
72 *
73 * @since 1.0.0
74 */
75 public function register_routes()
76 {
77 register_rest_route($this->namespace, '/' . $this->rest_base . '/saveFunnel/', [
78 [
79 'methods' => \WP_REST_Server::EDITABLE,
80 'callback' => [
81 $this,
82 'save_funnel_data'
83 ],
84 'permission_callback' => [
85 $this,
86 'update_items_permissions_check'
87 ],
88 ],
89 ]);
90
91 register_rest_route($this->namespace, '/' . $this->rest_base . '/getThankyouData/', [
92 [
93 'methods' => \WP_REST_Server::READABLE,
94 'callback' => [
95 $this,
96 'get_thankyou_data'
97 ],
98 'permission_callback' => [
99 $this,
100 'update_items_permissions_check'
101 ],
102 ],
103 ]);
104
105 register_rest_route($this->namespace, '/' . $this->rest_base . '/saveConditionalNode/', [
106 [
107 'methods' => \WP_REST_Server::EDITABLE,
108 'callback' => [
109 $this,
110 'save_conditional_node'
111 ],
112 'permission_callback' => [
113 $this,
114 'update_items_permissions_check'
115 ],
116 ],
117 ]);
118 register_rest_route($this->namespace, '/' . $this->rest_base . '/getConditionalNode/', [
119 [
120 'methods' => \WP_REST_Server::READABLE,
121 'callback' => [
122 $this,
123 'get_conditional_node'
124 ],
125 'permission_callback' => [
126 $this,
127 'get_items_permissions_check'
128 ],
129 ],
130 ]);
131 register_rest_route($this->namespace, '/' . $this->rest_base . '/getFunnel/', [
132 [
133 'methods' => \WP_REST_Server::READABLE,
134 'callback' => [
135 $this,
136 'get_funnel_data'
137 ],
138 'permission_callback' => [
139 $this,
140 'update_items_permissions_check'
141 ],
142 ],
143 ]);
144
145 register_rest_route($this->namespace, '/' . $this->rest_base . '/getFunnelStats/', [
146 [
147 'methods' => \WP_REST_Server::READABLE,
148 'callback' => [
149 $this,
150 'get_funnel_stats'
151 ],
152 'permission_callback' => [
153 $this,
154 'update_items_permissions_check'
155 ],
156 ],
157 ]);
158
159 register_rest_route($this->namespace, '/' . $this->rest_base . '/getStepType/', [
160 [
161 'methods' => \WP_REST_Server::READABLE,
162 'callback' => [
163 $this,
164 'get_step_type'
165 ],
166 'permission_callback' => [
167 $this,
168 'update_items_permissions_check'
169 ],
170 ],
171 ]);
172
173 register_rest_route($this->namespace, '/' . $this->rest_base . '/getFunnelInfo/', [
174 [
175 'methods' => \WP_REST_Server::READABLE,
176 'callback' => [
177 $this,
178 'get_funnel_info'
179 ],
180 'permission_callback' => [
181 $this,
182 'update_items_permissions_check'
183 ],
184 ],
185 ]);
186
187
188 register_rest_route($this->namespace, '/' . $this->rest_base . '/exportFunnel/', [
189 [
190 'methods' => \WP_REST_Server::READABLE,
191 'callback' => [
192 $this,
193 'export_funnel'
194 ],
195 'permission_callback' => [
196 $this,
197 'update_items_permissions_check'
198 ],
199 ],
200 ]);
201
202 register_rest_route($this->namespace, '/' . $this->rest_base . '/getallfunnels/', array(
203 array(
204 'methods' => \WP_REST_Server::READABLE,
205 'callback' => [
206 $this,
207 'get_all_funnels'
208 ],
209 'permission_callback' => [
210 $this,
211 'update_items_permissions_check'
212 ],
213 ),
214 ));
215
216 register_rest_route($this->namespace, '/' . $this->rest_base . '/getStepGeneralInfo/', [
217 [
218 'methods' => \WP_REST_Server::READABLE,
219 'callback' => [
220 $this,
221 'get_step_general_info'
222 ],
223 'permission_callback' => [
224 $this,
225 'update_items_permissions_check'
226 ],
227 ],
228 ]);
229
230 register_rest_route($this->namespace, '/' . $this->rest_base . '/steps/', array(
231 'args' => array(
232 'funnel_id' => array(
233 'description' => __('Funnel ID.', 'wpfnl'),
234 'type' => 'string',
235 ),
236 'step_id' => array(
237 'description' => __('Step ID.', 'wpfnl'),
238 'type' => 'string',
239 )
240 ),
241 array(
242 'methods' => \WP_REST_Server::EDITABLE,
243 'callback' => [
244 $this,
245 'update_step_meta'
246 ],
247 'permission_callback' => [
248 $this,
249 'update_items_permissions_check'
250 ],
251 )
252 ));
253
254
255 register_rest_route($this->namespace, '/' . $this->rest_base . '/get_gbf_data/(?P<funnel_id>\d+)', [
256 [
257 'methods' => \WP_REST_Server::READABLE,
258 'callback' => [
259 $this,
260 'get_GBF_data'
261 ],
262 'permission_callback' => [
263 $this,
264 'update_items_permissions_check'
265 ],
266 ],
267 ]);
268
269 register_rest_route($this->namespace, '/' . $this->rest_base . '/get-optimizations/(?P<step_id>[\d]+)', [
270 [
271 'methods' => \WP_REST_Server::READABLE,
272 'callback' => [
273 $this,
274 'get_optimizations'
275 ],
276 'permission_callback' => [
277 $this,
278 'update_items_permissions_check'
279 ],
280 ],
281 ]);
282
283 register_rest_route($this->namespace, '/' . $this->rest_base . '/save-optimizations/', [
284 [
285 'methods' => \WP_REST_Server::EDITABLE,
286 'callback' => [
287 $this,
288 'save_optimizations'
289 ],
290 'permission_callback' => [
291 $this,
292 'update_items_permissions_check'
293 ],
294 ],
295 ]);
296
297 register_rest_route($this->namespace, '/' . $this->rest_base . '/get-settings/(?P<step_id>[\d]+)', [
298 [
299 'methods' => \WP_REST_Server::READABLE,
300 'callback' => [
301 $this,
302 'get_ab_settings'
303 ],
304 'permission_callback' => [
305 $this,
306 'update_items_permissions_check'
307 ],
308 ],
309 ]);
310
311 register_rest_route(
312 $this->namespace,
313 '/' . $this->rest_base . '/(?P<funnel_id>\d+)/funnel-name-change',
314 array(
315 array(
316 'methods' => \WP_REST_Server::EDITABLE,
317 'callback' => array(
318 $this,
319 'funnel_name_change',
320 ),
321 'permission_callback' => array(
322 $this,
323 'update_items_permissions_check',
324 ),
325 ),
326 )
327 );
328
329 register_rest_route(
330 $this->namespace,
331 '/' . $this->rest_base . '/wpfnl-get-funnel-settings',
332 array(
333 array(
334 'methods' => \WP_REST_Server::EDITABLE,
335 'callback' => array(
336 $this,
337 'wpfnl_get_funnel_settings',
338 ),
339 'args' => array(
340 'funnel_id' => array(
341 'required' => true,
342 'type' => array('integer', 'string'),
343 'sanitize_callback' => 'absint',
344 ),
345 ),
346 'permission_callback' => array(
347 $this,
348 'update_items_permissions_check',
349 )
350 ),
351 )
352 );
353
354 register_rest_route(
355 $this->namespace,
356 '/' . $this->rest_base . '/bulk-delete-funnel',
357 array(
358 array(
359 'methods' => \WP_REST_Server::EDITABLE,
360 'callback' => array(
361 $this,
362 'delete_marked_funnels',
363 ),
364 'permission_callback' => array(
365 $this,
366 'update_items_permissions_check',
367 ),
368 ),
369 )
370 );
371
372 register_rest_route(
373 $this->namespace,
374 '/' . $this->rest_base . '/bulk-restore-funnel',
375 array(
376 array(
377 'methods' => \WP_REST_Server::EDITABLE,
378 'callback' => array(
379 $this,
380 'restore_marked_funnels',
381 ),
382 'permission_callback' => array(
383 $this,
384 'update_items_permissions_check',
385 ),
386 ),
387 )
388 );
389
390 register_rest_route(
391 $this->namespace,
392 '/' . $this->rest_base . '/bulk-trash-funnel',
393 array(
394 array(
395 'methods' => \WP_REST_Server::EDITABLE,
396 'callback' => array(
397 $this,
398 'trash_marked_funnels',
399 ),
400 'permission_callback' => array(
401 $this,
402 'update_items_permissions_check',
403 ),
404 ),
405 )
406 );
407
408 register_rest_route(
409 $this->namespace,
410 '/' . $this->rest_base . '/(?P<funnel_id>\d+)/convert-to-store-checkout',
411 array(
412 array(
413 'methods' => \WP_REST_Server::EDITABLE,
414 'callback' => array( $this, 'convert_to_store_checkout' ),
415 'permission_callback' => array( $this, 'update_items_permissions_check' ),
416 'args' => array(
417 'funnel_id' => array(
418 'required' => true,
419 'validate_callback' => function ( $param ) {
420 return is_numeric( $param );
421 },
422 ),
423 ),
424 ),
425 )
426 );
427
428 }
429
430 /**
431 * Check if funnel data exists or not
432 **/
433 public function get_all_funnels()
434 {
435 $args = array(
436 'post_type' => 'wpfunnel_steps',
437 'numberposts' => -1
438 );
439 $funnels = get_posts($args);
440
441 if ($funnels) {
442 if (count($funnels) > 0) {
443 return false;
444 }
445 }
446
447 return true;
448 }
449
450
451 /**
452 * Save thankyou data
453 */
454 public function get_thankyou_data($request)
455 {
456 $step_id = $request['step_id'];
457 $data = array();
458 $data['_wpfnl_thankyou_order_overview'] = get_post_meta($step_id, '_wpfnl_thankyou_order_overview', true) ? get_post_meta($step_id, '_wpfnl_thankyou_order_overview', true) : 'on';
459 $data['_wpfnl_thankyou_order_details'] = get_post_meta($step_id, '_wpfnl_thankyou_order_details', true) ? get_post_meta($step_id, '_wpfnl_thankyou_order_details', true) : 'on';
460 $data['_wpfnl_thankyou_billing_details'] = get_post_meta($step_id, '_wpfnl_thankyou_billing_details', true) ? get_post_meta($step_id, '_wpfnl_thankyou_billing_details', true) : 'on';
461 $data['_wpfnl_thankyou_shipping_details'] = get_post_meta($step_id, '_wpfnl_thankyou_shipping_details', true) ? get_post_meta($step_id, '_wpfnl_thankyou_shipping_details', true) : 'on';
462 $data['_wpfnl_thankyou_is_custom_redirect'] = get_post_meta($step_id, '_wpfnl_thankyou_is_custom_redirect', true) ? get_post_meta($step_id, '_wpfnl_thankyou_is_custom_redirect', true) : 'off';
463 $data['_wpfnl_thankyou_is_direct_redirect'] = get_post_meta($step_id, '_wpfnl_thankyou_is_direct_redirect', true) ? get_post_meta($step_id, '_wpfnl_thankyou_is_direct_redirect', true) : 'off';
464 $data['_wpfnl_thankyou_set_time'] = get_post_meta($step_id, '_wpfnl_thankyou_set_time', true) ? get_post_meta($step_id, '_wpfnl_thankyou_set_time', true) : '';
465 $data['_wpfnl_thankyou_custom_redirect_url'] = get_post_meta($step_id, '_wpfnl_thankyou_custom_redirect_url', true) ? get_post_meta($step_id, '_wpfnl_thankyou_custom_redirect_url', true) : '';
466 return $data;
467 }
468
469 /**
470 * Get conditional node
471 *
472 * @param WP_REST_Request $request request.
473 *
474 * @return array|WP_Error
475 */
476 public function get_conditional_node($request)
477 {
478 $funnel_id = $request['funnel_id'];
479
480 $steps = Wpfnl_functions::get_steps($funnel_id);
481 $optin_step = [];
482 if (is_array($steps)) {
483 foreach ($steps as $step) {
484 if ('landing' === $step['step_type'] || 'custom' === $step['step_type']) {
485 $optin_step[] = [
486 'name' => __('Optin form (', 'wpfnl') . $step['name'] . ')',
487 'value' => __('optin_', 'wpfnl') . $step['id'],
488 ];
489 }
490 }
491 }
492 $response = array(
493 'status' => 'error',
494 );
495 if (!empty($optin_step)) {
496 $response['status'] = 'success';
497 $response['optinStep'] = $optin_step;
498 }
499
500 return $this->prepare_item_for_response($response, $request);
501 }
502
503 /**
504 * Save conditional node
505 *
506 * @param string $request request.
507 *
508 * @return array|WP_Error
509 */
510 public function save_conditional_node($request)
511 {
512 $funnel_id = $request['funnel_id'];
513 $condition_data = $request['condition_data'];
514 $node_identifier = $request['node_identifier'];
515 update_post_meta($funnel_id, $node_identifier, $condition_data);
516 $response = array(
517 'status' => true,
518 );
519 return $this->prepare_item_for_response($response, $request);
520 }
521
522 /**
523 * Get step_type.
524 *
525 * @param string $request request.
526 *
527 * @return array|WP_Error
528 */
529 public function get_step_type($request)
530 {
531 $step_type = '';
532 $step_id = $request['step_id'];
533 $step_type = get_post_meta($step_id, '_step_type', true);
534 return $step_type;
535 }
536
537
538 /**
539 * Retrieve checkout optimization settings for a funnel step.
540 *
541 * Reads post meta for the given step and returns optimization settings
542 * including express checkout, field validation, product images display,
543 * collapsible order summary, and enhanced phone field configuration.
544 * Falls back to default values when no meta is stored.
545 *
546 * @param WP_REST_Request $request Request object. Expects 'step_id' parameter.
547 * @return WP_REST_Response REST response containing the optimization settings.
548 */
549 public function get_optimizations($request) {
550 $step_id = $request['step_id'];
551 $express_checkout_enabled = get_post_meta($step_id, '_wpfnl_express_checkout_enabled', true);
552 $express_checkout_position = get_post_meta($step_id, '_wpfnl_express_checkout_position', true);
553 $field_validation_enabled = get_post_meta($step_id, '_wpfnl_field_validation_enabled', true);
554 $field_validation_message = get_post_meta($step_id, '_wpfnl_field_validation_message', true);
555 $display_product_images = get_post_meta($step_id, '_wpfnl_display_product_images', true);
556 $collapsible_order_summary_enabled = get_post_meta($step_id, '_wpfnl_collapsible_order_summary_enabled', true);
557 $enhanced_phone_field_enabled = get_post_meta($step_id, '_wpfnl_enhanced_phone_field_enabled', true);
558 $validate_phone_number = get_post_meta($step_id, '_wpfnl_validate_phone_number', true);
559 $save_phone_number_format = get_post_meta($step_id, '_wpfnl_save_phone_number_format', true);
560 $phone_help_text = get_post_meta($step_id, '_wpfnl_phone_help_text', true);
561
562 $testimonial_raw = get_post_meta($step_id, 'wpf_checkout_testimonial', true);
563 $testimonial_default = array(
564 'enabled' => false,
565 'layout' => 'layout-1',
566 'position' => 'after_bump',
567 'testimonial' => array( 'text' => '', 'author' => '', 'rating' => 5 ),
568 'guarantee' => array( 'headline' => '', 'text' => '', 'days' => 30, 'image' => array( 'id' => 0, 'url' => '' ) ),
569 'benefits' => array( 'title' => "Here's what you get", 'items' => array() ),
570 );
571 if ( is_array( $testimonial_raw ) ) {
572 $testimonial_config = wp_parse_args( $testimonial_raw, $testimonial_default );
573 // Deep-merge guarantee sub-array so new fields always exist
574 if ( ! isset( $testimonial_config['guarantee'] ) || ! is_array( $testimonial_config['guarantee'] ) ) {
575 $testimonial_config['guarantee'] = $testimonial_default['guarantee'];
576 } else {
577 $testimonial_config['guarantee'] = wp_parse_args( $testimonial_config['guarantee'], $testimonial_default['guarantee'] );
578 }
579 } else {
580 $testimonial_config = $testimonial_default;
581 }
582
583 $response = array(
584 'express_checkout_enabled' => $express_checkout_enabled ? $express_checkout_enabled : 'yes',
585 'express_checkout_position' => $express_checkout_position ? $express_checkout_position : 'top',
586 'field_validation_enabled' => $field_validation_enabled ? $field_validation_enabled : 'no',
587 'field_validation_message' => $field_validation_message ? $field_validation_message : '{field} is required',
588 'display_product_images' => $display_product_images ? $display_product_images : 'no',
589 'collapsible_order_summary_enabled' => $collapsible_order_summary_enabled ? $collapsible_order_summary_enabled : 'yes',
590 'enhanced_phone_field_enabled' => $enhanced_phone_field_enabled ? $enhanced_phone_field_enabled : 'no',
591 'validate_phone_number' => $validate_phone_number ? $validate_phone_number : 'no',
592 'save_phone_number_format' => $save_phone_number_format ? $save_phone_number_format : 'without_country_code',
593 'phone_help_text' => $phone_help_text ? $phone_help_text : '',
594 'testimonial' => $testimonial_config,
595 );
596 return $this->prepare_item_for_response($response, $request);
597 }
598
599 public function save_optimizations($request) {
600 $step_id = $request['step_id'];
601 $express_checkout_enabled = sanitize_text_field($request['express_checkout_enabled']);
602 $express_checkout_position = sanitize_text_field($request['express_checkout_position']);
603 update_post_meta($step_id, '_wpfnl_express_checkout_enabled', $express_checkout_enabled);
604 if ( $express_checkout_position ) {
605 update_post_meta($step_id, '_wpfnl_express_checkout_position', $express_checkout_position);
606 }
607
608 $field_validation_enabled = isset($request['field_validation_enabled']) ? sanitize_text_field($request['field_validation_enabled']) : 'no';
609 $field_validation_message = isset($request['field_validation_message']) ? sanitize_text_field($request['field_validation_message']) : '{field} is required';
610 update_post_meta($step_id, '_wpfnl_field_validation_enabled', $field_validation_enabled);
611 update_post_meta($step_id, '_wpfnl_field_validation_message', $field_validation_message);
612
613 $display_product_images = isset($request['display_product_images']) ? sanitize_text_field($request['display_product_images']) : 'no';
614 update_post_meta($step_id, '_wpfnl_display_product_images', $display_product_images);
615
616 $collapsible_order_summary_enabled = isset($request['collapsible_order_summary_enabled']) ? sanitize_text_field($request['collapsible_order_summary_enabled']) : 'yes';
617 update_post_meta($step_id, '_wpfnl_collapsible_order_summary_enabled', $collapsible_order_summary_enabled);
618
619 $enhanced_phone_field_enabled = isset($request['enhanced_phone_field_enabled']) ? sanitize_text_field($request['enhanced_phone_field_enabled']) : 'no';
620 $validate_phone_number = isset($request['validate_phone_number']) ? sanitize_text_field($request['validate_phone_number']) : 'no';
621 $save_phone_number_format = isset($request['save_phone_number_format']) ? sanitize_text_field($request['save_phone_number_format']) : 'without_country_code';
622 $phone_help_text = isset($request['phone_help_text']) ? sanitize_text_field($request['phone_help_text']) : '';
623
624 update_post_meta($step_id, '_wpfnl_enhanced_phone_field_enabled', $enhanced_phone_field_enabled);
625 update_post_meta($step_id, '_wpfnl_validate_phone_number', $validate_phone_number);
626 update_post_meta($step_id, '_wpfnl_save_phone_number_format', $save_phone_number_format);
627 update_post_meta($step_id, '_wpfnl_phone_help_text', $phone_help_text);
628
629 // Testimonial section
630 if ( isset( $request['testimonial'] ) && is_array( $request['testimonial'] ) ) {
631 $raw = $request['testimonial'];
632
633 $testimonial_data = array(
634 'enabled' => ! empty( $raw['enabled'] ),
635 'layout' => sanitize_key( isset( $raw['layout'] ) ? $raw['layout'] : 'layout-1' ),
636 'position' => sanitize_key( isset( $raw['position'] ) ? $raw['position'] : 'after_bump' ),
637 'testimonial' => array(
638 'text' => isset( $raw['testimonial']['text'] ) ? sanitize_textarea_field( $raw['testimonial']['text'] ) : '',
639 'author' => isset( $raw['testimonial']['author'] ) ? sanitize_text_field( $raw['testimonial']['author'] ) : '',
640 'rating' => isset( $raw['testimonial']['rating'] ) ? max( 1, min( 5, absint( $raw['testimonial']['rating'] ) ) ) : 5,
641 ),
642 'guarantee' => array(
643 'headline' => isset( $raw['guarantee']['headline'] ) ? sanitize_text_field( $raw['guarantee']['headline'] ) : '',
644 'text' => isset( $raw['guarantee']['text'] ) ? sanitize_textarea_field( $raw['guarantee']['text'] ) : '',
645 'days' => isset( $raw['guarantee']['days'] ) ? max( 1, absint( $raw['guarantee']['days'] ) ) : 30,
646 'image' => array(
647 'id' => isset( $raw['guarantee']['image']['id'] ) ? absint( $raw['guarantee']['image']['id'] ) : 0,
648 'url' => isset( $raw['guarantee']['image']['url'] ) ? esc_url_raw( $raw['guarantee']['image']['url'] ) : '',
649 ),
650 ),
651 'benefits' => array(
652 'title' => isset( $raw['benefits']['title'] ) ? sanitize_text_field( $raw['benefits']['title'] ) : '',
653 'items' => array(),
654 ),
655 );
656
657 if ( isset( $raw['benefits']['items'] ) && is_array( $raw['benefits']['items'] ) ) {
658 foreach ( $raw['benefits']['items'] as $item ) {
659 $testimonial_data['benefits']['items'][] = sanitize_text_field( $item );
660 }
661 }
662
663 update_post_meta( $step_id, 'wpf_checkout_testimonial', $testimonial_data );
664 }
665
666 $response = array(
667 'success' => true,
668 'message' => __('Saved Successfully', 'wpfnl')
669 );
670 return $this->prepare_item_for_response($response, $request);
671 }
672
673 /**
674 * Get step_type.
675 *
676 * @param string $request request.
677 *
678 * @return array|WP_Error
679 */
680 public function get_step_general_info($request)
681 {
682 $step_type = '';
683 $step_id = isset($request['step_id']) ? $request['step_id'] : null;
684 $step_type = get_post_meta($step_id, '_step_type', true);
685 $custom_script = get_post_meta($step_id, '_wpfnl_custom_script', true);
686 $custom_css = get_post_meta($step_id, '_wpfnl_custom_css', true);
687
688 $response = array(
689 'step_type' => $step_type,
690 'step_title' => get_the_title($step_id),
691 'step_view_link' => get_post_permalink($step_id),
692 'custom_script' => html_entity_decode($custom_script),
693 'custom_css' => html_entity_decode($custom_css)
694 );
695 return $this->prepare_item_for_response($response, $request);
696 }
697
698
699 /**
700 * Get the funnel title and link
701 *
702 * @param $request
703 *
704 * @return \WP_REST_Response
705 */
706 public function get_funnel_info($request)
707 {
708 $funnel_id = $request['funnel_id'];
709 $title = html_entity_decode(get_the_title($funnel_id));
710 $steps = get_post_meta($funnel_id, '_steps_order', true);
711 $response['success'] = false;
712 if ($steps) {
713 if (isset($steps[0]) && $steps[0]['id']) {
714 $response['link'] = get_post_permalink($steps[0]['id']);
715 $response['title'] = $title;
716 $utm_params = $this->get_utm_params($funnel_id);
717 if ($utm_params != '') {
718 $response['link'] = $response['link'] . $utm_params;
719 $response['title'] = $title;
720 }
721 $response['success'] = true;
722 }
723 }
724 return $this->prepare_item_for_response($response, $request);
725 }
726
727 /**
728 * Retrieves the funnel data based on the provided request.
729 *
730 * @param mixed $request The request data.
731 * @return WP_Rest_Response The prepared funnel data response.
732 *
733 * @since 2.7.17
734 */
735 public function get_funnel_data($request)
736 {
737 $funnel_id = isset($request['funnel_id']) ? $request['funnel_id'] : null;
738 $response = [
739 'status' => 'error',
740 'success' => false
741 ];
742
743 if (!$funnel_id) {
744 return $this->prepare_item_for_response($response, $request);
745 }
746
747 if( version_compare(WPFNL_VERSION, '3.0.0', '>=') && 'yes' !== get_post_meta( $funnel_id, 'wpfnls_is_newui_migrated', true ) ){
748 $migration = new Migration();
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 $migration->update_funnel_data($funnel_id, $funnel_data);
751 update_post_meta($funnel_id, 'wpfnls_is_newui_migrated', 'yes');
752 }
753 $response = $this->prepare_funnel_data_response($funnel_id);
754 return $this->prepare_item_for_response($response, $request);
755 }
756
757
758 /**
759 * Retrieves visit/conversion figures for a funnel's steps plus funnel totals.
760 *
761 * Deliberately kept out of get_funnel_data() so that opening a funnel on the
762 * canvas never blocks on analytics. On sites with large amounts of tracked
763 * traffic these aggregates can be slow, so the canvas renders its structure
764 * first and fills the numbers in once this responds.
765 *
766 * @param mixed $request The request data.
767 * @return \WP_REST_Response
768 *
769 * @since 3.12.12
770 */
771 public function get_funnel_stats($request)
772 {
773 $funnel_id = isset($request['funnel_id']) ? absint($request['funnel_id']) : 0;
774
775 $response = [
776 'success' => false,
777 'steps' => (object) [],
778 'funnel_analytics' => [
779 'total_orders' => 0,
780 'total_revenue' => 0,
781 'aov' => 0,
782 ],
783 ];
784
785 if (!$funnel_id) {
786 return $this->prepare_item_for_response($response, $request);
787 }
788
789 $cache_key = 'wpfnl_funnel_stats_' . $funnel_id;
790 $cached = get_transient($cache_key);
791
792 if (false !== $cached) {
793 return $this->prepare_item_for_response($cached, $request);
794 }
795
796 $steps = get_post_meta($funnel_id, '_steps', true);
797 $steps = is_array($steps) ? $steps : [];
798 $step_ids = array_map(
799 function ($step) {
800 return isset($step['id']) ? $step['id'] : null;
801 },
802 $steps
803 );
804
805 $response['success'] = true;
806 $response['steps'] = self::get_steps_analytics_map($step_ids);
807 $response['funnel_analytics'] = self::get_funnel_analytics($funnel_id);
808
809 // Cached so repeated canvas opens don't recompute the aggregates. Kept
810 // short enough that the numbers still track recent activity.
811 set_transient($cache_key, $response, 10 * MINUTE_IN_SECONDS);
812
813 return $this->prepare_item_for_response($response, $request);
814 }
815
816
817 /**
818 * Prepares the response data for the funnel data based on the provided parameters.
819 *
820 * @param int $funnel_id The ID of the funnel.
821 * @param mixed $request The request data.
822 * @return array The prepared response data.
823 *
824 * @since 2.7.17
825 */
826
827 /**
828 * Restore node_identifier precision from the html field.
829 *
830 * PHP json_encode truncates floats, so node_identifier stored as a number loses digits.
831 * The html field is a string (e.g. "addstep311.7949003047232") and preserves full precision.
832 * This method extracts node_identifier from html so the API response always matches what JS stored.
833 *
834 * @param array $funnel_data
835 * @return array
836 */
837 private function fix_node_identifier_precision($funnel_data)
838 {
839 if (!isset($funnel_data['drawflow']['Home']['data']) || !is_array($funnel_data['drawflow']['Home']['data'])) {
840 return $funnel_data;
841 }
842 foreach ($funnel_data['drawflow']['Home']['data'] as &$node) {
843 if (!isset($node['html'], $node['data']['node_identifier'])) {
844 continue;
845 }
846 $html = $node['html'];
847 if (strpos($html, 'addstep') === 0) {
848 $node['data']['node_identifier'] = substr($html, 7);
849 } elseif (strpos($html, 'conditional') === 0) {
850 $node['data']['node_identifier'] = substr($html, 11);
851 }
852 }
853 unset($node);
854 return $funnel_data;
855 }
856
857 private function prepare_funnel_data_response($funnel_id)
858 {
859 $response = [
860 'status' => 'error',
861 'success' => false
862 ];
863
864 $is_new_ui_data = get_post_meta($funnel_id, 'wpfnls_is_newui_migrated', true);
865 $funnel_data = 'yes' === $is_new_ui_data ? get_post_meta($funnel_id, '_funnel_data', true) : get_post_meta($funnel_id, 'funnel_data', true);
866 $funnel_identifier = get_post_meta($funnel_id, 'funnel_identifier', true);
867 $_steps_order = get_post_meta($funnel_id, '_steps', true);
868 $_steps_order = is_array($_steps_order) ? $_steps_order : [];
869 $status = get_post_status($funnel_id);
870 $title = html_entity_decode(get_the_title($funnel_id));
871 $first_step_id = Wpfnl_functions::get_first_step($funnel_id);
872
873 // Notice: We recently updated the architecture of our funnel template showcase site to version 3.1.3.
874 // If user's site's template data isn't synchronized with the latest data from our showcase site,
875 // user may encounter issues importing templates. Unfortunately, this situation is unavoidable.
876 // To ensure a smooth experience, we'll display a notice if user's site's template data needs updating.
877 $is_imported_funnel = get_post_meta($funnel_id, '_is_imported', true);
878 $show_template_sync_notice = false;
879 if ('yes' === $is_new_ui_data && 'yes' === $is_imported_funnel && !$funnel_data) {
880 $show_template_sync_notice = true;
881 }
882
883 /**
884 * Fires to modify funnel view link for A/B testing
885 *
886 * @param string $link
887 * @param int $step_id
888 * @param int $funnel_id
889 *
890 * @return string $link
891 * @since 1.7.8
892 */
893 $link = apply_filters('wpfunnels/modify_funnel_view_link', get_post_permalink($first_step_id), $first_step_id, $funnel_id);
894 $response['title'] = $title;
895
896 $utm_settings = Wpfnl_functions::get_funnel_utm_settings($funnel_id);
897 $view_link = !empty($response['link']) ? $response['link'] : $link;
898
899 if ($utm_settings['utm_enable'] == 'on') {
900 unset($utm_settings['utm_enable']);
901 $view_link = add_query_arg($utm_settings, $view_link);
902 $view_link = strtolower($view_link);
903 $utm_settings['utm_enable'] = 'on';
904 }
905
906 $response['success'] = true;
907 $step_order_data = self::get_steps_order_data($_steps_order);
908 $response['steps_order'] = isset( $step_order_data['steps_order'] ) ? $step_order_data['steps_order'] : [];
909 $response['is_ob'] = isset( $step_order_data['is_ob'] ) ? $step_order_data['is_ob'] : false;
910 $response['ob_steps'] = isset( $step_order_data['ob_steps'] ) ? $step_order_data['ob_steps'] : [];
911
912 $init_position = [
913 'pos_x' => 383,
914 'pos_y' => 143
915 ];
916
917 $funnel_data = Wpfnl_functions::remove_disconnected_addstep_node($funnel_data, $funnel_id);
918 $funnel_data = $this->fix_node_identifier_precision($funnel_data);
919
920 if ($funnel_data) {
921 $response = [
922 'status' => 'success',
923 'funnel_data' => $funnel_data,
924 'funnel_identifier' => $funnel_identifier,
925 'steps_order' => isset ($response['steps_order']) ? $response['steps_order'] : [],
926 'funnel_status' => $status,
927 'title' => $title,
928 'link' => $view_link,
929 'reset_funnel' => get_post_meta($funnel_id, '_wpfnl_is_reset', true),
930 'is_ob' => isset ($response['is_ob']) ? $response['is_ob'] : false,
931 'ob_steps' => isset ($response['ob_steps']) ? $response['ob_steps'] : []
932 ];
933
934 if (empty($funnel_data['drawflow']['Home']['data'])) {
935 $data = $this->get_default_add_step_node_data($init_position);
936 $response['status'] = 'scratch-funnel';
937 $response['funnel_data'] = $data['data'];
938 $response['node_identifier'] = $data['node_identifier'];
939 }
940
941 $response['funnel_data'] = $this->update_step_id_in_funnel_data_and_identifier($response['funnel_data'], $utm_settings);
942
943 /**
944 * Fires to update funnel data response in case of A/B Testing
945 *
946 * @param array $response
947 *
948 * @return array $response
949 **@since 1.7.8
950 */
951 $response = apply_filters('wpfunnels/update_funnel_data_response', $response);
952 if (!empty($response['ab_data'])) {
953 $response['ab_data'] = $this->update_step_view_link_with_utm_params($response['ab_data'], $utm_settings);
954 }
955 } elseif ($title && $status) {
956
957 $data = $this->get_default_add_step_node_data($init_position);
958 $response = array(
959 'status' => 'scratch-funnel',
960 'title' => $title,
961 'funnel_status' => $status,
962 'funnel_data' => $data['data'],
963 'reset_funnel' => get_post_meta($funnel_id, '_wpfnl_is_reset', true),
964 'node_identifier' => $data['node_identifier'],
965 'show_template_sync_notice' => $show_template_sync_notice,
966 );
967 }
968
969
970 $response['first_node_id'] = $this->get_first_step_node($first_step_id, $response['funnel_data']);
971 $response['conditional_data'] = $this->get_conditional_step($response['funnel_data']);
972
973 // Visit/conversion figures and funnel totals are intentionally absent
974 // here — the canvas fetches them separately via getFunnelStats so that
975 // opening a funnel never waits on analytics. See get_funnel_stats().
976 return $response;
977 }
978
979
980 /**
981 * Update funnel identifier
982 *
983 * @param $remote_step
984 * @param $new_step
985 * @param $args
986 *
987 * @return void
988 */
989 public function update_step_id_in_funnel_data_and_identifier($funnel_flow_array, $utm_settings)
990 {
991
992 if (!isset($funnel_flow_array['drawflow']['Home']['data']) || !is_array($funnel_flow_array['drawflow']['Home']['data'])) {
993 return $funnel_flow_array;
994 }
995 $isUtm = false;
996 if (isset($utm_settings['utm_enable']) && $utm_settings['utm_enable'] == 'on') {
997 $isUtm = true;
998 unset($utm_settings['utm_enable']);
999 }
1000
1001 // Loop through each step in funnel
1002 foreach ($funnel_flow_array['drawflow']['Home']['data'] as &$step) {
1003
1004 // Find corresponding step in step id data
1005 if ('conditional' !== $step['data']['step_type'] && 'addstep' !== $step['data']['step_type'] ) {
1006
1007 $step_view_link = get_post_permalink($step['data']['step_id']);
1008 $step_view_link = $isUtm ? add_query_arg($utm_settings, $step_view_link) : $step_view_link;
1009 // Check if step id is not in used_step_types array
1010 $post_edit_link = base64_encode(get_edit_post_link($step['data']['step_id']));
1011 $post_view_link = base64_encode($step_view_link);
1012
1013 $step['data']['step_edit_link'] = $post_edit_link;
1014 $step['data']['step_view_link'] = $post_view_link;
1015 }
1016 }
1017 return $funnel_flow_array;
1018 }
1019
1020
1021 /**
1022 * get conditional steps with validity checking
1023 *
1024 * @param $funnel_flow_array
1025 *
1026 * @return array
1027 *
1028 * @since 3.4.13
1029 */
1030 public function get_conditional_step($funnel_flow_array){
1031 if (!isset($funnel_flow_array['drawflow']['Home']['data']) || !is_array($funnel_flow_array['drawflow']['Home']['data'])) {
1032 return [];
1033 }
1034
1035 $condition_data = [];
1036 foreach ($funnel_flow_array['drawflow']['Home']['data'] as &$step) {
1037
1038 // Find corresponding step in step id data
1039 if ('conditional' !== $step['data']['step_type'] && 'addstep' !== $step['data']['step_type'] ) {
1040 $step_id = $step['data']['step_id'];
1041 $is_condition_enabled = get_post_meta($step_id, '_wpfnl_maybe_enable_condition', true);
1042 if( 'yes' === $is_condition_enabled ){
1043 $data = [
1044 'step_id' => $step_id,
1045 'data' => 'no'
1046 ];
1047 $conditions = get_post_meta($step_id, '_wpfnl_step_conditions', true);
1048 if( is_array($conditions) && !empty($conditions) ){
1049 $data['data'] = 'yes';
1050 }
1051 array_push($condition_data, $data);
1052 }
1053 }
1054 }
1055 return $condition_data;
1056 }
1057
1058 /**
1059 * Updates A/B test variations' step view links with UTM parameters.
1060 *
1061 * This method takes A/B test data, specifically variations, and appends UTM parameters
1062 * to the step view links of each variation. It does so based on the provided UTM settings
1063 * if UTM tracking is enabled.
1064 *
1065 * @param array $ab_data An array containing A/B test data with variations.
1066 * @param array $utm_settings An array of UTM parameters and their values.
1067 *
1068 * @return array The modified A/B test data with UTM parameters appended to step view links.
1069 * @since 2.8.14
1070 */
1071 private function update_step_view_link_with_utm_params($ab_data, $utm_settings)
1072 {
1073 if (!empty($ab_data) && !empty($utm_settings['utm_enable']) && 'on' === $utm_settings['utm_enable']) {
1074 unset($utm_settings['utm_enable']);
1075 foreach ($ab_data as $ab_key => $data) {
1076 $variations = $data['data']['start_settings']['variations'] ?? [];
1077 if (!empty($variations)) {
1078 foreach ($variations as $var_key => $variation) {
1079 if (!empty($variation['step_view_link'])) {
1080 $ab_data[$ab_key]['data']['start_settings']['variations'][$var_key]['step_view_link'] = add_query_arg($utm_settings, $variation['step_view_link']);
1081 }
1082 }
1083 }
1084 }
1085 }
1086 return $ab_data;
1087 }
1088
1089 /**
1090 * Get the first step Node
1091 */
1092 public function get_first_step_node($first_step_id, $funnel_data)
1093 {
1094 $data = isset($funnel_data['drawflow']['Home']['data']) ? $funnel_data['drawflow']['Home']['data'] : [];
1095 foreach ($data as $step_data) {
1096 if (isset($step_data['data']['step_id']) && $step_data['data']['step_id'] == $first_step_id) {
1097 return $step_data['id'];
1098 }
1099 }
1100 }
1101
1102
1103 /**
1104 * Get default add step node data and identifire for canvas
1105 *
1106 * @return Array Returns an array containing the default add step node data and identifier
1107 * @since 2.8.0
1108 *
1109 */
1110 public function get_default_add_step_node_data($init_position)
1111 {
1112 $node_identifier = rand() * (500 - 100) + 100;
1113 $data = [
1114 'drawflow' => [
1115 'Home' => [
1116 'data' => [
1117 1 => [
1118 'id' => 1,
1119 'name' => 'addstep',
1120 'data' => [
1121 'step_type' => 'addstep',
1122 'node_identifier' => $node_identifier
1123 ],
1124 'class' => 'addstep',
1125 'html' => 'addstep' . $node_identifier,
1126 'typenode' => 'vue',
1127 'inputs' => [
1128 'input_1' => [
1129 'connections' => []
1130 ]
1131 ],
1132 'outputs' => [],
1133 'pos_x' => isset($init_position['pos_x']) ? $init_position['pos_x'] : 383,
1134 'pos_y' => isset($init_position['pos_y']) ? $init_position['pos_y'] : 143,
1135 ]
1136 ]
1137 ]
1138 ]
1139 ];
1140 return [
1141 'data' => $data,
1142 'node_identifier' => $node_identifier,
1143 ];
1144 }
1145
1146 /**
1147 * Get formatted steps order data.
1148 *
1149 * Formats the steps order data by populating additional fields and determining if any step is an order bump.
1150 *
1151 * @param array $steps_order The array of funnel steps order.
1152 * @return array Formatted steps order data with additional fields and order bump flag.
1153 *
1154 * @since 2.7.17
1155 */
1156 private static function get_steps_order_data($steps_order)
1157 {
1158 $is_order_bump = false;
1159 $ob_steps = [];
1160 $formatted_steps_order = [];
1161
1162 foreach ($steps_order as $step) {
1163 $step_id = isset($step['id']) ? $step['id'] : null;
1164 $_temp_step = $step;
1165
1166 // Null rather than 0 so the canvas can tell "not loaded yet" apart
1167 // from a genuine zero and show a placeholder until getFunnelStats
1168 // responds.
1169 $_temp_step['visit'] = null;
1170 $_temp_step['conversion'] = null;
1171 $_temp_step['name'] = get_the_title($step_id);
1172 $_step_type = get_post_meta($step_id, '_step_type', true);
1173 $should_assign_product = in_array($_step_type, ['checkout', 'upsell', 'downsell']) && !get_post_meta($step_id, '_wpfnl_' . $_step_type . '_products', true);
1174
1175 if ('checkout' === $_step_type) {
1176 $step_has_ob = self::is_order_bump($step_id);
1177 if ($step_has_ob) {
1178 $is_order_bump = true;
1179 $ob_steps[] = (int) $step_id;
1180 }
1181 }
1182
1183 $_temp_step['should_assign_product'] = $should_assign_product;
1184
1185 /**
1186 * Fires to add Mail Mint automation data
1187 *
1188 * @param array $step
1189 * @param int $step_id
1190 * @since 2.7.0
1191 */
1192 $formatted_steps_order[] = apply_filters('wpfunnels/step_data', $_temp_step, $step_id);
1193 }
1194
1195 return [
1196 'steps_order' => $formatted_steps_order,
1197 'is_ob' => $is_order_bump,
1198 'ob_steps' => $ob_steps,
1199 ];
1200 }
1201
1202 /**
1203 * Get visit and conversion analytics data for a set of steps in one batch.
1204 *
1205 * Replaces N per-step queries (previously run once per step in
1206 * get_steps_order_data()) with two GROUP BY queries covering the whole
1207 * funnel, avoiding an N+1 query pattern that caused the funnel canvas to
1208 * time out on funnels with several steps. Count semantics are unchanged
1209 * from the previous per-step queries — grouping by step_id and running
1210 * the same WHERE/JOIN conditions together is equivalent to running them
1211 * individually per step.
1212 *
1213 * @param array $step_ids The step IDs to fetch analytics for.
1214 * @return array Map of step_id => ['visit' => int, 'conversion' => int].
1215 *
1216 * @since 3.12.12
1217 */
1218 private static function get_steps_analytics_map($step_ids)
1219 {
1220 $step_ids = array_values(array_unique(array_filter($step_ids)));
1221
1222 $map = [];
1223 foreach ($step_ids as $step_id) {
1224 $map[$step_id] = array(
1225 'visit' => 0,
1226 'conversion' => 0,
1227 );
1228 }
1229
1230 if (empty($step_ids)) {
1231 return $map;
1232 }
1233
1234 global $wpdb;
1235 $analytics_table = $wpdb->prefix . 'wpfnl_analytics';
1236 $analytics_meta_table = $wpdb->prefix . 'wpfnl_analytics_meta';
1237
1238 // Check if the analytics table exists before querying.
1239 $table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $analytics_table));
1240 if (!$table_exists) {
1241 return $map;
1242 }
1243
1244 $cache_key = 'wpfnl_pro_steps_analytics_' . md5(implode(',', $step_ids));
1245 $cached_map = wp_cache_get($cache_key, 'wpfnl_pro');
1246
1247 if (false !== $cached_map) {
1248 return $cached_map;
1249 }
1250
1251 $placeholders = implode(', ', array_fill(0, count($step_ids), '%s'));
1252
1253 // Get total visits, grouped per step.
1254 $visits_query = $wpdb->prepare(
1255 "SELECT wpft1.step_id, COUNT( DISTINCT( wpft1.id ) ) AS total_visits
1256 FROM {$analytics_table} as wpft1
1257 WHERE wpft1.step_id IN ({$placeholders})
1258 GROUP BY wpft1.step_id
1259 ORDER BY NULL",
1260 $step_ids
1261 ); // phpcs:ignore
1262 $visits_rows = $wpdb->get_results($visits_query); // phpcs:ignore
1263
1264 foreach ($visits_rows as $row) {
1265 if (isset($map[$row->step_id])) {
1266 $map[$row->step_id]['visit'] = $row->total_visits;
1267 }
1268 }
1269
1270 // Get conversions, grouped per step.
1271 //
1272 // Read the meta table directly on its denormalised step_id rather than
1273 // joining back through the analytics table. The join form had to walk
1274 // every visit for the funnel (tens of thousands of rows) and look up
1275 // each one's meta rows, which dominated the whole request on sites
1276 // with real traffic volume. Filtering on step_id touches only the
1277 // conversion rows themselves — a few thousand instead of ~150k.
1278 //
1279 // Counting meta rows keeps the original numbers intact, including the
1280 // pre-existing behaviour where a visit carrying two 'conversion'='yes'
1281 // meta rows counts twice.
1282 $conversion_query = $wpdb->prepare(
1283 "SELECT step_id, COUNT(*) AS conversions
1284 FROM {$analytics_meta_table}
1285 WHERE step_id IN ({$placeholders})
1286 AND meta_key = 'conversion'
1287 AND meta_value = 'yes'
1288 GROUP BY step_id
1289 ORDER BY NULL",
1290 $step_ids
1291 ); // phpcs:ignore
1292 $conversion_rows = $wpdb->get_results($conversion_query); // phpcs:ignore
1293
1294 foreach ($conversion_rows as $row) {
1295 if (isset($map[$row->step_id])) {
1296 $map[$row->step_id]['conversion'] = $row->conversions;
1297 }
1298 }
1299
1300 wp_cache_set($cache_key, $map, 'wpfnl_pro', 3600);
1301
1302 return $map;
1303 }
1304
1305 /**
1306 * Get funnel-level analytics data (total orders, revenue, AOV, orderbump revenue).
1307 *
1308 * Queries the wpfnl_stats table for completed orders belonging to a specific funnel.
1309 *
1310 * @param int $funnel_id The funnel ID.
1311 * @return array Analytics data with total_orders, total_revenue, aov, and orderbump_revenue.
1312 *
1313 * @since 3.5.0
1314 */
1315 private static function get_funnel_analytics($funnel_id)
1316 {
1317 $analytics = array(
1318 'total_orders' => 0,
1319 'total_revenue' => 0,
1320 'aov' => 0,
1321 );
1322
1323 if (!$funnel_id) {
1324 return $analytics;
1325 }
1326
1327 global $wpdb;
1328 $stats_table = $wpdb->prefix . 'wpfnl_stats';
1329
1330 // Check if the stats table exists before querying.
1331 $table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $stats_table));
1332 if (!$table_exists) {
1333 return $analytics;
1334 }
1335
1336 $cache_key = 'wpfnl_funnel_analytics_' . $funnel_id;
1337 $cached_data = wp_cache_get($cache_key, 'wpfnl_pro');
1338
1339 if (false !== $cached_data) {
1340 return $cached_data;
1341 }
1342
1343 $row = $wpdb->get_row($wpdb->prepare(
1344 "SELECT
1345 COUNT(DISTINCT id) AS total_orders,
1346 COALESCE(SUM(total_sales), 0) AS total_revenue
1347 FROM {$stats_table}
1348 WHERE funnel_id = %d AND status IN (%s, %s)",
1349 $funnel_id,
1350 'completed',
1351 'processing'
1352 )); // phpcs:ignore
1353
1354 if ($row) {
1355 $analytics['total_orders'] = (int) $row->total_orders;
1356 $analytics['total_revenue'] = round((float) $row->total_revenue, 2);
1357 $analytics['aov'] = $analytics['total_orders'] > 0
1358 ? round($analytics['total_revenue'] / $analytics['total_orders'], 2)
1359 : 0;
1360 }
1361
1362 wp_cache_set($cache_key, $analytics, 'wpfnl_pro', 3600);
1363
1364 return $analytics;
1365 }
1366
1367 /**
1368 * Checks if a step in the funnel is an order bump.
1369 * Retrieves the order bump settings for the specified step and determines if it is an order bump.
1370 * @param array $steps_order The array of funnel steps.
1371 * @param int $step_id The ID of the step to check.
1372 *
1373 *
1374 * @return bool True if the step is an order bump, false otherwise.
1375 * @since 2.7.17
1376 *
1377 */
1378 public static function is_order_bump($step_id)
1379 {
1380 $all_settings = get_post_meta($step_id, 'order-bump-settings', true) ?: [];
1381 $is_multiple = Wpfnl_functions::check_array_is_multidimentional($all_settings);
1382
1383 if (!$is_multiple && $all_settings) {
1384 $all_settings['name'] = 'Order bump';
1385 $all_settings = Wpfnl_functions::migrate_order_bump($all_settings, $step_id);
1386 }
1387
1388 if (is_array($all_settings) && count($all_settings) > 0) {
1389 $funnel_id = get_post_meta($step_id, '_funnel_id', true);
1390 $type = get_post_meta($funnel_id, '_wpfnl_funnel_type', true) ?: 'wc';
1391 $class_object = Wpfnl_Controller_Type_Factory::build($type);
1392 if ($class_object) {
1393 $all_settings = $class_object->get_ob_settings($all_settings);
1394 }
1395 }
1396
1397
1398 if (count($all_settings)) {
1399 return true;
1400 }
1401 return false;
1402 }
1403
1404 /**
1405 * Get formatted funnel data
1406 *
1407 * @param $drawflow
1408 *
1409 * @return mixed
1410 *
1411 * @since 2.0.5
1412 */
1413 private function get_formatted_funnel_data($drawflow)
1414 {
1415 if (isset($drawflow['drawflow']['Home']['data'])) {
1416 $drawflow_data = $drawflow['drawflow']['Home']['data'];
1417 foreach ($drawflow_data as $key => $data) {
1418 $step_data = $data['data'];
1419 $step_type = $step_data['step_type'];
1420 if ('conditional' !== $step_type) {
1421 $step_id = $step_data['step_id'];
1422 $funnel_id = Wpfnl_functions::get_funnel_id_from_step($step_id);
1423 if ('conditional' !== $step_type) {
1424 $edit_post_link = get_edit_post_link($step_id);
1425 $view_link = get_the_permalink($step_id);
1426 $utm_params = $this->get_utm_params($funnel_id);
1427 if ($utm_params != '') {
1428 $view_link = $view_link . $utm_params;
1429 }
1430 $title = get_the_title($step_id);
1431 $drawflow['drawflow']['Home']['data'][$key]['data'] = array(
1432 'step_edit_link' => base64_encode($edit_post_link),
1433 'step_id' => $step_id,
1434 'step_type' => $step_data['step_type'],
1435 'step_view_link' => base64_encode(rtrim($view_link, '/')),
1436 'step_name' => $title,
1437 );
1438 }
1439 }
1440 }
1441 }
1442 return $drawflow;
1443 }
1444
1445 /**
1446 * Save funnel data.
1447 *
1448 * @param string $request request.
1449 *
1450 * @return array|WP_Error
1451 */
1452 public function save_funnel_data($request)
1453 {
1454 $funnel_id = $request['funnel_id'] ?? null;
1455 $funnel_json = $request['funnel_data'] ?? [];
1456 $funnel_identifier = $request['funnel_identifier'] ?? [];
1457 $should_update_steps_order = $request['should_update_steps_order'] ?? false;
1458 $should_update_steps = $request['should_update_steps'] ?? false;
1459 $funnel_data = [];
1460 $_steps = [];
1461 $response = ['success' => true, 'link' => home_url()];
1462
1463 if ($funnel_json) {
1464 $funnel_data = $funnel_json;
1465 $steps = $funnel_data['drawflow']['Home']['data'];
1466
1467 if (is_array($steps)) {
1468 foreach ($steps as $key => $step) {
1469 if ($step) {
1470 $node_data = $step['data'];
1471 if (isset($node_data["step_name"])) unset($node_data["step_name"]);
1472
1473 // Preserve node_identifier at full precision by extracting it from the html
1474 // field (a string) rather than using the float value decoded by json_decode,
1475 // which PHP truncates before we can read it.
1476 if (isset($step['html'], $node_data['node_identifier'])) {
1477 $html = $step['html'];
1478 if (strpos($html, 'addstep') === 0) {
1479 $node_data['node_identifier'] = substr($html, 7);
1480 } elseif (strpos($html, 'conditional') === 0) {
1481 $node_data['node_identifier'] = substr($html, 11);
1482 }
1483 }
1484
1485 $step['data'] = $node_data;
1486 $_steps[$key] = $step;
1487
1488 }
1489 }
1490 }
1491
1492 }
1493 $funnel_data['drawflow']['Home']['data'] = apply_filters('wpfunnels/modify_funnel_data', $_steps);
1494 update_post_meta($funnel_id, '_funnel_data', $funnel_data);
1495 update_post_meta($funnel_id, 'funnel_identifier', $funnel_identifier);
1496
1497 if ($should_update_steps) {
1498 $steps = $this->get_steps($funnel_data);
1499 update_post_meta($funnel_id, '_steps', $steps);
1500 Wpfnl_functions::generate_first_step($funnel_id, $steps);
1501 }
1502
1503 $_steps_order = $this->get_steps_order($funnel_data);
1504 $key = array_search('checkout', array_column($_steps_order, 'step_type'));
1505 if (false !== $key) {
1506 $funnel_type = get_post_meta($funnel_id, '_wpfnl_funnel_type', true);
1507 if ('lead' === $funnel_type) {
1508 if (Wpfnl_functions::is_wc_active()) {
1509 update_post_meta($funnel_id, '_wpfnl_funnel_type', 'wc');
1510 } else {
1511 // Check if LMS add-on is active AND at least one LMS plugin (LearnDash or CreatorLMS) is active
1512 if (Wpfnl_functions::is_lms_addon_active() && Wpfnl_functions::is_any_lms_plugin_active()) {
1513 update_post_meta($funnel_id, '_wpfnl_funnel_type', 'lms');
1514 }
1515 }
1516 }
1517 }
1518 if ($should_update_steps_order) {
1519 $steps_order = array();
1520 foreach ($_steps_order as $_step) {
1521 if (count($_step)) {
1522 $steps_order[] = $_step;
1523 }
1524 }
1525
1526 if (count($steps_order)) {
1527 update_post_meta($funnel_id, '_steps_order', $steps_order);
1528
1529 } else {
1530 delete_post_meta($funnel_id, '_steps_order');
1531 }
1532 }
1533
1534 $response['step_id'] = Wpfnl_functions::get_first_step($funnel_id);
1535
1536 // Fallback for existing users' existing funnel
1537 // For new funnel, this condition should not trigger
1538 if (!$response['step_id']) {
1539 Wpfnl_functions::generate_first_step($funnel_id);
1540 $response['step_id'] = Wpfnl_functions::get_first_step($funnel_id);
1541 }
1542
1543 $utm_settings = Wpfnl_functions::get_funnel_utm_settings($funnel_id);
1544 $view_link = get_post_permalink($response['step_id']);
1545
1546 if (!empty($utm_settings['utm_enable']) && 'on' === $utm_settings['utm_enable']) {
1547 unset($utm_settings['utm_enable']);
1548 $view_link = add_query_arg($utm_settings, $view_link);
1549 $view_link = strtolower($view_link);
1550 }
1551
1552 $response['link'] = esc_url($view_link);
1553
1554 $response['funnel_id'] = $funnel_id;
1555 $response['success'] = true;
1556 $response['funnel_type'] = get_post_meta($funnel_id, '_wpfnl_funnel_type', true);
1557 $response = apply_filters('wpfunnels/update_funnel_link', $response);
1558 do_action('wpfunnels/after_save_funnel_data', $funnel_id);
1559 if (isset($request['mintSteps'])) {
1560 do_action('wpfunnels/save_mint_automation', $funnel_id, $request['mintSteps']);
1561 }
1562 Wpfnl_functions::generate_first_step($funnel_id);
1563
1564 do_action( 'wpfunnels_canvas_saved', absint( $funnel_id ) );
1565
1566 return rest_ensure_response($response);
1567 }
1568
1569
1570 /**
1571 * Get steps
1572 *
1573 * @param $funnel_flow_data
1574 *
1575 * @return array
1576 *
1577 * @since 2.0.5
1578 */
1579 private function get_steps($funnel_flow_data)
1580 {
1581 $drawflow = $funnel_flow_data['drawflow'];
1582 $steps = array();
1583 if (isset($drawflow['Home']['data'])) {
1584 $drawflow_data = $drawflow['Home']['data'];
1585 foreach ($drawflow_data as $key => $data) {
1586 $step_data = $data['data'];
1587 if ('conditional' !== $step_data['step_type'] && 'addstep' !== $step_data['step_type']) {
1588 $step_id = $step_data['step_id'];
1589 $step_type = $step_data['step_type'];
1590 $step_name = sanitize_text_field(get_the_title($step_data['step_id']));
1591 $steps[] = array(
1592 'id' => $step_id,
1593 'step_type' => $step_type,
1594 'name' => $step_name,
1595 );
1596 }
1597 }
1598 }
1599 return $steps;
1600 }
1601
1602
1603 /**
1604 * Get steps order
1605 *
1606 * @param $funnel_flow_data
1607 *
1608 * @return array
1609 *
1610 * @since 2.0.5
1611 */
1612 public function get_steps_order($funnel_flow_data)
1613 {
1614 $drawflow = $funnel_flow_data['drawflow'];
1615 $nodes = array();
1616 $step_order = array();
1617 $first_node_id = '';
1618 $start_node = array();
1619
1620
1621 if (isset($drawflow['Home']['data'])) {
1622 $drawflow_data = $drawflow['Home']['data'];
1623
1624 /**
1625 * If has only one step, that only step will be the first step, no conditions should be checked.
1626 * just return the step order
1627 */
1628 if (1 === count($drawflow_data)) {
1629 $node_id = array_keys($drawflow_data)[0];
1630 $data = $drawflow_data[$node_id];
1631 $step_data = isset($data['data']) ? $data['data'] : array();
1632 $step_type = isset($step_data['step_type']) ? $step_data['step_type'] : '';
1633 $step_id = isset($step_data['step_id']) ? $step_data['step_id'] : 0;
1634 $step_order[] = array(
1635 'id' => $step_id,
1636 'step_type' => $step_type,
1637 'name' => sanitize_text_field(get_the_title($step_id)),
1638 );
1639 return $step_order;
1640
1641 }
1642
1643 /**
1644 * 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
1645 * step information includes output connection and input connection and it will be stored on $nodes
1646 */
1647 foreach ($drawflow_data as $key => $data) {
1648 $step_data = $data['data'];
1649
1650 $step_type = $step_data['step_type'];
1651 $step_id = 'conditional' !== $step_type && 'addstep' !== $step_type ? $step_data['step_id'] : 0;
1652 if (
1653 (isset($data['outputs']['output_1']['connections']) && count($data['outputs']['output_1']['connections'])) ||
1654 (isset($data['inputs']['input_1']['connections']) && count($data['inputs']['input_1']['connections']))
1655 ) {
1656
1657
1658 if ('conditional' === $step_type || 'addstep' === $step_type) {
1659 continue;
1660 }
1661
1662 /**
1663 * A starting node is a node which has only output connection but not any input connection.
1664 * 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.
1665 * for other step types (checkout, offer, thankyou), we will check if the step has any output connection and no input connection.
1666 */
1667 if ('landing' === $step_type) {
1668 if (
1669 isset($data['outputs']['output_1']['connections']) && count($data['outputs']['output_1']['connections']) &&
1670 (isset($data['inputs']) && (count($data['inputs']) == 0 || (isset($data['inputs']['input_1']['connections']) && count($data['inputs']['input_1']['connections']) == 0)))
1671 ) {
1672 $start_node = array(
1673 'id' => $step_id,
1674 'step_type' => $step_type,
1675 'name' => sanitize_text_field(get_the_title($step_id)),
1676 );
1677 }
1678 } else {
1679 if (
1680 isset($data['outputs']['output_1']['connections']) && count($data['outputs']['output_1']['connections']) &&
1681 (isset($data['inputs']['input_1']['connections']) && count($data['inputs']['input_1']['connections']) === 0)
1682 ) {
1683 $start_node = array(
1684 'id' => $step_id,
1685 'step_type' => $step_type,
1686 'name' => sanitize_text_field(get_the_title($step_id)),
1687 );
1688 } else {
1689 $step_order[] = array(
1690 'id' => $step_id,
1691 'step_type' => $step_type,
1692 'name' => sanitize_text_field(get_the_title($step_id)),
1693 );
1694 }
1695 }
1696 }
1697 }
1698
1699 $step_order = $this->array_insert($step_order, $start_node, 0);
1700 }
1701 return $step_order;
1702 }
1703
1704
1705 /**
1706 * Array insert element on position
1707 *
1708 * @param $original
1709 * @param $inserted
1710 * @param int $position
1711 *
1712 * @return mixed
1713 */
1714 private function array_insert(&$original, $inserted, $position)
1715 {
1716 array_splice($original, $position, 0, array($inserted));
1717 return $original;
1718 }
1719
1720
1721 /**
1722 * Export funnel data
1723 *
1724 * @param $request
1725 *
1726 * @return false|string
1727 *
1728 * @since 1.0.0
1729 */
1730 public function export_funnel($request)
1731 {
1732 $funnel_id = $request['funnel_id'];
1733
1734 //===main array of data which will be downloaded as json file===//
1735 $data = array();
1736
1737 $funnel_title = get_the_title($funnel_id);
1738 $funnel_meta = get_post_meta($funnel_id);
1739
1740 //=== Added title and meta of current funnel post===//
1741 $data['title'] = $funnel_title;
1742 $data['meta'] = $funnel_meta;
1743
1744 //=== Find list of steps and ther data===//
1745 //== Getting steps from identifier meta==//
1746 if (isset($data['meta']['funnel_identifier'])) {
1747 $identifier_meta = $data['meta']['funnel_identifier'];
1748 $all_steps_array = array();
1749 foreach ($identifier_meta as $identifier_meta_key => $identifier_meta_value) {
1750 $node_step_pair = json_decode($identifier_meta_value, true);
1751 foreach ($node_step_pair as $node_step_pair_key => $node_step_pair_value) {
1752 $current_steps_array = array();
1753 $step_id = $node_step_pair_value;
1754 $step_title = get_the_title($step_id);
1755 $step_meta = get_post_meta($step_id);
1756 $content_post = get_post($step_id);
1757 $content = json_encode($content_post->post_content);
1758 $current_steps_array['step_id'] = $step_id;
1759 $current_steps_array['title'] = $step_title;
1760 $current_steps_array['meta'] = $step_meta;
1761 $current_steps_array['content'] = $content;
1762 $all_steps_array[] = $current_steps_array;
1763 }
1764 }
1765 $data['steps'] = $all_steps_array;
1766 }
1767
1768 return json_encode($data);
1769 }
1770
1771 /**
1772 * Get UTM Params URL
1773 */
1774
1775 public function get_utm_params($funnel_id)
1776 {
1777 $utm_params = '';
1778 $utm_settings = $this->get_utm_settings($funnel_id);
1779 $utm_params = '?utm_source=' . 'fgddffd' . '&utm_medium=' . 'ghfds' . '&utm_campaign=' . 'dsaghfds';
1780 return $utm_params;
1781 if ($utm_settings['utm_enable'] == 'on') {
1782 $utm_params = '?utm_source=' . $utm_settings['utm_source'] . '&utm_medium=' . $utm_settings['utm_medium'] . '&utm_campaign=' . $utm_settings['utm_campaign'];
1783 $utm_params .= ((!empty($utm_settings['utm_content'])) ? '&utm_content=' . $utm_settings['utm_content'] : '');
1784 $utm_params = strtolower($utm_params);
1785 }
1786 return $utm_params;
1787 }
1788
1789 /**
1790 * Get GTM Settings
1791 *
1792 * @return array
1793 */
1794 public function get_utm_settings($funnel_id)
1795 {
1796 $default_settings = array(
1797 'utm_enable' => 'off',
1798 'utm_source' => '',
1799 'utm_medium' => '',
1800 'utm_campaign' => '',
1801 'utm_content' => '',
1802 );
1803 $utm_settings = get_post_meta($funnel_id, '_wpfunnels_utm_params', true);
1804 return wp_parse_args($utm_settings, $default_settings);
1805 }
1806
1807
1808 /**
1809 * Prepare a single setting object for response.
1810 *
1811 * @param object|array $item Setting object.
1812 * @param WP_REST_Request $request Request object.
1813 *
1814 * @return \WP_REST_Response $response Response data.
1815 * @since 1.0.0
1816 */
1817 public function prepare_item_for_response($item, $request)
1818 {
1819 $data = $this->add_additional_fields_to_object($item, $request);
1820 return rest_ensure_response($data);
1821 }
1822
1823
1824 /**
1825 * Get GBF data
1826 *
1827 * @param $request
1828 *
1829 * @return WP_Error|\WP_REST_Response
1830 */
1831 public function get_GBF_data($request)
1832 {
1833
1834 $funnel_id = $request['funnel_id'];
1835 $steps = Wpfnl_functions::get_steps($funnel_id);
1836
1837 if ( ! Wpfnl_functions::is_global_funnel_activated() ) {
1838 $response = array(
1839 'success' => false,
1840 'data' => 'Global Funnel is not activated'
1841 );
1842 return rest_ensure_response($response);
1843 }
1844
1845 $is_gbf = get_post_meta($funnel_id, 'is_global_funnel', true);
1846 if ('yes' === $is_gbf) {
1847 $start_condition = get_post_meta($funnel_id, 'global_funnel_start_condition', true);
1848 $step_ids = array();
1849 foreach ($steps as $step) {
1850 if ($step['step_type'] == 'checkout') {
1851 if (!empty($start_condition)) {
1852 array_push($step_ids, $step['id']);
1853 }
1854 } elseif ($step['step_type'] == 'upsell') {
1855 $upsell_rules = get_post_meta($step['id'], 'global_funnel_upsell_rules', true);
1856 if (!empty($upsell_rules)) {
1857 array_push($step_ids, $step['id']);
1858 }
1859 } elseif ($step['step_type'] == 'downsell') {
1860 $downsell_rules = get_post_meta($step['id'], 'global_funnel_downsell_rules', true);
1861 if (!empty($downsell_rules)) {
1862 array_push($step_ids, $step['id']);
1863 }
1864 }
1865 }
1866 }
1867 $response = array(
1868 'success' => false,
1869 'data' => []
1870 );
1871 return rest_ensure_response($response);
1872 }
1873
1874
1875 /**
1876 * Get ab testing default setttings
1877 *
1878 * @param WP_REST_Request $request
1879 *
1880 * @return Array
1881 *
1882 * @since 1.6.21
1883 */
1884 public function get_ab_settings(WP_REST_Request $request)
1885 {
1886
1887 $response = [];
1888 if (isset($request['step_id'])) {
1889 $step_id = $request['step_id'];
1890 $default_settings = $this->get_default_start_setting($step_id);
1891 $response['data'] = $default_settings;
1892 $response['success'] = true;
1893 } else {
1894 $response['data'] = '';
1895 $response['success'] = false;
1896 }
1897
1898 return rest_ensure_response($response);
1899 }
1900
1901 /**
1902 * Change funnel name
1903 *
1904 * @param WP_REST_Request $payload Funnel name payload.
1905 *
1906 * @return array
1907 * @since 2.7.5
1908 */
1909 public function funnel_name_change($payload)
1910 {
1911 if (!isset($payload['funnel_id'], $payload['funnel_name'])) {
1912 return new WP_Error(
1913 'rest_invalid_request_params',
1914 __('Invalid request params.'),
1915 array('status' => 404)
1916 );
1917 }
1918
1919 $funnel_id = sanitize_text_field($payload['funnel_id']);
1920 $updated_name = sanitize_text_field($payload['funnel_name']);
1921 Wpfnl::$instance->funnel_store->set_id($funnel_id);
1922 Wpfnl::$instance->funnel_store->update_funnel_name($updated_name);
1923 self::update_steps_url_on_funnel_name_update($funnel_id);
1924 flush_rewrite_rules();
1925
1926 $response = array(
1927 'success' => true,
1928 'message' => 'Funnel name changed successfully',
1929 'funnelID' => $funnel_id,
1930 'name' => $updated_name
1931 );
1932
1933 return rest_ensure_response($response);
1934 }
1935
1936 /**
1937 * Update the URL and funnel name for each step in a funnel.
1938 *
1939 * This function retrieves the step IDs associated with a specific funnel,
1940 * and updates the URL and funnel name for each step based on the step ID.
1941 *
1942 * @param int $funnel_id The ID of the funnel.
1943 * @return void
1944 * @since 2.7.10
1945 */
1946 public function update_steps_url_on_funnel_name_update($funnel_id)
1947 {
1948 $step_controller = new StepController();
1949 $step_ids = Wpfnl_functions::get_step_ids($funnel_id);
1950 foreach ($step_ids as $step_id) {
1951 $step_title = get_post_meta($step_id, '_wpf_step_title', true);
1952 $step_slug = get_post_meta($step_id, '_wpf_step_slug', true);
1953 $settings = array(
1954 'funnel_id' => $funnel_id,
1955 'step_id' => $step_id,
1956 'title' => !empty($step_title) ? $step_title : get_the_title($step_id),
1957 'slug' => !empty($step_slug) ? $step_slug : get_post_field('post_name', $step_id)
1958 );
1959 $step_controller->update_step_meta_on_funnel_name_change($funnel_id, $step_id, $settings);
1960 }
1961 }
1962
1963 /**
1964 * Get funnel settings by funnel id
1965 *
1966 * @param array $payload Funnel Settings payload data.
1967 * @return WP_Rest_Response
1968 *
1969 * @since 1.0.0
1970 */
1971 public function wpfnl_get_funnel_settings($payload)
1972 {
1973 // Early return in case of missing funnel id.
1974 if (!isset($payload['funnel_id'])) {
1975 return $this->prepare_wp_error_response(
1976 'rest_missing_funnelID',
1977 __('Funnel id is missing', 'wpfnl'),
1978 array(
1979 'status' => 400
1980 )
1981 );
1982 }
1983
1984 // To get global settings.
1985 $global_gtm = Wpfnl_functions::get_gtm_settings();
1986 $global_pixel = Wpfnl_functions::get_facebook_pixel_settings();
1987 $settings = Wpfnl_functions::get_funnel_settings($payload['funnel_id']);
1988
1989 // Set default skip offer settings.
1990 $skip_settings = array(
1991 'skip_offer' => 'no',
1992 'skip_if_quantity' => 'no',
1993 );
1994
1995 if (get_post_meta($payload['funnel_id'], '_wpfunnels_skip_offer', true)) {
1996 $skip_settings = get_post_meta($payload['funnel_id'], '_wpfunnels_skip_offer', true);
1997 }
1998
1999 // Preparing rest response.
2000 $response = array(
2001 'success' => true,
2002 'data' => $settings,
2003 'globalGtm' => isset ($global_gtm['gtm_enable']) ? $global_gtm['gtm_enable'] : 'no',
2004 'globalPixel' => isset ($global_pixel['enable_fb_pixel']) ? $global_pixel['enable_fb_pixel'] : 'no',
2005 'skipSettings' => $skip_settings,
2006 'skipRecurringOffer' => get_post_meta($payload['funnel_id'], '_wpfunnels_skip_recurring_offer', true),
2007 'skipRecurringOfferWithinDays' => sanitize_text_field(get_post_meta($payload['funnel_id'], '_wpfunnels_skip_recurring_offer_within_days', true)),
2008 );
2009
2010 /**
2011 * Fires to get an individual funnel settings.
2012 *
2013 * @param string $response The settings data.
2014 * @param string $payload ['funnel_id'] The individual funnel id.
2015 * @since 1.0.0
2016 *
2017 */
2018 $response = apply_filters('wpfunnels/funnel_individual_settings', $response, $payload['funnel_id']);
2019 $response = rest_ensure_response($response);
2020
2021 return $response;//phpcs:ignore
2022 }
2023
2024 /**
2025 * Delete marked funnel
2026 *
2027 * @param $payload
2028 *
2029 * @return array|bool
2030 * @since 1.0.0
2031 */
2032 public function delete_marked_funnels($payload)
2033 {
2034 $funnel_controller = Module::instance();
2035
2036 return $funnel_controller->delete_marked_funnels($payload);
2037 }
2038
2039
2040 /**
2041 * Restore marked funnel
2042 *
2043 * @param $payload
2044 *
2045 * @return array|bool
2046 * @since 3.1.8
2047 */
2048 public function restore_marked_funnels($payload)
2049 {
2050 $funnel_controller = Module::instance();
2051
2052 return $funnel_controller->restore_marked_funnels($payload);
2053 }
2054
2055 /**
2056 * Restore marked funnel
2057 *
2058 * @param $payload
2059 *
2060 * @return array|bool
2061 * @since 3.1.8
2062 */
2063 public function trash_marked_funnels($payload)
2064 {
2065 $funnel_controller = Module::instance();
2066
2067 return $funnel_controller->trash_marked_funnels($payload);
2068 }
2069
2070
2071 /**
2072 * Convert a regular funnel to a Store Checkout funnel.
2073 *
2074 * Sets the funnel type to store_checkout, removes any landing steps
2075 * permanently, and ensures the checkout step is first.
2076 *
2077 * @param WP_REST_Request $request
2078 * @return WP_REST_Response|WP_Error
2079 * @since 3.10.6
2080 */
2081 public function convert_to_store_checkout( $request ) {
2082 $funnel_id = absint( $request['funnel_id'] );
2083
2084 if ( ! $funnel_id ) {
2085 return new WP_Error(
2086 'rest_invalid_funnel_id',
2087 __( 'Invalid funnel ID.', 'wpfnl' ),
2088 array( 'status' => 400 )
2089 );
2090 }
2091
2092 // Set funnel type to store_checkout.
2093 update_post_meta( $funnel_id, '_wpfnl_funnel_type', 'store_checkout' );
2094
2095 // Get current steps order.
2096 $steps_order = get_post_meta( $funnel_id, '_steps_order', true );
2097 if ( ! is_array( $steps_order ) ) {
2098 $steps_order = array();
2099 }
2100
2101 // Find and permanently delete all landing steps.
2102 $new_steps_order = array();
2103 foreach ( $steps_order as $step ) {
2104 if ( isset( $step['step_type'] ) && 'landing' === $step['step_type'] ) {
2105 wp_delete_post( absint( $step['id'] ), true );
2106 } else {
2107 $new_steps_order[] = $step;
2108 }
2109 }
2110
2111 // Sort so checkout step comes first.
2112 usort( $new_steps_order, function( $a, $b ) {
2113 $priority = array( 'checkout' => 0, 'thankyou' => 1, 'upsell' => 2, 'downsell' => 3 );
2114 $a_priority = isset( $priority[ $a['step_type'] ] ) ? $priority[ $a['step_type'] ] : 99;
2115 $b_priority = isset( $priority[ $b['step_type'] ] ) ? $priority[ $b['step_type'] ] : 99;
2116 return $a_priority - $b_priority;
2117 } );
2118
2119 // Re-index to avoid serialization issues.
2120 $new_steps_order = array_values( $new_steps_order );
2121
2122 // Persist both step meta keys that the codebase reads from.
2123 update_post_meta( $funnel_id, '_steps_order', $new_steps_order );
2124 update_post_meta( $funnel_id, '_steps', $new_steps_order );
2125
2126 // Update _first_step to the checkout step.
2127 foreach ( $new_steps_order as $step ) {
2128 if ( isset( $step['step_type'] ) && 'checkout' === $step['step_type'] ) {
2129 Wpfnl_functions::update_funnel_first_step( $funnel_id, $step['id'] );
2130 break;
2131 }
2132 }
2133
2134 // Remove landing step nodes from funnel canvas data and clean up dangling connections.
2135 $funnel_data = get_post_meta( $funnel_id, '_funnel_data', true );
2136 if ( is_array( $funnel_data ) && isset( $funnel_data['drawflow']['Home']['data'] ) ) {
2137 // Collect landing node IDs first.
2138 $landing_node_ids = array();
2139 foreach ( $funnel_data['drawflow']['Home']['data'] as $node_id => $node ) {
2140 if ( isset( $node['data']['step_type'] ) && 'landing' === $node['data']['step_type'] ) {
2141 $landing_node_ids[] = (string) $node_id;
2142 }
2143 }
2144
2145 // Remove landing nodes.
2146 foreach ( $landing_node_ids as $landing_node_id ) {
2147 unset( $funnel_data['drawflow']['Home']['data'][ $landing_node_id ] );
2148 }
2149
2150 // Clean up input/output connections in remaining nodes that referenced landing nodes.
2151 foreach ( $funnel_data['drawflow']['Home']['data'] as $node_id => &$node ) {
2152 // Clean inputs.
2153 if ( isset( $node['inputs'] ) && is_array( $node['inputs'] ) ) {
2154 foreach ( $node['inputs'] as $input_key => &$input ) {
2155 if ( isset( $input['connections'] ) && is_array( $input['connections'] ) ) {
2156 $input['connections'] = array_values( array_filter(
2157 $input['connections'],
2158 function( $conn ) use ( $landing_node_ids ) {
2159 return ! in_array( (string) $conn['node'], $landing_node_ids, true );
2160 }
2161 ) );
2162 }
2163 }
2164 unset( $input );
2165 }
2166 // Clean outputs.
2167 if ( isset( $node['outputs'] ) && is_array( $node['outputs'] ) ) {
2168 foreach ( $node['outputs'] as $output_key => &$output ) {
2169 if ( isset( $output['connections'] ) && is_array( $output['connections'] ) ) {
2170 $output['connections'] = array_values( array_filter(
2171 $output['connections'],
2172 function( $conn ) use ( $landing_node_ids ) {
2173 return ! in_array( (string) $conn['node'], $landing_node_ids, true );
2174 }
2175 ) );
2176 }
2177 }
2178 unset( $output );
2179 }
2180 }
2181 unset( $node );
2182
2183 update_post_meta( $funnel_id, '_funnel_data', $funnel_data );
2184 }
2185
2186 // Return the checkout step ID so the frontend can use it immediately.
2187 $checkout_step_id = null;
2188 foreach ( $new_steps_order as $step ) {
2189 if ( isset( $step['step_type'] ) && 'checkout' === $step['step_type'] ) {
2190 $checkout_step_id = $step['id'];
2191 delete_post_meta( absint( $step['id'] ), '_wpfnl_checkout_products' );
2192 break;
2193 }
2194 }
2195
2196 return rest_ensure_response( array(
2197 'success' => true,
2198 'message' => __( 'Funnel converted to Store Checkout successfully.', 'wpfnl' ),
2199 'checkout_step_id' => $checkout_step_id,
2200 ) );
2201 }
2202
2203 }
2204