327 lines
11 KiB
PHP
327 lines
11 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/>.
|
|
|
|
/**
|
|
* Quarantine file
|
|
*
|
|
* @package core_antivirus
|
|
* @author Nathan Nguyen <nathannguyen@catalyst-au.net>
|
|
* @copyright Catalyst IT
|
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
*/
|
|
|
|
namespace core\antivirus;
|
|
|
|
defined('MOODLE_INTERNAL') || die();
|
|
require_once($CFG->libdir.'/filelib.php');
|
|
|
|
/**
|
|
* Quarantine file
|
|
*
|
|
* @package core_antivirus
|
|
* @author Nathan Nguyen <nathannguyen@catalyst-au.net>
|
|
* @copyright Catalyst IT
|
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
*/
|
|
class quarantine {
|
|
|
|
/** Default quarantine folder */
|
|
const DEFAULT_QUARANTINE_FOLDER = 'antivirus_quarantine';
|
|
|
|
/** Zip infected file */
|
|
const FILE_ZIP_INFECTED = '_infected_file.zip';
|
|
|
|
/** Zip all infected file */
|
|
const FILE_ZIP_ALL_INFECTED = '_all_infected_files.zip';
|
|
|
|
/** Incident details file */
|
|
const FILE_HTML_DETAILS = '_details.html';
|
|
|
|
/** Incident details file */
|
|
const DEFAULT_QUARANTINE_TIME = DAYSECS * 28;
|
|
|
|
/** Date format in filename */
|
|
const FILE_NAME_DATE_FORMAT = '%Y%m%d%H%M%S';
|
|
|
|
/**
|
|
* Move the infected file to the quarantine folder.
|
|
*
|
|
* @param string $file infected file.
|
|
* @param string $filename infected file name.
|
|
* @param string $incidentdetails incident details.
|
|
* @param string $notice notice details.
|
|
* @return string|null the name of the newly created quarantined file.
|
|
* @throws \dml_exception
|
|
*/
|
|
public static function quarantine_file(string $file, string $filename, string $incidentdetails, string $notice): ?string {
|
|
if (!self::is_quarantine_enabled()) {
|
|
return null;
|
|
}
|
|
// Generate file names.
|
|
$date = userdate(time(), self::FILE_NAME_DATE_FORMAT) . "_" . rand();
|
|
$zipfilepath = self::get_quarantine_folder() . $date . self::FILE_ZIP_INFECTED;
|
|
$detailsfilename = $date . self::FILE_HTML_DETAILS;
|
|
|
|
// Create Zip file.
|
|
$ziparchive = new \zip_archive();
|
|
if ($ziparchive->open($zipfilepath, \file_archive::CREATE)) {
|
|
$ziparchive->add_file_from_string($detailsfilename, format_text($incidentdetails, FORMAT_MOODLE));
|
|
$ziparchive->add_file_from_pathname($filename, $file);
|
|
$ziparchive->close();
|
|
}
|
|
$zipfile = basename($zipfilepath);
|
|
self::create_infected_file_record($filename, $zipfile, $notice);
|
|
return $zipfile;
|
|
}
|
|
|
|
/**
|
|
* Move the infected file to the quarantine folder.
|
|
*
|
|
* @param string $data data which is infected.
|
|
* @param string $filename infected file name.
|
|
* @param string $incidentdetails incident details.
|
|
* @param string $notice notice details.
|
|
* @return string|null the name of the newly created quarantined file.
|
|
* @throws \dml_exception
|
|
*/
|
|
public static function quarantine_data(string $data, string $filename, string $incidentdetails, string $notice): ?string {
|
|
if (!self::is_quarantine_enabled()) {
|
|
return null;
|
|
}
|
|
// Generate file names.
|
|
$date = userdate(time(), self::FILE_NAME_DATE_FORMAT) . "_" . rand();
|
|
$zipfilepath = self::get_quarantine_folder() . $date . self::FILE_ZIP_INFECTED;
|
|
$detailsfilename = $date . self::FILE_HTML_DETAILS;
|
|
|
|
// Create Zip file.
|
|
$ziparchive = new \zip_archive();
|
|
if ($ziparchive->open($zipfilepath, \file_archive::CREATE)) {
|
|
$ziparchive->add_file_from_string($detailsfilename, format_text($incidentdetails, FORMAT_MOODLE));
|
|
$ziparchive->add_file_from_string($filename, $data);
|
|
$ziparchive->close();
|
|
}
|
|
$zipfile = basename($zipfilepath);
|
|
self::create_infected_file_record($filename, $zipfile, $notice);
|
|
return $zipfile;
|
|
}
|
|
|
|
/**
|
|
* Check if the virus quarantine is allowed
|
|
*
|
|
* @return bool
|
|
* @throws \dml_exception
|
|
*/
|
|
public static function is_quarantine_enabled(): bool {
|
|
return !empty(get_config("antivirus", "enablequarantine"));
|
|
}
|
|
|
|
/**
|
|
* Get quarantine folder
|
|
*
|
|
* @return string path of quarantine folder
|
|
*/
|
|
private static function get_quarantine_folder(): string {
|
|
global $CFG;
|
|
$quarantinefolder = $CFG->dataroot . DIRECTORY_SEPARATOR . self::DEFAULT_QUARANTINE_FOLDER;
|
|
if (!file_exists($quarantinefolder)) {
|
|
make_upload_directory(self::DEFAULT_QUARANTINE_FOLDER);
|
|
}
|
|
return $quarantinefolder . DIRECTORY_SEPARATOR;
|
|
}
|
|
|
|
/**
|
|
* Checks whether a file exists inside the antivirus quarantine folder.
|
|
*
|
|
* @param string $filename the filename to check.
|
|
* @return boolean whether file exists.
|
|
*/
|
|
public static function quarantined_file_exists(string $filename): bool {
|
|
$folder = self::get_quarantine_folder();
|
|
return file_exists($folder . $filename);
|
|
}
|
|
|
|
/**
|
|
* Download quarantined file.
|
|
*
|
|
* @param int $fileid the id of file to be downloaded.
|
|
*/
|
|
public static function download_quarantined_file(int $fileid) {
|
|
global $DB;
|
|
|
|
// Get the filename to be downloaded.
|
|
$filename = $DB->get_field('infected_files', 'quarantinedfile', ['id' => $fileid], IGNORE_MISSING);
|
|
// If file record isnt found, user might be doing something naughty in params, or a stale request.
|
|
if (empty($filename)) {
|
|
return;
|
|
}
|
|
|
|
$file = self::get_quarantine_folder() . $filename;
|
|
send_file($file, $filename);
|
|
}
|
|
|
|
/**
|
|
* Delete quarantined file.
|
|
*
|
|
* @param int $fileid id of file to be deleted.
|
|
*/
|
|
public static function delete_quarantined_file(int $fileid) {
|
|
global $DB;
|
|
|
|
// Get the filename to be deleted.
|
|
$filename = $DB->get_field('infected_files', 'quarantinedfile', ['id' => $fileid], IGNORE_MISSING);
|
|
// If file record isnt found, user might be doing something naughty in params, or a stale request.
|
|
if (empty($filename)) {
|
|
return;
|
|
}
|
|
|
|
// Delete the file from the folder.
|
|
$file = self::get_quarantine_folder() . $filename;
|
|
if (file_exists($file)) {
|
|
unlink($file);
|
|
}
|
|
|
|
// Now we are finished with the record, delete the quarantine information.
|
|
self::delete_infected_file_record($fileid);
|
|
}
|
|
|
|
/**
|
|
* Download all quarantined files.
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function download_all_quarantined_files() {
|
|
$files = new \DirectoryIterator(self::get_quarantine_folder());
|
|
// Add all infected files to a zip file.
|
|
$date = userdate(time(), self::FILE_NAME_DATE_FORMAT);
|
|
$zipfilename = $date . self::FILE_ZIP_ALL_INFECTED;
|
|
$zipfilepath = self::get_quarantine_folder() . DIRECTORY_SEPARATOR . $zipfilename;
|
|
$tempfilestocleanup = [];
|
|
|
|
$ziparchive = new \zip_archive();
|
|
if ($ziparchive->open($zipfilepath, \file_archive::CREATE)) {
|
|
foreach ($files as $file) {
|
|
if (!$file->isDot()) {
|
|
// Only send the actual files.
|
|
$filename = $file->getFilename();
|
|
$filepath = $file->getPathname();
|
|
$ziparchive->add_file_from_pathname($filename, $filepath);
|
|
}
|
|
}
|
|
$ziparchive->close();
|
|
}
|
|
|
|
// Clean up temp files.
|
|
foreach ($tempfilestocleanup as $tempfile) {
|
|
if (file_exists($tempfile)) {
|
|
unlink($tempfile);
|
|
}
|
|
}
|
|
|
|
send_temp_file($zipfilepath, $zipfilename);
|
|
}
|
|
|
|
/**
|
|
* Return array of quarantined files.
|
|
*
|
|
* @return array list of quarantined files.
|
|
*/
|
|
public static function get_quarantined_files(): array {
|
|
$files = new \DirectoryIterator(self::get_quarantine_folder());
|
|
$filestosort = [];
|
|
|
|
// Grab all files that match the naming structure.
|
|
foreach ($files as $file) {
|
|
$filename = $file->getFilename();
|
|
if (!$file->isDot() && strpos($filename, self::FILE_ZIP_INFECTED) !== false) {
|
|
$filestosort[$filename] = $file->getPathname();
|
|
}
|
|
}
|
|
|
|
krsort($filestosort, SORT_NATURAL);
|
|
return $filestosort;
|
|
}
|
|
|
|
/**
|
|
* Clean up quarantine folder
|
|
*
|
|
* @param int $timetocleanup time to clean up
|
|
*/
|
|
public static function clean_up_quarantine_folder(int $timetocleanup) {
|
|
$files = new \DirectoryIterator(self::get_quarantine_folder());
|
|
// Clean up the folder.
|
|
foreach ($files as $file) {
|
|
$filename = $file->getFilename();
|
|
|
|
// Only delete files that match the correct name structure.
|
|
if (!$file->isDot() && strpos($filename, self::FILE_ZIP_INFECTED) !== false) {
|
|
$modifiedtime = $file->getMTime();
|
|
|
|
if ($modifiedtime <= $timetocleanup) {
|
|
unlink($file->getPathname());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Lastly cleanup the infected files table as well.
|
|
self::clean_up_infected_records($timetocleanup);
|
|
}
|
|
|
|
/**
|
|
* This function removes any stale records from the infected files table.
|
|
*
|
|
* @param int $timetocleanup the time to cleanup from
|
|
* @return void
|
|
*/
|
|
private static function clean_up_infected_records(int $timetocleanup) {
|
|
global $DB;
|
|
|
|
$select = "timecreated <= ?";
|
|
$DB->delete_records_select('infected_files', $select, [$timetocleanup]);
|
|
}
|
|
|
|
/**
|
|
* Create an infected file record
|
|
*
|
|
* @param string $filename original file name
|
|
* @param string $zipfile quarantined file name
|
|
* @param string $reason failure reason
|
|
* @throws \dml_exception
|
|
*/
|
|
private static function create_infected_file_record(string $filename, string $zipfile, string $reason) {
|
|
global $DB, $USER;
|
|
|
|
$record = new \stdClass();
|
|
$record->filename = $filename;
|
|
$record->quarantinedfile = $zipfile;
|
|
$record->userid = $USER->id;
|
|
$record->reason = $reason;
|
|
$record->timecreated = time();
|
|
|
|
$DB->insert_record('infected_files', $record);
|
|
}
|
|
|
|
/**
|
|
* Delete the database record for an infected file.
|
|
*
|
|
* @param int $fileid quarantined file id
|
|
* @throws \dml_exception
|
|
*/
|
|
private static function delete_infected_file_record(int $fileid) {
|
|
global $DB;
|
|
$DB->delete_records('infected_files', ['id' => $fileid]);
|
|
}
|
|
}
|