includes/admin/reporting/class.llms.admin.reporting.php
<?php
/**
* Admin Reporting Base Class
*
* @package LifterLMS/Admin/Reporting/Classes
*
* @since 3.2.0
* @version 7.3.0
*/
defined( 'ABSPATH' ) || exit;
/**
* Admin Reporting Base class.
*
* @since 3.2.0
* @since 3.31.0 Fix redundant `if` statement in the `output_widget` method.
* @since 3.32.0 Added Memberships tab.
* @since 3.32.0 The `output_event()` method now outputs the student's avatar whent in 'membership' context.
* @since 3.35.0 Sanitize input data.
* @since 3.36.3 Fixed sanitization for input data array.
*/
class LLMS_Admin_Reporting {
/**
* Constructor.
*
* @since 3.2.0
*/
public function __construct() {
self::includes();
}
/**
* Get array of course IDs selected according to applied filters.
*
* @since 3.2.0
* @since 3.35.0 Sanitize input data.
* @since 3.36.3 Fixed sanitization for input data array.
* @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.
*
* @return array
*/
public static function get_current_courses() {
$r = isset( $_GET['course_ids'] ) ? llms_filter_input( INPUT_GET, 'course_ids', FILTER_SANITIZE_NUMBER_INT, FILTER_REQUIRE_ARRAY ) : array();
if ( '' === $r ) {
$r = array();
}
if ( is_string( $r ) ) {
$r = array_map( 'absint', explode( ',', $r ) );
}
return $r;
}
/**
* Get array of membership IDs selected according to applied filters.
*
* @since 3.2.0
* @since 3.35.0 Sanitize input data.
* @since 3.36.3 Fixed sanitization for input data array.
* @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.
*
* @return array
*/
public static function get_current_memberships() {
$r = isset( $_GET['membership_ids'] ) ? llms_filter_input( INPUT_GET, 'membership_ids', FILTER_SANITIZE_NUMBER_INT, FILTER_REQUIRE_ARRAY ) : array();
if ( '' === $r ) {
$r = array();
}
if ( is_string( $r ) ) {
$r = array_map( 'absint', explode( ',', $r ) );
}
return $r;
}
/**
* Get the currently selected date range filter.
*
* @since 3.2.0
* @since 3.35.0 Sanitize input data.
* @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.
*
* @return string
*/
public static function get_current_range() {
return ( isset( $_GET['range'] ) ) ? llms_filter_input_sanitize_string( INPUT_GET, 'range' ) : 'last-7-days';
}
/**
* Get array of student IDs according to current filters.
*
* @since 3.2.0
* @since 3.35.0 Sanitize input data.
* @since 3.36.3 Fixed sanitization for input data array.
* @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.
*
* @return array
*/
public static function get_current_students() {
$r = isset( $_GET['student_ids'] ) ? llms_filter_input( INPUT_GET, 'student_ids', FILTER_SANITIZE_NUMBER_INT, FILTER_REQUIRE_ARRAY ) : array();
if ( '' === $r ) {
$r = array();
}
if ( is_string( $r ) ) {
$r = array_map( 'absint', explode( ',', $r ) );
}
return $r;
}
/**
* Retrieve the current reporting tab.
*
* @since 3.2.0
* @since 3.35.0 Sanitize input data.
* @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.
*
* @return string
*/
public static function get_current_tab() {
return isset( $_GET['tab'] ) ? llms_filter_input_sanitize_string( INPUT_GET, 'tab' ) : 'students';
}
/**
* Get the current end date according to filters.
*
* @since 3.2.0
* @since 3.35.0 Sanitize input data.
* @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.
*
* @return string
*/
public static function get_date_end() {
return ( isset( $_GET['date_end'] ) ) ? llms_filter_input_sanitize_string( INPUT_GET, 'date_end' ) : '';
}
/**
* Get the current start date according to filters.
*
* @since 3.2.0
* @since 3.35.0 Sanitize input data.
* @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.
*
* @return string
*/
public static function get_date_start() {
return ( isset( $_GET['date_start'] ) ) ? llms_filter_input_sanitize_string( INPUT_GET, 'date_start' ) : '';
}
/**
* Get dates via the current date string.
*
* @since 3.2.0
*
* @param string $range Date range string.
* @return array
*/
public static function get_dates( $range ) {
$now = current_time( 'timestamp' );
$dates = array(
'start' => '',
'end' => date( 'Y-m-d', $now ),
);
switch ( $range ) {
case 'this-year':
$dates['start'] = date( 'Y', $now ) . '-01-01';
break;
case 'last-month':
$dates['start'] = date( 'Y-m-d', strtotime( 'first day of last month', $now ) );
$dates['end'] = date( 'Y-m-d', strtotime( 'last day of last month', $now ) );
break;
case 'this-month':
$dates['start'] = date( 'Y-m', $now ) . '-01';
break;
case 'last-7-days':
$dates['start'] = date( 'Y-m-d', strtotime( '-7 days', $now ) );
break;
case 'custom':
$dates['start'] = self::get_date_start();
$dates['end'] = self::get_date_end();
break;
}
return $dates;
}
/**
* Returns an admin URL with the given arguments added as query variables.
*
* @since 3.2.0
*
* @param array $args Arguments to add to the query string.
* @return string
*/
public static function get_current_tab_url( $args = array() ) {
$args = wp_parse_args(
$args,
array(
'page' => 'llms-reporting',
'tab' => self::get_current_tab(),
)
);
return add_query_arg( $args, admin_url( 'admin.php' ) );
}
/**
* Retrieves arguments for {@see LifterLMS_Admin_Reporting::output_widget}.
*
* Merges the supplied arguments with the default args.
*
* @since 6.11.0
*
* @param array $args Widget settings and data, {@see LifterLMS_Adming_Reporting::output_widget}.
* @return array Merged arguments.
*/
private static function get_output_widget_args( $args = array() ) {
return wp_parse_args(
$args,
array(
'id' => '',
'text' => '',
'data' => '',
'data_compare' => '',
'data_type' => 'numeric', // Enum: numeric, monetary, text, percentage, or date.
'icon' => '',
'impact' => 'positive', // Enum: positive or negative.
'cols' => 'd-1of2',
)
);
}
/**
* Retrieve an array of period filters used by self::output_widget_range_filter().
*
* @since 3.16.0
*
* @return array
*/
public static function get_period_filters() {
return array(
'today' => esc_attr__( 'Today', 'lifterlms' ),
'yesterday' => esc_attr__( 'Yesterday', 'lifterlms' ),
'week' => esc_attr__( 'This Week', 'lifterlms' ),
'last_week' => esc_attr__( 'Last Week', 'lifterlms' ),
'month' => esc_attr__( 'This Month', 'lifterlms' ),
'last_month' => esc_attr__( 'Last Month', 'lifterlms' ),
'year' => esc_attr__( 'This Year', 'lifterlms' ),
'last_year' => esc_attr__( 'Last Year', 'lifterlms' ),
'all_time' => esc_attr__( 'All Time', 'lifterlms' ),
);
}
/**
* Get the full URL to a sub-tab within a reporting screen.
*
* @since 3.2.0
* @since 3.32.0 Added Memberships tab.
* @since 3.35.0 Sanitize input data.
*
* @param string $stab Slug of the sub-tab.
* @return string
*/
public static function get_stab_url( $stab ) {
$args = array(
'page' => 'llms-reporting',
'tab' => self::get_current_tab(),
'stab' => $stab,
);
switch ( self::get_current_tab() ) {
case 'memberships':
$args['membership_id'] = llms_filter_input( INPUT_GET, 'membership_id', FILTER_SANITIZE_NUMBER_INT );
break;
case 'courses':
$args['course_id'] = llms_filter_input( INPUT_GET, 'course_id', FILTER_SANITIZE_NUMBER_INT );
break;
case 'students':
$args['student_id'] = llms_filter_input( INPUT_GET, 'student_id', FILTER_SANITIZE_NUMBER_INT );
break;
case 'quizzes':
$args['quiz_id'] = llms_filter_input( INPUT_GET, 'quiz_id', FILTER_SANITIZE_NUMBER_INT );
break;
}
return add_query_arg( $args, admin_url( 'admin.php' ) );
}
/**
* Get an array of tabs to output in the main reporting menu.
*
* @since 3.2.0
* @since 3.32.0 Added Memberships tab.
*
* @return array
*/
private function get_tabs() {
$tabs = array(
'students' => __( 'Students', 'lifterlms' ),
'courses' => __( 'Courses', 'lifterlms' ),
'memberships' => __( 'Memberships', 'lifterlms' ),
'quizzes' => __( 'Quizzes', 'lifterlms' ),
'sales' => __( 'Sales', 'lifterlms' ),
'enrollments' => __( 'Enrollments', 'lifterlms' ),
);
foreach ( $tabs as $slug => $tab ) {
if ( ! current_user_can( $this->get_tab_cap( $slug ) ) ) {
unset( $tabs[ $slug ] );
}
}
return apply_filters( 'lifterlms_reporting_tabs', $tabs );
}
/**
* Get the WP capability required to access a reporting tab.
*
* Defaults to 'view_lifterlms_reports'. Most reports implement additional permissions within the view.
* Sales & Enrollments tab requires 'view_others_lifterlms_reports' b/c they don't add any additional filters
* within the view.
*
* @since 3.19.4
* @since 7.3.0 Use `in_array()` with strict type comparison.
*
* @param string $tab ID/slug of the tab.
* @return string
*/
private function get_tab_cap( $tab = null ) {
$tab = is_null( $tab ) ? self::get_current_tab() : $tab;
$cap = 'view_lifterlms_reports';
if ( in_array( $tab, array( 'sales', 'enrollments' ), true ) ) {
$cap = 'view_others_lifterlms_reports';
}
/**
* Filters the WP capability required to access a reporting tab.
*
* @since 3.19.4
* @since 7.3.0 Added the `$tab` parameter.
*
* @param string $cap The required WP capability.
* @param string|null $tab ID/slug of the tab.
*/
return apply_filters( 'lifterlms_reporting_tab_cap', $cap, $tab );
}
/**
* Retrieve an array of data to pass to the reporting page template.
*
* @since 3.2.0
*
* @return array
*/
private function get_template_data() {
return array(
'current_tab' => self::get_current_tab(),
'tabs' => $this->get_tabs(),
);
}
/**
* Include all required classes & files for the Reporting screens.
*
* @since 3.2.0
* @since 3.16.0 Unknown.
* @since 6.0.0 Removed loading of class files that don't instantiate their class in favor of autoloading.
*
* @return void
*/
public static function includes() {
// Include tab classes.
foreach ( glob( LLMS_PLUGIN_DIR . '/includes/admin/reporting/tabs/*.php' ) as $filename ) {
include_once $filename;
}
}
/**
* Output the reporting screen HTML.
*
* @since 3.2.0
* @since 3.19.4 Unknown.
*
* @return void
*/
public function output() {
if ( ! current_user_can( $this->get_tab_cap() ) ) {
wp_die( __( 'You don\'t have permission to do that', 'lifterlms' ) );
}
llms_get_template( 'admin/reporting/reporting.php', $this->get_template_data() );
}
/**
* Output the HTML for a postmeta event in the recent events sidebar of various reporting screens.
*
* @since 3.15.0
* @since 3.32.0 Outputs the student's avatar when in 'membership' context.
*
* @param LLMS_User_Postmeta $event Instance of an LLMS_User_Postmeta item.
* @param string $context Optional. Display context [course|student|quiz|membership]. Default 'course'.
* @return void
*/
public static function output_event( $event, $context = 'course' ) {
$student = $event->get_student();
if ( ! $student ) {
return;
}
$url = $event->get_link( $context );
?>
<div class="llms-reporting-event <?php echo $event->get( 'meta_key' ); ?> <?php echo $event->get( 'meta_value' ); ?>">
<?php if ( $url ) : ?>
<a href="<?php echo esc_url( $url ); ?>">
<?php endif; ?>
<?php if ( 'course' === $context || 'membership' === $context || 'quiz' === $context ) : ?>
<?php echo $student->get_avatar( 24 ); ?>
<?php endif; ?>
<?php echo $event->get_description( $context ); ?>
<time datetime="<?php echo $event->get( 'updated_date' ); ?>"><?php echo llms_get_date_diff( current_time( 'timestamp' ), $event->get( 'updated_date' ), 1 ); ?></time>
<?php if ( $url ) : ?>
</a>
<?php endif; ?>
</div>
<?php
}
/**
* Outputs the HTML for a reporting widget.
*
* @since 3.15.0
* @since 3.31.0 Remove redundant `if` statement.
* @since 6.11.0 Moved HTML into a view file.
* Fixed division by zero error encountered during data comparisons when `$data` is `0`.
* Added a check to ensure only numeric, monetary, or percentage data types will generate comparison data.
* @since 7.3.0 Better rounding of float values of percentage data types.
*
* @param array $args {
* Array of widget options and data to be displayed.
*
* @type string $id Required. A unique identifier for the widget.
* @type string $text A short description of the widget's data.
* @type int|string|float $data The value of the data to display.
* @type int|string|float $data_compare Additional data to compare $data against.
* @type string $data_type The type of data. Used to format displayed data. Accepts "numeric",
* "monetary", "text", "percentage", or "date".
* @type string $icon An optional Font Awesome icon used to help visually identify the widget.
* If supplied, should be supplied without the `fa-` icon prefix.
* @type string $impact The type of impact the data has, either "positive" or "negative". This
* is used when displaying comparisons to determine if the change was a positive
* change or negative change. For example: student enrollments has a positive
* impact while quiz failures has a negative impact. An increase in enrollments
* will be displayed in green while a decrease will be displayed in red. An
* increase in quiz failures will be displayed in red while a decrease will be
* displayed in green.
* @type string $cols Grid class widget width ID. See: assets/scss/admin/partials/_grid.scss.
* }
* @return void
*/
public static function output_widget( $args = array() ) {
$args = self::get_output_widget_args( $args );
// Only these data types can make comparisons.
$can_compare = in_array( $args['data_type'], array( 'numeric', 'monetary', 'percentage' ), true );
// Adds a percentage symbol after data.
$data_after = 'percentage' === $args['data_type'] && is_numeric( $args['data'] ) ? '<sup>%</sup>' : '';
$change = false;
$compare_operator = '';
$compare_class = '';
$compare_title = '';
$floating_precision = llms_get_floats_rounding_precision();
if ( $can_compare && $args['data_compare'] && floatval( $args['data'] ) ) {
$change = round( ( $args['data'] - $args['data_compare'] ) / $args['data'] * 100, $floating_precision );
$compare_operator = ( $change <= 0 ) ? '' : '+';
$compare_title = sprintf(
// Translators: %s = The value of the data from the previous data set.
esc_attr__( 'Previously %s', 'lifterlms' ),
round( $args['data_compare'], $floating_precision ) . wp_strip_all_tags( $data_after )
);
$compare_class = ( $change <= 0 ) ? 'negative' : 'positive';
if ( 'negative' === $args['impact'] ) {
$compare_class = ( $change <= 0 ) ? 'positive' : 'negative';
}
}
if ( is_numeric( $args['data'] ?? '' ) ) {
if ( 'percentage' === $args['data_type'] ) {
$args['data'] = round( $args['data'], $floating_precision );
} elseif ( 'monetary' === $args['data_type'] ) {
$args['data'] = llms_price( $args['data'] );
$args['data_compare'] = llms_price_raw( $args['data_compare'] );
}
}
$args['id'] = esc_attr( $args['id'] );
include LLMS_PLUGIN_DIR . 'includes/admin/views/reporting/widget.php';
}
/**
* Output a range filter select.
*
* Used by overview data tabs
*
* @since 3.16.0
*
* @param string $selected_period Currently selected period.
* @param string $tab Current tab name.
* @param array $args Additional args to be passed when form is submitted.
* @return void
*/
public static function output_widget_range_filter( $selected_period, $tab, $args = array() ) {
?>
<div class="llms-reporting-tab-filter">
<form action="<?php echo esc_url( admin_url( 'admin.php' ) ); ?>" method="GET">
<select class="llms-select2" name="period" onchange="this.form.submit();">
<?php foreach ( self::get_period_filters() as $val => $text ) : ?>
<option value="<?php echo $val; ?>"<?php selected( $val, $selected_period ); ?>><?php echo $text; ?></option>
<?php endforeach; ?>
</select>
<input type="hidden" name="page" value="llms-reporting">
<input type="hidden" name="tab" value="<?php echo $tab; ?>">
<?php foreach ( $args as $key => $val ) : ?>
<input type="hidden" name="<?php echo $key; ?>" value="<?php echo $val; ?>">
<?php endforeach; ?>
</form>
</div>
<?php
}
}