Merge branch 'MDL-50887-master' of https://github.com/lucisgit/moodle
authorDan Poltawski <dan@moodle.com>
Tue, 1 Mar 2016 06:49:33 +0000 (14:49 +0800)
committerDan Poltawski <dan@moodle.com>
Tue, 1 Mar 2016 06:49:33 +0000 (14:49 +0800)
32 files changed:
admin/antiviruses.php [new file with mode: 0644]
admin/settings/plugins.php
admin/settings/security.php
admin/tool/messageinbound/classes/manager.php
config-dist.php
lang/en/admin.php
lang/en/antivirus.php [new file with mode: 0644]
lang/en/moodle.php
lib/adminlib.php
lib/antivirus/clamav/classes/scanner.php [new file with mode: 0644]
lib/antivirus/clamav/db/upgrade.php [new file with mode: 0644]
lib/antivirus/clamav/lang/en/antivirus_clamav.php [new file with mode: 0644]
lib/antivirus/clamav/settings.php [new file with mode: 0644]
lib/antivirus/clamav/tests/scanner_test.php [new file with mode: 0644]
lib/antivirus/clamav/version.php [new file with mode: 0644]
lib/behat/lib.php
lib/classes/antivirus/manager.php [new file with mode: 0644]
lib/classes/antivirus/scanner.php [new file with mode: 0644]
lib/classes/antivirus/scanner_exception.php [new file with mode: 0644]
lib/classes/component.php
lib/classes/plugininfo/antivirus.php [new file with mode: 0644]
lib/db/install.php
lib/db/upgrade.php
lib/deprecatedlib.php
lib/phpunit/bootstrap.php
lib/tests/antivirus_test.php [new file with mode: 0644]
lib/tests/component_test.php
lib/upgrade.txt
lib/uploadlib.php
repository/lib.php
repository/upload/lib.php
version.php

diff --git a/admin/antiviruses.php b/admin/antiviruses.php
new file mode 100644 (file)
index 0000000..4bde452
--- /dev/null
@@ -0,0 +1,106 @@
+<?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/>.
+
+/**
+ * Allows admin to configure antiviruses.
+ *
+ * @package    core_antivirus
+ * @copyright  2015 Ruslan Kabalin, Lancaster University.
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once('../config.php');
+require_once($CFG->libdir.'/adminlib.php');
+require_once($CFG->libdir.'/tablelib.php');
+
+$action  = required_param('action', PARAM_ALPHANUMEXT);
+$antivirus  = required_param('antivirus', PARAM_PLUGIN);
+$confirm = optional_param('confirm', 0, PARAM_BOOL);
+
+$PAGE->set_url('/admin/antiviruses.php', array('action' => $action, 'antivirus' => $antivirus));
+$PAGE->set_context(context_system::instance());
+
+require_login();
+require_capability('moodle/site:config', context_system::instance());
+
+$returnurl = "$CFG->wwwroot/$CFG->admin/settings.php?section=manageantiviruses";
+
+// Get currently installed and enabled antivirus plugins.
+$availableantiviruses = \core\antivirus\manager::get_available();
+if (!empty($antivirus) and empty($availableantiviruses[$antivirus])) {
+    redirect ($returnurl);
+}
+
+$activeantiviruses = explode(',', $CFG->antiviruses);
+foreach ($activeantiviruses as $key => $active) {
+    if (empty($availableantiviruses[$active])) {
+        unset($activeantiviruses[$key]);
+    }
+}
+
+if (!confirm_sesskey()) {
+    redirect($returnurl);
+}
+
+switch ($action) {
+    case 'disable':
+        // Remove from enabled list.
+        $key = array_search($antivirus, $activeantiviruses);
+        unset($activeantiviruses[$key]);
+        break;
+
+    case 'enable':
+        // Add to enabled list.
+        if (!in_array($antivirus, $activeantiviruses)) {
+            $activeantiviruses[] = $antivirus;
+            $activeantiviruses = array_unique($activeantiviruses);
+        }
+        break;
+
+    case 'down':
+        $key = array_search($antivirus, $activeantiviruses);
+        // Check auth plugin is valid.
+        if ($key !== false) {
+            // Move down the list.
+            if ($key < (count($activeantiviruses) - 1)) {
+                $fsave = $activeantiviruses[$key];
+                $activeantiviruses[$key] = $activeantiviruses[$key + 1];
+                $activeantiviruses[$key + 1] = $fsave;
+            }
+        }
+        break;
+
+    case 'up':
+        $key = array_search($antivirus, $activeantiviruses);
+        // Check auth is valid.
+        if ($key !== false) {
+            // Move up the list.
+            if ($key >= 1) {
+                $fsave = $activeantiviruses[$key];
+                $activeantiviruses[$key] = $activeantiviruses[$key - 1];
+                $activeantiviruses[$key - 1] = $fsave;
+            }
+        }
+        break;
+
+    default:
+        break;
+}
+
+set_config('antiviruses', implode(',', $activeantiviruses));
+core_plugin_manager::reset_caches();
+
+redirect ($returnurl);
\ No newline at end of file
index 1e3322b..1bea1b2 100644 (file)
@@ -138,6 +138,16 @@ if ($hassiteconfig) {
         $plugin->load_settings($ADMIN, 'editorsettings', $hassiteconfig);
     }
 
+    // Antivirus plugins.
+    $ADMIN->add('modules', new admin_category('antivirussettings', new lang_string('antiviruses', 'antivirus')));
+    $temp = new admin_settingpage('manageantiviruses', new lang_string('antivirussettings', 'antivirus'));
+    $temp->add(new admin_setting_manageantiviruses());
+    $ADMIN->add('antivirussettings', $temp);
+    foreach (core_plugin_manager::instance()->get_plugins_of_type('antivirus') as $plugin) {
+        /* @var \core\plugininfo\antivirus $plugin */
+        $plugin->load_settings($ADMIN, 'antivirussettings', $hassiteconfig);
+    }
+
 /// License types
     $ADMIN->add('modules', new admin_category('licensesettings', new lang_string('licenses')));
     $temp = new admin_settingpage('managelicenses', new lang_string('managelicenses', 'admin'));
index 8703d35..00c9429 100644 (file)
@@ -127,19 +127,4 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
     }
     $temp->add(new admin_setting_configselect('notifyloginthreshold', new lang_string('notifyloginthreshold', 'admin'), new lang_string('confignotifyloginthreshold', 'admin'), '10', $options));
     $ADMIN->add('security', $temp);
-
-
-
-
-
-
-    // "antivirus" settingpage
-    $temp = new admin_settingpage('antivirus', new lang_string('antivirus', 'admin'));
-    $temp->add(new admin_setting_configcheckbox('runclamonupload', new lang_string('runclamavonupload', 'admin'), new lang_string('configrunclamavonupload', 'admin'), 0));
-    $temp->add(new admin_setting_configexecutable('pathtoclam', new lang_string('pathtoclam', 'admin'), new lang_string('configpathtoclam', 'admin'), ''));
-    $temp->add(new admin_setting_configdirectory('quarantinedir', new lang_string('quarantinedir', 'admin'), new lang_string('configquarantinedir', 'admin'), ''));
-    $temp->add(new admin_setting_configselect('clamfailureonupload', new lang_string('clamfailureonupload', 'admin'), new lang_string('configclamfailureonupload', 'admin'), 'donothing', array('donothing' => new lang_string('configclamdonothing', 'admin'),
-                                                                                                                                                                                      'actlikevirus' => new lang_string('configclamactlikevirus', 'admin'))));
-    $ADMIN->add('security', $temp);
-
 } // end of speedup
