// Test plugin dependencies.
$failed = array();
-if (!core_plugin_manager::instance()->all_plugins_ok($version, $failed)) {
+if (!core_plugin_manager::instance()->all_plugins_ok($version, $failed, $CFG->branch)) {
cli_problem(get_string('pluginscheckfailed', 'admin', array('pluginslist' => implode(', ', array_unique($failed)))));
cli_error(get_string('pluginschecktodo', 'admin'));
}
// check plugin dependencies
$failed = array();
- if (!core_plugin_manager::instance()->all_plugins_ok($version, $failed)) {
+ if (!core_plugin_manager::instance()->all_plugins_ok($version, $failed, $CFG->branch)) {
$PAGE->navbar->add(get_string('pluginscheck', 'admin'));
$PAGE->set_title($strinstallation);
$PAGE->set_heading($strinstallation . ' - Moodle ' . $CFG->target_release);
} else {
// Always verify plugin dependencies!
$failed = array();
- if (!core_plugin_manager::instance()->all_plugins_ok($version, $failed)) {
+ if (!core_plugin_manager::instance()->all_plugins_ok($version, $failed, $CFG->branch)) {
echo $output->unsatisfied_dependencies_page($version, $failed, $PAGE->url);
die();
}
// Make sure plugin dependencies are always checked.
$failed = array();
- if (!$pluginman->all_plugins_ok($version, $failed)) {
+ if (!$pluginman->all_plugins_ok($version, $failed, $CFG->branch)) {
$output = $PAGE->get_renderer('core', 'admin');
echo $output->unsatisfied_dependencies_page($version, $failed, $PAGE->url);
die();
* @return string HTML code
*/
public function plugins_check_table(core_plugin_manager $pluginman, $version, array $options = array()) {
-
+ global $CFG;
$plugininfo = $pluginman->get_plugins();
if (empty($plugininfo)) {
}
$coredependency = $plugin->is_core_dependency_satisfied($version);
+ $incompatibledependency = $plugin->is_core_compatible_satisfied($CFG->branch);
+
$otherpluginsdependencies = $pluginman->are_dependencies_satisfied($plugin->get_other_required_plugins());
- $dependenciesok = $coredependency && $otherpluginsdependencies;
+ $dependenciesok = $coredependency && $otherpluginsdependencies && $incompatibledependency;
$statuscode = $plugin->get_status();
$row->attributes['class'] .= ' status-' . $statuscode;
}
$status = new html_table_cell($sourcelabel.' '.$status);
-
- $requires = new html_table_cell($this->required_column($plugin, $pluginman, $version));
+ if ($plugin->pluginsupported != null) {
+ $requires = new html_table_cell($this->required_column($plugin, $pluginman, $version, $CFG->branch));
+ } else {
+ $requires = new html_table_cell($this->required_column($plugin, $pluginman, $version));
+ }
$statusisboring = in_array($statuscode, array(
core_plugin_manager::PLUGIN_STATUS_NODB, core_plugin_manager::PLUGIN_STATUS_UPTODATE));
* @param \core\plugininfo\base $plugin the plugin we are rendering the row for.
* @param core_plugin_manager $pluginman provides data on all the plugins.
* @param string $version
+ * @param int $branch the current Moodle branch
* @return string HTML code
*/
- protected function required_column(\core\plugininfo\base $plugin, core_plugin_manager $pluginman, $version) {
+ protected function required_column(\core\plugininfo\base $plugin, core_plugin_manager $pluginman, $version, $branch = null) {
$requires = array();
$displayuploadlink = false;
$displayupdateslink = false;
- foreach ($pluginman->resolve_requirements($plugin, $version) as $reqname => $reqinfo) {
+ $requirements = $pluginman->resolve_requirements($plugin, $version, $branch);
+ foreach ($requirements as $reqname => $reqinfo) {
if ($reqname === 'core') {
if ($reqinfo->status == $pluginman::REQUIREMENT_STATUS_OK) {
$class = 'requires-ok';
$class = 'requires-failed';
$label = html_writer::span(get_string('dependencyfails', 'core_plugin'), 'badge badge-danger');
}
- if ($reqinfo->reqver != ANY_VERSION) {
+
+ if ($branch != null && !$plugin->is_core_compatible_satisfied($branch)) {
+ $requires[] = html_writer::tag('li',
+ html_writer::span(get_string('incompatibleversion', 'core_plugin', $branch), 'dep dep-core').
+ ' '.$label, array('class' => $class));
+
+ } else if ($branch != null && $plugin->pluginsupported != null) {
+ $requires[] = html_writer::tag('li',
+ html_writer::span(get_string('moodlebranch', 'core_plugin',
+ array('min' => $plugin->pluginsupported[0], 'max' => $plugin->pluginsupported[1])), 'dep dep-core').
+ ' '.$label, array('class' => $class));
+
+ } else if ($reqinfo->reqver != ANY_VERSION) {
$requires[] = html_writer::tag('li',
html_writer::span(get_string('moodleversion', 'core_plugin', $plugin->versionrequires), 'dep dep-core').
' '.$label, array('class' => $class));
);
}
+ // Check if supports is present, and $branch is not in, only if $incompatible check was ok.
+ if ($plugin->pluginsupported != null && $class == 'requires-ok' && $branch != null) {
+ if ($pluginman->check_explicitly_supported($plugin, $branch) == $pluginman::VERSION_NOT_SUPPORTED) {
+ $out .= html_writer::div(get_string('notsupported', 'core_plugin', $branch));
+ }
+ }
+
return $out;
}
if (empty($CFG->session_handler_class) and $DB->session_lock_supported()) {
$temp->add(new admin_setting_configcheckbox('dbsessions', new lang_string('dbsessions', 'admin'), new lang_string('configdbsessions', 'admin'), 0));
}
-$temp->add(new admin_setting_configselect('sessiontimeout', new lang_string('sessiontimeout', 'admin'), new lang_string('configsessiontimeout', 'admin'), 7200, array(14400 => new lang_string('numhours', '', 4),
- 10800 => new lang_string('numhours', '', 3),
- 7200 => new lang_string('numhours', '', 2),
- 5400 => new lang_string('numhours', '', '1.5'),
- 3600 => new lang_string('numminutes', '', 60),
- 2700 => new lang_string('numminutes', '', 45),
- 1800 => new lang_string('numminutes', '', 30),
- 900 => new lang_string('numminutes', '', 15),
- 300 => new lang_string('numminutes', '', 5))));
+
+$temp->add(new admin_setting_configduration('sessiontimeout', new lang_string('sessiontimeout', 'admin'),
+ new lang_string('configsessiontimeout', 'admin'), 8 * 60 * 60));
+
$temp->add(new admin_setting_configtext('sessioncookie', new lang_string('sessioncookie', 'admin'), new lang_string('configsessioncookie', 'admin'), '', PARAM_ALPHANUM));
$temp->add(new admin_setting_configtext('sessioncookiepath', new lang_string('sessioncookiepath', 'admin'), new lang_string('configsessioncookiepath', 'admin'), '', PARAM_RAW));
$temp->add(new admin_setting_configtext('sessioncookiedomain', new lang_string('sessioncookiedomain', 'admin'), new lang_string('configsessioncookiedomain', 'admin'), '', PARAM_RAW, 50));
/**
* phpCAS version. accessible for the user by phpCAS::getVersion().
*/
-define('PHPCAS_VERSION', '1.3.7+');
+define('PHPCAS_VERSION', '1.3.8');
/**
* @addtogroup public
// set to callback mode if PgtIou and PgtId CGI GET parameters are provided
if ( $this->isProxy() ) {
- $this->_setCallbackMode(!empty($_GET['pgtIou'])&&!empty($_GET['pgtId']));
+ if(!empty($_GET['pgtIou'])&&!empty($_GET['pgtId'])) {
+ $this->_setCallbackMode(true);
+ $this->_setCallbackModeUsingPost(false);
+ } elseif (!empty($_POST['pgtIou'])&&!empty($_POST['pgtId'])) {
+ $this->_setCallbackMode(true);
+ $this->_setCallbackModeUsingPost(true);
+ } else {
+ $this->_setCallbackMode(false);
+ $this->_setCallbackModeUsingPost(false);
+ }
+
+
}
if ( $this->_isCallbackMode() ) {
return $this->_callback_mode;
}
+ /**
+ * @var bool a boolean to know if the CAS client is using POST parameters when in callback mode.
+ * Written by CAS_Client::_setCallbackModeUsingPost(), read by CAS_Client::_isCallbackModeUsingPost().
+ *
+ * @hideinitializer
+ */
+ private $_callback_mode_using_post = false;
+
+ /**
+ * This method sets/unsets usage of POST parameters in callback mode (default/false is GET parameters)
+ *
+ * @param bool $callback_mode_using_post true to use POST, false to use GET (default).
+ *
+ * @return void
+ */
+ private function _setCallbackModeUsingPost($callback_mode_using_post)
+ {
+ $this->_callback_mode_using_post = $callback_mode_using_post;
+ }
+
+ /**
+ * This method returns true when the callback mode is using POST, false otherwise.
+ *
+ * @return bool A boolean.
+ */
+ private function _isCallbackModeUsingPost()
+ {
+ return $this->_callback_mode_using_post;
+ }
+
/**
* the URL that should be used for the PGT callback (in fact the URL of the
* current request without any CGI parameter). Written and read by
private function _callback()
{
phpCAS::traceBegin();
- if (preg_match('/^PGTIOU-[\.\-\w]+$/', $_GET['pgtIou'])) {
- if (preg_match('/^[PT]GT-[\.\-\w]+$/', $_GET['pgtId'])) {
- $this->printHTMLHeader('phpCAS callback');
- $pgt_iou = $_GET['pgtIou'];
- $pgt = $_GET['pgtId'];
- phpCAS::trace('Storing PGT `'.$pgt.'\' (id=`'.$pgt_iou.'\')');
- echo '<p>Storing PGT `'.$pgt.'\' (id=`'.$pgt_iou.'\').</p>';
- $this->_storePGT($pgt, $pgt_iou);
- $this->printHTMLFooter();
+ if ($this->_isCallbackModeUsingPost()) {
+ $pgtId = $_POST['pgtId'];
+ $pgtIou = $_POST['pgtIou'];
+ } else {
+ $pgtId = $_GET['pgtId'];
+ $pgtIou = $_GET['pgtIou'];
+ }
+ if (preg_match('/^PGTIOU-[\.\-\w]+$/', $pgtIou)) {
+ if (preg_match('/^[PT]GT-[\.\-\w]+$/', $pgtId)) {
+ phpCAS::trace('Storing PGT `'.$pgtId.'\' (id=`'.$pgtIou.'\')');
+ $this->_storePGT($pgtId, $pgtIou);
+ if (array_key_exists('HTTP_ACCEPT', $_SERVER) &&
+ ( $_SERVER['HTTP_ACCEPT'] == 'application/xml' ||
+ $_SERVER['HTTP_ACCEPT'] == 'text/xml'
+ )
+ ) {
+ echo '<?xml version="1.0" encoding="UTF-8"?>' . "\r\n";
+ echo '<proxySuccess xmlns="http://www.yale.edu/tp/cas" />';
+ phpCAS::traceExit("XML response sent");
+ } else {
+ $this->printHTMLHeader('phpCAS callback');
+ echo '<p>Storing PGT `'.$pgtId.'\' (id=`'.$pgtIou.'\').</p>';
+ $this->printHTMLFooter();
+ phpCAS::traceExit("HTML response sent");
+ }
phpCAS::traceExit("Successfull Callback");
} else {
- phpCAS::error('PGT format invalid' . $_GET['pgtId']);
- phpCAS::traceExit('PGT format invalid' . $_GET['pgtId']);
+ phpCAS::error('PGT format invalid' . $pgtId);
+ phpCAS::traceExit('PGT format invalid' . $pgtId);
}
} else {
- phpCAS::error('PGTiou format invalid' . $_GET['pgtIou']);
- phpCAS::traceExit('PGTiou format invalid' . $_GET['pgtIou']);
+ phpCAS::error('PGTiou format invalid' . $pgtIou);
+ phpCAS::traceExit('PGTiou format invalid' . $pgtIou);
}
// Flush the buffer to prevent from sending anything other then a 200
phpCAS is an authentication library that allows PHP applications to easily authenticate
users via a Central Authentication Service (CAS) server.
-Please see the phpCAS website for more information:
+Please see the wiki website for more information:
https://wiki.jasig.org/display/CASC/phpCAS
-[](https://travis-ci.org/Jasig/phpCAS)
+Api documentation can be found here:
+
+https://apereo.github.io/phpCAS/
+
+
+[](https://travis-ci.org/apereo/phpCAS)
LICENSE
-Description of phpCAS 1.3.7 library import
+Description of phpCAS 1.3.8 library import
-* downloaded from http://downloads.jasig.org/cas-clients/php/current/
-* applied patch https://github.com/apereo/phpCAS/pull/247 for PHP 7.2 compatibility (MDL-60280)
-* applied patch https://github.com/apereo/phpCAS/pull/278 for PHP 7.3 compatibility (MDL-63422)
+* downloaded from http://downloads.jasig.org/cas-clients/php/current/
\ No newline at end of file
const INTERACTIVE_YES = true;
const INTERACTIVE_NO = false;
+ /** Release the session during backup/restore */
+ const RELEASESESSION_YES = true;
+ /** Don't release the session during backup/restore */
+ const RELEASESESSION_NO = false;
+
// Predefined modes (purposes) of the backup
const MODE_GENERAL = 10;
if (!($bc = backup_ui::load_controller($backupid))) {
$bc = new backup_controller($type, $id, backup::FORMAT_MOODLE,
- backup::INTERACTIVE_YES, $backupmode, $USER->id);
+ backup::INTERACTIVE_YES, $backupmode, $USER->id, backup::RELEASESESSION_YES);
// The backup id did not relate to a valid controller so we made a new controller.
// Now we need to reset the backup id to match the new controller.
$backupid = $bc->get_backupid();
* @param bool $interactive Whether this backup will require user interaction; backup::INTERACTIVE_YES or INTERACTIVE_NO
* @param int $mode One of backup::MODE_GENERAL, MODE_IMPORT, MODE_SAMESITE, MODE_HUB, MODE_AUTOMATED
* @param int $userid The id of the user making the backup
+ * @param bool $releasesession Should release the session? backup::RELEASESESSION_YES or backup::RELEASESESSION_NO
*/
- public function __construct($type, $id, $format, $interactive, $mode, $userid){
+ public function __construct($type, $id, $format, $interactive, $mode, $userid, $releasesession = backup::RELEASESESSION_NO) {
$this->type = $type;
$this->id = $id;
$this->courseid = backup_controller_dbops::get_courseid_from_type_id($this->type, $this->id);
$this->interactive = $interactive;
$this->mode = $mode;
$this->userid = $userid;
+ $this->releasesession = $releasesession;
// Apply some defaults
$this->operation = backup::OPERATION_BACKUP;
core_php_time_limit::raise(1 * 60 * 60); // 1 hour for 1 course initially granted
raise_memory_limit(MEMORY_EXTRA);
+ // Release the session so other tabs in the same session are not blocked.
+ if ($this->get_releasesession() === backup::RELEASESESSION_YES) {
+ \core\session\manager::write_close();
+ }
+
// If the controller has decided that we can include files, then check the setting, otherwise do not include files.
if ($this->get_include_files()) {
$this->set_include_files((bool) $this->get_plan()->get_setting('files')->get_value());
*/
protected $logger;
+ /** @var bool Whether this backup should release the session. */
+ protected $releasesession = backup::RELEASESESSION_NO;
+
/**
* Gets the progress reporter, which can be used to report progress within
* the backup or restore process.
public function log($message, $level, $a = null, $depth = null, $display = false) {
backup_helper::log($message, $level, $a, $depth, $display, $this->logger);
}
+
+ /**
+ * Returns the set value of releasesession.
+ * This is used to indicate if the session should be closed during the backup/restore.
+ *
+ * @return bool Indicates whether the session should be released.
+ */
+ public function get_releasesession() {
+ return $this->releasesession;
+ }
}
* @param int $userid
* @param int $target backup::TARGET_[ NEW_COURSE | CURRENT_ADDING | CURRENT_DELETING | EXISTING_ADDING | EXISTING_DELETING ]
* @param \core\progress\base $progress Optional progress monitor
+ * @param bool $releasesession Should release the session? backup::RELEASESESSION_YES or backup::RELEASESESSION_NO
*/
public function __construct($tempdir, $courseid, $interactive, $mode, $userid, $target,
- \core\progress\base $progress = null) {
+ \core\progress\base $progress = null, $releasesession = backup::RELEASESESSION_NO) {
$this->tempdir = $tempdir;
$this->courseid = $courseid;
$this->interactive = $interactive;
$this->mode = $mode;
$this->userid = $userid;
$this->target = $target;
+ $this->releasesession = $releasesession;
// Apply some defaults
$this->type = '';
core_php_time_limit::raise(1 * 60 * 60); // 1 hour for 1 course initially granted
raise_memory_limit(MEMORY_EXTRA);
+ // Release the session so other tabs in the same session are not blocked.
+ if ($this->get_releasesession() === backup::RELEASESESSION_YES) {
+ \core\session\manager::write_close();
+ }
+
// Do course cleanup precheck, if required. This was originally in restore_ui. Moved to handle async backup/restore.
if ($this->get_target() == backup::TARGET_CURRENT_DELETING || $this->get_target() == backup::TARGET_EXISTING_DELETING) {
$options = array();
require_capability('moodle/backup:backupactivity', $context);
} else {
require_capability('moodle/backup:backupcourse', $context);
- $instanceid = $course->id;
}
$results = array();
if (is_null($course)) {
$coursefullname = $SITE->fullname;
$courseshortname = $SITE->shortname;
+ $courseurl = new moodle_url('/');
} else {
$coursefullname = $course->fullname;
$courseshortname = $course->shortname;
+ $courseurl = course_get_url($course->id);
}
// Show page header.
$restore = restore_ui::engage_independent_stage($stage/2, $contextid);
if ($restore->process()) {
$rc = new restore_controller($restore->get_filepath(), $restore->get_course_id(), backup::INTERACTIVE_YES,
- $backupmode, $USER->id, $restore->get_target());
+ $backupmode, $USER->id, $restore->get_target(), null, backup::RELEASESESSION_YES);
}
}
if ($rc) {
\core\task\manager::queue_adhoc_task($asynctask);
// Add ajax progress bar and initiate ajax via a template.
- $courseurl = new moodle_url('/course/view.php', array('id' => $course->id));
$restoreurl = new moodle_url('/backup/restorefile.php', array('contextid' => $contextid));
$progresssetup = array(
'backupid' => $restoreid,
'restoreurl' => $restoreurl->out()
);
echo $renderer->render_from_template('core/async_backup_status', $progresssetup);
-
}
$restore->destroy();
// Build the WHERE condition for the sub-query.
if (!empty($subqueryconditions)) {
- $subquerywhere = 'WHERE ' . implode(" OR ", $subqueryconditions);
+ $unionstartquery = "SELECT modulename, instance, eventtype, priority
+ FROM {event} ev
+ WHERE ";
+ $subqueryunion = $unionstartquery . implode(" UNION $unionstartquery ", $subqueryconditions);
+ } else {
+ $subqueryunion = '{event}';
}
// Merge subquery parameters to the parameters of the main query.
ev.instance,
ev.eventtype,
MIN(ev.priority) as priority
- FROM {event} ev
- $subquerywhere
+ FROM ($subqueryunion) ev
GROUP BY ev.modulename, ev.instance, ev.eventtype";
// Build the main query.
* @return mixed
*/
protected static function get_tree($id) {
- global $DB;
$coursecattreecache = cache::make('core', 'coursecattree');
$rv = $coursecattreecache->get($id);
if ($rv !== false) {
return $rv;
}
+ // Might need to rebuild the tree. Put a lock in place to ensure other requests don't try and do this in parallel.
+ $lockfactory = \core\lock\lock_config::get_lock_factory('core_coursecattree');
+ $lock = $lockfactory->get_lock('core_coursecattree_cache',
+ course_modinfo::COURSE_CACHE_LOCK_WAIT, course_modinfo::COURSE_CACHE_LOCK_EXPIRY);
+ if ($lock === false) {
+ // Couldn't get a lock to rebuild the tree.
+ return [];
+ }
+ $rv = $coursecattreecache->get($id);
+ if ($rv !== false) {
+ // Tree was built while we were waiting for the lock.
+ $lock->release();
+ return $rv;
+ }
// Re-build the tree.
+ try {
+ $all = self::rebuild_coursecattree_cache_contents();
+ $coursecattreecache->set_many($all);
+ } finally {
+ $lock->release();
+ }
+ if (array_key_exists($id, $all)) {
+ return $all[$id];
+ }
+ // Requested non-existing category.
+ return array();
+ }
+
+ /**
+ * Rebuild the course category tree as an array, including an extra "countall" field.
+ *
+ * @return array
+ * @throws coding_exception
+ * @throws dml_exception
+ * @throws moodle_exception
+ */
+ private static function rebuild_coursecattree_cache_contents() : array {
+ global $DB;
$sql = "SELECT cc.id, cc.parent, cc.visible
FROM {course_categories} cc
ORDER BY cc.sortorder";
}
// We must add countall to all in case it was the requested ID.
$all['countall'] = $count;
- $coursecattreecache->set_many($all);
- if (array_key_exists($id, $all)) {
- return $all[$id];
- }
- // Requested non-existing category.
- return array();
+ return $all;
}
/**
foreach ($dbcourses as $dbcourse) {
$course = new stdClass();
$course->id = $dbcourse->id;
+ $course->timemodified = time();
$course->category = $category->id;
$course->sortorder = $category->sortorder + MAX_COURSES_IN_CATEGORY - $i++;
if ($category->visible == 0) {
}
}}
<div class="enable">
- <input type="checkbox" name="{{applyname}}" value="1" id="{{applyname}}">
+ <input type="checkbox" name="{{applyname}}" value="1" id="{{applyname}}" class="ml-0">
<label for="{{applyname}}">{{applylabel}}</label>
</div>
<fieldset class="form-inline">
<legend class="accesshide">{{label}}</legend>
<label for="{{menuname}}">{{menulabel}}</label>
- <select name="{{menuname}}" id="{{menuname}}" class="form-control">
+ <select name="{{menuname}}" id="{{menuname}}" class="form-control custom-select">
{{#menuoptions}}
<option value="{{value}}" {{#selected}}selected{{/selected}}>{{name}}</option>
{{/menuoptions}}
$string['pathdoesnotstartslash'] = 'No valid arguments supplied, path does not start with slash!';
$string['pleasereport'] = 'If you have time, please let us know what you were trying to do when the error occurred:';
$string['pluginrequirementsnotmet'] = 'Plugin "{$a->pluginname}" ({$a->pluginversion}) could not be installed. It requires a newer version of Moodle (currently you are using {$a->currentmoodle}, you need {$a->requiremoodle}).';
+$string['pluginunsupported'] = 'Plugin "{$a->pluginname}" {$a->pluginversion} does not support this version of Moodle {$a->moodleversion}. Seek plugin information to find supported versions.';
$string['prefixcannotbeempty'] = '<p>Error: database table prefix cannot be empty ({$a})</p>
<p>The site administrator must fix this problem.</p>';
$string['prefixtoolong'] = '<p>Error: database table prefix is too long ({$a->dbfamily})</p>
$string['alreadyloggedin'] = 'You are already logged in as {$a}, you need to log out before logging in as different user.';
$string['youcannotdeletecategory'] = 'You cannot delete category \'{$a}\' because you can neither delete the contents, nor move them elsewhere.';
$string['protected_cc_not_supported'] = 'Protected cartridges not supported.';
-
$string['shortsitename'] = 'Short name for site (eg single word)';
$string['show'] = 'Show';
$string['showactions'] = 'Show actions';
+$string['showadvancededitor'] = 'Advanced';
$string['showadvancedsettings'] = 'Show advanced settings';
$string['showall'] = 'Show all {$a}';
$string['showallcourses'] = 'Show all courses';
$string['filterall'] = 'Show all';
$string['filtercontribonly'] = 'Show additional plugins only';
$string['filterupdatesonly'] = 'Show updateable only';
+$string['incompatibleversion'] = 'Incompatible Moodle version: {$a}';
$string['isenabled'] = 'Enabled?';
$string['misdepinfoplugin'] = 'Plugin info';
$string['misdepinfoversion'] = 'Version info';
$string['misdepsunavail'] = 'Unavailable missing dependencies';
$string['misdepsunavaillist'] = 'No version found to fulfill the dependency requirements: {$a}.';
$string['misdepsunknownlist'] = 'Not in the Plugins directory: <strong>{$a}</strong>.';
+$string['moodlebranch'] = 'Moodle {$a->min} - {$a->max}';
$string['moodleversion'] = 'Moodle {$a}';
$string['noneinstalled'] = 'No plugins of this type are installed';
$string['notes'] = 'Notes';
$string['notdownloadable'] = 'Can not download the package';
$string['notdownloadable_help'] = 'ZIP package with the update can not be downloaded automatically. Please refer to the documentation page for more help.';
$string['notdownloadable_link'] = 'admin/mdeploy/notdownloadable';
+$string['notsupported'] = 'Plugin may not be compatible with Moodle version {$a}';
$string['notwritable'] = 'Plugin files not writable';
$string['notwritable_help'] = 'Plugin files are not writable by the web server. The web server process must have write access to the plugin folder and all its contents. Write access to the root folder of the given plugin type may also be required.';
$string['otherplugin'] = '{$a->component}';
$string['site:sendmessage'] = 'Send messages to any user';
$string['site:trustcontent'] = 'Trust submitted content';
$string['site:uploadusers'] = 'Upload new users from file';
+$string['site:viewanonymousevents'] = 'View anonymous events in reports';
$string['site:viewfullnames'] = 'Always see full names of users';
$string['site:viewparticipants'] = 'View participants';
$string['site:viewreports'] = 'View reports';
/**
* Validate data.
*
- * This ensures that unix socket transport is supported by this system.
+ * This ensures that the selected socket transport is supported by this system.
*
* @param string $data
* @return mixed True on success, else error message.
*/
public function validate($data) {
+ $supportedtransports = stream_get_transports();
if ($data === 'unixsocket') {
- $supportedtransports = stream_get_transports();
- if (!array_search('unix', $supportedtransports)) {
+ if (array_search('unix', $supportedtransports) === false) {
return get_string('errornounixsocketssupported', 'antivirus_clamav');
}
+ } else if ($data === 'tcpsocket') {
+ if (array_search('tcp', $supportedtransports) === false) {
+ return get_string('errornotcpsocketssupported', 'antivirus_clamav');
+ }
+ }
+ return true;
+ }
+}
+
+
+/**
+ * Abstract socket checking class
+ *
+ * @package antivirus_clamav
+ * @copyright 2015 Ruslan Kabalin, Lancaster University.
+ * @copyright 2019 Didier Raboud, Liip AG.
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class antivirus_clamav_socket_setting extends admin_setting_configtext {
+ /**
+ * Ping ClamAV socket.
+ *
+ * This ensures that a socket setting is correct and that ClamAV is running.
+ *
+ * @param string $socketaddress Address to the socket to connect to (for stream_socket_client)
+ * @return mixed True on success, else error message.
+ */
+ protected function validate_clamav_socket($socketaddress) {
+ $socket = stream_socket_client($socketaddress, $errno, $errstr, ANTIVIRUS_CLAMAV_SOCKET_TIMEOUT);
+ if (!$socket) {
+ return get_string('errorcantopensocket', 'antivirus_clamav', "$errstr ($errno)");
+ } else {
+ // Send PING query to ClamAV socket to check its running state.
+ fwrite($socket, "nPING\n");
+ $response = stream_get_line($socket, 4);
+ fclose($socket);
+ if ($response !== 'PONG') {
+ return get_string('errorclamavnoresponse', 'antivirus_clamav');
+ }
}
return true;
}
* @copyright 2015 Ruslan Kabalin, Lancaster University.
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-class antivirus_clamav_pathtounixsocket_setting extends admin_setting_configtext {
+class antivirus_clamav_pathtounixsocket_setting extends antivirus_clamav_socket_setting {
/**
* Validate data.
*
}
$runningmethod = get_config('antivirus_clamav', 'runningmethod');
if ($runningmethod === 'unixsocket') {
- $socket = stream_socket_client('unix://' . $data, $errno, $errstr, ANTIVIRUS_CLAMAV_SOCKET_TIMEOUT);
- if (!$socket) {
- return get_string('errorcantopensocket', 'antivirus_clamav', "$errstr ($errno)");
- } else {
- // Send PING query to ClamAV socket to check its running state.
- fwrite($socket, "nPING\n");
- $response = stream_get_line($socket, 4);
- fclose($socket);
- if ($response !== 'PONG') {
- return get_string('errorclamavnoresponse', 'antivirus_clamav');
- }
- }
+ return $this->validate_clamav_socket('unix://' . $data);
}
return true;
}
}
+
+/**
+ * Admin setting for Internet domain socket host, adds verification.
+ *
+ * @package antivirus_clamav
+ * @copyright 2019 Didier Raboud, Liip AG.
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class antivirus_clamav_tcpsockethost_setting extends antivirus_clamav_socket_setting {
+ /**
+ * Validate data.
+ *
+ * This ensures that Internet domain socket setting is correct and ClamAV is running.
+ *
+ * @param string $data
+ * @return mixed True on success, else error message.
+ */
+ public function validate($data) {
+ $result = parent::validate($data);
+ if ($result !== true) {
+ return $result;
+ }
+ $runningmethod = get_config('antivirus_clamav', 'runningmethod');
+ $tcpport = get_config('antivirus_clamav', 'tcpsocketport');
+ if ($runningmethod === 'tcpsocket') {
+ return $this->validate_clamav_socket('tcp://' . $data . ':' . $tcpport);
+ }
+ return true;
+ }
+}
\ No newline at end of file
/**
* Class implementing ClamAV antivirus.
* @copyright 2015 Ruslan Kabalin, Lancaster University.
+ * @copyright 2019 Didier Raboud, Liip AG.
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class scanner extends \core\antivirus\scanner {
return (bool)$this->get_config('pathtoclam');
} else if ($this->get_config('runningmethod') === 'unixsocket') {
return (bool)$this->get_config('pathtounixsocket');
+ } else if ($this->get_config('runningmethod') === 'tcpsocket') {
+ return (bool)$this->get_config('tcpsockethost') && (bool)$this->get_config('tcpsocketport');
}
return false;
}
return self::SCAN_RESULT_ERROR;
}
- // Execute the scan using preferable method.
- $method = 'scan_file_execute_' . $this->get_config('runningmethod');
- if (!method_exists($this, $method)) {
- throw new \coding_exception('Attempting to call non-existing method ' . $method);
+ // We can do direct stream scanning if unixsocket or tcpsocket running methods are in use,
+ // if not, use default process.
+ $runningmethod = $this->get_config('runningmethod');
+ switch ($runningmethod) {
+ case 'unixsocket':
+ case 'tcpsocket':
+ $return = $this->scan_file_execute_socket($file, $runningmethod);
+ break;
+ case 'commandline':
+ $return = $this->scan_file_execute_commandline($file);
+ break;
+ default:
+ // This should not happen.
+ debugging('Unknown running method.');
+ return self::SCAN_RESULT_ERROR;
}
- $return = $this->$method($file);
if ($return === self::SCAN_RESULT_ERROR) {
$this->message_admins($this->get_scanning_notice());
* @return int Scanning result constant.
*/
public function scan_data($data) {
- // We can do direct stream scanning if unixsocket running method is in use,
+ // We can do direct stream scanning if unixsocket or tcpsocket running methods are in use,
// if not, use default process.
- if ($this->get_config('runningmethod') === 'unixsocket') {
- $return = $this->scan_data_execute_unixsocket($data);
+ $runningmethod = $this->get_config('runningmethod');
+ if (in_array($runningmethod, array('unixsocket', 'tcpsocket'))) {
+ $return = $this->scan_data_execute_socket($data, $runningmethod);
if ($return === self::SCAN_RESULT_ERROR) {
$this->message_admins($this->get_scanning_notice());
}
}
+ /**
+ * Returns a Unix domain socket destination url
+ *
+ * @return string The socket url, fit for stream_socket_client()
+ */
+ private function get_unixsocket_destination() {
+ return 'unix://' . $this->get_config('pathtounixsocket');
+ }
+
+ /**
+ * Returns a Internet domain socket destination url
+ *
+ * @return string The socket url, fit for stream_socket_client()
+ */
+ private function get_tcpsocket_destination() {
+ return 'tcp://' . $this->get_config('tcpsockethost') . ':' . $this->get_config('tcpsocketport');
+ }
+
/**
* Returns the string equivalent of a numeric clam error code
*
}
/**
- * Scan file using Unix domain sockets.
+ * Scan file using sockets.
*
* @param string $file Full path to the file.
+ * @param string $type Either 'tcpsocket' or 'unixsocket'
* @return int Scanning result constant.
*/
- public function scan_file_execute_unixsocket($file) {
- $socket = stream_socket_client('unix://' . $this->get_config('pathtounixsocket'),
+ public function scan_file_execute_socket($file, $type) {
+ switch ($type) {
+ case "tcpsocket":
+ $socketurl = $this->get_tcpsocket_destination();
+ break;
+ case "unixsocket":
+ $socketurl = $this->get_unixsocket_destination();
+ break;
+ default;
+ // This should not happen.
+ debugging('Unknown socket type.');
+ return self::SCAN_RESULT_ERROR;
+ }
+
+ $socket = stream_socket_client($socketurl,
$errno, $errstr, ANTIVIRUS_CLAMAV_SOCKET_TIMEOUT);
if (!$socket) {
// Can't open socket for some reason, notify admins.
$this->set_scanning_notice($notice);
return self::SCAN_RESULT_ERROR;
} else {
- // Execute scanning. We are running SCAN command and passing file as an argument,
- // it is the fastest option, but clamav user need to be able to access it, so
- // we give group read permissions first and assume 'clamav' user is in web server
- // group (in Debian the default webserver group is 'www-data').
- // Using 'n' as command prefix is forcing clamav to only treat \n as newline delimeter,
- // this is to avoid unexpected newline characters on different systems.
- $perms = fileperms($file);
- chmod($file, 0640);
- fwrite($socket, "nSCAN ".$file."\n");
- $output = stream_get_line($socket, 4096);
+ if ($type == "unixsocket") {
+ // Execute scanning. We are running SCAN command and passing file as an argument,
+ // it is the fastest option, but clamav user need to be able to access it, so
+ // we give group read permissions first and assume 'clamav' user is in web server
+ // group (in Debian the default webserver group is 'www-data').
+ // Using 'n' as command prefix is forcing clamav to only treat \n as newline delimeter,
+ // this is to avoid unexpected newline characters on different systems.
+ $perms = fileperms($file);
+ chmod($file, 0640);
+
+ // Actual scan.
+ fwrite($socket, "nSCAN ".$file."\n");
+ // Get ClamAV answer.
+ $output = stream_get_line($socket, 4096);
+
+ // After scanning we revert permissions to initial ones.
+ chmod($file, $perms);
+ } else if ($type == "tcpsocket") {
+ // Execute scanning by passing the entire file through the TCP socket.
+ // This is not fast, but is the only possibility over a network.
+ // Using 'n' as command prefix is forcing clamav to only treat \n as newline delimeter,
+ // this is to avoid unexpected newline characters on different systems.
+
+ // Actual scan.
+ fwrite($socket, "nINSTREAM\n");
+
+ // Open the file for reading.
+ $fhandle = fopen($file, 'rb');
+ while (!feof($fhandle)) {
+ // Read it by chunks; write them to the TCP socket.
+ $chunk = fread($fhandle, ANTIVIRUS_CLAMAV_SOCKET_CHUNKSIZE);
+ $size = pack('N', strlen($chunk));
+ fwrite($socket, $size);
+ fwrite($socket, $chunk);
+ }
+ // Terminate streaming.
+ fwrite($socket, pack('N', 0));
+ // Get ClamAV answer.
+ $output = stream_get_line($socket, 4096);
+
+ fclose($fhandle);
+ }
+ // Free up the ClamAV socket.
fclose($socket);
- // After scanning we revert permissions to initial ones.
- chmod($file, $perms);
// Parse the output.
- return $this->parse_unixsocket_response($output);
+ return $this->parse_socket_response($output);
}
}
/**
- * Scan data using unix socket.
+ * Scan data socket.
*
* We are running INSTREAM command and passing data stream in chunks.
* The format of the chunk is: <length><data> where <length> is the size of the following
* Do not exceed StreamMaxLength as defined in clamd.conf, otherwise clamd will
* reply with INSTREAM size limit exceeded and close the connection.
*
- * @param string $data The varaible containing the data to scan.
+ * @param string $data The variable containing the data to scan.
+ * @param string $type Either 'tcpsocket' or 'unixsocket'
* @return int Scanning result constant.
*/
- public function scan_data_execute_unixsocket($data) {
- $socket = stream_socket_client('unix://' . $this->get_config('pathtounixsocket'), $errno, $errstr, ANTIVIRUS_CLAMAV_SOCKET_TIMEOUT);
+ public function scan_data_execute_socket($data, $type) {
+ switch ($type) {
+ case "tcpsocket":
+ $socketurl = $this->get_tcpsocket_destination();
+ break;
+ case "unixsocket":
+ $socketurl = $this->get_unixsocket_destination();
+ break;
+ default;
+ // This should not happen.
+ debugging('Unknown socket type!');
+ return self::SCAN_RESULT_ERROR;
+ }
+
+ $socket = stream_socket_client($socketurl, $errno, $errstr, ANTIVIRUS_CLAMAV_SOCKET_TIMEOUT);
if (!$socket) {
// Can't open socket for some reason, notify admins.
$notice = get_string('errorcantopensocket', 'antivirus_clamav', "$errstr ($errno)");
fclose($socket);
// Parse the output.
- return $this->parse_unixsocket_response($output);
+ return $this->parse_socket_response($output);
}
}
/**
- * Parse unix socket command response.
+ * Parse socket command response.
*
- * @param string $output The unix socket command response.
+ * @param string $output The socket response.
* @return int Scanning result constant.
*/
- private function parse_unixsocket_response($output) {
+ private function parse_socket_response($output) {
$splitoutput = explode(': ', $output);
$message = trim($splitoutput[1]);
if ($message === 'OK') {
}
}
}
+
+ /**
+ * Scan data using Unix domain socket.
+ *
+ * @deprecated since Moodle 3.9 MDL-64075 - please do not use this function any more.
+ * @see antivirus_clamav\scanner::scan_data_execute_socket()
+ *
+ * @param string $data The variable containing the data to scan.
+ * @return int Scanning result constant.
+ */
+ public function scan_data_execute_unixsocket($data) {
+ debugging('antivirus_clamav\scanner::scan_data_execute_unixsocket() is deprecated. ' .
+ 'Use antivirus_clamav\scanner::scan_data_execute_socket() instead.', DEBUG_DEVELOPER);
+ return $this->scan_data_execute_socket($data, "unixsocket");
+ }
+
+ /**
+ * Scan file using Unix domain socket.
+ *
+ * @deprecated since Moodle 3.9 MDL-64075 - please do not use this function any more.
+ * @see antivirus_clamav\scanner::scan_file_execute_socket()
+ *
+ * @param string $file Full path to the file.
+ * @return int Scanning result constant.
+ */
+ public function scan_file_execute_unixsocket($file) {
+ debugging('antivirus_clamav\scanner::scan_file_execute_unixsocket() is deprecated. ' .
+ 'Use antivirus_clamav\scanner::scan_file_execute_socket() instead.', DEBUG_DEVELOPER);
+ return $this->scan_file_execute_socket($file, "unixsocket");
+ }
+
}
$string['runningmethoddesc'] = 'Method of running ClamAV. Command line is used by default, however on Unix systems better performance can be obtained by using system sockets.';
$string['runningmethodcommandline'] = 'Command line';
$string['runningmethodunixsocket'] = 'Unix domain socket';
+$string['runningmethodtcpsocket'] = 'TCP socket';
+$string['tcpsockethost'] = 'TCP socket hostname';
+$string['tcpsockethostdesc'] = 'Domain name of the ClamAV server';
+$string['tcpsocketport'] = 'TCP socket port';
+$string['tcpsocketportdesc'] = 'The port to use when connecting to ClamAV';
$string['unknownerror'] = 'There was an unknown error with ClamAV.';
$runningmethodchoice = array(
'commandline' => get_string('runningmethodcommandline', 'antivirus_clamav'),
'unixsocket' => get_string('runningmethodunixsocket', 'antivirus_clamav'),
+ 'tcpsocket' => get_string('runningmethodtcpsocket', 'antivirus_clamav'),
);
$settings->add(new antivirus_clamav_runningmethod_setting('antivirus_clamav/runningmethod',
get_string('runningmethod', 'antivirus_clamav'),
new lang_string('pathtounixsocket', 'antivirus_clamav'),
new lang_string('pathtounixsocketdesc', 'antivirus_clamav'), '', PARAM_PATH));
+ // Hostname to reach ClamAV tcp socket (used in tcp socket running method).
+ $settings->add(new antivirus_clamav_tcpsockethost_setting('antivirus_clamav/tcpsockethost',
+ new lang_string('tcpsockethost', 'antivirus_clamav'),
+ new lang_string('tcpsockethostdesc', 'antivirus_clamav'), '', PARAM_HOST));
+
+ // Port to reach ClamAV tcp socket (used in tcp socket running method).
+ $settings->add(new admin_setting_configtext('antivirus_clamav/tcpsocketport',
+ new lang_string('tcpsocketport', 'antivirus_clamav'),
+ new lang_string('tcpsocketportdesc', 'antivirus_clamav'), 3310, PARAM_INT));
+
// How to act on ClamAV failure.
$options = array(
'donothing' => new lang_string('configclamdonothing', 'antivirus_clamav'),
public function test_scan_file_not_exists() {
$antivirus = $this->getMockBuilder('\antivirus_clamav\scanner')
- ->setMethods(array('scan_file_execute_commandline', 'message_admins'))
- ->getMock();
+ ->setMethods(array('scan_file_execute_commandline', 'message_admins'))
+ ->getMock();
// Test specifying file that does not exist.
$nonexistingfile = $this->tempfile . '_';
public function test_scan_file_no_virus() {
$methods = array(
'scan_file_execute_commandline',
- 'scan_file_execute_unixsocket',
+ 'scan_file_execute_socket',
'message_admins',
'get_config',
);
$antivirus = $this->getMockBuilder('\antivirus_clamav\scanner')
- ->setMethods($methods)
- ->getMock();
+ ->setMethods($methods)
+ ->getMock();
// Initiate mock scanning with configuration setting to use commandline.
$configmap = array(array('runningmethod', 'commandline'));
$antivirus->method('get_config')->will($this->returnValueMap($configmap));
- // Configure scan_file_execute_commandline and scan_file_execute_unixsocket
+ // Configure scan_file_execute_commandline and scan_file_execute_socket
// method stubs to behave as if no virus has been found (SCAN_RESULT_OK).
$antivirus->method('scan_file_execute_commandline')->willReturn(0);
- $antivirus->method('scan_file_execute_unixsocket')->willReturn(0);
+ $antivirus->method('scan_file_execute_socket')->willReturn(0);
// Set expectation that message_admins is NOT called.
$antivirus->expects($this->never())->method('message_admins');
// Run mock scanning.
$this->assertEquals(0, $antivirus->scan_file($this->tempfile, ''));
+
+ // Initiate mock scanning with configuration setting to use tcpsocket.
+ $configmap = array(array('runningmethod', 'tcpsocket'));
+ $antivirus->method('get_config')->will($this->returnValueMap($configmap));
+
+ // Run mock scanning.
+ $this->assertEquals(0, $antivirus->scan_file($this->tempfile, ''));
}
public function test_scan_file_virus() {
$methods = array(
'scan_file_execute_commandline',
- 'scan_file_execute_unixsocket',
+ 'scan_file_execute_socket',
'message_admins',
'get_config',
);
$antivirus = $this->getMockBuilder('\antivirus_clamav\scanner')
- ->setMethods($methods)
- ->getMock();
+ ->setMethods($methods)
+ ->getMock();
// Initiate mock scanning with configuration setting to use commandline.
$configmap = array(array('runningmethod', 'commandline'));
$antivirus->method('get_config')->will($this->returnValueMap($configmap));
- // Configure scan_file_execute_commandline and scan_file_execute_unixsocket
+ // Configure scan_file_execute_commandline and scan_file_execute_socket
// method stubs to behave as if virus has been found (SCAN_RESULT_FOUND).
$antivirus->method('scan_file_execute_commandline')->willReturn(1);
- $antivirus->method('scan_file_execute_unixsocket')->willReturn(1);
+ $antivirus->method('scan_file_execute_socket')->willReturn(1);
// Set expectation that message_admins is NOT called.
$antivirus->expects($this->never())->method('message_admins');
// Run mock scanning.
$this->assertEquals(1, $antivirus->scan_file($this->tempfile, ''));
+
+ // Initiate mock scanning with configuration setting to use tcpsocket.
+ $configmap = array(array('runningmethod', 'tcpsocket'));
+ $antivirus->method('get_config')->will($this->returnValueMap($configmap));
+
+ // Run mock scanning.
+ $this->assertEquals(1, $antivirus->scan_file($this->tempfile, ''));
}
public function test_scan_file_error_donothing() {
$methods = array(
'scan_file_execute_commandline',
- 'scan_file_execute_unixsocket',
+ 'scan_file_execute_socket',
'message_admins',
'get_config',
'get_scanning_notice',
);
$antivirus = $this->getMockBuilder('\antivirus_clamav\scanner')
- ->setMethods($methods)
- ->getMock();
+ ->setMethods($methods)
+ ->getMock();
- // Configure scan_file_execute_commandline and scan_file_execute_unixsocket
+ // Configure scan_file_execute_commandline and scan_file_execute_socket
// method stubs to behave as if there is a scanning error (SCAN_RESULT_ERROR).
$antivirus->method('scan_file_execute_commandline')->willReturn(2);
- $antivirus->method('scan_file_execute_unixsocket')->willReturn(2);
+ $antivirus->method('scan_file_execute_socket')->willReturn(2);
$antivirus->method('get_scanning_notice')->willReturn('someerror');
// Set expectation that message_admins is called.
// Run mock scanning.
$this->assertEquals(2, $antivirus->scan_file($this->tempfile, ''));
+
+ // Initiate mock scanning with configuration setting to do nothing on
+ // scanning error and using tcpsocket.
+ $configmap = array(array('clamfailureonupload', 'donothing'), array('runningmethod', 'tcpsocket'));
+ $antivirus->method('get_config')->will($this->returnValueMap($configmap));
+
+ // Run mock scanning.
+ $this->assertEquals(2, $antivirus->scan_file($this->tempfile, ''));
}
public function test_scan_file_error_actlikevirus() {
$methods = array(
'scan_file_execute_commandline',
- 'scan_file_execute_unixsocket',
+ 'scan_file_execute_socket',
'message_admins',
'get_config',
'get_scanning_notice',
);
$antivirus = $this->getMockBuilder('\antivirus_clamav\scanner')
- ->setMethods($methods)
- ->getMock();
+ ->setMethods($methods)
+ ->getMock();
- // Configure scan_file_execute_commandline and scan_file_execute_unixsocket
+ // Configure scan_file_execute_commandline and scan_file_execute_socket
// method stubs to behave as if there is a scanning error (SCAN_RESULT_ERROR).
$antivirus->method('scan_file_execute_commandline')->willReturn(2);
- $antivirus->method('scan_file_execute_unixsocket')->willReturn(2);
+ $antivirus->method('scan_file_execute_socket')->willReturn(2);
$antivirus->method('get_scanning_notice')->willReturn('someerror');
// Set expectation that message_admins is called.
// Run mock scanning, we expect SCAN_RESULT_FOUND since configuration
// require us to act like virus.
$this->assertEquals(1, $antivirus->scan_file($this->tempfile, ''));
+
+ // Initiate mock scanning with configuration setting to act like virus on
+ // scanning error and using tcpsocket.
+ $configmap = array(array('clamfailureonupload', 'actlikevirus'), array('runningmethod', 'tcpsocket'));
+ $antivirus->method('get_config')->will($this->returnValueMap($configmap));
+
+ // Run mock scanning, we expect SCAN_RESULT_FOUND since configuration
+ // require us to act like virus.
+ $this->assertEquals(1, $antivirus->scan_file($this->tempfile, ''));
}
public function test_scan_data_no_virus() {
$methods = array(
- 'scan_data_execute_unixsocket',
+ 'scan_data_execute_socket',
'message_admins',
'get_config',
);
$antivirus = $this->getMockBuilder('\antivirus_clamav\scanner')
- ->setMethods($methods)
- ->getMock();
+ ->setMethods($methods)
+ ->getMock();
// Initiate mock scanning with configuration setting to use unixsocket.
$configmap = array(array('runningmethod', 'unixsocket'));
$antivirus->method('get_config')->will($this->returnValueMap($configmap));
- // Configure scan_data_execute_unixsocket method stubs to behave as if
+ // Configure scan_data_execute_socket method stubs to behave as if
// no virus has been found (SCAN_RESULT_OK).
- $antivirus->method('scan_data_execute_unixsocket')->willReturn(0);
+ $antivirus->method('scan_data_execute_socket')->willReturn(0);
+
+ // Set expectation that message_admins is NOT called.
+ $antivirus->expects($this->never())->method('message_admins');
+
+ // Run mock scanning.
+ $this->assertEquals(0, $antivirus->scan_data(''));
+
+ // Re-initiate mock scanning with configuration setting to use tcpsocket.
+ $configmap = array(array('runningmethod', 'tcpsocket'));
+ $antivirus->method('get_config')->will($this->returnValueMap($configmap));
// Set expectation that message_admins is NOT called.
$antivirus->expects($this->never())->method('message_admins');
public function test_scan_data_virus() {
$methods = array(
- 'scan_data_execute_unixsocket',
+ 'scan_data_execute_socket',
'message_admins',
'get_config',
);
$antivirus = $this->getMockBuilder('\antivirus_clamav\scanner')
- ->setMethods($methods)
- ->getMock();
+ ->setMethods($methods)
+ ->getMock();
// Initiate mock scanning with configuration setting to use unixsocket.
$configmap = array(array('runningmethod', 'unixsocket'));
$antivirus->method('get_config')->will($this->returnValueMap($configmap));
- // Configure scan_data_execute_unixsocket method stubs to behave as if
+ // Configure scan_data_execute_socket method stubs to behave as if
// no virus has been found (SCAN_RESULT_FOUND).
- $antivirus->method('scan_data_execute_unixsocket')->willReturn(1);
+ $antivirus->method('scan_data_execute_socket')->willReturn(1);
+
+ // Set expectation that message_admins is NOT called.
+ $antivirus->expects($this->never())->method('message_admins');
+
+ // Run mock scanning.
+ $this->assertEquals(1, $antivirus->scan_data(''));
+
+ // Re-initiate mock scanning with configuration setting to use tcpsocket.
+ $configmap = array(array('runningmethod', 'tcpsocket'));
+ $antivirus->method('get_config')->will($this->returnValueMap($configmap));
// Set expectation that message_admins is NOT called.
$antivirus->expects($this->never())->method('message_admins');
public function test_scan_data_error_donothing() {
$methods = array(
- 'scan_data_execute_unixsocket',
+ 'scan_data_execute_socket',
'message_admins',
'get_config',
'get_scanning_notice',
);
$antivirus = $this->getMockBuilder('\antivirus_clamav\scanner')
- ->setMethods($methods)
- ->getMock();
+ ->setMethods($methods)
+ ->getMock();
// Initiate mock scanning with configuration setting to do nothing on
// scanning error and using unixsocket.
$configmap = array(array('clamfailureonupload', 'donothing'), array('runningmethod', 'unixsocket'));
$antivirus->method('get_config')->will($this->returnValueMap($configmap));
- // Configure scan_data_execute_unixsocket method stubs to behave as if
+ // Configure scan_data_execute_socket method stubs to behave as if
// there is a scanning error (SCAN_RESULT_ERROR).
- $antivirus->method('scan_data_execute_unixsocket')->willReturn(2);
+ $antivirus->method('scan_data_execute_socket')->willReturn(2);
$antivirus->method('get_scanning_notice')->willReturn('someerror');
// Set expectation that message_admins is called.
// Run mock scanning.
$this->assertEquals(2, $antivirus->scan_data(''));
+
+ // Re-initiate mock scanning with configuration setting to do nothing on
+ // scanning error and using tcsocket.
+ $configmap = array(array('clamfailureonupload', 'donothing'), array('runningmethod', 'tcpsocket'));
+ $antivirus->method('get_config')->will($this->returnValueMap($configmap));
+
+ // Set expectation that message_admins is called.
+ $antivirus->expects($this->atLeastOnce())->method('message_admins')->with($this->equalTo('someerror'));
+
+ // Run mock scanning.
+ $this->assertEquals(2, $antivirus->scan_data(''));
}
public function test_scan_data_error_actlikevirus() {
$methods = array(
- 'scan_data_execute_unixsocket',
+ 'scan_data_execute_socket',
'message_admins',
'get_config',
'get_scanning_notice',
);
$antivirus = $this->getMockBuilder('\antivirus_clamav\scanner')
- ->setMethods($methods)
- ->getMock();
+ ->setMethods($methods)
+ ->getMock();
// Initiate mock scanning with configuration setting to act like virus on
// scanning error and using unixsocket.
$configmap = array(array('clamfailureonupload', 'actlikevirus'), array('runningmethod', 'unixsocket'));
$antivirus->method('get_config')->will($this->returnValueMap($configmap));
- // Configure scan_data_execute_unixsocket method stubs to behave as if
+ // Configure scan_data_execute_socket method stubs to behave as if
// there is a scanning error (SCAN_RESULT_ERROR).
- $antivirus->method('scan_data_execute_unixsocket')->willReturn(2);
+ $antivirus->method('scan_data_execute_socket')->willReturn(2);
$antivirus->method('get_scanning_notice')->willReturn('someerror');
// Set expectation that message_admins is called.
// Run mock scanning, we expect SCAN_RESULT_FOUND since configuration
// require us to act like virus.
$this->assertEquals(1, $antivirus->scan_data(''));
+
+ // Re-initiate mock scanning with configuration setting to act like virus on
+ // scanning error and using tcpsocket.
+ $configmap = array(array('clamfailureonupload', 'actlikevirus'), array('runningmethod', 'tcpsocket'));
+ $antivirus->method('get_config')->will($this->returnValueMap($configmap));
+
+ // Set expectation that message_admins is called.
+ $antivirus->expects($this->atLeastOnce())->method('message_admins')->with($this->equalTo('someerror'));
+
+ // Run mock scanning, we expect SCAN_RESULT_FOUND since configuration
+ // require us to act like virus.
+ $this->assertEquals(1, $antivirus->scan_data(''));
}
}
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2019111800; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version = 2019122900; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2019111200; // Requires this Moodle version.
$plugin->component = 'antivirus_clamav'; // Full name of the plugin (used for diagnostics).
const REQUIREMENT_STATUS_OUTDATED = 'outdated';
/** the required dependency is not installed */
const REQUIREMENT_STATUS_MISSING = 'missing';
+ /** the current Moodle version is too high for plugin. */
+ const REQUIREMENT_STATUS_NEWER = 'newer';
/** the required dependency is available in the plugins directory */
const REQUIREMENT_AVAILABLE = 'available';
/** the required dependency is available in the plugins directory */
const REQUIREMENT_UNAVAILABLE = 'unavailable';
+ /** the moodle version is explicitly supported */
+ const VERSION_SUPPORTED = 'supported';
+ /** the moodle version is not explicitly supported */
+ const VERSION_NOT_SUPPORTED = 'notsupported';
+ /** the plugin does not specify supports */
+ const VERSION_NO_SUPPORTS = 'nosupports';
+
/** @var core_plugin_manager holds the singleton instance */
protected static $singletoninstance;
/** @var array of raw plugins information */
*
* @param int $moodleversion the version from version.php.
* @param array $failedplugins to return the list of plugins with non-satisfied dependencies
+ * @param int $branch the current moodle branch, null if not provided
* @return bool true if all the dependencies are satisfied for all plugins.
*/
- public function all_plugins_ok($moodleversion, &$failedplugins = array()) {
-
+ public function all_plugins_ok($moodleversion, &$failedplugins = array(), $branch = null) {
+ global $CFG;
+ if (empty($branch)) {
+ $branch = $CFG->branch;
+ if (empty($branch)) {
+ // During initial install there is no branch set.
+ require($CFG->dirroot . '/version.php');
+ $branch = (int)$branch;
+ // Force CFG->branch to int value during install.
+ $CFG->branch = $branch;
+ }
+ }
$return = true;
foreach ($this->get_plugins() as $type => $plugins) {
foreach ($plugins as $plugin) {
$return = false;
$failedplugins[] = $plugin->component;
}
+
+ if (!$plugin->is_core_compatible_satisfied($branch)) {
+ $return = false;
+ $failedplugins[] = $plugin->component;
+ }
}
}
}
$reqs = array();
- $reqcore = $this->resolve_core_requirements($plugin, $moodleversion);
+ $reqcore = $this->resolve_core_requirements($plugin, $moodleversion, $moodlebranch);
if (!empty($reqcore)) {
$reqs['core'] = $reqcore;
* @param string|int|double $moodleversion moodle core branch to check against
* @return stdObject
*/
- protected function resolve_core_requirements(\core\plugininfo\base $plugin, $moodleversion) {
+ protected function resolve_core_requirements(\core\plugininfo\base $plugin, $moodleversion, $moodlebranch) {
$reqs = (object)array(
'hasver' => null,
'status' => null,
'availability' => null,
);
-
$reqs->hasver = $moodleversion;
if (empty($plugin->versionrequires)) {
$reqs->status = self::REQUIREMENT_STATUS_OUTDATED;
}
+ // Now check if there is an explicit incompatible, supersedes requires.
+ if (isset($plugin->pluginincompatible) && $plugin->pluginincompatible != null) {
+ if (!$plugin->is_core_compatible_satisfied($moodlebranch)) {
+
+ $reqs->status = self::REQUIREMENT_STATUS_NEWER;
+ }
+ }
+
return $reqs;
}
return $reqs;
}
+ /**
+ * Helper method to determine whether a moodle version is explicitly supported.
+ *
+ * @param \core\plugininfo\base $plugin the plugin we are checking
+ * @param int $branch the moodle branch to check support for
+ * @return bool
+ */
+ public function check_explicitly_supported($plugin, $branch) : bool {
+ // Check for correctly formed supported.
+ if (isset($plugin->pluginsupported)) {
+ // Broken apart for readability.
+ $error = false;
+ if (!is_array($plugin->pluginsupported)) {
+ $error = true;
+ }
+ if (!is_int($plugin->pluginsupported[0]) || !is_int($plugin->pluginsupported[1])) {
+ $error = true;
+ }
+ if (count($plugin->pluginsupported) != 2) {
+ $error = true;
+ }
+ if ($error) {
+ throw new coding_exception(get_string('err_supported_syntax', 'core_plugin'));
+ }
+ }
+
+ if (isset($plugin->pluginsupported) && $plugin->pluginsupported != null) {
+ if ($plugin->pluginsupported[0] <= $branch && $branch <= $plugin->pluginsupported[1]) {
+ return self::VERSION_SUPPORTED;
+ } else {
+ return self::VERSION_NOT_SUPPORTED;
+ }
+ } else {
+ // If supports aren't specified, but incompatible is, return not supported if not incompatible.
+ if (!isset($plugin->pluginsupported) && isset($plugin->pluginincompatible) && !empty($plugin->pluginincompatible)) {
+ if (!$plugin->is_core_compatible_satisfied($branch)) {
+ return self::VERSION_NOT_SUPPORTED;
+ }
+ }
+ return self::VERSION_NO_SUPPORTS;
+ }
+ }
+
/**
* Is the given plugin version available in the plugins directory?
*
public $versiondb;
/** @var int|float|string required version of Moodle core */
public $versionrequires;
+ /** @var array explicitly supported branches of Moodle core */
+ public $pluginsupported;
+ /** @var int first incompatible branch of Moodle core */
+ public $pluginincompatible;
/** @var mixed human-readable release information */
public $release;
/** @var array other plugins that this one depends on, lazy-loaded by {@link get_other_required_plugins()} */
$this->versiondisk = null;
$this->versionrequires = null;
+ $this->pluginsupported = null;
+ $this->pluginincompatible = null;
$this->dependencies = array();
if (!isset($versions[$this->name])) {
if (isset($plugin->dependencies)) {
$this->dependencies = $plugin->dependencies;
}
+
+ // Check that supports and incompatible are wellformed, exception otherwise.
+ if (isset($plugin->supported)) {
+ // Checks for structure of supported.
+ $isint = (is_int($plugin->supported[0]) && is_int($plugin->supported[1]));
+ $isrange = ($plugin->supported[0] <= $plugin->supported[1] && count($plugin->supported) == 2);
+
+ if (is_array($plugin->supported) && $isint && $isrange) {
+ $this->pluginsupported = $plugin->supported;
+ } else {
+ throw new coding_exception('Incorrect syntax in $plugin->supported in '."$this->name");
+ }
+ }
+
+ if (isset($plugin->incompatible) && $plugin->incompatible !== null) {
+ if ((ctype_digit($plugin->incompatible) || is_int($plugin->incompatible)) && (int) $plugin->incompatible > 0) {
+ $this->pluginincompatible = intval($plugin->incompatible);
+ } else {
+ throw new coding_exception('Incorrect syntax in $plugin->incompatible in '."$this->name");
+ }
+ }
+
}
/**
}
}
+ /**
+ * Returns true if the the given moodle branch is not stated incompatible with the plugin
+ *
+ * @param int $branch the moodle branch number
+ * @return bool true if not incompatible with moodle branch
+ */
+ public function is_core_compatible_satisfied(int $branch) : bool {
+ if (!empty($this->pluginincompatible) && ($branch >= $this->pluginincompatible)) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+
/**
* Returns the status of the plugin
*
$records = self::ensure_adhoc_task_qos($records);
$cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
- if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
- throw new \moodle_exception('locktimeout');
- }
$skipclasses = array();
}
}
+ // The global cron lock is under the most contention so request it
+ // as late as possible and release it as soon as possible.
+ if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
+ $lock->release();
+ throw new \moodle_exception('locktimeout');
+ }
+
$task->set_lock($lock);
if (!$task->is_blocking()) {
$cronlock->release();
}
}
- // No tasks.
- $cronlock->release();
return null;
}
global $DB;
$cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
- if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
- throw new \moodle_exception('locktimeout');
- }
-
$where = "(lastruntime IS NULL OR lastruntime < :timestart1)
AND (nextruntime IS NULL OR nextruntime < :timestart2)
AND disabled = 0
continue;
}
+ // The global cron lock is under the most contention so request it
+ // as late as possible and release it as soon as possible.
+ if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
+ $lock->release();
+ throw new \moodle_exception('locktimeout');
+ }
+
if (!$task->is_blocking()) {
$cronlock->release();
} else {
}
}
- // No tasks.
- $cronlock->release();
return null;
}
if (CLI_SCRIPT) {
fwrite(STDERR, print_r($object, true));
fwrite(STDERR, PHP_EOL);
+ } else if (AJAX_SCRIPT) {
+ foreach (explode("\n", print_r($object, true)) as $line) {
+ error_log($line);
+ }
} else {
echo html_writer::tag('pre', s(print_r($object, true)), array('class' => 'notifytiny'));
}
)
),
+ 'moodle/site:viewanonymousevents' => array(
+
+ 'riskbitmask' => RISK_PERSONAL,
+
+ 'captype' => 'read',
+ 'contextlevel' => CONTEXT_MODULE,
+ 'archetypes' => array(
+ 'manager' => CAP_ALLOW,
+ )
+ ),
+
'moodle/site:viewfullnames' => array(
'captype' => 'read',
'backup_version' => 2008111700,
'backup_release' => '2.0 dev',
'mnet_dispatcher_mode' => 'off',
- 'sessiontimeout' => 7200, // must be present during roles installation
- 'stringfilters' => '', // These two are managed in a strange way by the filters
+ 'sessiontimeout' => 8 * 60 * 60, // Must be present during roles installation.
+ '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' => '',
<INDEX NAME="uuid" UNIQUE="false" FIELDS="uuid"/>
<INDEX NAME="type-timesort" UNIQUE="false" FIELDS="type, timesort"/>
<INDEX NAME="groupid-courseid-categoryid-visible-userid" UNIQUE="false" FIELDS="groupid, courseid, categoryid, visible, userid" COMMENT="used for calendar view"/>
+ <INDEX NAME="eventtype" UNIQUE="false" FIELDS="eventtype"/>
+ <INDEX NAME="modulename-instance" UNIQUE="false" FIELDS="modulename, instance"/>
</INDEXES>
</TABLE>
<TABLE NAME="cache_filters" COMMENT="For keeping information about cached data">
upgrade_main_savepoint(true, 2019122000.01);
}
+ if ($oldversion < 2020010900.02) {
+ $table = new xmldb_table('event');
+
+ // This index will improve the performance when the Events API retrieves category and group events.
+ $index = new xmldb_index('eventtype', XMLDB_INDEX_NOTUNIQUE, ['eventtype']);
+ if (!$dbman->index_exists($table, $index)) {
+ $dbman->add_index($table, $index);
+ }
+
+ // This index improves the performance of backups, deletion and visibilty changes on activities.
+ $index = new xmldb_index('modulename-instance', XMLDB_INDEX_NOTUNIQUE, ['modulename', 'instance']);
+ if (!$dbman->index_exists($table, $index)) {
+ $dbman->add_index($table, $index);
+ }
+
+ upgrade_main_savepoint(true, 2020010900.02);
+ }
return true;
}
// Check for non-empty text.
if (Y.Lang.trim(node.get('text')) !== '') {
- foreground = node.getComputedStyle('color');
- background = node.getComputedStyle('backgroundColor');
+ foreground = Y.Color.fromArray(
+ this._getComputedBackgroundColor(node, node.getComputedStyle('color')),
+ Y.Color.TYPES.RGBA
+ );
+ background = Y.Color.fromArray(this._getComputedBackgroundColor(node), Y.Color.TYPES.RGBA);
lum1 = this._getLuminanceFromCssColor(foreground);
lum2 = this._getLuminanceFromCssColor(background);
* Generate the HTML that lists the found warnings.
*
* @method _addWarnings
- * @param {Node} A Node to append the html to.
+ * @param {Node} list Node to append the html to.
* @param {String} description Description of this failure.
* @param {array} nodes An array of failing nodes.
* @param {boolean} imagewarnings true if the warnings are related to images, false if text.
b1 = part1(color[2]);
return 0.2126 * r1 + 0.7152 * g1 + 0.0722 * b1;
+ },
+
+ /**
+ * Get the computed RGB converted to full alpha value, considering the node hierarchy.
+ *
+ * @method _getComputedBackgroundColor
+ * @param {Node} node
+ * @param {String} color The initial colour. If not specified, fetches the backgroundColor from the node.
+ * @return {Array} Colour in Array form (RGBA)
+ * @private
+ */
+ _getComputedBackgroundColor: function(node, color) {
+ color = color || node.getComputedStyle('backgroundColor');
+
+ if (color.toLowerCase() === 'transparent') {
+ // Y.Color doesn't handle 'transparent' properly.
+ color = 'rgba(1, 1, 1, 0)';
+ }
+
+ // Convert the colour to its constituent parts in RGBA format, then fetch the alpha.
+ var colorParts = Y.Color.toArray(color);
+ var alpha = colorParts[3];
+
+ if (alpha === 1) {
+ // If the alpha of the background is already 1, then the parent background colour does not change anything.
+ return colorParts;
+ }
+
+ // Fetch the computed background colour of the parent and use it to calculate the RGB of this item.
+ var parentColor = this._getComputedBackgroundColor(node.get('parentNode'));
+ return [
+ // RGB = (alpha * R|G|B) + (1 - alpha * solid parent colour).
+ (1 - alpha) * parentColor[0] + alpha * colorParts[0],
+ (1 - alpha) * parentColor[1] + alpha * colorParts[1],
+ (1 - alpha) * parentColor[2] + alpha * colorParts[2],
+ // We always return a colour with full alpha.
+ 1
+ ];
}
});
/**
* The required version of the python package that performs all calculations.
*/
- const REQUIRED_PIP_PACKAGE_VERSION = '2.3.0';
+ const REQUIRED_PIP_PACKAGE_VERSION = '2.3.1';
/**
* The python package is installed in a server.
// Start session and prepare global $SESSION, $USER.
if (empty($CFG->sessiontimeout)) {
- $CFG->sessiontimeout = 7200;
+ $CFG->sessiontimeout = 8 * 60 * 60;
}
\core\session\manager::start();
return null;
}
+
+ /**
+ * Adds fake plugin information from record.
+ *
+ * @param stdClass $record
+ * @return void
+ */
+ public function add_fake_plugin_info($record): void {
+ $this->load_present_plugins();
+
+ $this->presentplugins[$record->type][$record->name] = $record;
+ }
}
$this->displayname = 'Testable fake pluginfo instance';
}
- public function load_disk_version() {
- $this->versiondisk = null;
- $this->versionrequires = null;
- $this->dependencies = array();
- }
-
public function load_db_version() {
$this->versiondb = null;
}
'testable_plugininfo_base', $pluginman);
$pluginfo->versiondisk = null;
$this->assertEmpty($pluginman->resolve_requirements($pluginfo, 2015110900, 30));
+
+ // Test plugin fails for incompatible version.
+ $pluginfo = testable_plugininfo_base::fake_plugin_instance('fake', '/dev/null', 'two', '/dev/null/fake',
+ 'testable_plugininfo_base', $pluginman);
+ $pluginfo->versiondisk = 2015060600;
+ $pluginfo->pluginincompatible = 30;
+ $reqs = $pluginman->resolve_requirements($pluginfo, 2015110900, 30);
+ $this->assertEquals($pluginman::REQUIREMENT_STATUS_NEWER, $reqs['core']->status);
+
+ // Test no failure for no incompatible version.
+ $pluginfo->pluginincompatible = 30;
+ $reqs = $pluginman->resolve_requirements($pluginfo, 2015110900, 29);
+ $this->assertEquals($pluginman::REQUIREMENT_STATUS_OK, $reqs['core']->status);
}
public function test_missing_dependencies() {
$this->assertInstanceOf('\core\update\remote_info', $misdeps['foo_bar']);
$this->assertEquals(2015100500, $misdeps['foo_bar']->version->version);
}
+
+ /**
+ * Tests for check_explicitly_supported function to ensure that versions are correctly reported.
+ *
+ * @dataProvider check_explicitly_supported_provider
+ * @param array|null $supported Supported versions to inject
+ * @param string|int|null $incompatible Incompatible version to inject.
+ * @param int $version Version to test
+ * @param int $expected
+ * @return void
+ */
+ public function test_explicitly_supported($supported, $incompatible, $version, $expected): void {
+ $pluginman = testable_core_plugin_manager::instance();
+
+ // Prepare a fake pluginfo instance.
+ $plugininfo = new testable_plugininfo_base();
+ $plugininfo->type = 'fake';
+ $plugininfo->typerootdir = '/dev/null';
+ $plugininfo->name = 'example';
+ $plugininfo->rootdir = '/dev/null/fake';
+ $plugininfo->pluginman = $pluginman;
+ $plugininfo->versiondisk = 2015060600;
+ $plugininfo->supported = $supported;
+ $plugininfo->incompatible = $incompatible;
+
+ $pluginman->add_fake_plugin_info($plugininfo);
+
+ $plugininfo->load_disk_version();
+
+ $this->assertEquals($expected, $pluginman->check_explicitly_supported($plugininfo, $version));
+ }
+
+ /**
+ * Data provider for check_explicitly_supported with a range of correctly defined version support values.
+ *
+ * @return array
+ */
+ public function check_explicitly_supported_provider(): array {
+ return [
+ 'Range, branch in support, lowest' => [
+ 'supported' => [29, 31],
+ 'incompatible' => null,
+ 'version' => 29,
+ 'expected' => core_plugin_manager::VERSION_SUPPORTED,
+ ],
+ 'Range, branch in support, mid' => [
+ 'supported' => [29, 31],
+ 'incompatible' => null,
+ 'version' => 30,
+ 'expected' => core_plugin_manager::VERSION_SUPPORTED,
+ ],
+ 'Range, branch in support, highest' => [
+ 'supported' => [29, 31],
+ 'incompatible' => null,
+ 'version' => 31,
+ 'expected' => core_plugin_manager::VERSION_SUPPORTED,
+ ],
+
+ 'Range, branch not in support, high' => [
+ 'supported' => [29, 31],
+ 'incompatible' => null,
+ 'version' => 32,
+ 'expected' => core_plugin_manager::VERSION_NOT_SUPPORTED,
+ ],
+ 'Range, branch not in support, low' => [
+ 'supported' => [29, 31],
+ 'incompatible' => null,
+ 'version' => 28,
+ 'expected' => core_plugin_manager::VERSION_NOT_SUPPORTED,
+ ],
+ 'Range, incompatible, high.' => [
+ 'supported' => [29, 31],
+ 'incompatible' => 32,
+ 'version' => 33,
+ 'expected' => core_plugin_manager::VERSION_NOT_SUPPORTED,
+ ],
+ 'Range, incompatible, low.' => [
+ 'supported' => [29, 31],
+ 'incompatible' => 32,
+ 'version' => 31,
+ 'expected' => core_plugin_manager::VERSION_SUPPORTED,
+ ],
+ 'Range, incompatible, equal.' => [
+ 'supported' => [29, 31],
+ 'incompatible' => 32,
+ 'version' => 32,
+ 'expected' => core_plugin_manager::VERSION_NOT_SUPPORTED,
+ ],
+ 'No supports' => [
+ 'supported' => null,
+ 'incompatible' => null,
+ 'version' => 32,
+ 'expected' => core_plugin_manager::VERSION_NO_SUPPORTS,
+ ],
+ 'No supports, but incompatible, older' => [
+ 'supported' => null,
+ 'incompatible' => 30,
+ 'version' => 32,
+ 'expected' => core_plugin_manager::VERSION_NOT_SUPPORTED,
+ ],
+ 'No supports, but incompatible, equal' => [
+ 'supported' => null,
+ 'incompatible' => 32,
+ 'version' => 32,
+ 'expected' => core_plugin_manager::VERSION_NOT_SUPPORTED,
+ ],
+ 'No supports, but incompatible, newer' => [
+ 'supported' => null,
+ 'incompatible' => 34,
+ 'version' => 32,
+ 'expected' => core_plugin_manager::VERSION_NO_SUPPORTS,
+ ],
+ ];
+ }
}
--- /dev/null
+<?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/>.
+
+/**
+ * Unit tests for plugin base class.
+ *
+ * @package core
+ * @copyright 2019 Andrew Nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+declare(strict_types = 1);
+namespace core\plugininfo;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core_plugin_manager;
+use testable_core_plugin_manager;
+use testable_plugininfo_base;
+
+
+/**
+ * Tests of the basic API of the plugin manager.
+ */
+class base_testcase extends \advanced_testcase {
+
+ /**
+ * Setup to ensure that fixtures are loaded.
+ */
+ public static function setUpBeforeClass(): void {
+ global $CFG;
+
+ require_once($CFG->dirroot.'/lib/tests/fixtures/testable_plugin_manager.php');
+ require_once($CFG->dirroot.'/lib/tests/fixtures/testable_plugininfo_base.php');
+ }
+
+ /**
+ * Tear down the testable plugin manager singleton between tests.
+ */
+ public function tearDown() {
+ // The caches of the testable singleton must be reset explicitly. It is
+ // safer to kill the whole testable singleton at the end of every test.
+ testable_core_plugin_manager::reset_caches();
+ }
+
+ /**
+ * Test the load_disk_version function to check that it handles a variety of invalid supported fields.
+ *
+ * @dataProvider load_disk_version_invalid_supported_version_provider
+ * @param array|null $supported Supported versions to inject
+ * @param string|int|null $incompatible Incompatible version to inject.
+ * @param int $version Version to test
+ */
+ public function test_load_disk_version_invalid_supported_version($supported, $incompatible, $version): void {
+ $pluginman = testable_core_plugin_manager::instance();
+
+ // Prepare a fake plugininfo instance.
+ $plugininfo = new testable_plugininfo_base();
+ $plugininfo->type = 'fake';
+ $plugininfo->typerootdir = '/dev/null';
+ $plugininfo->name = 'example';
+ $plugininfo->rootdir = '/dev/null/fake';
+ $plugininfo->pluginman = $pluginman;
+ $plugininfo->versiondisk = 2015060600;
+ $plugininfo->supported = $supported;
+ $plugininfo->incompatible = $incompatible;
+
+ $pluginman->add_fake_plugin_info($plugininfo);
+
+ $this->expectException(\coding_exception::class);
+ $this->expectExceptionMessage('Incorrect syntax in $plugin->supported in example');
+ $plugininfo->load_disk_version();
+ }
+
+ /**
+ * Data provider for the load_disk_version tests for testing with invalid supported fields.
+ *
+ * @return array
+ */
+ public function load_disk_version_invalid_supported_version_provider(): array {
+ return [
+ 'Invalid supported range.' => [
+ 'supported' => [31, 29],
+ 'incompatible' => null,
+ 'version' => 32,
+ ],
+ 'Explicit list, low' => [
+ 'supported' => [29, 30, 31, 32],
+ 'incompatible' => null,
+ 'version' => 28,
+ ],
+ 'Explicit list, high' => [
+ 'supported' => [29, 30, 31, 32],
+ 'incompatible' => null,
+ 'version' => 33,
+ ],
+ 'Explicit list, in list' => [
+ 'supported' => [29, 30, 31, 32, 33],
+ 'incompatible' => null,
+ 'version' => 31,
+ ],
+ 'Explicit list, missing value, unsupported' => [
+ 'supported' => [29, 30, 32],
+ 'incompatible' => null,
+ 'version' => 31,
+ ],
+ 'Explicit list, missing value, supported' => [
+ 'supported' => [29, 30, 32],
+ 'incompatible' => null,
+ 'version' => 30,
+ ],
+ ];
+ }
+
+ /**
+ * Test the load_disk_version function to check that it handles a variety of invalid incompatible fields.
+ *
+ * @dataProvider load_disk_version_invalid_incompatible_version_provider
+ * @param mixed $incompatible
+ */
+ public function test_load_disk_version_invalid_incompatible_version($incompatible): void {
+ $pluginman = testable_core_plugin_manager::instance();
+
+ // Prepare a fake plugininfo instance.
+ $plugininfo = new testable_plugininfo_base();
+ $plugininfo->type = 'fake';
+ $plugininfo->typerootdir = '/dev/null';
+ $plugininfo->name = 'example';
+ $plugininfo->rootdir = '/dev/null/fake';
+ $plugininfo->pluginman = $pluginman;
+ $plugininfo->versiondisk = 2015060600;
+ $plugininfo->incompatible = $incompatible;
+
+ $pluginman->add_fake_plugin_info($plugininfo);
+
+ $this->expectException(\coding_exception::class);
+ $this->expectExceptionMessage('Incorrect syntax in $plugin->incompatible in example');
+ $plugininfo->load_disk_version();
+ }
+
+ /**
+ * Data provider for the load_disk_version tests for testing with invalid incompatible fields.
+ *
+ * @return array
+ */
+ public function load_disk_version_invalid_incompatible_version_provider(): array {
+ return [
+ [[38]],
+ [['38']],
+ [3.8],
+ ['3.8'],
+ [''],
+ ['somestring'],
+ ];
+
+ }
+
+ /**
+ * Test the load_disk_version function to check that it handles a range of correct supported and incompatible field
+ * definitions.
+ *
+ * @dataProvider test_load_disk_version_branch_supports_provider
+ * @param array|null $supported Supported versions to inject
+ * @param string|int|null $incompatible Incompatible version to inject.
+ * @param int $version Version to test
+ */
+ public function test_load_disk_version_branch_supports($supported, $incompatible, $version): void {
+ $pluginman = testable_core_plugin_manager::instance();
+
+ // Prepare a fake plugininfo instance.
+ $plugininfo = new testable_plugininfo_base();
+ $plugininfo->type = 'fake';
+ $plugininfo->typerootdir = '/dev/null';
+ $plugininfo->name = 'example';
+ $plugininfo->rootdir = '/dev/null/fake';
+ $plugininfo->pluginman = $pluginman;
+ $plugininfo->versiondisk = 2015060600;
+ $plugininfo->supported = $supported;
+ $plugininfo->incompatible = $incompatible;
+
+ $pluginman->add_fake_plugin_info($plugininfo);
+
+ $plugininfo->load_disk_version();
+
+ $this->assertEquals($supported, $plugininfo->supported);
+ $this->assertEquals($incompatible, $plugininfo->incompatible);
+ }
+
+ /**
+ * Test cases for tests of load_disk_version for testing the supported/incompatible fields.
+ *
+ * @return array
+ */
+ public function test_load_disk_version_branch_supports_provider(): array {
+ return [
+ 'Range, branch in support, lowest' => [
+ 'supported' => [29, 31],
+ 'incompatible' => null,
+ 'version' => 29,
+ ],
+ 'Range, branch in support, mid' => [
+ 'supported' => [29, 31],
+ 'incompatible' => null,
+ 'version' => 30,
+ ],
+ 'Range, branch in support, highest' => [
+ 'supported' => [29, 31],
+ 'incompatible' => null,
+ 'version' => 31,
+ ],
+
+ 'Range, branch not in support, high' => [
+ 'supported' => [29, 31],
+ 'incompatible' => null,
+ 'version' => 32,
+ ],
+ 'Range, branch not in support, low' => [
+ 'supported' => [29, 31],
+ 'incompatible' => null,
+ 'version' => 28,
+ ],
+ 'Range, incompatible, high.' => [
+ 'supported' => [29, 31],
+ 'incompatible' => 32,
+ 'version' => 33,
+ ],
+ 'Range, incompatible, low.' => [
+ 'supported' => [29, 31],
+ 'incompatible' => 32,
+ 'version' => 31,
+ ],
+ 'Range, incompatible, equal.' => [
+ 'supported' => [29, 31],
+ 'incompatible' => 32,
+ 'version' => 32,
+ ],
+ 'No supports' => [
+ 'supported' => null,
+ 'incompatible' => null,
+ 'version' => 32,
+ ],
+ 'No supports, but incompatible, older' => [
+ 'supported' => null,
+ 'incompatible' => 30,
+ 'version' => 32,
+ ],
+ 'No supports, but incompatible, equal' => [
+ 'supported' => null,
+ 'incompatible' => 32,
+ 'version' => 32,
+ ],
+ 'No supports, but incompatible, newer' => [
+ 'supported' => null,
+ 'incompatible' => 34,
+ 'version' => 32,
+ ],
+ 'String incompatible' => [
+ 'supported' => null,
+ 'incompatible' => '34',
+ 'version' => 32,
+ ],
+ 'Empty incompatible' => [
+ 'supported' => null,
+ 'incompatible' => null,
+ 'version' => 32,
+ ],
+ ];
+ }
+}
which means auto-detecting number of decimal points.
* plagiarism_save_form_elements() has been deprecated. Please use {plugin name}_coursemodule_edit_post_actions() instead.
* plagiarism_get_form_elements_module() has been deprecated. Please use {plugin name}_coursemodule_standard_elements() instead.
+* Changed default sessiontimeout to 8 hours to cover most normal working days
+* Plugins can now explicitly declare supported and incompatible Moodle versions in version.php
+ - $plugin->supported = [37,39];
+ supported takes an array of ascending numbers, that correspond to a range of branch numbers of supported versions, inclusive.
+ Moodle versions that are outside of this range will produce a message notifying at install time, but will allow for installation.
+ - $plugin->incompatible = 36;
+ incompatible takes a single int corresponding to the first incompatible branch. Any Moodle versions including and
+ above this will be prevented from installing the plugin, and a message will be given when attempting installation.
=== 3.8 ===
* Add CLI option to notify all cron tasks to stop: admin/cli/cron.php --stop
}
}
+/**
+ * Exception thrown when attempting to install a plugin that declares incompatibility with moodle version
+ *
+ * @package core
+ * @subpackage upgrade
+ * @copyright 2019 Peter Burnett <peterburnett@catalyst-au.net>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class plugin_incompatible_exception extends moodle_exception {
+ /**
+ * Constructor function for exception
+ *
+ * @param \core\plugininfo\base $plugin The plugin causing the exception
+ * @param int $pluginversion The version of the plugin causing the exception
+ */
+ public function __construct($plugin, $pluginversion) {
+ global $CFG;
+ $a = new stdClass();
+ $a->pluginname = $plugin;
+ $a->pluginversion = $pluginversion;
+ $a->moodleversion = $CFG->branch;
+
+ parent::__construct('pluginunsupported', 'error', "$CFG->wwwroot/$CFG->admin/index.php", $a);
+ }
+}
+
/**
* @package core
* @subpackage upgrade
}
}
+ // Throw exception if plugin is incompatible with moodle version.
+ if (!empty($plugin->incompatible)) {
+ if ($CFG->branch <= $plugin->incompatible) {
+ throw new plugin_incompatible_exception($component, $plugin->version);
+ }
+ }
+
// try to recover from interrupted install.php if needed
if (file_exists($fullplug.'/db/install.php')) {
if (get_config($plugin->fullname, 'installrunning')) {
}
}
- // Take off 15px top and bottom for borders, plus 40px each for the title and button area before setting the
- // new max-height
+ // Take off 15px top and bottom for borders, plus 69px for the title and 57px for the
+ // button area before setting the new max-height.
totalheight = newheight;
- newheight = newheight - (15 + 15 + 40 + 40);
+ newheight = newheight - (69 + 57 + 15 + 15);
dialogue.setStyle('maxHeight', newheight + 'px');
var dialogueheight = bb.getStyle('height');
if (dialogueheight < this.get('baseheight')) {
dialogueheight = this.get('baseheight');
dialogue.setStyle('height', dialogueheight + 'px');
+ } else {
+ dialogue.setStyle('height', 'auto');
}
this.panel.centerDialogue();
// Only ever send a max of one days worth of updates.
$yesterday = time() - (24 * 3600);
$timenow = time();
- $lastcron = $DB->get_field('modules', 'lastcron', array('name' => 'assign'));
+ $lastruntime = $DB->get_field('task_scheduled', 'lastruntime', array('component' => 'mod_assign'));
// Collect all submissions that require mailing.
// Submissions are included if all are true:
$sql = 'SELECT id
FROM {assign}
WHERE
- allowsubmissionsfromdate >= :lastcron AND
+ allowsubmissionsfromdate >= :lastruntime AND
allowsubmissionsfromdate <= :timenow AND
alwaysshowdescription = 0';
- $params = array('lastcron' => $lastcron, 'timenow' => $timenow);
+ $params = array('lastruntime' => $lastruntime, 'timenow' => $timenow);
$newlyavailable = $DB->get_records_sql($sql, $params);
foreach ($newlyavailable as $record) {
$cm = get_coursemodule_from_instance('assign', $record->id, 0, false, MUST_EXIST);
// Additional attribs to handle collapsible div.
['data-toggle' => 'collapse', 'data-target' => "#collapseAddForm"]);
$buttonarray[] = &$mform->createElement('submit', 'advancedadddiscussion',
- get_string('advanced'), null, null, ['customclassoverride' => 'btn-link']);
+ get_string('showadvancededitor'), null, null, ['customclassoverride' => 'btn-link']);
$mform->addGroup($buttonarray, 'buttonar', '', array(' '), false);
$mform->closeHeaderBefore('buttonar');
upgrade_mod_savepoint(true, 2019100109, 'forum');
}
+
// Automatically generated Moodle v3.8.0 release upgrade line.
// Put any upgrade step following this.
+ if ($oldversion < 2019111801) {
+ $sql = "SELECT d.id AS discussionid, p.userid AS correctuser
+ FROM {forum_discussions} d
+ INNER JOIN {forum_posts} p ON p.id = d.firstpost
+ WHERE d.userid <> p.userid";
+ $recordset = $DB->get_recordset_sql($sql);
+ foreach ($recordset as $record) {
+ $object = new stdClass();
+ $object->id = $record->discussionid;
+ $object->userid = $record->correctuser;
+ $DB->update_record('forum_discussions', $object);
+ }
+
+ $recordset->close();
+
+ // Forum savepoint reached.
+ upgrade_mod_savepoint(true, 2019111801, 'forum');
+ }
+
return true;
}
$string['sendstudentnotificationsdefault_help'] = 'Set the default value for the "Notify students" checkbox on the grading form.';
$string['settings'] = 'Settings';
$string['shortpost'] = 'Short post';
-$string['showingcountoftotaldiscussions'] = 'Showing {$a->count} of {$a->total} discussions';
+$string['showingcountoftotaldiscussions'] = 'List of discussions. Showing {$a->count} of {$a->total} discussions';
$string['showgraderpanel'] = 'Show grader panel';
$string['showpreviousrepliescount'] = 'Show previous replies ({$a})';
$string['showsubscribers'] = 'Show/edit current subscribers';
$newdiscussion->forum = $discussion->forum;
$newdiscussion->name = $name;
$newdiscussion->firstpost = $post->id;
- $newdiscussion->userid = $discussion->userid;
+ $newdiscussion->userid = $post->userid;
$newdiscussion->groupid = $discussion->groupid;
$newdiscussion->assessed = $discussion->assessed;
$newdiscussion->usermodified = $post->userid;
{{{ pagination }}}
{{/discussion_top_pagination}}
{{$discussion_list_output}}
- <span id="discussion-table-description-{{uniqid}}" class="sr-only">
- {{#str}} showingcountoftotaldiscussions, mod_forum, {"count": "{{visiblediscussioncount}}", "total":"{{totaldiscussioncount}}"} {{/str}}
- </span>
- <table
- class="table discussion-list"
- aria-label='{{#str}} showingcountoftotaldiscussions, mod_forum, {"count": "{{visiblediscussioncount}}", "total":"{{totaldiscussioncount}}"} {{/str}}'
- aria-describedby="discussion-table-description-{{uniqid}}"
- >
+ <table class="table discussion-list">
+ <caption id="discussion-table-description-{{uniqid}}" class="sr-only">
+ {{#str}} showingcountoftotaldiscussions, mod_forum, {"count": "{{visiblediscussioncount}}", "total":"{{totaldiscussioncount}}"} {{/str}}
+ </caption>
{{$discussion_list_header}}
<thead>
<tr>
// Navigate to forum.
$this->execute('behat_general::click_link', $this->escape($forumname));
$this->execute('behat_general::click_link', $buttonstr);
- $this->execute('behat_forms::press_button', get_string('advanced'));
+ $this->execute('behat_forms::press_button', get_string('showadvancededitor'));
$this->fill_new_discussion_form($table);
}
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2019111800; // The current module version (Date: YYYYMMDDXX)
+$plugin->version = 2019111801; // The current module version (Date: YYYYMMDDXX)
$plugin->requires = 2019111200; // Requires this Moodle version
$plugin->component = 'mod_forum'; // Full name of the plugin (used for diagnostics)
if (!($this->filterparams->logreader instanceof logstore_legacy\log\store)) {
// Filter out anonymous actions, this is N/A for legacy log because it never stores them.
- $joins[] = "anonymous = 0";
+ if ($this->filterparams->modid) {
+ $context = context_module::instance($this->filterparams->modid);
+ } else {
+ $context = context_course::instance($this->filterparams->courseid);
+ }
+ if (!has_capability('moodle/site:viewanonymousevents', $context)) {
+ $joins[] = "anonymous = 0";
+ }
}
$selector = implode(' AND ', $joins);
$filter = new \stdClass();
if (!empty($this->course)) {
$filter->courseid = $this->course->id;
+ $context = context_course::instance($filter->courseid);
+ if (!has_capability('moodle/site:viewanonymousevents', $context)) {
+ $filter->anonymous = 0;
+ }
} else {
$filter->courseid = 0;
}
$filter->logreader = $readers[$this->selectedlogreader];
$filter->date = $this->date;
$filter->orderby = $this->order;
- $filter->anonymous = 0;
return $filter;
}
defined('MOODLE_INTERNAL') || die();
-$version = 2020010900.00; // YYYYMMDD = weekly release date of this DEV branch.
+$version = 2020010900.02; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
$release = '3.9dev (Build: 20200109)'; // Human-friendly version name