PluginProbe
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell / 3.12.13
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell v3.12.13
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 / includes / core / rest-api / Controllers / DashboardController.php

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

603 lines 16.1 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 * Build a cache key from a prefix and parameter array.
186 *
187 * @param string $prefix
188 * @param array $params
189 * @return string
190 *
191 * @since 3.9.6
192 */
193 protected function get_cache_key( $prefix, $params ) {
194 return 'wpfnl_dash_' . $prefix . '_' . md5( serialize( $params ) );
195 }
196
197
198 /**
199 * Delete all dashboard transients from the database.
200 * Called by the sync-cache endpoint or when cache must be invalidated.
201 *
202 * @since 3.9.6
203 */
204 public static function delete_dashboard_cache() {
205 global $wpdb;
206 $wpdb->query(
207 "DELETE FROM {$wpdb->options}
208 WHERE option_name LIKE '_transient_wpfnl_dash_%'
209 OR option_name LIKE '_transient_timeout_wpfnl_dash_%'"
210 );
211 }
212
213
214 /**
215 * Get overview data of funnels (with 24-hour server-side cache).
216 *
217 * @param \WP_REST_Request $request
218 * @return \WP_Error|\WP_REST_Response
219 * @throws \Exception
220 *
221 * @since 3.2.0
222 */
223 public function get_overview(\WP_REST_Request $request)
224 {
225 $params = $request->get_params();
226 $start_date = isset($params['after']) ? $params['after'] : $this->default_after()->format('Y-m-d H:i:s');
227 $end_date = isset($params['before']) ? $params['before'] : $this->default_before()->format('Y-m-d H:i:s');
228
229 $cache_key = $this->get_cache_key( 'ov', array( $start_date, $end_date ) );
230 $cached = get_transient( $cache_key );
231 if ( false !== $cached ) {
232 return rest_ensure_response( $cached );
233 }
234
235 $response = ReportGenerator::get_overview( $start_date, $end_date );
236 set_transient( $cache_key, $response, DAY_IN_SECONDS );
237
238 return rest_ensure_response( $response );
239 }
240
241
242 /**
243 * Get stats of the funnels with intervals period (with 24-hour server-side cache).
244 *
245 * @param \WP_REST_Request $request
246 * @return \WP_Error|\WP_REST_Response
247 * @throws \Exception
248 *
249 * @since 3.2.0
250 */
251 public function get_stats(\WP_REST_Request $request)
252 {
253 $params = $request->get_params();
254 $start_date = isset($params['after']) ? $params['after'] : $this->default_after()->format('Y-m-d H:i:s');
255 $end_date = isset($params['before']) ? $params['before'] : $this->default_before()->format('Y-m-d H:i:s');
256 $interval = isset($params['interval']) ? $params['interval'] : 'day';
257
258 $cache_key = $this->get_cache_key( 'st', array( $start_date, $end_date, $interval ) );
259 $cached = get_transient( $cache_key );
260 if ( false !== $cached ) {
261 return rest_ensure_response( $cached );
262 }
263
264 $response = ReportGenerator::get_stats( $start_date, $end_date, $interval );
265 set_transient( $cache_key, $response, DAY_IN_SECONDS );
266
267 return rest_ensure_response( $response );
268 }
269
270
271 /**
272 * Get 3 top performing funnels
273 *
274 * @param \WP_REST_Request $request
275 * @return \WP_Error|\WP_REST_Response
276 *
277 * @since 3.2.0
278 */
279 public function get_top_funnels(\WP_REST_Request $request)
280 {
281 $params = $request->get_params();
282 $start_date = isset( $params['after'] ) ? $params['after'] : $this->default_after()->format( 'Y-m-d H:i:s' );
283 $end_date = isset( $params['before'] ) ? $params['before'] : $this->default_before()->format( 'Y-m-d H:i:s' );
284
285 $response['status'] = true;
286 $response['data'] = ReportGenerator::get_top_funnels( $start_date, $end_date );
287 return rest_ensure_response( $response );
288 }
289
290
291 /**
292 * Get onboarding status.
293 *
294 * Determines whether the onboarding section should be shown on the dashboard.
295 * The onboarding is hidden when ALL three conditions are met:
296 * 1. At least one funnel is published (live)
297 * 2. At least one funnel contains an order bump (upsell is optional/bonus)
298 * 3. At least one real order has been placed through any funnel
299 *
300 * Also returns individual step completion states for the checklist.
301 *
302 * @return \WP_REST_Response
303 * @since 3.9.5
304 */
305 public function get_onboarding_status()
306 {
307 // ── Check if onboarding has been dismissed ──
308 $is_dismissed = get_transient( 'wpfnl_onboarding_dismissed' );
309 if ( $is_dismissed ) {
310 return rest_ensure_response( array(
311 'success' => true,
312 'show_onboarding' => false,
313 'steps' => array(),
314 'completed_count' => 0,
315 'total_steps' => 0,
316 'progress_percent' => 0,
317 ) );
318 }
319
320 $is_wc_active = class_exists('WooCommerce');
321 $has_live_funnel = false;
322 $has_order_bump = false;
323 $has_upsell = false;
324 $has_real_order = false;
325 $has_funnel_created = false;
326 $has_store_checkout_created = false;
327
328 // Meta query to exclude store checkout funnels
329 $exclude_sc_meta = array(
330 'relation' => 'OR',
331 array(
332 'key' => '_wpfnl_funnel_type',
333 'compare' => 'NOT EXISTS',
334 ),
335 array(
336 'key' => '_wpfnl_funnel_type',
337 'value' => 'store_checkout',
338 'compare' => '!=',
339 ),
340 );
341
342 // ── Step 1: Check for published funnels (excluding store checkouts) ──
343 $funnels = get_posts(array(
344 'post_type' => WPFNL_FUNNELS_POST_TYPE,
345 'post_status' => 'publish',
346 'numberposts' => -1,
347 'fields' => 'ids',
348 'meta_query' => $exclude_sc_meta,
349 ));
350
351 if (!empty($funnels)) {
352 $has_funnel_created = true;
353 $has_live_funnel = true;
354 }
355
356 // Also check for draft funnels (counts as "created" but not "live")
357 if (!$has_funnel_created) {
358 $draft_funnels = get_posts(array(
359 'post_type' => WPFNL_FUNNELS_POST_TYPE,
360 'post_status' => array('publish', 'draft'),
361 'numberposts' => 1,
362 'fields' => 'ids',
363 'meta_query' => $exclude_sc_meta,
364 ));
365 $has_funnel_created = !empty($draft_funnels);
366 }
367
368 // ── Check for store checkout funnels separately ──
369 $sc_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' => array(
375 array(
376 'key' => '_wpfnl_funnel_type',
377 'value' => 'store_checkout',
378 ),
379 ),
380 ));
381 $has_store_checkout_created = !empty($sc_funnels);
382
383 // ── Step 2: Check for order bump & upsell across all published funnels ──
384 if (!empty($funnels)) {
385 foreach ($funnels as $funnel_id) {
386 $steps = Wpfnl_functions::get_steps($funnel_id);
387
388 if (!is_array($steps) || empty($steps)) {
389 continue;
390 }
391
392 foreach ($steps as $step) {
393 $step_id = isset($step['id']) ? $step['id'] : 0;
394 $step_type = isset($step['step_type']) ? $step['step_type'] : '';
395
396 // Check for order bump on checkout steps
397 if ('checkout' === $step_type && !$has_order_bump) {
398 $ob_settings = get_post_meta($step_id, 'order-bump-settings', true);
399 if (!empty($ob_settings) && is_array($ob_settings)) {
400 $is_multidimensional = Wpfnl_functions::check_array_is_multidimentional($ob_settings);
401 if ($is_multidimensional) {
402 foreach ($ob_settings as $ob) {
403 if (!empty($ob['product']) && !empty($ob['isEnabled'])) {
404 $has_order_bump = true;
405 break;
406 }
407 }
408 }
409 else {
410 // Single OB (legacy format)
411 if (!empty($ob_settings['product'])) {
412 $has_order_bump = true;
413 }
414 }
415 }
416 }
417
418 // Check for upsell step
419 if ('upsell' === $step_type) {
420 $has_upsell = true;
421 }
422 }
423
424 // If we already found both, no need to check more funnels
425 if ($has_order_bump && $has_upsell) {
426 break;
427 }
428 }
429 }
430
431 // ── Build the checklist deep links ──
432 // The checklist actions should drop the user into the place where the step
433 // actually gets done, rather than the funnel list: the order bump tab of the
434 // newest funnel's checkout step, and the canvas of that funnel for the upsell.
435 // Drafts count too — a funnel does not need to be live to be worked on.
436 $order_bump_url = '';
437 $funnel_builder_url = '';
438 $recent_funnels = get_posts(array(
439 'post_type' => WPFNL_FUNNELS_POST_TYPE,
440 'post_status' => array('publish', 'draft'),
441 'numberposts' => 10,
442 'orderby' => 'date',
443 'order' => 'DESC',
444 'fields' => 'ids',
445 'meta_query' => $exclude_sc_meta,
446 ));
447
448 if (!empty($recent_funnels)) {
449 // The upsell step is added on the canvas, so that is where its action goes.
450 $funnel_builder_url = admin_url(
451 sprintf('admin.php?page=edit_funnel&id=%d', $recent_funnels[0])
452 );
453 }
454
455 foreach ($recent_funnels as $recent_funnel_id) {
456 $recent_steps = Wpfnl_functions::get_steps($recent_funnel_id);
457
458 if (!is_array($recent_steps) || empty($recent_steps)) {
459 continue;
460 }
461
462 foreach ($recent_steps as $recent_step) {
463 $recent_step_type = isset($recent_step['step_type']) ? $recent_step['step_type'] : '';
464 $recent_step_id = isset($recent_step['id']) ? (int)$recent_step['id'] : 0;
465
466 if ('checkout' !== $recent_step_type || !$recent_step_id) {
467 continue;
468 }
469
470 // The hash is what tells the canvas which step settings drawer and
471 // tab to open once the funnel editor has finished loading.
472 $order_bump_url = admin_url(
473 sprintf(
474 'admin.php?page=edit_funnel&id=%d&step_id=%d#%d/settings/order-bump/',
475 $recent_funnel_id,
476 $recent_step_id,
477 $recent_step_id
478 )
479 );
480 break 2;
481 }
482 }
483
484 // ── Step 3: Check for real orders through funnels ──
485 if ($is_wc_active && !empty($funnels)) {
486 global $wpdb;
487 $stats_table = $wpdb->prefix . 'wpfnl_stats';
488
489 // Check if stats table exists
490 $table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $stats_table));
491 if ($table_exists) {
492 $order_count = $wpdb->get_var(
493 "SELECT COUNT(id) FROM {$stats_table} WHERE status IN ('completed', 'processing') LIMIT 1"
494 );
495 $has_real_order = (int)$order_count > 0;
496 }
497 }
498
499 // ── Determine if onboarding should be shown ──
500 // Hide onboarding only when ALL conditions are met:
501 // - At least 1 live funnel
502 // - Has an order bump (upsell is optional)
503 // - Has at least 1 real order
504 $should_hide_onboarding = $has_live_funnel && $has_order_bump && $has_real_order;
505
506 // ── Build checklist step completion data ──
507 $completed_count = 0;
508 $steps_status = array(
509 'store_checkout_created' => $has_store_checkout_created,
510 'funnel_created' => $has_funnel_created,
511 'order_bump_added' => $has_order_bump,
512 'upsell_added' => $has_upsell,
513 'test_order_placed' => $has_real_order,
514 'funnel_live' => $has_live_funnel,
515 );
516
517 foreach ($steps_status as $status) {
518 if ($status) {
519 $completed_count++;
520 }
521 }
522
523 $total_steps = count($steps_status);
524 $progress_percent = $total_steps > 0 ? round(($completed_count / $total_steps) * 100) : 0;
525
526 return rest_ensure_response(array(
527 'success' => true,
528 'show_onboarding' => !$should_hide_onboarding,
529 'steps' => $steps_status,
530 'completed_count' => $completed_count,
531 'total_steps' => $total_steps,
532 'progress_percent' => $progress_percent,
533 'order_bump_url' => $order_bump_url,
534 'funnel_builder_url' => $funnel_builder_url,
535 ));
536 }
537
538 /**
539 * Check if there are any funnels
540 *
541 * @param \WP_REST_Request $request
542 * @return \WP_REST_Response
543 *
544 * @since 3.9.5
545 */
546 public function check_has_funnels( \WP_REST_Request $request ) {
547 global $wpdb;
548
549 // Count only non-store-checkout funnels
550 $funnel_count = $wpdb->get_var(
551 $wpdb->prepare(
552 "SELECT COUNT(*) FROM {$wpdb->posts} p
553 LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = '_wpfnl_funnel_type'
554 WHERE p.post_type = %s
555 AND p.post_status IN ('publish', 'draft')
556 AND (pm.meta_value IS NULL OR pm.meta_value != 'store_checkout')",
557 'wpfunnels'
558 )
559 );
560
561 return rest_ensure_response( array(
562 'success' => true,
563 'has_funnels' => (int) $funnel_count > 0,
564 'funnel_count' => (int) $funnel_count
565 ) );
566 }
567
568 /**
569 * Clear all dashboard transient caches so the next request fetches fresh data.
570 *
571 * @param \WP_REST_Request $request
572 * @return \WP_REST_Response
573 *
574 * @since 3.9.6
575 */
576 public function sync_cache( \WP_REST_Request $request ) {
577 self::delete_dashboard_cache();
578 return rest_ensure_response( array(
579 'success' => true,
580 'message' => __( 'Dashboard cache cleared. Data will refresh on next load.', 'wpfnl' ),
581 ) );
582 }
583
584
585 /**
586 * Dismiss the onboarding checklist
587 *
588 * @param \WP_REST_Request $request
589 * @return \WP_REST_Response
590 *
591 * @since 3.9.5
592 */
593 public function dismiss_onboarding( \WP_REST_Request $request ) {
594 // Store dismissal in a transient for 30 days
595 set_transient( 'wpfnl_onboarding_dismissed', true, 30 * DAY_IN_SECONDS );
596
597 return rest_ensure_response( array(
598 'success' => true,
599 'message' => 'Onboarding dismissed successfully'
600 ) );
601 }
602 }
603