MDL-66222 antivirus: Improved error email capture

This commit is contained in:
Peter Burnett
2020-08-21 12:21:09 +10:00
parent adbe92ce0a
commit 8e0e99e4fd
22 changed files with 664 additions and 239 deletions
+21 -8
View File
@@ -172,22 +172,35 @@ if ($hassiteconfig) {
$temp->add(new admin_setting_heading('antiviruscommonsettings', new lang_string('antiviruscommonsettings', 'antivirus'), ''));
// Alert email.
$temp->add(new admin_setting_configtext('antivirus/notifyemail',
$temp->add(
new admin_setting_configtext(
'antivirus/notifyemail',
new lang_string('notifyemail', 'antivirus'),
new lang_string('notifyemail_help', 'antivirus'), '', PARAM_EMAIL)
new lang_string('notifyemail_help', 'antivirus'),
'',
PARAM_EMAIL
)
);
// Enable quarantine.
$temp->add(new admin_setting_configcheckbox('antivirus/enablequarantine',
new lang_string('enablequarantine', 'antivirus'),
new lang_string('enablequarantine_help', 'antivirus',
\core\antivirus\quarantine::DEFAULT_QUARANTINE_FOLDER), 0));
$temp->add(
new admin_setting_configcheckbox(
'antivirus/enablequarantine',
new lang_string('enablequarantine', 'antivirus'),
new lang_string('enablequarantine_help', 'antivirus',
\core\antivirus\quarantine::DEFAULT_QUARANTINE_FOLDER),
0
)
);
// Quarantine time.
$temp->add(new admin_setting_configduration('antivirus/quarantinetime',
$temp->add(
new admin_setting_configduration(
'antivirus/quarantinetime',
new lang_string('quarantinetime', 'antivirus'),
new lang_string('quarantinetime_desc', 'antivirus'),
\core\antivirus\quarantine::DEFAULT_QUARANTINE_TIME)
\core\antivirus\quarantine::DEFAULT_QUARANTINE_TIME
)
);
$ADMIN->add('antivirussettings', $temp);
+17 -14
View File
@@ -27,32 +27,35 @@ $string['antiviruses'] = 'Antivirus plugins';
$string['antiviruscommonsettings'] = 'Common antivirus settings';
$string['antivirussettings'] = 'Manage antivirus plugins';
$string['configantivirusplugins'] = 'Please choose the antivirus plugins you wish to use and arrange them in order of being applied.';
$string['confirmdelete'] = 'Do you really want to delete this file';
$string['confirmdeleteall'] = 'Do you really want to delete all files';
$string['datastream'] = 'Data';
$string['datainfecteddesc'] = 'There is a virus infected data';
$string['datainfecteddesc'] = 'Infected data was detected.';
$string['datainfectedname'] = 'Data infected';
$string['emailadditionalinfo'] = 'Additional details returned from the virus engine: ';
$string['emailauthor'] = 'Uploaded by: ';
$string['emailcontenthash'] = 'Content hash: ';
$string['emailcontenttype'] = 'Content type: ';
$string['emaildate'] = 'Date uploaded: ';
$string['emailfilename'] = 'Filename: ';
$string['emailfilesize'] = 'File size: ';
$string['emailgeoinfo'] = 'Geolocation: ';
$string['emailinfectedfiledetected'] = 'Infected file detected';
$string['emailipaddress'] = 'IP Address: ';
$string['emailreferer'] = 'Referer: ';
$string['emailreport'] = 'Report: ';
$string['emailscanner'] = 'Scanner: ';
$string['emailscannererrordetected'] = 'A scanner error occured';
$string['emailsubject'] = '{$a} :: Antivirus notification';
$string['enablequarantine'] = 'Enable quarantine';
$string['enablequarantine_help'] = 'When quarantine is enabled, any files which are detected as viruses will be kept in a quarantine folder for later inspection ([dataroot]/{$a}).
The upload into Moodle will still fail.
If you have any file system level virus scanning in place, the quarantine folder should be excluded from the antivirus check to avoid detecting the quarantined files.';
$string['fileinfecteddesc'] = 'An infected file was detected.';
$string['fileinfectedname'] = 'File infected';
$string['incidencedetails'] = 'Infected file detected:
Report: {$a->report}
File name: {$a->filename}
File size: {$a->filesize}
File content hash: {$a->contenthash}
File content type: {$a->contenttype}
Uploaded by: {$a->author}
IP: {$a->ipaddress}
REFERER: {$a->referer}
Date: {$a->date}
{$a->notice}';
$string['notifyemail'] = 'Antivirus alert email';
$string['notifyemail_help'] = 'If set, then only the specified email will be notified when a virus is detected.
If blank, then all site admins will be notified by email when a virus is detected.';
$string['privacy:metadata'] = 'The Antivirus system does not store any personal data.';
$string['quarantinedisabled'] = 'Quarantine disabled, file not stored.';
$string['quarantinedfiles'] = 'Antivirus quarantined files';
$string['quarantinetime'] = 'Maximum quarantine time';
$string['quarantinetime_desc'] = 'Quarantined files older than specified period will be removed.';
+1
View File
@@ -1240,6 +1240,7 @@ $string['messageprovider:gradenotifications'] = 'Grade notifications';
$string['messageprovider:messagecontactrequests'] = 'Message contact requests notification';
$string['messageprovider:notices'] = 'Notices about minor problems';
$string['messageprovider:notices_help'] = 'These are notices that an administrator might be interested in seeing.';
$string['messageprovider:infected'] = 'Antivirus failure notifications.';
$string['messageprovider:insights'] = 'Insights generated by prediction models';
$string['messageprovider:instantmessage'] = 'Personal messages between users';
$string['messageprovider:instantmessage_help'] = 'This section configures what happens to messages that are sent to you directly from other users on this site.';
+107 -19
View File
@@ -70,29 +70,47 @@ class manager {
global $USER;
$antiviruses = self::get_enabled();
foreach ($antiviruses as $antivirus) {
$result = $antivirus->scan_file($file, $filename);
// Attempt to scan, catching internal exceptions.
try {
$result = $antivirus->scan_file($file, $filename);
} catch (\core\antivirus\scanner_exception $e) {
// If there was a scanner exception (such as ClamAV denying upload), send messages and rethrow.
$notice = $antivirus->get_scanning_notice();
$incidentdetails = $antivirus->get_incident_details($file, $filename, $notice, false);
self::send_antivirus_messages($antivirus, $incidentdetails);
throw $e;
}
$notice = $antivirus->get_scanning_notice();
if ($result === $antivirus::SCAN_RESULT_FOUND) {
// Infection found, send notification.
$notice = $antivirus->get_scanning_notice();
$incidencedetails = $antivirus->get_incidence_details($file, $filename, $notice);
$antivirus->message_admins($notice, FORMAT_MOODLE, 'infected');
$incidentdetails = $antivirus->get_incident_details($file, $filename, $notice);
self::send_antivirus_messages($antivirus, $incidentdetails);
// Move to quarantine folder.
$zipfile = \core\antivirus\quarantine::quarantine_file($file, $filename, $incidencedetails, $notice);
$zipfile = \core\antivirus\quarantine::quarantine_file($file, $filename, $incidentdetails, $notice);
// If file not stored due to disabled quarantine, store a message.
if (empty($zipfile)) {
$zipfile = get_string('quarantinedisabled', 'antivirus');
}
// Log file infected event.
$params = array(
$params = [
'context' => \context_system::instance(),
'relateduserid' => $USER->id,
'other' => ['filename' => $filename, 'zipfile' => $zipfile, 'incidencedetails' => $incidencedetails],
);
$event = \core\event\antivirus_file_infected::create($params);
'other' => ['filename' => $filename, 'zipfile' => $zipfile, 'incidentdetails' => $incidentdetails],
];
$event = \core\event\virus_infected_file_detected::create($params);
$event->trigger();
if ($deleteinfected) {
unlink($file);
}
throw new \core\antivirus\scanner_exception('virusfound', '', array('item' => $filename));
} else if ($result === $antivirus::SCAN_RESULT_ERROR) {
// Here we need to generate a different incident based on an error.
$incidentdetails = $antivirus->get_incident_details($file, $filename, $notice, false);
self::send_antivirus_messages($antivirus, $incidentdetails);
}
}
}
@@ -108,27 +126,48 @@ class manager {
global $USER;
$antiviruses = self::get_enabled();
foreach ($antiviruses as $antivirus) {
$result = $antivirus->scan_data($data);
// Attempt to scan, catching internal exceptions.
try {
$result = $antivirus->scan_data($data);
} catch (\core\antivirus\scanner_exception $e) {
// If there was a scanner exception (such as ClamAV denying upload), send messages and rethrow.
$notice = $antivirus->get_scanning_notice();
$filename = get_string('datastream', 'antivirus');
$incidentdetails = $antivirus->get_incident_details('', $filename, $notice, false);
self::send_antivirus_messages($antivirus, $incidentdetails);
throw $e;
}
$filename = get_string('datastream', 'antivirus');
$notice = $antivirus->get_scanning_notice();
if ($result === $antivirus::SCAN_RESULT_FOUND) {
// Infection found, send notification.
$filename = get_string('datastream', 'antivirus');
$notice = $antivirus->get_scanning_notice();
$incidencedetails = $antivirus->get_incidence_details('', $filename, $notice);
$antivirus->message_admins($notice, FORMAT_MOODLE, 'infected');
$incidentdetails = $antivirus->get_incident_details('', $filename, $notice);
self::send_antivirus_messages($antivirus, $incidentdetails);
// Copy data to quarantine folder.
$zipfile = \core\antivirus\quarantine::quarantine_data($data, $filename, $incidencedetails, $notice);
$zipfile = \core\antivirus\quarantine::quarantine_data($data, $filename, $incidentdetails, $notice);
// If file not stored due to disabled quarantine, store a message.
if (empty($zipfile)) {
$zipfile = get_string('quarantinedisabled', 'antivirus');
}
// Log file infected event.
$params = array(
$params = [
'context' => \context_system::instance(),
'relateduserid' => $USER->id,
'other' => ['filename' => $filename, 'zipfile' => $zipfile, 'incidencedetails' => $incidencedetails],
);
$event = \core\event\antivirus_data_infected::create($params);
'other' => ['filename' => $filename, 'zipfile' => $zipfile, 'incidentdetails' => $incidentdetails],
];
$event = \core\event\virus_infected_data_detected::create($params);
$event->trigger();
throw new \core\antivirus\scanner_exception('virusfound', '', array('item' => get_string('datastream', 'antivirus')));
} else if ($result === $antivirus::SCAN_RESULT_ERROR) {
// Here we need to generate a different incident based on an error.
$incidentdetails = $antivirus->get_incident_details('', $filename, $notice, false);
self::send_antivirus_messages($antivirus, $incidentdetails);
}
}
}
@@ -161,4 +200,53 @@ class manager {
}
return $antiviruses;
}
/**
* This function puts all relevant information into the messages required, and sends them.
*
* @param \core\antivirus\scanner $antivirus the scanner engine.
* @param string $incidentdetails details of the incident.
* @return void
*/
public static function send_antivirus_messages(\core\antivirus\scanner $antivirus, string $incidentdetails) {
$messages = $antivirus->get_messages();
// If there is no messages, and a virus is found, we should generate one, then send it.
if (empty($messages)) {
$antivirus->message_admins($antivirus->get_scanning_notice(), FORMAT_MOODLE, 'infected');
$messages = $antivirus->get_messages();
}
foreach ($messages as $message) {
// Check if the information is already in the current scanning notice.
if (!empty($antivirus->get_scanning_notice()) &&
strpos($antivirus->get_scanning_notice(), $message->fullmessage) === false) {
// This is some extra information. We should append this to the end of the incident details.
$incidentdetails .= \html_writer::tag('pre', $message->fullmessage);
}
// Now update the message to the detailed version, and format.
$message->name = 'infected';
$message->fullmessagehtml = $incidentdetails;
$message->fullmessageformat = FORMAT_MOODLE;
$message->fullmessage = format_text_email($incidentdetails, $message->fullmessageformat);
// Now we must check if message is going to a real account.
// It may be an email that needs to be sent to non-user address.
if ($message->userto->id === -1) {
// If this doesnt exist, send a regular email.
email_to_user(
$message->userto,
get_admin(),
$message->subject,
$message->fullmessage,
$message->fullmessagehtml
);
} else {
// And now we can send.
message_send($message);
}
}
}
}
+108 -46
View File
@@ -47,27 +47,28 @@ class quarantine {
/** Zip all infected file */
const FILE_ZIP_ALL_INFECTED = '_all_infected_files.zip';
/** Incidence details file */
/** Incident details file */
const FILE_HTML_DETAILS = '_details.html';
/** Incidence details file */
/** 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
* Move the infected file to the quarantine folder.
*
* @param string $file infected file
* @param string $filename infected file name
* @param string $incidencedetails incidence details
* @param string $notice notice details
* @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($file, $filename, $incidencedetails, $notice) {
if (!self::is_allowed_quarantine()) {
return;
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();
@@ -77,7 +78,7 @@ class quarantine {
// Create Zip file.
$ziparchive = new \zip_archive();
if ($ziparchive->open($zipfilepath, \file_archive::CREATE)) {
$ziparchive->add_file_from_string($detailsfilename, format_text($incidencedetails, FORMAT_MOODLE));
$ziparchive->add_file_from_string($detailsfilename, format_text($incidentdetails, FORMAT_MOODLE));
$ziparchive->add_file_from_pathname($filename, $file);
$ziparchive->close();
}
@@ -87,17 +88,18 @@ class quarantine {
}
/**
* Move the infected file to the quarantine folder
* Move the infected file to the quarantine folder.
*
* @param string $data data which is infected
* @param string $filename infected file name
* @param string $incidencedetails incidence details
* @param string $notice notice details
* @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($data, $filename, $incidencedetails, $notice) {
if (!self::is_allowed_quarantine()) {
return;
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();
@@ -107,7 +109,7 @@ class quarantine {
// Create Zip file.
$ziparchive = new \zip_archive();
if ($ziparchive->open($zipfilepath, \file_archive::CREATE)) {
$ziparchive->add_file_from_string($detailsfilename, format_text($incidencedetails, FORMAT_MOODLE));
$ziparchive->add_file_from_string($detailsfilename, format_text($incidentdetails, FORMAT_MOODLE));
$ziparchive->add_file_from_string($filename, $data);
$ziparchive->close();
}
@@ -122,7 +124,7 @@ class quarantine {
* @return bool
* @throws \dml_exception
*/
public static function is_allowed_quarantine() {
public static function is_quarantine_enabled() : bool {
return !empty(get_config("antivirus", "enablequarantine"));
}
@@ -131,7 +133,7 @@ class quarantine {
*
* @return string path of quarantine folder
*/
public static function get_quarantine_folder() {
private static function get_quarantine_folder() : string {
global $CFG;
$quarantinefolder = $CFG->dataroot . DIRECTORY_SEPARATOR . self::DEFAULT_QUARANTINE_FOLDER;
if (!file_exists($quarantinefolder)) {
@@ -141,43 +143,78 @@ class quarantine {
}
/**
* Download quarantined file
* Checks whether a file exists inside the antivirus quarantine folder.
*
* @param string $filename name of file to be downloaded
* @param string $filename the filename to check.
* @return boolean whether file exists.
*/
public static function download_quarantined_file($filename) {
$file = self::get_quarantine_folder() . $filename;
// send_file($file, $filename);
public static function quarantined_file_exists(string $filename) : bool {
$folder = self::get_quarantine_folder();
return file_exists($folder . $filename);
}
/**
* Delete quarantined file
* Download quarantined file.
*
* @param string $filename name of file to be deleted
* @param int $fileid the id of file to be downloaded.
*/
public static function delete_quarantined_file($filename) {
self::delete_infected_file_record($filename);
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);
unlink($file);
}
// Now we are finished with the record, delete the quarantine information.
self::delete_infected_file_record($fileid);
}
/**
* Download all quarantined files
* Download all quarantined files.
*
* @return void
*/
public static function download_all_quarantined_files() {
$files = new \DirectoryIterator(self::get_quarantine_folder());
// Add all infected file to a zip file.
// 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);
@@ -185,29 +222,34 @@ class quarantine {
}
$ziparchive->close();
}
// Clean up temp files.
foreach ($tempfilestocleanup as $tempfile) {
if (file_exists($tempfile)) {
unlink($tempfile);
}
}
// send_temp_file($zipfilepath, $zipfilename);
send_temp_file($zipfilepath, $zipfilename);
}
/**
* Return array of quarantined files
* Return array of quarantined files.
*
* @return array list of quarantined files
* @return array list of quarantined files.
*/
public static function get_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;
}
@@ -217,19 +259,37 @@ class quarantine {
*
* @param int $timetocleanup time to clean up
*/
public static function clean_up_quarantine_folder($timetocleanup) {
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());
self::delete_infected_file_record($filename);
}
}
}
// 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]);
}
/**
@@ -240,25 +300,27 @@ class quarantine {
* @param string $reason failure reason
* @throws \dml_exception
*/
private static function create_infected_file_record($filename, $zipfile, $reason) {
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->author = fullname($USER);
$record->userid = $USER->id;
$record->reason = $reason;
$record->timecreated = time();
$DB->insert_record('infected_files', $record);
}
/**
* Delete an infected_file_record
* Delete the database record for an infected file.
*
* @param string $zipfile quarantined file name
* @param int $fileid quarantined file id
* @throws \dml_exception
*/
private static function delete_infected_file_record($zipfile) {
private static function delete_infected_file_record(int $fileid) {
global $DB;
$DB->delete_records('infected_files', ['quarantinedfile' => $zipfile]);
$DB->delete_records('infected_files', ['id' => $fileid]);
}
}
+45 -13
View File
@@ -25,6 +25,7 @@
namespace core\antivirus;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '../../../../iplookup/lib.php');
/**
* Base abstract antivirus scanner class.
@@ -46,6 +47,8 @@ abstract class scanner {
protected $config;
/** @var string scanning notice */
protected $scanningnotice = '';
/** @var array any admin messages generated by a plugin. */
protected $messages = [];
/**
* Class constructor.
@@ -130,7 +133,7 @@ abstract class scanner {
}
/**
* Email admins about antivirus scan outcomes.
* This function pushes given messages into the message queue, which will be sent by the antivirus manager.
*
* @param string $notice The body of the email to be sent.
* @param string $format The body format.
@@ -145,15 +148,18 @@ abstract class scanner {
$subject = get_string('emailsubject', 'antivirus', format_string($site->fullname));
$notifyemail = get_config('antivirus', 'notifyemail');
// If one email address is specified, construct a message to fake account.
if (!empty($notifyemail)) {
$user = new \stdClass();
$user->id = -1;
$user->email = $notifyemail;
email_to_user($user, get_admin(), $subject, $noticehtml);
return;
$user->mailformat = 1;
$admins = [$user];
} else {
// Otherwise, we message all admins.
$admins = get_admins();
}
$admins = get_admins();
foreach ($admins as $admin) {
$eventdata = new \core\message\message();
$eventdata->courseid = SITEID;
@@ -166,29 +172,38 @@ abstract class scanner {
$eventdata->fullmessageformat = $format;
$eventdata->fullmessagehtml = $noticehtml;
$eventdata->smallmessage = '';
message_send($eventdata);
// Now add the message to an array to be sent by the antivirus manager.
$this->messages[] = $eventdata;
}
}
/**
* Return incidence details
* Return incident details
*
* @param string $file full path to the file
* @param string $filename original name of the file
* @param string $notice notice from antivirus
* @return string the incidence details
* @param string $virus if this template is due to a virus found.
* @return string the incident details
* @throws \coding_exception
*/
public function get_incidence_details($file = '', $filename = '', $notice = '') {
global $USER;
public function get_incident_details($file = '', $filename = '', $notice = '', $virus = true) {
global $OUTPUT, $USER;
if (empty($notice)) {
$notice = $this->get_scanning_notice();
}
$classname = get_class($this);
$component = explode('\\', $classname)[0];
$content = new \stdClass();
$unknown = get_string('unknown', 'antivirus');;
$unknown = get_string('unknown', 'antivirus');
$content->header = get_string('emailinfectedfiledetected', 'antivirus');
$content->filename = !empty($filename) ? $filename : $unknown;
if (!empty($file)) {
$content->filesize = filesize($file);
$content->scanner = $component;
// Check for empty file, or file not uploaded.
if (!empty($file) && filesize($file) !== false) {
$content->filesize = display_size(filesize($file));
$content->contenthash = \file_storage::hash_from_string(file_get_contents($file));
$content->contenttype = mime_content_type($file);
} else {
@@ -199,11 +214,28 @@ abstract class scanner {
$content->author = \core_user::is_real_user($USER->id) ? fullname($USER) . " ($USER->username)" : $unknown;
$content->ipaddress = getremoteaddr();
$geoinfo = iplookup_find_location(getremoteaddr());
$content->geoinfo = $geoinfo['city'] . ', ' . $geoinfo['country'];
$content->date = userdate(time(), get_string('strftimedatetimeshort'));
$content->referer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : $unknown;
$content->notice = $notice;
$report = new \moodle_url('/report/infectedfiles/index.php');
$content->report = $report->out();
return get_string('incidencedetails', 'antivirus', $content);
// If this is not due to a virus, we need to change the header line.
if (!$virus) {
$content->header = get_string('emailscannererrordetected', 'antivirus');
}
return $OUTPUT->render_from_template('core/infected_file_email', $content);
}
/**
* Getter method for messages queued by the antivirus scanner.
*
* @return array
*/
public function get_messages() : array {
return $this->messages;
}
}
@@ -34,7 +34,7 @@ defined('MOODLE_INTERNAL') || die();
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class antivirus_data_infected extends \core\event\base {
class virus_infected_data_detected extends \core\event\base {
/**
* Event data
*/
@@ -50,8 +50,11 @@ class antivirus_data_infected extends \core\event\base {
* @throws \coding_exception
*/
public function get_description() {
return isset($this->other['incidencedetails']) ?
format_text($this->other['incidencedetails'], FORMAT_MOODLE) : 'Infected data';
if (isset($this->other['incidentdetails'])) {
return format_text($this->other['incidentdetails'], FORMAT_MOODLE);
} else {
return get_string('datainfecteddesc', 'antivirus');
}
}
/**
@@ -34,7 +34,7 @@ defined('MOODLE_INTERNAL') || die();
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class antivirus_file_infected extends \core\event\base {
class virus_infected_file_detected extends \core\event\base {
/**
* Event data
*/
@@ -50,8 +50,11 @@ class antivirus_file_infected extends \core\event\base {
* @throws \coding_exception
*/
public function get_description() {
return isset($this->other['incidencedetails']) ?
format_text($this->other['incidencedetails'], FORMAT_MOODLE) : 'Infected file';
if (isset($this->other['incidentdetails'])) {
return format_text($this->other['incidentdetails'], FORMAT_MOODLE);
} else {
return get_string('fileinfecteddesc', 'antivirus');
}
}
/**
+2 -2
View File
@@ -1966,8 +1966,8 @@ class core_plugin_manager {
'report' => array(
'backups', 'competency', 'completion', 'configlog', 'courseoverview', 'eventlist',
'insights', 'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances',
'security', 'stats', 'status', 'performance', 'usersessions'
'infectedfiles', 'insights', 'log', 'loglive', 'outline', 'participation', 'progress',
'questioninstances', 'security', 'stats', 'status', 'performance', 'usersessions'
),
'repository' => array(
+1 -1
View File
@@ -35,7 +35,7 @@ defined('MOODLE_INTERNAL') || die();
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class antivirus_cleanup_task extends \core\task\scheduled_task {
class antivirus_cleanup_task extends scheduled_task {
/**
* Get a descriptive name for this task.
+9 -5
View File
@@ -4275,15 +4275,19 @@
<INDEX NAME="instance" UNIQUE="false" FIELDS="contextid, contenttype, instanceid"/>
</INDEXES>
</TABLE>
<TABLE NAME="infected_files" COMMENT="Store virus infected file details">
<TABLE NAME="infected_files" COMMENT="Table to store infected file details.">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="filename" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="Original file name"/>
<FIELD NAME="quarantinedfile" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="Quarantine zip file"/>
<FIELD NAME="author" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="User who uploaded the infected files"/>
<FIELD NAME="reason" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="Failure reason"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="quarantinedfile" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Quarantine zip file"/>
<FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The user that uploaded the infected file."/>
<FIELD NAME="reason" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="The reason for the antivirus failure"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The time the infected file was uploaded."/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id" COMMENT="Foreign key for the userid"/>
</KEYS>
</TABLE>
</TABLES>
</XMLDB>
+2 -2
View File
@@ -144,7 +144,7 @@ $messageproviders = array (
],
// Infected files.
'infected' => array (
'capability' => 'moodle/site:config'
'infected' => array(
'capability' => 'moodle/site:config',
),
);
+2 -2
View File
@@ -404,10 +404,10 @@ $tasks = array(
array(
'classname' => 'core\task\antivirus_cleanup_task',
'blocking' => 0,
'minute' => '0',
'minute' => 'R',
'hour' => '0',
'day' => '*',
'dayofweek' => '*',
'month' => '*'
'month' => '*',
),
);
+8 -6
View File
@@ -2539,25 +2539,27 @@ function xmldb_main_upgrade($oldversion) {
upgrade_main_savepoint(true, 2020072300.01);
}
if ($oldversion < 2020080500.01) {
if ($oldversion < 2020081400.01) {
// Define table to store virus infected details.
$table = new xmldb_table('infected_files');
// Adding fields.
// Adding fields to table infected_files.
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('filename', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null);
$table->add_field('quarantinedfile', XMLDB_TYPE_CHAR, 255, null, XMLDB_NOTNULL, null, null);
$table->add_field('author', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null);
$table->add_field('quarantinedfile', XMLDB_TYPE_TEXT, null, null, null, null, null);
$table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
$table->add_field('reason', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null);
$table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
// Adding keys.
// Adding keys to table infected_files.
$table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
$table->add_key('userid', XMLDB_KEY_FOREIGN, ['userid'], 'user', ['id']);
// Conditionally launch create table for infected_files.
if (!$dbman->table_exists($table)) {
$dbman->create_table($table);
}
upgrade_main_savepoint(true, 2020080500.01);
upgrade_main_savepoint(true, 2020081400.01);
}
return true;
}
@@ -0,0 +1,80 @@
{{!
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/>.
}}
{{!
@template core/infected_file_email
Moodle template for infected files emails.
Context variables required for this template:
* report - Hyperlink to the report page,
* scanner - Scanning engine that found this file,
* filename - Name of the infected file,
* filesize - Size of the file,
* contenthash - File content hash,
* contenttype - Type of uploaded file,
* author - User that uploaded the file,
* ipaddress - IP address the file was uploaded from,
* geoinfo - Geo information about IP address,
* referer - The referring page,
* identityproviders - List of identiy providers,
* date - Date of upload
* notice - Notice returned from the scanning engine
* additionalinfo - Any additional information from the scanning engine
Example context (json):
{
"header": "Infected file detected",
"report": "http://example.moodle/report/infectedfiles/index.php",
"scanner": "antivirus_clamav",
"filename": "virus.txt",
"filesize": 100,
"contenthash": "3395856ce81f2b7382dee72602f798b642f14140",
"contenttype": "text/plain",
"author": "Example User (exampleuser)",
"ipaddress": "192.168.0.1",
"geoinfo": "Brisbane, Australia",
"referer": "http://example.moodle/user/files.php",
"date": "28/05/20, 11:19",
"notice": "Clamav scanning has tried 1 time(s). ClamAV has failed to run.",
"additionalinfo": "Here is the output from ClamAV: /tmp/phpElIcr2: Not a regular file ERROR"
}
}}
<div>
<b>{{header}}</b>
<div>
<p><b>{{#str}} emailreport, antivirus {{/str}}</b><a href={{report}}>{{report}}</a></p>
<p><b>{{#str}} emailscanner, antivirus {{/str}}</b>{{scanner}}</p>
<br>
<p><b>{{#str}} emailfilename, antivirus {{/str}}</b>{{filename}}</p>
<p><b>{{#str}} emailfilesize, antivirus {{/str}}</b>{{filesize}}</p>
<p><b>{{#str}} emailcontenthash, antivirus {{/str}}</b>{{contenthash}}</p>
<p><b>{{#str}} emailcontenttype, antivirus {{/str}}</b>{{contenttype}}</p>
<p><b>{{#str}} emaildate, antivirus {{/str}}</b>{{date}}</p>
<br>
<p><b>{{#str}} emailauthor, antivirus {{/str}}</b>{{author}}</p>
<p><b>{{#str}} emailipaddress, antivirus {{/str}}</b>{{ipaddress}}</p>
<p><b>{{#str}} emailgeoinfo, antivirus {{/str}}</b>{{geoinfo}}</p>
<p><b>{{#str}} emailreferer, antivirus {{/str}}</b><a href={{referer}}>{{referer}}</a></p>
</div>
<div>
<br>
<pre>{{notice}}</pre>
<br>
<p><b>{{#str}} emailadditionalinfo, antivirus {{/str}} </b></p>
</div>
</div>
@@ -23,6 +23,7 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace report_infectedfiles\output;
use report_infectedfiles\table\infectedfiles_table;
defined('MOODLE_INTERNAL') || die();
@@ -24,6 +24,9 @@
*/
namespace report_infectedfiles\privacy;
use core_privacy\local\metadata\collection;
use core_privacy\local\request;
defined('MOODLE_INTERNAL') || die();
/**
@@ -34,14 +37,136 @@ defined('MOODLE_INTERNAL') || die();
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements \core_privacy\local\metadata\null_provider {
class provider implements
\core_privacy\local\metadata\provider,
request\plugin\provider,
request\core_userlist_provider {
/**
* This plugin does not contain any personal data
* This plugin stores the userid of infected users.
*
* @return string
* @param collection $collection the collection object to add data to.
* @return collection The populated collection.
*/
public static function get_reason() : string {
return 'privacy:metadata';
public static function get_metadata(collection $collection) : collection {
$collection->add_database_table(
'infected_files',
[
'userid' => 'privacy:metadata:infected_files:userid',
'filename' => 'privacy:metadata:infected_files:filename',
'timecreated' => 'privacy:metadata:infected_files:timecreated',
],
'privacy:metadata:infected_files'
);
return $collection;
}
/**
* This function gets the contexts containing data for a userid.
*
* @param int $userid The userid to get contexts for.
* @return request\contextlist the context list for the user.
*/
public static function get_contexts_for_userid(int $userid) : request\contextlist {
$contextlist = new request\contextlist();
// The system context is the only context where information is stored.
$contextlist->add_system_context();
return $contextlist;
}
/**
* This function exports user data on infected files from the contextlist provided.
*
* @param request\approved_contextlist $contextlist
* @return void
*/
public static function export_user_data(request\approved_contextlist $contextlist) {
global $DB;
foreach ($contextlist as $context) {
// We only export from system context.
if ($context->contextlevel === CONTEXT_SYSTEM) {
$userid = $contextlist->get_user()->id;
$exportdata = [];
$records = $DB->get_records('infected_files', ['userid' => $userid]);
foreach ($records as $record) {
// Export only the data that does not expose internal information.
$data = [];
$data['userid'] = $record->userid;
$data['timecreated'] = $record->timecreated;
$data['filename'] = $record->filename;
$exportdata[] = $data;
}
// Now export this data in the infected files table as subcontext.
request\writer::with_context($context)->export_data(
[get_string('privacy:metadata:infected_files_subcontext', 'report_infectedfiles')],
(object) $exportdata
);
}
}
}
/**
* As this report tracks potential attempted security violations,
* This data should not be deleted at request. This would allow for an
* avenue for a malicious user to cover their tracks. This function deliberately
* does no deletes.
*
* @param \context $context the context to delete for.
* @return void
*/
public static function delete_data_for_all_users_in_context(\context $context) {
return;
}
/**
* As this report tracks potential attempted security violations,
* This data should not be deleted at request. This would allow for an
* avenue for a malicious user to cover their tracks. This function deliberately
* does no deletes.
*
* @param \core_privacy\local\request\approved_contextlist $contextlist the contextlist to delete for.
* @return void
*/
public static function delete_data_for_user(request\approved_contextlist $contextlist) {
return;
}
/**
* This gets the list of users inside of the provided context. In this case, its only system context
* which contains users.
*
* @param \core_privacy\local\request\userlist $userlist
* @return void
*/
public static function get_users_in_context(request\userlist $userlist) {
$context = $userlist->get_context();
if ($context->contextlevel === CONTEXT_SYSTEM) {
// If we are checking system context, we need to get all distinct userids from the table.
$sql = 'SELECT DISTINCT userid
FROM {infected_files}';
$userlist->add_from_sql('userid', $sql, []);
}
}
/**
* As this report tracks potential attempted security violations,
* This data should not be deleted at request. This would allow for an
* avenue for a malicious user to cover their tracks. This function deliberately
* does no deletes.
*
* @param request\approved_userlist $userlist
* @return void
*/
public static function delete_data_for_users(request\approved_userlist $userlist) {
return;
}
}
@@ -22,7 +22,7 @@
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace report_infectedfiles\output;
namespace report_infectedfiles\table;
defined('MOODLE_INTERNAL') || die();
@@ -70,7 +70,6 @@ class infectedfiles_table extends \table_sql implements \renderable {
protected function define_table_columns() {
$cols = array(
'filename' => get_string('filename', 'report_infectedfiles'),
'quarantinedfile' => get_string('quarantinedfile', 'report_infectedfiles'),
'author' => get_string('author', 'report_infectedfiles'),
'reason' => get_string('reason', 'report_infectedfiles'),
'timecreated' => get_string('timecreated', 'report_infectedfiles'),
@@ -102,7 +101,7 @@ class infectedfiles_table extends \table_sql implements \renderable {
* @param bool $count When true, return the count SQL.
* @return array containing sql to use and an array of params.
*/
protected function get_sql_and_params($count = false) {
protected function get_sql_and_params($count = false) : array {
if ($count) {
$select = "COUNT(1)";
} else {
@@ -143,48 +142,80 @@ class infectedfiles_table extends \table_sql implements \renderable {
}
}
/**
* Column to display the authors fullname from userid.
*
* @param \stdClass $row the row from sql.
* @return string the authors name.
*/
protected function col_author($row) : string {
// Get user fullname from ID.
$user = \core_user::get_user($row->userid);
$url = new \moodle_url('/user/profile.php', ['id' => $row->userid]);
return \html_writer::link($url, fullname($user));
}
/**
* Column to display the failure reason.
*
* @param \stdClass $row the row from sql.
* @return string the formatted reason.
*/
protected function col_reason($row) {
return format_text($row->reason);
}
/**
* Custom actions column
*
* @param \stdClass $row an incidence record
* @return string content of action column
* @param \stdClass $row an incident record.
* @return string content of action column.
* @throws \coding_exception
* @throws \moodle_exception
*/
protected function col_actions($row) {
protected function col_actions($row) : string {
global $OUTPUT;
$filename = $row->quarantinedfile;
$zipfile = \core\antivirus\quarantine::get_quarantine_folder() . $filename;
if (!file_exists($zipfile)) {
$fileid = $row->id;
// If the file isn't found, we can do nothing in this column.
// This shouldn't happen, unless the file is manually deleted from the server externally.
if (!\core\antivirus\quarantine::quarantined_file_exists($filename)) {
return '';
}
$links = '';
$managefilepage = new \moodle_url('/report/infectedfiles/manage_infected_files.php');
$managefilepage = new \moodle_url('/report/infectedfiles/index.php');
// Download.
$downloadparams = ['filename' => $filename, 'action' => 'download', 'sesskey' => sesskey()];
$downloadparams = ['file' => $fileid, 'action' => 'download', 'sesskey' => sesskey()];
$downloadurl = new \moodle_url($managefilepage, $downloadparams);
$icon = $OUTPUT->pix_icon('t/download', get_string('download'));
$downloadlink = \html_writer::link($downloadurl, $icon);
$links .= ' ' . $downloadlink;
$downloadconfirm = new \confirm_action(get_string('confirmdownload', 'report_infectedfiles'));
$links .= $OUTPUT->action_icon(
$downloadurl,
new \pix_icon('t/download', get_string('download')),
$downloadconfirm
);
// Delete.
$deleteparams = ['filename' => $filename, 'action' => 'confirmdelete', 'sesskey' => sesskey()];
$deleteparams = ['file' => $fileid, 'action' => 'delete', 'sesskey' => sesskey()];
$deleteurl = new \moodle_url($managefilepage, $deleteparams);
$icon = $OUTPUT->pix_icon('t/delete', get_string('delete'));
$deletelink = \html_writer::link($deleteurl, $icon);
$links .= ' ' . $deletelink;
$deleteconfirm = new \confirm_action(get_string('confirmdelete', 'report_infectedfiles'));
$links .= $OUTPUT->action_icon(
$deleteurl,
new \pix_icon('t/delete', get_string('delete')),
$deleteconfirm
);
return $links;
}
/**
* Custom time column
* Custom time column.
*
* @param \stdClass $row an incidence record
* @return string time created in user-friendly format
* @param \stdClass $row an incident record.
* @return string time created in user-friendly format.
*/
protected function col_timecreated($row) {
protected function col_timecreated($row) : string {
return userdate($row->timecreated);
}
@@ -194,26 +225,36 @@ class infectedfiles_table extends \table_sql implements \renderable {
* @param int $pagesize number or records perpage
* @param bool $useinitialsbar use the bar or not
* @param string $downloadhelpbutton help button
* @return void
* @throws \coding_exception
* @throws \moodle_exception
*/
public function display($pagesize, $useinitialsbar, $downloadhelpbutton='') {
global $OUTPUT;
// Output the table, and then display buttons.
$this->out($pagesize, $useinitialsbar, $downloadhelpbutton);
$managefilepage = new \moodle_url('/report/infectedfiles/index.php');
// If there are no rows, dont bother rendering extra buttons.
if (empty($this->rawdata)) {
return;
}
$managefilepage = new \moodle_url('/report/infectedfiles/manage_infected_files.php');
// Delete All.
$button = \html_writer::tag('button', get_string('deleteall'), ['class' => 'btn btn-primary']);
$deleteallparams = ['action' => 'confirmdeleteall', 'sesskey' => sesskey()];
$deleteallparams = ['action' => 'deleteall', 'sesskey' => sesskey()];
$deleteallurl = new \moodle_url($managefilepage, $deleteallparams);
echo \html_writer::link($deleteallurl, $button);
$deletebutton = new \single_button($deleteallurl, get_string('deleteall'), 'post', true);
$deletebutton->add_confirm_action(get_string('confirmdeleteall', 'report_infectedfiles'));
echo $OUTPUT->render($deletebutton);
echo "&nbsp";
// Download All.
$button = \html_writer::tag('button', get_string('downloadall'), ['class' => 'btn btn-primary']);
$downloadallparams = ['action' => 'downloadall', 'sesskey' => sesskey()];
$downloadallurl = new \moodle_url($managefilepage, $downloadallparams);
echo \html_writer::link($downloadallurl, $button);
$downloadbutton = new \single_button($downloadallurl, get_string('downloadall'), 'post', true);
$downloadbutton->add_confirm_action(get_string('confirmdownloadall', 'report_infectedfiles'));
echo $OUTPUT->render($downloadbutton);
}
}
+39 -1
View File
@@ -25,12 +25,50 @@
require(__DIR__.'/../../config.php');
require_once($CFG->libdir.'/adminlib.php');
use \core\antivirus\quarantine;
admin_externalpage_setup('reportinfectedfiles', '', null, '', array('pagelayout' => 'report'));
$action = optional_param('action', '', PARAM_TEXT);
// If action exists, we need to process the actions safely.
if (!empty($action)) {
// Nothing can be done without a valid sesskey.
require_sesskey();
// For any cancel actions, just reload the page clean.
$cancel = $PAGE->url;
// Decide on page action.
switch ($action) {
case 'download':
$fileid = required_param('file', PARAM_INT);
quarantine::download_quarantined_file($fileid);
break;
case 'downloadall':
quarantine::download_all_quarantined_files();
break;
case 'delete':
$fileid = required_param('file', PARAM_INT);
quarantine::delete_quarantined_file($fileid);
break;
case 'deleteall':
// Remove file until current time.
quarantine::clean_up_quarantine_folder(time());
break;
}
// Reload page cleanly once actions are processed.
redirect($PAGE->url);
}
// Once actions are dealt with, display the page.
$page = optional_param('page', 0, PARAM_INT);
echo $OUTPUT->header();
echo $OUTPUT->heading(get_string('infectedfiles', 'report_infectedfiles'));
$table = new \report_infectedfiles\output\infectedfiles_table('report-infectedfiles-report-table', $PAGE->url, $page);
$table = new \report_infectedfiles\table\infectedfiles_table('report-infectedfiles-report-table', $PAGE->url, $page);
$table->define_baseurl($PAGE->url);
echo $PAGE->get_renderer('report_infectedfiles')->render($table);
echo $OUTPUT->footer();
@@ -23,11 +23,17 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['author'] = 'Author';
$string['confirmdelete'] = 'Do you really want to delete this file';
$string['confirmdeleteall'] = 'Do you really want to delete all files';
$string['confirmdelete'] = 'Do you really wish to delete this file?';
$string['confirmdeleteall'] = 'Do you really wish to delete all files?';
$string['confirmdownload'] = 'Do you really wish to download this file?';
$string['confirmdownloadall'] = 'Do you really wish to download all files?';
$string['filename'] = 'File name';
$string['infectedfiles'] = 'Infected files';
$string['privacy:metadata'] = 'This plugin does not contain any personal data';
$string['infectedfiles'] = 'Antivirus failures';
$string['privacy:metadata:infected_files'] = 'This table stores information on antivirus failures detected by the system.';
$string['privacy:metadata:infected_files:filename'] = 'The name of the infected file uploaded by the user.';
$string['privacy:metadata:infected_files:timecreated'] = 'The timestamp of when a user uploaded an infected file.';
$string['privacy:metadata:infected_files:userid'] = 'The userid of the user who uploaded an infected file.';
$string['privacy:metadata:infected_files_subcontext'] = 'Antivirus failures';
$string['pluginname'] = 'Infected files';
$string['quarantinedfile'] = 'Quarantined file';
$string['reason'] = 'Failure reason';
@@ -1,77 +0,0 @@
<?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/>.
/**
* Manage infected files.
*
* @package report_infectedfiles
* @author Nathan Nguyen <[email protected]>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__.'/../../config.php');
require_once($CFG->libdir.'/adminlib.php');
use \core\antivirus\quarantine;
require_admin();
require_sesskey();
$action = optional_param('action', '', PARAM_TEXT);
$reportpage = new moodle_url('/report/infectedfiles/index.php');
$thispage = new moodle_url('/report/infectedfiles/manage_infected_files.php');
$PAGE->set_context(context_system::instance());
$PAGE->set_url($thispage);
$PAGE->navbar->add(get_string('infectedfiles', 'report_infectedfiles'), $reportpage);
switch ($action) {
case 'download':
$filename = required_param('filename', PARAM_TEXT);
quarantine::download_quarantined_file($filename);
case 'downloadall':
quarantine::download_all_quarantined_files();
case 'confirmdelete':
$filename = required_param('filename', PARAM_TEXT);
$deleteparams = ['filename' => $filename, 'action' => 'delete', 'sesskey' => sesskey()];
$confirmeddelete = new single_button(new moodle_url($thispage, $deleteparams), get_string('delete'), 'post');
$cancel = new single_button(new moodle_url($reportpage), get_string('cancel'), 'post');
echo $OUTPUT->header();
echo $OUTPUT->heading(get_string('infectedfiles', 'report_infectedfiles'));
echo $OUTPUT->confirm(get_string('confirmdelete', 'report_infectedfiles'), $confirmeddelete, $cancel);
echo $OUTPUT->footer();
die;
case 'delete':
$filename = required_param('filename', PARAM_TEXT);
quarantine::delete_quarantined_file($filename);
redirect($reportpage);
case 'confirmdeleteall':
require_sesskey();
$deleteallparams = ['action' => 'deleteall', 'sesskey' => sesskey()];
$confirmeddeleteall = new single_button(new moodle_url($thispage, $deleteallparams), get_string('delete'), 'post');
$cancel = new single_button(new moodle_url($reportpage), get_string('cancel'), 'post');
echo $OUTPUT->header();
echo $OUTPUT->heading(get_string('infectedfiles', 'report_infectedfiles'));
echo $OUTPUT->confirm(get_string('confirmdeleteall', 'report_infectedfiles'), $confirmeddeleteall, $cancel);
echo $OUTPUT->footer();
die;
case 'deleteall':
require_sesskey();
// Remove file until current time.
quarantine::clean_up_quarantine_folder(time());
redirect($reportpage);
default:
break;
}
+1 -1
View File
@@ -29,7 +29,7 @@
defined('MOODLE_INTERNAL') || die();
$version = 2020081400.00; // YYYYMMDD = weekly release date of this DEV branch.
$version = 2020081400.01; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
$release = '4.0dev (Build: 20200814)'; // Human-friendly version name