Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.
<?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();
require_once($CFG->dirroot . '/iplookup/lib.php');

/**
 * 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 {
    /** Scanning result indicating no virus found. */
    const SCAN_RESULT_OK = 0;
    /** Scanning result indicating that virus is found. */
    const SCAN_RESULT_FOUND = 1;
    /** Scanning result indicating the error. */
    const SCAN_RESULT_ERROR = 2;

    /** @var stdClass the config for antivirus */
    protected $config;
    /** @var string scanning notice */
    protected $scanningnotice = '';
    /** @var array any admin messages generated by a plugin. */
    protected $messages = [];

    /**
     * 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');
    }

    /**
     * Get scanning notice.
     *
     * @return string
     */
    public function get_scanning_notice() {
        return $this->scanningnotice;
    }

    /**
     * Set scanning notice.
     *
     * @param string $notice notice to set.
     * @return void
     */
    protected function set_scanning_notice($notice) {
        $this->scanningnotice = $notice;
    }

    /**
     * Are the antivirus settings configured?
     *
     * @return bool True if plugin has been configured.
     */
    public abstract function is_configured();

    /**
     * Scan 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).
     * @return int Scanning result constants.
     */
    public abstract function scan_file($file, $filename);

    /**
     * Scan data.
     *
     * By default it saves data variable content to file and then scans it using
     * scan_file method, however if antivirus plugin permits scanning data directly,
     * the method can be overridden.
     *
     * @param string $data The variable containing the data to scan.
     * @return int Scanning result constants.
     */
    public function scan_data($data) {
        // Prepare temp file.
        $tempdir = make_request_directory();
        $tempfile = $tempdir . DIRECTORY_SEPARATOR . rand();
        file_put_contents($tempfile, $data);

        // Perform a virus scan now.
        return $this->scan_file($tempfile, get_string('datastream', 'antivirus'));
    }

    /**
     * This function pushes given messages into the message queue, which will be sent by the antivirus manager.
     *
     * @param string $notice The body of the email to be sent.
     * @param string $format The body format.
     * @param string $eventname event name
     * @return void
     * @throws \coding_exception
     * @throws \moodle_exception
     */
    public function message_admins($notice, $format = FORMAT_PLAIN, $eventname = 'errors') {
        $noticehtml = $format !== FORMAT_PLAIN ? format_text($notice, $format) : '';
        $site = get_site();

        $subject = get_string('emailsubject', 'antivirus', format_string($site->fullname));
        $notifyemail = get_config('antivirus', 'notifyemail');
        // If one email address is specified, construct a message to fake account.
        if (!empty($notifyemail)) {
            $user = new \stdClass();
            $user->id = -1;
            $user->email = $notifyemail;
            $user->mailformat = 1;
            $admins = [$user];
        } else {
            // Otherwise, we message all admins.
            $admins = get_admins();
        }

        foreach ($admins as $admin) {
            $eventdata = new \core\message\message();
            $eventdata->courseid          = SITEID;
            $eventdata->component         = 'moodle';
            $eventdata->name              = $eventname;
            $eventdata->userfrom          = get_admin();
            $eventdata->userto            = $admin;
            $eventdata->subject           = $subject;
            $eventdata->fullmessage       = $notice;
            $eventdata->fullmessageformat = $format;
            $eventdata->fullmessagehtml   = $noticehtml;
            $eventdata->smallmessage      = '';

            // Now add the message to an array to be sent by the antivirus manager.
            $this->messages[] = $eventdata;
        }
    }

    /**
     * Return incident details
     *
     * @param string $file full path to the file
     * @param string $filename original name of the file
     * @param string $notice notice from antivirus
     * @param string $virus if this template is due to a virus found.
     * @return string the incident details
     * @throws \coding_exception
     */
    public function get_incident_details($file = '', $filename = '', $notice = '', $virus = true) {
        global $OUTPUT, $USER;
        if (empty($notice)) {
            $notice = $this->get_scanning_notice();
        }
        $classname = get_class($this);
        $component = explode('\\', $classname)[0];

        $content = new \stdClass();
        $unknown = get_string('unknown', 'antivirus');
        $content->header = get_string('emailinfectedfiledetected', 'antivirus');
        $content->filename = !empty($filename) ? $filename : $unknown;
        $content->scanner = $component;
        // Check for empty file, or file not uploaded.
        if (!empty($file) && filesize($file) !== false) {
            $content->filesize = display_size(filesize($file));
            $content->contenthash = \file_storage::hash_from_path($file);
            $content->contenttype = mime_content_type($file);
        } else {
            $content->filesize = $unknown;
            $content->contenthash = $unknown;
            $content->contenttype = $unknown;
        }

        $content->author = \core_user::is_real_user($USER->id) ? fullname($USER) . " ($USER->username)" : $unknown;
        $content->ipaddress = getremoteaddr();
        $geoinfo = iplookup_find_location(getremoteaddr());
        $content->geoinfo = $geoinfo['city'] . ', ' . $geoinfo['country'];
        $content->date = userdate(time(), get_string('strftimedatetimeshort'));
        $content->referer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : $unknown;
        $content->notice = $notice;
        $report = new \moodle_url('/report/infectedfiles/index.php');
        $content->report = $report->out();

        // If this is not due to a virus, we need to change the header line.
        if (!$virus) {
            $content->header = get_string('emailscannererrordetected', 'antivirus');
        }

        return $OUTPUT->render_from_template('core/infected_file_email', $content);
    }

    /**
     * Getter method for messages queued by the antivirus scanner.
     *
     * @return array
     */
    public function get_messages() : array {
        return $this->messages;
    }
> } > /** > * Getter method for the antivirus message displayed in the exception. > * > * @return array array of string and component to pass to exception constructor. > */ > public function get_virus_found_message() { > // Base antivirus found string. > return ['string' => 'virusfound', 'component' => 'antivirus', 'placeholders' => []]; > }