See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Manager class for antivirus integration. 19 * 20 * @package core_antivirus 21 * @copyright 2015 Ruslan Kabalin, Lancaster University. 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 namespace core\antivirus; 26 27 defined('MOODLE_INTERNAL') || die(); 28 29 /** 30 * Class used for various antivirus related stuff. 31 * 32 * @package core_antivirus 33 * @copyright 2015 Ruslan Kabalin, Lancaster University. 34 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 35 */ 36 class manager { 37 38 /** 39 * Returns list of enabled antiviruses. 40 * 41 * @return array Array ('antivirusname'=>stdClass antivirus object). 42 */ 43 private static function get_enabled() { 44 global $CFG; 45 46 $active = array(); 47 if (empty($CFG->antiviruses)) { 48 return $active; 49 } 50 51 foreach (explode(',', $CFG->antiviruses) as $e) { 52 if ($antivirus = self::get_antivirus($e)) { 53 if ($antivirus->is_configured()) { 54 $active[$e] = $antivirus; 55 } 56 } 57 } 58 return $active; 59 } 60 61 /** 62 * Scan file using all enabled antiviruses, throws exception in case of infected file. 63 * 64 * @param string $file Full path to the file. 65 * @param string $filename Name of the file (could be different from physical file if temp file is used). 66 * @param bool $deleteinfected whether infected file needs to be deleted. 67 * @throws \core\antivirus\scanner_exception If file is infected. 68 * @return void 69 */ 70 public static function scan_file($file, $filename, $deleteinfected) { 71 global $USER; 72 $antiviruses = self::get_enabled(); 73 $notifylevel = (int)get_config('antivirus', 'notifylevel'); 74 foreach ($antiviruses as $antivirus) { 75 // Attempt to scan, catching internal exceptions. 76 try { 77 $result = $antivirus->scan_file($file, $filename); 78 } catch (\core\antivirus\scanner_exception $e) { 79 $notice = $antivirus->get_scanning_notice(); 80 $incidentdetails = $antivirus->get_incident_details($file, $filename, $notice, false); 81 82 // Log scan error event. 83 $params = [ 84 'context' => \context_system::instance(), 85 'relateduserid' => $USER->id, 86 'other' => ['filename' => $filename, 'incidentdetails' => $incidentdetails], 87 ]; 88 $event = \core\event\antivirus_scan_file_error::create($params); 89 $event->trigger(); 90 91 // If there was a scanner exception (such as ClamAV denying 92 // upload), send messages (on error and above), and rethrow. 93 if ($notifylevel === $antivirus::SCAN_RESULT_ERROR) { 94 $notice = $antivirus->get_scanning_notice(); 95 self::send_antivirus_messages($antivirus, $incidentdetails); 96 } 97 98 throw $e; 99 } 100 101 $notice = $antivirus->get_scanning_notice(); 102 if ($result === $antivirus::SCAN_RESULT_FOUND) { 103 // Infection found, send notification. 104 $incidentdetails = $antivirus->get_incident_details($file, $filename, $notice); 105 self::send_antivirus_messages($antivirus, $incidentdetails); 106 107 // Move to quarantine folder. 108 $zipfile = \core\antivirus\quarantine::quarantine_file($file, $filename, $incidentdetails, $notice); 109 // If file not stored due to disabled quarantine, store a message. 110 if (empty($zipfile)) { 111 $zipfile = get_string('quarantinedisabled', 'antivirus'); 112 } 113 114 // Log file infected event. 115 $params = [ 116 'context' => \context_system::instance(), 117 'relateduserid' => $USER->id, 118 'other' => ['filename' => $filename, 'zipfile' => $zipfile, 'incidentdetails' => $incidentdetails], 119 ]; 120 $event = \core\event\virus_infected_file_detected::create($params); 121 $event->trigger(); 122 123 if ($deleteinfected) { 124 unlink($file); 125 } 126 127 // Get custom message to display to user from antivirus engine. 128 $displaymessage = $antivirus->get_virus_found_message(); 129 $placeholders = array_merge(['item' => $filename], $displaymessage['placeholders']); 130 131 throw new \core\antivirus\scanner_exception( 132 $displaymessage['string'], 133 '', 134 $placeholders, 135 null, 136 $displaymessage['component'] 137 ); 138 } else if ($result === $antivirus::SCAN_RESULT_ERROR) { 139 // Here we need to generate a different incident based on an error. 140 $incidentdetails = $antivirus->get_incident_details($file, $filename, $notice, false); 141 142 // Log scan error event. 143 $params = [ 144 'context' => \context_system::instance(), 145 'relateduserid' => $USER->id, 146 'other' => ['filename' => $filename, 'incidentdetails' => $incidentdetails], 147 ]; 148 $event = \core\event\antivirus_scan_file_error::create($params); 149 $event->trigger(); 150 151 // Send a notification if required (error or above). 152 if ($notifylevel === $antivirus::SCAN_RESULT_ERROR) { 153 self::send_antivirus_messages($antivirus, $incidentdetails); 154 } 155 } 156 } 157 } 158 159 /** 160 * Scan data steam using all enabled antiviruses, throws exception in case of infected data. 161 * 162 * @param string $data The variable containing the data to scan. 163 * @throws \core\antivirus\scanner_exception If data is infected. 164 * @return void 165 */ 166 public static function scan_data($data) { 167 global $USER; 168 $antiviruses = self::get_enabled(); 169 $notifylevel = (int)get_config('antivirus', 'notifylevel'); 170 foreach ($antiviruses as $antivirus) { 171 // Attempt to scan, catching internal exceptions. 172 try { 173 $result = $antivirus->scan_data($data); 174 } catch (\core\antivirus\scanner_exception $e) { 175 $notice = $antivirus->get_scanning_notice(); 176 $incidentdetails = $antivirus->get_incident_details('', $filename, $notice, false); 177 178 // Log scan error event. 179 $params = [ 180 'context' => \context_system::instance(), 181 'relateduserid' => $USER->id, 182 'other' => ['filename' => $filename, 'incidentdetails' => $incidentdetails], 183 ]; 184 $event = \core\event\antivirus_scan_file_error::create($params); 185 $event->trigger(); 186 187 // If there was a scanner exception (such as ClamAV denying upload), send messages and rethrow. 188 if ($notifylevel === $antivirus::SCAN_RESULT_ERROR) { 189 $notice = $antivirus->get_scanning_notice(); 190 $filename = get_string('datastream', 'antivirus'); 191 self::send_antivirus_messages($antivirus, $incidentdetails); 192 } 193 194 throw $e; 195 } 196 197 $filename = get_string('datastream', 'antivirus'); 198 $notice = $antivirus->get_scanning_notice(); 199 200 if ($result === $antivirus::SCAN_RESULT_FOUND) { 201 // Infection found, send notification. 202 $incidentdetails = $antivirus->get_incident_details('', $filename, $notice); 203 self::send_antivirus_messages($antivirus, $incidentdetails); 204 205 // Copy data to quarantine folder. 206 $zipfile = \core\antivirus\quarantine::quarantine_data($data, $filename, $incidentdetails, $notice); 207 // If file not stored due to disabled quarantine, store a message. 208 if (empty($zipfile)) { 209 $zipfile = get_string('quarantinedisabled', 'antivirus'); 210 } 211 212 // Log file infected event. 213 $params = [ 214 'context' => \context_system::instance(), 215 'relateduserid' => $USER->id, 216 'other' => ['filename' => $filename, 'zipfile' => $zipfile, 'incidentdetails' => $incidentdetails], 217 ]; 218 $event = \core\event\virus_infected_data_detected::create($params); 219 $event->trigger(); 220 221 // Get custom message to display to user from antivirus engine. 222 $displaymessage = $antivirus->get_virus_found_message(); 223 $placeholders = array_merge(['item' => get_string('datastream', 'antivirus')], $displaymessage['placeholders']); 224 225 throw new \core\antivirus\scanner_exception( 226 $displaymessage['string'], 227 '', 228 $placeholders, 229 null, 230 $displaymessage['component'] 231 ); 232 } else if ($result === $antivirus::SCAN_RESULT_ERROR) { 233 // Here we need to generate a different incident based on an error. 234 $incidentdetails = $antivirus->get_incident_details('', $filename, $notice, false); 235 236 // Log scan error event. 237 $params = [ 238 'context' => \context_system::instance(), 239 'relateduserid' => $USER->id, 240 'other' => ['filename' => $filename, 'incidentdetails' => $incidentdetails], 241 ]; 242 $event = \core\event\antivirus_scan_data_error::create($params); 243 $event->trigger(); 244 245 // Send a notification if required (error or above). 246 if ($notifylevel === $antivirus::SCAN_RESULT_ERROR) { 247 self::send_antivirus_messages($antivirus, $incidentdetails); 248 } 249 } 250 } 251 } 252 253 /** 254 * Returns instance of antivirus. 255 * 256 * @param string $antivirusname name of antivirus. 257 * @return object|bool antivirus instance or false if does not exist. 258 */ 259 public static function get_antivirus($antivirusname) { 260 global $CFG; 261 262 $classname = '\\antivirus_' . $antivirusname . '\\scanner'; 263 if (!class_exists($classname)) { 264 return false; 265 } 266 return new $classname(); 267 } 268 269 /** 270 * Get the list of available antiviruses. 271 * 272 * @return array Array ('antivirusname'=>'localised antivirus name'). 273 */ 274 public static function get_available() { 275 $antiviruses = array(); 276 foreach (\core_component::get_plugin_list('antivirus') as $antivirusname => $dir) { 277 $antiviruses[$antivirusname] = get_string('pluginname', 'antivirus_'.$antivirusname); 278 } 279 return $antiviruses; 280 } 281 282 /** 283 * This function puts all relevant information into the messages required, and sends them. 284 * 285 * @param \core\antivirus\scanner $antivirus the scanner engine. 286 * @param string $incidentdetails details of the incident. 287 * @return void 288 */ 289 public static function send_antivirus_messages(\core\antivirus\scanner $antivirus, string $incidentdetails) { 290 $messages = $antivirus->get_messages(); 291 292 // If there is no messages, and a virus is found, we should generate one, then send it. 293 if (empty($messages)) { 294 $antivirus->message_admins($antivirus->get_scanning_notice(), FORMAT_MOODLE, 'infected'); 295 $messages = $antivirus->get_messages(); 296 } 297 298 foreach ($messages as $message) { 299 300 // Check if the information is already in the current scanning notice. 301 if (!empty($antivirus->get_scanning_notice()) && 302 strpos($antivirus->get_scanning_notice(), $message->fullmessage) === false) { 303 // This is some extra information. We should append this to the end of the incident details. 304 $incidentdetails .= \html_writer::tag('pre', $message->fullmessage); 305 } 306 307 // Now update the message to the detailed version, and format. 308 $message->name = 'infected'; 309 $message->fullmessagehtml = $incidentdetails; 310 $message->fullmessageformat = FORMAT_MOODLE; 311 $message->fullmessage = format_text_email($incidentdetails, $message->fullmessageformat); 312 313 // Now we must check if message is going to a real account. 314 // It may be an email that needs to be sent to non-user address. 315 if ($message->userto->id === -1) { 316 // If this doesnt exist, send a regular email. 317 email_to_user( 318 $message->userto, 319 get_admin(), 320 $message->subject, 321 $message->fullmessage, 322 $message->fullmessagehtml 323 ); 324 } else { 325 // And now we can send. 326 message_send($message); 327 } 328 } 329 } 330 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body