'Default Browser',
'browser_version' => '',
'browser_type' => 0,
'platform' => 'unknown',
'user_agent' => self::_get_user_agent(),
];
if (empty($browser['user_agent'])) {
return $browser;
}
if (null !== $cached && $cached['user_agent'] === $browser['user_agent']) {
return $cached;
}
if ('on' == wp_slimstat::$settings['enable_browscap'] && PHP_VERSION_ID >= 70400 && extension_loaded('fileinfo')) {
$browser = self::get_browser_from_browscap($browser, wp_slimstat::$upload_dir . '/browscap-cache-master/');
}
if ('Default Browser' == $browser['browser']) {
$browser = UADetector::get_browser($browser['user_agent']);
} elseif (empty($browser['browser_version'])) {
$browser_version = UADetector::get_browser($browser['user_agent']);
$browser['browser_version'] = $browser_version['browser_version'];
}
// Safety net: re-check any non-crawler type (desktop/mobile/touch) against
// BOT_GENERIC_REGEX. Browscap misclassifies Chrome-based Googlebot mobile
// UAs as type=2 because Android/Mobile signals match before the bot suffix.
if (1 !== (int) $browser['browser_type']) {
$browser = self::apply_bot_safety_net($browser);
}
// Let third-party tools manipulate the data
$browser = apply_filters('slimstat_filter_browscap', $browser);
$cached = $browser;
return $browser;
}
// end get_browser
/**
* Post-resolution bot safety net. Checks the UA string for known bot
* indicators when Browscap resolved a browser but did not flag it as
* a crawler (e.g., Chrome-based Googlebot identified as "Chrome").
*
* Only fires on the Browscap-resolved path — when UADetector runs
* full detection, its own generic bot regex already covers this.
*
* @since 5.4.9
* @param array $browser Browser data with 'user_agent' key.
* @return array Browser data with browser_type=1 if bot detected.
*/
public static function apply_bot_safety_net(array $browser): array
{
if (empty($browser['user_agent'])) {
return $browser;
}
if (preg_match(UADetector::BOT_GENERIC_REGEX, $browser['user_agent']) > 0) {
$browser['browser_type'] = 1;
}
return $browser;
}
public static function get_browser_from_browscap($_browser = [], $_cache_path = '')
{
// Flysystem's LocalFilesystemAdapter eagerly constructs FinfoMimeTypeDetector,
// which calls `new finfo(...)` and fatals on hosts without ext-fileinfo (#303).
if (!extension_loaded('fileinfo')) {
return $_browser;
}
try {
$file_cache = new LocalFilesystemAdapter($_cache_path);
$filesystem = new Filesystem($file_cache);
$cache = new SimpleCache(
new Flysystem($filesystem)
);
$logger = new NullLogger();
$browscap = new \SlimStat\Dependencies\BrowscapPHP\Browscap($cache, $logger);
$search_object = $browscap->getBrowser();
} catch (\Throwable $exception) {
\wp_slimstat::log('Browscap path failed: ' . $exception->getMessage(), 'error');
$search_object = '';
}
if (is_object($search_object) && 'Default Browser' != $search_object->browser && 'unknown' != $search_object->browser) {
$_browser['browser'] = $search_object->browser;
$_browser['browser_version'] = floatval($search_object->version);
$_browser['platform'] = strtolower($search_object->platform);
// Browser Types:
// 0: default (desktop, not touch)
// 1: crawler
// 2: mobile
// 3: touch, not mobile
if ($search_object->crawler) {
$_browser['browser_type'] = 1;
} elseif ($search_object->ismobiledevice || $search_object->istablet) {
$_browser['browser_type'] = 2;
} elseif (false !== stripos($search_object->device_pointing_method, 'touch')) {
$_browser['browser_type'] = 3;
}
}
return $_browser;
}
/**
* Downloads the Browscap User Agent database from our repository
*/
public static function update_browscap_database($_force_download = false)
{
if (defined('DOING_AJAX') && DOING_AJAX) {
return [2, __('No updates are performed during AJAX requests.', 'wp-slimstat')];
}
if (defined('FS_METHOD') && FS_METHOD != 'direct') {
return [3, __('Please set your FS_METHOD variable to "direct" in your wp-config.php file, or contact our support to obtain a copy of our Browscap Library.', 'wp-slimstat')];
}
// Create the folder, if it doesn't exist
if (!file_exists(wp_slimstat::$upload_dir)) {
wp_slimstat::create_upload_directory();
}
$download_remote_file = $_force_download;
$current_timestamp = intval(date('U'));
$browscap_zip = wp_slimstat::$upload_dir . '/browscap-db.zip';
if (empty(wp_slimstat::$settings['browscap_last_modified'])) {
if (file_exists(wp_slimstat::$upload_dir . '/browscap-cache-master/version.txt')) {
$file_stat = @stat(wp_slimstat::$upload_dir . '/browscap-cache-master/version.txt');
if (false !== $file_stat) {
wp_slimstat::$settings['browscap_last_modified'] = intval($file_stat['mtime']);
}
}
// The variable could be still empty if the file does not exist or stat failed to open it
if (empty(wp_slimstat::$settings['browscap_last_modified'])) {
wp_slimstat::$settings['browscap_last_modified'] = $current_timestamp;
}
}
// Check for updates once a week ( 604800 seconds ), if $_force_download is not true
if (file_exists(wp_slimstat::$upload_dir . '/browscap-cache-master/version.txt')) {
if ($current_timestamp - wp_slimstat::$settings['browscap_last_modified'] > 604800) {
// No matter what the outcome is, we'll check again in one week.
// Update only the timestamp key in the DB — do NOT write the full settings array.
// Writing wp_slimstat::$settings here persists runtime-derived values (e.g.
// use_slimstat_banner computed from consent_integration) and clobbers settings
// intentionally saved by the migration with different values.
wp_slimstat::$settings['browscap_last_modified'] = $current_timestamp;
$_stored_for_browscap = get_option('slimstat_options', []);
$_stored_for_browscap['browscap_last_modified'] = $current_timestamp;
wp_slimstat::update_option('slimstat_options', $_stored_for_browscap);
unset($_stored_for_browscap);
// Now check the version number on the server
$response = wp_remote_get('https://raw.githubusercontent.com/slimstat/browscap-cache/master/version.txt');
if (!is_array($response) || is_wp_error($response) || 200 != wp_remote_retrieve_response_code($response)) {
return [5, __('There was an error checking the remote library version. Please try again later.', 'wp-slimstat')];
}
$download_remote_file = (self::$browscap_local_version != trim(wp_remote_retrieve_body($response)));
} else {
return [0, __('Your version of the library does not need to be updated.', 'wp-slimstat')];
}
}
// Download the most recent version of our pre-processed Browscap database
if ($download_remote_file) {
// wp_remote_get (not wp_safe_remote_get) — GitHub archive URLs redirect to
// codeload.github.com; wp_safe_remote_get blocks external redirects on some hosts.
$response = wp_remote_get('https://github.com/slimstat/browscap-cache/archive/master.zip', ['timeout' => 300, 'stream' => true, 'filename' => $browscap_zip]);
if (!file_exists($browscap_zip)) {
return [6, __('There was an error saving the Browscap data file on your server. Please check your folder permissions.', 'wp-slimstat')];
}
if (is_wp_error($response) || 200 != wp_remote_retrieve_response_code($response)) {
$http_code = is_wp_error($response) ? $response->get_error_message() : wp_remote_retrieve_response_code($response);
@unlink($browscap_zip);
return [7, sprintf(__('There was an error downloading the Browscap data file (%s). Please try again later.', 'wp-slimstat'), $http_code)];
}
// Validate the downloaded file is actually a ZIP archive
$header = file_get_contents($browscap_zip, false, null, 0, 4);
if (empty($header) || $header !== "PK\x03\x04") {
@unlink($browscap_zip);
return [8, __('The downloaded Browscap file is not a valid ZIP archive. Your host may be blocking the download.', 'wp-slimstat')];
}
// Ensure WP File API is loaded (not auto-loaded on frontend init hook)
if (!function_exists('unzip_file')) {
require_once ABSPATH . 'wp-admin/includes/file.php';
}
// Initialize WP_Filesystem — required by unzip_file() (see file.php:1595)
if (!WP_Filesystem(false, wp_slimstat::$upload_dir)) {
@unlink($browscap_zip);
return [10, __('Could not initialize the WordPress filesystem. Please check your server permissions or set FS_METHOD to "direct" in wp-config.php.', 'wp-slimstat')];
}
// Delete the old cache only after WP_Filesystem succeeds — preserves
// working Browscap data if filesystem initialization fails.
wp_slimstat_admin::rmdir(wp_slimstat::$upload_dir . '/browscap-cache-master/');
// We're ready to unzip the file
$result = unzip_file($browscap_zip, wp_slimstat::$upload_dir);
if (is_wp_error($result)) {
return [9, sprintf(__('There was an error uncompressing the Browscap data file: %s', 'wp-slimstat'), $result->get_error_message())];
}
@unlink($browscap_zip);
}
return [0, __('The Browscap data file has been installed on your server.', 'wp-slimstat')];
}
protected static function _get_user_agent()
{
// CVE-2026-7634: sanitize at the source so a malicious UA cannot reach
// storage or render layers as raw HTML. Mirrors the pattern used in
// Session.php and IPHashProvider.php. Bot/crawler regex matching downstream
// (UADetector::BOT_GENERIC_REGEX, BrowscapPHP) operates on alphanumerics
// and punctuation that sanitize_text_field preserves.
$user_agent = empty($_SERVER['HTTP_USER_AGENT'])
? ''
: trim(sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])));
$real_user_agent = '';
if (!empty($_SERVER['HTTP_X_DEVICE_USER_AGENT'])) {
$real_user_agent = trim(sanitize_text_field(wp_unslash($_SERVER['HTTP_X_DEVICE_USER_AGENT'])));
} elseif (!empty($_SERVER['HTTP_X_ORIGINAL_USER_AGENT'])) {
$real_user_agent = trim(sanitize_text_field(wp_unslash($_SERVER['HTTP_X_ORIGINAL_USER_AGENT'])));
} elseif (!empty($_SERVER['HTTP_X_MOBILE_UA'])) {
$real_user_agent = trim(sanitize_text_field(wp_unslash($_SERVER['HTTP_X_MOBILE_UA'])));
} elseif (!empty($_SERVER['HTTP_X_OPERAMINI_PHONE_UA'])) {
$real_user_agent = trim(sanitize_text_field(wp_unslash($_SERVER['HTTP_X_OPERAMINI_PHONE_UA'])));
}
if ('' !== $real_user_agent && '0' !== $real_user_agent && (strlen($real_user_agent) >= 5 || ('' === $user_agent || '0' === $user_agent))) {
return $real_user_agent;
}
return $user_agent;
}
}