# wpfunnels/3.13.1/includes/core/rest-api/Controllers/DashboardController.php

WPFunnels – Funnel Builder for WooCommerce with Checkout &amp; One Click Upsell, version 3.13.1. 614 lines.

- Page: https://pluginprobe.com/plugins/wpfunnels/3.13.1/code/includes/core/rest-api/Controllers/DashboardController.php
- Raw: https://pluginprobe.com/plugins/wpfunnels/3.13.1/raw/includes/core/rest-api/Controllers/DashboardController.php
- Modified: 2026-09-15T04:31:16+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/wpfunnels/3.13.1/code/includes/core/rest-api/Controllers/DashboardController.php#L10-L20`.

```php
<?php


namespace WPFunnels\Rest\Controllers;

use WPFunnels\Report\ReportGenerator;
use WPFunnels\Wpfnl_functions;

/**
 *
 *
 * Class DashboardController
 * @package WPFunnels\Rest\Controllers
 * @since 3.2.0
 */
class DashboardController extends Wpfnl_REST_Controller
{

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 * @since 3.2.0
	 */
	protected $namespace = 'wpfunnels/v1';


	/**
	 * Route base.
	 *
	 * @var string
	 * @since 3.2.0
	 */
	protected $rest_base = 'report';


	/**
	 * Makes sure the current user has access to READ the settings APIs.
	 *
	 *
	 * @param $request
	 * @return \WP_Error|boolean
	 * @since  3.2.0
	 */
	public function get_items_permissions_check($request)
	{
		if (!Wpfnl_functions::wpfnl_rest_check_manager_permissions('settings')) {
			return new \WP_Error('wpfunnels_rest_cannot_edit', __('Sorry, you cannot list resources.', 'wpfnl'), array('status' => rest_authorization_required_code()));
		}
		return true;
	}


	/**
	 * Register rest routes
	 *
	 * @since 3.2.0
	 */
	public function register_routes()
	{
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/overview',
			array(
				array(
				'methods' => \WP_REST_Server::READABLE,
				'args' => $this->get_stats_args(),
				'callback' => array($this, 'get_overview'),
				'permission_callback' => array($this, 'get_items_permissions_check'),
			),
		)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/stats',
			array(
				array(
				'methods' => \WP_REST_Server::READABLE,
				'args' => $this->get_stats_args(),
				'callback' => array($this, 'get_stats'),
				'permission_callback' => array($this, 'get_items_permissions_check'),
			),
		)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/top-funnels',
			array(
				array(
				'methods' => \WP_REST_Server::READABLE,
				'args' => $this->get_stats_args(),
				'callback' => array($this, 'get_top_funnels'),
				'permission_callback' => array($this, 'get_items_permissions_check'),
			),
		)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/onboarding-status',
			array(
				array(
				'methods' => \WP_REST_Server::READABLE,
				'callback' => array($this, 'get_onboarding_status'),
				'permission_callback' => array($this, 'get_items_permissions_check'),
			),
		)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/has-funnels',
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array($this, 'check_has_funnels'),
					'permission_callback' => array( $this,  'get_items_permissions_check' ),
				),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/dismiss-onboarding',
			array(
				array(
					'methods'             => \WP_REST_Server::CREATABLE,
					'callback'            => array($this, 'dismiss_onboarding'),
					'permission_callback' => array( $this,  'get_items_permissions_check' ),
				),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/sync-cache',
			array(
				array(
					'methods'             => \WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'sync_cache' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
				),
			)
		);
	}

	/**
	 * Get stats arguments
	 *
	 * @return mixed|void
	 * @since 3.2.0
	 */
	public function get_stats_args()
	{
		return array(
			'after' => array(
				'type' => 'string',
				'format' => 'date-time',
				'validate_callback' => 'rest_validate_request_arg',
			),
			'before' => array(
				'type' => 'string',
				'format' => 'date-time',
				'validate_callback' => 'rest_validate_request_arg',
			),
			'interval' => array(
				'type' => 'string',
				'default' => 'week',
				'enum' => array(
					'hour',
					'day',
					'week',
					'month',
					'quarter',
					'year',
				),
			),
		);
	}


	/**
	 * Cache payload version. Bump whenever the shape or the meaning of a cached
	 * report response changes, so entries written by an older build are ignored
	 * instead of served until they expire.
	 *
	 * @var string
	 * @since 3.13.0
	 */
	const CACHE_VERSION = 'v2';


	/**
	 * Build a cache key from a prefix and parameter array.
	 *
	 * @param string $prefix
	 * @param array  $params
	 * @return string
	 *
	 * @since 3.9.6
	 */
	protected function get_cache_key( $prefix, $params ) {
		return 'wpfnl_dash_' . $prefix . '_' . self::CACHE_VERSION . '_' . md5( serialize( $params ) );
	}


	/**
	 * Delete all dashboard transients from the database.
	 * Called by the sync-cache endpoint or when cache must be invalidated.
	 *
	 * @since 3.9.6
	 */
	public static function delete_dashboard_cache() {
		global $wpdb;
		$wpdb->query(
			"DELETE FROM {$wpdb->options}
			 WHERE option_name LIKE '_transient_wpfnl_dash_%'
			    OR option_name LIKE '_transient_timeout_wpfnl_dash_%'"
		);
	}


	/**
	 * Get overview data of funnels (with 24-hour server-side cache).
	 *
	 * @param \WP_REST_Request $request
	 * @return \WP_Error|\WP_REST_Response
	 * @throws \Exception
	 *
	 * @since 3.2.0
	 */
	public function get_overview(\WP_REST_Request $request)
	{
		$params     = $request->get_params();
		$start_date = isset($params['after'])  ? $params['after']  : $this->default_after()->format('Y-m-d H:i:s');
		$end_date   = isset($params['before']) ? $params['before'] : $this->default_before()->format('Y-m-d H:i:s');

		$cache_key = $this->get_cache_key( 'ov', array( $start_date, $end_date ) );
		$cached    = get_transient( $cache_key );
		if ( false !== $cached ) {
			return rest_ensure_response( $cached );
		}

		$response = ReportGenerator::get_overview( $start_date, $end_date );
		set_transient( $cache_key, $response, DAY_IN_SECONDS );

		return rest_ensure_response( $response );
	}


	/**
	 * Get stats of the funnels with intervals period (with 24-hour server-side cache).
	 *
	 * @param \WP_REST_Request $request
	 * @return \WP_Error|\WP_REST_Response
	 * @throws \Exception
	 *
	 * @since 3.2.0
	 */
	public function get_stats(\WP_REST_Request $request)
	{
		$params     = $request->get_params();
		$start_date = isset($params['after'])    ? $params['after']    : $this->default_after()->format('Y-m-d H:i:s');
		$end_date   = isset($params['before'])   ? $params['before']   : $this->default_before()->format('Y-m-d H:i:s');
		$interval   = isset($params['interval']) ? $params['interval'] : 'day';

		$cache_key = $this->get_cache_key( 'st', array( $start_date, $end_date, $interval ) );
		$cached    = get_transient( $cache_key );
		if ( false !== $cached ) {
			return rest_ensure_response( $cached );
		}

		$response = ReportGenerator::get_stats( $start_date, $end_date, $interval );
		set_transient( $cache_key, $response, DAY_IN_SECONDS );

		return rest_ensure_response( $response );
	}


	/**
	 * Get 3 top performing funnels
	 *
	 * @param \WP_REST_Request $request
	 * @return \WP_Error|\WP_REST_Response
	 *
	 * @since 3.2.0
	 */
	public function get_top_funnels(\WP_REST_Request $request)
	{
		$params     = $request->get_params();
		$start_date = isset( $params['after'] )  ? $params['after']  : $this->default_after()->format( 'Y-m-d H:i:s' );
		$end_date   = isset( $params['before'] ) ? $params['before'] : $this->default_before()->format( 'Y-m-d H:i:s' );

		$response['status'] = true;
		$response['data']   = ReportGenerator::get_top_funnels( $start_date, $end_date );
		return rest_ensure_response( $response );
	}


	/**
	 * Get onboarding status.
	 *
	 * Determines whether the onboarding section should be shown on the dashboard.
	 * The onboarding is hidden when ALL three conditions are met:
	 *   1. At least one funnel is published (live)
	 *   2. At least one funnel contains an order bump (upsell is optional/bonus)
	 *   3. At least one real order has been placed through any funnel
	 *
	 * Also returns individual step completion states for the checklist.
	 *
	 * @return \WP_REST_Response
	 * @since 3.9.5
	 */
	public function get_onboarding_status()
	{
		// ── Check if onboarding has been dismissed ──
		$is_dismissed = get_transient( 'wpfnl_onboarding_dismissed' );
		if ( $is_dismissed ) {
			return rest_ensure_response( array(
				'success' => true,
				'show_onboarding' => false,
				'steps' => array(),
				'completed_count' => 0,
				'total_steps' => 0,
				'progress_percent' => 0,
			) );
		}

		$is_wc_active = class_exists('WooCommerce');
		$has_live_funnel = false;
		$has_order_bump = false;
		$has_upsell = false;
		$has_real_order = false;
		$has_funnel_created = false;
		$has_store_checkout_created = false;

		// Meta query to exclude store checkout funnels
		$exclude_sc_meta = array(
			'relation' => 'OR',
			array(
				'key'     => '_wpfnl_funnel_type',
				'compare' => 'NOT EXISTS',
			),
			array(
				'key'     => '_wpfnl_funnel_type',
				'value'   => 'store_checkout',
				'compare' => '!=',
			),
		);

		// ── Step 1: Check for published funnels (excluding store checkouts) ──
		$funnels = get_posts(array(
			'post_type' => WPFNL_FUNNELS_POST_TYPE,
			'post_status' => 'publish',
			'numberposts' => -1,
			'fields' => 'ids',
			'meta_query' => $exclude_sc_meta,
		));

		if (!empty($funnels)) {
			$has_funnel_created = true;
			$has_live_funnel = true;
		}

		// Also check for draft funnels (counts as "created" but not "live")
		if (!$has_funnel_created) {
			$draft_funnels = get_posts(array(
				'post_type' => WPFNL_FUNNELS_POST_TYPE,
				'post_status' => array('publish', 'draft'),
				'numberposts' => 1,
				'fields' => 'ids',
				'meta_query' => $exclude_sc_meta,
			));
			$has_funnel_created = !empty($draft_funnels);
		}

		// ── Check for store checkout funnels separately ──
		$sc_funnels = get_posts(array(
			'post_type'   => WPFNL_FUNNELS_POST_TYPE,
			'post_status' => array('publish', 'draft'),
			'numberposts' => 1,
			'fields'      => 'ids',
			'meta_query'  => array(
				array(
					'key'   => '_wpfnl_funnel_type',
					'value' => 'store_checkout',
				),
			),
		));
		$has_store_checkout_created = !empty($sc_funnels);

		// ── Step 2: Check for order bump & upsell across all published funnels ──
		if (!empty($funnels)) {
			foreach ($funnels as $funnel_id) {
				$steps = Wpfnl_functions::get_steps($funnel_id);

				if (!is_array($steps) || empty($steps)) {
					continue;
				}

				foreach ($steps as $step) {
					$step_id = isset($step['id']) ? $step['id'] : 0;
					$step_type = isset($step['step_type']) ? $step['step_type'] : '';

					// Check for order bump on checkout steps
					if ('checkout' === $step_type && !$has_order_bump) {
						$ob_settings = get_post_meta($step_id, 'order-bump-settings', true);
						if (!empty($ob_settings) && is_array($ob_settings)) {
							$is_multidimensional = Wpfnl_functions::check_array_is_multidimentional($ob_settings);
							if ($is_multidimensional) {
								foreach ($ob_settings as $ob) {
									if (!empty($ob['product']) && !empty($ob['isEnabled'])) {
										$has_order_bump = true;
										break;
									}
								}
							}
							else {
								// Single OB (legacy format)
								if (!empty($ob_settings['product'])) {
									$has_order_bump = true;
								}
							}
						}
					}

					// Check for upsell step
					if ('upsell' === $step_type) {
						$has_upsell = true;
					}
				}

				// If we already found both, no need to check more funnels
				if ($has_order_bump && $has_upsell) {
					break;
				}
			}
		}

		// ── Build the checklist deep links ──
		// The checklist actions should drop the user into the place where the step
		// actually gets done, rather than the funnel list: the order bump tab of the
		// newest funnel's checkout step, and the canvas of that funnel for the upsell.
		// Drafts count too — a funnel does not need to be live to be worked on.
		$order_bump_url     = '';
		$funnel_builder_url = '';
		$recent_funnels     = get_posts(array(
			'post_type'   => WPFNL_FUNNELS_POST_TYPE,
			'post_status' => array('publish', 'draft'),
			'numberposts' => 10,
			'orderby'     => 'date',
			'order'       => 'DESC',
			'fields'      => 'ids',
			'meta_query'  => $exclude_sc_meta,
		));

		if (!empty($recent_funnels)) {
			// The upsell step is added on the canvas, so that is where its action goes.
			$funnel_builder_url = admin_url(
				sprintf('admin.php?page=edit_funnel&id=%d', $recent_funnels[0])
			);
		}

		foreach ($recent_funnels as $recent_funnel_id) {
			$recent_steps = Wpfnl_functions::get_steps($recent_funnel_id);

			if (!is_array($recent_steps) || empty($recent_steps)) {
				continue;
			}

			foreach ($recent_steps as $recent_step) {
				$recent_step_type = isset($recent_step['step_type']) ? $recent_step['step_type'] : '';
				$recent_step_id   = isset($recent_step['id']) ? (int)$recent_step['id'] : 0;

				if ('checkout' !== $recent_step_type || !$recent_step_id) {
					continue;
				}

				// The hash is what tells the canvas which step settings drawer and
				// tab to open once the funnel editor has finished loading.
				$order_bump_url = admin_url(
					sprintf(
						'admin.php?page=edit_funnel&id=%d&step_id=%d#%d/settings/order-bump/',
						$recent_funnel_id,
						$recent_step_id,
						$recent_step_id
					)
				);
				break 2;
			}
		}

		// ── Step 3: Check for real orders through funnels ──
		if ($is_wc_active && !empty($funnels)) {
			global $wpdb;
			$stats_table = $wpdb->prefix . 'wpfnl_stats';

			// Check if stats table exists
			$table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $stats_table));
			if ($table_exists) {
				$order_count = $wpdb->get_var(
					"SELECT COUNT(id) FROM {$stats_table} WHERE status IN ('completed', 'processing') LIMIT 1"
				);
				$has_real_order = (int)$order_count > 0;
			}
		}

		// ── Determine if onboarding should be shown ──
		// Hide onboarding only when ALL conditions are met:
		//   - At least 1 live funnel
		//   - Has an order bump (upsell is optional)
		//   - Has at least 1 real order
		$should_hide_onboarding = $has_live_funnel && $has_order_bump && $has_real_order;

		// ── Build checklist step completion data ──
		$completed_count = 0;
		$steps_status = array(
			'store_checkout_created' => $has_store_checkout_created,
			'funnel_created' => $has_funnel_created,
			'order_bump_added' => $has_order_bump,
			'upsell_added' => $has_upsell,
			'test_order_placed' => $has_real_order,
			'funnel_live' => $has_live_funnel,
		);

		foreach ($steps_status as $status) {
			if ($status) {
				$completed_count++;
			}
		}

		$total_steps = count($steps_status);
		$progress_percent = $total_steps > 0 ? round(($completed_count / $total_steps) * 100) : 0;

		return rest_ensure_response(array(
			'success' => true,
			'show_onboarding' => !$should_hide_onboarding,
			'steps' => $steps_status,
			'completed_count' => $completed_count,
			'total_steps' => $total_steps,
			'progress_percent' => $progress_percent,
			'order_bump_url' => $order_bump_url,
			'funnel_builder_url' => $funnel_builder_url,
		));
	}

	/**
	 * Check if there are any funnels
	 *
	 * @param \WP_REST_Request $request
	 * @return \WP_REST_Response
	 *
	 * @since 3.9.5
	 */
	public function check_has_funnels( \WP_REST_Request $request ) {
		global $wpdb;

		// Count only non-store-checkout funnels
		$funnel_count = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COUNT(*) FROM {$wpdb->posts} p
				 LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = '_wpfnl_funnel_type'
				 WHERE p.post_type = %s
				 AND p.post_status IN ('publish', 'draft')
				 AND (pm.meta_value IS NULL OR pm.meta_value != 'store_checkout')",
				'wpfunnels'
			)
		);

		return rest_ensure_response( array(
			'success' => true,
			'has_funnels' => (int) $funnel_count > 0,
			'funnel_count' => (int) $funnel_count
		) );
	}

	/**
	 * Clear all dashboard transient caches so the next request fetches fresh data.
	 *
	 * @param \WP_REST_Request $request
	 * @return \WP_REST_Response
	 *
	 * @since 3.9.6
	 */
	public function sync_cache( \WP_REST_Request $request ) {
		self::delete_dashboard_cache();
		return rest_ensure_response( array(
			'success' => true,
			'message' => __( 'Dashboard cache cleared. Data will refresh on next load.', 'wpfnl' ),
		) );
	}


	/**
	 * Dismiss the onboarding checklist
	 *
	 * @param \WP_REST_Request $request
	 * @return \WP_REST_Response
	 *
	 * @since 3.9.5
	 */
	public function dismiss_onboarding( \WP_REST_Request $request ) {
		// Store dismissal in a transient for 30 days
		set_transient( 'wpfnl_onboarding_dismissed', true, 30 * DAY_IN_SECONDS );

		return rest_ensure_response( array(
			'success' => true,
			'message' => 'Onboarding dismissed successfully'
		) );
	}
}

```
