'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; } }