Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.
<?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/>.

/**
 * Manager class for antivirus integration.
 *
 * @package    core_antivirus
 * @copyright  2015 Ruslan Kabalin, Lancaster University.
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

namespace core\antivirus;

defined('MOODLE_INTERNAL') || die();

/**
 * Class used for various antivirus related stuff.
 *
 * @package    core_antivirus
 * @copyright  2015 Ruslan Kabalin, Lancaster University.
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class manager {
>
/** * Returns list of enabled antiviruses. * * @return array Array ('antivirusname'=>stdClass antivirus object). */ private static function get_enabled() { global $CFG; $active = array(); if (empty($CFG->antiviruses)) { return $active; } foreach (explode(',', $CFG->antiviruses) as $e) { if ($antivirus = self::get_antivirus($e)) { if ($antivirus->is_configured()) { $active[$e] = $antivirus; } } } return $active; } /** * Scan file using all enabled antiviruses, throws exception in case of infected file. * * @param string $file Full path to the file. * @param string $filename Name of the file (could be different from physical file if temp file is used). * @param bool $deleteinfected whether infected file needs to be deleted. * @throws \core\antivirus\scanner_exception If file is infected. * @return void */ public static function scan_file($file, $filename, $deleteinfected) { global $USER; $antiviruses = self::get_enabled();
> $notifylevel = (int)get_config('antivirus', 'notifylevel');
foreach ($antiviruses as $antivirus) { // 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); > // Log scan error event. throw $e; > $params = [ } > 'context' => \context_system::instance(), > 'relateduserid' => $USER->id, $notice = $antivirus->get_scanning_notice(); > 'other' => ['filename' => $filename, 'incidentdetails' => $incidentdetails], if ($result === $antivirus::SCAN_RESULT_FOUND) { > ]; // Infection found, send notification. > $event = \core\event\antivirus_scan_file_error::create($params); $incidentdetails = $antivirus->get_incident_details($file, $filename, $notice); > $event->trigger(); self::send_antivirus_messages($antivirus, $incidentdetails); > > // If there was a scanner exception (such as ClamAV denying // Move to quarantine folder. > // upload), send messages (on error and above), and rethrow. $zipfile = \core\antivirus\quarantine::quarantine_file($file, $filename, $incidentdetails, $notice); > if ($notifylevel === $antivirus::SCAN_RESULT_ERROR) { // If file not stored due to disabled quarantine, store a message. > $notice = $antivirus->get_scanning_notice();
if (empty($zipfile)) {
> } $zipfile = get_string('quarantinedisabled', 'antivirus'); >
} // Log file infected event. $params = [ 'context' => \context_system::instance(), 'relateduserid' => $USER->id, 'other' => ['filename' => $filename, 'zipfile' => $zipfile, 'incidentdetails' => $incidentdetails], ]; $event = \core\event\virus_infected_file_detected::create($params); $event->trigger(); if ($deleteinfected) { unlink($file); } // Get custom message to display to user from antivirus engine. $displaymessage = $antivirus->get_virus_found_message(); $placeholders = array_merge(['item' => $filename], $displaymessage['placeholders']); throw new \core\antivirus\scanner_exception( $displaymessage['string'], '', $placeholders, null, $displaymessage['component'] ); } 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); > // Log scan error event. } > $params = [ } > 'context' => \context_system::instance(), } > 'relateduserid' => $USER->id, > 'other' => ['filename' => $filename, 'incidentdetails' => $incidentdetails], /** > ]; * Scan data steam using all enabled antiviruses, throws exception in case of infected data. > $event = \core\event\antivirus_scan_file_error::create($params); * > $event->trigger(); * @param string $data The variable containing the data to scan. > * @throws \core\antivirus\scanner_exception If data is infected. > // Send a notification if required (error or above). * @return void > if ($notifylevel === $antivirus::SCAN_RESULT_ERROR) {
*/
> }
public static function scan_data($data) { global $USER; $antiviruses = self::get_enabled();
> $notifylevel = (int)get_config('antivirus', 'notifylevel');
foreach ($antiviruses as $antivirus) { // Attempt to scan, catching internal exceptions. try { $result = $antivirus->scan_data($data); } catch (\core\antivirus\scanner_exception $e) {
> $notice = $antivirus->get_scanning_notice(); // If there was a scanner exception (such as ClamAV denying upload), send messages and rethrow. > $incidentdetails = $antivirus->get_incident_details('', $filename, $notice, false); $notice = $antivirus->get_scanning_notice(); > $filename = get_string('datastream', 'antivirus'); > // Log scan error event. $incidentdetails = $antivirus->get_incident_details('', $filename, $notice, false); > $params = [ self::send_antivirus_messages($antivirus, $incidentdetails); > 'context' => \context_system::instance(), > 'relateduserid' => $USER->id, throw $e; > 'other' => ['filename' => $filename, 'incidentdetails' => $incidentdetails], } > ]; > $event = \core\event\antivirus_scan_file_error::create($params); $filename = get_string('datastream', 'antivirus'); > $event->trigger(); $notice = $antivirus->get_scanning_notice(); >
> if ($notifylevel === $antivirus::SCAN_RESULT_ERROR) {
< $incidentdetails = $antivirus->get_incident_details('', $filename, $notice, false);
// Infection found, send notification.
> }
$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, $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 = [ 'context' => \context_system::instance(), 'relateduserid' => $USER->id, 'other' => ['filename' => $filename, 'zipfile' => $zipfile, 'incidentdetails' => $incidentdetails], ]; $event = \core\event\virus_infected_data_detected::create($params); $event->trigger(); // Get custom message to display to user from antivirus engine. $displaymessage = $antivirus->get_virus_found_message(); $placeholders = array_merge(['item' => get_string('datastream', 'antivirus')], $displaymessage['placeholders']); throw new \core\antivirus\scanner_exception( $displaymessage['string'], '', $placeholders, null, $displaymessage['component'] ); } 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); > // Log scan error event. } > $params = [ } > 'context' => \context_system::instance(), } > 'relateduserid' => $USER->id, > 'other' => ['filename' => $filename, 'incidentdetails' => $incidentdetails], /** > ]; * Returns instance of antivirus. > $event = \core\event\antivirus_scan_data_error::create($params); * > $event->trigger(); * @param string $antivirusname name of antivirus. > * @return object|bool antivirus instance or false if does not exist. > // Send a notification if required (error or above). */ > if ($notifylevel === $antivirus::SCAN_RESULT_ERROR) {
public static function get_antivirus($antivirusname) {
> }
global $CFG; $classname = '\\antivirus_' . $antivirusname . '\\scanner'; if (!class_exists($classname)) { return false; } return new $classname(); } /** * Get the list of available antiviruses. * * @return array Array ('antivirusname'=>'localised antivirus name'). */ public static function get_available() { $antiviruses = array(); foreach (\core_component::get_plugin_list('antivirus') as $antivirusname => $dir) { $antiviruses[$antivirusname] = get_string('pluginname', 'antivirus_'.$antivirusname); } 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); } } } }