ÿØÿà JFIF    ÿÛ „  ( %!1"%)+...383-7(-.+  -%%--------+------------/----------------------------ÿÀ  ¶" ÿÄ     ÿÄ @    !1AQaq‘¡"±ÁÑð2’BRráS‚ñ3b¢CT²Ò #cÿÄ    ÿÄ '   1!AQ2a"‘±#ÿÚ   ? ò(/p‘—–¢·,M|¯G»R£üešT. ôŸEoCFñ‡ 4ÔkåóxR|µÁ¨ÓDà!Ç´µ˜‡p ÍQ‹§!þž^õWsŒy  åÁ§$á«%ºq˜™JyyË0y«^Ϗ -3ÃÏËÈ£Æ\P’ä„s&•‹Üs üô'àWt¹pñCp$bÚF`·7.^)¾!)ÒÙ˜–ÅÏš^h¾ø³·ìe±ŒaeÖ—Í,e=B}Jéߦ$$½—šÙÜCC¯¶è8β–Åkèwx LÉngT±æ¹£Ócœ¿Í/©Éÿ ‰ÚCÒ­mq;Tt¶–}0"zÖPcd1š­¤tˆ„ÐÓâqú[™öUCŠ2޿د­É(¸ßúçšmW,Ò ºf‹Ån™ÄLêÁ8üÁkÁ€L¥ÍuJ1œôrÂSŒÓöt!7EUíïb)AËi¬â-ÙHk8òÁiXYwÅ)O^"ÇØriÛô{»wRUKÙÏÚlÓ¬€‚’שT ] ªb—ÊîKC^×M“xùäñz¬zJ× {4[¹M'IÆeH1LCWÕ]œûº WDæà°OÅ‚¹c²ÞuA–rL`HšpCez‡V’— Ž‚Ë„–ñ­'µR0ÕÖ=À:±G»C\nÆÉ5*¢ †3šhQ4ýÒi$Õ1"Ü]¢p-q3zcQ$ªö¨¥æf›‘»´ÝÚHá„e²JÇ–iÊ:·à¬Øc5Åk»Laª*wi [0Ówk ¦a¦0Õ²ÅÅ‚T¸’³q:Æ<¹ú Ò˜Æ}2VE•Ѐp¡Ô3Úº …h³^LJºçì¥Ánó|˜ÖˆÎˆ+« ¦dø‘Mº–d\ªDðÛ„Ì×LÝ——Ð9Ç|•FèçVm™¨€™3õS–9Äå¢<̉Ìm¢±`·º˜&Z²ž Ë®D«¶»6™‰©­HÈ Àá«h­i³04æ’hxœi3ï¨(ÓL·†‹¶ËpŒ ƒÜÒ? :õdsÀ –3æÓ·Üjâ¬9—]8n¼&]I F£¼…ì–—Œ0v°LǐÅfÛ2TÖ’aµ˜N@ÖBSÏ^à´4-±°b×nÈÈœÆD=KdŒø…ÄÈK,›ÖR§fjü$^ÔÎ!„6^ s:Üun\ôkCÞò÷<—Lúl Í›G¹ÀPá¼Nc¡šÒ…ÙWͳœ±vÊáЄ×9‹P‰cEØ‘™›µŸP<×g1 º©Ø¬wiÁ^l5× I/';Ë+´Z‡ÓÄFÕp[Èð’ HÄ ó 9¬Dl4“é±Ï”V=^Hð;žLädÉa‚°Ë,#/Ž~²BÔÛ QãñQtIeórVB,ÏÂf‹4#Μc‹É3ZIÖ Œ¢¥·üxa‡Ãçè„jŠ[5 Ä!U?,3žÎ×…ðdÓ÷háŠA‰ìÄ4÷ÃÜZÃE{‰®+W\ZÍE[‰»µk»MÝ­`¢©†˜ÃV‹-f¢¡†¢a«eŠ%«Y¨©q%jâd,Ôq`) ¤¦…šˆ ¦E>0Ùb²t–Ÿ  ÷jsςҒKÈcø(é›;k(’#ôŒQ ™C> õ³M9óºÆµ¤çâ;uÀ)5>RÔ¸fÓ~Ȧ—’-$fe«"FâUˆÊwµPÏu w+"åÉó¨6s™Û9(À¨ù©hE°½†n`m~“@©›p!Vkhµ Á1šbEqqÊ@ºèl›2 –Yã%‘‚hÛ{a¶‡ÅÞM²¬çL34®Çl]®‹ŽØŒm'RÜe35æöˆ­Á­ cP9šeÁköo´L³Ì=p8]”‡B¶,šº|ɏdzXˆØk*ÇÚ{#ñ‰pã(€¶‡9ý'šÜ³½¯kƒ†Â’ëSOƒ•Å®H6+a¢µˆ‹Y¨b˜b+XˆÖ!°u!¢"µˆˆn2ˆ Å0Ä`Å Ô®c(ÓÜGº•Ä»Œ âWî%q.áÐÄ÷î%umÍ£+ÜQ,Vn¦-GphU¸¢X­&,Gt ·K¢ÅÅ·¥RÄÊÉbKn N­R2g$VµÕó Qr#–ÓZUÏð0ÎtÙÀœz ëöo“Îk¯n„T°"SÞ+³/…€½"ižãO%Í%&Δâ‘ËØ´IˆÓ°Ë‰ÞM3.™yçÁzit›`4C`k¢¼á!álÄï sÂ[×!¤l¾æo"ƒIÔ'@ªu)3›ô®“@Z à¾] üœ'‡4¸PÞkÁ-qÄH‰ÓÍBd´ÿ —¹°ÄA&ÊUC„öz‚‚RæŠtf¬>˜ÑidšEçÑL÷áý¡V±h§­c¿¥¨ƒ@g¼…Ýö^ÞËc.ž„æ“I[ ª+·zÐÒÚ,½Ð»¶€C›7jk$๖€Ž¾ÅÙpp¶ÍþéΤ ½ØÎŒq ñpæžÅ¥#Øâ49ÃÂK^MYV“,¶Éz_òæ:…;Õ«¦I3âJ£¤û5$7Èxün\â]Êe¯(Ÿ&Ž‹¶2<6Ćf× æ¢ƱqWı[(ÙÂŽç‚T7¨@ÈÉÍß‚ïÙ"&3TYlGށ†)†#5ŠA©^T2ÆÁ†)©†©†¤yGXÁ†©"†)©¼ÅV WS†#©]R–r‹q5ÕbâWþBh¯u+ªÅÄ×ü„nÑ^êkªÁj‰jeœWˆÕÕ`µDµ2Ì+Ä µDµµ5Ô{¤Þ0RF’H÷AÛ<ÊÁ§ ƈæ0Ñ£ê4 öÚµ[¬/1²€â<-¬šö²r×0o×w ´xÐÒË„ g01¬„åEM蛇Ã\6µE›‘fÙO k÷CºnÓ1`iîÒ² áÂ!ÑLÄÿ ³¬\õ*:M=Ç»c‹2[‰ØsÕLò\ŒK L^™üÄN\¶#» Ç\……ßïIœÉ$¸ÎñÍ®ÔHÇ Ûí/´>Li”„†g^$ 7NÍÑÑ2M2ªNg=k½Ð}Ÿl6‡ —JWˆ”ÞéT·+µ¦üÂFè¢Vs6ýÆ4»2+2IÝ9ô9®ÇGÙÇ1+Ä8;*Õ§ž½D*îÑWÚçá3Ma¤·ˆ¥t}š²Ü…Q+Æüµ8HíS”üxGéÁˆ¦Ël„éÈ 5 µÄƒ6äÜ(kIš¯S†fÖ&¸®×v|Äyˆ*禌Éù@7-®Ä[]ÎÖ¿ê`º…=:…žJVnݳvðÖKE¼‚ »mà ÷f®:Ë ™–4rð¡4` ܹçž~‹C=œ…¾Ïj|VDtK.ƒ"9 Ï ­–éXá³6WÒïIL-Ї Ç manager.php000064400000033256152267641510006711 0ustar00. /** * 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) { $notice = $antivirus->get_scanning_notice(); $incidentdetails = $antivirus->get_incident_details($file, $filename, $notice, false); // Log scan error event. $params = [ 'context' => \context_system::instance(), 'relateduserid' => $USER->id, 'other' => ['filename' => $filename, 'incidentdetails' => $incidentdetails], ]; $event = \core\event\antivirus_scan_file_error::create($params); $event->trigger(); // If there was a scanner exception (such as ClamAV denying // upload), send messages (on error and above), and rethrow. if ($notifylevel === $antivirus::SCAN_RESULT_ERROR) { $notice = $antivirus->get_scanning_notice(); self::send_antivirus_messages($antivirus, $incidentdetails); } throw $e; } $notice = $antivirus->get_scanning_notice(); if ($result === $antivirus::SCAN_RESULT_FOUND) { // Infection found, send notification. $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, $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_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); // Log scan error event. $params = [ 'context' => \context_system::instance(), 'relateduserid' => $USER->id, 'other' => ['filename' => $filename, 'incidentdetails' => $incidentdetails], ]; $event = \core\event\antivirus_scan_file_error::create($params); $event->trigger(); // Send a notification if required (error or above). if ($notifylevel === $antivirus::SCAN_RESULT_ERROR) { self::send_antivirus_messages($antivirus, $incidentdetails); } } } } /** * Scan data steam using all enabled antiviruses, throws exception in case of infected data. * * @param string $data The variable containing the data to scan. * @throws \core\antivirus\scanner_exception If data is infected. * @return void */ 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(); $incidentdetails = $antivirus->get_incident_details('', $filename, $notice, false); // Log scan error event. $params = [ 'context' => \context_system::instance(), 'relateduserid' => $USER->id, 'other' => ['filename' => $filename, 'incidentdetails' => $incidentdetails], ]; $event = \core\event\antivirus_scan_file_error::create($params); $event->trigger(); // If there was a scanner exception (such as ClamAV denying upload), send messages and rethrow. if ($notifylevel === $antivirus::SCAN_RESULT_ERROR) { $notice = $antivirus->get_scanning_notice(); $filename = get_string('datastream', 'antivirus'); 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. $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); // Log scan error event. $params = [ 'context' => \context_system::instance(), 'relateduserid' => $USER->id, 'other' => ['filename' => $filename, 'incidentdetails' => $incidentdetails], ]; $event = \core\event\antivirus_scan_data_error::create($params); $event->trigger(); // Send a notification if required (error or above). if ($notifylevel === $antivirus::SCAN_RESULT_ERROR) { self::send_antivirus_messages($antivirus, $incidentdetails); } } } } /** * Returns instance of antivirus. * * @param string $antivirusname name of antivirus. * @return object|bool antivirus instance or false if does not exist. */ 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); } } } } scanner_exception.php000064400000003156152267641510011002 0ustar00. /** * Exception for antivirus. * * @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(); /** * An antivirus scanner exception class. * * @package core * @subpackage antivirus * @copyright 2015 Ruslan Kabalin, Lancaster University. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class scanner_exception extends \moodle_exception { /** * Constructs a new exception * * @param string $errorcode * @param string $link * @param mixed $a * @param mixed $debuginfo * @param string $module optional plugin name */ public function __construct($errorcode, $link = '', $a = null, $debuginfo = null, $module = 'antivirus') { parent::__construct($errorcode, $module, $link, $a, $debuginfo); } } quarantine.php000064400000025772152267641510007452 0ustar00. /** * Quarantine file * * @package core_antivirus * @author Nathan Nguyen * @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 * @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]); } } scanner.php000064400000021433152267641510006722 0ustar00. /** * Base 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(); require_once($CFG->dirroot . '/iplookup/lib.php'); /** * Base abstract antivirus scanner class. * * @package core * @subpackage antivirus * @copyright 2015 Ruslan Kabalin, Lancaster University. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class scanner { /** Scanning result indicating no virus found. */ const SCAN_RESULT_OK = 0; /** Scanning result indicating that virus is found. */ const SCAN_RESULT_FOUND = 1; /** Scanning result indicating the error. */ const SCAN_RESULT_ERROR = 2; /** @var \stdClass the config for antivirus */ protected $config; /** @var string scanning notice */ protected $scanningnotice = ''; /** @var array any admin messages generated by a plugin. */ protected $messages = []; /** * Class constructor. * * @return void. */ public function __construct() { // Populate config variable, inheriting class namespace is matching // full plugin name, so we can use it directly to retrieve plugin // configuration. $ref = new \ReflectionClass(get_class($this)); $this->config = get_config($ref->getNamespaceName()); } /** * Config get method. * * @param string $property config property to get. * @return mixed * @throws \coding_exception */ public function get_config($property) { if (property_exists($this->config, $property)) { return $this->config->$property; } throw new \coding_exception('Config property "' . $property . '" doesn\'t exist'); } /** * Get scanning notice. * * @return string */ public function get_scanning_notice() { return $this->scanningnotice; } /** * Set scanning notice. * * @param string $notice notice to set. * @return void */ protected function set_scanning_notice($notice) { $this->scanningnotice = $notice; } /** * Are the antivirus settings configured? * * @return bool True if plugin has been configured. */ abstract public function is_configured(); /** * Scan 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). * @return int Scanning result constants. */ abstract public function scan_file($file, $filename); /** * Scan data. * * By default it saves data variable content to file and then scans it using * scan_file method, however if antivirus plugin permits scanning data directly, * the method can be overridden. * * @param string $data The variable containing the data to scan. * @return int Scanning result constants. */ public function scan_data($data) { // Prepare temp file. $tempdir = make_request_directory(); $tempfile = $tempdir . DIRECTORY_SEPARATOR . rand(); file_put_contents($tempfile, $data); // Perform a virus scan now. return $this->scan_file($tempfile, get_string('datastream', 'antivirus')); } /** * 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. * @param string $eventname event name * @return void * @throws \coding_exception * @throws \moodle_exception */ public function message_admins($notice, $format = FORMAT_PLAIN, $eventname = 'errors') { $noticehtml = $format !== FORMAT_PLAIN ? format_text($notice, $format) : ''; $site = get_site(); $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; $user->mailformat = 1; $admins = [$user]; } else { // Otherwise, we message all admins. $admins = get_admins(); } foreach ($admins as $admin) { $eventdata = new \core\message\message(); $eventdata->courseid = SITEID; $eventdata->component = 'moodle'; $eventdata->name = $eventname; $eventdata->userfrom = get_admin(); $eventdata->userto = $admin; $eventdata->subject = $subject; $eventdata->fullmessage = $notice; $eventdata->fullmessageformat = $format; $eventdata->fullmessagehtml = $noticehtml; $eventdata->smallmessage = ''; // Now add the message to an array to be sent by the antivirus manager. $this->messages[] = $eventdata; } } /** * 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 * @param string $virus if this template is due to a virus found. * @return string the incident details * @throws \coding_exception */ 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'); $content->header = get_string('emailinfectedfiledetected', 'antivirus'); $content->filename = !empty($filename) ? $filename : $unknown; $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_path($file); $content->contenttype = mime_content_type($file); } else { $content->filesize = $unknown; $content->contenthash = $unknown; $content->contenttype = $unknown; } $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(); // 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; } /** * Getter method for the antivirus message displayed in the exception. * * @return array array of string and component to pass to exception constructor. */ public function get_virus_found_message() { // Base antivirus found string. return ['string' => 'virusfound', 'component' => 'antivirus', 'placeholders' => []]; } }