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 * Quarantine file 19 * 20 * @package core_antivirus 21 * @author Nathan Nguyen <nathannguyen@catalyst-au.net> 22 * @copyright Catalyst IT 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 namespace core\antivirus; 27 28 defined('MOODLE_INTERNAL') || die(); 29 require_once($CFG->libdir.'/filelib.php'); 30 31 /** 32 * Quarantine file 33 * 34 * @package core_antivirus 35 * @author Nathan Nguyen <nathannguyen@catalyst-au.net> 36 * @copyright Catalyst IT 37 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 38 */ 39 class quarantine { 40 41 /** Default quarantine folder */ 42 const DEFAULT_QUARANTINE_FOLDER = 'antivirus_quarantine'; 43 44 /** Zip infected file */ 45 const FILE_ZIP_INFECTED = '_infected_file.zip'; 46 47 /** Zip all infected file */ 48 const FILE_ZIP_ALL_INFECTED = '_all_infected_files.zip'; 49 50 /** Incident details file */ 51 const FILE_HTML_DETAILS = '_details.html'; 52 53 /** Incident details file */ 54 const DEFAULT_QUARANTINE_TIME = DAYSECS * 28; 55 56 /** Date format in filename */ 57 const FILE_NAME_DATE_FORMAT = '%Y%m%d%H%M%S'; 58 59 /** 60 * Move the infected file to the quarantine folder. 61 * 62 * @param string $file infected file. 63 * @param string $filename infected file name. 64 * @param string $incidentdetails incident details. 65 * @param string $notice notice details. 66 * @return string|null the name of the newly created quarantined file. 67 * @throws \dml_exception 68 */ 69 public static function quarantine_file(string $file, string $filename, string $incidentdetails, string $notice) : ?string { 70 if (!self::is_quarantine_enabled()) { 71 return null; 72 } 73 // Generate file names. 74 $date = userdate(time(), self::FILE_NAME_DATE_FORMAT) . "_" . rand(); 75 $zipfilepath = self::get_quarantine_folder() . $date . self::FILE_ZIP_INFECTED; 76 $detailsfilename = $date . self::FILE_HTML_DETAILS; 77 78 // Create Zip file. 79 $ziparchive = new \zip_archive(); 80 if ($ziparchive->open($zipfilepath, \file_archive::CREATE)) { 81 $ziparchive->add_file_from_string($detailsfilename, format_text($incidentdetails, FORMAT_MOODLE)); 82 $ziparchive->add_file_from_pathname($filename, $file); 83 $ziparchive->close(); 84 } 85 $zipfile = basename($zipfilepath); 86 self::create_infected_file_record($filename, $zipfile, $notice); 87 return $zipfile; 88 } 89 90 /** 91 * Move the infected file to the quarantine folder. 92 * 93 * @param string $data data which is infected. 94 * @param string $filename infected file name. 95 * @param string $incidentdetails incident details. 96 * @param string $notice notice details. 97 * @return string|null the name of the newly created quarantined file. 98 * @throws \dml_exception 99 */ 100 public static function quarantine_data(string $data, string $filename, string $incidentdetails, string $notice) : ?string { 101 if (!self::is_quarantine_enabled()) { 102 return null; 103 } 104 // Generate file names. 105 $date = userdate(time(), self::FILE_NAME_DATE_FORMAT) . "_" . rand(); 106 $zipfilepath = self::get_quarantine_folder() . $date . self::FILE_ZIP_INFECTED; 107 $detailsfilename = $date . self::FILE_HTML_DETAILS; 108 109 // Create Zip file. 110 $ziparchive = new \zip_archive(); 111 if ($ziparchive->open($zipfilepath, \file_archive::CREATE)) { 112 $ziparchive->add_file_from_string($detailsfilename, format_text($incidentdetails, FORMAT_MOODLE)); 113 $ziparchive->add_file_from_string($filename, $data); 114 $ziparchive->close(); 115 } 116 $zipfile = basename($zipfilepath); 117 self::create_infected_file_record($filename, $zipfile, $notice); 118 return $zipfile; 119 } 120 121 /** 122 * Check if the virus quarantine is allowed 123 * 124 * @return bool 125 * @throws \dml_exception 126 */ 127 public static function is_quarantine_enabled() : bool { 128 return !empty(get_config("antivirus", "enablequarantine")); 129 } 130 131 /** 132 * Get quarantine folder 133 * 134 * @return string path of quarantine folder 135 */ 136 private static function get_quarantine_folder() : string { 137 global $CFG; 138 $quarantinefolder = $CFG->dataroot . DIRECTORY_SEPARATOR . self::DEFAULT_QUARANTINE_FOLDER; 139 if (!file_exists($quarantinefolder)) { 140 make_upload_directory(self::DEFAULT_QUARANTINE_FOLDER); 141 } 142 return $quarantinefolder . DIRECTORY_SEPARATOR; 143 } 144 145 /** 146 * Checks whether a file exists inside the antivirus quarantine folder. 147 * 148 * @param string $filename the filename to check. 149 * @return boolean whether file exists. 150 */ 151 public static function quarantined_file_exists(string $filename) : bool { 152 $folder = self::get_quarantine_folder(); 153 return file_exists($folder . $filename); 154 } 155 156 /** 157 * Download quarantined file. 158 * 159 * @param int $fileid the id of file to be downloaded. 160 */ 161 public static function download_quarantined_file(int $fileid) { 162 global $DB; 163 164 // Get the filename to be downloaded. 165 $filename = $DB->get_field('infected_files', 'quarantinedfile', ['id' => $fileid], IGNORE_MISSING); 166 // If file record isnt found, user might be doing something naughty in params, or a stale request. 167 if (empty($filename)) { 168 return; 169 } 170 171 $file = self::get_quarantine_folder() . $filename; 172 send_file($file, $filename); 173 } 174 175 /** 176 * Delete quarantined file. 177 * 178 * @param int $fileid id of file to be deleted. 179 */ 180 public static function delete_quarantined_file(int $fileid) { 181 global $DB; 182 183 // Get the filename to be deleted. 184 $filename = $DB->get_field('infected_files', 'quarantinedfile', ['id' => $fileid], IGNORE_MISSING); 185 // If file record isnt found, user might be doing something naughty in params, or a stale request. 186 if (empty($filename)) { 187 return; 188 } 189 190 // Delete the file from the folder. 191 $file = self::get_quarantine_folder() . $filename; 192 if (file_exists($file)) { 193 unlink($file); 194 } 195 196 // Now we are finished with the record, delete the quarantine information. 197 self::delete_infected_file_record($fileid); 198 } 199 200 /** 201 * Download all quarantined files. 202 * 203 * @return void 204 */ 205 public static function download_all_quarantined_files() { 206 $files = new \DirectoryIterator(self::get_quarantine_folder()); 207 // Add all infected files to a zip file. 208 $date = userdate(time(), self::FILE_NAME_DATE_FORMAT); 209 $zipfilename = $date . self::FILE_ZIP_ALL_INFECTED; 210 $zipfilepath = self::get_quarantine_folder() . DIRECTORY_SEPARATOR . $zipfilename; 211 $tempfilestocleanup = []; 212 213 $ziparchive = new \zip_archive(); 214 if ($ziparchive->open($zipfilepath, \file_archive::CREATE)) { 215 foreach ($files as $file) { 216 if (!$file->isDot()) { 217 // Only send the actual files. 218 $filename = $file->getFilename(); 219 $filepath = $file->getPathname(); 220 $ziparchive->add_file_from_pathname($filename, $filepath); 221 } 222 } 223 $ziparchive->close(); 224 } 225 226 // Clean up temp files. 227 foreach ($tempfilestocleanup as $tempfile) { 228 if (file_exists($tempfile)) { 229 unlink($tempfile); 230 } 231 } 232 233 send_temp_file($zipfilepath, $zipfilename); 234 } 235 236 /** 237 * Return array of quarantined files. 238 * 239 * @return array list of quarantined files. 240 */ 241 public static function get_quarantined_files() : array { 242 $files = new \DirectoryIterator(self::get_quarantine_folder()); 243 $filestosort = []; 244 245 // Grab all files that match the naming structure. 246 foreach ($files as $file) { 247 $filename = $file->getFilename(); 248 if (!$file->isDot() && strpos($filename, self::FILE_ZIP_INFECTED) !== false) { 249 $filestosort[$filename] = $file->getPathname(); 250 } 251 } 252 253 krsort($filestosort, SORT_NATURAL); 254 return $filestosort; 255 } 256 257 /** 258 * Clean up quarantine folder 259 * 260 * @param int $timetocleanup time to clean up 261 */ 262 public static function clean_up_quarantine_folder(int $timetocleanup) { 263 $files = new \DirectoryIterator(self::get_quarantine_folder()); 264 // Clean up the folder. 265 foreach ($files as $file) { 266 $filename = $file->getFilename(); 267 268 // Only delete files that match the correct name structure. 269 if (!$file->isDot() && strpos($filename, self::FILE_ZIP_INFECTED) !== false) { 270 $modifiedtime = $file->getMTime(); 271 272 if ($modifiedtime <= $timetocleanup) { 273 unlink($file->getPathname()); 274 } 275 } 276 } 277 278 // Lastly cleanup the infected files table as well. 279 self::clean_up_infected_records($timetocleanup); 280 } 281 282 /** 283 * This function removes any stale records from the infected files table. 284 * 285 * @param int $timetocleanup the time to cleanup from 286 * @return void 287 */ 288 private static function clean_up_infected_records(int $timetocleanup) { 289 global $DB; 290 291 $select = "timecreated <= ?"; 292 $DB->delete_records_select('infected_files', $select, [$timetocleanup]); 293 } 294 295 /** 296 * Create an infected file record 297 * 298 * @param string $filename original file name 299 * @param string $zipfile quarantined file name 300 * @param string $reason failure reason 301 * @throws \dml_exception 302 */ 303 private static function create_infected_file_record(string $filename, string $zipfile, string $reason) { 304 global $DB, $USER; 305 306 $record = new \stdClass(); 307 $record->filename = $filename; 308 $record->quarantinedfile = $zipfile; 309 $record->userid = $USER->id; 310 $record->reason = $reason; 311 $record->timecreated = time(); 312 313 $DB->insert_record('infected_files', $record); 314 } 315 316 /** 317 * Delete the database record for an infected file. 318 * 319 * @param int $fileid quarantined file id 320 * @throws \dml_exception 321 */ 322 private static function delete_infected_file_record(int $fileid) { 323 global $DB; 324 $DB->delete_records('infected_files', ['id' => $fileid]); 325 } 326 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body