is_writable($resolved) || !wplocation_private_database_directory(dirname($resolved), $filesystem) || strtolower(pathinfo($resolved, PATHINFO_EXTENSION)) !== 'mmdb') { return 'configuration_error'; } $lock = wplocation_acquire_database_lock(); if (!$lock) { return 'busy'; } $archive = false; $candidate = false; try { list($current_account, $current_secret) = wplocation_maxmind_credentials(); if (!get_option('wplocation_updates_enabled') || $account !== $current_account || $secret !== $current_secret || !wplocation_source_matches($source_at_start) || wplocation_database_path() !== $target) { return 'configuration_error'; } $old = new \MaxMind\Db\Reader($resolved); $old_meta = $old->metadata(); $old_epoch = (int) $old_meta->buildEpoch; $old_type = $old_meta->databaseType; $old->close(); if ($old_type !== 'GeoLite2-Country') { return 'configuration_error'; } $reply = wp_safe_remote_get('https://download.maxmind.com/geoip/databases/GeoLite2-Country/download?suffix=tar.gz', array( 'headers' => array('Authorization' => 'Basic ' . base64_encode($account . ':' . $secret)), 'redirection' => 0, 'timeout' => 30, 'limit_response_size' => 1024, )); if (is_wp_error($reply) || !in_array(wp_remote_retrieve_response_code($reply), array(301, 302, 303, 307, 308), true)) { return 'download_error'; } $location = wp_remote_retrieve_header($reply, 'location'); $url = wp_parse_url($location); $host = 'mm-prod-geoip-databases.a2649acb697e2c09b632799562c076f2.r2.cloudflarestorage.com'; if (!is_array($url) || ($url['scheme'] ?? '') !== 'https' || strtolower($url['host'] ?? '') !== $host || isset($url['user']) || isset($url['pass']) || (isset($url['port']) && (int) $url['port'] !== 443)) { return 'download_error'; } $archive = tempnam(dirname($resolved), '.wpl-archive-'); $candidate = tempnam(dirname($resolved), '.wpl-db-'); if (!$archive || !$candidate) { return 'file_error'; } if (!$filesystem->chmod($archive, 0600) || !$filesystem->chmod($candidate, 0600)) { return 'file_error'; } // No Authorization header is sent to the signed download host. $file_reply = wp_safe_remote_get($location, array('redirection' => 0, 'timeout' => 90, 'stream' => true, 'filename' => $archive, 'limit_response_size' => 33554433)); if (is_wp_error($file_reply) || wp_remote_retrieve_response_code($file_reply) !== 200 || filesize($archive) > 33554432 || !wplocation_verify_gzip_integrity($archive, $filesystem) || !wplocation_extract_country_database($archive, $candidate, $filesystem)) { return 'download_error'; } $new = new \MaxMind\Db\Reader($candidate); $meta = $new->metadata(); $type = $meta->databaseType; $epoch = (int) $meta->buildEpoch; $new->get('8.8.8.8'); $new->close(); if ($type !== 'GeoLite2-Country' || $epoch < $old_epoch || $epoch <= 0 || $epoch > time() + DAY_IN_SECONDS) { return 'invalid_database'; } $mode = fileperms($resolved); if ($mode !== false && !$filesystem->chmod($candidate, $mode & 0777)) { return 'file_error'; } if (!wplocation_install_managed_database($candidate, $resolved, $filesystem, $source_at_start)) { return 'file_error'; } $candidate = false; if (function_exists('wplocation_purge_supported_cache')) { wplocation_purge_supported_cache(); } return 'success'; } catch (Throwable $error) { return 'update_error'; } finally { if ($archive && is_file($archive)) { $filesystem->delete($archive); } if ($candidate && is_file($candidate)) { $filesystem->delete($candidate); } wplocation_release_database_lock($lock); } } /** Explicit first GeoLite2 download or provider switch using the saved account. */ function wplocation_maxmind_install_initial($directory) { list($account, $secret) = wplocation_maxmind_credentials(); if (defined('WPLOCATION_GEOIP_DB_PATH') || !ctype_digit((string) $account) || $secret === '') { return 'not_configured'; } require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php'; require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php'; $filesystem = new WP_Filesystem_Direct(null); $dir = wplocation_private_database_directory($directory, $filesystem); if (!$dir) { return 'configuration_error'; } $lock = wplocation_acquire_database_lock(); if (!$lock) { return 'busy'; } $archive = false; $candidate = false; $destination = false; $created = false; $committed = false; $old_path = get_option('wplocation_db_path', false); $old_managed = get_option('wplocation_managed_database', false); $old_maxmind_updates = get_option('wplocation_updates_enabled', false); try { if (wplocation_maxmind_credentials() !== array($account, $secret)) { return 'configuration_error'; } $reply = wp_safe_remote_get('https://download.maxmind.com/geoip/databases/GeoLite2-Country/download?suffix=tar.gz', array( 'headers' => array('Authorization' => 'Basic ' . base64_encode($account . ':' . $secret)), 'redirection' => 0, 'timeout' => 30, 'limit_response_size' => 1024, )); if (is_wp_error($reply) || !in_array(wp_remote_retrieve_response_code($reply), array(301, 302, 303, 307, 308), true)) { return 'download_error'; } $location = wp_remote_retrieve_header($reply, 'location'); $url = wp_parse_url($location); $host = 'mm-prod-geoip-databases.a2649acb697e2c09b632799562c076f2.r2.cloudflarestorage.com'; if (!is_array($url) || ($url['scheme'] ?? '') !== 'https' || strtolower($url['host'] ?? '') !== $host || isset($url['user']) || isset($url['pass']) || (isset($url['port']) && (int) $url['port'] !== 443)) { return 'download_error'; } $archive = tempnam($dir, '.wpl-mm-gz-'); $candidate = tempnam($dir, '.wpl-mm-db-'); if (!$archive || !$candidate || !$filesystem->chmod($archive, 0600) || !$filesystem->chmod($candidate, 0600)) { return 'file_error'; } // The signed file host never receives the MaxMind Authorization header. $file_reply = wp_safe_remote_get($location, array('redirection' => 0, 'timeout' => 90, 'stream' => true, 'filename' => $archive, 'limit_response_size' => 33554433)); if (is_wp_error($file_reply) || wp_remote_retrieve_response_code($file_reply) !== 200 || filesize($archive) > 33554432 || !wplocation_verify_gzip_integrity($archive, $filesystem) || !wplocation_extract_country_database($archive, $candidate, $filesystem)) { return 'download_error'; } $reader = new \MaxMind\Db\Reader($candidate); $meta = $reader->metadata(); $reader->get('8.8.8.8'); $reader->close(); $epoch = (int) $meta->buildEpoch; if ($meta->databaseType !== 'GeoLite2-Country' || $epoch <= 0 || $epoch < time() - 62 * DAY_IN_SECONDS || $epoch > time() + DAY_IN_SECONDS) { return 'invalid_database'; } $destination = $dir . DIRECTORY_SEPARATOR . '.wplocation-source-' . wp_generate_uuid4() . '.mmdb'; if ($filesystem->exists($destination) || !$filesystem->move($candidate, $destination, false)) { return 'file_error'; } $candidate = false; $created = true; update_option('wplocation_db_path', $destination, false); if (get_option('wplocation_db_path') !== $destination) { return 'file_error'; } wplocation_retire_selected_managed_database(); delete_option('wplocation_managed_database'); update_option('wplocation_updates_enabled', 0, false); wplocation_update_schedule(); $committed = true; wplocation_purge_supported_cache(); return 'success'; } catch (Throwable $error) { return 'update_error'; } finally { if (!$committed) { if (get_option('wplocation_db_path') === $destination) { if (false === $old_path) { delete_option('wplocation_db_path'); } else { update_option('wplocation_db_path', $old_path, false); } if (false === $old_managed) { delete_option('wplocation_managed_database'); } else { update_option('wplocation_managed_database', $old_managed, false); } update_option('wplocation_updates_enabled', $old_maxmind_updates, false); wplocation_update_schedule(); } if ($created && $destination && is_file($destination)) { $filesystem->delete($destination); } } if ($archive && is_file($archive)) { $filesystem->delete($archive); } if ($candidate && is_file($candidate)) { $filesystem->delete($candidate); } wplocation_release_database_lock($lock); } } /** Verify gzip CRC and uncompressed size before accepting a download. */ function wplocation_verify_gzip_integrity($archive, $filesystem) { if (!function_exists('gzopen')) { return false; } $bytes = $filesystem->get_contents($archive); if (!is_string($bytes) || strlen($bytes) < 18 || substr($bytes, 0, 2) !== "\x1f\x8b") { return false; } $trailer = unpack('Vcrc/Visize', substr($bytes, -8)); unset($bytes); if (!is_array($trailer)) { return false; } $input = gzopen($archive, 'rb'); if (!$input) { return false; } $hash = hash_init('crc32b'); $size = 0; $valid = true; while (!gzeof($input)) { $chunk = gzread($input, 65536); if ($chunk === false) { $valid = false; break; } if ($chunk === '') { if (!gzeof($input)) { $valid = false; } break; } $size += strlen($chunk); if ($size > 134217728) { $valid = false; break; } hash_update($hash, $chunk); } gzclose($input); $crc = hash_final($hash); return $valid && $size === (int) $trailer['isize'] && hash_equals(sprintf('%08x', $trailer['crc']), $crc); } /** Read one regular MMDB member; never extract paths from the tar archive. */ function wplocation_extract_country_database($archive, $destination, $filesystem) { if (!function_exists('gzopen')) { return false; } $input = gzopen($archive, 'rb'); if (!$input) { return false; } $found = false; $total = 0; $ok = true; $database = ''; while (!gzeof($input)) { $header = gzread($input, 512); if ($header === '' || $header === false || strlen($header) !== 512) { $ok = false; break; } if ($header === str_repeat("\0", 512)) { break; } $name = rtrim(substr($header, 0, 100), "\0"); $size_text = trim(substr($header, 124, 12), " \0"); if ($size_text === '' || !preg_match('/^[0-7]+$/', $size_text)) { $ok = false; break; } $size = octdec($size_text); $total += 512 + $size; if ($size > 67108864 || $total > 134217728) { $ok = false; break; } $type = substr($header, 156, 1); $match = basename($name) === 'GeoLite2-Country.mmdb' && ($type === '0' || $type === "\0"); if ($match && $found) { $ok = false; break; } if ($match) { $limit = wp_convert_hr_to_bytes(ini_get('memory_limit')); if ($size > 33554432 || ($limit > 0 && $limit - memory_get_usage(true) < $size * 3 + 16777216)) { $ok = false; break; } $found = true; } $remaining = $size; while ($remaining > 0) { $chunk = gzread($input, min(65536, $remaining)); if ($chunk === false || $chunk === '') { $ok = false; break 2; } if ($match) { $database .= $chunk; } $remaining -= strlen($chunk); } $padding = (512 - ($size % 512)) % 512; if ($padding && strlen((string) gzread($input, $padding)) !== $padding) { $ok = false; break; } } gzclose($input); return $ok && $found && $filesystem->put_contents($destination, $database, 0600); } function wplocation_scheduled_database_update() { $result = wplocation_update_database(); update_option('wplocation_update_status', array('result' => $result, 'time' => time()), false); wplocation_record_event('maxmind_update', $result); } add_action('wplocation_update_database_event', 'wplocation_scheduled_database_update'); function wplocation_update_schedule() { if (get_option('wplocation_updates_enabled') && !wp_next_scheduled('wplocation_update_database_event')) { wp_schedule_event(time() + HOUR_IN_SECONDS, 'daily', 'wplocation_update_database_event'); } if (!get_option('wplocation_updates_enabled')) { wp_clear_scheduled_hook('wplocation_update_database_event'); } } add_action('init', 'wplocation_update_schedule'); function wplocation_clear_update_schedule() { wp_clear_scheduled_hook('wplocation_update_database_event'); } register_deactivation_hook(WPLOCATION_PLUGIN_FILE, 'wplocation_clear_update_schedule');