includes/abstracts/abstract.llms.analytics.widget.php
<?php
/**
* Analytics Widget Abstract
*
* @package LifterLMS/Abstracts/Classes
*
* @since 3.0.0
* @version 7.3.0
*/
defined( 'ABSPATH' ) || exit;
/**
* Analytics Widget abstract class.
*
* @since 3.0.0
* @since 3.30.3 Define undefined properties.
* @since 3.33.1 In `set_order_data_query()` always set $order_clause variable to avoid PHP notices.
* @since 3.35.0 Sanitize input data from reporting filters.
* @since 3.36.3 Avoid warnings on using wpdb::prepare without placeholders.
* @since 4.0.0 Remove previously deprecated class properties: `$date_end`, `$date_start`, & `$date_end`.
*/
abstract class LLMS_Analytics_Widget {
/**
* @var array
* @since 3.0.0
*/
public $chart_data;
/**
* @var bool
* @since 3.0.0
*/
public $charts = false;
/**
* @var string
* @since 3.0.0
*/
public $message = '';
/**
* One of the wpdb constants: OBJECT, OBJECT_K, ARRAY_A, or ARRAY_N
*
* @var string
* @since 3.0.0
*/
protected $output_type;
/**
* @var string
* @since 3.0.0
*/
protected $prepared_query;
/**
* @var string
* @since 3.0.0
*/
protected $query;
/**
* @var string
* @since 3.0.0
*/
protected $query_function;
/**
* @var array
* @since 1.0.0
*/
protected $query_vars;
/**
* @var int
* @since 3.0.0
*/
public $response;
/**
* @var array
* @since 1.0.0
*/
public $results = array();
/**
* @var bool
* @since 3.0.0
*/
public $success = false;
abstract protected function format_response();
abstract protected function set_query();
protected function get_chart_data() {
return array(
'type' => 'count',
'header' => array(
'id' => '',
'label' => '',
'type' => 'string',
),
);
}
public function __construct() {}
/**
* Retrieve posted dates.
*
* @since 3.0.0
* @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.
*
* @return string[]|string
*/
protected function get_posted_dates() {
$dates = llms_filter_input_sanitize_string( INPUT_POST, 'dates', array( FILTER_REQUIRE_ARRAY ) );
return $dates ? $dates : '';
}
protected function get_posted_courses() {
$courses = llms_filter_input( INPUT_POST, 'courses', FILTER_SANITIZE_NUMBER_INT, FILTER_REQUIRE_ARRAY );
return $courses ? $courses : array();
}
protected function get_posted_memberships() {
$memberships = llms_filter_input( INPUT_POST, 'memberships', FILTER_SANITIZE_NUMBER_INT, FILTER_REQUIRE_ARRAY );
return $memberships ? $memberships : array();
}
protected function get_posted_posts() {
return array_merge( $this->get_posted_courses(), $this->get_posted_memberships() );
}
protected function get_posted_students() {
$students = llms_filter_input( INPUT_POST, 'students', FILTER_SANITIZE_NUMBER_INT, FILTER_REQUIRE_ARRAY );
return $students ? $students : array();
}
protected function get_prepared_query() {
return $this->prepared_query;
}
protected function get_query() {
return $this->query;
}
protected function get_query_vars() {
return $this->query_vars;
}
protected function get_results() {
return $this->results;
}
protected function format_date( $date, $type ) {
switch ( $type ) {
case 'start':
$date .= ' 00:00:00';
break;
case 'end':
$date .= ' 23:23:59';
break;
}
return $date;
}
protected function is_error() {
return ( $this->success ) ? false : true;
}
protected function set_order_data_query( $args = array() ) {
extract(
wp_parse_args(
$args,
array(
'select' => array( '*' ),
'date_range' => true, // whether or not to add a "where" for the posted date range.
'date_field' => 'post_date',
'query_function' => 'get_results', // query function to pass to $wpdb->query().
'output_type' => OBJECT,
'joins' => array(), // array of JOIN statements.
'statuses' => array(), // array of order statuses to query.
'wheres' => array(), // array of "WHERE" statements.
'order' => 'ASC',
'orderby' => '',
)
)
);
$this->query_function = $query_function;
$this->query_vars = array();
$this->output_type = $output_type;
global $wpdb;
// setup student join & where clauses.
$students = $this->get_posted_students();
$students_join = '';
$students_where = '';
if ( $students ) {
$students_join = "JOIN {$wpdb->postmeta} AS m1 ON orders.ID = m1.post_id";
$students_where .= "AND m1.meta_key = '_llms_user_id'";
$students_where .= ' AND m1.meta_value IN ( ' . implode( ', ', $students ) . ' )';
}
// setup post (product) joins & where clauses.
$posts = $this->get_posted_posts();
$products_join = '';
$products_where = '';
if ( $posts ) {
$products_join = "JOIN {$wpdb->postmeta} AS m2 ON orders.ID = m2.post_id";
$products_where .= "AND m2.meta_key = '_llms_product_id'";
$products_where .= ' AND m2.meta_value IN ( ' . implode( ', ', $posts ) . ' )';
}
$order_dates = '';
if ( $date_range ) {
$dates = $this->get_posted_dates();
$order_dates = "AND orders.{$date_field} BETWEEN CAST( %s AS DATETIME ) AND CAST( %s AS DATETIME )";
$this->query_vars[] = $this->format_date( $dates['start'], 'start' );
$this->query_vars[] = $this->format_date( $dates['end'], 'end' );
}
// setup post status conditions in the where clause.
$post_statuses = '';
if ( $statuses ) {
$post_statuses .= ' AND ( ';
foreach ( $statuses as $i => $status ) {
if ( $i > 0 ) {
$post_statuses .= ' OR ';
}
$post_statuses .= 'post_status = %s';
$this->query_vars[] = $status;
}
$post_statuses .= ' )';
}
// setup the select clause.
$select_clause = '';
foreach ( $select as $i => $s ) {
if ( $i > 0 ) {
$select_clause .= ', ';
}
$select_clause .= $s;
}
$joins_clause = '';
foreach ( $joins as $join ) {
$joins_clause .= $join . "\r\n";
}
$wheres_clause = '';
foreach ( $wheres as $where ) {
$wheres_clause .= $where . "\r\n";
}
$order_clause = '';
if ( $order && $orderby ) {
$order_clause = 'ORDER BY ' . $orderby . ' ' . $order;
}
$this->query = "SELECT {$select_clause}
FROM {$wpdb->posts} AS orders
{$students_join}
{$products_join}
{$joins_clause}
WHERE orders.post_type = 'llms_order'
{$order_dates}
{$post_statuses}
{$students_where}
{$products_where}
{$wheres_clause}
{$order_clause}
;";
}
/**
* Perform the query.
*
* @since unknown.
* @since 3.36.3 Avoid warnings on using wpdb::prepare without placeholders.
*/
protected function query() {
global $wpdb;
// Roughly avoid warnings on using wpdb::prepare without placeholders.
// The following strpos simple check is the same wpdb::prepare() does to check the correct usage.
if ( strpos( $this->query, '%' ) === false || empty( $this->query_vars ) ) {
$query = $this->query;
} else {
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- It is prepared.
$query = $wpdb->prepare( $this->query, $this->query_vars );
// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
}
// no output options.
if ( in_array( $this->query_function, array( 'get_var', 'get_col' ), true ) ) {
$this->results = $wpdb->{$this->query_function}( $query );
} else {
$this->results = $wpdb->{$this->query_function}( $query, $this->output_type );
}
$this->prepared_query = trim( str_replace( array( "\r", "\n", "\t", ' ' ), ' ', $wpdb->last_query ) );
if ( ! $wpdb->last_error ) {
$this->success = true;
$this->message = 'success';
} else {
$this->message = $wpdb->last_error;
}
}
/**
* Whether or not the current widget can be processed/displayed.
*
* @since 7.3.0
*
* @return true|WP_Error True if the widget can be processed, `WP_Error` otherwise.
*/
protected function _can_be_processed() { // phpcs:ignore -- PSR2.Methods.MethodDeclaration.Underscore.
$can_be_processed = true;
if ( ! current_user_can( 'view_others_lifterlms_reports' ) ) {
$can_be_processed = new WP_Error(
WP_Http::FORBIDDEN, // 403.
esc_html__( 'You are not authorized to access the requested widget', 'lifterlms' )
);
}
return $can_be_processed;
}
/**
* Whether or not the current widget can be processed/displayed.
*
* @since 7.3.0
*
* @return true|WP_Error
*/
public function can_be_processed() {
$widget_name = str_replace(
array( 'llms_analytics_', '_widget' ),
'',
strtolower( get_class( $this ) )
);
/**
* Whether or not the current widget can be processed/displayed.
*
* @param true|WP_Error True if the widget can be processed, `WP_Error` otherwise.
* @param string The widget name.
* @param LLMS_Analytics_Widget The instance extending `LLMS_Analytics_Widget`.
*/
return apply_filters(
'llms_can_analytics_widget_be_processed',
$this->_can_be_processed(),
$widget_name,
$this
);
}
/**
* Output widget.
*
* @since 3.0.0
* @since 7.3.0 Use `wp_json_encode` in place of the deprecated `json_encode`.
*
* @return void
*/
public function output() {
$this->set_query();
$this->query();
$this->response = $this->format_response();
if ( $this->charts ) {
$this->chart_data = $this->get_chart_data();
}
header( 'Content-Type: application/json' );
echo wp_json_encode( $this );
wp_die();
}
}