. namespace core\task; use core\http_client; use moodle_exception; use PharData; /** * Simple task to update the GeoIP database file. * * @package core * @author Trisha Milan * @copyright Monash University 2024 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class update_geoip2file_task extends scheduled_task { /** * Get a descriptive name for this task (shown to admins). * * @return string */ public function get_name(): string { return get_string('taskupdategeoip2file', 'admin'); } /** * Execute the task to update the GeoIP2 database file. * * @throws \GuzzleHttp\Exception\GuzzleException * @throws moodle_exception */ public function execute(): void { global $CFG; if (!$CFG->geoipmaxmindaccid || !$CFG->geoipmaxmindlicensekey) { mtrace("MaxMind account information is incomplete. Please configure the account ID and license key."); return; } // Setup base directory path and permissions. $geoip2file = $CFG->geoip2file; $geoipdirectory = dirname($geoip2file); if (!check_dir_exists($geoipdirectory) && !mkdir($geoipdirectory, $CFG->directorypermissions, true)) { throw new moodle_exception("Cannot create output directory $geoipdirectory"); } $geoippermalink = 'https://download.maxmind.com/geoip/databases/' . $CFG->geoipdbedition . '/download'; $client = new http_client(['auth' => [$CFG->geoipmaxmindaccid, $CFG->geoipmaxmindlicensekey]]); $response = $client->head($geoippermalink, ['query' => ['suffix' => 'tar.gz']]); $headers = $response->getHeaders(); $lastmodified = strtotime($headers['Last-Modified'][0]); if (!$this->is_update_needed($geoip2file, $lastmodified)) { mtrace("No update needed. The GeoIP database is up to date."); return; } // Define path for downloading the GeoIP2 archive. $archivefilename = 'GeoIP-City.tar.gz'; $tempdirectory = make_request_directory(true); $geoipdownloadpath = $tempdirectory . '/' . $archivefilename; mtrace("Downloading $CFG->geoipdbedition database from MaxMind......"); $response = $client->request('GET', $geoippermalink, [ 'query' => ['suffix' => 'tar.gz'], 'sink' => $geoipdownloadpath, ]); if ($response->getStatusCode() != 200) { throw new moodle_exception("Error downloading file."); } mtrace("File downloaded successfully to $geoipdownloadpath"); mtrace("Verifying checksum......"); // Get the latest checksum from MaxMind. $checksumcontent = $client->get($geoippermalink, ['query' => ['suffix' => 'tar.gz.sha256']])->getBody()->getContents(); list($checksum) = explode(' ', $checksumcontent); if (!$this->verify_checksum($checksum, $geoipdownloadpath)) { throw new moodle_exception("Checksum verification failed."); } mtrace("Checksum verified successfully."); if ($this->update_geoip2file($geoipdownloadpath, $tempdirectory, $geoip2file)) { // Store the last seen timestamp. set_config('geoip_last_seen_timestamp', $lastmodified); mtrace("GeoIP database update successful!"); } else { throw new moodle_exception("GeoIP database update failed."); } } /** * Determines if an update is needed for the GeoIP2 file based on the last modified date. * * @param string $geoip2file The path to the GeoIP2 file that needs to be checked for updates. * @param string $lastmodified The last modified date to be compared against the stored last seen timestamp. * @return bool */ private function is_update_needed(string $geoip2file, string $lastmodified): bool { return !file_exists($geoip2file) || $lastmodified !== get_config('core', 'geoip_last_seen_timestamp'); } /** * Verify the checksum of the downloaded file against an expected checksum. * * @param string $expectedchecksum The checksum expected for the file. * @param string $geoipdownloadpath The path where the downloaded geoip archive is located. * @return bool Returns true if the checksums match, returns false otherwise. */ private function verify_checksum(string $expectedchecksum, string $geoipdownloadpath): bool { $actualchecksum = hash_file('sha256', $geoipdownloadpath); return $expectedchecksum === $actualchecksum; } /** * Extract the archive and update the GeoIP2 database file. * * @param string $archivepath The path to the archive file that needs to be extracted. * @param string $targetdirectory Directory where the archive contents will be extracted. * @param string $geoip2file The path to move the extracted GeoIP2 file. * @return bool Returns true if the file was successfully extracted and moved to the specified location, * false if any part of the process fails. */ private function update_geoip2file(string $archivepath, string $targetdirectory, string $geoip2file): bool { $archive = new PharData($archivepath); $archivename = $archive->getFilename(); mtrace("Extracting file......"); $archive->extractTo($targetdirectory); $sourcefolder = $targetdirectory . '/' . $archivename; // Find the mmdb file. $mmdbfiles = glob($sourcefolder . '/*.mmdb'); if (count($mmdbfiles) > 1) { throw new moodle_exception("Multiple .mmdb files found in the extracted folder."); } else if (count($mmdbfiles) === 0) { throw new moodle_exception("GeoIP file does not exist."); } // Backup existing GeoIP file before attempting to update. $geoip2filename = basename($geoip2file); $backuppath = $targetdirectory . '/' . 'backup_' . $geoip2filename; if (file_exists($geoip2file)) { if (!rename($geoip2file, $backuppath)) { mtrace("Failed to create a backup of the existing GeoIP database."); } mtrace("Temporary backup of existing GeoIP file has been created."); } mtrace("Moving {$mmdbfiles[0]} into $geoip2file"); if (!copy($mmdbfiles[0], $geoip2file)) { mtrace("Failed to update $geoip2filename."); // Attempt to restore the original file from the backup. if (file_exists($backuppath)) { mtrace("Attempting to restore from backup."); if (!copy($backuppath, $geoip2file)) { throw new moodle_exception("Failed to restore the GeoIP database from backup."); } else { mtrace("The GeoIP database has been restored from the backup successfully."); } } return false; } mtrace("$geoip2filename updated successfully."); return true; } }