AutorÃa | Ultima modificación | Ver Log |
<?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]);
}
}