diff --git a/admin/antiviruses.php b/admin/antiviruses.php
new file mode 100644
index 00000000000..d4d5dcdf037
--- /dev/null
+++ b/admin/antiviruses.php
@@ -0,0 +1,106 @@
+.
+
+/**
+ * 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.
+$available_antiviruses = antiviruses_get_available();
+if (!empty($antivirus) and empty($available_antiviruses[$antivirus])) {
+ redirect ($returnurl);
+}
+
+$active_antiviruses = explode(',', $CFG->antiviruses);
+foreach ($active_antiviruses as $key=>$active) {
+ if (empty($available_antiviruses[$active])) {
+ unset($active_antiviruses[$key]);
+ }
+}
+
+if (!confirm_sesskey()) {
+ redirect($returnurl);
+}
+
+switch ($action) {
+ case 'disable':
+ // Remove from enabled list.
+ $key = array_search($antivirus, $active_antiviruses);
+ unset($active_antiviruses[$key]);
+ break;
+
+ case 'enable':
+ // Add to enabled list.
+ if (!in_array($antivirus, $active_antiviruses)) {
+ $active_antiviruses[] = $antivirus;
+ $active_antiviruses = array_unique($active_antiviruses);
+ }
+ break;
+
+ case 'down':
+ $key = array_search($antivirus, $active_antiviruses);
+ // Check auth plugin is valid.
+ if ($key !== false) {
+ // move down the list
+ if ($key < (count($active_antiviruses) - 1)) {
+ $fsave = $active_antiviruses[$key];
+ $active_antiviruses[$key] = $active_antiviruses[$key + 1];
+ $active_antiviruses[$key + 1] = $fsave;
+ }
+ }
+ break;
+
+ case 'up':
+ $key = array_search($antivirus, $active_antiviruses);
+ // Check auth is valid.
+ if ($key !== false) {
+ // move up the list
+ if ($key >= 1) {
+ $fsave = $active_antiviruses[$key];
+ $active_antiviruses[$key] = $active_antiviruses[$key - 1];
+ $active_antiviruses[$key - 1] = $fsave;
+ }
+ }
+ break;
+
+ default:
+ break;
+}
+
+set_config('antiviruses', implode(',', $active_antiviruses));
+core_plugin_manager::reset_caches();
+
+redirect ($returnurl);
\ No newline at end of file
diff --git a/admin/settings/plugins.php b/admin/settings/plugins.php
index 1e3322b1ab0..cd6659ff5b9 100644
--- a/admin/settings/plugins.php
+++ b/admin/settings/plugins.php
@@ -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'));
diff --git a/lang/en/antivirus.php b/lang/en/antivirus.php
new file mode 100644
index 00000000000..77a51b4fd49
--- /dev/null
+++ b/lang/en/antivirus.php
@@ -0,0 +1,28 @@
+.
+
+/**
+ * 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 antiviruses';
+$string['antiviruses'] = 'Antiviruses';
+$string['antivirussettings'] = 'Manage antiviruses';
+$string['configantivirusplugins'] = 'Please choose the antivirus plugins you wish to use and arrange them in order of being applied.';
diff --git a/lib/adminlib.php b/lib/adminlib.php
index eedbcea2a00..faf8972d254 100644
--- a/lib/adminlib.php
+++ b/lib/adminlib.php
@@ -6494,6 +6494,179 @@ class admin_setting_manageeditors extends admin_setting {
}
}
+/**
+ * Special class for antiviruses administration.
+ *
+ * @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
+ *
+ * @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;
+ }
+
+ $antiviruses_available = antiviruses_get_available();
+ foreach ($antiviruses_available 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";
+
+ $antiviruses_available = antiviruses_get_available();
+ $active_antiviruses = explode(',', $CFG->antiviruses);
+
+ $active_antiviruses = array_reverse($active_antiviruses);
+ foreach ($active_antiviruses as $key=>$antivirus) {
+ if (empty($antiviruses_available[$antivirus])) {
+ unset($active_antiviruses[$key]);
+ } else {
+ $name = $antiviruses_available[$antivirus];
+ unset($antiviruses_available[$antivirus]);
+ $antiviruses_available[$antivirus] = $name;
+ }
+ }
+ $antiviruses_available = array_reverse($antiviruses_available, 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($active_antiviruses);
+ $url = "antiviruses.php?sesskey=" . sesskey();
+ foreach ($antiviruses_available as $antivirus => $name) {
+ // hide/show link
+ $class = '';
+ if (in_array($antivirus, $active_antiviruses)) {
+ $hideshow = "";
+ $hideshow .= "
pix_url('t/hide') . "\" class=\"iconsmall\" alt=\"disable\" />";
+ $enabled = true;
+ $displayname = $name;
+ }
+ else {
+ $hideshow = "";
+ $hideshow .= "
pix_url('t/show') . "\" class=\"iconsmall\" alt=\"enable\" />";
+ $enabled = false;
+ $displayname = $name;
+ $class = 'dimmed_text';
+ }
+
+ // up/down link (only if auth is enabled)
+ $updown = '';
+ if ($enabled) {
+ if ($updowncount > 1) {
+ $updown .= "";
+ $updown .= "
pix_url('t/up') . "\" alt=\"up\" class=\"iconsmall\" /> ";
+ }
+ else {
+ $updown .= "
pix_url('spacer') . "\" class=\"iconsmall\" alt=\"\" /> ";
+ }
+ if ($updowncount < $antiviruscount) {
+ $updown .= "";
+ $updown .= "
pix_url('t/down') . "\" alt=\"down\" class=\"iconsmall\" />";
+ }
+ else {
+ $updown .= "
pix_url('spacer') . "\" class=\"iconsmall\" alt=\"\" />";
+ }
+ ++ $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 = "{$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').'
'.get_string('tablenosave', 'admin');
+ $return .= $OUTPUT->box_end();
+ return highlight($query, $return);
+ }
+}
/**
* Special class for license administration.
diff --git a/lib/antiviruslib.php b/lib/antiviruslib.php
new file mode 100644
index 00000000000..2da0e27e76e
--- /dev/null
+++ b/lib/antiviruslib.php
@@ -0,0 +1,140 @@
+.
+
+/**
+ * Utility classes and functions 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
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Returns list of enabled antiviruses.
+ *
+ * @return array Array ('antivirusname'=>stdClass antivirus object).
+ */
+function antiviruses_get_enabled() {
+ global $CFG;
+
+ $active = array();
+ if (empty($CFG->antiviruses)) {
+ return $active;
+ }
+
+ foreach(explode(',', $CFG->antiviruses) as $e) {
+ if ($antivirus = antiviruses_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 bool $deleteinfected whether infected file needs to be deleted.
+ * @throws moodle_exception If file is infected.
+ * @return void
+ */
+function antiviruses_scan_file($file, $deleteinfected) {
+ $antiviruses = antiviruses_get_enabled();
+ foreach ($antiviruses as $antivirus) {
+ $antivirus->scan_file($file, $deleteinfected);
+ }
+}
+
+/**
+ * Returns instance of antivirus.
+ *
+ * @param string $antivirusname name of antivirus.
+ * @return object|bool antivirus instance or false if does not exist.
+ */
+function antiviruses_get_antivirus($antivirusname) {
+ global $CFG;
+
+ $libfile = "$CFG->libdir/antivirus/$antivirusname/lib.php";
+ if (!file_exists($libfile)) {
+ return false;
+ }
+ require_once($libfile);
+ $classname = 'antivirus_' . $antivirusname;
+ if (!class_exists($classname)) {
+ return false;
+ }
+ return new $classname();
+}
+
+/**
+ * Get the list of available antiviruses.
+ *
+ * @return array Array ('antivirusname'=>'localised antivirus name').
+ */
+function antiviruses_get_available() {
+ $antiviruses = array();
+ foreach (core_component::get_plugin_list('antivirus') as $antivirusname => $dir) {
+ $antiviruses[$antivirusname] = get_string('pluginname', 'antivirus_'.$antivirusname);
+ }
+ return $antiviruses;
+}
+
+/**
+ * Base abstract antivirus 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 antivirus {
+ /** @var stdClass the config for antivirus */
+ protected $config;
+
+ /**
+ * Class constructor.
+ *
+ * @return void.
+ */
+ public function __construct() {
+ // Populate config variable, child class name is matching full plugin name,
+ // so we can use it directly to retrieve plugin configuration.
+ $this->config = get_config(get_class($this));
+ }
+
+ /**
+ * 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 moodle_exception If file is infected.
+ * @return void
+ */
+ public abstract function scan_file($file, $filename, $deleteinfected);
+}
diff --git a/lib/classes/component.php b/lib/classes/component.php
index 4fcad1509a7..8bd659ddffd 100644
--- a/lib/classes/component.php
+++ b/lib/classes/component.php
@@ -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
index 00000000000..c9f776d1e28
--- /dev/null
+++ b/lib/classes/plugininfo/antivirus.php
@@ -0,0 +1,119 @@
+.
+
+/**
+ * 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
+ */
+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;
+ }
+
+ public function get_settings_section_name() {
+ return 'antivirussettings' . $this->name;
+ }
+
+ 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();
+ }
+}
diff --git a/lib/db/install.php b/lib/db/install.php
index f8dbdce278a..f93ad10bebc 100644
--- a/lib/db/install.php
+++ b/lib/db/install.php
@@ -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.
diff --git a/lib/setup.php b/lib/setup.php
index 4c5930bc03f..4bb4e6ae6b8 100644
--- a/lib/setup.php
+++ b/lib/setup.php
@@ -607,6 +607,7 @@ require_once($CFG->libdir .'/editorlib.php'); // All text editor related f
require_once($CFG->libdir .'/messagelib.php'); // Messagelib functions
require_once($CFG->libdir .'/modinfolib.php'); // Cached information on course-module instances
require_once($CFG->dirroot.'/cache/lib.php'); // Cache API
+require_once($CFG->libdir .'/antiviruslib.php'); // Antivirus functions and classes.
// make sure PHP is not severly misconfigured
setup_validate_php_configuration();