[], 'slimview2' => [], 'slimview3' => [], 'slimview4' => [], 'slimview5' => [], 'slimview6' => [], 'dashboard' => [], 'inactive' => [], ]; public static $resource_titles = []; /** * Initalize class properties */ public static function init() { // Has the class already been initialized? if (!empty(self::$reports)) { return true; } // Include and initialize the API to interact with the database include_once(__DIR__ . '/wp-slimstat-db.php'); wp_slimstat_db::init(); // Include the localization library include_once(plugin_dir_path(dirname(__FILE__, 2)) . 'languages/index.php'); // Define all the reports // // Parameters // - title : report name // - callback : function to use to render the report // - callback_args : parameters to pass to the function // - classes : determine the look and feel of this report ( tall, large, extralarge, full-width ) // - locations : where should the report appear ( slimview1, .., slimview4, dashboard ) // - tooltip : contextual help to be displayed on hover $chart_tooltip = '' . __('Chart Controls', 'wp-slimstat') . '
' . __('From search result page', 'wp-slimstat') . '
' . __('Has Left Comments', 'wp-slimstat') . '
' . __('WP User', 'wp-slimstat') . '
' . __('Other Human', 'wp-slimstat') . '
' . __('Bot or Crawler', 'wp-slimstat') . '
', ], 'slim_p1_01' => [ 'title' => __('Pageviews', 'wp-slimstat'), 'callback' => [self::class, 'show_chart'], 'callback_args' => [ 'id' => 'slim_p1_01', 'chart_data' => [ 'data1' => 'COUNT( ip )', 'data2' => 'COUNT( DISTINCT ip )', ], 'chart_labels' => [ __('Pageviews', 'wp-slimstat'), (('on' == (wp_slimstat::$settings['hash_ip'] ?? 'off')) ? __('Unique Visitors', 'wp-slimstat') : __('Unique IPs', 'wp-slimstat')), ], ], 'classes' => ['extralarge', 'chart'], 'locations' => ['slimview2', 'dashboard'], 'tooltip' => $pageviews_chart_tooltip, ], 'slim_p1_03' => [ 'title' => __('At a Glance', 'wp-slimstat'), 'callback' => [self::class, 'raw_results_to_html'], 'callback_args' => [ 'raw' => ['wp_slimstat_db', 'get_overview_summary'], ], 'classes' => ['normal'], 'locations' => ['slimview2', 'dashboard'], ], 'slim_p1_04' => [ 'title' => __('Currently Online', 'wp-slimstat'), 'callback' => [self::class, 'raw_results_to_html'], 'callback_args' => [ 'type' => 'top', 'columns' => 'ip', 'where' => '(dt_out > ' . (date_i18n('U') - 300) . ') OR (dt > ' . (date_i18n('U') - 300) . ')', 'order_by' => 'MAX(dt) DESC', 'more_select' => 'MAX(dt) AS dt', 'use_date_filters' => false, 'raw' => ['wp_slimstat_db', 'get_top'], ], 'classes' => ['normal'], 'locations' => ['slimview2', 'dashboard'], ], 'slim_p1_06' => [ 'title' => __('Recent Search Terms', 'wp-slimstat'), 'callback' => [self::class, 'raw_results_to_html'], 'callback_args' => [ 'type' => 'recent', 'columns' => 'searchterms', 'where' => 'searchterms <> "_" AND searchterms <> "" AND searchterms IS NOT NULL', 'more_columns' => 'referer, resource', 'raw' => ['wp_slimstat_db', 'get_recent'], ], 'classes' => ['normal'], 'locations' => ['slimview2', 'slimview5'], 'tooltip' => __('Keywords used by your visitors to find your website on a search engine.', 'wp-slimstat'), ], 'slim_p1_08' => [ 'title' => __('Top Web Pages', 'wp-slimstat'), 'callback' => [self::class, 'raw_results_to_html'], 'callback_args' => [ 'type' => 'top', 'columns' => 'resource', 'raw' => ['wp_slimstat_db', 'get_top'], ], 'classes' => ['normal'], 'locations' => ['slimview2', 'dashboard'], 'tooltip' => __('Here a "page" is not just a WordPress page type, but any webpage on your site, including posts, products, categories, and any other custom post type. For example, you can set the corresponding filter where Resource Content Type equals cpt:you_cpt_slug_here to get top web pages for a specific custom post type you have.', 'wp-slimstat'), ], 'slim_p1_10' => [ 'title' => __('Top Referring Domains', 'wp-slimstat'), 'callback' => [self::class, 'raw_results_to_html'], 'callback_args' => [ 'type' => 'top', 'columns' => 'REPLACE( SUBSTRING_INDEX( ( SUBSTRING_INDEX( ( SUBSTRING_INDEX( referer, "://", -1 ) ), "/", 1 ) ), ".", -5 ), "www.", "" )', 'as_column' => 'referer', 'filter_op' => 'contains', 'where' => 'referer NOT LIKE %s', 'where_params' => ['%' . str_replace('www.', '', parse_url(home_url(), PHP_URL_HOST)) . '%'], 'raw' => ['wp_slimstat_db', 'get_top'], ], 'classes' => ['normal'], 'locations' => ['slimview2', 'slimview5', 'dashboard'], ], 'slim_p1_11' => [ 'title' => __('Top Known Visitors', 'wp-slimstat'), 'callback' => [self::class, 'raw_results_to_html'], 'callback_args' => [ 'type' => 'top', 'columns' => 'username', 'raw' => ['wp_slimstat_db', 'get_top'], ], 'classes' => ['normal'], 'locations' => ['slimview2', 'dashboard'], ], 'slim_p1_12' => [ 'title' => __('Top Search Terms', 'wp-slimstat'), 'callback' => [self::class, 'raw_results_to_html'], 'callback_args' => [ 'type' => 'top', 'columns' => 'searchterms', 'where' => 'searchterms <> "_" AND searchterms <> "" AND searchterms IS NOT NULL', 'raw' => ['wp_slimstat_db', 'get_top'], ], 'classes' => ['normal'], 'locations' => ['slimview2', 'slimview4', 'slimview5', 'dashboard'], ], 'slim_p1_13' => [ 'title' => __('Top Countries', 'wp-slimstat'), 'callback' => [self::class, 'raw_results_to_html'], 'callback_args' => [ 'type' => 'top', 'columns' => 'country', 'raw' => ['wp_slimstat_db', 'get_top'], ], 'classes' => ['normal'], // 'color' => '#fff7ed', 'locations' => ['slimview2', 'slimview3', 'slimview5', 'dashboard'], 'tooltip' => __('You can configure Slimstat to not track specific Countries by setting the corresponding filter in Slimstat > Settings > Exclusions.', 'wp-slimstat'), ], 'slim_p1_15' => [ 'title' => __('Rankings', 'wp-slimstat'), 'callback' => [self::class, 'show_rankings'], 'callback_args' => [ 'id' => 'slim_p1_15', ], 'classes' => ['normal'], 'locations' => ['inactive'], 'tooltip' => __("Slimstat retrieves live information from Alexa, Facebook and Mozscape, to measures your site's rankings. Values are updated every 12 hours. Please enter your personal access ID in the settings to access your personalized Mozscape data.", 'wp-slimstat'), ], 'slim_p1_17' => [ 'title' => __('Top Language Families', 'wp-slimstat'), 'callback' => [self::class, 'raw_results_to_html'], 'callback_args' => [ 'type' => 'top', 'columns' => 'SUBSTRING( language, 1, 2 )', 'as_column' => 'language', 'filter_op' => 'contains', 'raw' => ['wp_slimstat_db', 'get_top'], ], 'classes' => ['normal'], 'locations' => ['inactive'], ], 'slim_p1_18' => [ 'title' => __('Users Currently Online', 'wp-slimstat'), 'callback' => [self::class, 'raw_results_to_html'], 'callback_args' => [ 'type' => 'top', 'columns' => 'username', // Group OR conditions explicitly to help MySQL use indexes effectively // AND filter excludes anonymous/empty usernames from the deduplication 'where' => '((dt_out > ' . (date_i18n('U') - 300) . ') OR (dt > ' . (date_i18n('U') - 300) . ')) AND username <> "" AND username IS NOT NULL', 'use_date_filters' => false, 'raw' => ['wp_slimstat_db', 'get_top'], ], 'classes' => ['normal'], 'locations' => ['slimview2', 'dashboard'], 'tooltip' => __('When visitors leave a comment on your blog, WordPress assigns them a cookie. Slimstat leverages this information to identify returning visitors. Please note that visitors also include registered users.', 'wp-slimstat'), ], 'slim_p1_19_01' => [ 'title' => __('Search Terms', 'wp-slimstat'), 'callback' => [self::class, 'show_chart'], 'callback_args' => [ 'id' => 'slim_p1_19_01', 'chart_data' => [ 'data1' => 'COUNT( searchterms )', 'data2' => 'COUNT( DISTINCT searchterms )', 'where' => 'searchterms <> "_" AND searchterms IS NOT NULL AND searchterms <> ""', ], 'chart_labels' => [ __('Search Terms', 'wp-slimstat'), __('Unique Terms', 'wp-slimstat'), ], ], 'classes' => ['extralarge', 'chart'], 'locations' => ['slimview2'], 'tooltip' => $search_terms_chart_tooltip, ], 'slim_p2_01' => [ 'title' => __('Human Visits', 'wp-slimstat'), 'callback' => [self::class, 'show_chart'], 'callback_args' => [ 'id' => 'slim_p2_01', 'chart_data' => [ 'data1' => 'COUNT( DISTINCT visit_id )', 'data2' => 'COUNT( DISTINCT ip )', 'where' => '(visit_id > 0 AND browser_type <> 1)', ], 'chart_labels' => [ __('Visits', 'wp-slimstat'), __('Unique IPs', 'wp-slimstat'), ], ], 'classes' => ['extralarge', 'chart'], 'locations' => ['slimview3'], 'tooltip' => $human_visits_chart_tooltip, ], 'slim_p2_02' => [ 'title' => __('Audience Overview', 'wp-slimstat'), 'callback' => [self::class, 'raw_results_to_html'], 'callback_args' => [ 'raw' => ['wp_slimstat_db', 'get_visitors_summary'], ], 'classes' => ['normal'], 'locations' => ['slimview3', 'dashboard'], 'tooltip' => __('Where not otherwise specified, the metrics in this report are referred to human visitors.', 'wp-slimstat'), ], 'slim_p2_03' => [ 'title' => __('Top Languages', 'wp-slimstat'), 'callback' => [self::class, 'raw_results_to_html'], 'callback_args' => [ 'type' => 'top', 'columns' => 'language', 'raw' => ['wp_slimstat_db', 'get_top'], ], 'classes' => ['normal'], 'locations' => ['slimview3'], ], 'slim_p2_04' => [ 'title' => __('Top User Agents', 'wp-slimstat'), 'callback' => [self::class, 'raw_results_to_html'], 'callback_args' => [ 'type' => 'top', 'columns' => 'browser, browser_version', 'raw' => ['wp_slimstat_db', 'get_top'], ], 'classes' => ['normal'], 'locations' => ['slimview3', 'dashboard'], 'tooltip' => __('This report includes all types of clients, both bots and humans.', 'wp-slimstat'), ], 'slim_p2_05' => [ 'title' => __('Top Service Providers', 'wp-slimstat'), 'callback' => [self::class, 'raw_results_to_html'], 'callback_args' => [ 'type' => 'top', 'columns' => 'ip', 'raw' => ['wp_slimstat_db', 'get_top'], ], 'classes' => ['extralarge'], 'locations' => ['inactive'], 'tooltip' => __('Internet Service Provider: a company which provides other companies or individuals with access to the Internet. Your DSL or cable internet service is provided to you by your ISP.' . sprintf(__('Showing %s - %s of %s', 'wp-slimstat'), number_format_i18n($effective_start + 1), number_format_i18n($endpoint), number_format_i18n($_count_all_results) . (($_count_all_results == wp_slimstat::$settings['limit_results']) ? '+' : '')); if ($_show_refresh_countdown && wp_slimstat::$settings['refresh_interval'] > 0 && wp_slimstat_db::$filters_normalized['utime']['end'] >= date_i18n('U') - 300) { $pagination .= ' [' . __('Refresh in', 'wp-slimstat') . ' ]'; } return $pagination . ($pagination_buttons . '
'); } /** * Returns the total result count for pagination display. * * For type=recent reports, get_recent() uses LIMIT+OFFSET so * count($all_results) shrinks on later pages. We use count_records() * for the true total, but cap it to limit_results because get_recent() * can only access that many rows via SQL. The existing "200+" suffix * in report_pagination() signals when more rows exist beyond the cap. * * For type=top reports, SQL fetches the full set (no OFFSET) and * count($all_results) is already stable. * * Reports may set 'total_callback' to a callable that returns the * true total for expanded result sets (e.g. get_recent_outbound * explodes ;;;-delimited rows). */ private static function get_report_total_count($_args, $all_results) { // Report-specific total override (e.g. expanded outbound links) if (!empty($_args['total_callback']) && is_callable($_args['total_callback'])) { $limit = intval(wp_slimstat::$settings['limit_results']); return min(call_user_func($_args['total_callback']), $limit); } if (!empty($_args['type']) && $_args['type'] === 'recent') { $use_date_filters = $_args['use_date_filters'] ?? true; $true_total = wp_slimstat_db::count_records( 'id', !empty($_args['where']) ? $_args['where'] : '', $use_date_filters, $_args['where_params'] ?? [] ); $limit = intval(wp_slimstat::$settings['limit_results']); return min($true_total, $limit); } return count($all_results); } public static function callback_wrapper() { // If this user is whitelisted, we use the minimum capability $minimum_capability = 'read'; if (false === strpos(wp_slimstat::$settings['can_view'], (string) $GLOBALS['current_user']->user_login) && !empty(wp_slimstat::$settings['capability_can_view'])) { $minimum_capability = wp_slimstat::$settings['capability_can_view']; } if (!current_user_can($minimum_capability)) { return; } $_args = self::_check_args(func_get_args()); if (!empty($_args) && !empty($_args['callback'])) { call_user_func($_args['callback'], $_args['callback_args']); } } public static function raw_results_to_html($_args = []) { if ('on' == wp_slimstat::$settings['async_load'] && (!defined('DOING_AJAX') || !DOING_AJAX) && empty($_args['is_widget'])) { return ''; } wp_slimstat_db::$debug_message = ''; $where_params = $_args['where_params'] ?? null; if (!empty($_args['raw']) && is_array($_args['raw']) && isset($_args['raw'][0]) && method_exists($_args['raw'][0], 'get_combined_where')) { $_args['where'] = call_user_func([$_args['raw'][0], 'get_combined_where'], $_args['where'], '', true, '', $where_params); } $all_results = call_user_func($_args['raw'], $_args); // Fix for Recent Outbound Links: wrap strings as arrays with the correct key if (!empty($_args['columns']) && 'outbound_resource' === $_args['columns'] && !empty($all_results) && is_array($all_results)) { foreach ($all_results as $k => $v) { if (!is_array($v)) { $all_results[$k] = ['outbound_resource' => $v]; } } } // Backward compatibility if (!is_array($all_results)) { $all_results = []; } echo wp_kses_post(wp_slimstat_db::$debug_message); // Some reports don't need any kind of pre/post-processing, we just display the data contained in the array if (empty($_args['columns'])) { foreach ($all_results as $a_result) { echo ''; echo sprintf('%s %s', $a_result[ 'metric' ], $a_result[ 'value' ]); if (!empty($a_result['tooltip'])) { self::inline_help($a_result['tooltip']); } if (!empty($a_result['details'])) { echo sprintf("%s", $a_result[ 'details' ]); } echo '
'; } } else { // SQL-paginated reports (type=recent) already applied OFFSET in // the query — slice from 0. PHP-paginated reports (type=top) fetch // all rows up to limit_results — slice from start_from. $start_from = (!empty($_args['type']) && $_args['type'] === 'recent') ? 0 : self::get_clamped_start(count($all_results), wp_slimstat::$settings['rows_to_show']); $results = array_slice( $all_results, $start_from, wp_slimstat::$settings['rows_to_show'] ); // Count the results $count_page_results = count($results); if (0 == $count_page_results) { echo '' . __('No data to display', 'wp-slimstat') . '
'; if (defined('DOING_AJAX') && DOING_AJAX) { die(); } else { return []; } } // Some reports use aliases for column names if (!empty($_args['as_column'])) { $_args['columns'] = $_args['as_column']; } elseif (!empty($_args['outer_select_column'])) { $_args['columns'] = $_args['outer_select_column']; } // Some reports query more than one column if (false !== strpos($_args['columns'], ',')) { $_args['columns'] = explode(',', $_args['columns']); $_args['columns'] = trim($_args['columns'][0]); } // Ensure columns is a string (for safety) if (is_array($_args['columns'])) { $_args['columns'] = trim($_args['columns'][0]); } $permalinks_enabled = get_option('permalink_structure'); for ($i = 0; $i < $count_page_results; $i++) { $row_details = ''; $percentage = ''; $element_pre_value = ''; // Ensure $results[$i] is an array and the key exists if (is_array($results[$i]) && isset($results[$i][$_args['columns']])) { $element_value = $results[$i][$_args['columns']]; } else { $element_value = ''; } // Some columns require a special pre-treatment switch ($_args['columns']) { case 'browser': if (!empty($results[$i]['user_agent']) && 'on' == wp_slimstat::$settings['show_complete_user_agent_tooltip']) { $element_pre_value = self::inline_help($results[$i]['user_agent'], false); } $browser = $results[$i]['browser'] ?? ''; if (realpath(SLIMSTAT_ANALYTICS_DIR . ('/admin/assets/images/browsers/' . strtolower($browser) . '.png'))) { $image_url = SLIMSTAT_ANALYTICS_URL . ('/admin/assets/images/browsers/' . strtolower($browser) . '.png'); } else { $image_url = SLIMSTAT_ANALYTICS_URL . ('/admin/assets/images/unk.png'); } $element_value = '%s%s%s%s %s
", $bar, $element_pre_value, $element_value, $percentage, $row_details); // Strip all the filter links, if this information is shown on the frontend if (!is_admin()) { $row_output = preg_replace('/(.*?)<\/a>/', '\\2', $row_output); } echo $row_output; } if (!defined('DOING_AJAX') || !DOING_AJAX) { echo ''; } echo self::report_pagination($count_page_results, self::get_report_total_count($_args, $all_results)); if (!defined('DOING_AJAX') || !DOING_AJAX) { echo '' . __('No data to display', 'wp-slimstat') . '
'; if (defined('DOING_AJAX') && DOING_AJAX) { die(); } else { return []; } } $blog_url = ''; if (isset($results[0]['blog_id'])) { $blog_url = get_site_url($results[0]['blog_id']); } foreach ($results as $a_result) { // Parse JSON notes to show human-readable label: text > value > id > type > raw $label = $a_result['notes']; $note_data = json_decode($a_result['notes'], true); if (is_array($note_data)) { if (!empty($note_data['text'])) { $label = trim($note_data['text']); } elseif (!empty($note_data['value'])) { $label = $note_data['value']; } elseif (!empty($note_data['id'])) { $label = $note_data['id']; } elseif (!empty($note_data['type'])) { $label = ucfirst($note_data['type']); } } echo "" . esc_html($label);
if (!empty($a_result['counthits'])) {
echo sprintf('%s', esc_html($a_result['counthits']));
}
$has_tooltip = false;
if (!empty($a_result['dt'])) {
$date_time = date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $a_result['dt'], true);
echo '' . __('IP', 'wp-slimstat') . ': ' . esc_html($a_result['ip']) . '
' . __('Page', 'wp-slimstat') . sprintf(": %s
", esc_url($blog_url . $a_result['resource']), esc_html($blog_url . $a_result['resource'])) . __('Coordinates', 'wp-slimstat') . sprintf(': %s
', esc_html($a_result['position'])) . __('Date', 'wp-slimstat') . (': ' . $date_time);
$has_tooltip = true;
} elseif (is_array($note_data)) {
// For "top" mode (no dt), show full JSON breakdown in tooltip
$tooltip_parts = [];
foreach ($note_data as $key => $val) {
if (!empty($val)) {
$tooltip_parts[] = esc_html(ucfirst($key)) . ': ' . esc_html($val);
}
}
if (!empty($tooltip_parts)) {
echo '' . implode('
', $tooltip_parts);
$has_tooltip = true;
}
}
if ($has_tooltip) {
echo '';
}
echo '
' . esc_html__('No goals defined yet.', 'wp-slimstat') . '
'; } else { echo '| ' . esc_html__('Goal', 'wp-slimstat') . ' | '; echo '' . esc_html__('Uniques', 'wp-slimstat') . ' | '; echo '' . esc_html__('Total', 'wp-slimstat') . ' | '; echo '' . esc_html__('CR%', 'wp-slimstat') . ' | '; echo '
|---|---|---|---|
| ' . esc_html($goal['name']) . ' | '; echo '' . esc_html(number_format_i18n($data['uniques'])) . ' | '; echo '' . esc_html(number_format_i18n($data['total'])) . ' | '; echo '' . esc_html($data['cr']) . '% | '; echo '
' . esc_html__('No funnels configured.', 'wp-slimstat') . '
'; return; } // Render every funnel. With more than one we emit a tab strip; the admin // JS (goals-funnels.js) hides the inactive panels and switches on click. // The tab class is intentionally distinct from the main page's // .slimstat-gf-tab so the two delegated handlers never collide. Panels // stay visible server-side, so no-JS consumers (email report / CSV) still // see every funnel stacked instead of just the first. $multi = count($funnels) > 1; // Unique id base per widget render so the tab/panel ARIA ids don't collide // when more than one compact widget renders on a page (e.g. two shortcodes). static $widget_seq = 0; $uid = ++$widget_seq; echo ''; } public static function show_group_by($_args = []) { $all_results = call_user_func($_args['raw'], $_args); $results = []; if (is_array($all_results) && count($all_results)) { $start_from = (!empty($_args['type']) && $_args['type'] === 'recent') ? 0 : self::get_clamped_start(count($all_results), wp_slimstat::$settings['rows_to_show']); $results = array_slice( $all_results, $start_from, wp_slimstat::$settings['rows_to_show'] ); } // Count the results $count_page_results = count($results); if (0 == $count_page_results) { echo '' . __('No data to display', 'wp-slimstat') . '
'; if (defined('DOING_AJAX') && DOING_AJAX) { die(); } else { return 0; } } echo wp_kses_post(wp_slimstat_db::$debug_message); foreach ($results as $a_result) { if (empty($a_result['counthits'])) { $a_result['counthits'] = 0; } $a_result['resource'] = " " . self::get_resource_title($a_result['resource']) . ''; $group_markup = []; if (!empty($a_result['column_group'])) { $exploded_group = explode(';;;', $a_result['column_group']); $group_markup = []; foreach ($exploded_group as $a_item) { $user = get_user_by('login', $a_item); if ($user) { $group_markup[] = '' . get_avatar($user->ID, 16) . esc_html( $user->display_name ) . ''; } else { $group_markup[] = '' . esc_html( $a_item ) . ''; } } } echo sprintf('%s %s
', wp_kses_post( $a_result[ 'resource' ] ), esc_html( $a_result[ 'counthits' ] )) . wp_kses_post( implode(', ', $group_markup) ) . '
' . self::inline_help($a_ranking[2], false) . $a_ranking[1] . '' . $a_ranking[0] . '
'; } if (defined('DOING_AJAX') && DOING_AJAX) { die(); } } public static function show_world_map() { $countries = wp_slimstat_db::get_top('country') ?: []; $recent_visits = wp_slimstat_db::get_recent('location', '', '', true, '', 'city'); $data_points = []; if (!empty($recent_visits)) { $recent_visits = array_slice($recent_visits, 0, wp_slimstat::$settings['max_dots_on_map']); foreach ($recent_visits as $a_recent_visit) { if (!empty($a_recent_visit['city']) && !empty($a_recent_visit['location'])) { [$latitude, $longitude] = explode(',', $a_recent_visit['location']); $clean_city_name = htmlentities($a_recent_visit['city'], ENT_QUOTES, 'UTF-8'); $date_time = date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $a_recent_visit['dt'], true); $data_points[] = sprintf("{zoomLevel:7,type:'circle',title:'%s