index 46ae5d9..8364e90 100644 (file)
@@ -622,9 +622,6 @@ class manager {
     private function process_message_part_attachment($messagedata, $partdata, $part, $filename) {
         global $CFG;
 
-        // For Antivirus, the repository/lib.php must be included as it is not autoloaded.
-        require_once($CFG->dirroot . '/repository/lib.php');
-
         // If a filename is present, assume that this part is an attachment.
         $attachment = new \stdClass();
         $attachment->filename       = $filename;
@@ -635,7 +632,7 @@ class manager {
         $attachment->contentid      = $partdata->getContentId();
         $attachment->filesize       = $messagedata->getBodyPartSize($part);
 
-        if (empty($CFG->runclamonupload) or empty($CFG->pathtoclam)) {
+        if (!empty($CFG->antiviruses)) {
             mtrace("--> Attempting virus scan of '{$attachment->filename}'");
 
             // Store the file on disk - it will need to be virus scanned first.
@@ -655,8 +652,8 @@ class manager {
 
             // Perform a virus scan now.
             try {
-                \repository::antivir_scan_file($filepath, $attachment->filename, true);
-            } catch (\moodle_exception $e) {
+                \core\antivirus\manager::scan_file($filepath, $attachment->filename, true);
+            } catch (\core\antivirus\scanner_exception $e) {
                 mtrace("--> A virus was found in the attachment '{$attachment->filename}'.");
                 $this->inform_attachment_virus();
                 return;
index 469884d..8d8d481 100644 (file)
@@ -807,11 +807,6 @@ $CFG->admin = 'admin';
 // and 'gsdll32.dll' to a new folder without a space in the path)
 //      $CFG->pathtogs = '/usr/bin/gs';
 //
-// Clam AV path.
-// Probably something like /usr/bin/clamscan or /usr/bin/clamdscan. You need
-// this in order for clam AV to run.
-//      $CFG->pathtoclam = '';
-//
 // Path to du.
 // Probably something like /usr/bin/du. If you enter this, pages that display
 // directory contents will run much faster for directories with a lot of files.
index 322c08e..a38048b 100644 (file)
@@ -57,7 +57,6 @@ $string['allowuserswitchrolestheycantassign'] = 'Allow users without the assign
 $string['allowuserthemes'] = 'Allow user themes';
 $string['alternativefullnameformat'] = 'Alternative full name format';
 $string['alternativefullnameformat_desc'] = 'This defines how names are shown to users with the viewfullnames capability (by default users with the role of manager, teacher or non-editing teacher). Placeholders that can be used are as for the "Full name format" setting.';
-$string['antivirus'] = 'Anti-Virus';
 $string['appearance'] = 'Appearance';
 $string['aspellpath'] = 'Path to aspell';
 $string['authentication'] = 'Authentication';
@@ -103,7 +102,6 @@ $string['cannotdeletemodfilter'] = 'You cannot uninstall the \'{$a->filter}\' be
 $string['cannotuninstall'] = '{$a} can not be uninstalled.';
 $string['cfgwwwrootslashwarning'] = 'You have defined $CFG->wwwroot incorrectly in your config.php file. You have included a \'/\' character at the end. Please remove it, or you will experience strange bugs like <a href=\'http://tracker.moodle.org/browse/MDL-11061\'>MDL-11061</a>.';
 $string['cfgwwwrootwarning'] = 'You have defined $CFG->wwwroot incorrectly in your config.php file. It does not match the URL you are using to access this page. Please correct it, or you will experience strange bugs like <a href=\'http://tracker.moodle.org/browse/MDL-11061\'>MDL-11061</a>.';
-$string['clamfailureonupload'] = 'On clam AV failure';
 $string['cleanup'] = 'Cleanup';
 $string['clianswerno'] = 'n';
 $string['cliansweryes'] = 'y';
@@ -151,9 +149,6 @@ $string['configautologinguests'] = 'Should visitors be logged in as guests autom
 $string['configbloglevel'] = 'This setting allows you to restrict the level to which user blogs can be viewed on this site.  Note that they specify the maximum context of the VIEWER not the poster or the types of blog posts.  Blogs can also be disabled completely if you don\'t want them at all.';
 $string['configcalendarcustomexport'] = 'Enable custom date range export of calendar';
 $string['configcalendarexportsalt'] = 'This random text is used for improving of security of authentication tokens used for exporting of calendars. Please note that all current tokens are invalidated if you change this hash salt.';
-$string['configclamactlikevirus'] = 'Treat files like viruses';
-$string['configclamdonothing'] = 'Treat files as OK';
-$string['configclamfailureonupload'] = 'If you have configured clam to scan uploaded files, but it is configured incorrectly or fails to run for some unknown reason, how should it behave?  If you choose \'Treat files like viruses\', they\'ll be moved into the quarantine area, or deleted. If you choose \'Treat files as OK\', the files will be moved to the destination directory like normal. Either way, admins will be alerted that clam has failed.  If you choose \'Treat files like viruses\' and for some reason clam fails to run (usually because you have entered an invalid pathtoclam), ALL files that are uploaded will be moved to the given quarantine area, or deleted. Be careful with this setting.';
 $string['configcookiehttponly'] = 'Enables new PHP 5.2.0 feature - browsers are instructed to send cookie with real http requests only, cookies should not be accessible by scripting languages. This is not supported in all browsers and it may not be fully compatible with current code. It helps to prevent some types of XSS attacks.';
 $string['configcookiesecure'] = 'If server is accepting only https connections it is recommended to enable sending of secure cookies. If enabled please make sure that web server is not accepting http:// or set up permanent redirection to https:// address. When <em>wwwroot</em> address does not start with https:// this setting is turned off automatically.';
 $string['configcountry'] = 'If you set a country here, then this country will be selected by default on new user accounts.  To force users to choose a country, just leave this unset.';
@@ -282,7 +277,6 @@ $string['configopentogoogle'] = 'If you enable this setting, then Google will be
 $string['configoverride'] = 'Defined in config.php';
 $string['configpasswordpolicy'] = 'Turning this on will make Moodle check user passwords against a valid password policy. Use the settings below to specify your policy (they will be ignored if you set this to \'No\').';
 $string['configpasswordresettime'] = 'This specifies the amount of time people have to validate a password reset request before it expires. Usually 30 minutes is a good value.';
-$string['configpathtoclam'] = 'Path to clam AV.  Probably something like /usr/bin/clamscan or /usr/bin/clamdscan. You need this in order for clam AV to run.';
 $string['configpathtodu'] = 'Path to du. Probably something like /usr/bin/du. If you enter this, pages that display directory contents will run much faster for directories with a lot of files.';
 $string['configperfdebug'] = 'If you turn this on, performance info will be printed in the footer of the standard theme';
 $string['configprofileroles'] = 'List of roles that are visible on user profiles and participation page.';
@@ -294,7 +288,6 @@ $string['configproxypassword'] = 'Password needed to access internet through pro
 $string['configproxyport'] = 'If this server needs to use a proxy computer, then provide the proxy port here.';
 $string['configproxytype'] = 'Type of web proxy (PHP5 and cURL extension required for SOCKS5 support).';
 $string['configproxyuser'] = 'Username needed to access internet through proxy if required, empty if none (PHP cURL extension required).';
-$string['configquarantinedir'] = 'If you want clam AV to move infected files to a quarantine directory, enter it here. It must be writable by the webserver.  If you leave this blank, or if you enter a directory that doesn\'t exist or isn\'t writable, infected files will be deleted.  Do not include a trailing slash.';
 $string['configrecaptchaprivatekey'] = 'String of characters used to communicate between your Moodle server and the recaptcha server. Obtain one for this site by visiting http://www.google.com/recaptcha';
 $string['configrecaptchapublickey'] = 'String of characters used to display the reCAPTCHA element in the signup form. Generated by http://www.google.com/recaptcha';
 $string['configrequestcategoryselection'] = 'Allow the selection of a category when requesting a course.';
@@ -302,8 +295,6 @@ $string['configrequestedstudentname'] = 'Word for student used in requested cour
 $string['configrequestedstudentsname'] = 'Word for students used in requested courses';
 $string['configrequestedteachername'] = 'Word for teacher used in requested courses';
 $string['configrequestedteachersname'] = 'Word for teachers used in requested courses';
-$string['configrunclamavonupload'] = 'When enabled, clam AV will be used to scan all uploaded files.';
-$string['configrunclamonupload'] = 'Run clam AV on file upload? You will need a correct path in pathtoclam for this to work.  (Clam AV is a free virus scanner that you can get from http://www.clamav.net/)';
 $string['configuserquota'] = 'The maximum number of bytes that a user can store in their own private file area. {$a->bytes} bytes == {$a->displaysize}';
 $string['configsectioninterface'] = 'Interface';
 $string['configsectionmail'] = 'Mail';
@@ -788,7 +779,6 @@ $string['passwordpolicy'] = 'Password policy';
 $string['passwordresettime'] = 'Maximum time to validate password reset request';
 $string['passwordreuselimit'] = 'Password rotation limit';
 $string['passwordreuselimit_desc'] = 'Number of times a user must change their password before they are allowed to reuse a password. Hashes of previously used passwords are stored in local database table. This feature might not be compatible with some external authentication plugins.';
-$string['pathtoclam'] = 'clam AV path';
 $string['pathtodot'] = 'Path to dot';
 $string['pathtodot_help'] = 'Path to dot. Probably something like /usr/bin/dot. To be able to generate graphics from DOT files, you must have installed the dot executable and point to it here. Note that, for now, this only used by the profiling features (Development->Profiling) built into Moodle.';
 $string['pathtodu'] = 'Path to du';
@@ -892,7 +882,6 @@ $string['proxypassword'] = 'Proxy password';
 $string['proxyport'] = 'Proxy port';
 $string['proxytype'] = 'Proxy type';
 $string['proxyuser'] = 'Proxy username';
-$string['quarantinedir'] = 'Quarantine directory';
 $string['question'] = 'Question';
 $string['questionbehaviours'] = 'Question behaviours';
 $string['questioncwqpfscheck'] = 'One or more \'random\' questions in a quiz are set up to select questions from a mixture of shared and unshared question categories. There is a more detailed report <a href="{$a->reporturl}">here</a> and see Moodle Docs page <a href="{$a->docsurl}">here</a>.';
@@ -941,7 +930,6 @@ $string['riskxss'] = 'Users could add files and texts that allow cross-site scri
 $string['riskxssshort'] = 'XSS risk';
 $string['roleswithexceptions'] = '{$a->roles}, with {$a->exceptions}';
 $string['rssglobaldisabled'] = 'Disabled at server level';
-$string['runclamavonupload'] = 'Use clam AV on uploaded files';
 $string['save'] = 'Save';
 $string['savechanges'] = 'Save changes';
 $string['search'] = 'Search';
diff --git a/lang/en/antivirus.php b/lang/en/antivirus.php
new file mode 100644 (file)
index 0000000..27a0c79
--- /dev/null
@@ -0,0 +1,30 @@
+<?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/>.
+
+/**
+ * Strings for component 'antivirus', language 'en'
+ *
+ * @package   core_antivirus
+ * @copyright 2015 Ruslan Kabalin, Lancaster University.
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['actantivirushdr'] = 'Available antivirus plugins';
+$string['antiviruses'] = 'Antivirus plugins';
+$string['antivirussettings'] = 'Manage antivirus plugins';
+$string['configantivirusplugins'] = 'Please choose the antivirus plugins you wish to use and arrange them in order of being applied.';
+$string['emailsubject'] = '{$a} :: Antivirus notification';
+$string['virusfounduser'] = 'The file you have uploaded, {$a->filename}, has been scanned by a virus checker and found to be infected! Your file upload was NOT successful.';
index 2b5ecd9..1675768 100644 (file)
@@ -237,17 +237,6 @@ $string['idnumbercoursecategory_help'] = 'The ID number of a course category  is
 $string['categoryupdated'] = 'The category \'{$a}\' was updated';
 $string['changesmadereallygoaway'] = 'You have made changes. Are you sure you want to navigate away and lose your changes?';
 $string['city'] = 'City/town';
-$string['clambroken'] = 'Your administrator has enabled virus checking for file uploads but has misconfigured something.<br />Your file upload was NOT successful. Your administrator has been emailed to notify them so they can fix it.<br />Maybe try uploading this file later.';
-$string['clamdeletedfile'] = 'The file has been deleted';
-$string['clamdeletedfilefailed'] = 'The file could not be deleted';
-$string['clamemailsubject'] = '{$a} :: Clam AV notification';
-$string['clamfailed'] = 'Clam AV has failed to run.  The return error message was {$a}. Here is the output from Clam:';
-$string['clamlost'] = 'Moodle is configured to run clam on file upload, but the path supplied to Clam AV, {$a},  is invalid.';
-$string['clamlostandactinglikevirus'] = 'In addition, Moodle is configured so that if clam fails to run, files are treated like viruses.  This essentially means that no student can upload a file successfully until you fix this.';
-$string['clammovedfile'] = 'The file has been moved to your specified quarantine directory, the new location is {$a}';
-$string['clammovedfilebasic'] = 'The file has been moved to a quarantine directory.';
-$string['clamquarantinedirfailed'] = 'Could not move the file into your specified quarantine directory, {$a}. You need to fix this as files are being deleted if they\'re found to be infected.';
-$string['clamunknownerror'] = 'There was an unknown error with clam.';
 $string['cleaningtempdata'] = 'Cleaning temp data';
 $string['clear'] = 'Clear';
 $string['clickhelpiconformoreinfo'] = '... continues ... Click on the help icon to read the full article';
@@ -1971,25 +1960,6 @@ $string['viewfileinpopup'] = 'View file in a popup window';
 $string['viewprofile'] = 'View profile';
 $string['views'] = 'Views';
 $string['viewsolution'] = 'view solution';
-$string['virusfound'] = 'Attention administrator! Clam AV has found a virus in a file uploaded by {$a->user} for the course {$a->course}. Here is the output of clamscan:';
-$string['virusfoundlater'] = 'A file you uploaded on {$a->date} with the filename {$a->filename} for the course {$a->course} has since been found to contain a virus.  Here is a summary of what has happened to your file:
-
-{$a->action}
-
-If this was submitted work, you may want to resubmit it so that your tutor can see it.';
-$string['virusfoundlateradmin'] = 'Attention administrator! A file that was uploaded on {$a->date} with the filename {$a->filename} for the course {$a->course} by the user {$a->user} has since been found to contain a virus.  Here is a summary of what has happened to the file:
-
-{$a->action}
-
-The user has also been notified.';
-$string['virusfoundlateradminnolog'] = 'Attention administrator! A file that was uploaded with the filename {$a->filename} has since been found to contain a virus. Moodle was unable to resolve this file back to the user that originally uploaded it.
-
-Here is a summary of what has happened to the file:
-
-{$a->action}';
-$string['virusfoundsubject'] = '{$a}: Virus found!';
-$string['virusfounduser'] = 'The file you have uploaded, {$a->filename}, has been scanned by a virus checker and found to be infected! Your file upload was NOT successful.';
-$string['virusplaceholder'] = 'This file that has been uploaded was found to contain a virus and has been moved or deleted and the user notified.';
 $string['visible'] = 'Visible';
 $string['visible_help'] = 'This setting determines whether the course appears in the list of courses. Apart from teachers and administrators, users are not allowed to enter the course.';
 $string['visibletostudents'] = 'Visible to {$a}';
index eedbcea..5deb194 100644 (file)
@@ -6494,6 +6494,186 @@ class admin_setting_manageeditors extends admin_setting {
     }
 }
 
+/**
+ * Special class for antiviruses administration.
+ *
+ * @copyright  2015 Ruslan Kabalin, Lancaster University.
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class admin_setting_manageantiviruses extends admin_setting {
+    /**
+     * Calls parent::__construct with specific arguments
+     */
+    public function __construct() {
+        $this->nosave = true;
+        parent::__construct('antivirusesui', get_string('antivirussettings', 'antivirus'), '', '');
+    }
+
+    /**
+     * Always returns true, does nothing
+     *
+     * @return true
+     */
+    public function get_setting() {
+        return true;
+    }
+
+    /**
+     * Always returns true, does nothing
+     *
+     * @return true
+     */
+    public function get_defaultsetting() {
+        return true;
+    }
+
+    /**
+     * Always returns '', does not write anything
+     *
+     * @param string $data Unused
+     * @return string Always returns ''
+     */
+    public function write_setting($data) {
+        // Do not write any setting.
+        return '';
+    }
+
+    /**
+     * Checks if $query is one of the available editors
+     *
+     * @param string $query The string to search for
+     * @return bool Returns true if found, false if not
+     */
+    public function is_related($query) {
+        if (parent::is_related($query)) {
+            return true;
+        }
+
+        $antivirusesavailable = \core\antivirus\manager::get_available();
+        foreach ($antivirusesavailable as $antivirus => $antivirusstr) {
+            if (strpos($antivirus, $query) !== false) {
+                return true;
+            }
+            if (strpos(core_text::strtolower($antivirusstr), $query) !== false) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Builds the XHTML to display the control
+     *
+     * @param string $data Unused
+     * @param string $query
+     * @return string
+     */
+    public function output_html($data, $query='') {
+        global $CFG, $OUTPUT;
+
+        // Display strings.
+        $txt = get_strings(array('administration', 'settings', 'edit', 'name', 'enable', 'disable',
+            'up', 'down', 'none'));
+        $struninstall = get_string('uninstallplugin', 'core_admin');
+
+        $txt->updown = "$txt->up/$txt->down";
+
+        $antivirusesavailable = \core\antivirus\manager::get_available();
+        $activeantiviruses = explode(',', $CFG->antiviruses);
+
+        $activeantiviruses = array_reverse($activeantiviruses);
+        foreach ($activeantiviruses as $key => $antivirus) {
+            if (empty($antivirusesavailable[$antivirus])) {
+                unset($activeantiviruses[$key]);
+            } else {
+                $name = $antivirusesavailable[$antivirus];
+                unset($antivirusesavailable[$antivirus]);
+                $antivirusesavailable[$antivirus] = $name;
+            }
+        }
+        $antivirusesavailable = array_reverse($antivirusesavailable, true);
+        $return = $OUTPUT->heading(get_string('actantivirushdr', 'antivirus'), 3, 'main', true);
+        $return .= $OUTPUT->box_start('generalbox antivirusesui');
+
+        $table = new html_table();
+        $table->head  = array($txt->name, $txt->enable, $txt->updown, $txt->settings, $struninstall);
+        $table->colclasses = array('leftalign', 'centeralign', 'centeralign', 'centeralign', 'centeralign');
+        $table->id = 'antivirusmanagement';
+        $table->attributes['class'] = 'admintable generaltable';
+        $table->data  = array();
+
+        // Iterate through auth plugins and add to the display table.
+        $updowncount = 1;
+        $antiviruscount = count($activeantiviruses);
+        $baseurl = new moodle_url('/admin/antiviruses.php', array('sesskey' => sesskey()));
+        foreach ($antivirusesavailable as $antivirus => $name) {
+            // Hide/show link.
+            $class = '';
+            if (in_array($antivirus, $activeantiviruses)) {
+                $hideshowurl = $baseurl;
+                $hideshowurl->params(array('action' => 'disable', 'antivirus' => $antivirus));
+                $hideshowimg = html_writer::img($OUTPUT->pix_url('t/hide'), 'disable', array('class' => 'iconsmall'));
+                $hideshow = html_writer::link($hideshowurl, $hideshowimg);
+                $enabled = true;
+                $displayname = $name;
+            } else {
+                $hideshowurl = $baseurl;
+                $hideshowurl->params(array('action' => 'enable', 'antivirus' => $antivirus));
+                $hideshowimg = html_writer::img($OUTPUT->pix_url('t/show'), 'enable', array('class' => 'iconsmall'));
+                $hideshow = html_writer::link($hideshowurl, $hideshowimg);
+                $enabled = false;
+                $displayname = $name;
+                $class = 'dimmed_text';
+            }
+
+            // Up/down link.
+            $updown = '';
+            if ($enabled) {
+                if ($updowncount > 1) {
+                    $updownurl = $baseurl;
+                    $updownurl->params(array('action' => 'up', 'antivirus' => $antivirus));
+                    $updownimg = html_writer::img($OUTPUT->pix_url('t/up'), 'up', array('class' => 'iconsmall'));
+                    $updown = html_writer::link($updownurl, $updownimg);
+                } else {
+                    $updown .= html_writer::img($OUTPUT->pix_url('spacer'), '', array('class' => 'iconsmall'));
+                }
+                if ($updowncount < $antiviruscount) {
+                    $updownurl = $baseurl;
+                    $updownurl->params(array('action' => 'down', 'antivirus' => $antivirus));
+                    $updownimg = html_writer::img($OUTPUT->pix_url('t/down'), 'down', array('class' => 'iconsmall'));
+                    $updown = html_writer::link($updownurl, $updownimg);
+                } else {
+                    $updown .= html_writer::img($OUTPUT->pix_url('spacer'), '', array('class' => 'iconsmall'));
+                }
+                ++ $updowncount;
+            }
+
+            // Settings link.
+            if (file_exists($CFG->dirroot.'/lib/antivirus/'.$antivirus.'/settings.php')) {
+                $eurl = new moodle_url('/admin/settings.php', array('section' => 'antivirussettings'.$antivirus));
+                $settings = html_writer::link($eurl, $txt->settings);
+            } else {
+                $settings = '';
+            }
+
+            $uninstall = '';
+            if ($uninstallurl = core_plugin_manager::instance()->get_uninstall_url('antivirus_'.$antivirus, 'manage')) {
+                $uninstall = html_writer::link($uninstallurl, $struninstall);
+            }
+
+            // Add a row to the table.
+            $row = new html_table_row(array($displayname, $hideshow, $updown, $settings, $uninstall));
+            if ($class) {
+                $row->attributes['class'] = $class;
+            }
+            $table->data[] = $row;
+        }
+        $return .= html_writer::table($table);
+        $return .= get_string('configantivirusplugins', 'antivirus') . html_writer::empty_tag('br') . get_string('tablenosave', 'admin');
+        $return .= $OUTPUT->box_end();
+        return highlight($query, $return);
+    }
+}
 
 /**
  * Special class for license administration.
diff --git a/lib/antivirus/clamav/classes/scanner.php b/lib/antivirus/clamav/classes/scanner.php
new file mode 100644 (file)
index 0000000..e8e58e3
--- /dev/null
@@ -0,0 +1,156 @@
+<?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/>.
+
+/**
+ * ClamAV antivirus integration.
+ *
+ * @package    antivirus_clamav
+ * @copyright  2015 Ruslan Kabalin, Lancaster University.
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace antivirus_clamav;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class implemeting ClamAV antivirus.
+ * @copyright  2015 Ruslan Kabalin, Lancaster University.
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class scanner extends \core\antivirus\scanner {
+    /**
+     * Are the necessary antivirus settings configured?
+     *
+     * @return bool True if all necessary config settings been entered
+     */
+    public function is_configured() {
+        return (bool)$this->get_config('pathtoclam');
+    }
+    /**
+     * Scan file, 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 moodle_exception If file is infected.
+     * @return void
+     */
+    public function scan_file($file, $filename, $deleteinfected) {
+        global $CFG;
+
+        if (!is_readable($file)) {
+            // This should not happen.
+            debugging('File is not readable.');
+            return;
+        }
+
+        // Execute the scan using preferable method.
+        list($return, $notice) = $this->scan_file_execute_commandline($file);
+
+        if ($return == 0) {
+            // Perfect, no problem found, file is clean.
+            return;
+        } else if ($return == 1) {
+            // Infection found.
+            if ($deleteinfected) {
+                unlink($file);
+            }
+            throw new \core\antivirus\scanner_exception('virusfounduser', '', array('filename' => $filename));
+        } else {
+            // Unknown problem.
+            $this->message_admins($notice);
+            if ($this->get_config('clamfailureonupload') === 'actlikevirus') {
+                if ($deleteinfected) {
+                    unlink($file);
+                }
+                throw new \core\antivirus\scanner_exception('virusfounduser', '', array('filename' => $filename));
+            } else {
+                return;
+            }
+        }
+    }
+
+    /**
+     * 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
+     */
+    private 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 (isset($returncodes[$returncode])) {
+            return $returncodes[$returncode];
+        }
+        return get_string('unknownerror', 'antivirus_clamav');
+    }
+
+    /**
+     * Scan file using command line utility.
+     *
+     * @param string $file Full path to the file.
+     * @return array ($return, $notice) Execution return code and notification text.
+     */
+    public function scan_file_execute_commandline($file) {
+        $pathtoclam = trim($this->get_config('pathtoclam'));
+
+        if (!file_exists($pathtoclam) or !is_executable($pathtoclam)) {
+            // Misconfigured clam, notify admins.
+            $notice = get_string('invalidpathtoclam', 'antivirus_clamav', $pathtoclam);
+            return array(-1, $notice);
+        }
+
+        $clamparam = ' --stdout ';
+        // If we are dealing with clamdscan, clamd is likely run as a different user
+        // that might not have permissions to access your file.
+        // To make clamdscan work, we use --fdpass parameter that passes the file
+        // descriptor permissions to clamd, which allows it to scan given file
+        // irrespective of directory and file permissions.
+        if (basename($pathtoclam) == 'clamdscan') {
+            $clamparam .= '--fdpass ';
+        }
+        // Execute scan.
+        $cmd = escapeshellcmd($pathtoclam).$clamparam.escapeshellarg($file);
+        exec($cmd, $output, $return);
+        $notice = '';
+        if ($return > 1) {
+            $notice = get_string('clamfailed', 'antivirus_clamav', $this->get_clam_error_code($return));
+            $notice .= "\n\n". implode("\n", $output);
+        }
+
+        return array($return, $notice);
+    }
+}
diff --git a/lib/antivirus/clamav/db/upgrade.php b/lib/antivirus/clamav/db/upgrade.php
new file mode 100644 (file)
index 0000000..618657e
--- /dev/null
@@ -0,0 +1,35 @@
+<?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/>.
+
+/**
+ * ClamAV antivirus plugin upgrade script.
+ *
+ * @package    antivirus_clamav
+ * @copyright  2015 Ruslan Kabalin, Lancaster University.
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Run all ClamAV plugin upgrade steps between the current DB version and the current version on disk.
+ *
+ * @param int $oldversion The old version of atto in the DB.
+ * @return bool
+ */
+function xmldb_antivirus_clamav_upgrade($oldversion) {
+    return true;
+}
diff --git a/lib/antivirus/clamav/lang/en/antivirus_clamav.php b/lib/antivirus/clamav/lang/en/antivirus_clamav.php
new file mode 100644 (file)
index 0000000..d83a608
--- /dev/null
@@ -0,0 +1,36 @@
+<?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/>.
+
+/**
+ * Strings for component 'antivirus_clamav', language 'en'.
+ *
+ * @package    antivirus_clamav
+ * @copyright  2015 Ruslan Kabalin, Lancaster University.
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['configclamactlikevirus'] = 'Treat files like viruses';
+$string['configclamdonothing'] = 'Treat files as OK';
+$string['configclamfailureonupload'] = 'If you have configured clam to scan uploaded files, but it is configured incorrectly or fails to run for some unknown reason, how should it behave?  If you choose \'Treat files like viruses\', they\'ll be moved into the quarantine area, or deleted. If you choose \'Treat files as OK\', the files will be moved to the destination directory like normal. Either way, admins will be alerted that clam has failed.  If you choose \'Treat files like viruses\' and for some reason clam fails to run (usually because you have entered an invalid pathtoclam), ALL files that are uploaded will be moved to the given quarantine area, or deleted. Be careful with this setting.';
+$string['configpathtoclam'] = 'Path to ClamAV.  Probably something like /usr/bin/clamscan or /usr/bin/clamdscan. You need this in order for ClamAV to run.';
+$string['configquarantinedir'] = 'If you want ClamAV to move infected files to a quarantine directory, enter it here. It must be writable by the webserver.  If you leave this blank, or if you enter a directory that doesn\'t exist or isn\'t writable, infected files will be deleted.  Do not include a trailing slash.';
+$string['clamfailed'] = 'ClamAV has failed to run.  The return error message was "{$a}". Here is the output from ClamAV:';
+$string['clamfailureonupload'] = 'On ClamAV failure';
+$string['invalidpathtoclam'] = 'Path to ClamAV, {$a}, is invalid.';
+$string['pathtoclam'] = 'ClamAV path';
+$string['pluginname'] = 'ClamAV antivirus';
+$string['quarantinedir'] = 'Quarantine directory';
+$string['unknownerror'] = 'There was an unknown error with ClamAV.';
diff --git a/lib/antivirus/clamav/settings.php b/lib/antivirus/clamav/settings.php
new file mode 100644 (file)
index 0000000..6e41ec7
--- /dev/null
@@ -0,0 +1,39 @@
+<?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/>.
+
+/**
+ * ClamAV admin settings.
+ *
+ * @package    antivirus_clamav
+ * @copyright  2015 Ruslan Kabalin, Lancaster University.
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+if ($ADMIN->fulltree) {
+    $settings->add(new admin_setting_configexecutable('antivirus_clamav/pathtoclam',
+            new lang_string('pathtoclam', 'antivirus_clamav'), new lang_string('configpathtoclam', 'antivirus_clamav'), ''));
+    $settings->add(new admin_setting_configdirectory('antivirus_clamav/quarantinedir',
+            new lang_string('quarantinedir', 'antivirus_clamav'), new lang_string('configquarantinedir', 'antivirus_clamav'), ''));
+    $options = array(
+        'donothing' => new lang_string('configclamdonothing', 'antivirus_clamav'),
+        'actlikevirus' => new lang_string('configclamactlikevirus', 'antivirus_clamav'),
+    );
+    $settings->add(new admin_setting_configselect('antivirus_clamav/clamfailureonupload',
+            new lang_string('clamfailureonupload', 'antivirus_clamav'),
+            new lang_string('configclamfailureonupload', 'antivirus_clamav'), 'donothing', $options));
+}
\ No newline at end of file
diff --git a/lib/antivirus/clamav/tests/scanner_test.php b/lib/antivirus/clamav/tests/scanner_test.php
new file mode 100644 (file)
index 0000000..0a81d42
--- /dev/null
@@ -0,0 +1,174 @@
+<?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/>.
+
+/**
+ * Tests for ClamAV antivirus scanner class.
+ *
+ * @package    antivirus_clamav
+ * @category   phpunit
+ * @copyright  2016 Ruslan Kabalin, Lancaster University.
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+class antivirus_clamav_scanner_testcase extends advanced_testcase {
+    protected $tempfile;
+
+    protected function setUp() {
+        $this->resetAfterTest();
+
+        // Create tempfile.
+        $tempfolder = make_request_directory(false);
+        $this->tempfile = $tempfolder . '/' . rand();
+        touch($this->tempfile);
+    }
+
+    protected function tearDown() {
+        @unlink($this->tempfile);
+    }
+
+    public function test_scan_file_not_exists() {
+        $antivirus = $this->getMockBuilder('\antivirus_clamav\scanner')
+                ->setMethods(array('scan_file_execute_commandline', 'message_admins'))
+                ->getMock();
+
+        // Test specifying file that does not exist.
+        $nonexistingfile = $this->tempfile . '_';
+        $this->assertFileNotExists($nonexistingfile);
+        // Run mock scanning with deleting infected file.
+        $antivirus->scan_file($nonexistingfile, '', true);
+        $this->assertDebuggingCalled();
+    }
+
+    public function test_scan_file_no_virus() {
+        $antivirus = $this->getMockBuilder('\antivirus_clamav\scanner')
+                ->setMethods(array('scan_file_execute_commandline', 'message_admins'))
+                ->getMock();
+
+        // Configure scan_file_execute_commandline method stub to behave
+        // as if no virus has been found.
+        $antivirus->method('scan_file_execute_commandline')->willReturn(array(0, ''));
+
+        // Set expectation that message_admins is NOT called.
+        $antivirus->expects($this->never())->method('message_admins');
+
+        // Run mock scanning with deleting infected file.
+        $this->assertFileExists($this->tempfile);
+        try {
+            $antivirus->scan_file($this->tempfile, '', true);
+        } catch (\core\antivirus\scanner_exception $e) {
+            $this->fail('Exception scanner_exception is not expected in clean file scanning.');
+        }
+        // File expected to remain in place.
+        $this->assertFileExists($this->tempfile);
+    }
+
+    public function test_scan_file_virus() {
+        $antivirus = $this->getMockBuilder('\antivirus_clamav\scanner')
+                ->setMethods(array('scan_file_execute_commandline', 'message_admins'))
+                ->getMock();
+
+        // Configure scan_file_execute_commandline method stub to behave
+        // as if virus has been found.
+        $antivirus->method('scan_file_execute_commandline')->willReturn(array(1, ''));
+
+        // Set expectation that message_admins is NOT called.
+        $antivirus->expects($this->never())->method('message_admins');
+
+        // Run mock scanning without deleting infected file.
+        $this->assertFileExists($this->tempfile);
+        try {
+            $antivirus->scan_file($this->tempfile, '', false);
+        } catch (\moodle_exception $e) {
+            $this->assertInstanceOf('\core\antivirus\scanner_exception', $e);
+        }
+        // File expected to remain in place.
+        $this->assertFileExists($this->tempfile);
+
+        // Run mock scanning with deleting infected file.
+        try {
+            $antivirus->scan_file($this->tempfile, '', true);
+        } catch (\moodle_exception $e) {
+            $this->assertInstanceOf('\core\antivirus\scanner_exception', $e);
+        }
+        // File expected to be deleted.
+        $this->assertFileNotExists($this->tempfile);
+    }
+
+    public function test_scan_file_error_donothing() {
+        $antivirus = $this->getMockBuilder('\antivirus_clamav\scanner')
+                ->setMethods(array('scan_file_execute_commandline', 'message_admins', 'get_config'))
+                ->getMock();
+
+        // Configure scan_file_execute_commandline method stub to behave
+        // as if there is a scanning error.
+        $antivirus->method('scan_file_execute_commandline')->willReturn(array(2, 'someerror'));
+
+        // Set expectation that message_admins is called.
+        $antivirus->expects($this->atLeastOnce())->method('message_admins')->with($this->equalTo('someerror'));
+
+        // Initiate mock scanning with configuration setting to do nothing on scanning error.
+        $configmap = array(array('clamfailureonupload', 'donothing'));
+        $antivirus->method('get_config')->will($this->returnValueMap($configmap));
+
+        // Run mock scanning with deleting infected file.
+        $this->assertFileExists($this->tempfile);
+        try {
+            $antivirus->scan_file($this->tempfile, '', true);
+        } catch (\core\antivirus\scanner_exception $e) {
+            $this->fail('Exception scanner_exception is not expected with config setting to do nothing on error.');
+        }
+        // File expected to remain in place.
+        $this->assertFileExists($this->tempfile);
+    }
+
+    public function test_scan_file_error_actlikevirus() {
+        $antivirus = $this->getMockBuilder('\antivirus_clamav\scanner')
+                ->setMethods(array('scan_file_execute_commandline', 'message_admins', 'get_config'))
+                ->getMock();
+
+        // Configure scan_file_execute_commandline method stub to behave
+        // as if there is a scanning error.
+        $antivirus->method('scan_file_execute_commandline')->willReturn(array(2, 'someerror'));
+
+        // Set expectation that message_admins is called.
+        $antivirus->expects($this->atLeastOnce())->method('message_admins')->with($this->equalTo('someerror'));
+
+        // Initiate mock scanning with configuration setting to act like virus on scanning error.
+        $configmap = array(array('clamfailureonupload', 'actlikevirus'));
+        $antivirus->method('get_config')->will($this->returnValueMap($configmap));
+
+        // Run mock scanning without deleting infected file.
+        $this->assertFileExists($this->tempfile);
+        try {
+            $antivirus->scan_file($this->tempfile, '', false);
+        } catch (\moodle_exception $e) {
+            $this->assertInstanceOf('\core\antivirus\scanner_exception', $e);
+        }
+        // File expected to remain in place.
+        $this->assertFileExists($this->tempfile);
+
+        // Run mock scanning with deleting infected file.
+        try {
+            $antivirus->scan_file($this->tempfile, '', true);
+        } catch (\moodle_exception $e) {
+            $this->assertInstanceOf('\core\antivirus\scanner_exception', $e);
+        }
+        // File expected to be deleted.
+        $this->assertFileNotExists($this->tempfile);
+    }
+}
diff --git a/lib/antivirus/clamav/version.php b/lib/antivirus/clamav/version.php
new file mode 100644 (file)
index 0000000..4be6250
--- /dev/null
@@ -0,0 +1,29 @@
+<?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/>.
+
+/**
+ * ClamAV antivirus version file.
+ *
+ * @package    antivirus_clamav
+ * @copyright  2015 Ruslan Kabalin, Lancaster University.
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->version   = 2015062500;          // The current plugin version (Date: YYYYMMDDXX).
+$plugin->requires  = 2015061800;          // Requires this Moodle version.
+$plugin->component = 'antivirus_clamav';  // Full name of the plugin (used for diagnostics).
index 3333801..1ab06c2 100644 (file)
@@ -165,7 +165,7 @@ function behat_clean_init_config() {
         'wwwroot', 'dataroot', 'dirroot', 'admin', 'directorypermissions', 'filepermissions',
         'umaskpermissions', 'dbtype', 'dblibrary', 'dbhost', 'dbname', 'dbuser', 'dbpass', 'prefix',
         'dboptions', 'proxyhost', 'proxyport', 'proxytype', 'proxyuser', 'proxypassword',
-        'proxybypass', 'theme', 'pathtogs', 'pathtoclam', 'pathtodu', 'aspellpath', 'pathtodot', 'skiplangupgrade',
+        'proxybypass', 'theme', 'pathtogs', 'pathtodu', 'aspellpath', 'pathtodot', 'skiplangupgrade',
         'altcacheconfigpath'
     ));
 
diff --git a/lib/classes/antivirus/manager.php b/lib/classes/antivirus/manager.php
new file mode 100644 (file)
index 0000000..0ea9847
--- /dev/null
@@ -0,0 +1,104 @@
+<?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) {
+        $antiviruses = self::get_enabled();
+        foreach ($antiviruses as $antivirus) {
+            $antivirus->scan_file($file, $filename, $deleteinfected);
+        }
+    }
+
+    /**
+     * 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;
+    }
+}
diff --git a/lib/classes/antivirus/scanner.php b/lib/classes/antivirus/scanner.php
new file mode 100644 (file)
index 0000000..369c48d
--- /dev/null
@@ -0,0 +1,113 @@
+<?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/>.
+
+/**
+ * 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();
+
+/**
+ * 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 {
+    /** @var stdClass the config for antivirus */
+    protected $config;
+
+    /**
+     * 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');
+    }
+
+    /**
+     * Are the antivirus settings configured?
+     *
+     * @return bool True if plugin has been configured.
+     */
+    public abstract function is_configured();
+
+    /**
+     * Scan file, 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 abstract function scan_file($file, $filename, $deleteinfected);
+
+    /**
+     * Email admins about antivirus scan outcomes.
+     *
+     * @param string $notice The body of the email to be sent.
+     * @return void
+     */
+    public function message_admins($notice) {
+
+        $site = get_site();
+
+        $subject = get_string('emailsubject', 'antivirus', format_string($site->fullname));
+        $admins = get_admins();
+        foreach ($admins as $admin) {
+            $eventdata = new \stdClass();
+            $eventdata->component         = 'moodle';
+            $eventdata->name              = 'errors';
+            $eventdata->userfrom          = get_admin();
+            $eventdata->userto            = $admin;
+            $eventdata->subject           = $subject;
+            $eventdata->fullmessage       = $notice;
+            $eventdata->fullmessageformat = FORMAT_PLAIN;
+            $eventdata->fullmessagehtml   = '';
+            $eventdata->smallmessage      = '';
+            message_send($eventdata);
+        }
+    }
+}
\ No newline at end of file
diff --git a/lib/classes/antivirus/scanner_exception.php b/lib/classes/antivirus/scanner_exception.php
new file mode 100644 (file)
index 0000000..138901c
--- /dev/null
@@ -0,0 +1,49 @@
+<?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/>.
+
+/**
+ * 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
+     */
+    public function __construct($errorcode, $link = '', $a = null, $debuginfo = null) {
+        parent::__construct($errorcode, 'antivirus', $link, $a, $debuginfo);
+    }
+}
index 4fcad15..8bd659d 100644 (file)
@@ -341,6 +341,7 @@ $cache = '.var_export($cache, true).';
         $info = array(
             'access'      => null,
             'admin'       => $CFG->dirroot.'/'.$CFG->admin,
+            'antivirus'   => $CFG->dirroot . '/lib/antivirus',
             'auth'        => $CFG->dirroot.'/auth',
             'availability' => $CFG->dirroot . '/availability',
             'backup'      => $CFG->dirroot.'/backup/util/ui',
@@ -417,6 +418,7 @@ $cache = '.var_export($cache, true).';
         global $CFG;
 
         $types = array(
+            'antivirus'     => $CFG->dirroot . '/lib/antivirus',
             'availability'  => $CFG->dirroot . '/availability/condition',
             'qtype'         => $CFG->dirroot.'/question/type',
             'mod'           => $CFG->dirroot.'/mod',
diff --git a/lib/classes/plugininfo/antivirus.php b/lib/classes/plugininfo/antivirus.php
new file mode 100644 (file)
index 0000000..1786f21
--- /dev/null
@@ -0,0 +1,138 @@
+<?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/>.
+
+/**
+ * Defines classes used for plugin info.
+ *
+ * @package    core_antivirus
+ * @copyright  2015 Ruslan Kabalin, Lancaster University.
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core\plugininfo;
+
+use moodle_url, part_of_admin_tree, admin_settingpage;
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * Class for Antiviruses
+ *
+ * @package    core_antivirus
+ * @copyright  2015 Ruslan Kabalin, Lancaster University.
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class antivirus extends base {
+    /**
+     * Finds all enabled plugins, the result may include missing plugins.
+     * @return array|null of enabled plugins $pluginname=>$pluginname, null means unknown
+     */
+    public static function get_enabled_plugins() {
+        global $CFG;
+
+        if (empty($CFG->antiviruses)) {
+            return array();
+        }
+
+        $enabled = array();
+        foreach (explode(',', $CFG->antiviruses) as $antivirus) {
+            $enabled[$antivirus] = $antivirus;
+        }
+
+        return $enabled;
+    }
+
+    /**
+     * Return the node name to use in admin settings menu for this plugin.
+     *
+     * @return string node name
+     */
+    public function get_settings_section_name() {
+        return 'antivirussettings' . $this->name;
+    }
+
+    /**
+     * Loads plugin settings to the settings tree
+     *
+     * This function usually includes settings.php file in plugins folder.
+     * Alternatively it can create a link to some settings page (instance of admin_externalpage)
+     *
+     * @param \part_of_admin_tree $adminroot
+     * @param string $parentnodename
+     * @param bool $hassiteconfig whether the current user has moodle/site:config capability
+     */
+    public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
+        global $CFG, $USER, $DB, $OUTPUT, $PAGE; // In case settings.php wants to refer to them.
+        $ADMIN = $adminroot; // May be used in settings.php.
+        $plugininfo = $this; // Also can be used inside settings.php.
+        $antivirus = $this;  // Also can be used inside settings.php.
+
+        if (!$this->is_installed_and_upgraded()) {
+            return;
+        }
+
+        if (!$hassiteconfig or !file_exists($this->full_path('settings.php'))) {
+            return;
+        }
+
+        $section = $this->get_settings_section_name();
+
+        $settings = new admin_settingpage($section, $this->displayname, 'moodle/site:config', $this->is_enabled() === false);
+        include($this->full_path('settings.php')); // This may also set $settings to null.
+
+        if ($settings) {
+            $ADMIN->add($parentnodename, $settings);
+        }
+    }
+
+    /**
+     * Clamav antivirus can not be uninstalled.
+     */
+    public function is_uninstall_allowed() {
+        if ($this->name === 'clamav') {
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    /**
+     * Return URL used for management of plugins of this type.
+     * @return moodle_url
+     */
+    public static function get_manage_url() {
+        return new moodle_url('/admin/settings.php', array('section' => 'manageantiviruses'));
+    }
+
+    /**
+     * Pre-uninstall hook.
+     */
+    public function uninstall_cleanup() {
+        global $CFG;
+
+        if (!empty($CFG->antiviruses)) {
+            $antiviruses = explode(',', $CFG->antiviruses);
+            $antiviruses = array_unique($antiviruses);
+        } else {
+            $antiviruses = array();
+        }
+        if (($key = array_search($this->name, $antiviruses)) !== false) {
+            unset($antiviruses[$key]);
+            set_config('antiviruses', implode(',', $antiviruses));
+        }
+        parent::uninstall_cleanup();
+    }
+}
index f8dbdce..f93ad10 100644 (file)
@@ -130,6 +130,7 @@ function xmldb_main_install() {
         'stringfilters'         => '', // These two are managed in a strange way by the filters
         'filterall'             => 0, // setting page, so have to be initialised here.
         'texteditors'           => 'atto,tinymce,textarea',
+        'antiviruses'           => '',
         'upgrade_minmaxgradestepignored' => 1, // New installs should not run this upgrade step.
         'upgrade_extracreditweightsstepignored' => 1, // New installs should not run this upgrade step.
         'upgrade_calculatedgradeitemsignored' => 1, // New installs should not run this upgrade step.
index 7e04aed..f88de99 100644 (file)
@@ -1416,5 +1416,40 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2016021501.00);
     }
 
+    if ($oldversion < 2016022500.01) {
+
+        // MDL-50887. Implement plugins infrastructure for antivirus and create ClamAV plugin.
+        // This routine moves core ClamAV configuration to plugin level.
+
+        // If clamav was configured and enabled, enable the plugin.
+        if (!empty($CFG->runclamonupload) && !empty($CFG->pathtoclam)) {
+            set_config('antiviruses', 'clamav');
+        } else {
+            set_config('antiviruses', '');
+        }
+
+        if (isset($CFG->runclamonupload)) {
+            // Just unset global configuration, we have already enabled the plugin
+            // which implies that ClamAV will be used for scanning uploaded files.
+            unset_config('runclamonupload');
+        }
+        // Move core ClamAV configuration settings to plugin.
+        if (isset($CFG->pathtoclam)) {
+            set_config('pathtoclam', $CFG->pathtoclam, 'antivirus_clamav');
+            unset_config('pathtoclam');
+        }
+        if (isset($CFG->quarantinedir)) {
+            set_config('quarantinedir', $CFG->quarantinedir, 'antivirus_clamav');
+            unset_config('quarantinedir');
+        }
+        if (isset($CFG->clamfailureonupload)) {
+            set_config('clamfailureonupload', $CFG->clamfailureonupload, 'antivirus_clamav');
+            unset_config('clamfailureonupload');
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2016022500.01);
+    }
+
     return true;
 }
index f71453c..5b806b1 100644 (file)
@@ -4380,3 +4380,31 @@ function events_pending_count($eventname) {
 
     return $DB->count_records_sql($sql, array($eventname));
 }
+
+/**
+ * Emails admins about a clam outcome
+ *
+ * @deprecated since Moodle 3.0 - this is a part of clamav plugin now.
+ * @param string $notice The body of the email to be sent.
+ * @return void
+ */
+function clam_message_admins($notice) {
+    debugging('clam_message_admins() is deprecated, please use message_admins() method of \antivirus_clamav\scanner class.', DEBUG_DEVELOPER);
+
+    $antivirus = \core\antivirus\manager::get_antivirus('clamav');
+    $antivirus->message_admins($notice);
+}
+
+/**
+ * Returns the string equivalent of a numeric clam error code
+ *
+ * @deprecated since Moodle 3.0 - this is a part of clamav plugin now.
+ * @param int $returncode The numeric error code in question.
+ * @return string The definition of the error code
+ */
+function get_clam_error_code($returncode) {
+    debugging('get_clam_error_code() is deprecated, please use get_clam_error_code() method of \antivirus_clamav\scanner class.', DEBUG_DEVELOPER);
+
+    $antivirus = \core\antivirus\manager::get_antivirus('clamav');
+    return $antivirus->get_clam_error_code($returncode);
+}
index add6312..16bad00 100644 (file)
@@ -185,7 +185,7 @@ $CFG->dboptions = isset($CFG->phpunit_dboptions) ? $CFG->phpunit_dboptions : $CF
 $allowed = array('wwwroot', 'dataroot', 'dirroot', 'admin', 'directorypermissions', 'filepermissions',
                  'dbtype', 'dblibrary', 'dbhost', 'dbname', 'dbuser', 'dbpass', 'prefix', 'dboptions',
                  'proxyhost', 'proxyport', 'proxytype', 'proxyuser', 'proxypassword', 'proxybypass', // keep proxy settings from config.php
-                 'altcacheconfigpath', 'pathtogs', 'pathtoclam', 'pathtodu', 'aspellpath', 'pathtodot'
+                 'altcacheconfigpath', 'pathtogs', 'pathtodu', 'aspellpath', 'pathtodot'
                 );
 $productioncfg = (array)$CFG;
 $CFG = new stdClass();
diff --git a/lib/tests/antivirus_test.php b/lib/tests/antivirus_test.php
new file mode 100644 (file)
index 0000000..13320e9
--- /dev/null
@@ -0,0 +1,37 @@
+<?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/>.
+
+/**
+ * Tests for antivirus manager.
+ *
+ * @package    core_antivirus
+ * @category   phpunit
+ * @copyright  2016 Ruslan Kabalin, Lancaster University.
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+class core_antivirus_testcase extends advanced_testcase {
+
+    public function test_manager_get_antivirus() {
+        // We are using clamav plugin in the test,
+        // as the only plugin we know exists for sure.
+        $antivirusviaget = \core\antivirus\manager::get_antivirus('clamav');
+        $antivirusdirect = new \antivirus_clamav\scanner();
+        $this->assertEquals($antivirusdirect, $antivirusviaget);
+    }
+}
index f7b31c8..d570e27 100644 (file)
@@ -34,7 +34,7 @@ class core_component_testcase extends advanced_testcase {
     // To be changed if number of subsystems increases/decreases,
     // this is defined here to annoy devs that try to add more without any thinking,
     // always verify that it does not collide with any existing add-on modules and subplugins!!!
-    const SUBSYSTEMCOUNT = 63;
+    const SUBSYSTEMCOUNT = 64;
 
     public function test_get_core_subsystems() {
         global $CFG;
index c6796a4..d23f702 100644 (file)
@@ -65,6 +65,11 @@ information provided here is intended especially for developers.
   are assigned to fields and buttons through a self-contained JS function.
 * Added $CFG->urlrewriteclass option to config.php allowing clean / semantic urls to
   be implemented in a plugin, eg local_cleanurls.
+* $CFG->pathtoclam global setting has been moved to clamav antivirus plugin setting of the same name.
+* clam_message_admins() and get_clam_error_code() have been deprecated, its functionality
+  is now a part of \antivirus_clamav\scanner class methods.
+* \repository::antivir_scan_file() has been deprecated, \core\antivirus\manager::scan_file() that
+  applies antivirus plugins is replacing its functionality.
 
 === 3.0 ===
 
index 4d74609..8d8ed42 100644 (file)
@@ -49,68 +49,3 @@ class upload_manager {
         throw new coding_exception('upload_manager class can not be used any more, please use file picker instead');
     }
 }
-
-/**************************************************************************************
-THESE FUNCTIONS ARE OUTSIDE THE CLASS BECAUSE THEY NEED TO BE CALLED FROM OTHER PLACES.
-FOR EXAMPLE CLAM_HANDLE_INFECTED_FILE AND CLAM_REPLACE_INFECTED_FILE USED FROM CRON
-UPLOAD_PRINT_FORM_FRAGMENT DOESN'T REALLY BELONG IN THE CLASS BUT CERTAINLY IN THIS FILE
-***************************************************************************************/
-
-/**
- * Emails admins about a clam outcome
- *
- * @param string $notice The body of the email to be sent.
- */
-function clam_message_admins($notice) {
-
-    $site = get_site();
-
-    $subject = get_string('clamemailsubject', 'moodle', format_string($site->fullname));
-    $admins = get_admins();
-    foreach ($admins as $admin) {
-        $eventdata = new stdClass();
-        $eventdata->component         = 'moodle';
-        $eventdata->name              = 'errors';
-        $eventdata->userfrom          = get_admin();
-        $eventdata->userto            = $admin;
-        $eventdata->subject           = $subject;
-        $eventdata->fullmessage       = $notice;
-        $eventdata->fullmessageformat = FORMAT_PLAIN;
-        $eventdata->fullmessagehtml   = '';
-        $eventdata->smallmessage      = '';
-        message_send($eventdata);
-    }
-}
-
-/**
- * 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');
-}
index 1e9bb67..a8258a2 100644 (file)
@@ -1185,72 +1185,14 @@ abstract class repository implements cacheable_object {
      * permissions of the file are not modified here!
      *
      * @static
+     * @deprecated since Moodle 3.0
      * @param string $thefile
      * @param string $filename name of the file
      * @param bool $deleteinfected
      */
     public static function antivir_scan_file($thefile, $filename, $deleteinfected) {
-        global $CFG;
-
-        if (!is_readable($thefile)) {
-            // this should not happen
-            return;
-        }
-
-        if (empty($CFG->runclamonupload) or empty($CFG->pathtoclam)) {
-            // clam not enabled
-            return;
-        }
-
-        $CFG->pathtoclam = trim($CFG->pathtoclam);
-
-        if (!file_exists($CFG->pathtoclam) or !is_executable($CFG->pathtoclam)) {
-            // misconfigured clam - use the old notification for now
-            require("$CFG->libdir/uploadlib.php");
-            $notice = get_string('clamlost', 'moodle', $CFG->pathtoclam);
-            clam_message_admins($notice);
-            return;
-        }
-
-        $clamparam = ' --stdout ';
-        // If we are dealing with clamdscan, clamd is likely run as a different user
-        // that might not have permissions to access your file.
-        // To make clamdscan work, we use --fdpass parameter that passes the file
-        // descriptor permissions to clamd, which allows it to scan given file
-        // irrespective of directory and file permissions.
-        if (basename($CFG->pathtoclam) == 'clamdscan') {
-            $clamparam .= '--fdpass ';
-        }
-        // execute test
-        $cmd = escapeshellcmd($CFG->pathtoclam).$clamparam.escapeshellarg($thefile);
-        exec($cmd, $output, $return);
-
-        if ($return == 0) {
-            // perfect, no problem found
-            return;
-
-        } else if ($return == 1) {
-            // infection found
-            if ($deleteinfected) {
-                unlink($thefile);
-            }
-            throw new moodle_exception('virusfounduser', 'moodle', '', array('filename'=>$filename));
-
-        } else {
-            //unknown problem
-            require("$CFG->libdir/uploadlib.php");
-            $notice = get_string('clamfailed', 'moodle', get_clam_error_code($return));
-            $notice .= "\n\n". implode("\n", $output);
-            clam_message_admins($notice);
-            if ($CFG->clamfailureonupload === 'actlikevirus') {
-                if ($deleteinfected) {
-                    unlink($thefile);
-                }
-                throw new moodle_exception('virusfounduser', 'moodle', '', array('filename'=>$filename));
-            } else {
-                return;
-            }
-        }
+        debugging('Please upgrade your code to use \core\antivirus\manager::scan_file instead', DEBUG_DEVELOPER);
+        \core\antivirus\manager::scan_file($thefile, $filename, $deleteinfected);
     }
 
     /**
@@ -1377,7 +1319,8 @@ abstract class repository implements cacheable_object {
         global $DB, $CFG, $USER, $OUTPUT;
 
         // scan for viruses if possible, throws exception if problem found
-        self::antivir_scan_file($thefile, $record->filename, empty($CFG->repository_no_delete)); //TODO: MDL-28637 this repository_no_delete is a bloody hack!
+        // TODO: MDL-28637 this repository_no_delete is a bloody hack!
+        \core\antivirus\manager::scan_file($thefile, $record->filename, empty($CFG->repository_no_delete));
 
         $fs = get_file_storage();
         // If file name being used.
index 5b3675b..2f14477 100644 (file)
@@ -141,7 +141,7 @@ class repository_upload extends repository {
             }
         }
 
-        self::antivir_scan_file($_FILES[$elname]['tmp_name'], $_FILES[$elname]['name'], true);
+        \core\antivirus\manager::scan_file($_FILES[$elname]['tmp_name'], $_FILES[$elname]['name'], true);
 
         // {@link repository::build_source_field()}
         $sourcefield = $this->get_file_source_info($_FILES[$elname]['name']);
index 4adc2d6..31b7b05 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2016030102.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2016030103.00;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.