moodle/lib/classes/task/update_geoip2file_task.php
Trisha Milan d0e82b746e MDL-81807 core: Add scheduled task for updating the GeoIP database
This commit includes the following changes:

- Implemented a scheduled task to automate the download and update of the GeoIP database.
- Integrated checksum verification for the downloaded database file.
- Added new configuration settings to enable administrators to enter MaxMind account credentials,
  allowing authenticated access to both free and paid GeoIP database versions from Maxmind.
2024-06-25 15:17:30 +10:00

184 lines
7.6 KiB
PHP

<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
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 <trishamilan@catalyst-au.net>
* @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;
}
}