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 / DashboardController.php

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

614 lines 16.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3
4 namespace WPFunnels\Rest\Controllers;
5
6 use WPFunnels\Report\ReportGenerator;
7 use WPFunnels\Wpfnl_functions;
8
9 /**
10 *
11 *
12 * Class DashboardController
13 * @package WPFunnels\Rest\Controllers
14 * @since 3.2.0
15 */
16 class DashboardController extends Wpfnl_REST_Controller
17 {
18
19 /**
20 * Endpoint namespace.
21 *
22 * @var string
23 * @since 3.2.0
24 */
25 protected $namespace = 'wpfunnels/v1';
26
27
28 /**
29 * Route base.
30 *
31 * @var string
32 * @since 3.2.0
33 */
34 protected $rest_base = 'report';
35
36
37 /**
38 * Makes sure the current user has access to READ the settings APIs.
39 *
40 *
41 * @param $request
42 * @return \WP_Error|boolean
43 * @since 3.2.0
44 */
45 public function get_items_permissions_check($request)
46 {
47 if (!Wpfnl_functions::wpfnl_rest_check_manager_permissions('settings')) {
48 return new \WP_Error('wpfunnels_rest_cannot_edit', __('Sorry, you cannot list resources.', 'wpfnl'), array('status' => rest_authorization_required_code()));
49 }
50 return true;
51 }
52
53
54 /**
55 * Register rest routes
56 *
57 * @since 3.2.0
58 */
59 public function register_routes()
60 {
61 register_rest_route(
62 $this->namespace,
63 '/' . $this->rest_base . '/overview',
64 array(
65 array(
66 'methods' => \WP_REST_Server::READABLE,
67 'args' => $this->get_stats_args(),
68 'callback' => array($this, 'get_overview'),
69 'permission_callback' => array($this, 'get_items_permissions_check'),
70 ),
71 )
72 );
73
74 register_rest_route(
75 $this->namespace,
76 '/' . $this->rest_base . '/stats',
77 array(
78 array(
79 'methods' => \WP_REST_Server::READABLE,
80 'args' => $this->get_stats_args(),
81 'callback' => array($this, 'get_stats'),
82 'permission_callback' => array($this, 'get_items_permissions_check'),
83 ),
84 )
85 );
86
87 register_rest_route(
88 $this->namespace,
89 '/' . $this->rest_base . '/top-funnels',
90 array(
91 array(
92 'methods' => \WP_REST_Server::READABLE,
93 'args' => $this->get_stats_args(),
94 'callback' => array($this, 'get_top_funnels'),
95 'permission_callback' => array($this, 'get_items_permissions_check'),
96 ),
97 )
98 );
99
100 register_rest_route(
101 $this->namespace,
102 '/' . $this->rest_base . '/onboarding-status',
103 array(
104 array(
105 'methods' => \WP_REST_Server::READABLE,
106 'callback' => array($this, 'get_onboarding_status'),
107 'permission_callback' => array($this, 'get_items_permissions_check'),
108 ),
109 )
110 );
111
112 register_rest_route(
113 $this->namespace,
114 '/' . $this->rest_base . '/has-funnels',
115 array(
116 array(
117 'methods' => \WP_REST_Server::READABLE,
118 'callback' => array($this, 'check_has_funnels'),
119 'permission_callback' => array( $this, 'get_items_permissions_check' ),
120 ),
121 )
122 );
123
124 register_rest_route(
125 $this->namespace,
126 '/' . $this->rest_base . '/dismiss-onboarding',
127 array(
128 array(
129 'methods' => \WP_REST_Server::CREATABLE,
130 'callback' => array($this, 'dismiss_onboarding'),
131 'permission_callback' => array( $this, 'get_items_permissions_check' ),
132 ),
133 )
134 );
135
136 register_rest_route(
137 $this->namespace,
138 '/' . $this->rest_base . '/sync-cache',
139 array(
140 array(
141 'methods' => \WP_REST_Server::CREATABLE,
142 'callback' => array( $this, 'sync_cache' ),
143 'permission_callback' => array( $this, 'get_items_permissions_check' ),
144 ),
145 )
146 );
147 }
148
149 /**
150 * Get stats arguments
151 *
152 * @return mixed|void
153 * @since 3.2.0
154 */
155 public function get_stats_args()
156 {
157 return array(
158 'after' => array(
159 'type' => 'string',
160 'format' => 'date-time',
161 'validate_callback' => 'rest_validate_request_arg',
162 ),
163 'before' => array(
164 'type' => 'string',
165 'format' => 'date-time',
166 'validate_callback' => 'rest_validate_request_arg',
167 ),
168 'interval' => array(
169 'type' => 'string',
170 'default' => 'week',
171 'enum' => array(
172 'hour',
173 'day',
174 'week',
175 'month',
176 'quarter',
177 'year',
178 ),
179 ),
180 );
181 }
182
183
184 /**
185 * Cache payload version. Bump whenever the shape or the meaning of a cached
186 * report response changes, so entries written by an older build are ignored
187 * instead of served until they expire.
188 *
189 * @var string
190 * @since 3.13.0
191 */
192 const CACHE_VERSION = 'v2';
193
194
195 /**
196 * Build a cache key from a prefix and parameter array.
197 *
198 * @param string $prefix
199 * @param array $params
200 * @return string
201 *
202 * @since 3.9.6
203 */
204 protected function get_cache_key( $prefix, $params ) {
205 return 'wpfnl_dash_' . $prefix . '_' . self::CACHE_VERSION . '_' . md5( serialize( $params ) );
206 }
207
208
209 /**
210 * Delete all dashboard transients from the database.
211 * Called by the sync-cache endpoint or when cache must be invalidated.
212 *
213 * @since 3.9.6
214 */
215 public static function delete_dashboard_cache() {
216 global $wpdb;
217 $wpdb->query(
218 "DELETE FROM {$wpdb->options}
219 WHERE option_name LIKE '_transient_wpfnl_dash_%'
220 OR option_name LIKE '_transient_timeout_wpfnl_dash_%'"
221 );
222 }
223
224
225 /**
226 * Get overview data of funnels (with 24-hour server-side cache).
227 *
228 * @param \WP_REST_Request $request
229 * @return \WP_Error|\WP_REST_Response
230 * @throws \Exception
231 *
232 * @since 3.2.0
233 */
234 public function get_overview(\WP_REST_Request $request)
235 {
236 $params = $request->get_params();
237 $start_date = isset($params['after']) ? $params['after'] : $this->default_after()->format('Y-m-d H:i:s');
238 $end_date = isset($params['before']) ? $params['before'] : $this->default_before()->format('Y-m-d H:i:s');
239
240 $cache_key = $this->get_cache_key( 'ov', array( $start_date, $end_date ) );
241 $cached = get_transient( $cache_key );
242 if ( false !== $cached ) {
243 return rest_ensure_response( $cached );
244 }
245
246 $response = ReportGenerator::get_overview( $start_date, $end_date );
247 set_transient( $cache_key, $response, DAY_IN_SECONDS );
248
249 return rest_ensure_response( $response );
250 }
251
252
253 /**
254 * Get stats of the funnels with intervals period (with 24-hour server-side cache).
255 *
256 * @param \WP_REST_Request $request
257 * @return \WP_Error|\WP_REST_Response
258 * @throws \Exception
259 *
260 * @since 3.2.0
261 */
262 public function get_stats(\WP_REST_Request $request)
263 {
264 $params = $request->get_params();
265 $start_date = isset($params['after']) ? $params['after'] : $this->default_after()->format('Y-m-d H:i:s');
266 $end_date = isset($params['before']) ? $params['before'] : $this->default_before()->format('Y-m-d H:i:s');
267 $interval = isset($params['interval']) ? $params['interval'] : 'day';
268
269 $cache_key = $this->get_cache_key( 'st', array( $start_date, $end_date, $interval ) );
270 $cached = get_transient( $cache_key );
271 if ( false !== $cached ) {
272 return rest_ensure_response( $cached );
273 }
274
275 $response = ReportGenerator::get_stats( $start_date, $end_date, $interval );
276 set_transient( $cache_key, $response, DAY_IN_SECONDS );
277
278 return rest_ensure_response( $response );
279 }
280
281
282 /**
283 * Get 3 top performing funnels
284 *
285 * @param \WP_REST_Request $request
286 * @return \WP_Error|\WP_REST_Response
287 *
288 * @since 3.2.0
289 */
290 public function get_top_funnels(\WP_REST_Request $request)
291 {
292 $params = $request->get_params();
293 $start_date = isset( $params['after'] ) ? $params['after'] : $this->default_after()->format( 'Y-m-d H:i:s' );
294 $end_date = isset( $params['before'] ) ? $params['before'] : $this->default_before()->format( 'Y-m-d H:i:s' );
295
296 $response['status'] = true;
297 $response['data'] = ReportGenerator::get_top_funnels( $start_date, $end_date );
298 return rest_ensure_response( $response );
299 }
300
301
302 /**
303 * Get onboarding status.
304 *
305 * Determines whether the onboarding section should be shown on the dashboard.
306 * The onboarding is hidden when ALL three conditions are met:
307 * 1. At least one funnel is published (live)
308 * 2. At least one funnel contains an order bump (upsell is optional/bonus)
309 * 3. At least one real order has been placed through any funnel
310 *
311 * Also returns individual step completion states for the checklist.
312 *
313 * @return \WP_REST_Response
314 * @since 3.9.5
315 */
316 public function get_onboarding_status()
317 {
318 // ── Check if onboarding has been dismissed ──
319 $is_dismissed = get_transient( 'wpfnl_onboarding_dismissed' );
320 if ( $is_dismissed ) {
321 return rest_ensure_response( array(
322 'success' => true,
323 'show_onboarding' => false,
324 'steps' => array(),
325 'completed_count' => 0,
326 'total_steps' => 0,
327 'progress_percent' => 0,
328 ) );
329 }
330
331 $is_wc_active = class_exists('WooCommerce');
332 $has_live_funnel = false;
333 $has_order_bump = false;
334 $has_upsell = false;
335 $has_real_order = false;
336 $has_funnel_created = false;
337 $has_store_checkout_created = false;
338
339 // Meta query to exclude store checkout funnels
340 $exclude_sc_meta = array(
341 'relation' => 'OR',
342 array(
343 'key' => '_wpfnl_funnel_type',
344 'compare' => 'NOT EXISTS',
345 ),
346 array(
347 'key' => '_wpfnl_funnel_type',
348 'value' => 'store_checkout',
349 'compare' => '!=',
350 ),
351 );
352
353 // ── Step 1: Check for published funnels (excluding store checkouts) ──
354 $funnels = get_posts(array(
355 'post_type' => WPFNL_FUNNELS_POST_TYPE,
356 'post_status' => 'publish',
357 'numberposts' => -1,
358 'fields' => 'ids',
359 'meta_query' => $exclude_sc_meta,
360 ));
361
362 if (!empty($funnels)) {
363 $has_funnel_created = true;
364 $has_live_funnel = true;
365 }
366
367 // Also check for draft funnels (counts as "created" but not "live")
368 if (!$has_funnel_created) {
369 $draft_funnels = get_posts(array(
370 'post_type' => WPFNL_FUNNELS_POST_TYPE,
371 'post_status' => array('publish', 'draft'),
372 'numberposts' => 1,
373 'fields' => 'ids',
374 'meta_query' => $exclude_sc_meta,
375 ));
376 $has_funnel_created = !empty($draft_funnels);
377 }
378
379 // ── Check for store checkout funnels separately ──
380 $sc_funnels = get_posts(array(
381 'post_type' => WPFNL_FUNNELS_POST_TYPE,
382 'post_status' => array('publish', 'draft'),
383 'numberposts' => 1,
384 'fields' => 'ids',
385 'meta_query' => array(
386 array(
387 'key' => '_wpfnl_funnel_type',
388 'value' => 'store_checkout',
389 ),
390 ),
391 ));
392 $has_store_checkout_created = !empty($sc_funnels);
393
394 // ── Step 2: Check for order bump & upsell across all published funnels ──
395 if (!empty($funnels)) {
396 foreach ($funnels as $funnel_id) {
397 $steps = Wpfnl_functions::get_steps($funnel_id);
398
399 if (!is_array($steps) || empty($steps)) {
400 continue;
401 }
402
403 foreach ($steps as $step) {
404 $step_id = isset($step['id']) ? $step['id'] : 0;
405 $step_type = isset($step['step_type']) ? $step['step_type'] : '';
406
407 // Check for order bump on checkout steps
408 if ('checkout' === $step_type && !$has_order_bump) {
409 $ob_settings = get_post_meta($step_id, 'order-bump-settings', true);
410 if (!empty($ob_settings) && is_array($ob_settings)) {
411 $is_multidimensional = Wpfnl_functions::check_array_is_multidimentional($ob_settings);
412 if ($is_multidimensional) {
413 foreach ($ob_settings as $ob) {
414 if (!empty($ob['product']) && !empty($ob['isEnabled'])) {
415 $has_order_bump = true;
416 break;
417 }
418 }
419 }
420 else {
421 // Single OB (legacy format)
422 if (!empty($ob_settings['product'])) {
423 $has_order_bump = true;
424 }
425 }
426 }
427 }
428
429 // Check for upsell step
430 if ('upsell' === $step_type) {
431 $has_upsell = true;
432 }
433 }
434
435 // If we already found both, no need to check more funnels
436 if ($has_order_bump && $has_upsell) {
437 break;
438 }
439 }
440 }
441
442 // ── Build the checklist deep links ──
443 // The checklist actions should drop the user into the place where the step
444 // actually gets done, rather than the funnel list: the order bump tab of the
445 // newest funnel's checkout step, and the canvas of that funnel for the upsell.
446 // Drafts count too — a funnel does not need to be live to be worked on.
447 $order_bump_url = '';
448 $funnel_builder_url = '';
449 $recent_funnels = get_posts(array(
450 'post_type' => WPFNL_FUNNELS_POST_TYPE,
451 'post_status' => array('publish', 'draft'),
452 'numberposts' => 10,
453 'orderby' => 'date',
454 'order' => 'DESC',
455 'fields' => 'ids',
456 'meta_query' => $exclude_sc_meta,
457 ));
458
459 if (!empty($recent_funnels)) {
460 // The upsell step is added on the canvas, so that is where its action goes.
461 $funnel_builder_url = admin_url(
462 sprintf('admin.php?page=edit_funnel&id=%d', $recent_funnels[0])
463 );
464 }
465
466 foreach ($recent_funnels as $recent_funnel_id) {
467 $recent_steps = Wpfnl_functions::get_steps($recent_funnel_id);
468
469 if (!is_array($recent_steps) || empty($recent_steps)) {
470 continue;
471 }
472
473 foreach ($recent_steps as $recent_step) {
474 $recent_step_type = isset($recent_step['step_type']) ? $recent_step['step_type'] : '';
475 $recent_step_id = isset($recent_step['id']) ? (int)$recent_step['id'] : 0;
476
477 if ('checkout' !== $recent_step_type || !$recent_step_id) {
478 continue;
479 }
480
481 // The hash is what tells the canvas which step settings drawer and
482 // tab to open once the funnel editor has finished loading.
483 $order_bump_url = admin_url(
484 sprintf(
485 'admin.php?page=edit_funnel&id=%d&step_id=%d#%d/settings/order-bump/',
486 $recent_funnel_id,
487 $recent_step_id,
488 $recent_step_id
489 )
490 );
491 break 2;
492 }
493 }
494
495 // ── Step 3: Check for real orders through funnels ──
496 if ($is_wc_active && !empty($funnels)) {
497 global $wpdb;
498 $stats_table = $wpdb->prefix . 'wpfnl_stats';
499
500 // Check if stats table exists
501 $table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $stats_table));
502 if ($table_exists) {
503 $order_count = $wpdb->get_var(
504 "SELECT COUNT(id) FROM {$stats_table} WHERE status IN ('completed', 'processing') LIMIT 1"
505 );
506 $has_real_order = (int)$order_count > 0;
507 }
508 }
509
510 // ── Determine if onboarding should be shown ──
511 // Hide onboarding only when ALL conditions are met:
512 // - At least 1 live funnel
513 // - Has an order bump (upsell is optional)
514 // - Has at least 1 real order
515 $should_hide_onboarding = $has_live_funnel && $has_order_bump && $has_real_order;
516
517 // ── Build checklist step completion data ──
518 $completed_count = 0;
519 $steps_status = array(
520 'store_checkout_created' => $has_store_checkout_created,
521 'funnel_created' => $has_funnel_created,
522 'order_bump_added' => $has_order_bump,
523 'upsell_added' => $has_upsell,
524 'test_order_placed' => $has_real_order,
525 'funnel_live' => $has_live_funnel,
526 );
527
528 foreach ($steps_status as $status) {
529 if ($status) {
530 $completed_count++;
531 }
532 }
533
534 $total_steps = count($steps_status);
535 $progress_percent = $total_steps > 0 ? round(($completed_count / $total_steps) * 100) : 0;
536
537 return rest_ensure_response(array(
538 'success' => true,
539 'show_onboarding' => !$should_hide_onboarding,
540 'steps' => $steps_status,
541 'completed_count' => $completed_count,
542 'total_steps' => $total_steps,
543 'progress_percent' => $progress_percent,
544 'order_bump_url' => $order_bump_url,
545 'funnel_builder_url' => $funnel_builder_url,
546 ));
547 }
548
549 /**
550 * Check if there are any funnels
551 *
552 * @param \WP_REST_Request $request
553 * @return \WP_REST_Response
554 *
555 * @since 3.9.5
556 */
557 public function check_has_funnels( \WP_REST_Request $request ) {
558 global $wpdb;
559
560 // Count only non-store-checkout funnels
561 $funnel_count = $wpdb->get_var(
562 $wpdb->prepare(
563 "SELECT COUNT(*) FROM {$wpdb->posts} p
564 LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = '_wpfnl_funnel_type'
565 WHERE p.post_type = %s
566 AND p.post_status IN ('publish', 'draft')
567 AND (pm.meta_value IS NULL OR pm.meta_value != 'store_checkout')",
568 'wpfunnels'
569 )
570 );
571
572 return rest_ensure_response( array(
573 'success' => true,
574 'has_funnels' => (int) $funnel_count > 0,
575 'funnel_count' => (int) $funnel_count
576 ) );
577 }
578
579 /**
580 * Clear all dashboard transient caches so the next request fetches fresh data.
581 *
582 * @param \WP_REST_Request $request
583 * @return \WP_REST_Response
584 *
585 * @since 3.9.6
586 */
587 public function sync_cache( \WP_REST_Request $request ) {
588 self::delete_dashboard_cache();
589 return rest_ensure_response( array(
590 'success' => true,
591 'message' => __( 'Dashboard cache cleared. Data will refresh on next load.', 'wpfnl' ),
592 ) );
593 }
594
595
596 /**
597 * Dismiss the onboarding checklist
598 *
599 * @param \WP_REST_Request $request
600 * @return \WP_REST_Response
601 *
602 * @since 3.9.5
603 */
604 public function dismiss_onboarding( \WP_REST_Request $request ) {
605 // Store dismissal in a transient for 30 days
606 set_transient( 'wpfnl_onboarding_dismissed', true, 30 * DAY_IN_SECONDS );
607
608 return rest_ensure_response( array(
609 'success' => true,
610 'message' => 'Onboarding dismissed successfully'
611 ) );
612 }
613 }
614