Commit e14fcaeb authored by Richard Mansfield's avatar Richard Mansfield

Virus checking

parent 975dfdfb
......@@ -220,7 +220,7 @@ function checknoattachments() {
// need to be unique.
function attachtopost(data) {
var rowid = data.uploadnumber ? 'uploaded:' + data.uploadnumber : 'artefact:' + data.id;
if (fileattached_id(rowid)) {
if (fileattached_id(rowid) || data.error) {
return;
}
appendChildNodes(attached.tbody,
......
......@@ -40,14 +40,17 @@ $createid = param_variable('createid');
// javascript on the edit blog post page.
safe_require('artefact', 'blog');
if (ArtefactTypeBlogPost::save_attachment_temporary('userfile', session_id() . $createid,
$result->uploadnumber)) {
$errormessage = ArtefactTypeBlogPost::save_attachment_temporary('userfile', session_id() . $createid,
$result->uploadnumber);
if (!$errormessage) {
$result->error = false;
$result->message = get_string('uploadoffilecomplete', 'artefact.file', $result->title);
}
else {
$result->error = 'uploadfailed';
$result->message = get_string('uploadoffilefailed', 'artefact.file', $result->title);
$result->error = 'local';
$result->message = get_string('uploadoffilefailed', 'artefact.file', $result->title)
. ': ' . $errormessage;
}
$r = json_encode($result);
......
......@@ -287,19 +287,18 @@ class ArtefactTypeFile extends ArtefactTypeFileBase {
public function save_uploaded_file($inputname) {
require_once('uploadmanager.php');
$um = new upload_manager($inputname);
if (!$um->preprocess_file()) {
return false;
if ($error = $um->preprocess_file()) {
return $error;
}
$this->size = $um->file['size'];
$this->dirty = true;
$this->commit();
// Save the file using its id as the filename, and use its id modulo
// the number of subdirectories as the directory name.
if (!$um->save_file(self::get_file_directory($this->id) , $this->id)) {
if ($error = $um->save_file(self::get_file_directory($this->id) , $this->id)) {
$this->delete();
return false;
}
return true;
}
return $error;
}
......
......@@ -60,15 +60,16 @@ if ($oldid = ArtefactTypeFileBase::exists_in_db($data->title, $data->owner, $par
}
if (!isset($result->error)) {
$f = new ArtefactTypeFile(0, $data);
if ($f->save_uploaded_file('userfile')) {
$errmsg = $f->save_uploaded_file('userfile');
if (!$errmsg) {
$result->error = false;
$result->message = get_string('uploadoffilecomplete', 'artefact.file',
$f->get('title'));
}
else {
$result->error = 'uploadfailed';
$result->error = 'local';
$result->message = get_string('uploadoffilefailed', 'artefact.file',
$f->get('title'));
$f->get('title')) . ': ' . $errmsg;
}
}
......
......@@ -402,4 +402,18 @@ $string['friendlistfailure'] = 'Failed to modify your friends list';
$string['viewavailable'] = 'View available';
$string['viewsavailable'] = 'Views available';
$string['allviews'] = 'All views';
// Upload manager
$string['quarantinedirname'] = 'quarantine';
$string['clammovedfile'] = 'The file has been moved to a quarantine directory.';
$string['clamdeletedfile'] = 'The file has been deleted';
$string['clamdeletedfilefailed'] = 'The file could not be deleted';
$string['clambroken'] = 'Your administrator has enabled virus checking for file uploads but has misconfigured something. Your file upload was NOT successful. Your administrator has been emailed to notify them so they can fix it. Maybe try uploading this file later.';
$string['clamemailsubject'] = '%s :: Clam AV notification';
$string['clamlost'] = 'Clam AV is configured to run on file upload, but the path supplied to Clam AV, %s, is invalid.';
$string['virusfounduser'] = 'The file you have uploaded, %s, has been scanned by a virus checker and found to be infected! Your file upload was NOT successful.';
$string['clamfailed'] = 'Clam AV has failed to run. The return error message was %s. Here is the output from Clam:';
$string['clamunknownerror'] = 'There was an unknown error with clam.';
?>
......@@ -524,6 +524,17 @@
<KEY NAME="institution" TYPE="foreign" FIELDS="institution" REFTABLE="institution" REFFIELDS="name"/>
</KEYS>
</TABLE>
<TABLE NAME="usr_infectedupload">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true" />
<FIELD NAME="usr" TYPE="int" LENGTH="10" NOTNULL="true" />
<FIELD NAME="time" TYPE="datetime" NOTNULL="true"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id" />
<KEY NAME="usrfk" TYPE="foreign" FIELDS="usr" REFTABLE="usr" REFFIELDS="id" />
</KEYS>
</TABLE>
<TABLE NAME="usr_password_request">
<FIELDS>
<FIELD NAME="usr" TYPE="int" LENGTH="10" NOTNULL="true" />
......
......@@ -131,6 +131,21 @@ function xmldb_core_upgrade($oldversion=0) {
}
if ($oldversion < 2006122200) {
// Creating usr_infectedupload table
$table = new XMLDBTable('usr_infectedupload');
$table->addFieldInfo('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED,
XMLDB_NOTNULL, XMLDB_SEQUENCE, null, null, null);
$table->addFieldInfo('usr', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL);
$table->addFieldInfo('time', XMLDB_TYPE_DATETIME, null, null, XMLDB_NOTNULL);
$table->addKeyInfo('primary', XMLDB_KEY_PRIMARY, array('id'));
$table->addKeyInfo('usrfk', XMLDB_KEY_FOREIGN, array('usr'), 'usr', array('id'));
$status = $status && create_table($table);
}
return $status;
}
......
......@@ -34,30 +34,31 @@ class upload_manager {
* Gets file information out of $_FILES and stores it locally in $files.
* Checks file against max upload file size.
* Scans file for viruses.
* @return boolean
* @return false for no errors, or a string describing the error
*/
function preprocess_file() {
$name = $this->inputname;
if (!isset($_FILES[$name])) {
return false;
return get_string('noinputnamesupplied');
}
$file = $_FILES[$name];
if (!is_uploaded_file($file['tmp_name']) || $file['size'] == 0) {
return false;
if (!is_uploaded_file($file['tmp_name'])) {
return get_string('notphpuploadedfile');
}
$maxsize = get_config('maxuploadsize');
if ($maxsize && $file['size'] > $maxsize) {
return false;
return get_string('uploadedfiletoobig');
}
if (get_config('viruschecking') && !clam_scan_file($file)) {
return false;
if (get_config('viruschecking') && ($errormsg = clam_scan_file($file))) {
return $errormsg;
}
$this->file = $file;
return true;
return false;
}
/**
......@@ -66,12 +67,12 @@ class upload_manager {
* @uses $CFG
* @param string $destination The destination directory.
* @param string $newname The filename
* @return boolean status;
* @return false if no errors, or a string describing the error
*/
function save_file($destination, $newname) {
if (!isset($this->file)) {
return false;
return get_string('unknownerror');
}
$dataroot = get_config('dataroot');
......@@ -101,9 +102,9 @@ class upload_manager {
if (move_uploaded_file($this->file['tmp_name'], $destination . '/' . $newname)) {
chmod($destination . '/' . $newname, 0700);
return true;
return false;
}
return false;
return get_string('failedmovingfiletodataroot');
}
......@@ -114,10 +115,11 @@ class upload_manager {
* @return boolean
*/
function process_file_upload($destination, $newname) {
if ($this->preprocess_file()) {
$error = $this->preprocess_file();
if (!$error) {
return $this->save_file($destination, $newname);
}
return false;
return $error;
}
/**
......@@ -147,9 +149,178 @@ FOR EXAMPLE CLAM_HANDLE_INFECTED_FILE AND CLAM_REPLACE_INFECTED_FILE USED FROM C
UPLOAD_PRINT_FORM_FRAGMENT DOESN'T REALLY BELONG IN THE CLASS BUT CERTAINLY IN THIS FILE
***************************************************************************************/
/**
* Deals with an infected file - either moves it to a quarantinedir
* (specified in CFG->quarantinedir) or deletes it.
*
* If moving it fails, it deletes it.
*
*@uses $CFG
* @uses $USER
* @param string $file Full path to the file
* @param int $userid If not used, defaults to $USER->id (there in case called from cron)
* @param boolean $basiconly Admin level reporting or user level reporting.
* @return string Details of what the function did.
*/
function clam_handle_infected_file($file) {
global $USER;
$userid = $USER->get('id');
$quarantinedir = get_config('dataroot') . get_string('quarantinedirname');
check_dir_exists($quarantinedir);
if (is_dir($quarantinedir) && is_writable($quarantinedir)) {
$now = date('YmdHis');
$newname = $quarantinedir .'/'. $now .'-user-'. $userid .'-infected';
if (rename($file, $newname)) {
clam_log_infected($file, $newname);
return get_string('clammovedfile');
}
}
if (unlink($file)) {
clam_log_infected($file, '', $userid);
return get_string('clamdeletedfile');
}
return get_string('clamdeletefilefailed');
}
/**
* If $CFG->runclamonupload is set, we scan a given file. (called from {@link preprocess_files()})
*
* This function will add on a uploadlog index in $file.
* @param mixed $file The file to scan from $files. or an absolute path to a file.
* @return false if no errors, or a string if there's an error.
*/
function clam_scan_file(&$file) {
return true;
if (is_array($file) && is_uploaded_file($file['tmp_name'])) { // it's from $_FILES
$fullpath = $file['tmp_name'];
}
else if (file_exists($file)) { // it's a path to somewhere on the filesystem!
$fullpath = $file;
}
else {
return get_string('unknownerror');
}
$pathtoclam = trim(get_config('pathtoclam'));
if (!$pathtoclam || !file_exists($pathtoclam) || !is_executable($pathtoclam)) {
clam_mail_admins(get_string('clamlost', 'mahara', $pathtoclam));
clam_handle_infected_file($fullpath);
return get_string('clambroken');
}
$cmd = $pathtoclam .' '. $fullpath ." 2>&1";
// before we do anything we need to change perms so that clamscan
// can read the file (clamdscan won't work otherwise)
chmod($fullpath,0644);
exec($cmd, $output, $return);
switch ($return) {
case 0: // glee! we're ok.
return false; // no error
case 1: // bad wicked evil, we have a virus.
global $USER;
$userid = $USER->get('id');
$username = $USER->get('username') . ' (' . full_name() . ')';
clam_handle_infected_file($fullpath);
// Notify admins if user has uploaded more than 3 infected
// files in the last month
if (count_records_sql('SELECT COUNT(*) FROM ' . get_config('dbprefix') . 'usr_infectedupload
WHERE usr = ? AND time > CURRENT_TIMESTAMP - ?::INTERVAL;', array($userid, '1 month')) >= 2) {
log_debug('sending virusrepeat notification');
$data = (object) array('name' => $username, 'id' => $userid);
require_once('activity.php');
activity_occurred('virusrepeat', $data);
}
$data = (object) array('usr' => $userid, 'time' => db_format_timestamp(time()));
insert_record('usr_infectedupload', $data, 'id');
return get_string('virusfounduser', 'mahara', display_name($USER));
default:
// error - clam failed to run or something went wrong
$notice = get_string('clamfailed', 'mahara', get_clam_error_code($return));
$notice .= "\n\n". implode("\n", $output);
$notice .= "\n". clam_handle_infected_file($fullpath);
clam_mail_admins($notice);
return get_string('clambroken');
}
}
/**
* Emails admins about a clam outcome
*
* @param string $notice The body of the email to be sent.
*/
function clam_mail_admins($notice) {
$subject = get_string('clamemailsubject', 'mahara', get_config('sitename'));
$adminusers = get_records_array('usr', 'admin', 1);
if ($adminusers) {
foreach ($adminusers as $admin) {
// This should probably be some kind of notification
// rather than an email
email_user($admin, null, $subject, $notice);
}
}
}
/**
* Returns the string equivalent of a numeric clam error code
*
* @param int $returncode The numeric error code in question.
* return string The definition of the error code
*/
function get_clam_error_code($returncode) {
$returncodes = array();
$returncodes[0] = 'No virus found.';
$returncodes[1] = 'Virus(es) found.';
$returncodes[2] = ' An error occured'; // specific to clamdscan
// all after here are specific to clamscan
$returncodes[40] = 'Unknown option passed.';
$returncodes[50] = 'Database initialization error.';
$returncodes[52] = 'Not supported file type.';
$returncodes[53] = 'Can\'t open directory.';
$returncodes[54] = 'Can\'t open file. (ofm)';
$returncodes[55] = 'Error reading file. (ofm)';
$returncodes[56] = 'Can\'t stat input file / directory.';
$returncodes[57] = 'Can\'t get absolute path name of current working directory.';
$returncodes[58] = 'I/O error, please check your filesystem.';
$returncodes[59] = 'Can\'t get information about current user from /etc/passwd.';
$returncodes[60] = 'Can\'t get information about user \'clamav\' (default name) from /etc/passwd.';
$returncodes[61] = 'Can\'t fork.';
$returncodes[63] = 'Can\'t create temporary files/directories (check permissions).';
$returncodes[64] = 'Can\'t write to temporary directory (please specify another one).';
$returncodes[70] = 'Can\'t allocate and clear memory (calloc).';
$returncodes[71] = 'Can\'t allocate memory (malloc).';
if ($returncodes[$returncode])
return $returncodes[$returncode];
return get_string('clamunknownerror');
}
/**
* This function logs to error_log and to the log table that an infected file has been found and what's happened to it.
*
* @param string $oldfilepath Full path to the infected file before it was moved.
* @param string $newfilepath Full path to the infected file since it was moved to the quarantine directory (if the file was deleted, leave empty).
* @param int $userid The user id of the user who uploaded the file.
*/
function clam_log_infected($oldfilepath='', $newfilepath='', $userid=0) {
global $USER;
$username = $USER->get('username') . ' (' . full_name() . ')';
$errorstr = 'Clam AV has found a file that is infected with a virus. It was uploaded by '
. full_name()
. ((empty($oldfilepath)) ? '. The infected file was caught on upload ('.$oldfilepath.')'
: '. The original file path of the infected file was '. $oldfilepath)
. ((empty($newfilepath)) ? '. The file has been deleted ' : '. The file has been moved to a quarantine directory and the new path is '. $newfilepath);
error_log($errorstr);
}
?>
......@@ -27,7 +27,7 @@
defined('INTERNAL') || die();
$config = new StdClass;
$config->version = 2006121800;
$config->version = 2006122200;
$config->release = '0.2';
$config->minupgradefrom = 2006121501;
$config->minupgraderelease = '0.1 (build tag BUILD_20061215)';
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment