suppress_errors(true); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- A plain INSERT is required for an atomic, single-owner database lock; add_option() can upsert. $inserted = $wpdb->query($wpdb->prepare("INSERT INTO {$wpdb->options} (option_name, option_value, autoload) VALUES (%s, %s, %s)", 'wplocation_update_lock', $token, 'no')); $wpdb->suppress_errors($previous_suppression); if ($inserted !== 1) { return false; } wp_cache_delete('wplocation_update_lock', 'options'); wp_cache_delete('notoptions', 'options'); // Another request may have changed these options while this request waited. foreach (array('wplocation_db_path', 'wplocation_managed_database', 'wplocation_managed_files', 'wplocation_updates_enabled', 'wplocation_maxmind_account_id', 'wplocation_maxmind_license_key') as $name) { wp_cache_delete($name, 'options'); } wp_cache_delete('alloptions', 'options'); return $token; } function wplocation_database_lock_value() { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Read the lock from the database, not a potentially stale object cache. return $wpdb->get_var($wpdb->prepare("SELECT option_value FROM {$wpdb->options} WHERE option_name = %s LIMIT 1", 'wplocation_update_lock')); } function wplocation_release_database_lock($token) { if (!is_string($token) || $token === '') { return false; } global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Conditional DELETE ensures only the lock holder can release its token. $removed = $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->options} WHERE option_name = %s AND BINARY option_value = BINARY %s", 'wplocation_update_lock', $token)); if ($removed !== 1) { return false; } wp_cache_delete('wplocation_update_lock', 'options'); wp_cache_delete('alloptions', 'options'); wp_cache_delete('notoptions', 'options'); return true; } function wplocation_database_lock_is_old($value) { return is_string($value) && preg_match('/^(\d+)(?::[a-f0-9-]+)?$/', $value, $match) && (int) $match[1] > 0 && (int) $match[1] <= time() - HOUR_IN_SECONDS; } /** Validate a writable private directory for first database downloads. */ function wplocation_private_database_directory($directory, $filesystem = null) { $dir = is_string($directory) ? realpath($directory) : false; if (!$dir || !is_dir($dir) || ($filesystem && !$filesystem->is_writable($dir))) { return false; } $roots = array(realpath(ABSPATH), realpath(WP_CONTENT_DIR), realpath(WP_PLUGIN_DIR)); if (!empty($_SERVER['DOCUMENT_ROOT']) && is_string($_SERVER['DOCUMENT_ROOT'])) { $roots[] = realpath(sanitize_text_field(wp_unslash($_SERVER['DOCUMENT_ROOT']))); } foreach ($roots as $root) { if ($root && ($dir === $root || strpos($dir, rtrim($root, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR) === 0)) { return false; } } return $dir; } /** Select a previously validated managed copy only for the configured source. */ function wplocation_configured_database_path() { return defined('WPLOCATION_GEOIP_DB_PATH') ? WPLOCATION_GEOIP_DB_PATH : get_option('wplocation_db_path', ''); } function wplocation_active_database_path() { $source = wplocation_configured_database_path(); $selected = get_option('wplocation_managed_database', array()); if (!is_string($source) || !is_array($selected) || ($selected['source'] ?? null) !== $source || !isset($selected['file']) || !is_string($selected['file'])) { return $source; } $source_dir = realpath(dirname($source)); $file = $selected['file']; $resolved = realpath($file); if (!$source_dir || !$resolved || is_link($file) || !is_file($resolved) || dirname($resolved) !== $source_dir || !preg_match('/^\.wplocation-[a-f0-9-]+\.mmdb$/', basename($resolved))) { return $source; } return $resolved; } /** Resolve per site on each request, including after switch_to_blog(). * Preserve the legacy constant if another integration explicitly defined it. */ function wplocation_database_path() { // Respect an explicit legacy constant, but do not define a global alias // ourselves. The active path can change after a validated database update. $path = defined('WPLOCATION_GEOIP_DB_PATH') ? wplocation_active_database_path() : (defined('GEOIPDBFILE') ? GEOIPDBFILE : wplocation_active_database_path()); return is_string($path) ? $path : ''; } /** Re-read the stored option rather than trusting a request-local cache after a * long download; another administrator may have changed the source meanwhile. */ function wplocation_source_matches($expected) { if (!is_string($expected)) { return false; } if (defined('WPLOCATION_GEOIP_DB_PATH')) { return WPLOCATION_GEOIP_DB_PATH === $expected; } global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Re-read after a long download to reject a concurrent source switch. $stored = $wpdb->get_var($wpdb->prepare("SELECT option_value FROM {$wpdb->options} WHERE option_name = %s LIMIT 1", 'wplocation_db_path')); return is_string($stored) && $stored === $expected; } /** Install a new immutable file, then switch the database option in one write. * Existing readers and the original file remain available if anything fails. */ function wplocation_install_managed_database($candidate, $current, $filesystem, $expected_source) { $source = wplocation_configured_database_path(); if (!is_string($expected_source) || $source !== $expected_source || !wplocation_source_matches($expected_source)) { return false; } $source_dir = is_string($source) ? realpath(dirname($source)) : false; if (!$source_dir || $source_dir !== dirname($current) || dirname($candidate) !== $source_dir) { return false; } $destination = $source_dir . DIRECTORY_SEPARATOR . '.wplocation-' . wp_generate_uuid4() . '.mmdb'; if ($filesystem->exists($destination) || !$filesystem->move($candidate, $destination, false)) { return false; } $hash = hash_file('sha256', $destination); if (!is_string($hash)) { $filesystem->delete($destination); return false; } $owned = get_option('wplocation_managed_files', array()); if (!is_array($owned)) { $owned = array(); } $owned[$destination] = array('source' => $source, 'created' => time(), 'sha256' => $hash); update_option('wplocation_managed_files', $owned, false); if (get_option('wplocation_managed_files') !== $owned) { $filesystem->delete($destination); return false; } $previous = get_option('wplocation_managed_database', array()); $selection = array('source' => $source, 'file' => $destination); if (!wplocation_source_matches($expected_source)) { if ($filesystem->delete($destination)) { unset($owned[$destination]); update_option('wplocation_managed_files', $owned, false); } return false; } update_option('wplocation_managed_database', $selection, false); if (get_option('wplocation_managed_database') !== $selection) { if ($filesystem->delete($destination)) { unset($owned[$destination]); update_option('wplocation_managed_files', $owned, false); } return false; } if (is_array($previous) && ($previous['source'] ?? null) === $source && isset($previous['file']) && isset($owned[$previous['file']])) { $owned[$previous['file']]['retired'] = time(); update_option('wplocation_managed_files', $owned, false); } return true; } /** Retire a recorded managed copy when the administrator selects another source. */ function wplocation_retire_selected_managed_database() { $selected = get_option('wplocation_managed_database', array()); $owned = get_option('wplocation_managed_files', array()); if (!is_array($selected) || !is_array($owned) || empty($selected['file']) || !is_string($selected['file'])) { return; } $file = $selected['file']; if (!isset($owned[$file]) || !is_array($owned[$file]) || ($owned[$file]['source'] ?? null) !== ($selected['source'] ?? null)) { return; } $owned[$file]['retired'] = time(); update_option('wplocation_managed_files', $owned, false); } /** Remove only recorded, unchanged copies for the currently configured source. * Leave recent copies and the selected file intact; never scan or delete a * user-supplied database. Concurrent database updates use the same lock. */ function wplocation_cleanup_managed_databases() { $lock = wplocation_acquire_database_lock(); if (!$lock) { return; } try { $owned = get_option('wplocation_managed_files', array()); $selection = get_option('wplocation_managed_database', array()); $active = is_array($selection) && isset($selection['file']) ? $selection['file'] : ''; if (!is_array($owned)) { return; } 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); $changed = false; foreach ($owned as $path => $entry) { $dir = is_array($entry) && isset($entry['source']) && is_string($entry['source']) ? realpath(dirname($entry['source'])) : false; if (!is_string($path) || !is_array($entry) || !$dir || !isset($entry['retired'], $entry['sha256']) || !is_string($entry['sha256']) || !preg_match('/^[a-f0-9]{64}$/', $entry['sha256']) || (int) $entry['retired'] <= 0 || (int) $entry['retired'] > time() - 2 * DAY_IN_SECONDS || $path === $active || is_link($path) || dirname($path) !== $dir || !preg_match('/^\.wplocation-[a-f0-9-]+\.mmdb$/', basename($path))) { continue; } if (!$filesystem->exists($path)) { unset($owned[$path]); $changed = true; continue; } if (!is_file($path) || realpath($path) !== $path || !hash_equals($entry['sha256'], (string) hash_file('sha256', $path))) { continue; } if ($filesystem->delete($path)) { unset($owned[$path]); $changed = true; } } if ($changed) { update_option('wplocation_managed_files', $owned, false); } } finally { wplocation_release_database_lock($lock); } } add_action('wplocation_cleanup_managed_databases', 'wplocation_cleanup_managed_databases'); function wplocation_schedule_database_cleanup() { if (get_option('wplocation_managed_files') && !wp_next_scheduled('wplocation_cleanup_managed_databases')) { wp_schedule_event(time() + DAY_IN_SECONDS, 'daily', 'wplocation_cleanup_managed_databases'); } } add_action('init', 'wplocation_schedule_database_cleanup'); function wplocation_deactivate_database_cleanup() { wp_clear_scheduled_hook('wplocation_cleanup_managed_databases'); } register_deactivation_hook(WPLOCATION_PLUGIN_FILE, 'wplocation_deactivate_database_cleanup');