| Server IP : 216.238.66.20 / Your IP : 216.73.216.199 Web Server : nginx/1.30.4 System : Linux woropds 5.15.0-187-generic #197-Ubuntu SMP Fri Jul 17 19:17:01 UTC 2026 x86_64 User : root ( 0) PHP Version : 8.2.33 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : OFF | Perl : OFF | Python : OFF | Sudo : OFF | Pkexec : OFF Directory : /var/www/sweetheart.mx/htdocs/wp-content/plugins/wp-slimstat/ |
Upload File : |
<?php
/*
* Plugin Name: SlimStat Analytics
* Plugin URI: https://wp-slimstat.com/
* Description: The leading web analytics plugin for WordPress
* Version: 5.5.0
* Author: Jason Crouse, VeronaLabs
* Text Domain: wp-slimstat
* Domain Path: /languages
* Author URI: https://wp-slimstat.com/
* License: GPL-2.0+
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Requires at least: 5.6
* Requires PHP: 7.4
*/
// check if composer autoloader exists
if (!file_exists(__DIR__ . '/vendor/autoload.php')) {
return;
}
// Set the plugin version and directory
define('SLIMSTAT_ANALYTICS_VERSION', '5.5.0');
define('SLIMSTAT_FILE', __FILE__);
define('SLIMSTAT_DIR', __DIR__);
define('SLIMSTAT_URL', plugins_url('', __FILE__));
// include the autoloader if it exists
require_once __DIR__ . '/vendor/autoload.php';
// Load the Mozart-scoped Symfony/Polyfill/Php80 so own code can use PHP 8.0+
// stdlib functions (str_contains, str_starts_with, fdiv, get_debug_type, …)
// on PHP 7.4 hosts. The bootstrap short-circuits on PHP_VERSION_ID >= 80000.
// Skipping this load on PHP 7.4 is what produced the v5.4.14 wp-admin fatal.
require_once __DIR__ . '/src/Dependencies/Symfony/Polyfill/Php80/bootstrap.php';
// Include Constants.php to make SLIMSTAT_ANALYTICS_DIR available to traits
require_once __DIR__ . '/src/Constants.php';
/**
* Main Slimstat Analytics Class
*
* @package Wp_SlimStat
*
* @todo REFACTOR TRACKING STATE: The $data_js and $stat properties should be refactored into a
* proper state object pattern to maintain encapsulation. Currently these properties are
* public to support refactored tracker classes (SlimStat\Tracker\*), but this breaks
* encapsulation and creates security risks. Future implementation should:
* 1. Create a TrackingState class to encapsulate state management
* 2. Update all Tracker classes to use the state object
* 3. Make properties protected or private
* 4. Ensure all state modifications go through validated methods
* This is tracked as technical debt for version 6.0
*/
// Include Constants.php to make SLIMSTAT_ANALYTICS_DIR available to traits
require_once __DIR__ . '/src/Constants.php';
class wp_slimstat
{
public static $settings = [];
public static $wpdb;
public static $upload_dir = '';
/**
* Flag indicating programmatic (server-side) tracking is active.
*
* When true, CMP consent checks are bypassed in Consent::canTrack() and
* Consent::piiAllowed(). This is used by slimtrack_server() for server-side
* contexts (cron, CLI, redirect handlers) where no browser session exists.
*
* DNT headers, IP anonymization/hashing, and other non-consent settings
* remain enforced.
*
* @var bool
* @since 5.4.3
*/
public static $is_programmatic_tracking = false;
public static $update_checker = [];
public static $raw_post_array = [];
/**
* @var array Tracking data from JavaScript (for internal tracking use only)
* @internal Use get_data_js() / set_data_js() methods for controlled access.
*
* This property is now protected to maintain proper encapsulation and prevent external code
* from bypassing consent checks or corrupting tracking state. All tracker classes use the
* getter/setter methods which include validation and filter hooks for GDPR compliance.
*/
protected static $data_js = ['id' => 0];
/**
* @var array Current pageview tracking data (for internal tracking use only)
* @internal Use get_stat() / set_stat() methods for controlled access.
*
* This property is now protected to maintain proper encapsulation and prevent external code
* from bypassing consent checks or corrupting tracking state. All tracker classes use the
* getter/setter methods which include validation and filter hooks for GDPR compliance.
*/
protected static $stat = [];
protected static $date_i18n_filters = [];
/**
* Gets the current data_js array (for internal tracking use only)
*
* @return array
*/
public static function get_data_js()
{
return self::$data_js;
}
/**
* Sets the data_js array (for internal tracking use only)
*
* This method provides controlled access to the data_js property and includes
* basic validation to prevent tampering.
*
* @param array $data_js The tracking data from JavaScript
* @return void
* @internal For use by SlimStat tracking classes only
*/
public static function set_data_js($data_js)
{
// Validate that we're receiving an array
if (!is_array($data_js)) {
return;
}
// Apply filter to allow validation/modification by consent management systems
$data_js = apply_filters('slimstat_set_data_js', $data_js);
self::$data_js = $data_js;
}
/**
* Gets the current stat array (for internal tracking use only)
*
* @return array Current tracking state
* @internal For use by SlimStat tracking classes only
*/
public static function get_stat()
{
return self::$stat;
}
/**
* Sets the stat array (for internal tracking use only)
*
* This method provides controlled access to the stat property and includes
* basic validation to prevent tampering and ensure consent compliance.
*
* @param array $stat The pageview tracking data
* @return void
* @internal For use by SlimStat tracking classes only
*/
public static function set_stat($stat)
{
// Validate that we're receiving an array
if (!is_array($stat)) {
return;
}
// Apply filter to allow validation/modification by consent management systems
// This is critical for GDPR compliance - CMPs can inspect and modify data
$stat = apply_filters('slimstat_set_stat', $stat);
self::$stat = $stat;
}
/**
* Backward-compatible wrapper for the tracking API.
*
* This method delegates to the new namespaced Tracker class while maintaining
* the original method signature for third-party integrations.
*
* @since 5.4.3
* @return int|false The record ID on success, or a negative error code on failure.
*/
public static function slimtrack()
{
return \SlimStat\Tracker\Tracker::slimtrack();
}
/**
* Server-side tracking API that bypasses CMP consent checks.
*
* Use this method for programmatic tracking in server-side contexts where no
* browser session exists (e.g., cron jobs, CLI scripts, redirect handlers).
*
* CMP consent is a browser-side concept. In server-side contexts, there is no
* browser session and CMP consent has no meaningful role.
*
* The following settings remain enforced:
* - DNT (Do Not Track) headers
* - IP anonymization and hashing settings
* - Tracker cookie configuration
* - All exclusion rules
*
* @since 5.4.3
* @return int|false The record ID on success, or a negative error code on failure.
*/
public static function slimtrack_server()
{
$previous_programmatic_state = self::$is_programmatic_tracking;
self::$is_programmatic_tracking = true;
try {
$result = \SlimStat\Tracker\Tracker::slimtrack();
} finally {
self::$is_programmatic_tracking = $previous_programmatic_state;
}
return $result;
}
/**
* Initializes variables and actions
*/
public static function init()
{
\SlimStat\Providers\RestApiManager::run();
// Load all the settings
if (is_network_admin() && (empty($_GET['page']) || false === strpos($_GET['page'], 'slimview'))) {
self::$settings = get_site_option('slimstat_options', []);
} else {
self::$settings = get_option('slimstat_options', []);
}
if (empty(self::$settings)) {
// Fresh install: set defaults including geolocation_provider=dbip
self::$settings = self::get_fresh_defaults();
self::update_option('slimstat_options', self::$settings);
}
self::$settings = array_merge(self::init_options(), self::$settings);
// One-shot migration: runs once on first boot after installing this build.
// '_migration_5460' is absent from all pre-5.4.6 installs; array_merge fills it
// with '0' from init_options(). After running, the flag stores the version that ran it.
// On downgrade→re-upgrade, the stored version will differ from SLIMSTAT_ANALYTICS_VERSION,
// allowing the migration to re-run if needed. '0' = never ran, version string = ran.
$_migration_ran = self::$settings['_migration_5460'] ?? '0';
if ('0' === $_migration_ran || (is_string($_migration_ran) && '0' !== $_migration_ran && version_compare($_migration_ran, SLIMSTAT_ANALYTICS_VERSION, '<'))) {
// --- Consent intent detection ---
// Read legacy v5.3.x consent settings to detect if user had configured privacy.
// These survive through v5.3.x → v5.4.x upgrades because array_merge preserves DB values.
$_had_opt_out_banner = ('on' === (self::$settings['display_opt_out'] ?? 'no'));
$_had_opt_out_cookies = !empty(trim(self::$settings['opt_out_cookie_names'] ?? ''));
$_had_opt_in_cookies = !empty(trim(self::$settings['opt_in_cookie_names'] ?? ''));
// Check if user deliberately chose a third-party CMP in v5.4.x
$_current_integration = self::$settings['consent_integration'] ?? '';
$_has_third_party_cmp = in_array($_current_integration, ['wp_consent_api', 'real_cookie_banner'], true);
if ($_has_third_party_cmp) {
// User deliberately configured a third-party CMP — preserve their setup
self::$settings['gdpr_enabled'] = 'on';
} elseif ($_had_opt_out_banner || $_had_opt_out_cookies || $_had_opt_in_cookies) {
// User had consent/privacy config in v5.3.x — map to GDPR system
self::$settings['gdpr_enabled'] = 'on';
self::$settings['use_slimstat_banner'] = 'on';
// Auto-detect best CMP: if opt-in cookies were set (third-party plugin)
// and WP Consent API is installed, use it. Otherwise use SlimStat Banner.
if ($_had_opt_in_cookies && function_exists('wp_has_consent')) {
self::$settings['consent_integration'] = 'wp_consent_api';
} else {
self::$settings['consent_integration'] = 'slimstat_banner';
}
} else {
// No consent config ever — pure v5.3.x behavior: all tracked, no banner
self::$settings['gdpr_enabled'] = 'off';
self::$settings['consent_integration'] = '';
self::$settings['use_slimstat_banner'] = 'off';
}
unset($_had_opt_out_banner, $_had_opt_out_cookies, $_had_opt_in_cookies,
$_current_integration, $_has_third_party_cmp);
// One-time resets for settings broken by v5.4.0-5.4.6 defaults.
// Gated on < 5.4.7 so future upgrades (5.4.8+) don't override admin choices.
// Skip for fresh installs ('0' = never ran, no broken settings to fix).
if ('0' !== $_migration_ran && version_compare($_migration_ran, '5.4.7', '<')) {
// Restore session cookie — Consent::piiAllowed() in Session.php gates
// the actual setcookie() call at runtime, not this setting.
if ('off' === (self::$settings['set_tracker_cookie'] ?? 'on')) {
self::$settings['set_tracker_cookie'] = 'on';
}
// javascript_mode='off' baked a stale per-visitor stat ID into cached HTML.
// Always reset — server-side mode was a v5.4.0 default, not a user choice.
if ('off' === (self::$settings['javascript_mode'] ?? 'on')) {
self::$settings['javascript_mode'] = 'on';
}
// anonymize_ip='on' and hash_ip='on' were v5.4.1 defaults that changed IP storage.
$_ss_ip_was_anonymized = ('on' === (self::$settings['anonymize_ip'] ?? 'off'));
$_ss_ip_was_hashed = ('on' === (self::$settings['hash_ip'] ?? 'off'));
if ($_ss_ip_was_anonymized) {
self::$settings['anonymize_ip'] = 'off';
}
if ($_ss_ip_was_hashed) {
self::$settings['hash_ip'] = 'off';
}
if ($_ss_ip_was_anonymized || $_ss_ip_was_hashed) {
set_transient('slimstat_migration_5460_ip_notice', '1', 7 * DAY_IN_SECONDS);
}
}
unset($_ss_ip_was_anonymized, $_ss_ip_was_hashed);
// Mark done — store the version so downgrade→re-upgrade can re-trigger if needed.
self::$settings['_migration_5460'] = SLIMSTAT_ANALYTICS_VERSION;
self::update_option('slimstat_options', self::$settings);
// Rewrite rules are flushed via two other paths:
// 1. Activation hook: admin/index.php init_environment() calls flush_rewrite_rules()
// 2. Settings change: RestApiManager sets 'slimstat_permalink_structure_updated' option,
// which triggers flush_rewrite_rules() on next init via rewriteRuleRequest()
// No flush needed here — doing so during migration (plugins_loaded) would fire before
// the rewrite rule is registered on 'init' and waste a DB write.
}
// Allow third party tools to edit the options
self::$settings = apply_filters('slimstat_init_options', self::$settings);
// Consent-sync: derive use_slimstat_banner from consent_integration.
// Only run when GDPR is on — when off, banner stays off and canTrack() returns true early.
if ('on' === (self::$settings['gdpr_enabled'] ?? 'off')) {
$consent_integration = self::$settings['consent_integration'] ?? '';
// If WP Consent API is selected but the plugin isn't installed, fall back to
// SlimStat's own banner so consent enforcement stays active. Resetting to ''
// would leave GDPR on but with no consent mechanism — getIntegrationKey()
// silently picks 'slimstat_banner' but without the banner UI enabled.
if ('wp_consent_api' === $consent_integration && !function_exists('wp_has_consent')) {
$consent_integration = 'slimstat_banner';
self::$settings['consent_integration'] = 'slimstat_banner';
}
if ('' === $consent_integration && ('on' === (self::$settings['use_slimstat_banner'] ?? 'off'))) {
$consent_integration = 'slimstat_banner';
self::$settings['consent_integration'] = $consent_integration;
}
if ('slimstat_banner' === $consent_integration) {
self::$settings['use_slimstat_banner'] = 'on';
} else {
self::$settings['use_slimstat_banner'] = 'off';
}
} // end GDPR consent-sync
// Allow third-party tools to use a custom database for Slimstat
self::$wpdb = apply_filters('slimstat_custom_wpdb', $GLOBALS['wpdb']);
// Define the folder where to store the geolocation database (shared among sites in a network, by default)
if (defined('UPLOADS')) {
self::$upload_dir = ABSPATH . UPLOADS . '/wp-slimstat';
} else {
$upload_dir_info = wp_upload_dir();
self::$upload_dir = $upload_dir_info['basedir'];
// Handle multisite environment
if (is_multisite() && !(is_main_network() && is_main_site() && defined('MULTISITE'))) {
self::$upload_dir = str_replace('/sites/' . get_current_blog_id(), '', self::$upload_dir);
}
self::$upload_dir .= '/wp-slimstat';
}
// Apply filter to allow customization of the upload directory
self::$upload_dir = apply_filters('slimstat_maxmind_path', self::$upload_dir);
// Allow add-ons to turn off the tracker based on other conditions.
// Exclude internal SlimStat endpoints from server-side tracking so they
// don't appear as page visits in the Access Log:
// - admin-ajax.php (AJAX tracking handler)
// - /request/{hash}/ (adblock bypass tracking endpoint)
// - /{hash}.js, /{hash}.css (adblock bypass JS/CSS file serving via Routing.php)
$_request_uri = self::get_request_uri();
$_is_internal_endpoint = false !== strpos($_request_uri, 'wp-admin/admin-ajax.php')
|| (bool) preg_match('#/request/[a-f0-9]{32}/?$|/[a-f0-9]{32}\.(?:js|css)(?:\?|$)#', $_request_uri);
$is_tracking_filter = apply_filters('slimstat_filter_pre_tracking', !$_is_internal_endpoint);
$is_tracking_filter_js = apply_filters('slimstat_filter_pre_tracking_js', true);
unset($_request_uri, $_is_internal_endpoint);
// Enable the tracker (both server- and client-side)
if ((!is_admin() || 'on' == self::$settings['track_admin_pages']) && 'on' == self::$settings['is_tracking'] && $is_tracking_filter) {
// Is server-side tracking active?
if ('on' != self::$settings['javascript_mode']) {
add_action(is_admin() ? 'admin_init' : 'wp', [\SlimStat\Tracker\Tracker::class, 'slimtrack'], 5);
if ('on' != self::$settings['ignore_wp_users']) {
add_action('login_init', [\SlimStat\Tracker\Tracker::class, 'slimtrack'], 10);
}
}
// Slimstat tracks screen resolutions, outbound links and other client-side information using a client-side tracker
add_action(is_admin() ? 'admin_enqueue_scripts' : 'wp_enqueue_scripts', [self::class, 'enqueue_tracker'], 15);
if ('on' != self::$settings['ignore_wp_users']) {
add_action('login_enqueue_scripts', [self::class, 'enqueue_tracker'], 10);
}
add_filter('script_loader_tag', [self::class, 'add_defer_to_script_tag'], 10, 2);
}
$banner_enabled = ('on' === (self::$settings['gdpr_enabled'] ?? 'off'))
&& ('on' === (self::$settings['use_slimstat_banner'] ?? 'off'));
if ($banner_enabled) {
add_action('wp_enqueue_scripts', [self::class, 'enqueue_gdpr_assets'], 20);
add_action('login_enqueue_scripts', [self::class, 'enqueue_gdpr_assets'], 20);
add_action('wp_footer', [self::class, 'render_gdpr_banner'], 5);
add_action('login_footer', [self::class, 'render_gdpr_banner'], 5);
}
// Registers Slimstat with WP Consent API if enabled in plugin settings
if ((self::$settings['consent_integration'] ?? '') === 'wp_consent_api') {
// Check if WP Consent API plugin is actually active
if (function_exists('wp_has_consent')) {
$plugin = plugin_basename(SLIMSTAT_FILE);
add_filter("wp_consent_api_registered_{$plugin}", '__return_true');
// Register cookie info with WP Consent API for CMP display.
// Deferred to 'init' (priority 10) so the textdomain is loaded first
// (load_textdomain runs on 'init' priority 1). Calling __() here would
// trigger a _load_textdomain_just_in_time notice in WordPress 6.7+.
if (function_exists('wp_add_cookie_info')) {
$session_duration = intval(self::$settings['session_duration'] ?? 1800);
add_action('init', static function () use ($session_duration) {
wp_add_cookie_info(
'slimstat_tracking_code',
__('SlimStat Analytics', 'wp-slimstat'),
'statistics',
sprintf(
/* translators: %d: number of seconds for session duration */
_n('%d second', '%d seconds', $session_duration, 'wp-slimstat'),
$session_duration
),
__('Session cookie that identifies returning visitors for analytics.', 'wp-slimstat'),
'',
false,
false
);
}, 10);
}
}
}
// Register WordPress Privacy API exporters and erasers (GDPR Article 15 & 17)
add_filter('wp_privacy_personal_data_exporters', [\SlimStat\Services\Privacy\DataExporter::class, 'registerExporters']);
add_filter('wp_privacy_personal_data_erasers', [\SlimStat\Services\Privacy\DataEraser::class, 'registerErasers']);
// Register privacy policy content
add_action('admin_init', [self::class, 'registerPrivacyPolicyContent']);
// One-time notice when the v5.4.6 migration reset IP anonymization settings
add_action('admin_notices', [self::class, 'show_migration_5460_ip_notice']);
// Register AJAX handlers for consent upgrade/revocation (anonymous tracking mode)
\SlimStat\Services\Privacy\ConsentHandler::registerAjaxHandlers();
// Hook a DB clean-up routine to the daily cronjob
add_action('wp_slimstat_purge', [self::class, 'wp_slimstat_purge']);
// Hook IP hashing daily salt generation (for GDPR compliance)
add_action('wp_slimstat_generate_daily_salt', [\SlimStat\Providers\IPHashProvider::class, 'generateDailySalt']);
// Hook a GeoIP database update routine to the daily cronjob
add_action('wp_slimstat_update_geoip_database', [self::class, 'wp_slimstat_update_geoip_database']);
// Allow external domains on CORS requests
add_filter('allowed_http_origins', [self::class, 'open_cors_admin_ajax']);
// Internal GDPR banner/consent handling removed. Use external CMP plugins.
// If this request was a redirect, we should update the content type accordingly
add_filter('wp_redirect_status', [\SlimStat\Tracker\Tracker::class, 'update_content_type'], 10, 2);
// Shortcodes
add_shortcode('slimstat', [self::class, 'slimstat_shortcode'], 15);
// Init the plugin functionality
add_action('init', [self::class, 'init_plugin']);
// REST API Support
add_action('rest_api_init', [self::class, 'register_rest_route']);
// Load the admin library
if (is_user_logged_in()) {
include_once(plugin_dir_path(__FILE__) . 'admin/index.php');
add_action('init', ['wp_slimstat_admin', 'init'], 60);
}
}
// end init
/**
* Load plugin textdomain
*
* @return void
*/
public static function load_textdomain()
{
load_plugin_textdomain('wp-slimstat', false, '/wp-slimstat/languages');
}
/**
* Show a one-time admin notice when the v5.4.6 migration reset anonymize_ip
* or hash_ip from 'on' to 'off'. EU-facing sites may need to re-enable these.
* The transient is deleted after display so the notice appears exactly once.
*/
public static function show_migration_5460_ip_notice(): void
{
if (!current_user_can('manage_options')) {
return;
}
if (!get_transient('slimstat_migration_5460_ip_notice')) {
return;
}
delete_transient('slimstat_migration_5460_ip_notice');
$settings_url = admin_url('admin.php?page=slimconfig&tab=2');
?>
<div class="notice notice-warning">
<p>
<strong><?php esc_html_e('SlimStat Analytics — IP Privacy Settings Reset', 'wp-slimstat'); ?></strong><br>
<?php esc_html_e('This update restored full-IP storage (the 5.3.x default) by turning off IP anonymization and daily visitor hashing. If your site serves EU visitors, please review your Data Protection settings.', 'wp-slimstat'); ?>
<a href="<?php echo esc_url($settings_url); ?>"><?php esc_html_e('Review Settings → Data Protection', 'wp-slimstat'); ?></a>
</p>
</div>
<?php
}
/**
* The main logging function
*
* @param string $message The message to be logged.
* @param string $level The log level (e.g., 'info', 'warning', 'error'). Default is 'info'.
*
* @uses error_log
*/
public static function log($message, $level = 'info')
{
if (is_array($message)) {
$message = wp_json_encode($message);
}
$log_level = strtoupper($level);
// Log when debug is enabled
if (defined('WP_DEBUG') && WP_DEBUG) {
error_log(sprintf('[WP SLIMSTAT] [%s]: %s', $log_level, $message));
}
}
/**
* Resolve the active geolocation provider.
*
* New UI sets 'geolocation_provider' explicitly (incl. 'disable').
* Legacy installs only have 'enable_maxmind' (tri-state: 'on', 'no', 'disable').
*
* @return string|false 'maxmind', 'dbip', 'cloudflare', or false if disabled
*/
public static function resolve_geolocation_provider()
{
static $cache = [];
// Sanitize both settings that drive resolution
$provider_san = sanitize_text_field(self::$settings['geolocation_provider'] ?? '');
// Normalize legacy tri-state ('on'|'no'|'disable') to deterministic token
$legacy_san = sanitize_text_field(self::$settings['enable_maxmind'] ?? '');
if ('on' === $legacy_san) {
$legacy_norm = 'on';
} elseif ('no' === $legacy_san) {
$legacy_norm = 'no';
} else {
$legacy_norm = 'disable';
}
// Cache key invalidates when settings change mid-request (e.g. settings save)
$cache_key = $provider_san . '|' . $legacy_norm;
if (array_key_exists($cache_key, $cache)) {
return $cache[$cache_key];
}
$result = false;
if ('' !== $provider_san) {
if ('disable' === $provider_san) {
$cache[$cache_key] = false;
return false;
}
if (in_array($provider_san, \SlimStat\Services\GeoService::ALL_PROVIDERS, true)) {
$cache[$cache_key] = $provider_san;
return $provider_san;
}
// Invalid value — fall through to legacy flag
}
if ('on' === $legacy_norm) {
$result = 'maxmind';
} elseif ('no' === $legacy_norm) {
$result = 'dbip';
}
$cache[$cache_key] = $result;
return $result;
}
/**
* Decodes the permalink
*/
public static function get_request_uri()
{
$request_url = '';
if (isset($_SERVER['REQUEST_URI'])) {
return urldecode(sanitize_url(wp_unslash($_SERVER['REQUEST_URI'])));
} elseif (isset($_SERVER['SCRIPT_NAME'])) {
$request_url = sanitize_text_field(wp_unslash($_SERVER['SCRIPT_NAME']));
} elseif (isset($_SERVER['PHP_SELF'])) {
$request_url = sanitize_text_field(wp_unslash($_SERVER['PHP_SELF']));
}
if (isset($_SERVER['QUERY_STRING'])) {
$request_url .= '?' . sanitize_text_field(wp_unslash($_SERVER['QUERY_STRING']));
}
return $request_url;
}
// end get_request_uri
public static function is_local_ip_address($ip_address = '')
{
return !filter_var($ip_address, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE|FILTER_FLAG_NO_RES_RANGE);
}
/**
* Implements the Slimstat Shortcode API
*/
public static function slimstat_shortcode($_attributes = '', $_content = '')
{
shortcode_atts([
'f' => '', // recent, popular, count, widget
'w' => '', // column to use (for recent, popular and count) or widget to use
's' => ' ', // separator
'o' => 0, // offset for counters
], $_attributes);
$f = $_attributes['f'] ?? '';
$w = $_attributes['w'] ?? '';
$s = $_attributes['s'] ?? '';
$o = $_attributes['o'] ?? 0;
$output = '';
$where = '';
$as_column = '';
$s = sprintf("<span class='slimstat-item-separator'>%s</span>", $s);
// Look for required fields
if (empty($f) || empty($w)) {
return '<!-- Slimstat Shortcode Error: missing parameter -->';
}
// Validation the parameter w
$w = (string) $w;
if (false === in_array($w, ['*', 'count', 'display_name', 'hostname', 'post_link', 'post_link_no_qs', 'dt', 'username', 'post_link', 'ip', 'id', 'searchterms', 'username', 'resource', 'country', 'browser', 'platform', 'language', 'slim_p1_01', 'slim_p1_03', 'slim_p1_04', 'slim_p1_06', 'slim_p1_08', 'slim_p1_10', 'slim_p1_11', 'slim_p1_12', 'slim_p1_13', 'slim_p1_15', 'slim_p1_17', 'slim_p1_18', 'slim_p1_19_01', 'slim_p2_01', 'slim_p2_02', 'slim_p2_03', 'slim_p2_04', 'slim_p2_05', 'slim_p2_06', 'slim_p2_07', 'slim_p2_08', 'slim_p2_12', 'slim_p2_13', 'slim_p2_14', 'slim_p2_15', 'slim_p2_16', 'slim_p2_17', 'slim_p2_18', 'slim_p2_19', 'slim_p2_20', 'slim_p2_21', 'slim_p2_22_01', 'slim_p2_24', 'slim_p2_25', 'slim_p3_01', 'slim_p3_02', 'slim_p4_01', 'slim_p4_02', 'slim_p4_04', 'slim_p4_05', 'slim_p4_06', 'slim_p4_07', 'slim_p4_09', 'slim_p4_10', 'slim_p4_11', 'slim_p4_12', 'slim_p4_13', 'slim_p4_15', 'slim_p4_16', 'slim_p4_18', 'slim_p4_19', 'slim_p4_20', 'slim_p4_21', 'slim_p4_22', 'slim_p4_23', 'slim_p4_24', 'slim_p4_25', 'slim_p4_26_01', 'slim_p4_27', 'slim_p6_01', 'slim_p9_01', 'slim_p9_02', 'slim_p2_23'], true)) {
return '<!-- Slimstat Shortcode Error: invalid parameter for w -->';
}
// Include the Reports Library, but don't initialize the database, since we will do that separately later
include_once(plugin_dir_path(__FILE__) . 'admin/view/wp-slimstat-reports.php');
wp_slimstat_reports::init();
/**
* @SecurityProfile https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-0630
* Disabled because of the report from WP Scan
*/
// Init the database library with the appropriate filters
/*if ( strpos ( $_content, 'WHERE:' ) !== false ) {
$where = html_entity_decode( str_replace( 'WHERE:', '', $_content ), ENT_QUOTES, 'UTF-8' );
}
else{*/
wp_slimstat_db::init(html_entity_decode($_content, ENT_QUOTES, 'UTF-8'));
//}
switch ($f) {
case 'count':
case 'count-all':
$output = wp_slimstat_db::count_records($w, $where, false === strpos($f, 'all')) + $o;
break;
case 'widget':
if (empty(wp_slimstat_reports::$reports[$w])) {
return __('Invalid Report ID', 'wp-slimstat');
}
wp_register_style('wp-slimstat-frontend', plugins_url('/admin/assets/css/slimstat.css', __FILE__), true, SLIMSTAT_ANALYTICS_VERSION);
wp_enqueue_style('wp-slimstat-frontend');
wp_slimstat_reports::$reports[$w]['callback_args']['is_widget'] = true;
ob_start();
echo wp_slimstat_reports::report_header($w);
call_user_func(wp_slimstat_reports::$reports[$w]['callback'], wp_slimstat_reports::$reports[$w]['callback_args']);
wp_slimstat_reports::report_footer();
$output = ob_get_contents();
ob_end_clean();
break;
case 'recent':
case 'recent-all':
case 'top':
case 'top-all':
$function = 'get_' . str_replace('-all', '', $f);
if ('*' === $w) {
$w = 'id';
}
$w = esc_html($w);
$w = self::string_to_array($w);
// Some columns are 'special' and need be removed from the list
$w_clean = array_diff($w, ['count', 'display_name', 'hostname', 'post_link', 'post_link_no_qs', 'dt']);
// The special value 'display_name' requires the username to be retrieved
if (in_array('display_name', $w)) {
$w_clean[] = 'username';
}
// The special value 'post_list' requires the resource to be retrieved
if (in_array('post_link', $w)) {
$w_clean[] = 'resource';
}
// The special value 'post_list_no_qs' requires a substring to be calculated
if (in_array('post_link_no_qs', $w)) {
$w_clean = ['SUBSTRING_INDEX( resource, "' . (get_option('permalink_structure') ? '?' : '&') . '", 1 )'];
$as_column = 'resource';
}
// Retrieve the data
$results = wp_slimstat_db::$function(implode(', ', $w_clean), $where, '', false === strpos($f, 'all'), $as_column);
// No data? No problem!
if (empty($results)) {
return '<!-- Slimstat Shortcode: No Data -->';
}
// Are nice permalinks enabled?
$permalinks_enabled = get_option('permalink_structure');
// Format results
$output = [];
foreach ($results as $result_idx => $a_result) {
foreach ($w as $a_column) {
$output[$result_idx][$a_column] = sprintf("<span class='col-%s'>", $a_column);
switch ($a_column) {
case 'count':
$output[$result_idx][$a_column] .= $a_result['counthits'];
break;
case 'country':
$output[$result_idx][$a_column] .= wp_slimstat_i18n::get_string('c-' . $a_result[$a_column]);
break;
case 'display_name':
$user_details = get_user_by('login', $a_result['username']);
if (!empty($user_details)) {
$output[$result_idx][$a_column] .= $user_details->display_name;
} else {
$output[$result_idx][$a_column] .= $a_result['username'];
}
break;
case 'dt':
$output[$result_idx][$a_column] .= date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $a_result['dt']);
break;
case 'hostname':
$output[$result_idx][$a_column] .= self::gethostbyaddr($a_result['ip']);
break;
case 'language':
$output[$result_idx][$a_column] .= wp_slimstat_i18n::get_string('l-' . $a_result[$a_column]);
break;
case 'platform':
$output[$result_idx][$a_column] .= wp_slimstat_i18n::get_string($a_result[$a_column]);
break;
case 'post_link':
case 'post_link_no_qs':
$post_id = url_to_postid($a_result['resource']);
if ($post_id > 0) {
$output[$result_idx][$a_column] .= sprintf("<a href='%s'>", esc_url( $a_result[ 'resource' ] )) . esc_html( get_the_title($post_id) ) . '</a>';
} else {
$output[$result_idx][$a_column] .= sprintf("<a href='%s'>%s</a>", esc_url( $a_result[ 'resource' ] ), esc_html( $a_result[ 'resource' ] ));
}
break;
default:
$output[$result_idx][$a_column] .= $a_result[$a_column] ?? '';
break;
}
$output[$result_idx][$a_column] .= '</span>';
}
$output[$result_idx] = '<li>' . implode($s, $output[$result_idx]) . '</li>';
}
$output = '<ul class="slimstat-shortcode ' . $f . implode('-', $w) . '">' . implode('', $output) . '</ul>';
break;
default:
break;
}
return $output;
}
// end slimstat_shortcode
public static function init_plugin()
{
// Include our browser detector library
\SlimStat\Services\Browscap::init();
// Make sure the upload directory is exist and is protected.
self::create_upload_directory();
// Ensure daily salt exists for IP hashing (GDPR compliance)
// This runs on every page load but only generates if missing
\SlimStat\Providers\IPHashProvider::generateDailySalt();
// Initialize adblock bypass functionality
\SlimStat\Tracker\Tracker::rewrite_rule_tracker();
add_action('template_redirect', [\SlimStat\Tracker\Tracker::class, 'adblocker_javascript']);
add_action('init', [\SlimStat\Tracker\Tracker::class, 'rewrite_rule_tracker']);
}
/**
* Opens given domains during CORS requests to admin-ajax.php
*/
public static function open_cors_admin_ajax($_allowed_origins = [])
{
$exploded_domains = self::string_to_array(self::$settings['external_domains']);
if (!empty($exploded_domains) && !empty($exploded_domains[0])) {
$_allowed_origins = array_merge($_allowed_origins, $exploded_domains);
}
return $_allowed_origins;
}
// end open_cors_admin_ajax
/**
* Implements a REST API interface to retrieve Slimstat reports and metrics
*/
public static function rest_api_response($_request = [])
{
$filters = '';
if (!empty($_request['filters'])) {
$filters = $_request['filters'];
}
if (empty($_request['dimension'])) {
return new WP_Error('rest_invalid', esc_html__('[REST API] The <code>dimension</code> parameter is required. Please review your request and try again.', 'wp-slimstat'), ['status' => 400]);
}
if (empty($_request['function'])) {
return new WP_Error('rest_invalid', esc_html__('[REST API] The <code>function</code> parameter is required. Please review your request and try again.', 'wp-slimstat'), ['status' => 400]);
}
include_once(plugin_dir_path(__FILE__) . 'admin/view/wp-slimstat-db.php');
wp_slimstat_db::init($filters);
$response = [
'function' => htmlentities($_request['function'], ENT_QUOTES, 'UTF-8'),
'dimension' => htmlentities($_request['dimension'], ENT_QUOTES, 'UTF-8'),
'data' => 0,
];
switch ($_request['function']) {
case 'count':
case 'count-all':
$response['data'] = wp_slimstat_db::count_records($_request['dimension'], '', false === strpos($_request['function'], '-all'));
break;
case 'recent':
case 'recent-all':
case 'top':
case 'top-all':
$function = 'get_' . str_replace('-all', '', $_request['function']);
// Retrieve the data
$response['data'] = array_values(wp_slimstat_db::$function($_request['dimension'], '', '', false === strpos($_request['function'], '-all')));
break;
default:
// This should never happen, because of the 'enum' condition for this parameter. But never say never...
$response['data'] = new WP_Error('rest_invalid', esc_html__('[REST API] You sent an invalid request. Accepted function values include: <code>count, count-all, recent, recent-all, top and top-all</code>. Please review your request and try again.', 'wp-slimstat'), ['status' => 400]);
break;
}
return rest_ensure_response($response);
}
// end rest_api_response
/**
* Implements a REST API authentication mechanism via token
*/
public static function rest_api_authorization($_request = [])
{
if (empty($_request['token'])) {
return new WP_Error('rest_invalid', esc_html__('[REST API] Please use a valid token in order to access the REST API endpoint at this URL.', 'wp-slimstat'), ['status' => 400]);
}
$valid_tokens = self::string_to_array(self::$settings['rest_api_tokens']);
foreach ($valid_tokens as $valid_token) {
if (is_string($valid_token) && is_string($_request['token']) && hash_equals($valid_token, $_request['token'])) {
return true;
}
}
return false;
}
// end rest_api_authorization
/**
* Registers a new REST API route for the Slimstat endpoint
*/
public static function register_rest_route()
{
register_rest_route('slimstat/v1', '/get', [
'methods' => WP_REST_Server::READABLE,
'callback' => [self::class, 'rest_api_response'],
'permission_callback' => [self::class, 'rest_api_authorization'],
'args' => [
'token' => [
'description' => __('You will need to specify a valid token to be able to query the data. Tokens are defined in Slimstat > Settings > Access Control.', 'wp-slimstat'),
'type' => 'string',
],
'function' => [
'description' => __('This parameter specifies the type of QUERY you would like to perform. Accepted funciton values include: count, count-all, recent, recent-all, top and top-all.', 'wp-slimstat'),
'type' => 'string',
'enum' => ['count', 'count-all', 'recent', 'recent-all', 'top', 'top-all'],
],
'dimension' => [
'description' => __('This parameter indicates what dimension to return: * (all data), ip, resource, browser, operating system, etc. You can only specify one dimension at a time.', 'wp-slimstat'),
'type' => 'string',
'enum' => ['*', 'id', 'ip', 'username', 'email', 'country', 'referer', 'resource', 'searchterms', 'browser', 'platform', 'language', 'resolution', 'content_type', 'content_id', 'tz_offset', 'outbound_resource'],
],
'filters' => [
'description' => __('This parameter is used to filter a given dimension (resources, browsers, operating systems, etc) so that it satisfies certain conditions (i.e.: browser contains Chrome). Please make sure to urlencode this value, and to use the usual filter format: browser contains Chrome&&&referer contains slim (encoded: browser%20contains%20Chrome%26%26%26referer%20contains%20slim)', 'wp-slimstat'),
'type' => 'string',
],
],
]);
}
// end register_rest_route
/**
* Converts a series of comma separated values into an array
*/
public static function string_to_array($_option = '')
{
if (empty($_option) || !is_string($_option)) {
return [];
} else {
return array_filter(array_map('trim', explode(',', $_option)));
}
}
// end string_to_array
/**
* Returns Matomo search engine mapping JSON, cached.
*/
public static function get_search_engines()
{
static $cached_search_engines = null;
if (null !== $cached_search_engines) {
return $cached_search_engines;
}
$data = get_transient('slimstat_matomo_searchengine');
if (false === $data) {
$json_path = plugin_dir_path(__FILE__) . 'admin/assets/data/matomo-searchengine.json';
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Local plugin file, WP_Filesystem not needed
$json = @file_get_contents($json_path);
$data = json_decode($json, true);
if (!is_array($data)) {
$data = [];
}
set_transient('slimstat_matomo_searchengine', $data, WEEK_IN_SECONDS);
}
$cached_search_engines = $data;
return $cached_search_engines;
}
// end get_search_engines
/**
* Toggles WordPress filters on date_i18n function
*/
public static function toggle_date_i18n_filters($_turn_on = true)
{
if ($_turn_on && !empty(self::$date_i18n_filters) && is_array(self::$date_i18n_filters)) {
foreach (self::$date_i18n_filters as $i18n_priority => $i18n_func_list) {
foreach ($i18n_func_list as $func_args) {
if (!empty($func_args['function']) && is_string($func_args['function'])) {
add_filter('date_i8n', $func_args['function'], $i18n_priority, intval($func_args['accepted_args']));
}
}
}
} elseif (!empty($GLOBALS['wp_filter']['date_i18n']['callbacks']) && is_array($GLOBALS['wp_filter']['date_i18n']['callbacks'])) {
self::$date_i18n_filters = $GLOBALS['wp_filter']['date_i18n']['callbacks'];
remove_all_filters('date_i18n');
}
}
// end toggle_date_i18n_filters
/**
* Calls the date_i18n function without filters
*/
public static function date_i18n($_format)
{
self::toggle_date_i18n_filters(false);
$date = date_i18n($_format);
self::toggle_date_i18n_filters(true);
return $date;
}
// end date_i18n
/**
* Returns the current timestamp in the same format stored in the dt column.
* MUST be used by all queries that compare against dt values.
*
* WordPress date_i18n('U') returns current_time('timestamp') — a legacy
* quirk where 'U' format includes the site's GMT offset. This matches
* how Processor::process() stores $stat['dt'] via self::date_i18n('U').
*
* @since 5.4.7
* @return int Current timestamp matching dt column format
*/
public static function now(): int {
return (int) self::date_i18n('U');
}
/**
* Returns default options with geolocation_provider set for fresh installs and resets.
*
* geolocation_provider is excluded from init_options() because init() merges
* those defaults into stored settings — which would override the legacy
* enable_maxmind flag on upgraded installs before lazy migration runs.
*
* Fresh installs default to DB-IP (free, no license key required).
*/
public static function get_fresh_defaults()
{
$defaults = self::init_options();
$defaults['geolocation_provider'] = 'dbip';
return $defaults;
}
/**
* Returns the current geolocation precision ('country' or 'city').
*/
public static function get_geolocation_precision()
{
return ('on' == self::$settings['geolocation_country']) ? 'country' : 'city';
}
/**
* Sets the default values for all the options
*/
public static function init_options()
{
return [
'version' => SLIMSTAT_ANALYTICS_VERSION,
'_migration_5460' => '0', // one-shot: reset broken v5.4.1 defaults on first boot after this build
'secret' => wp_hash(wp_generate_password(64, true, true)),
'browscap_last_modified' => 0,
// General
// -----------------------------------------------------------------------
// General - Tracker
'is_tracking' => 'on',
'track_admin_pages' => 'no',
'javascript_mode' => 'on', // Client mode: works with all caching plugins (WP Rocket, W3TC, etc.)
// General - WordPress Integration
'add_dashboard_widgets' => 'on',
'use_separate_menu' => 'on',
'add_posts_column' => 'no',
'posts_column_pageviews' => 'on',
'display_notifications' => 'on',
// General - Database
'auto_purge' => 420,
'auto_purge_delete' => 'on',
// Tracker
// -----------------------------------------------------------------------
// Tracker - Data Protection
// anonymize_ip: mask IP before storing; hash_ip: generate daily visitor_id based on masked IP + UA
'gdpr_enabled' => 'off', // v5.3.x had no GDPR — off by default; admin enables when ready
'anonymize_ip' => 'off', // Restored: full IPs stored by default (5.3.x behavior)
'hash_ip' => 'off', // Restored: no daily visitor hash by default (5.3.x behavior)
'set_tracker_cookie' => 'on', // v5.3.x default: session cookie identifies returning visitors
'use_slimstat_banner' => 'off', // Admin must explicitly enable via consent integration
'consent_integration' => '', // No CMP by default — admin selects when enabling GDPR
'consent_level_integration'=> 'statistics',
'opt_out_message' => '',
'gdpr_accept_button_text' => 'Accept',
'gdpr_decline_button_text' => 'Decline',
'gdpr_theme_mode' => 'auto', // 'light', 'dark', 'auto'
'anonymous_tracking' => 'off', // Changed: Enable anonymous tracking by default
'do_not_track' => 'off',
'display_opt_out' => 'no',
'opt_out_cookie_names' => '',
'opt_in_cookie_names' => '',
// Tracker - Link Tracking
'track_same_domain_referers' => 'no',
'do_not_track_outbound_classes_rel_href' => 'noslimstat,ab-item',
'extensions_to_track' => 'pdf,doc,xls,zip',
// Tracker - Advanced Options
// NOTE: geolocation_provider is intentionally NOT in init_options().
// init() merges these defaults into stored settings, which would override
// the legacy enable_maxmind flag on upgraded installs before lazy migration runs.
// Use get_fresh_defaults() for new installs and settings reset.
'geolocation_country' => 'on',
'session_duration' => 1800,
'extend_session' => 'no',
'enable_cdn' => 'no',
'ajax_relative_path' => 'no',
// Tracker - External Pages
'external_domains' => '',
// Reports
// -----------------------------------------------------------------------
// Reports - Functionality
'use_current_month_timespan' => 'no',
'posts_column_day_interval' => 28,
'rows_to_show' => '20',
'ip_lookup_service' => 'https://ip-api.com/#',
'comparison_chart' => 'on',
'show_display_name' => 'no',
'convert_resource_urls_to_titles' => 'on',
'convert_ip_addresses' => 'no',
// Reports - Access Log and World Map
'refresh_interval' => '60',
'number_results_raw_data' => '50',
'max_dots_on_map' => '50',
// Reports - Miscellaneous
'custom_css' => '',
'chart_colors' => '',
'mozcom_access_id' => '',
'mozcom_secret_key' => '',
'show_complete_user_agent_tooltip' => 'no',
'async_load' => 'no',
'limit_results' => '200',
'enable_sov' => 'no',
// Exclusions
// -----------------------------------------------------------------------
// Exclusions - User Properties
'ignore_wp_users' => 'no',
'ignore_spammers' => 'on',
'ignore_bots' => 'no',
'ignore_prefetch' => 'on',
'ignore_users' => '',
'ignore_ip' => '',
'ignore_countries' => '',
'ignore_languages' => '',
'ignore_browsers' => '',
'ignore_platforms' => '',
'ignore_capabilities' => '',
// Exclusions - Page Properties
'ignore_resources' => '',
'ignore_referers' => '',
'ignore_content_types' => '',
// Access Control
// -----------------------------------------------------------------------
// Access Control - Reports
'restrict_authors_view' => 'on',
'capability_can_view' => 'manage_options',
'can_view' => '',
// Access Control - Reports
'tracking_request_method' => 'ajax',
// Access Control - Customizer
'capability_can_customize' => 'manage_options',
'can_customize' => '',
// Access Control - Settings
'capability_can_admin' => 'manage_options',
'can_admin' => '',
// Access Control - REST API
'rest_api_tokens' => wp_hash(wp_generate_password(64, true, true)),
// Maintenance
// -----------------------------------------------------------------------
'last_tracker_error' => [0, '', 0],
'show_sql_debug' => 'no',
'slimstat_debug' => 'off',
'db_indexes' => 'on',
'enable_maxmind' => 'disable',
'maxmind_license_key' => '',
'enable_browscap' => 'no',
// Notices
// -----------------------------------------------------------------------
'notice_latest_news' => 'on',
'notice_browscap' => 'on',
'notice_browscap_fileinfo' => 'on',
'notice_geolite' => 'on',
'notice_caching' => 'on',
// Network-wide Settings
'locked_options' => '',
];
}
// end init_options
/**
* Saves a given option in the database
*/
public static function update_option($_key = '', $_value = '')
{
if (!is_network_admin()) {
update_option($_key, $_value);
} else {
update_site_option($_key, $_value);
}
}
// end update_option
/**
* Attach a script to every page to track visitors' screen resolution and other browser-based information
*/
public static function enqueue_tracker()
{
// Use the new unified tracking method setting
$method = self::$settings['tracking_request_method'] ?? 'rest';
// Handle legacy 'adblock' value (renamed to 'adblock_bypass' in v5.3.0)
if ( 'adblock' === $method ) {
$method = 'adblock_bypass';
}
// Prepare URLs for all methods
$rest_url = rest_url('slimstat/v1/hit');
$rest_base_url = rest_url();
// Mirror WordPress core's non-pretty REST routing so query fallback still works
// on index-permalink and subdirectory installs.
$rest_query_base = trailingslashit(get_home_url(null, '', 'rest'));
if ('index.php' !== substr(untrailingslashit($rest_query_base), -9)) {
$rest_query_base .= 'index.php';
}
$rest_query_url = add_query_arg('rest_route', '/slimstat/v1/hit', $rest_query_base);
$ajax_url = admin_url('admin-ajax.php');
$ajax_url_relative = admin_url('admin-ajax.php', 'relative');
$params = [
'transport' => $method,
'ajaxurl_rest' => $rest_url,
'ajaxurl_rest_query' => $rest_query_url,
'resturl' => $rest_base_url,
'ajaxurl_ajax' => ('on' == self::$settings['ajax_relative_path']) ? $ajax_url_relative : $ajax_url,
];
// Only provide adblock bypass URL when the rewrite rule is active.
// The rewrite rule is only registered for 'adblock_bypass' transport,
// so this URL would 404 for other transports — a dead fallback.
if ('adblock_bypass' === $method) {
$adblock_hash = \SlimStat\Providers\RestApiManager::getSecureAdblockHash();
$params['ajaxurl_adblock'] = home_url(sprintf('request/%s/', $adblock_hash));
}
// Set the primary ajaxurl based on the selected method
if ('rest' === $method) {
$params['ajaxurl'] = $rest_url;
} elseif ('ajax' === $method) {
$params['ajaxurl'] = ('on' == self::$settings['ajax_relative_path']) ? $ajax_url_relative : $ajax_url;
} elseif ('adblock_bypass' === $method) {
$params['ajaxurl'] = $params['ajaxurl_adblock'];
// Also set transport to 'adblock_bypass' for JS clarity
$params['transport'] = 'adblock_bypass';
} else {
$params['ajaxurl'] = $rest_url;
}
$baseurl = parse_url(get_home_url());
$params['baseurl'] = empty($baseurl['path']) ? '/' : $baseurl['path'];
if (!empty(self::$settings['do_not_track_outbound_classes_rel_href'])) {
$params['dnt'] = str_replace(' ', '', self::$settings['do_not_track_outbound_classes_rel_href']);
}
// Internal GDPR banner is optionally available alongside CMP integrations.
if ('on' != self::$settings['javascript_mode']) {
if (empty(self::$stat['id']) || intval(self::$stat['id']) < 0) {
return false;
}
$params['id'] = \SlimStat\Tracker\Utils::getValueWithChecksum(intval(self::$stat['id']));
} else {
$params['ci'] = \SlimStat\Tracker\Utils::getValueWithChecksum(\SlimStat\Tracker\Utils::base64UrlEncode(wp_json_encode(\SlimStat\Tracker\Utils::getContentInfo())));
}
// Always generate wp_rest_nonce (needed for consent banner CSRF protection).
// The JS uses is_logged_in to decide whether to send it as X-WP-Nonce header.
// Anonymous pages: is_logged_in='0' → no header → no 403 on cached pages.
// Admin-cached pages: is_logged_in='1' (stale) → sends nonce → may 403 → retry
// without nonce (handled by JS retry logic). This is acceptable since most caches
// exclude logged-in users, and the retry adds only one extra request.
$params['wp_rest_nonce'] = wp_create_nonce('wp_rest');
$params['is_logged_in'] = is_user_logged_in() ? '1' : '0';
// Expose consent/DNT info to client
$params['wp_consent_integration'] = (self::$settings['consent_integration'] ?? '') === 'wp_consent_api' ? 'enabled' : 'disabled';
$params['consent_integration'] = self::$settings['consent_integration'] ?? '';
$params['consent_level_integration'] = (self::$settings['consent_level_integration'] ?? 'statistics');
$params['respect_dnt'] = self::$settings['do_not_track'] ?? 'off';
$gdpr_enabled_setting = strtolower((string) (self::$settings['gdpr_enabled'] ?? 'off'));
$params['gdpr_enabled'] = in_array($gdpr_enabled_setting, ['off', 'no', 'false', '0'], true) ? 'off' : 'on';
$params['anonymous_tracking'] = self::$settings['anonymous_tracking'] ?? 'off';
$params['anonymize_ip'] = self::$settings['anonymize_ip'] ?? 'no';
$params['hash_ip'] = self::$settings['hash_ip'] ?? 'no';
$params['set_tracker_cookie'] = self::$settings['set_tracker_cookie'] ?? 'on';
// Mirror the same dual-condition guard used by the PHP banner output (lines 305-306):
// banner HTML is only rendered when BOTH gdpr_enabled=on AND use_slimstat_banner=on.
// If gdpr_enabled is off, the banner DOM never exists — JS must not enter banner-init mode
// or it will set a "ran" lock and silently skip _send_pageview for all visitors.
$params['use_slimstat_banner'] = ('on' === $params['gdpr_enabled'] && 'on' === (self::$settings['use_slimstat_banner'] ?? 'off')) ? 'on' : 'off';
if ('on' === $params['use_slimstat_banner']) {
// Set GDPR consent endpoint based on tracking method
if ('rest' === $method) {
$params['gdpr_consent_endpoint'] = rest_url('slimstat/v1/gdpr/consent');
} elseif ('ajax' === $method) {
$params['gdpr_consent_endpoint'] = ('on' == self::$settings['ajax_relative_path']) ? $ajax_url_relative : $ajax_url;
} elseif ('adblock_bypass' === $method) {
$params['gdpr_consent_endpoint'] = $params['ajaxurl_adblock'];
} else {
$params['gdpr_consent_endpoint'] = rest_url('slimstat/v1/gdpr/consent');
}
$params['gdpr_cookie_name'] = \SlimStat\Services\GDPRService::CONSENT_COOKIE_NAME;
$params['gdpr_cookie_path'] = defined('COOKIEPATH') ? COOKIEPATH : '/';
$params['gdpr_cookie_domain'] = defined('COOKIE_DOMAIN') ? COOKIE_DOMAIN : '';
$params['gdpr_consent_method'] = $method;
}
if ('on' === self::$settings['slimstat_debug'] || (defined('WP_DEBUG') && WP_DEBUG)) {
$params['slimstat_debug'] = 'on';
}
$params = apply_filters('slimstat_js_params', $params);
// Add dependencies for consent integrations (e.g., WP Consent API)
$dependencies = [];
if ((self::$settings['consent_integration'] ?? '') === 'wp_consent_api') {
// Only add dependency if the WP Consent API script is actually registered
if (wp_script_is('wp-consent-api', 'registered') || wp_script_is('wp-consent-api', 'enqueued')) {
$dependencies[] = 'wp-consent-api';
}
}
// Register the correct script for adblock bypass, CDN, or default
$local_script_version = SLIMSTAT_ANALYTICS_VERSION;
$local_script_path = plugin_dir_path(__FILE__) . 'wp-slimstat.min.js';
if (file_exists($local_script_path)) {
$local_script_version .= '.' . filemtime($local_script_path);
}
if ('adblock_bypass' === $method) {
$hash_js = md5(site_url() . 'slimstat');
wp_register_script('wp_slimstat', home_url(sprintf('/%s.js/', $hash_js)), $dependencies, SLIMSTAT_ANALYTICS_VERSION, true);
} elseif ('on' == self::$settings['enable_cdn']) {
wp_register_script('wp_slimstat', 'https://cdn.jsdelivr.net/wp/wp-slimstat/tags/' . SLIMSTAT_ANALYTICS_VERSION . '/wp-slimstat.min.js', $dependencies, null, true);
} else {
wp_register_script('wp_slimstat', plugins_url('/wp-slimstat.min.js', __FILE__), $dependencies, $local_script_version, true);
}
wp_enqueue_script('wp_slimstat');
/**
* Registers the 'wp_slimstat' script as an interactivity module if the registration function exists.
*
* Ensures compatibility with WordPress Interactivity API by registering the script module and its dependencies.
*/
if (function_exists('wp_interactivity_register_script_module')) {
wp_interactivity_register_script_module('wp_slimstat', [
'name' => 'wp_slimstat',
'dependencies' => [],
]);
}
wp_localize_script('wp_slimstat', 'SlimStatParams', $params);
return null;
}
// end enqueue_tracker
/**
* Enqueue assets for the internal SlimStat GDPR banner.
*
* @return void
*/
public static function enqueue_gdpr_assets()
{
if ('on' !== (self::$settings['use_slimstat_banner'] ?? 'off')) {
return;
}
wp_enqueue_style(
'wp_slimstat_gdpr_banner',
plugins_url('/assets/css/gdpr-banner.css', __FILE__),
[],
SLIMSTAT_ANALYTICS_VERSION
);
}
/**
* Render the SlimStat GDPR banner markup.
*
* @return void
*/
public static function render_gdpr_banner()
{
if ('on' !== (self::$settings['use_slimstat_banner'] ?? 'off')) {
return;
}
if (is_admin() && !wp_doing_ajax()) {
return;
}
$gdpr_service = new \SlimStat\Services\GDPRService(self::$settings);
$banner_html = $gdpr_service->getBannerHtml();
if ('' === $banner_html) {
return;
}
echo $banner_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Sanitized in GDPRService
}
public static function add_defer_to_script_tag($_tag, $_handle)
{
if ('wp_slimstat' === $_handle && false === stripos($_tag, 'defer')) {
$_tag = str_replace('<script ', '<script defer ', $_tag);
}
return $_tag;
}
/**
* Removes old entries from the main table and performs other daily tasks
*/
public static function wp_slimstat_purge()
{
$autopurge_interval = intval(self::$settings['auto_purge']);
if ($autopurge_interval <= 0) {
return;
}
$days_ago = self::now() - ( $autopurge_interval * DAY_IN_SECONDS );
$table_stats = $GLOBALS['wpdb']->prefix . 'slim_stats';
$table_stats_archive = $GLOBALS['wpdb']->prefix . 'slim_stats_archive';
$table_events = $GLOBALS['wpdb']->prefix . 'slim_events';
$table_events_archive = $GLOBALS['wpdb']->prefix . 'slim_events_archive';
// Copy entries to the archive table, if needed
if ('no' != self::$settings['auto_purge_delete']) {
// Use Query builder for INSERT INTO ... SELECT ... with prepared statements
$insert_sql = self::$wpdb->prepare(
"INSERT INTO {$table_stats_archive} (id, ip, other_ip, username, email, country, location, city, referer, resource, searchterms, notes, visit_id, server_latency, page_performance, browser, browser_version, browser_type, platform, language, fingerprint, user_agent, resolution, screen_width, screen_height, content_type, category, author, content_id, tz_offset, outbound_resource, dt_out, dt) SELECT id, ip, other_ip, username, email, country, location, city, referer, resource, searchterms, notes, visit_id, server_latency, page_performance, browser, browser_version, browser_type, platform, language, fingerprint, user_agent, resolution, screen_width, screen_height, content_type, category, author, content_id, tz_offset, outbound_resource, dt_out, dt FROM {$table_stats} WHERE dt < %d",
$days_ago
);
$is_copy_done = self::$wpdb->query($insert_sql);
if (false !== $is_copy_done) {
\SlimStat\Utils\Query::delete($table_stats)->where('dt', '<', $days_ago)->execute();
}
$insert_sql_events = self::$wpdb->prepare(
"INSERT INTO {$table_events_archive} (type, event_description, notes, position, id, dt) SELECT type, event_description, notes, position, id, dt FROM {$table_events} WHERE dt < %d",
$days_ago
);
$is_copy_done = self::$wpdb->query($insert_sql_events);
if (false !== $is_copy_done) {
\SlimStat\Utils\Query::delete($table_events)->where('dt', '<', $days_ago)->execute();
}
} else {
// Delete old entries
\SlimStat\Utils\Query::delete($table_stats)->where('dt', '<', $days_ago)->execute();
\SlimStat\Utils\Query::delete($table_events)->where('dt', '<', $days_ago)->execute();
}
// Optimize tables (keep as direct queries)
self::$wpdb->query('OPTIMIZE TABLE ' . $table_stats);
self::$wpdb->query('OPTIMIZE TABLE ' . $table_stats_archive);
self::$wpdb->query('OPTIMIZE TABLE ' . $table_events);
self::$wpdb->query('OPTIMIZE TABLE ' . $table_events_archive);
}
public static function wp_slimstat_update_geoip_database()
{
// Calculate the most recent "first Tuesday + 2 days" that has already passed
$this_month_update = strtotime('first Tuesday of this month') + (86400 * 2);
$current_time = time();
// If this month's update window hasn't arrived yet, use last month's window
if ($current_time < $this_month_update) {
$this_update = strtotime('first Tuesday of last month') + (86400 * 2);
} else {
$this_update = $this_month_update;
}
$last_update = get_option('slimstat_last_geoip_dl', 0);
if ($last_update < $this_update) {
// Determine which geolocation provider to use
$provider = self::resolve_geolocation_provider();
if (false === $provider) {
return;
}
try {
$geographicProvider = new \SlimStat\Services\Geolocation\GeolocationService($provider, []);
$ok = $geographicProvider->updateDatabase();
if ($ok) {
update_option('slimstat_last_geoip_dl', time());
}
} catch (\Throwable $e) {
wp_slimstat::log('Geolocation database update failed: ' . $e->getMessage(), 'error');
}
}
}
/**
* Register privacy policy content for WordPress Privacy Tools
*
* @since 5.4.0
*/
public static function registerPrivacyPolicyContent()
{
if (!function_exists('wp_add_privacy_policy_content')) {
return;
}
$content = '<h2>' . __('SlimStat Analytics', 'wp-slimstat') . '</h2>';
$content .= '<p><strong>' . __('What personal data we collect and why', 'wp-slimstat') . '</strong></p>';
$content .= '<p>' . __('SlimStat Analytics collects the following data about website visitors:', 'wp-slimstat') . '</p>';
$content .= '<ul>';
$content .= '<li>' . __('IP Address: Collected for analytics and security purposes. May be anonymized or hashed based on your privacy settings.', 'wp-slimstat') . '</li>';
$content .= '<li>' . __('Page URLs: Tracks which pages are visited to analyze website usage.', 'wp-slimstat') . '</li>';
$content .= '<li>' . __('Referrer Information: Tracks where visitors came from (search engines, other websites, etc.).', 'wp-slimstat') . '</li>';
$content .= '<li>' . __('Browser and Device Information: User agent, screen resolution, and device type for analytics.', 'wp-slimstat') . '</li>';
$content .= '<li>' . __('Timestamp: Date and time of each page visit.', 'wp-slimstat') . '</li>';
if ('on' === (self::$settings['set_tracker_cookie'] ?? 'off')) {
$content .= '<li>' . __('Cookies: A tracking cookie is used to identify returning visitors and maintain session continuity.', 'wp-slimstat') . '</li>';
}
if ('on' !== (self::$settings['ignore_wp_users'] ?? 'off')) {
$content .= '<li>' . __('User Information: If you are logged in, your username and email may be associated with your visits (only with consent when GDPR mode is enabled).', 'wp-slimstat') . '</li>';
}
$content .= '</ul>';
$content .= '<p><strong>' . __('How long we retain your data', 'wp-slimstat') . '</strong></p>';
$retention_days = intval(self::$settings['auto_purge'] ?? 420);
if ($retention_days > 0) {
$content .= '<p>' . sprintf(__('Analytics data is automatically deleted after %d days, in compliance with GDPR data retention requirements.', 'wp-slimstat'), $retention_days) . '</p>';
} else {
$content .= '<p>' . __('Analytics data retention is currently disabled. Please contact the site administrator for information about data retention policies.', 'wp-slimstat') . '</p>';
}
$content .= '<p><strong>' . __('Your rights', 'wp-slimstat') . '</strong></p>';
$content .= '<p>' . __('Under GDPR, you have the right to:', 'wp-slimstat') . '</p>';
$content .= '<ul>';
$content .= '<li>' . __('Access your personal data collected by SlimStat', 'wp-slimstat') . '</li>';
$content .= '<li>' . __('Request deletion of your personal data (Right to be Forgotten)', 'wp-slimstat') . '</li>';
$content .= '<li>' . __('Opt-out of tracking by revoking consent (if GDPR mode is enabled)', 'wp-slimstat') . '</li>';
$content .= '</ul>';
if ('on' === (self::$settings['gdpr_enabled'] ?? 'off')) {
$content .= '<p>' . __('You can exercise these rights by using the WordPress Privacy Tools (Tools → Export Personal Data / Erase Personal Data) or by contacting the site administrator.', 'wp-slimstat') . '</p>';
}
$content .= '<p><strong>' . __('Consent Management', 'wp-slimstat') . '</strong></p>';
if ('on' === (self::$settings['anonymous_tracking'] ?? 'off')) {
$content .= '<p>' . __('This website uses Anonymous Tracking Mode. Initial tracking occurs without collecting personally identifiable information (PII). Full tracking with PII collection only occurs after you grant explicit consent.', 'wp-slimstat') . '</p>';
} else {
$content .= '<p>' . __('Tracking requires your consent when GDPR mode is enabled. You can grant or revoke consent at any time through the consent management interface.', 'wp-slimstat') . '</p>';
}
wp_add_privacy_policy_content('SlimStat Analytics', $content);
}
public static function add_plugin_manual_download_link($_links = [], $_plugin_file = '')
{
$a_clean_slug = str_replace(['wp-slimstat-', '/index.php'], ['', ''], $_plugin_file);
if (false !== ($download_url = get_transient('wp-slimstat-download-link-' . $a_clean_slug))) {
$_links[] = '<a href="' . $download_url . '">Download ZIP</a>';
} else {
$url = 'https://www.wp-slimstat.com/update-checker/?slug=' . $a_clean_slug . '&key=' . urlencode(self::$settings['addon_licenses']['wp-slimstat-' . $a_clean_slug]);
$response = wp_safe_remote_get($url, ['timeout' => 300, 'user-agent' => 'Slimstat Analytics/' . SLIMSTAT_ANALYTICS_VERSION . '; ' . home_url()]);
if (!is_wp_error($response) && 200 == wp_remote_retrieve_response_code($response)) {
$data = @json_decode($response['body']);
if (is_object($data)) {
$_links[] = '<a href="' . $data->download_url . '">Download ZIP</a>';
set_transient('wp-slimstat-download-link-' . $a_clean_slug, $data->download_url, 172800); // 48 hours
}
}
}
return $_links;
}
/**
* Resolves a given IP address, by keeping a local cache of hostnames to avoid multiple requests to the DNS server
*/
public static function gethostbyaddr($_ip = '')
{
$hostname = get_transient('slimstat_' . $_ip);
if (empty($hostname)) {
$hostname = gethostbyaddr($_ip);
set_transient('slimstat_' . $_ip, $hostname, HOUR_IN_SECONDS);
}
return $hostname;
}
// end gethostbyaddr
/**
* Registers the Slimstat widget
*/
public static function register_widget()
{
return register_widget('slimstat_widget');
}
// end register_widget
/**
* Generates the key to see if a given host is listed as a search engine in the corresponding Json data file
*/
public static function get_lossy_url($_url = '')
{
return preg_replace(
[
'/^(w+\d*|search)\./',
'/(^|\.)m\./',
'/(\.(com|org|net|co|it|edu))?\.(ad|ae|af|ag|ai|al|am|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bl|bm|bn|bo|bq|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cu|cv|cw|cx|cy|cz|de|dj|dk|dm|do|dz|ec|ee|eg|eh|er|es|et|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mf|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|ss|st|sv|sx|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tr|tt|tv|tw|tz|ua|ug|um|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|za|zm|zw)(\/|$)/',
'/(^|\.)(ad|ae|af|ag|ai|al|am|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bl|bm|bn|bo|bq|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cu|cv|cw|cx|cy|cz|de|dj|dk|dm|do|dz|ec|ee|eg|eh|er|es|et|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mf|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|ss|st|sv|sx|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tr|tt|tv|tw|tz|ua|ug|um|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|za|zm|zw)\./',
],
[
'',
'$1',
'.{}$4',
'$1{}.',
],
$_url
);
}
// end get_lossy_url
/**
* Check if slimstat pro plugin is installed
*/
public static function pro_is_installed($pluginSlug = 'wp-slimstat-pro/wp-slimstat-pro.php')
{
include_once(ABSPATH . 'wp-admin/includes/plugin.php');
return (bool) is_plugin_active($pluginSlug);
}
/**
* create upload directory
*/
public static function create_upload_directory()
{
$upload_dir = self::$upload_dir;
wp_mkdir_p($upload_dir);
/**
* Create .htaccess to avoid public access.
*/
if (is_dir($upload_dir) && is_writable($upload_dir)) {
$htaccess_file = path_join($upload_dir, '.htaccess');
if (!file_exists($htaccess_file) && $handle = @fopen($htaccess_file, 'w')) {
fwrite($handle, "Deny from all\n");
fclose($handle);
}
}
}
public static function get_schedule_interval($schedule)
{
$schedulesInterval = wp_get_schedules();
$timeInterval = 86400;
if (isset($schedulesInterval[$schedule]['interval'])) {
$timeInterval = $schedulesInterval[$schedule]['interval'];
}
return $timeInterval;
}
}
// end of class declaration
class slimstat_widget extends WP_Widget
{
/**
* Sets up the widgets name etc
*/
public function __construct()
{
parent::__construct('slimstat_widget', 'Slimstat', [
'classname' => 'slimstat_widget',
'description' => 'Add a Slimstat report to your sidebar',
]);
}
/**
* Outputs the content of the widget
*
* @param array $args
* @param array $instance
*/
public function widget($_args = [], $_instance = [])
{
extract(shortcode_atts([
'slimstat_widget_id' => '',
'slimstat_widget_title' => '',
'slimstat_widget_filters' => '',
], $_instance));
if (!empty($slimstat_widget_title)) {
echo (empty($_args['before_title']) ? '<h2 class="widget-title">' : $_args['before_title']) . esc_html($slimstat_widget_title) . (empty($_args['after_title']) ? '</h2>' : $_args['after_title']);
}
if (!empty($slimstat_widget_id)) {
echo do_shortcode(sprintf("[slimstat f='widget' w='%s']%s[/slimstat]", $slimstat_widget_id, $slimstat_widget_filters));
} else {
echo '';
}
}
/**
* Outputs the options form on admin
*
* @param array $instance The widget options
*/
public function form($_instance)
{
extract(shortcode_atts([
'slimstat_widget_id' => '',
'slimstat_widget_title' => '',
'slimstat_widget_filters' => '',
], $_instance));
// Let's build the dropdown
include_once(plugin_dir_path(__FILE__) . 'admin/view/wp-slimstat-reports.php');
wp_slimstat_reports::init();
$select_options = '';
foreach (wp_slimstat_reports::$reports as $a_report_id => $a_report_info) {
$select_options .= sprintf("<option value='%s' ", $a_report_id) . (($slimstat_widget_id == $a_report_id) ? 'selected="selected"' : '') . sprintf('>%s</option>', $a_report_info[ 'title' ]);
}
?>
<p>
<label for="<?php echo esc_attr($this->get_field_id('slimstat_widget_id')); ?>"><?php _e('Report', 'wp-slimstat') ?></label>
<select class="widefat" id="<?php echo esc_attr($this->get_field_id('slimstat_widget_id')); ?>" name="<?php echo esc_attr($this->get_field_name('slimstat_widget_id')); ?>">
<option value="">Select a widget</option>
<?php echo $select_options ?>
</select>
</p>
<p>
<label for="<?php echo esc_attr($this->get_field_id('slimstat_widget_title')); ?>"><?php _e('Title', 'wp-slimstat') ?></label>
<input type="text" class="widefat" id="<?php echo esc_attr($this->get_field_id('slimstat_widget_title')); ?>" name="<?php echo esc_attr($this->get_field_name('slimstat_widget_title')); ?>" value="<?php echo trim(strip_tags($slimstat_widget_title)) ?>">
</p>
<p>
<label for="<?php echo esc_attr($this->get_field_id('slimstat_widget_filters')); ?>"><?php _e('Optional filters', 'wp-slimstat'); ?></label>
<a href="https://wp-slimstat.com/resources/what-is-the-syntax-of-a-slimstat-shortcode-#slimstat-operators" target="_blank">[?]</a>
<textarea class="widefat" id="<?php echo esc_attr($this->get_field_id('slimstat_widget_filters')); ?>" name="<?php echo esc_attr($this->get_field_name('slimstat_widget_filters')); ?>"><?php echo trim(strip_tags($slimstat_widget_filters)) ?></textarea>
</p>
<?php
}
/**
* Processing widget options on save
*
* @param array $new_instance The new options
* @param array $old_instance The previous options
*/
public function update($_new_instance, $_old_instance)
{
$instance = $_old_instance;
$instance['slimstat_widget_id'] = sanitize_key($_new_instance['slimstat_widget_id'] ?? '');
$instance['slimstat_widget_title'] = sanitize_text_field(wp_unslash($_new_instance['slimstat_widget_title'] ?? ''));
$instance['slimstat_widget_filters'] = sanitize_textarea_field(wp_unslash($_new_instance['slimstat_widget_filters'] ?? ''));
return $instance;
}
}
// Early initialize DB handle for add-ons that may access wp_slimstat::$wpdb before init() runs
if (empty(wp_slimstat::$wpdb) && isset($GLOBALS['wpdb'])) {
wp_slimstat::$wpdb = $GLOBALS['wpdb'];
}
// Ok, let's go, Sparky!
if (function_exists('add_action')) {
// Since we use sendBeacon, this function sends raw POST data, which does not populate the $_POST variable automatically
$http_content_type = isset($_SERVER['HTTP_CONTENT_TYPE']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_CONTENT_TYPE'])) : '';
$content_type = isset($_SERVER['CONTENT_TYPE']) ? sanitize_text_field(wp_unslash($_SERVER['CONTENT_TYPE'])) : '';
if ((!empty($http_content_type) || !empty($content_type)) && [] === $_POST) {
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Required for reading php://input stream
$raw_post_string = file_get_contents('php://input');
parse_str($raw_post_string, wp_slimstat::$raw_post_array);
// Sanitize the action key from the raw body before using it
if (!empty(wp_slimstat::$raw_post_array['action'])) {
wp_slimstat::$raw_post_array['action'] = sanitize_key(
wp_unslash(wp_slimstat::$raw_post_array['action'])
);
}
} elseif ([] !== $_POST) {
wp_slimstat::$raw_post_array = $_POST;
}
// Init the Ajax listener
if (!empty(wp_slimstat::$raw_post_array['action']) && 'slimtrack' == wp_slimstat::$raw_post_array['action']) {
// This is needed because admin-ajax.php is reading $_REQUEST to fire the corresponding action
// Use a hardcoded literal instead of passing the user-supplied value
if (empty($_POST['action'])) {
$_POST['action'] = 'slimtrack';
}
add_action('wp_ajax_nopriv_slimtrack', [\SlimStat\Tracker\Ajax::class, 'handle']);
add_action('wp_ajax_slimtrack', [\SlimStat\Tracker\Ajax::class, 'handle']);
}
// From the codex: You can't call register_activation_hook() inside a function hooked to the 'plugins_loaded' or 'init' hooks (or any other hook). These hooks are called before the plugin is loaded or activated.
if (is_admin()) {
include_once(plugin_dir_path(__FILE__) . 'admin/index.php');
register_activation_hook(__FILE__, ['wp_slimstat_admin', 'init_environment']);
register_deactivation_hook(__FILE__, ['wp_slimstat_admin', 'deactivate']);
}
add_action('widgets_init', ['wp_slimstat', 'register_widget']);
// Load textdomain at init (required by WordPress 6.7.0+)
add_action('init', ['wp_slimstat', 'load_textdomain'], 1);
// Add the appropriate actions
add_action('plugins_loaded', ['wp_slimstat', 'init'], 20);
// Add the action to fetch chart data
add_action('wp_ajax_slimstat_fetch_chart_data', [\SlimStat\Modules\Chart::class, 'ajaxFetchChartData']);
}
add_action('wp_ajax_slimstat_clear_cache', 'wp_slimstat_clear_cache_handler');
function wp_slimstat_clear_cache_handler()
{
if (!current_user_can('manage_options')) {
wp_send_json_error(__('Permission denied', 'wp-slimstat'));
}
// Optional: check nonce if you add it to JS
if (empty($_POST['security']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['security'])), 'slimstat_clear_cache')) {
wp_send_json_error(__('Invalid nonce', 'wp-slimstat'));
}
global $wpdb;
$transients = $wpdb->get_col(
sprintf("SELECT option_name FROM %s WHERE option_name LIKE '_transient_wp_slimstat_query_%%' OR option_name LIKE '_transient_timeout_wp_slimstat_query_%%'", $wpdb->options)
);
$count = 0;
foreach ($transients as $transient) {
delete_option($transient);
$count++;
}
wp_send_json_success(sprintf(__('Slimstat cache cleared (%d items)', 'wp-slimstat'), $count));
}