first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
@@ -0,0 +1,467 @@
<?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/>.
/**
* Helper functions to implement the complex get_user_capability_course function.
*
* @package core
* @copyright 2017 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\access;
defined('MOODLE_INTERNAL') || die();
/**
* Helper functions to implement the complex get_user_capability_course function.
*
* @package core
* @copyright 2017 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class get_user_capability_course_helper {
/**
* Based on the given user's access data (roles) and system role definitions, works out
* an array of capability values at each relevant context for the given user and capability.
*
* This is organised by the effective context path (the one at which the capability takes
* effect) and then by role id. Note, however, that the resulting array only has
* the information that will be needed later. If there are Prohibits present in some
* roles, then they cannot be overridden by other roles or role overrides in lower contexts,
* therefore, such information, if any, is absent from the results.
*
* @param int $userid User id
* @param string $capability Capability e.g. 'moodle/course:view'
* @return array Array of capability constants, indexed by context path and role id
*/
protected static function get_capability_info_at_each_context($userid, $capability) {
// Get access data for user.
$accessdata = get_user_accessdata($userid);
// Get list of roles for user (any location) and information about these roles.
$roleids = [];
foreach ($accessdata['ra'] as $path => $roles) {
foreach ($roles as $roleid) {
$roleids[$roleid] = true;
}
}
$rdefs = get_role_definitions(array_keys($roleids));
// A prohibit in any relevant role prevents the capability
// in that context and all subcontexts. We need to track that.
// Here, the array keys are the paths where there is a prohibit the values are the role id.
$prohibitpaths = [];
// Get data for required capability at each context path where the user has a role that can
// affect it.
$pathroleperms = [];
foreach ($accessdata['ra'] as $rapath => $roles) {
foreach ($roles as $roleid) {
// Get role definition for that role.
foreach ($rdefs[$roleid] as $rdefpath => $caps) {
// Ignore if this override/definition doesn't refer to the relevant cap.
if (!array_key_exists($capability, $caps)) {
continue;
}
// Check a role definition or override above ra.
if (self::path_is_above($rdefpath, $rapath)) {
// Note that $rdefs is sorted by path, so if a more specific override
// exists, it will be processed later and override this one.
$effectivepath = $rapath;
} else if (self::path_is_above($rapath, $rdefpath)) {
$effectivepath = $rdefpath;
} else {
// Not inside an area where the user has the role, so ignore.
continue;
}
// Check for already seen prohibits in higher context. Overrides can't change that.
if (self::any_path_is_above($prohibitpaths, $effectivepath)) {
continue;
}
// This is a releavant role assignment / permission combination. Save it.
if (!array_key_exists($effectivepath, $pathroleperms)) {
$pathroleperms[$effectivepath] = [];
}
$pathroleperms[$effectivepath][$roleid] = $caps[$capability];
// Update $prohibitpaths if necessary.
if ($caps[$capability] == CAP_PROHIBIT) {
// First remove any lower-context prohibits that might have come from other roles.
foreach ($prohibitpaths as $otherprohibitpath => $notused) {
if (self::path_is_above($effectivepath, $otherprohibitpath)) {
unset($prohibitpaths[$otherprohibitpath]);
}
}
$prohibitpaths[$effectivepath] = $roleid;
}
}
}
}
// Finally, if a later role had a higher-level prohibit that an earlier role,
// there may be more bits we can prune - but don't prune the prohibits!
foreach ($pathroleperms as $effectivepath => $roleperms) {
if ($roleid = self::any_path_is_above($prohibitpaths, $effectivepath)) {
unset($pathroleperms[$effectivepath]);
$pathroleperms[$effectivepath][$roleid] = CAP_PROHIBIT;
}
}
return $pathroleperms;
}
/**
* Test if a context path $otherpath is the same as, or underneath, $parentpath.
*
* @param string $parentpath the path of the parent context.
* @param string $otherpath the path of another context.
* @return bool true if $otherpath is underneath (or equal to) $parentpath.
*/
protected static function path_is_above($parentpath, $otherpath) {
return preg_match('~^' . $parentpath . '($|/)~', $otherpath);
}
/**
* Test if a context path $otherpath is the same as, or underneath, any of $prohibitpaths.
*
* @param array $prohibitpaths array keys are context paths.
* @param string $otherpath the path of another context.
* @return int releavant $roleid if $otherpath is underneath (or equal to)
* any of the $prohibitpaths, 0 otherwise (so, can be used as a bool).
*/
protected static function any_path_is_above($prohibitpaths, $otherpath) {
foreach ($prohibitpaths as $prohibitpath => $roleid) {
if (self::path_is_above($prohibitpath, $otherpath)) {
return $roleid;
}
}
return 0;
}
/**
* Calculates a permission tree based on an array of information about role permissions.
*
* The input parameter must be in the format returned by get_capability_info_at_each_context.
*
* The output is the root of a tree of stdClass objects with the fields 'path' (a context path),
* 'allow' (true or false), and 'children' (an array of similar objects).
*
* @param array $pathroleperms Array of permissions
* @return \stdClass Root object of permission tree
*/
protected static function calculate_permission_tree(array $pathroleperms) {
// Considering each discovered context path as an inflection point, evaluate the user's
// permission (based on all roles) at each point.
$pathallows = [];
$mindepth = 1000;
$maxdepth = 0;
foreach ($pathroleperms as $path => $roles) {
$evaluatedroleperms = [];
// Walk up the tree starting from this path.
$innerpath = $path;
while ($innerpath !== '') {
$roles = $pathroleperms[$innerpath];
// Evaluate roles at this path level.
foreach ($roles as $roleid => $perm) {
if (!array_key_exists($roleid, $evaluatedroleperms)) {
$evaluatedroleperms[$roleid] = $perm;
} else {
// The existing one is at a more specific level so it takes precedence
// UNLESS this is a prohibit.
if ($perm == CAP_PROHIBIT) {
$evaluatedroleperms[$roleid] = $perm;
}
}
}
// Go up to next path level (if any).
do {
$innerpath = substr($innerpath, 0, strrpos($innerpath, '/'));
if ($innerpath === '') {
// No higher level data.
break;
}
} while (!array_key_exists($innerpath, $pathroleperms));
}
// If we have an allow from any role, and no prohibits, then user can access this path,
// else not.
$allow = false;
foreach ($evaluatedroleperms as $perm) {
if ($perm == CAP_ALLOW) {
$allow = true;
} else if ($perm == CAP_PROHIBIT) {
$allow = false;
break;
}
}
// Store the result based on path and depth so that we can process in depth order in
// the next step.
$depth = strlen(preg_replace('~[^/]~', '', $path));
$mindepth = min($depth, $mindepth);
$maxdepth = max($depth, $maxdepth);
$pathallows[$depth][$path] = $allow;
}
// Organise into a tree structure, processing in depth order so that we have ancestors
// set up before we encounter their children.
$root = (object)['allow' => false, 'path' => null, 'children' => []];
$nodesbypath = [];
for ($depth = $mindepth; $depth <= $maxdepth; $depth++) {
// Skip any missing depth levels.
if (!array_key_exists($depth, $pathallows)) {
continue;
}
foreach ($pathallows[$depth] as $path => $allow) {
// Value for new tree node.
$leaf = (object)['allow' => $allow, 'path' => $path, 'children' => []];
// Try to find a place to join it on if there is one.
$ancestorpath = $path;
$found = false;
while ($ancestorpath) {
$ancestorpath = substr($ancestorpath, 0, strrpos($ancestorpath, '/'));
if (array_key_exists($ancestorpath, $nodesbypath)) {
$found = true;
break;
}
}
if ($found) {
$nodesbypath[$ancestorpath]->children[] = $leaf;
} else {
$root->children[] = $leaf;
}
$nodesbypath[$path] = $leaf;
}
}
return $root;
}
/**
* Given a permission tree (in calculate_permission_tree format), removes any subtrees that
* are negative from the root. For example, if a top-level node of the permission tree has
* 'false' permission then it is meaningless because the default permission is already false;
* this function will remove it. However, if there is a child within that node that is positive,
* then that will need to be kept.
*
* @param \stdClass $root Root object
* @return \stdClass Filtered tree root
*/
protected static function remove_negative_subtrees($root) {
// If a node 'starts' negative, we don't need it (as negative is the default) - extract only
// subtrees that start with a positive value.
$positiveroot = (object)['allow' => false, 'path' => null, 'children' => []];
$consider = [$root];
while ($consider) {
$first = array_shift($consider);
foreach ($first->children as $node) {
if ($node->allow) {
// Add directly to new root.
$positiveroot->children[] = $node;
} else {
// Consider its children for adding to root (if there are any positive ones).
$consider[] = $node;
}
}
}
return $positiveroot;
}
/**
* Removes duplicate nodes of a tree - where a child node has the same permission as its
* parent.
*
* @param \stdClass $parent Tree root node
*/
protected static function remove_duplicate_nodes($parent) {
$length = count($parent->children);
$index = 0;
while ($index < $length) {
$child = $parent->children[$index];
if ($child->allow === $parent->allow) {
// Remove child node, but add its children to this node instead.
array_splice($parent->children, $index, 1);
$length--;
$index--;
foreach ($child->children as $grandchild) {
$parent->children[] = $grandchild;
$length++;
}
} else {
// Keep child node, but recurse to remove its unnecessary children.
self::remove_duplicate_nodes($child);
}
$index++;
}
}
/**
* Gets a permission tree for the given user and capability, representing the value of that
* capability at different contexts across the system. The tree will be simplified as far as
* possible.
*
* The output is the root of a tree of stdClass objects with the fields 'path' (a context path),
* 'allow' (true or false), and 'children' (an array of similar objects).
*
* @param int $userid User id
* @param string $capability Capability e.g. 'moodle/course:view'
* @return \stdClass Root node of tree
*/
protected static function get_tree($userid, $capability) {
// Extract raw capability data for this user and capability.
$pathroleperms = self::get_capability_info_at_each_context($userid, $capability);
// Convert the raw data into a permission tree based on context.
$root = self::calculate_permission_tree($pathroleperms);
unset($pathroleperms);
// Simplify the permission tree by removing unnecessary nodes.
$root = self::remove_negative_subtrees($root);
self::remove_duplicate_nodes($root);
// Return the tree.
return $root;
}
/**
* Creates SQL suitable for restricting by contexts listed in the given permission tree.
*
* This function relies on the permission tree being in the format created by get_tree.
* Specifically, all the children of the root element must be set to 'allow' permission,
* children of those children must be 'not allow', children of those grandchildren 'allow', etc.
*
* @param \stdClass $parent Root node of permission tree
* @return array Two-element array of SQL (containing ? placeholders) and then a params array
*/
protected static function create_sql($parent) {
global $DB;
$sql = '';
$params = [];
if ($parent->path !== null) {
// Except for the root element, create the condition that it applies to the context of
// this element (or anything within it).
$sql = ' (x.path = ? OR ' . $DB->sql_like('x.path', '?') .')';
$params[] = $parent->path;
$params[] = $parent->path . '/%';
if ($parent->children) {
// When there are children, these are assumed to have the opposite sign i.e. if we
// are allowing the parent, we are not allowing the children, and vice versa. So
// the 'OR' clause for children will be inside this 'AND NOT'.
$sql .= ' AND NOT (';
}
} else if (count($parent->children) > 1) {
// Place brackets in the query when it is going to be an OR of multiple conditions.
$sql .= ' (';
}
if ($parent->children) {
$first = true;
foreach ($parent->children as $child) {
if ($first) {
$first = false;
} else {
$sql .= ' OR';
}
// Recuse to get the child requirements - this will be the check that the context
// is within the child, plus possibly and 'AND NOT' for any different contexts
// within the child.
list ($childsql, $childparams) = self::create_sql($child);
$sql .= $childsql;
$params = array_merge($params, $childparams);
}
// Close brackets if opened above.
if ($parent->path !== null || count($parent->children) > 1) {
$sql .= ')';
}
}
return [$sql, $params];
}
/**
* Gets SQL to restrict a query to contexts in which the user has a capability.
*
* This returns an array with two elements (SQL containing ? placeholders, and a params array).
* The SQL is intended to be used as part of a WHERE clause. It relies on the prefix 'x' being
* used for the Moodle context table.
*
* If the user does not have the permission anywhere at all (so that there is no point doing
* the query) then the two returned values will both be false.
*
* @param int $userid User id
* @param string $capability Capability e.g. 'moodle/course:view'
* @return array Two-element array of SQL (containing ? placeholders) and then a params array
*/
public static function get_sql($userid, $capability) {
// Get a tree of capability permission at various contexts for current user.
$root = self::get_tree($userid, $capability);
// The root node always has permission false. If there are no child nodes then the user
// cannot access anything.
if (!$root->children) {
return [false, false];
}
// Get SQL to limit contexts based on the permission tree.
return self::create_sql($root);
}
/**
* Map fieldnames to get ready for the SQL query.
*
* @param string $fieldsexceptid A comma-separated list of the fields you require, not including id.
* Add ctxid, ctxpath, ctxdepth etc to return course context information for preloading.
* @return string Mapped field list for the SQL query.
*/
public static function map_fieldnames(string $fieldsexceptid = ''): string {
// Convert fields list and ordering.
$fieldlist = '';
if ($fieldsexceptid) {
$fields = array_map('trim', explode(',', $fieldsexceptid));
foreach ($fields as $field) {
// Context fields have a different alias.
if (strpos($field, 'ctx') === 0) {
switch($field) {
case 'ctxlevel' :
$realfield = 'contextlevel';
break;
case 'ctxinstance' :
$realfield = 'instanceid';
break;
default:
$realfield = substr($field, 3);
break;
}
$fieldlist .= ',x.' . $realfield . ' AS ' . $field;
} else {
$fieldlist .= ',c.'.$field;
}
}
}
return $fieldlist;
}
}
+101
View File
@@ -0,0 +1,101 @@
<?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/>.
/**
* Contains the base class for fetching the important dates in an activity module for a given module instance and a user.
*
* @package core
* @copyright Shamim Rezaie <shamim@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
declare(strict_types=1);
namespace core;
use cm_info;
/**
* Class for fetching the important dates of an activity module for a given module instance and a user.
*
* @copyright Shamim Rezaie <shamim@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class activity_dates {
/**
* @var cm_info The course module information object.
*/
protected $cm;
/**
* @var int The user id.
*/
protected $userid;
/**
* activity_dates constructor.
*
* @param cm_info $cm course module
* @param int $userid user id
*/
public function __construct(cm_info $cm, int $userid) {
$this->cm = $cm;
$this->userid = $userid;
}
/**
* Returns a list of important dates in the given module for the user.
*
* @param cm_info $cm The course module information.
* @param int $userid The user ID.
* @return array|array[]
*/
public static function get_dates_for_module(cm_info $cm, int $userid): array {
$cmdatesclassname = static::get_dates_classname($cm->modname);
if (!$cmdatesclassname) {
return [];
}
/** @var activity_dates $dates */
$dates = new $cmdatesclassname($cm, $userid);
return $dates->get_dates();
}
/**
* Fetches the module's dates class implementation if it's available.
*
* @param string $modname The activity module name. Usually from cm_info::modname.
* @return string|null
*/
private static function get_dates_classname(string $modname): ?string {
$cmdatesclass = "mod_{$modname}\\dates";
if (class_exists($cmdatesclass) && is_subclass_of($cmdatesclass, self::class)) {
return $cmdatesclass;
}
return null;
}
/**
* Returns a list of important dates for this module.
*
* @return array[] Each element of the array is an array with keys:
* label - The label for the date
* timestamp - The date
*/
abstract protected function get_dates(): array;
}
+139
View File
@@ -0,0 +1,139 @@
<?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/>.
/**
* Courses analyser working at course level (insights for the course teachers).
*
* @package core
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\analytics\analyser;
defined('MOODLE_INTERNAL') || die();
/**
* Courses analyser working at course level (insights for the course teachers).
*
* @package core
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class courses extends \core_analytics\local\analyser\by_course {
/**
* Samples origin is course table.
*
* @return string
*/
public function get_samples_origin() {
return 'course';
}
/**
* Just one sample per analysable.
*
* @return bool
*/
public static function one_sample_per_analysable() {
return true;
}
/**
* Returns the analysable of a sample
*
* @param int $sampleid
* @return \core_analytics\analysable
*/
public function get_sample_analysable($sampleid) {
return \core_analytics\course::instance($sampleid);
}
/**
* This provides samples' course and context.
*
* @return string[]
*/
protected function provided_sample_data() {
return array('course', 'context');
}
/**
* Returns the context of a sample.
*
* @param int $sampleid
* @return \context
*/
public function sample_access_context($sampleid) {
return \context_course::instance($sampleid);
}
/**
* This will return just one course as we analyse 'by_course'.
*
* @param \core_analytics\analysable $course
* @return array
*/
public function get_all_samples(\core_analytics\analysable $course) {
$context = \context_course::instance($course->get_id());
// Just 1 sample per analysable.
return array(
array($course->get_id() => $course->get_id()),
array($course->get_id() => array('course' => $course->get_course_data(), 'context' => $context))
);
}
/**
* Returns samples data from sample ids.
*
* @param int[] $sampleids
* @return array
*/
public function get_samples($sampleids) {
global $DB;
list($sql, $params) = $DB->get_in_or_equal($sampleids, SQL_PARAMS_NAMED);
$courses = $DB->get_records_select('course', "id $sql", $params);
$courseids = array_keys($courses);
$sampleids = array_combine($courseids, $courseids);
$courses = array_map(function($course) {
return array('course' => $course, 'context' => \context_course::instance($course->id));
}, $courses);
// No related data attached.
return array($sampleids, $courses);
}
/**
* Returns the sample description
*
* @param int $sampleid
* @param int $contextid
* @param array $sampledata
* @return array array(string, \renderable)
*/
public function sample_description($sampleid, $contextid, $sampledata) {
$description = format_string(
get_course_display_name_for_list($sampledata['course']), true, array('context' => $sampledata['context']));
$courseimage = new \pix_icon('i/course', get_string('course'));
return array($description, $courseimage);
}
}
@@ -0,0 +1,139 @@
<?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/>.
/**
* Site courses analyser working at system level (insights for the site admin).
*
* @package core
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\analytics\analyser;
defined('MOODLE_INTERNAL') || die();
/**
* Site courses analyser working at system level (insights for the site admin).
*
* @package core
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class site_courses extends \core_analytics\local\analyser\sitewide {
/**
* Samples origin is course table.
*
* @return string
*/
public function get_samples_origin() {
return 'course';
}
/**
* Returns the sample analysable
*
* @param int $sampleid
* @return \core_analytics\analysable
*/
public function get_sample_analysable($sampleid) {
return new \core_analytics\site();
}
/**
* Data this analyer samples provide.
*
* @return string[]
*/
protected function provided_sample_data() {
return array('course', 'context');
}
/**
* Returns the sample context.
*
* @param int $sampleid
* @return \context
*/
public function sample_access_context($sampleid) {
return \context_system::instance();
}
/**
* Returns all site courses.
*
* @param \core_analytics\analysable $site
* @return array
*/
public function get_all_samples(\core_analytics\analysable $site) {
global $DB;
// Getting courses from DB instead of from the site as these samples
// will be stored in memory and we just want the id.
$select = 'id != 1';
$courses = get_courses('all', 'c.sortorder ASC');
unset($courses[SITEID]);
$courseids = array_keys($courses);
$sampleids = array_combine($courseids, $courseids);
$courses = array_map(function($course) {
return array('course' => $course, 'context' => \context_course::instance($course->id));
}, $courses);
// No related data attached.
return array($sampleids, $courses);
}
/**
* Return all complete samples data from sample ids.
*
* @param int[] $sampleids
* @return array
*/
public function get_samples($sampleids) {
global $DB;
list($sql, $params) = $DB->get_in_or_equal($sampleids, SQL_PARAMS_NAMED);
$courses = $DB->get_records_select('course', "id $sql", $params);
$courseids = array_keys($courses);
$sampleids = array_combine($courseids, $courseids);
$courses = array_map(function($course) {
return array('course' => $course, 'context' => \context_course::instance($course->id));
}, $courses);
// No related data attached.
return array($sampleids, $courses);
}
/**
* Returns the description of a sample.
*
* @param int $sampleid
* @param int $contextid
* @param array $sampledata
* @return array array(string, \renderable)
*/
public function sample_description($sampleid, $contextid, $sampledata) {
$description = format_string(
get_course_display_name_for_list($sampledata['course']), true, array('context' => $sampledata['context']));
$courseimage = new \pix_icon('i/course', get_string('course'));
return array($description, $courseimage);
}
}
@@ -0,0 +1,251 @@
<?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/>.
/**
* Student enrolments analyser.
*
* @package core
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\analytics\analyser;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/lib/enrollib.php');
/**
* Student enrolments analyser.
*
* It does return all student enrolments including the suspended ones.
*
* @package core
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class student_enrolments extends \core_analytics\local\analyser\by_course {
/**
* @var array Cache for user_enrolment id - course id relation.
*/
protected $samplecourses = array();
/**
* Defines the origin of the samples in the database.
*
* @return string
*/
public function get_samples_origin() {
return 'user_enrolments';
}
/**
* Returns the student enrolment course context.
*
* @param int $sampleid
* @return \context
*/
public function sample_access_context($sampleid) {
return \context_course::instance($this->get_sample_courseid($sampleid));
}
/**
* Returns the student enrolment course.
*
* @param int $sampleid
* @return \core_analytics\analysable
*/
public function get_sample_analysable($sampleid) {
$course = enrol_get_course_by_user_enrolment_id($sampleid);
return \core_analytics\course::instance($course);
}
/**
* Data provided by get_all_samples & get_samples.
*
* @return string[]
*/
protected function provided_sample_data() {
return array('user_enrolments', 'context', 'course', 'user');
}
/**
* We need to delete associated data if a user requests his data to be deleted.
*
* @return bool
*/
public function processes_user_data() {
return true;
}
/**
* Join the samples origin table with the user id table.
*
* @param string $sampletablealias
* @return string
*/
public function join_sample_user($sampletablealias) {
return "JOIN {user_enrolments} ue ON {$sampletablealias}.sampleid = ue.id " .
"JOIN {user} u ON u.id = ue.userid";
}
/**
* All course student enrolments.
*
* It does return all student enrolments including the suspended ones.
*
* @param \core_analytics\analysable $course
* @return array
*/
public function get_all_samples(\core_analytics\analysable $course) {
$enrolments = enrol_get_course_users($course->get_id());
// We fetch all enrolments, but we are only interested in students.
$studentids = $course->get_students();
$samplesdata = array();
foreach ($enrolments as $userenrolmentid => $user) {
if (empty($studentids[$user->id])) {
// Not a student or an analysed one.
continue;
}
$sampleid = $userenrolmentid;
$samplesdata[$sampleid]['user_enrolments'] = (object)array(
'id' => $user->ueid,
'status' => $user->uestatus,
'enrolid' => $user->ueenrolid,
'userid' => $user->id,
'timestart' => $user->uetimestart,
'timeend' => $user->uetimeend,
'modifierid' => $user->uemodifierid,
'timecreated' => $user->uetimecreated,
'timemodified' => $user->uetimemodified
);
unset($user->ueid);
unset($user->uestatus);
unset($user->ueenrolid);
unset($user->uetimestart);
unset($user->uetimeend);
unset($user->uemodifierid);
unset($user->uetimecreated);
unset($user->uetimemodified);
// This student has been already analysed. We analyse each student once.
unset($studentids[$user->id]);
$samplesdata[$sampleid]['course'] = $course->get_course_data();
$samplesdata[$sampleid]['context'] = $course->get_context();
$samplesdata[$sampleid]['user'] = $user;
// Fill the cache.
$this->samplecourses[$sampleid] = $course->get_id();
}
$enrolids = array_keys($samplesdata);
return array(array_combine($enrolids, $enrolids), $samplesdata);
}
/**
* Returns all samples from the samples ids.
*
* @param int[] $sampleids
* @return array
*/
public function get_samples($sampleids) {
global $DB;
$enrolments = enrol_get_course_users(false, false, array(), $sampleids);
// Some course enrolments.
list($enrolsql, $params) = $DB->get_in_or_equal($sampleids, SQL_PARAMS_NAMED);
$samplesdata = array();
foreach ($enrolments as $userenrolmentid => $user) {
$sampleid = $userenrolmentid;
$samplesdata[$sampleid]['user_enrolments'] = (object)array(
'id' => $user->ueid,
'status' => $user->uestatus,
'enrolid' => $user->ueenrolid,
'userid' => $user->id,
'timestart' => $user->uetimestart,
'timeend' => $user->uetimeend,
'modifierid' => $user->uemodifierid,
'timecreated' => $user->uetimecreated,
'timemodified' => $user->uetimemodified
);
unset($user->ueid);
unset($user->uestatus);
unset($user->ueenrolid);
unset($user->uetimestart);
unset($user->uetimeend);
unset($user->uemodifierid);
unset($user->uetimecreated);
unset($user->uetimemodified);
// Enrolment samples are grouped by the course they belong to, so all $sampleids belong to the same
// course, $courseid and $coursemodinfo will only query the DB once and cache the course data in memory.
$courseid = $this->get_sample_courseid($sampleid);
$coursemodinfo = get_fast_modinfo($courseid);
$coursecontext = \context_course::instance($courseid);
$samplesdata[$sampleid]['course'] = $coursemodinfo->get_course();
$samplesdata[$sampleid]['context'] = $coursecontext;
$samplesdata[$sampleid]['user'] = $user;
// Fill the cache.
$this->samplecourses[$sampleid] = $coursemodinfo->get_course()->id;
}
$enrolids = array_keys($samplesdata);
return array(array_combine($enrolids, $enrolids), $samplesdata);
}
/**
* Returns the student enrolment course id.
*
* @param int $sampleid
* @return int
*/
protected function get_sample_courseid($sampleid) {
global $DB;
if (empty($this->samplecourses[$sampleid])) {
$course = enrol_get_course_by_user_enrolment_id($sampleid);
$this->samplecourses[$sampleid] = $course->id;
}
return $this->samplecourses[$sampleid];
}
/**
* Returns the visible name of a sample + a renderable to display as sample picture.
*
* @param int $sampleid
* @param int $contextid
* @param array $sampledata
* @return array array(string, \renderable)
*/
public function sample_description($sampleid, $contextid, $sampledata) {
$description = fullname($sampledata['user'], true, array('context' => $contextid));
return array($description, new \user_picture($sampledata['user']));
}
}
+192
View File
@@ -0,0 +1,192 @@
<?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/>.
/**
* Users analyser (insights for users).
*
* @package core
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\analytics\analyser;
defined('MOODLE_INTERNAL') || die();
/**
* Users analyser (insights for users).
*
* @package core
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class users extends \core_analytics\local\analyser\base {
/**
* The site users are the analysable elements returned by this analyser.
*
* @param string|null $action 'prediction', 'training' or null if no specific action needed.
* @param \context[] $contexts Only analysables that depend on the provided contexts. All analysables in the system if empty.
* @return \Iterator
*/
public function get_analysables_iterator(?string $action = null, array $contexts = []) {
global $DB, $CFG;
$siteadmins = explode(',', $CFG->siteadmins);
list($sql, $params) = $this->get_iterator_sql('user', CONTEXT_USER, $action, 'u', $contexts);
$sql .= " AND u.deleted = :deleted AND u.confirmed = :confirmed AND u.suspended = :suspended";
$params = $params + ['deleted' => 0, 'confirmed' => 1, 'suspended' => 0];
$ordersql = $this->order_sql('timecreated', 'ASC', 'u');
$recordset = $DB->get_recordset_sql($sql, $params);
if (!$recordset->valid()) {
$this->add_log(get_string('nousersfound'));
return new \ArrayIterator([]);
}
return new \core\dml\recordset_walk($recordset, function($record) use ($siteadmins) {
if (in_array($record->id, $siteadmins) || isguestuser($record->id)) {
// Skip admins and the guest user.
return false;
}
$context = \context_helper::preload_from_record($record);
return \core_analytics\user::instance($record, $context);
});
}
/**
* Just one sample per analysable.
*
* @return bool
*/
public static function one_sample_per_analysable() {
return true;
}
/**
* Samples origin is user table.
*
* @return string
*/
public function get_samples_origin() {
return 'user';
}
/**
* Returns the analysable of a sample
*
* @param int $sampleid
* @return \core_analytics\analysable
*/
public function get_sample_analysable($sampleid) {
return \core_analytics\user::instance($sampleid);
}
/**
* This provides samples' user and context.
*
* @return string[]
*/
protected function provided_sample_data() {
return ['user', 'context'];
}
/**
* Returns the context of a sample.
*
* @param int $sampleid
* @return \context
*/
public function sample_access_context($sampleid) {
return \context_user::instance($sampleid);
}
/**
* This will return just one user as we analyse users separately.
*
* @param \core_analytics\analysable $user
* @return array
*/
public function get_all_samples(\core_analytics\analysable $user) {
$context = \context_user::instance($user->get_id());
// Just 1 sample per analysable.
return [
[$user->get_id() => $user->get_id()],
[$user->get_id() => ['user' => $user->get_user_data(), 'context' => $context]]
];
}
/**
* Returns samples data from sample ids.
*
* @param int[] $sampleids
* @return array
*/
public function get_samples($sampleids) {
global $DB;
list($sql, $params) = $DB->get_in_or_equal($sampleids, SQL_PARAMS_NAMED);
$users = $DB->get_records_select('user', "id $sql", $params);
$userids = array_keys($users);
$sampleids = array_combine($userids, $userids);
$users = array_map(function($user) {
return ['user' => $user, 'context' => \context_user::instance($user->id)];
}, $users);
// No related data attached.
return [$sampleids, $users];
}
/**
* Returns the description of a sample.
*
* @param int $sampleid
* @param int $contextid
* @param array $sampledata
* @return array array(string, \renderable)
*/
public function sample_description($sampleid, $contextid, $sampledata) {
$description = fullname($sampledata['user']);
return [$description, new \user_picture($sampledata['user'])];
}
/**
* We need to delete associated data if a user requests his data to be deleted.
*
* @return bool
*/
public function processes_user_data() {
return true;
}
/**
* Join the samples origin table with the user id table.
*
* @param string $sampletablealias
* @return string
*/
public function join_sample_user($sampletablealias) {
return "JOIN {user} u ON u.id = {$sampletablealias}.sampleid";
}
}
@@ -0,0 +1,88 @@
<?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/>.
/**
* Any access after the official end of the course.
*
* @package core
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\analytics\indicator;
defined('MOODLE_INTERNAL') || die();
/**
* Any access after the official end of the course.
*
* @package core
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class any_access_after_end extends \core_analytics\local\indicator\binary {
/**
* Returns the name.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('indicator:accessesafterend');
}
/**
* required_sample_data
*
* @return string[]
*/
public static function required_sample_data() {
return array('user', 'course', 'context');
}
/**
* calculate_sample
*
* @param int $sampleid
* @param string $samplesorigin
* @param int $starttime
* @param int $endtime
* @return float
*/
protected function calculate_sample($sampleid, $samplesorigin, $starttime = false, $endtime = false) {
global $DB;
$user = $this->retrieve('user', $sampleid);
$course = \core_analytics\course::instance($this->retrieve('course', $sampleid));
// Filter by context to use the db table index.
$context = $this->retrieve('context', $sampleid);
$select = "userid = :userid AND contextlevel = :contextlevel AND contextinstanceid = :contextinstanceid AND " .
"timecreated > :end";
$params = array('userid' => $user->id, 'contextlevel' => $context->contextlevel,
'contextinstanceid' => $context->instanceid, 'end' => $course->get_end());
$logstore = \core_analytics\manager::get_analytics_logstore();
$nlogs = $logstore->get_events_select_count($select, $params);
if ($nlogs) {
return self::get_max_value();
} else {
return self::get_min_value();
}
}
}
@@ -0,0 +1,91 @@
<?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/>.
/**
* Any access before the official start of the course.
*
* @package core
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\analytics\indicator;
defined('MOODLE_INTERNAL') || die();
/**
* Any access before the official start of the course.
*
* @package core
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class any_access_before_start extends \core_analytics\local\indicator\binary {
/**
* Returns the name.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('indicator:accessesbeforestart');
}
/**
* required_sample_data
*
* @return string[]
*/
public static function required_sample_data() {
return array('user', 'course', 'context');
}
/**
* calculate_sample
*
* @param int $sampleid
* @param string $samplesorigin
* @param int $starttime
* @param int $endtime
* @return float
*/
protected function calculate_sample($sampleid, $samplesorigin, $starttime = false, $endtime = false) {
global $DB;
$user = $this->retrieve('user', $sampleid);
$course = \core_analytics\course::instance($this->retrieve('course', $sampleid));
if (!$logstore = \core_analytics\manager::get_analytics_logstore()) {
throw new \coding_exception('No available log stores');
}
// Filter by context to use the logstore_standard_log db table index.
$context = $this->retrieve('context', $sampleid);
$select = "userid = :userid AND contextlevel = :contextlevel AND contextinstanceid = :contextinstanceid AND " .
"timecreated < :start";
$params = array('userid' => $user->id, 'contextlevel' => $context->contextlevel,
'contextinstanceid' => $context->instanceid, 'start' => $course->get_start());
$nlogs = $logstore->get_events_select_count($select, $params);
if ($nlogs) {
return self::get_max_value();
} else {
return self::get_min_value();
}
}
}
@@ -0,0 +1,138 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Any access indicator.
*
* @package core
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\analytics\indicator;
defined('MOODLE_INTERNAL') || die();
/**
* Any access indicator.
*
* @package core
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class any_course_access extends \core_analytics\local\indicator\binary {
/** @var array user last access. */
protected $lastaccesses = [];
/**
* Returns the name.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('indicator:anycourseaccess');
}
/**
* required_sample_data
*
* @return string[]
*/
public static function required_sample_data() {
return array('course', 'user');
}
/**
* Store userid => timeaccess relation if the provided analysable is a course.
*
* @param \core_analytics\analysable $analysable
* @return null
*/
public function fill_per_analysable_caches(\core_analytics\analysable $analysable) {
global $DB;
if ($analysable instanceof \core_analytics\course) {
// Indexed by userid (there is a UNIQUE KEY at DB level).
$this->lastaccesses = $DB->get_records('user_lastaccess', ['courseid' => $analysable->get_id()],
'', 'userid, timeaccess');
}
}
/**
* calculate_sample
*
* @param int $sampleid
* @param string $sampleorigin
* @param int $starttime
* @param int $endtime
* @return float
*/
protected function calculate_sample($sampleid, $sampleorigin, $starttime = false, $endtime = false) {
$course = $this->retrieve('course', $sampleid);
$user = $this->retrieve('user', $sampleid);
// We first try using user_lastaccess as it is much faster than the log table.
if (empty($this->lastaccesses[$user->id]->timeaccess)) {
// The user never accessed.
return self::get_min_value();
} else if (!$starttime && !$endtime) {
// No time restrictions, so all good as long as there is a record.
return self::get_max_value();
} else if ($starttime && $this->lastaccesses[$user->id]->timeaccess < $starttime) {
// The last access is prior to $starttime.
return self::get_min_value();
} else if ($endtime && $this->lastaccesses[$user->id]->timeaccess < $endtime) {
// The last access is before the $endtime.
return self::get_max_value();
} else if ($starttime && !$endtime && $starttime <= $this->lastaccesses[$user->id]->timeaccess) {
// No end time, so max value as long as the last access is after $starttime.
return self::get_max_value();
}
// If the last access is after $endtime we can not know for sure if the user accessed or not
// between $starttime and $endtime, we need to check the logs table in this case. Note that
// it is unlikely that we will reach this point as this indicator will be used in models whose
// dates are in the past.
if (!$logstore = \core_analytics\manager::get_analytics_logstore()) {
throw new \coding_exception('No available log stores');
}
// Filter by context to use the logstore_standard_log db table index.
$select = "userid = :userid AND courseid = :courseid";
$params = ['courseid' => $course->id, 'userid' => $user->id];
if ($starttime) {
$select .= " AND timecreated > :starttime";
$params['starttime'] = $starttime;
}
if ($endtime) {
$select .= " AND timecreated <= :endtime";
$params['endtime'] = $endtime;
}
$nlogs = $logstore->get_events_select_count($select, $params);
if ($nlogs) {
return self::get_max_value();
} else {
return self::get_min_value();
}
}
}
@@ -0,0 +1,106 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Write actions indicator.
*
* @package core
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\analytics\indicator;
defined('MOODLE_INTERNAL') || die();
/**
* Write actions indicator.
*
* @package core
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class any_write_action extends \core_analytics\local\indicator\binary {
/**
* Returns the name.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('indicator:anywrite');
}
/**
* required_sample_data
*
* @return string[]
*/
public static function required_sample_data() {
// User is not required, calculate_sample can handle its absence.
return array('context');
}
/**
* calculate_sample
*
* @param int $sampleid
* @param string $sampleorigin
* @param int $starttime
* @param int $endtime
* @return float
*/
protected function calculate_sample($sampleid, $sampleorigin, $starttime = false, $endtime = false) {
global $DB;
$select = '';
$params = array();
if ($user = $this->retrieve('user', $sampleid)) {
$select .= "userid = :userid AND ";
$params = $params + array('userid' => $user->id);
}
if (!$logstore = \core_analytics\manager::get_analytics_logstore()) {
throw new \coding_exception('No available log stores');
}
// Filter by context to use the logstore_standard_log db table index.
$context = $this->retrieve('context', $sampleid);
$select .= "contextlevel = :contextlevel AND contextinstanceid = :contextinstanceid AND " .
"(crud = 'c' OR crud = 'u')";
$params = $params + array('contextlevel' => $context->contextlevel,
'contextinstanceid' => $context->instanceid);
if ($starttime) {
$select .= " AND timecreated > :starttime";
$params['starttime'] = $starttime;
}
if ($endtime) {
$select .= " AND timecreated <= :endtime";
$params['endtime'] = $endtime;
}
$nlogs = $logstore->get_events_select_count($select, $params);
if ($nlogs) {
return self::get_max_value();
} else {
return self::get_min_value();
}
}
}
@@ -0,0 +1,101 @@
<?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/>.
/**
* Write actions in a course indicator.
*
* @package core
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\analytics\indicator;
defined('MOODLE_INTERNAL') || die();
/**
* Write actions in a course indicator.
*
* @package core
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class any_write_action_in_course extends \core_analytics\local\indicator\binary {
/**
* Returns the name.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('indicator:anywriteincourse');
}
/**
* required_sample_data
*
* @return string[]
*/
public static function required_sample_data() {
// User is not required, calculate_sample can handle its absence.
return array('course');
}
/**
* calculate_sample
*
* @param int $sampleid
* @param string $sampleorigin
* @param int $starttime
* @param int $endtime
* @return float
*/
protected function calculate_sample($sampleid, $sampleorigin, $starttime = false, $endtime = false) {
global $DB;
if (!$logstore = \core_analytics\manager::get_analytics_logstore()) {
throw new \coding_exception('No available log stores');
}
// Filter by context to use the logstore_standard_log db table index.
$course = $this->retrieve('course', $sampleid);
$select = "courseid = :courseid AND anonymous = :anonymous AND (crud = 'c' OR crud = 'u')";
$params = array('courseid' => $course->id, 'anonymous' => '0');
if ($user = $this->retrieve('user', $sampleid)) {
$select .= " AND userid = :userid";
$params['userid'] = $user->id;
}
if ($starttime) {
$select .= " AND timecreated > :starttime";
$params['starttime'] = $starttime;
}
if ($endtime) {
$select .= " AND timecreated <= :endtime";
$params['endtime'] = $endtime;
}
$nlogs = $logstore->get_events_select_count($select, $params);
if ($nlogs) {
return self::get_max_value();
} else {
return self::get_min_value();
}
}
}
@@ -0,0 +1,114 @@
<?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/>.
/**
* Read actions indicator.
*
* @package core
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\analytics\indicator;
defined('MOODLE_INTERNAL') || die();
/**
* Read actions indicator.
*
* @package core
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class read_actions extends \core_analytics\local\indicator\linear {
/**
* Returns the name.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('indicator:readactions');
}
/**
* required_sample_data
*
* @return string[]
*/
public static function required_sample_data() {
// User is not required, calculate_sample can handle its absence.
return array('context');
}
/**
* calculate_sample
*
* @param int $sampleid
* @param string $sampleorigin
* @param int $starttime
* @param int $endtime
* @return float
*/
protected function calculate_sample($sampleid, $sampleorigin, $starttime = false, $endtime = false) {
if (!$starttime || !$endtime) {
return null;
}
$select = '';
$params = array();
if ($user = $this->retrieve('user', $sampleid)) {
$select .= "userid = :userid AND ";
$params = $params + array('userid' => $user->id);
}
if (!$logstore = \core_analytics\manager::get_analytics_logstore()) {
throw new \coding_exception('No available log stores');
}
// Filter by context to use the logstore_standard_log db table index.
$context = $this->retrieve('context', $sampleid);
$select .= "contextlevel = :contextlevel AND contextinstanceid = :contextinstanceid AND " .
"crud = 'r' AND timecreated > :starttime AND timecreated <= :endtime";
$params = $params + array('contextlevel' => $context->contextlevel,
'contextinstanceid' => $context->instanceid, 'starttime' => $starttime, 'endtime' => $endtime);
$nrecords = $logstore->get_events_select_count($select, $params);
// We define a list of ranges to fit $nrecords into it
// # Done absolutely nothing
// # Not much really, just accessing the course once a week
// # More than just accessing the course, some interaction
// # Significant contribution, will depend on the course anyway.
// We need to adapt the limits to the time range duration.
$nweeks = $this->get_time_range_weeks_number($starttime, $endtime);
// Careful with this, depends on the course.
$limit = $nweeks * 3 * 10;
$ranges = array(
array('eq', 0),
// 1 course access per week (3 records are easily generated).
array('le', $nweeks * 3),
// 3 course accesses per week doing some stuff.
array('le', $limit),
array('gt', $limit)
);
return $this->classify_value($nrecords, $ranges);
}
}
@@ -0,0 +1,59 @@
<?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/>.
/**
* 10 parts time splitting method.
*
* @package core
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\analytics\time_splitting;
defined('MOODLE_INTERNAL') || die();
/**
* 10 parts time splitting method.
*
* @package core
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class deciles extends \core_analytics\local\time_splitting\equal_parts {
/**
* Returns a lang_string object representing the name for the time splitting method.
*
* Used as column identificator.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('timesplitting:deciles');
}
/**
* 10 parts.
*
* @return int
*/
protected function get_number_parts() {
return 10;
}
}
@@ -0,0 +1,59 @@
<?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/>.
/**
* Range processor splitting the course in ten parts and accumulating data.
*
* @package core
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\analytics\time_splitting;
defined('MOODLE_INTERNAL') || die();
/**
* Range processor splitting the course in ten parts and accumulating data.
*
* @package core
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class deciles_accum extends \core_analytics\local\time_splitting\accumulative_parts {
/**
* Returns a lang_string object representing the name for the time splitting method.
*
* Used as column identificator.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('timesplitting:decilesaccum');
}
/**
* 10 parts.
*
* @return int
*/
protected function get_number_parts() {
return 10;
}
}
@@ -0,0 +1,80 @@
<?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/>.
/**
* No time splitting method.
*
* Used when time is not a factor to consider into the equation.
*
* @package core
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\analytics\time_splitting;
defined('MOODLE_INTERNAL') || die();
/**
* No time splitting method.
*
* Used when time is not a factor to consider into the equation.
*
* @package core
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class no_splitting extends \core_analytics\local\time_splitting\base {
/**
* Returns a lang_string object representing the name for the time splitting method.
*
* Used as column identificator.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('timesplitting:nosplitting');
}
/**
* ready_to_predict
*
* @param array $range
* @return true
*/
public function ready_to_predict($range) {
return true;
}
/**
* define_ranges
*
* @return array
*/
protected function define_ranges() {
return [
[
'start' => 0,
'end' => \core_analytics\analysable::MAX_TIME,
// Time is ignored as we overwrite ready_to_predict.
'time' => 0
]
];
}
}
@@ -0,0 +1,56 @@
<?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/>.
/**
* Time splitting method that generates predictions one month after the analysable start.
*
* @package core_analytics
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\analytics\time_splitting;
defined('MOODLE_INTERNAL') || die();
/**
* Time splitting method that generates predictions one month after the analysable start.
*
* @package core_analytics
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class one_month_after_start extends \core_analytics\local\time_splitting\after_start {
/**
* The time splitting method name.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('timesplitting:onemonthafterstart');
}
/**
* The period we should wait until we generate predictions for this.
*
* @param \core_analytics\analysable $analysable Not used in this implementation.
* @return \DateInterval
*/
protected function wait_period(\core_analytics\analysable $analysable) {
return new \DateInterval('P1M');
}
}
@@ -0,0 +1,56 @@
<?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/>.
/**
* Time splitting method that generates predictions one week after the analysable start.
*
* @package core_analytics
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\analytics\time_splitting;
defined('MOODLE_INTERNAL') || die();
/**
* Time splitting method that generates predictions one week after the analysable start.
*
* @package core_analytics
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class one_week_after_start extends \core_analytics\local\time_splitting\after_start {
/**
* The time splitting method name.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('timesplitting:oneweekafterstart');
}
/**
* The period we should wait until we generate predictions for this.
*
* @param \core_analytics\analysable $analysable Not used in this implementation.
* @return \DateInterval
*/
protected function wait_period(\core_analytics\analysable $analysable) {
return new \DateInterval('P1W');
}
}
@@ -0,0 +1,55 @@
<?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/>.
/**
* Time splitting method that generates predictions every 3 days.
*
* @package core_analytics
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\analytics\time_splitting;
defined('MOODLE_INTERNAL') || die();
/**
* Time splitting method that generates predictions every 3 days.
*
* @package core_analytics
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class past_3_days extends \core_analytics\local\time_splitting\past_periodic {
/**
* The time splitting method name.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('timesplitting:past3days');
}
/**
* Once every 3 days.
*
* @return \DateInterval
*/
public function periodicity() {
return new \DateInterval('P3D');
}
}
@@ -0,0 +1,55 @@
<?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/>.
/**
* Time splitting method that generates monthly predictions.
*
* @package core_analytics
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\analytics\time_splitting;
defined('MOODLE_INTERNAL') || die();
/**
* Time splitting method that generates monthly predictions.
*
* @package core_analytics
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class past_month extends \core_analytics\local\time_splitting\past_periodic {
/**
* The time splitting method name.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('timesplitting:pastmonth');
}
/**
* Once a month.
*
* @return \DateInterval
*/
public function periodicity() {
return new \DateInterval('P1M');
}
}
@@ -0,0 +1,54 @@
<?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/>.
/**
* Time splitting method that generates weekly predictions.
*
* @package core_analytics
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\analytics\time_splitting;
defined('MOODLE_INTERNAL') || die();
/**
* Time splitting method that generates weekly predictions.
*
* @package core_analytics
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class past_week extends \core_analytics\local\time_splitting\past_periodic {
/**
* The time splitting method name.
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('timesplitting:pastweek');
}
/**
* Once per week.
*
* @return \DateInterval
*/
public function periodicity() {
return new \DateInterval('P1W');
}
}
@@ -0,0 +1,59 @@
<?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/>.
/**
* Quarters time splitting method.
*
* @package core
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\analytics\time_splitting;
defined('MOODLE_INTERNAL') || die();
/**
* Quarters time splitting method.
*
* @package core
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quarters extends \core_analytics\local\time_splitting\equal_parts {
/**
* Returns a lang_string object representing the name for the time spliting method.
*
* Used as column identificator.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('timesplitting:quarters');
}
/**
* 4 parts
*
* @return int
*/
protected function get_number_parts() {
return 4;
}
}
@@ -0,0 +1,59 @@
<?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/>.
/**
* Range processor splitting the course in quarters and accumulating data.
*
* @package core
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\analytics\time_splitting;
defined('MOODLE_INTERNAL') || die();
/**
* Range processor splitting the course in quarters and accumulating data.
*
* @package core
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quarters_accum extends \core_analytics\local\time_splitting\accumulative_parts {
/**
* Returns a lang_string object representing the name for the time spliting method.
*
* Used as column identificator.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('timesplitting:quartersaccum');
}
/**
* 4 parts.
*
* @return int
*/
protected function get_number_parts() {
return 4;
}
}
@@ -0,0 +1,67 @@
<?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/>.
/**
* Single time splitting method.
*
* @package core
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\analytics\time_splitting;
defined('MOODLE_INTERNAL') || die();
/**
* Single time splitting method.
*
* @package core
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class single_range extends \core_analytics\local\time_splitting\base
implements \core_analytics\local\time_splitting\before_now {
/**
* Returns a lang_string object representing the name for the time spliting method.
*
* Used as column identificator.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('timesplitting:singlerange');
}
/**
* One single range covering all analysable duration.
*
* @return array
*/
protected function define_ranges() {
// Key 'time' == 0 because we want it to start predicting from the beginning.
return [
[
'start' => $this->analysable->get_start(),
'end' => $this->analysable->get_end(),
'time' => 0
]
];
}
}
@@ -0,0 +1,81 @@
<?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/>.
/**
* Time splitting method that generates predictions 3 days after the analysable start.
*
* @package core_analytics
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\analytics\time_splitting;
defined('MOODLE_INTERNAL') || die();
/**
* Time splitting method that generates predictions 3 days after the analysable start.
*
* @package core_analytics
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class ten_percent_after_start extends \core_analytics\local\time_splitting\after_start {
/**
* The time splitting method name.
*
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('timesplitting:tenpercentafterstart');
}
/**
* Extended as we require and end date here.
*
* @param \core_analytics\analysable $analysable
* @return bool
*/
public function is_valid_analysable(\core_analytics\analysable $analysable) {
// We require an end date to calculate the 10%.
if (!$analysable->get_end()) {
return false;
}
return parent::is_valid_analysable($analysable);
}
/**
* The period we should wait until we generate predictions for this.
*
* @throws \coding_exception
* @param \core_analytics\analysable $analysable
* @return \DateInterval
*/
protected function wait_period(\core_analytics\analysable $analysable) {
if (!$analysable->get_end() || !$analysable->get_start()) {
throw new \coding_exception('Analysables with no start or end should be discarded in is_valid_analysable.');
}
$diff = $analysable->get_end() - $analysable->get_start();
// A 10% of $diff.
return new \DateInterval('PT' . intval($diff / 10) . 'S');
}
}
@@ -0,0 +1,53 @@
<?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/>.
/**
* Time splitting method that generates insights every three days and calculates indicators using upcoming dates.
*
* @package core_analytics
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\analytics\time_splitting;
defined('MOODLE_INTERNAL') || die();
/**
* Time splitting method that generates insights every three days and calculates indicators using upcoming dates.
*
* @package core_analytics
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class upcoming_3_days extends \core_analytics\local\time_splitting\upcoming_periodic {
/**
* The time splitting method name.
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('timesplitting:upcoming3days');
}
/**
* Once every three days.
* @return \DateInterval
*/
public function periodicity() {
return new \DateInterval('P3D');
}
}
@@ -0,0 +1,53 @@
<?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/>.
/**
* Time splitting method that generates insights every fortnight and calculates indicators using upcoming dates.
*
* @package core_analytics
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\analytics\time_splitting;
defined('MOODLE_INTERNAL') || die();
/**
* Time splitting method that generates insights every fortnight and calculates indicators using upcoming dates.
*
* @package core_analytics
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class upcoming_fortnight extends \core_analytics\local\time_splitting\upcoming_periodic {
/**
* The time splitting method name.
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('timesplitting:upcomingfortnight');
}
/**
* Every two weeks.
* @return \DateInterval
*/
public function periodicity() {
return new \DateInterval('P2W');
}
}
@@ -0,0 +1,53 @@
<?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/>.
/**
* Time splitting method that generates weekly predictions.
*
* @package core_analytics
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\analytics\time_splitting;
defined('MOODLE_INTERNAL') || die();
/**
* Time splitting method that generates weekly predictions.
*
* @package core_analytics
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class upcoming_week extends \core_analytics\local\time_splitting\upcoming_periodic {
/**
* The time splitting method name.
* @return \lang_string
*/
public static function get_name(): \lang_string {
return new \lang_string('timesplitting:upcomingweek');
}
/**
* Once per week.
* @return \DateInterval
*/
public function periodicity() {
return new \DateInterval('P1W');
}
}
+330
View File
@@ -0,0 +1,330 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Manager class for antivirus integration.
*
* @package core_antivirus
* @copyright 2015 Ruslan Kabalin, Lancaster University.
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\antivirus;
defined('MOODLE_INTERNAL') || die();
/**
* Class used for various antivirus related stuff.
*
* @package core_antivirus
* @copyright 2015 Ruslan Kabalin, Lancaster University.
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class manager {
/**
* Returns list of enabled antiviruses.
*
* @return array Array ('antivirusname'=>stdClass antivirus object).
*/
private static function get_enabled() {
global $CFG;
$active = array();
if (empty($CFG->antiviruses)) {
return $active;
}
foreach (explode(',', $CFG->antiviruses) as $e) {
if ($antivirus = self::get_antivirus($e)) {
if ($antivirus->is_configured()) {
$active[$e] = $antivirus;
}
}
}
return $active;
}
/**
* Scan file using all enabled antiviruses, throws exception in case of infected file.
*
* @param string $file Full path to the file.
* @param string $filename Name of the file (could be different from physical file if temp file is used).
* @param bool $deleteinfected whether infected file needs to be deleted.
* @throws \core\antivirus\scanner_exception If file is infected.
* @return void
*/
public static function scan_file($file, $filename, $deleteinfected) {
global $USER;
$antiviruses = self::get_enabled();
$notifylevel = (int)get_config('antivirus', 'notifylevel');
foreach ($antiviruses as $antivirus) {
// Attempt to scan, catching internal exceptions.
try {
$result = $antivirus->scan_file($file, $filename);
} catch (\core\antivirus\scanner_exception $e) {
$notice = $antivirus->get_scanning_notice();
$incidentdetails = $antivirus->get_incident_details($file, $filename, $notice, false);
// Log scan error event.
$params = [
'context' => \context_system::instance(),
'relateduserid' => $USER->id,
'other' => ['filename' => $filename, 'incidentdetails' => $incidentdetails],
];
$event = \core\event\antivirus_scan_file_error::create($params);
$event->trigger();
// If there was a scanner exception (such as ClamAV denying
// upload), send messages (on error and above), and rethrow.
if ($notifylevel === $antivirus::SCAN_RESULT_ERROR) {
$notice = $antivirus->get_scanning_notice();
self::send_antivirus_messages($antivirus, $incidentdetails);
}
throw $e;
}
$notice = $antivirus->get_scanning_notice();
if ($result === $antivirus::SCAN_RESULT_FOUND) {
// Infection found, send notification.
$incidentdetails = $antivirus->get_incident_details($file, $filename, $notice);
self::send_antivirus_messages($antivirus, $incidentdetails);
// Move to quarantine folder.
$zipfile = \core\antivirus\quarantine::quarantine_file($file, $filename, $incidentdetails, $notice);
// If file not stored due to disabled quarantine, store a message.
if (empty($zipfile)) {
$zipfile = get_string('quarantinedisabled', 'antivirus');
}
// Log file infected event.
$params = [
'context' => \context_system::instance(),
'relateduserid' => $USER->id,
'other' => ['filename' => $filename, 'zipfile' => $zipfile, 'incidentdetails' => $incidentdetails],
];
$event = \core\event\virus_infected_file_detected::create($params);
$event->trigger();
if ($deleteinfected) {
unlink($file);
}
// Get custom message to display to user from antivirus engine.
$displaymessage = $antivirus->get_virus_found_message();
$placeholders = array_merge(['item' => $filename], $displaymessage['placeholders']);
throw new \core\antivirus\scanner_exception(
$displaymessage['string'],
'',
$placeholders,
null,
$displaymessage['component']
);
} else if ($result === $antivirus::SCAN_RESULT_ERROR) {
// Here we need to generate a different incident based on an error.
$incidentdetails = $antivirus->get_incident_details($file, $filename, $notice, false);
// Log scan error event.
$params = [
'context' => \context_system::instance(),
'relateduserid' => $USER->id,
'other' => ['filename' => $filename, 'incidentdetails' => $incidentdetails],
];
$event = \core\event\antivirus_scan_file_error::create($params);
$event->trigger();
// Send a notification if required (error or above).
if ($notifylevel === $antivirus::SCAN_RESULT_ERROR) {
self::send_antivirus_messages($antivirus, $incidentdetails);
}
}
}
}
/**
* Scan data steam using all enabled antiviruses, throws exception in case of infected data.
*
* @param string $data The variable containing the data to scan.
* @throws \core\antivirus\scanner_exception If data is infected.
* @return void
*/
public static function scan_data($data) {
global $USER;
$antiviruses = self::get_enabled();
$notifylevel = (int)get_config('antivirus', 'notifylevel');
foreach ($antiviruses as $antivirus) {
// Attempt to scan, catching internal exceptions.
try {
$result = $antivirus->scan_data($data);
} catch (\core\antivirus\scanner_exception $e) {
$notice = $antivirus->get_scanning_notice();
$incidentdetails = $antivirus->get_incident_details('', $filename, $notice, false);
// Log scan error event.
$params = [
'context' => \context_system::instance(),
'relateduserid' => $USER->id,
'other' => ['filename' => $filename, 'incidentdetails' => $incidentdetails],
];
$event = \core\event\antivirus_scan_file_error::create($params);
$event->trigger();
// If there was a scanner exception (such as ClamAV denying upload), send messages and rethrow.
if ($notifylevel === $antivirus::SCAN_RESULT_ERROR) {
$notice = $antivirus->get_scanning_notice();
$filename = get_string('datastream', 'antivirus');
self::send_antivirus_messages($antivirus, $incidentdetails);
}
throw $e;
}
$filename = get_string('datastream', 'antivirus');
$notice = $antivirus->get_scanning_notice();
if ($result === $antivirus::SCAN_RESULT_FOUND) {
// Infection found, send notification.
$incidentdetails = $antivirus->get_incident_details('', $filename, $notice);
self::send_antivirus_messages($antivirus, $incidentdetails);
// Copy data to quarantine folder.
$zipfile = \core\antivirus\quarantine::quarantine_data($data, $filename, $incidentdetails, $notice);
// If file not stored due to disabled quarantine, store a message.
if (empty($zipfile)) {
$zipfile = get_string('quarantinedisabled', 'antivirus');
}
// Log file infected event.
$params = [
'context' => \context_system::instance(),
'relateduserid' => $USER->id,
'other' => ['filename' => $filename, 'zipfile' => $zipfile, 'incidentdetails' => $incidentdetails],
];
$event = \core\event\virus_infected_data_detected::create($params);
$event->trigger();
// Get custom message to display to user from antivirus engine.
$displaymessage = $antivirus->get_virus_found_message();
$placeholders = array_merge(['item' => get_string('datastream', 'antivirus')], $displaymessage['placeholders']);
throw new \core\antivirus\scanner_exception(
$displaymessage['string'],
'',
$placeholders,
null,
$displaymessage['component']
);
} else if ($result === $antivirus::SCAN_RESULT_ERROR) {
// Here we need to generate a different incident based on an error.
$incidentdetails = $antivirus->get_incident_details('', $filename, $notice, false);
// Log scan error event.
$params = [
'context' => \context_system::instance(),
'relateduserid' => $USER->id,
'other' => ['filename' => $filename, 'incidentdetails' => $incidentdetails],
];
$event = \core\event\antivirus_scan_data_error::create($params);
$event->trigger();
// Send a notification if required (error or above).
if ($notifylevel === $antivirus::SCAN_RESULT_ERROR) {
self::send_antivirus_messages($antivirus, $incidentdetails);
}
}
}
}
/**
* Returns instance of antivirus.
*
* @param string $antivirusname name of antivirus.
* @return object|bool antivirus instance or false if does not exist.
*/
public static function get_antivirus($antivirusname) {
global $CFG;
$classname = '\\antivirus_' . $antivirusname . '\\scanner';
if (!class_exists($classname)) {
return false;
}
return new $classname();
}
/**
* Get the list of available antiviruses.
*
* @return array Array ('antivirusname'=>'localised antivirus name').
*/
public static function get_available() {
$antiviruses = array();
foreach (\core_component::get_plugin_list('antivirus') as $antivirusname => $dir) {
$antiviruses[$antivirusname] = get_string('pluginname', 'antivirus_'.$antivirusname);
}
return $antiviruses;
}
/**
* This function puts all relevant information into the messages required, and sends them.
*
* @param \core\antivirus\scanner $antivirus the scanner engine.
* @param string $incidentdetails details of the incident.
* @return void
*/
public static function send_antivirus_messages(\core\antivirus\scanner $antivirus, string $incidentdetails) {
$messages = $antivirus->get_messages();
// If there is no messages, and a virus is found, we should generate one, then send it.
if (empty($messages)) {
$antivirus->message_admins($antivirus->get_scanning_notice(), FORMAT_MOODLE, 'infected');
$messages = $antivirus->get_messages();
}
foreach ($messages as $message) {
// Check if the information is already in the current scanning notice.
if (!empty($antivirus->get_scanning_notice()) &&
strpos($antivirus->get_scanning_notice(), $message->fullmessage) === false) {
// This is some extra information. We should append this to the end of the incident details.
$incidentdetails .= \html_writer::tag('pre', $message->fullmessage);
}
// Now update the message to the detailed version, and format.
$message->name = 'infected';
$message->fullmessagehtml = $incidentdetails;
$message->fullmessageformat = FORMAT_MOODLE;
$message->fullmessage = format_text_email($incidentdetails, $message->fullmessageformat);
// Now we must check if message is going to a real account.
// It may be an email that needs to be sent to non-user address.
if ($message->userto->id === -1) {
// If this doesnt exist, send a regular email.
email_to_user(
$message->userto,
get_admin(),
$message->subject,
$message->fullmessage,
$message->fullmessagehtml
);
} else {
// And now we can send.
message_send($message);
}
}
}
}
+326
View File
@@ -0,0 +1,326 @@
<?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/>.
/**
* Quarantine file
*
* @package core_antivirus
* @author Nathan Nguyen <nathannguyen@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\antivirus;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir.'/filelib.php');
/**
* Quarantine file
*
* @package core_antivirus
* @author Nathan Nguyen <nathannguyen@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quarantine {
/** Default quarantine folder */
const DEFAULT_QUARANTINE_FOLDER = 'antivirus_quarantine';
/** Zip infected file */
const FILE_ZIP_INFECTED = '_infected_file.zip';
/** Zip all infected file */
const FILE_ZIP_ALL_INFECTED = '_all_infected_files.zip';
/** Incident details file */
const FILE_HTML_DETAILS = '_details.html';
/** Incident details file */
const DEFAULT_QUARANTINE_TIME = DAYSECS * 28;
/** Date format in filename */
const FILE_NAME_DATE_FORMAT = '%Y%m%d%H%M%S';
/**
* Move the infected file to the quarantine folder.
*
* @param string $file infected file.
* @param string $filename infected file name.
* @param string $incidentdetails incident details.
* @param string $notice notice details.
* @return string|null the name of the newly created quarantined file.
* @throws \dml_exception
*/
public static function quarantine_file(string $file, string $filename, string $incidentdetails, string $notice): ?string {
if (!self::is_quarantine_enabled()) {
return null;
}
// Generate file names.
$date = userdate(time(), self::FILE_NAME_DATE_FORMAT) . "_" . rand();
$zipfilepath = self::get_quarantine_folder() . $date . self::FILE_ZIP_INFECTED;
$detailsfilename = $date . self::FILE_HTML_DETAILS;
// Create Zip file.
$ziparchive = new \zip_archive();
if ($ziparchive->open($zipfilepath, \file_archive::CREATE)) {
$ziparchive->add_file_from_string($detailsfilename, format_text($incidentdetails, FORMAT_MOODLE));
$ziparchive->add_file_from_pathname($filename, $file);
$ziparchive->close();
}
$zipfile = basename($zipfilepath);
self::create_infected_file_record($filename, $zipfile, $notice);
return $zipfile;
}
/**
* Move the infected file to the quarantine folder.
*
* @param string $data data which is infected.
* @param string $filename infected file name.
* @param string $incidentdetails incident details.
* @param string $notice notice details.
* @return string|null the name of the newly created quarantined file.
* @throws \dml_exception
*/
public static function quarantine_data(string $data, string $filename, string $incidentdetails, string $notice): ?string {
if (!self::is_quarantine_enabled()) {
return null;
}
// Generate file names.
$date = userdate(time(), self::FILE_NAME_DATE_FORMAT) . "_" . rand();
$zipfilepath = self::get_quarantine_folder() . $date . self::FILE_ZIP_INFECTED;
$detailsfilename = $date . self::FILE_HTML_DETAILS;
// Create Zip file.
$ziparchive = new \zip_archive();
if ($ziparchive->open($zipfilepath, \file_archive::CREATE)) {
$ziparchive->add_file_from_string($detailsfilename, format_text($incidentdetails, FORMAT_MOODLE));
$ziparchive->add_file_from_string($filename, $data);
$ziparchive->close();
}
$zipfile = basename($zipfilepath);
self::create_infected_file_record($filename, $zipfile, $notice);
return $zipfile;
}
/**
* Check if the virus quarantine is allowed
*
* @return bool
* @throws \dml_exception
*/
public static function is_quarantine_enabled(): bool {
return !empty(get_config("antivirus", "enablequarantine"));
}
/**
* Get quarantine folder
*
* @return string path of quarantine folder
*/
private static function get_quarantine_folder(): string {
global $CFG;
$quarantinefolder = $CFG->dataroot . DIRECTORY_SEPARATOR . self::DEFAULT_QUARANTINE_FOLDER;
if (!file_exists($quarantinefolder)) {
make_upload_directory(self::DEFAULT_QUARANTINE_FOLDER);
}
return $quarantinefolder . DIRECTORY_SEPARATOR;
}
/**
* Checks whether a file exists inside the antivirus quarantine folder.
*
* @param string $filename the filename to check.
* @return boolean whether file exists.
*/
public static function quarantined_file_exists(string $filename): bool {
$folder = self::get_quarantine_folder();
return file_exists($folder . $filename);
}
/**
* Download quarantined file.
*
* @param int $fileid the id of file to be downloaded.
*/
public static function download_quarantined_file(int $fileid) {
global $DB;
// Get the filename to be downloaded.
$filename = $DB->get_field('infected_files', 'quarantinedfile', ['id' => $fileid], IGNORE_MISSING);
// If file record isnt found, user might be doing something naughty in params, or a stale request.
if (empty($filename)) {
return;
}
$file = self::get_quarantine_folder() . $filename;
send_file($file, $filename);
}
/**
* Delete quarantined file.
*
* @param int $fileid id of file to be deleted.
*/
public static function delete_quarantined_file(int $fileid) {
global $DB;
// Get the filename to be deleted.
$filename = $DB->get_field('infected_files', 'quarantinedfile', ['id' => $fileid], IGNORE_MISSING);
// If file record isnt found, user might be doing something naughty in params, or a stale request.
if (empty($filename)) {
return;
}
// Delete the file from the folder.
$file = self::get_quarantine_folder() . $filename;
if (file_exists($file)) {
unlink($file);
}
// Now we are finished with the record, delete the quarantine information.
self::delete_infected_file_record($fileid);
}
/**
* Download all quarantined files.
*
* @return void
*/
public static function download_all_quarantined_files() {
$files = new \DirectoryIterator(self::get_quarantine_folder());
// Add all infected files to a zip file.
$date = userdate(time(), self::FILE_NAME_DATE_FORMAT);
$zipfilename = $date . self::FILE_ZIP_ALL_INFECTED;
$zipfilepath = self::get_quarantine_folder() . DIRECTORY_SEPARATOR . $zipfilename;
$tempfilestocleanup = [];
$ziparchive = new \zip_archive();
if ($ziparchive->open($zipfilepath, \file_archive::CREATE)) {
foreach ($files as $file) {
if (!$file->isDot()) {
// Only send the actual files.
$filename = $file->getFilename();
$filepath = $file->getPathname();
$ziparchive->add_file_from_pathname($filename, $filepath);
}
}
$ziparchive->close();
}
// Clean up temp files.
foreach ($tempfilestocleanup as $tempfile) {
if (file_exists($tempfile)) {
unlink($tempfile);
}
}
send_temp_file($zipfilepath, $zipfilename);
}
/**
* Return array of quarantined files.
*
* @return array list of quarantined files.
*/
public static function get_quarantined_files(): array {
$files = new \DirectoryIterator(self::get_quarantine_folder());
$filestosort = [];
// Grab all files that match the naming structure.
foreach ($files as $file) {
$filename = $file->getFilename();
if (!$file->isDot() && strpos($filename, self::FILE_ZIP_INFECTED) !== false) {
$filestosort[$filename] = $file->getPathname();
}
}
krsort($filestosort, SORT_NATURAL);
return $filestosort;
}
/**
* Clean up quarantine folder
*
* @param int $timetocleanup time to clean up
*/
public static function clean_up_quarantine_folder(int $timetocleanup) {
$files = new \DirectoryIterator(self::get_quarantine_folder());
// Clean up the folder.
foreach ($files as $file) {
$filename = $file->getFilename();
// Only delete files that match the correct name structure.
if (!$file->isDot() && strpos($filename, self::FILE_ZIP_INFECTED) !== false) {
$modifiedtime = $file->getMTime();
if ($modifiedtime <= $timetocleanup) {
unlink($file->getPathname());
}
}
}
// Lastly cleanup the infected files table as well.
self::clean_up_infected_records($timetocleanup);
}
/**
* This function removes any stale records from the infected files table.
*
* @param int $timetocleanup the time to cleanup from
* @return void
*/
private static function clean_up_infected_records(int $timetocleanup) {
global $DB;
$select = "timecreated <= ?";
$DB->delete_records_select('infected_files', $select, [$timetocleanup]);
}
/**
* Create an infected file record
*
* @param string $filename original file name
* @param string $zipfile quarantined file name
* @param string $reason failure reason
* @throws \dml_exception
*/
private static function create_infected_file_record(string $filename, string $zipfile, string $reason) {
global $DB, $USER;
$record = new \stdClass();
$record->filename = $filename;
$record->quarantinedfile = $zipfile;
$record->userid = $USER->id;
$record->reason = $reason;
$record->timecreated = time();
$DB->insert_record('infected_files', $record);
}
/**
* Delete the database record for an infected file.
*
* @param int $fileid quarantined file id
* @throws \dml_exception
*/
private static function delete_infected_file_record(int $fileid) {
global $DB;
$DB->delete_records('infected_files', ['id' => $fileid]);
}
}
+251
View File
@@ -0,0 +1,251 @@
<?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.
*/
abstract public 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.
*/
abstract public 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' => []];
}
}
@@ -0,0 +1,50 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Exception for antivirus.
*
* @package core_antivirus
* @copyright 2015 Ruslan Kabalin, Lancaster University.
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\antivirus;
defined('MOODLE_INTERNAL') || die();
/**
* An antivirus scanner exception class.
*
* @package core
* @subpackage antivirus
* @copyright 2015 Ruslan Kabalin, Lancaster University.
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class scanner_exception extends \moodle_exception {
/**
* Constructs a new exception
*
* @param string $errorcode
* @param string $link
* @param mixed $a
* @param mixed $debuginfo
* @param string $module optional plugin name
*/
public function __construct($errorcode, $link = '', $a = null, $debuginfo = null, $module = 'antivirus') {
parent::__construct($errorcode, $module, $link, $a, $debuginfo);
}
}
+56
View File
@@ -0,0 +1,56 @@
<?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/>.
namespace core\attribute;
/**
* Attribute to describe a deprecated item.
*
* @package core
* @copyright 2023 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
#[\Attribute]
class deprecated {
/**
* A deprecated item.
*
* This attribute can be applied to any function, class, method, constant, property, enum, etc.
*
* Note: The mere presence of the attribute does not do anything. It must be checked by some part of the code.
*
* @param null|string $replacement Any replacement for the deprecated thing
* @param null|string $since When it was deprecated
* @param null|string $reason Why it was deprecated
* @param null|string $mdl Link to the Moodle Tracker issue for more information
* @param bool $final Whether this is a final deprecation
* @param bool $emit Whether to emit a deprecation warning
*/
public function __construct(
public readonly ?string $replacement,
public readonly ?string $since = null,
public readonly ?string $reason = null,
public readonly ?string $mdl = null,
public readonly bool $final = false,
public readonly bool $emit = true,
) {
if ($replacement === null && $reason === null && $mdl === null) {
throw new \coding_exception(
'A deprecated item which is not deprecated must provide a reason, or an issue number.',
);
}
}
}
@@ -0,0 +1,58 @@
<?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/>.
namespace core\attribute;
/**
* Attribute to describe a deprecated item which contains a reference to the owning feature.
*
* @package core
* @copyright 2023 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class deprecated_with_reference extends deprecated {
/**
* A deprecated item which also includes a reference to the owning feature.
*
* This attribute is not expected to be used more generally. It is an internal feature.
*
* @param string $owner The code which owns the usage
* @param null|string $replacement Any replacement for the deprecated thing
* @param null|string $since When it was deprecated
* @param null|string $reason Why it was deprecated
* @param null|string $mdl Link to the Moodle Tracker issue for more information
* @param bool $final Whether this is a final deprecation
* @param bool $emit Whether to emit a deprecation warning
*/
public function __construct(
public readonly string $owner,
?string $replacement,
?string $since,
?string $reason,
?string $mdl,
bool $final,
bool $emit,
) {
parent::__construct(
replacement: $replacement,
since: $since,
reason: $reason,
mdl: $mdl,
final: $final,
emit: $emit,
);
}
}
@@ -0,0 +1,35 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core\attribute\hook;
/**
* A set of callbacks that this hook replaces.
*
* @package core
* @copyright 2024 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
#[\Attribute]
class replaces_callbacks {
/** @var string[] A list of callbacks that this hook replaces */
public readonly array $callbacks;
public function __construct(
...$callbacks,
) {
$this->callbacks = $callbacks;
}
}
+32
View File
@@ -0,0 +1,32 @@
<?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/>.
namespace core\attribute;
/**
* An unstranslated string attribute used to label an object.
*
* @package core
* @copyright 2024 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
#[\Attribute]
class label {
public function __construct(
public readonly string $label,
) {
}
}
+37
View File
@@ -0,0 +1,37 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core\attribute;
/**
* A set of string tags used to categorise an object.
*
* Note: These are not the same as the tags API.
*
* @package core
* @copyright 2024 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
#[\Attribute]
class tags {
/** @var string[] A list of tags */
public readonly array $tags;
public function __construct(
...$tags,
) {
$this->tags = $tags;
}
}
+245
View File
@@ -0,0 +1,245 @@
<?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/>.
namespace core;
use ReflectionAttribute;
/**
* Helper for loading attributes.
*
* @package core
* @copyright 2024 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class attribute_helper {
/**
* Get an instance of an attribute from a reference.
*
* The reference can be:
* - a string, in which case it will be checked for a function, class, method, property, constant, or enum.
* - an array
* - an instantiated object, in which case the object will be checked for a class, method, property, or constant.
*
* @param array|string|object $reference A reference of where to find the attribute
* @param null|string $attributename The name of the attribute to find
* @param int $attributeflags The flags to use when finding the attribute
* @return ?object
*/
public static function instance(
array|string|object $reference,
?string $attributename = null,
int $attributeflags = ReflectionAttribute::IS_INSTANCEOF,
): ?object {
return self::one_from($reference, $attributename, $attributeflags)?->newInstance();
}
/**
* Get all instance of an attribute from a reference.
*
* The reference can be:
* - a string, in which case it will be checked for a function, class, method, property, constant, or enum.
* - an array
* - an instantiated object, in which case the object will be checked for a class, method, property, or constant.
*
* @param array|string|object $reference A reference of where to find the attribute
* @param null|string $attributename The name of the attribute to find
* @param int $attributeflags The flags to use when finding the attribute
* @return ?object[]
*/
public static function instances(
array|string|object $reference,
?string $attributename = null,
int $attributeflags = ReflectionAttribute::IS_INSTANCEOF,
): ?array {
if ($attributes = self::from($reference, $attributename, $attributeflags)) {
return array_map(fn ($attribute) => $attribute->newInstance(), $attributes);
}
return null;
}
/**
* Get one attribute from a reference.
*
* The reference can be:
* - a string, in which case it will be checked for a function, class, method, property, constant, or enum.
* - an array
* - an instantiated object, in which case the object will be checked for a class, method, property, or constant.
*
* @param array|string|object $reference A reference of where to find the attribute
* @param null|string $attributename The name of the attribute to find
* @param int $attributeflags The flags to use when finding the attribute
* @return \ReflectionAttribute|null
*/
public static function one_from(
array|string|object $reference,
?string $attributename = null,
int $attributeflags = ReflectionAttribute::IS_INSTANCEOF,
): ?\ReflectionAttribute {
$attributes = self::from($reference, $attributename, $attributeflags);
if ($attributes && count($attributes) > 1) {
throw new \coding_exception('More than one attribute found');
}
return $attributes ? $attributes[0] : null;
}
/**
* Get the attribute from a reference.
*
* The reference can be:
* - a string, in which case it will be checked for a function, class, method, property, constant, or enum.
* - an array
* - an instantiated object, in which case the object will be checked for a class, method, property, or constant.
*
* @param array|string|object $reference A reference of where to find the attribute
* @param null|string $attributename The name of the attribute to find
* @param int $attributeflags The flags to use when finding the attribute
* @return \ReflectionAttribute[]|null
*/
public static function from(
array|string|object $reference,
?string $attributename = null,
int $attributeflags = ReflectionAttribute::IS_INSTANCEOF,
): ?array {
if (is_string($reference)) {
if (str_contains($reference, '::')) {
// The reference is a string but it looks to be in the format `object::item`.
return self::from(explode('::', $reference), $attributename, $attributeflags);;
}
if (class_exists($reference)) {
// The reference looks to be a class name.
return self::from([$reference], $attributename, $attributeflags);
}
if (function_exists($reference)) {
// The reference looks to be a global function.
$ref = new \ReflectionFunction($reference);
return $ref->getAttributes(
name: $attributename,
flags: $attributeflags,
);
}
return null;
}
if (is_object($reference)) {
// The reference is an object. Normalise and check again.
return self::from([$reference], $attributename, $attributeflags);
}
if (is_array($reference) && count($reference)) {
if (is_object($reference[0])) {
// The first array key is an instance of a class, enum, etc.
$rc = new \ReflectionObject($reference[0]);
if ($rc->isEnum() && $reference[0]->name) {
// Enums can be passed via ::from([enum::NAME]).
// In this case they will have a 'name', which must exist.
return self::from_reflected_object(
rc: $rc,
referenceproperty: $reference[0]->name,
attributename: $attributename,
flags: $attributeflags,
);
}
// The object is an instance of a class, or similar.
// That means that, if provided, the second array key is the name of the property, constant, method, etc.
return self::from_reflected_object(
rc: $rc,
referenceproperty: $reference[1] ?? null,
attributename: $attributename,
flags: $attributeflags,
);
}
if (is_string($reference[0]) && class_exists($reference[0])) {
// The first array key is a class name.
// That means that, if provided, the second array key is the name of the property, constant, method, etc.
$rc = new \ReflectionClass($reference[0]);
return self::from_reflected_object(
rc: $rc,
referenceproperty: $reference[1] ?? null,
attributename: $attributename,
flags: $attributeflags,
);
}
// The reference is an array, but it's not an object or a class that currently exists.
return null;
}
}
/**
* Fetch an attribute from a reflected object.
*
* @param \ReflectionClass $rc The reflected object
* @param null|string $referenceproperty The name of the thing to find attributes on
* @param null|string $attributename The name of the attribute to find
* @param int $attributeflags The flags to use when finding the attribute
* @return \ReflectionAttribute[]|null
*/
protected static function from_reflected_object(
\ReflectionClass $rc,
?string $referenceproperty,
?string $attributename = null,
int $flags = 0,
): ?array {
if ($referenceproperty === null) {
// No name specified - may be the whole class..
return $rc->getAttributes(
name: $attributename,
flags: $flags,
);
}
if ($rc->hasConstant($referenceproperty)) {
// This class has a constant with the specified name.
// Note: This also applies to enums.
$ref = $rc->getReflectionConstant($referenceproperty);
return $ref->getAttributes(
name: $attributename,
flags: $flags,
);
}
if ($rc->hasMethod($referenceproperty)) {
// This class has a method with the specified name.
$ref = $rc->getMethod($referenceproperty);
return $ref->getAttributes(
name: $attributename,
flags: $flags,
);
}
if ($rc->hasProperty($referenceproperty)) {
// This class has a property with the specified name.
$ref = $rc->getProperty($referenceproperty);
return $ref->getAttributes(
name: $attributename,
flags: $flags,
);
}
return null;
}
}
@@ -0,0 +1,77 @@
<?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/>.
/**
* Admin setting for AWS regions.
*
* @package core
* @author Dmitrii Metelkin <dmitriim@catalyst-au.net>
* @copyright 2020 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\aws;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/lib/adminlib.php');
/**
* Admin setting for a list of AWS regions.
*
* @package core
* @copyright 2020 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class admin_settings_aws_region extends \admin_setting_configtext {
/**
* Return part of form with setting.
*
* @param mixed $data array or string depending on setting
* @param string $query
* @return string
*/
public function output_html($data, $query='') {
global $CFG, $OUTPUT;
$default = $this->get_defaultsetting();
$options = [];
// We do require() not require_once() here, as the file returns a value and we may need to get
// this value more than once.
$all = require($CFG->dirroot . '/lib/aws-sdk/src/data/endpoints.json.php');
$ends = $all['partitions'][0]['regions'];
if ($ends) {
foreach ($ends as $key => $value) {
$options[] = [
'value' => $key,
'label' => $key . ' - ' . $value['description'],
];
}
}
$context = [
'list' => $this->get_full_name(),
'name' => $this->get_full_name(),
'id' => $this->get_id(),
'value' => $data,
'size' => $this->size,
'options' => $options,
];
$element = $OUTPUT->render_from_template('core/aws/setting_aws_region', $context);
return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $default, $query);
}
}
+100
View File
@@ -0,0 +1,100 @@
<?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/>.
/**
* AWS helper class. Contains useful functions when interacting with the SDK.
*
* @package core
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright 2020 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\aws;
use Aws\CommandInterface;
use Aws\AwsClient;
use Psr\Http\Message\RequestInterface;
/**
* This class contains functions that help plugins to interact with the AWS SDK.
*
* @copyright 2020 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class aws_helper {
/**
* This creates a proxy string suitable for use with the AWS SDK.
*
* @return string the string to use for proxy settings.
*/
public static function get_proxy_string(): string {
global $CFG;
$proxy = '';
if (empty($CFG->proxytype)) {
return $proxy;
}
if ($CFG->proxytype === 'SOCKS5') {
// If it is a SOCKS proxy, append the protocol info.
$protocol = 'socks5://';
} else {
$protocol = '';
}
if (!empty($CFG->proxyhost)) {
$proxy = $CFG->proxyhost;
if (!empty($CFG->proxyport)) {
$proxy .= ':'. $CFG->proxyport;
}
if (!empty($CFG->proxyuser) && !empty($CFG->proxypassword)) {
$proxy = $protocol . $CFG->proxyuser . ':' . $CFG->proxypassword . '@' . $proxy;
}
}
return $proxy;
}
/**
* Configure the provided AWS client to route traffic via the moodle proxy for any hosts not excluded.
*
* @param AwsClient $client
* @return AwsClient
*/
public static function configure_client_proxy(AwsClient $client): AwsClient {
$client->getHandlerList()->appendBuild(self::add_proxy_when_required(), 'proxy_bypass');
return $client;
}
/**
* Generate a middleware higher order function to wrap the handler and append proxy configuration based on target.
*
* @return callable Middleware high order callable.
*/
protected static function add_proxy_when_required(): callable {
return function (callable $fn) {
return function (CommandInterface $command, ?RequestInterface $request = null) use ($fn) {
if (isset($request)) {
$target = (string) $request->getUri();
if (!is_proxybypass($target)) {
$command['@http']['proxy'] = self::get_proxy_string();
}
}
$promise = $fn($command, $request);
return $promise;
};
};
}
}
+62
View File
@@ -0,0 +1,62 @@
<?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/>.
/**
* AWS Client factory. Retrieves a client with moodle specific HTTP configuration.
*
* @package core
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright 2022 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\aws;
use Aws\AwsClient;
/**
* AWS Client factory. Retrieves a client with moodle specific HTTP configuration.
*
* @copyright 2022 Catalyst IT
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class client_factory {
/**
* Get an AWS client with moodle specific HTTP configuration.
*
* @param string $class Fully qualified AWS classname e.g. \Aws\S3\S3Client
* @param array $opts array of constructor options for AWS Client.
* @return AwsClient
*/
public static function get_client(string $class, array $opts): AwsClient {
// Modify the opts to add HTTP timeouts.
if (empty($opts['http'])) {
$opts['http'] = ['connect_timeout' => HOURSECS];
} else if (!array_key_exists('connect_timeout', $opts['http'])) {
// Try not to override existing settings.
$opts['http']['connect_timeout'] = HOURSECS;
}
// Blindly trust the call here. If it exceptions, the raw message is the most useful.
$client = new $class($opts);
if (!$client instanceof \Aws\AwsClient) {
throw new \moodle_exception('clientnotfound', 'factor_sms');
}
// Now we can configure the proxy with the routing aware middleware.
return aws_helper::configure_client_proxy($client);
}
}
+197
View File
@@ -0,0 +1,197 @@
<?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/>.
/**
* Chart axis.
*
* @package core
* @copyright 2016 Frédéric Massart - FMCorz.net
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core;
defined('MOODLE_INTERNAL') || die();
use coding_exception;
use JsonSerializable;
use renderable;
/**
* Chart axis class.
*
* @package core
* @copyright 2016 Frédéric Massart - FMCorz.net
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class chart_axis implements JsonSerializable {
/** Default axis position. */
const POS_DEFAULT = null;
/** Bottom axis position. */
const POS_BOTTOM = 'bottom';
/** Left axis position. */
const POS_LEFT = 'left';
/** Right axis position. */
const POS_RIGHT = 'right';
/** Top axis position. */
const POS_TOP = 'top';
/** @var string The axis label. */
protected $label = null;
/** @var string[] The axis labels, tick values. */
protected $labels = null;
/** @var float The maximum tick value. */
protected $max = null;
/** @var float The minimum tick value. */
protected $min = null;
/** @var string The axis position. */
protected $position = self::POS_DEFAULT;
/** @var float The stepsize between ticks. */
protected $stepsize = null;
/**
* Constructor.
*
* Must not take any argument.
*/
public function __construct() {
}
/**
* Get the label.
*
* @return string
*/
public function get_label() {
return $this->label;
}
/**
* Get the labels.
*
* @return string[]
*/
public function get_labels() {
return $this->labels;
}
/**
* Get the max value.
*
* @return float
*/
public function get_max() {
return $this->max;
}
/**
* Get the min value.
*
* @return float
*/
public function get_min() {
return $this->min;
}
/**
* Get the axis position.
*
* @return string
*/
public function get_position() {
return $this->position;
}
/**
* Get the step size.
*
* @return float
*/
public function get_stepsize() {
return $this->stepsize;
}
/**
* Serialize the object.
*
* @return array
*/
public function jsonSerialize(): array {
return [
'label' => $this->label,
'labels' => $this->labels,
'max' => $this->max,
'min' => $this->min,
'position' => $this->position,
'stepSize' => $this->stepsize,
];
}
/**
* Set the label.
*
* @param string $label The label.
*/
public function set_label($label) {
$this->label = $label;
}
/**
* Set the labels.
*
* @param string[] $labels The labels.
*/
public function set_labels($labels) {
$this->labels = $labels;
}
/**
* Set the max value.
*
* @param float $max The max value.
*/
public function set_max($max) {
$this->max = $max;
}
/**
* Set the min value.
*
* @param float $min The min value.
*/
public function set_min($min) {
$this->min = $min;
}
/**
* Set the position.
*
* @param string $position Use constant self::POS_*.
*/
public function set_position($position) {
$this->position = $position;
}
/**
* Set the step size.
*
* @param float $stepsize The step size.
*/
public function set_stepsize($stepsize) {
$this->stepsize = $stepsize;
}
}
+97
View File
@@ -0,0 +1,97 @@
<?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/>.
/**
* Chart bar.
*
* @package core
* @copyright 2016 Frédéric Massart - FMCorz.net
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core;
defined('MOODLE_INTERNAL') || die();
/**
* Chart bar class.
*
* @package core
* @copyright 2016 Frédéric Massart - FMCorz.net
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class chart_bar extends chart_base {
/** @var bool Whether the bars should be displayed horizontally or not. */
protected $horizontal = false;
/** @var bool Whether the chart should be stacked or not. */
protected $stacked = null;
/**
* Add the horizontal to the parent and return the serialized data.
*
* @return array
*/
public function jsonSerialize(): array {
$data = parent::jsonSerialize();
$data['horizontal'] = $this->get_horizontal();
$data['stacked'] = $this->get_stacked();
return $data;
}
/**
* Set the defaults.
*/
protected function set_defaults() {
parent::set_defaults();
$yaxis = $this->get_yaxis(0, true);
$yaxis->set_min(0);
}
/**
* Get whether the bars should be displayed horizontally or not.
*
* @return bool
*/
public function get_horizontal() {
return $this->horizontal;
}
/**
* Get whether the bars should be stacked or not.
*
* @return bool
*/
public function get_stacked() {
return $this->stacked;
}
/**
* Set whether the bars should be displayed horizontally or not.
*
* @param bool $horizontal True if the bars should be displayed horizontally, false otherwise.
*/
public function set_horizontal($horizontal) {
$this->horizontal = $horizontal;
}
/**
* Set whether the bars should be stacked or not.
*
* @param bool $stacked True if the chart should be stacked or false otherwise.
*/
public function set_stacked($stacked) {
$this->stacked = $stacked;
}
}
+306
View File
@@ -0,0 +1,306 @@
<?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/>.
/**
* Chart base.
*
* @package core
* @copyright 2016 Frédéric Massart - FMCorz.net
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core;
defined('MOODLE_INTERNAL') || die();
use coding_exception;
use JsonSerializable;
use renderable;
/**
* Chart base class.
*
* @package core
* @copyright 2016 Frédéric Massart - FMCorz.net
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class chart_base implements JsonSerializable, renderable {
/** @var chart_series[] The series constituting this chart. */
protected $series = [];
/** @var string[] The labels for the X axis when categorised. */
protected $labels = [];
/** @var string The title of the chart. */
protected $title = null;
/** @var chart_axis[] The X axes. */
protected $xaxes = [];
/** @var chart_axis[] The Y axes. */
protected $yaxes = [];
/** @var array Options for the chart legend. */
protected $legendoptions = [];
/**
* Constructor.
*
* Must not take any argument.
*
* Most of the time you do not want to extend this, rather extend the
* method {@link self::set_defaults} to set the defaults on instantiation.
*/
public function __construct() {
$this->set_defaults();
}
/**
* Add a series to the chart.
*
* @param chart_series $serie The serie.
*/
public function add_series(chart_series $serie) {
$this->series[] = $serie;
}
/**
* Serialize the object.
*
* @return array
*/
public function jsonSerialize(): array {
global $CFG;
return [
'type' => $this->get_type(),
'series' => $this->series,
'labels' => $this->labels,
'title' => $this->title,
'axes' => [
'x' => $this->xaxes,
'y' => $this->yaxes,
],
'legend_options' => !empty($this->legendoptions) ? $this->legendoptions : null,
'config_colorset' => !empty($CFG->chart_colorset) ? $CFG->chart_colorset : null
];
}
/**
* Get an axis.
*
* @param string $type Accepts values 'x' or 'y'.
* @param int $index The index of this axis.
* @param bool $createifnotexists Whether to create the axis if not found.
* @return chart_axis
*/
private function get_axis($type, $index, $createifnotexists) {
$isx = $type === 'x';
if ($isx) {
$axis = isset($this->xaxes[$index]) ? $this->xaxes[$index] : null;
} else {
$axis = isset($this->yaxes[$index]) ? $this->yaxes[$index] : null;
}
if ($axis === null) {
if (!$createifnotexists) {
throw new coding_exception('Unknown axis.');
}
$axis = new chart_axis();
if ($isx) {
$this->set_xaxis($axis, $index);
} else {
$this->set_yaxis($axis, $index);
}
}
return $axis;
}
/**
* Get the labels of the X axis.
*
* @return string[]
*/
public function get_labels() {
return $this->labels;
}
/**
* Get an array of options for the chart legend.
*
* @return array
*/
public function get_legend_options() {
return $this->legendoptions;
}
/**
* Get the series.
*
* @return chart_series[]
*/
public function get_series() {
return $this->series;
}
/**
* Get the title.
*
* @return string
*/
public function get_title() {
return $this->title;
}
/**
* Get the chart type.
*
* @return string
*/
public function get_type() {
$classname = get_class($this);
return substr($classname, strpos($classname, '_') + 1);
}
/**
* Get the X axes.
*
* @return chart_axis[]
*/
public function get_xaxes() {
return $this->xaxes;
}
/**
* Get an X axis.
*
* @param int $index The index of the axis.
* @param bool $createifnotexists When true, create an instance of the axis if none exist at this index yet.
* @return chart_axis
*/
public function get_xaxis($index = 0, $createifnotexists = false) {
return $this->get_axis('x', $index, $createifnotexists);
}
/**
* Get the Y axes.
*
* @return chart_axis[]
*/
public function get_yaxes() {
return $this->yaxes;
}
/**
* Get a Y axis.
*
* @param int $index The index of the axis.
* @param bool $createifnotexists When true, create an instance of the axis if none exist at this index yet.
* @return chart_axis
*/
public function get_yaxis($index = 0, $createifnotexists = false) {
return $this->get_axis('y', $index, $createifnotexists);
}
/**
* Set the defaults for this chart type.
*
* Child classes can extend this to set default values on instantiation.
*
* In general the constructor could be used, but this method is here to
* emphasize and self-document the default values set by the chart type.
*
* @return void
*/
protected function set_defaults() {
}
/**
* Set the chart labels.
*
* @param string[] $labels The labels.
*/
public function set_labels(array $labels) {
$this->labels = $labels;
}
/**
* Set options for the chart legend.
* See https://www.chartjs.org/docs/2.7.0/configuration/legend.html for options.
*
* Note: Setting onClick and onHover events is not directly supported through
* this method. These config options must be set directly within Javascript
* on the page.
*
* @param array $legendoptions Whether or not to display the chart's legend.
*/
public function set_legend_options(array $legendoptions) {
$this->legendoptions = $legendoptions;
}
/**
* Set the title.
*
* @param string $title The title.
*/
public function set_title($title) {
$this->title = $title;
}
/**
* Set an X axis.
*
* Note that this will override any predefined axis without warning.
*
* @param chart_axis $axis The axis.
* @param int $index The index of the axis.
*/
public function set_xaxis(chart_axis $axis, $index = 0) {
$this->validate_axis('x', $axis, $index);
return $this->xaxes[$index] = $axis;
}
/**
* Set an Y axis.
*
* Note that this will override any predefined axis without warning.
*
* @param chart_axis $axis The axis.
* @param int $index The index of the axis.
*/
public function set_yaxis(chart_axis $axis, $index = 0) {
$this->validate_axis('y', $axis, $index);
return $this->yaxes[$index] = $axis;
}
/**
* Validate an axis.
*
* We validate this from PHP because not doing it here could result in errors being
* hard to trace down. For instance, if we were to add axis at keys without another
* axis preceding, we would effectively contain the axes in an associative array
* rather than a simple array, and that would have consequences on serialisation.
*
* @param string $xy Accepts x or y.
* @param chart_axis $axis The axis to validate.
* @param index $index The index of the axis.
*/
protected function validate_axis($xy, chart_axis $axis, $index = 0) {
if ($index > 0) {
$axes = $xy == 'x' ? $this->xaxes : $this->yaxes;
if (!isset($axes[$index - 1])) {
throw new coding_exception('Missing ' . $xy . ' axis at index lower than ' . $index);
}
}
}
}
+68
View File
@@ -0,0 +1,68 @@
<?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/>.
/**
* Chart line.
*
* @package core
* @copyright 2016 Frédéric Massart - FMCorz.net
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core;
defined('MOODLE_INTERNAL') || die();
/**
* Chart line class.
*
* @package core
* @copyright 2016 Frédéric Massart - FMCorz.net
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class chart_line extends chart_base {
/** @var bool Whether the line should be smooth or not. */
protected $smooth = false;
/**
* Add the smooth to the parent and return the serialized data.
*
* @return array
*/
public function jsonSerialize(): array {
$data = parent::jsonSerialize();
$data['smooth'] = $this->get_smooth();
return $data;
}
/**
* Get whether a lines should be smooth or not.
*
* @return bool
*/
public function get_smooth() {
return $this->smooth;
}
/**
* Set Whether the line should be smooth or not.
*
* @param bool $smooth True if the line chart should be smooth, false otherwise.
*/
public function set_smooth($smooth) {
$this->smooth = $smooth;
}
}
+68
View File
@@ -0,0 +1,68 @@
<?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/>.
/**
* Chart pie.
*
* @package core
* @copyright 2016 Frédéric Massart - FMCorz.net
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core;
defined('MOODLE_INTERNAL') || die();
/**
* Chart pie class.
*
* @package core
* @copyright 2016 Frédéric Massart - FMCorz.net
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class chart_pie extends chart_base {
/** @var bool $doughnut Whether the chart should be displayed as doughnut. */
protected $doughnut = null;
/**
* Get parent JSON and add specific pie related attributes and values.
*
* @return array
*/
public function jsonSerialize(): array {
$data = parent::jsonSerialize();
$data['doughnut'] = $this->get_doughnut();
return $data;
}
/**
* Get whether the chart should be displayed as doughnut.
*
* @return bool
*/
public function get_doughnut() {
return $this->doughnut;
}
/**
* Set whether the chart should be displayed as doughnut.
*
* @param bool $doughnut True for doughnut type, false for pie.
*/
public function set_doughnut($doughnut) {
$this->doughnut = $doughnut;
}
}
+279
View File
@@ -0,0 +1,279 @@
<?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/>.
/**
* Chart series.
*
* @package core
* @copyright 2016 Frédéric Massart - FMCorz.net
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core;
defined('MOODLE_INTERNAL') || die();
use coding_exception;
use JsonSerializable;
/**
* Chart series class.
*
* @package core
* @copyright 2016 Frédéric Massart - FMCorz.net
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class chart_series implements JsonSerializable {
/** Default type for a series. */
const TYPE_DEFAULT = null;
/** Series of type line. */
const TYPE_LINE = 'line';
/** @var string[] Colors of the series. */
protected $colors = [];
/** @var string Fill mode for area charts. See https://www.chartjs.org/docs/latest/charts/area.html */
protected $fill = null;
/** @var string Label for this series. */
protected $label;
/** @var string[] Labels for the values of the series. */
protected $labels = null;
/** @var bool Whether the line of the serie should be smooth or not. */
protected $smooth = null;
/** @var string Type of the series. */
protected $type = self::TYPE_DEFAULT;
/** @var float[] Values of the series. */
protected $values = [];
/** @var int Index of the X axis. */
protected $xaxis = null;
/** @var int Index of the Y axis. */
protected $yaxis = null;
/**
* Constructor.
*
* @param string $label The label of the series.
* @param float[] $values The values of this series.
*/
public function __construct($label, $values) {
$this->values = $values;
$this->label = $label;
}
/**
* Get the color.
*
* @return string|null
*/
public function get_color() {
return isset($this->colors[0]) ? $this->colors[0] : null;
}
/**
* Get the colors for each value in the series.
*
* @return string[]
*/
public function get_colors() {
return $this->colors;
}
/**
* Get the number of values in this series.
*
* @return int
*/
public function get_count() {
return count($this->values);
}
/**
* Get area fill mode for series.
*/
public function get_fill() {
return $this->fill;
}
/**
* Get the label of the series.
*
* @return string
*/
public function get_label() {
return $this->label;
}
/**
* Set labels for the values of the series.
*
* @return array
*/
public function get_labels() {
return $this->labels;
}
/**
* Get whether the line of the serie should be smooth or not.
*
* @return bool
*/
public function get_smooth() {
return $this->smooth;
}
/**
* Get the type of series.
*
* @return string
*/
public function get_type() {
return $this->type;
}
/**
* Get the values of the series.
*
* @return string[]
*/
public function get_values() {
return $this->values;
}
/**
* Get the index of the X axis.
*
* @return int
*/
public function get_xaxis() {
return $this->xaxis;
}
/**
* Get the index of the Y axis.
*
* @return int
*/
public function get_yaxis() {
return $this->yaxis;
}
/**
* Whether there is a color per value.
*
* @return bool
*/
public function has_colored_values() {
return count($this->colors) == $this->get_count();
}
/**
* Serialize the object.
*
* @return array
*/
public function jsonSerialize(): array {
$data = [
'label' => $this->label,
'labels' => $this->labels,
'type' => $this->type,
'values' => $this->values,
'colors' => $this->colors,
'fill' => $this->fill,
'axes' => [
'x' => $this->xaxis,
'y' => $this->yaxis,
],
'smooth' => $this->smooth
];
return $data;
}
/**
* Set the color of the series.
*
* @param string $color CSS compatible color.
*/
public function set_color($color) {
$this->colors = [$color];
}
/**
* Set a color for each value in the series.
*
* @param string[] $colors CSS compatible colors.
*/
public function set_colors(array $colors) {
$this->colors = $colors;
}
/**
* Set fill mode for the series.
* @param string $fill
*/
public function set_fill($fill) {
$this->fill = $fill;
}
/**
* Set labels for the values of the series.
*
* @param array $labels The labels for the series values.
*/
public function set_labels($labels) {
$this->labels = $labels;
}
/**
* Set whether the line of the serie should be smooth or not.
*
* Only applicable for line chart or a line series, if null it assumes the chart default (not smooth).
*
* @param bool $smooth True if the line should be smooth, false for tensioned lines.
*/
public function set_smooth($smooth) {
$this->smooth = $smooth;
}
/**
* Set the type of the series.
*
* @param string $type Constant value from self::TYPE_*.
*/
public function set_type($type) {
if (!in_array($type, [self::TYPE_DEFAULT, self::TYPE_LINE])) {
throw new coding_exception('Invalid serie type.');
}
$this->type = $type;
}
/**
* Set the index of the X axis.
*
* @param int $index The index.
*/
public function set_xaxis($index) {
$this->xaxis = $index;
}
/**
* Set the index of the Y axis.
*
* @param int $index The index.
*/
public function set_yaxis($index) {
$this->yaxis = $index;
}
}
@@ -0,0 +1,115 @@
<?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/>.
namespace core\check\access;
use core\check\check;
use core\check\result;
/**
* Verifies sanity of default user role.
*
* @package core
* @category check
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @copyright 2008 petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class defaultuserrole extends check {
/**
* Get the short check name
*
* @return string
*/
public function get_name(): string {
return get_string('check_defaultuserrole_name', 'report_security');
}
/**
* A link to a place to action this
*
* @return \action_link|null
*/
public function get_action_link(): ?\action_link {
global $CFG, $DB;
$defaultrole = $DB->get_record('role', ['id' => $CFG->defaultuserroleid]);
return new \action_link(
new \moodle_url('/admin/roles/define.php', ['action' => 'view', 'roleid' => $defaultrole->id]),
get_string('definitionofrolex', 'core_role', role_get_name($defaultrole))
);
}
/**
* Return result
* @return result
*/
public function get_result(): result {
global $DB, $CFG;
$details = '';
if (!$defaultrole = $DB->get_record('role', ['id' => $CFG->defaultuserroleid])) {
$status = result::WARNING;
$summary = get_string('check_defaultuserrole_notset', 'report_security');
return new result($status, $summary, $details);
}
// Risky caps - usually very dangerous.
$sql = "SELECT rc.id, rc.contextid, rc.capability
FROM {role_capabilities} rc
JOIN {capabilities} cap ON cap.name = rc.capability
WHERE " . $DB->sql_bitand('cap.riskbitmask', (RISK_XSS | RISK_CONFIG | RISK_DATALOSS)) . " <> 0
AND rc.permission = :capallow
AND rc.roleid = :roleid";
$riskyresults = $DB->get_records_sql($sql, [
'capallow' => CAP_ALLOW,
'roleid' => $defaultrole->id,
]);
// If automatic approval is disabled, then the requestdelete capability is not risky.
if (!get_config('tool_dataprivacy', 'automaticdatadeletionapproval')) {
$riskyresults = array_filter($riskyresults, function ($object) {
return $object->capability !== 'tool/dataprivacy:requestdelete';
});
}
// Count the number of unique contexts that have risky caps.
$riskycount = count(array_unique(array_column($riskyresults, 'contextid')));
// It may have either none or 'user' archetype - nothing else, or else it would break during upgrades badly.
if ($defaultrole->archetype === '' or $defaultrole->archetype === 'user') {
$legacyok = true;
} else {
$legacyok = false;
}
if ($riskycount or !$legacyok) {
$status = result::CRITICAL;
$summary = get_string('check_defaultuserrole_error', 'report_security', role_get_name($defaultrole));
} else {
$status = result::OK;
$summary = get_string('check_defaultuserrole_ok', 'report_security');
}
$details = get_string('check_defaultuserrole_details', 'report_security');
return new result($status, $summary, $details);
}
}
+111
View File
@@ -0,0 +1,111 @@
<?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/>.
/**
* Verifies sanity of frontpage role
*
* @package core
* @category check
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @copyright 2008 petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\check\access;
defined('MOODLE_INTERNAL') || die();
use core\check\check;
use core\check\result;
/**
* Verifies sanity of frontpage role
*
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @copyright 2008 petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class frontpagerole extends check {
/**
* Get the short check name
*
* @return string
*/
public function get_name(): string {
return get_string('check_frontpagerole_name', 'report_security');
}
/**
* A link to a place to action this
*
* @return \action_link|null
*/
public function get_action_link(): ?\action_link {
return new \action_link(
new \moodle_url('/admin/settings.php?section=frontpagesettings#admin-defaultfrontpageroleid'),
get_string('frontpagesettings', 'admin'));
}
/**
* Return result
* @return result
*/
public function get_result(): result {
global $DB, $CFG;
if (!$frontpagerole = $DB->get_record('role', array('id' => $CFG->defaultfrontpageroleid))) {
$status = result::INFO;
$summary = get_string('check_frontpagerole_notset', 'report_security');
$details = get_string('check_frontpagerole_details', 'report_security');
return new result($status, $summary, $details);
}
// Risky caps - usually very dangerous.
$sql = "SELECT COUNT(DISTINCT rc.contextid)
FROM {role_capabilities} rc
JOIN {capabilities} cap ON cap.name = rc.capability
WHERE " . $DB->sql_bitand('cap.riskbitmask', (RISK_XSS | RISK_CONFIG | RISK_DATALOSS)) . " <> 0
AND rc.permission = :capallow
AND rc.roleid = :roleid";
$riskycount = $DB->count_records_sql($sql, [
'capallow' => CAP_ALLOW,
'roleid' => $frontpagerole->id,
]);
// There is no legacy role type for frontpage yet - anyway we can not allow teachers or admins there!
if ($frontpagerole->archetype === 'teacher' or $frontpagerole->archetype === 'editingteacher'
or $frontpagerole->archetype === 'coursecreator' or $frontpagerole->archetype === 'manager') {
$legacyok = false;
} else {
$legacyok = true;
}
if ($riskycount or !$legacyok) {
$status = result::CRITICAL;
$summary = get_string('check_frontpagerole_error', 'report_security', role_get_name($frontpagerole));
} else {
$status = result::OK;
$summary = get_string('check_frontpagerole_ok', 'report_security');
}
$details = get_string('check_frontpagerole_details', 'report_security');
return new result($status, $summary, $details);
}
}
+109
View File
@@ -0,0 +1,109 @@
<?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/>.
/**
* Verifies sanity of guest role
*
* @package core
* @category check
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @copyright 2008 petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\check\access;
defined('MOODLE_INTERNAL') || die();
use core\check\check;
use core\check\result;
/**
* Verifies sanity of guest role
*
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @copyright 2008 petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class guestrole extends check {
/**
* Get the short check name
*
* @return string
*/
public function get_name(): string {
return get_string('check_guestrole_name', 'report_security');
}
/**
* A link to a place to action this
*
* @return \action_link|null
*/
public function get_action_link(): ?\action_link {
return new \action_link(
new \moodle_url('/admin/settings.php?section=userpolicies'),
get_string('userpolicies', 'admin'));
}
/**
* Return result
* @return result
*/
public function get_result(): result {
global $DB, $CFG;
if (!$guestrole = $DB->get_record('role', ['id' => $CFG->guestroleid])) {
$status = result::WARNING;
$summary = get_string('check_guestrole_notset', 'report_security');
return new result($status, $summary);
}
// Risky caps - usually very dangerous.
$sql = "SELECT COUNT(DISTINCT rc.contextid)
FROM {role_capabilities} rc
JOIN {capabilities} cap ON cap.name = rc.capability
WHERE " . $DB->sql_bitand('cap.riskbitmask', (RISK_XSS | RISK_CONFIG | RISK_DATALOSS)) . " <> 0
AND rc.permission = :capallow
AND rc.roleid = :roleid";
$riskycount = $DB->count_records_sql($sql, [
'capallow' => CAP_ALLOW,
'roleid' => $guestrole->id,
]);
// It may have either no or 'guest' archetype - nothing else, or else it would break during upgrades badly.
if ($guestrole->archetype === '' or $guestrole->archetype === 'guest') {
$legacyok = true;
} else {
$legacyok = false;
}
if ($riskycount or !$legacyok) {
$status = result::CRITICAL;
$summary = get_string('check_guestrole_error', 'report_security', format_string($guestrole->name));
} else {
$status = result::OK;
$summary = get_string('check_guestrole_ok', 'report_security');
}
$details = get_string('check_guestrole_details', 'report_security');
return new result($status, $summary, $details);
}
}
+91
View File
@@ -0,0 +1,91 @@
<?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/>.
/**
* Lists all admins.
*
* @package core
* @category check
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @copyright 2008 petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\check\access;
defined('MOODLE_INTERNAL') || die();
use core\check\check;
use core\check\result;
/**
* Lists all admins.
*
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @copyright 2008 petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class riskadmin extends check {
/**
* Get the short check name
*
* @return string
*/
public function get_name(): string {
return get_string('check_riskadmin_name', 'report_security');
}
/**
* A link to a place to action this
*
* @return \action_link|null
*/
public function get_action_link(): ?\action_link {
return new \action_link(
new \moodle_url('/admin/roles/admins.php'),
get_string('siteadministrators', 'role'));
}
/**
* Return result
* @return result
*/
public function get_result(): result {
global $DB, $CFG;
$userfieldsapi = \core_user\fields::for_userpic();
$userfields = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
$sql = "SELECT $userfields
FROM {user} u
WHERE u.id IN ($CFG->siteadmins)";
$admins = $DB->get_records_sql($sql);
$admincount = count($admins);
foreach ($admins as $uid => $user) {
$url = "$CFG->wwwroot/user/view.php?id=$user->id";
$link = \html_writer::link($url, fullname($user, true) . ' (' . s($user->email) . ')');
$admins[$uid] = \html_writer::tag('li' , $link);
}
$admins = \html_writer::tag('ul', implode('', $admins));
$status = result::INFO;
$summary = get_string('check_riskadmin_ok', 'report_security', $admincount);
$details = get_string('check_riskadmin_detailsok', 'report_security', $admins);
return new result($status, $summary, $details);
}
}
+69
View File
@@ -0,0 +1,69 @@
<?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/>.
/**
* Lists all roles that have the ability to backup user data, as well as users
*
* @package core
* @category check
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\check\access;
defined('MOODLE_INTERNAL') || die();
use core\check\check;
use core\check\result;
/**
* Lists all roles that have the ability to backup user data, as well as users
*
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class riskbackup extends check {
/**
* Get the short check name
*
* @return string
*/
public function get_name(): string {
return get_string('check_riskbackup_name', 'report_security');
}
/**
* A link to a place to action this
*
* @return \action_link|null
*/
public function get_action_link(): ?\action_link {
return new \action_link(
new \moodle_url('/admin/roles/manage.php'),
get_string('manageroles', 'role'));
}
/**
* Return result
* @return result
*/
public function get_result(): result {
return new riskbackup_result();
}
}
@@ -0,0 +1,201 @@
<?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/>.
/**
* Lists all roles that have the ability to backup user data, as well as users
*
* @package core
* @category check
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @copyright 2008 petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\check\access;
use context;
use stdClass;
use core\check\result;
/**
* Lists all roles that have the ability to backup user data, as well as users
*
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @copyright 2008 petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class riskbackup_result extends \core\check\result {
/** @var stdClass[] $systemroles */
private $systemroles;
/** @var stdClass[] $overriddenroles */
private $overriddenroles;
/** @var string $sqluserinfo */
private $sqluserinfo;
/**
* Constructor
*/
public function __construct() {
global $DB;
$syscontext = \context_system::instance();
$params = array('capability' => 'moodle/backup:userinfo', 'permission' => CAP_ALLOW, 'contextid' => $syscontext->id);
$sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder, r.archetype
FROM {role} r
JOIN {role_capabilities} rc ON rc.roleid = r.id
WHERE rc.capability = :capability
AND rc.contextid = :contextid
AND rc.permission = :permission";
$this->systemroles = $DB->get_records_sql($sql, $params);
// Ensure first field is unique (role.id + role_capabilities.contextid).
$roleidcontextfield = $DB->sql_concat_join("','", ['r.id', 'rc.contextid']);
$params = array('capability' => 'moodle/backup:userinfo', 'permission' => CAP_ALLOW, 'contextid' => $syscontext->id);
$sql = "SELECT DISTINCT {$roleidcontextfield} AS rolecontext, r.id, r.name, r.shortname, r.sortorder, r.archetype,
rc.contextid
FROM {role} r
JOIN {role_capabilities} rc ON rc.roleid = r.id
WHERE rc.capability = :capability
AND rc.contextid <> :contextid
AND rc.permission = :permission";
$this->overriddenroles = $DB->get_records_sql($sql, $params);
// List of users that are able to backup personal info
// note:
// "sc" is context where is role assigned,
// "c" is context where is role overridden or system context if in role definition.
$params = [
'capability' => 'moodle/backup:userinfo',
'permission' => CAP_ALLOW,
'context1' => CONTEXT_COURSE,
'context2' => CONTEXT_COURSE,
];
$this->sqluserinfo = "
FROM (SELECT DISTINCT rcx.contextid,
rcx.roleid
FROM {role_capabilities} rcx
WHERE rcx.permission = :permission
AND rcx.capability = :capability) rc
JOIN {context} c ON c.id = rc.contextid
JOIN {context} sc ON sc.contextlevel <= :context1
JOIN {role_assignments} ra ON ra.contextid = sc.id AND ra.roleid = rc.roleid
JOIN {user} u ON u.id = ra.userid AND u.deleted = 0
WHERE (sc.path = c.path OR
sc.path LIKE " . $DB->sql_concat('c.path', "'/%'") . " OR
c.path LIKE " . $DB->sql_concat('sc.path', "'/%'") . ")
AND c.contextlevel <= :context2";
$usercount = $DB->count_records_sql("SELECT COUNT('x') FROM (SELECT DISTINCT u.id $this->sqluserinfo) userinfo", $params);
$systemrolecount = empty($this->systemroles) ? 0 : count($this->systemroles);
$overriddenrolecount = empty($this->overriddenroles) ? 0 : count($this->overriddenroles);
if (max($usercount, $systemrolecount, $overriddenrolecount) > 0) {
$this->status = result::WARNING;
} else {
$this->status = result::OK;
}
$a = (object)array(
'rolecount' => $systemrolecount,
'overridecount' => $overriddenrolecount,
'usercount' => $usercount,
);
$this->summary = get_string('check_riskbackup_warning', 'report_security', $a);
}
/**
* Showing the full list of roles may be slow so defer it
*
* @return string
*/
public function get_details(): string {
global $CFG, $DB;
$details = '';
// Make a list of roles.
if ($this->systemroles) {
$links = array();
foreach ($this->systemroles as $role) {
$role->name = role_get_name($role);
$role->url = (new \moodle_url('/admin/roles/manage.php', ['action' => 'edit', 'roleid' => $role->id]))->out();
$links[] = \html_writer::tag('li', get_string('check_riskbackup_editrole', 'report_security', $role));
}
$links = \html_writer::tag('ul', implode('', $links));
$details .= get_string('check_riskbackup_details_systemroles', 'report_security', $links);
}
// Make a list of overrides to roles.
if ($this->overriddenroles) {
$links = array();
foreach ($this->overriddenroles as $role) {
$context = context::instance_by_id($role->contextid);
$role->name = role_get_name($role, $context, ROLENAME_BOTH);
$role->contextname = $context->get_context_name();
$role->url = (new \moodle_url('/admin/roles/override.php',
['contextid' => $role->contextid, 'roleid' => $role->id]))->out();
$links[] = \html_writer::tag('li', get_string('check_riskbackup_editoverride', 'report_security', $role));
}
$links = \html_writer::tag('ul', implode('', $links));
$details .= get_string('check_riskbackup_details_overriddenroles', 'report_security', $links);
}
// Get a list of affected users as well.
$users = array();
list($sort, $sortparams) = users_order_by_sql('u');
$params = [
'capability' => 'moodle/backup:userinfo',
'permission' => CAP_ALLOW,
'context1' => CONTEXT_COURSE,
'context2' => CONTEXT_COURSE,
];
$userfieldsapi = \core_user\fields::for_userpic();
$userfields = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
$rs = $DB->get_recordset_sql("
SELECT DISTINCT $userfields,
ra.contextid,
ra.roleid
$this->sqluserinfo
ORDER BY $sort", array_merge($params, $sortparams));
foreach ($rs as $user) {
$context = \context::instance_by_id($user->contextid);
$url = new \moodle_url('/admin/roles/assign.php', ['contextid' => $user->contextid, 'roleid' => $user->roleid]);
$a = (object)array(
'fullname' => fullname($user),
'url' => $url->out(),
'email' => s($user->email),
'contextname' => $context->get_context_name(),
);
$users[] = \html_writer::tag('li', get_string('check_riskbackup_unassign', 'report_security', $a));
}
$rs->close();
if (!empty($users)) {
$users = \html_writer::tag('ul', implode('', $users));
$details .= get_string('check_riskbackup_details_users', 'report_security', $users);
}
return $details;
}
}
+76
View File
@@ -0,0 +1,76 @@
<?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/>.
/**
* Lists all users with XSS risk
*
* It would be great to combine this with risk trusts in user table,
* unfortunately nobody implemented user trust UI yet :-(
*
* @package core
* @category check
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @copyright 2008 petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\check\access;
defined('MOODLE_INTERNAL') || die();
use core\check\result;
/**
* Lists all users with XSS risk
*
* It would be great to combine this with risk trusts in user table,
* unfortunately nobody implemented user trust UI yet :-(
*
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @copyright 2008 petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class riskxss extends \core\check\check {
/**
* Get the short check name
*
* @return string
*/
public function get_name(): string {
return get_string('check_riskxss_name', 'report_security');
}
/**
* A link to a place to action this
*
* @return \action_link|null
*/
public function get_action_link(): ?\action_link {
return new \action_link(
new \moodle_url('/admin/roles/manage.php'),
get_string('manageroles', 'role'));
}
/**
* Return result
* @return result
*/
public function get_result(): result {
return new riskxss_result();
}
}
+112
View File
@@ -0,0 +1,112 @@
<?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/>.
/**
* Lists all users with XSS risk
*
* It would be great to combine this with risk trusts in user table,
* unfortunately nobody implemented user trust UI yet :-(
*
* @package core
* @category check
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @copyright 2008 petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\check\access;
defined('MOODLE_INTERNAL') || die();
use core\check\result;
/**
* Lists all users with XSS risk
*
* It would be great to combine this with risk trusts in user table,
* unfortunately nobody implemented user trust UI yet :-(
*
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @copyright 2008 petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class riskxss_result extends \core\check\result {
/** @var array SQL parameters. */
protected $params = [];
/** @var string SQL statement. */
protected $sqlfrom;
/**
* Constructor
*/
public function __construct() {
global $DB;
$this->params = array('capallow' => CAP_ALLOW);
$this->sqlfrom = "FROM (SELECT DISTINCT rcx.contextid, rcx.roleid
FROM {role_capabilities} rcx
JOIN {capabilities} cap ON (cap.name = rcx.capability AND
" . $DB->sql_bitand('cap.riskbitmask', RISK_XSS) . " <> 0)
WHERE rcx.permission = :capallow) rc,
{context} c,
{context} sc,
{role_assignments} ra,
{user} u
WHERE c.id = rc.contextid
AND (sc.path = c.path OR
sc.path LIKE " . $DB->sql_concat('c.path', "'/%'") . " OR
c.path LIKE " . $DB->sql_concat('sc.path', "'/%'") . ")
AND u.id = ra.userid AND u.deleted = 0
AND ra.contextid = sc.id
AND ra.roleid = rc.roleid";
$count = $DB->count_records_sql("SELECT COUNT(DISTINCT u.id) $this->sqlfrom", $this->params);
if ($count == 0) {
$this->status = result::OK;
} else {
$this->status = result::WARNING;
}
$this->summary = get_string('check_riskxss_warning', 'report_security', $count);
}
/**
* Showing the full list of user may be slow so defer it
*
* @return string
*/
public function get_details(): string {
global $CFG, $DB;
$userfieldsapi = \core_user\fields::for_userpic();
$userfields = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
$users = $DB->get_records_sql("SELECT DISTINCT $userfields $this->sqlfrom", $this->params);
foreach ($users as $uid => $user) {
$url = "$CFG->wwwroot/user/view.php?id=$user->id";
$link = \html_writer::link($url, fullname($user, true) . ' (' . s($user->email) . ')');
$users[$uid] = \html_writer::tag('li' , $link);
}
$users = \html_writer::tag('ul', implode('', $users));
return get_string('check_riskxss_details', 'report_security', $users);
}
}
+132
View File
@@ -0,0 +1,132 @@
<?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 checks
*
* @package core
* @category check
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\check;
use coding_exception;
/**
* Base class for checks
*
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class check {
/**
* @var string $component - The component / plugin this task belongs to.
*
* This can be autopopulated by the check manager.
* Otherwise, it is dynamically determined by get_component().
*/
protected $component = '';
/**
* Get the frankenstyle component name
*
* @return string
*/
public function get_component(): string {
// Return component if has been set by the manager.
if (!empty($this->component)) {
return $this->component;
}
// Else work it out based on the classname.
// Because the first part of the classname is always the component.
$parts = explode("\\", get_called_class());
if (empty($parts)) {
throw new coding_exception("Unable to determine component for check");
}
return $parts[0];
}
/**
* Get the frankenstyle component name
*
* @param string $component name
*/
public function set_component(string $component) {
$this->component = $component;
}
/**
* Get the check's id
*
* This defaults to the base name of the class which is ok in the most
* cases but if you have a check which can have multiple instances then
* you should override this to be unique.
*
* @return string must be unique within a component
*/
public function get_id(): string {
$class = get_class($this);
$id = explode("\\", $class);
return end($id);
}
/**
* Get the check reference
*
* @return string must be globally unique
*/
public function get_ref(): string {
$ref = $this->get_component();
if (!empty($ref)) {
$ref .= '_';
}
$ref .= $this->get_id();
return $ref;
}
/**
* Get the short check name
*
* @return string
*/
public function get_name(): string {
$id = $this->get_id();
return get_string("check{$id}", $this->get_component());
}
/**
* A link to a place to action this
*
* @return \action_link|null
*/
public function get_action_link(): ?\action_link {
return null;
}
/**
* Return the result
*
* @return result object
*/
abstract public function get_result(): result;
}
+123
View File
@@ -0,0 +1,123 @@
<?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/>.
namespace core\check\environment;
defined('MOODLE_INTERNAL') || die();
use core\check\check;
use core\check\result;
/**
* Checks status of antivirus scanners by looking back at any recent scans.
*
* @package core
* @category check
* @author Kevin Pham <kevinpham@catalyst-au.net>
* @copyright Catalyst IT, 2021
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class antivirus extends check {
/**
* Get the short check name
*
* @return string
*/
public function get_name(): string {
return get_string('check_antivirus_name', 'report_security');
}
/**
* A link to a place to action this
*
* @return \action_link|null
*/
public function get_action_link(): ?\action_link {
return new \action_link(
new \moodle_url('/admin/settings.php', ['section' => 'manageantiviruses']),
get_string('antivirussettings', 'antivirus'));
}
/**
* Return result
* @return result
*/
public function get_result(): result {
global $CFG, $DB;
$details = \html_writer::tag('p', get_string('check_antivirus_details', 'report_security'));
// If no scanners are enabled, then return an NA status since the results do not matter.
if (empty($CFG->antiviruses)) {
$status = result::NA;
$summary = get_string('check_antivirus_info', 'report_security');
return new result($status, $summary, $details);
}
$logmanager = get_log_manager();
$readers = $logmanager->get_readers('\core\log\sql_internal_table_reader');
// If reader is not a sql_internal_table_reader return UNKNOWN since we
// aren't able to fetch the required information. Legacy logs are not
// supported here. They do not hold enough adequate information to be
// used for these checks.
if (empty($readers)) {
$status = result::UNKNOWN;
$summary = get_string('check_antivirus_logstore_not_supported', 'report_security');
return new result($status, $summary, $details);
}
$reader = reset($readers);
// If there has been a recent timestamp within threshold period, then
// set the status to ERROR and describe the problem, e.g. X issues in
// the last N period.
$threshold = get_config('antivirus', 'threshold');
$params = [];
$params['lookback'] = time() - $threshold;
// Type of "targets" to include.
list($targetsqlin, $inparams) = $DB->get_in_or_equal([
'antivirus_scan_file',
'antivirus_scan_data',
], SQL_PARAMS_NAMED);
$params = array_merge($inparams, $params);
// Specify criteria for search.
$selectwhere = "timecreated > :lookback
AND target $targetsqlin
AND action = 'error'";
$totalerrors = $reader->get_events_select_count($selectwhere, $params);
if (!empty($totalerrors)) {
$status = result::ERROR;
$summary = get_string('check_antivirus_error', 'report_security', [
'errors' => $totalerrors,
'lookback' => format_time($threshold)
]);
} else if (!empty($CFG->antiviruses)) {
$status = result::OK;
// Fetch count of enabled antiviruses (we don't care about which ones).
$totalantiviruses = !empty($CFG->antiviruses) ? count(explode(',', $CFG->antiviruses)) : 0;
$summary = get_string('check_antivirus_ok', 'report_security', [
'scanners' => $totalantiviruses,
'lookback' => format_time($threshold)
]);
}
return new result($status, $summary, $details);
}
}
@@ -0,0 +1,70 @@
<?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/>.
/**
* Verifies config.php is not writable anymore after installation
*
* @package core
* @category check
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @copyright 2008 petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\check\environment;
defined('MOODLE_INTERNAL') || die();
use core\check\check;
use core\check\result;
/**
* Verifies config.php is not writable anymore after installation
*
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @copyright 2008 petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class configrw extends check {
/**
* Get the short check name
*
* @return string
*/
public function get_name(): string {
return get_string('check_configrw_name', 'report_security');
}
/**
* Return result
* @return result
*/
public function get_result(): result {
global $CFG;
$details = get_string('check_configrw_details', 'report_security');
if (is_writable($CFG->dirroot . '/config.php')) {
$status = result::WARNING;
$summary = get_string('check_configrw_warning', 'report_security');
} else {
$status = result::OK;
$summary = get_string('check_configrw_ok', 'report_security');
}
return new result($status, $summary, $details);
}
}
@@ -0,0 +1,75 @@
<?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/>.
/**
* Verifies displaying of errors
*
* Problem for lib files and 3rd party code because we can not disable debugging
* in these scripts (they do not include config.php)
*
* @package core
* @category check
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @copyright 2008 petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\check\environment;
defined('MOODLE_INTERNAL') || die();
use core\check\result;
use core\check\check;
/**
* Verifies displaying of errors
*
* Problem for lib files and 3rd party code because we can not disable debugging
* in these scripts (they do not include config.php)
*
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @copyright 2008 petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class displayerrors extends check {
/**
* Get the short check name
*
* @return string
*/
public function get_name(): string {
return get_string('check_displayerrors_name', 'report_security');
}
/**
* Return result
* @return result
*/
public function get_result(): result {
$details = get_string('check_displayerrors_details', 'report_security');
if (defined('WARN_DISPLAY_ERRORS_ENABLED')) {
$status = result::WARNING;
$summary = get_string('check_displayerrors_error', 'report_security');
} else {
$status = result::OK;
$summary = get_string('check_displayerrors_ok', 'report_security');
}
return new result($status, $summary, $details);
}
}
@@ -0,0 +1,83 @@
<?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/>.
/**
* Environment check
*
* @package core
* @category check
* @copyright 2020 Brendan Heywood (brendan@catalyst-au.net)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\check\environment;
defined('MOODLE_INTERNAL') || die();
use core\check\check;
use core\check\result;
/**
* Environment check
*
* @package core
* @copyright 2020 Brendan Heywood (brendan@catalyst-au.net)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class environment extends check {
/**
* Get the short check name
*
* @return string
*/
public function get_name(): string {
return get_string('environment', 'admin');
}
/**
* A link to a place to action this
*
* @return \action_link|null
*/
public function get_action_link(): ?\action_link {
return new \action_link(
new \moodle_url('/admin/environment.php'),
get_string('environment', 'admin'));
}
/**
* Return result
* @return result
*/
public function get_result(): result {
global $CFG;
require_once($CFG->libdir.'/environmentlib.php');
list($status, $details) = check_moodle_environment($CFG->release, ENV_SELECT_NEWER);
if ($status) {
$summary = get_string('environmentok', 'admin');
$status = result::OK;
} else {
$summary = get_string('environmenterrortodo', 'admin');
$status = result::ERROR;
}
return new result($status, $summary, '');
}
}
@@ -0,0 +1,69 @@
<?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/>.
/**
* Verifies the status of preventexecpath
*
* @package core
* @category check
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @copyright 2008 petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\check\environment;
defined('MOODLE_INTERNAL') || die();
use core\check\check;
use core\check\result;
/**
* Verifies the status of preventexecpath
*
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @copyright 2008 petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class preventexecpath extends check {
/**
* Get the short check name
*
* @return string
*/
public function get_name(): string {
return get_string('check_preventexecpath_name', 'report_security');
}
/**
* Return result
* @return result
*/
public function get_result(): result {
global $CFG;
$details = get_string('check_preventexecpath_details', 'report_security');
if (empty($CFG->preventexecpath)) {
$status = result::WARNING;
$summary = get_string('check_preventexecpath_warning', 'report_security');
} else {
$status = result::OK;
$summary = get_string('check_preventexecpath_ok', 'report_security');
}
return new result($status, $summary, $details);
}
}
@@ -0,0 +1,307 @@
<?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/>.
/**
* Check the presence of public paths via curl.
*
* @package core
* @category check
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\check\environment;
defined('MOODLE_INTERNAL') || die();
use core\check\check;
use core\check\result;
/**
* Check the public access of various paths.
*
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class publicpaths extends check {
/**
* Get the short check name
*
* @return string
*/
public function get_name(): string {
return get_string('check_publicpaths_name', 'report_security');
}
/**
* Returns a list of test urls and metadata.
*/
public function get_pathsets() {
global $CFG;
// The intention here is that each pattern is a simple regex such that
// in future perhaps the various webserver config could be generated as more
// pattens are added to these checks.
return [
[
'pattern' => '/vendor/',
'404' => [
'vendor/',
'vendor/bin/behat',
],
'details' => get_string('check_vendordir_details', 'report_security', ['path' => $CFG->dirroot.'/vendor']),
'summary' => get_string('check_vendordir_info', 'report_security'),
],
[
'pattern' => '/node_modules/',
'404' => [
'node_modules/',
'node_modules/cli/cli.js',
],
'summary' => get_string('check_nodemodules_info', 'report_security'),
'details' => get_string('check_nodemodules_details', 'report_security',
['path' => $CFG->dirroot . '/node_modules']),
],
[
'pattern' => '^\..*',
'404' => [
'.git/',
'.git/HEAD',
'.github/FUNDING.yml',
'.stylelintrc',
],
],
[
'pattern' => 'composer.json',
'404' => [
'composer.json',
],
],
[
'pattern' => '.lock',
'404' => [
'composer.lock',
],
],
[
'pattern' => 'environment.xml',
'404' => [
'admin/environment.xml',
],
],
[
'pattern' => '',
'404' => [
'doesnotexist', // Just to make sure that real 404s are still 404s.
],
'summary' => '',
],
[
'pattern' => '',
'404' => [
'lib/classes/',
],
'summary' => get_string('check_dirindex_info', 'report_security'),
],
[
'pattern' => 'db/install.xml',
'404' => [
'lib/db/install.xml',
'mod/assign/db/install.xml',
],
],
[
'pattern' => 'readme.txt',
'404' => [
'lib/scssphp/readme_moodle.txt',
'mod/resource/readme.txt',
],
],
[
'pattern' => 'README',
'404' => [
'mod/README.txt',
'mod/book/README.md',
'mod/chat/README.txt',
],
],
[
'pattern' => '/upgrade.txt',
'404' => [
'auth/manual/upgrade.txt',
'lib/upgrade.txt',
],
],
[
'pattern' => 'phpunit.xml',
'404' => ['phpunit.xml.dist'],
],
[
'pattern' => '/fixtures/',
'404' => [
'privacy/tests/fixtures/logo.png',
'enrol/lti/tests/fixtures/input.xml',
],
],
[
'pattern' => '/behat/',
'404' => ['blog/tests/behat/delete.feature'],
],
];
}
/**
* Return result
* @return result
*/
public function get_result(): result {
global $CFG, $OUTPUT;
$status = result::OK;
$details = '';
$summary = get_string('check_publicpaths_ok', 'report_security');
$errors = [];
$c = new \curl();
$paths = $this->get_pathsets();
$table = new \html_table();
$table->align = ['center', 'right', 'left'];
$table->size = ['1%', '1%', '1%', '1%', '1%', '99%'];
$table->head = [
get_string('status'),
get_string('checkexpected'),
get_string('checkactual'),
get_string('url'),
get_string('category'),
get_string('details'),
];
$table->attributes['class'] = 'flexible generaltable generalbox table-sm';
$table->data = [];
// Used to track duplicated errors.
$lastdetail = '-';
$curl = new \curl();
$requests = [];
// Build up a list of all url so we can load them in parallel.
foreach ($paths as $path) {
foreach (['200', '404'] as $expected) {
if (!isset($path[$expected])) {
continue;
}
foreach ($path[$expected] as $test) {
$requests[] = [
'nobody' => true,
'header' => 1,
'url' => $CFG->wwwroot . '/' . $test,
'returntransfer' => true,
];
}
}
}
$headers = $curl->download($requests);
foreach ($paths as $path) {
foreach (['200', '404'] as $expected) {
if (!isset($path[$expected])) {
continue;
}
foreach ($path[$expected] as $test) {
$rowsummary = '';
$rowdetail = '';
$url = $CFG->wwwroot . '/' . $test;
// Parse the HTTP header to get the 200 / 404 code.
$header = array_shift($headers);
$actual = strtok($header, "\n");
$actual = strtok($actual, " ");
$actual = strtok(" ");
if ($actual != $expected) {
if (isset($path['summary'])) {
$rowsummary = $path['summary'];
} else {
$rowsummary = get_string('check_publicpaths_generic',
'report_security', $path['pattern']);
}
// Special case where a 404 is ideal but a 403 is ok too.
if ($actual == 403) {
$result = new result(result::INFO, '', '');
$rowsummary .= get_string('check_publicpaths_403', 'report_security');
} else {
$result = new result(result::ERROR, '', '');
$status = result::ERROR;
$summary = get_string('check_publicpaths_warning', 'report_security');
}
$rowdetail = isset($path['details']) ? $path['details'] : $rowsummary;
if (empty($errors[$path['pattern']])) {
$summary .= '<li>' . $rowsummary . '</li>';
$errors[$path['pattern']] = 1;
}
} else {
$result = new result(result::OK, '', '');
}
$table->data[] = [
$OUTPUT->check_result($result),
$expected,
$actual,
$OUTPUT->action_link($url, $test, null, ['target' => '_blank']),
"<pre>{$path['pattern']}</pre>",
];
// Merge duplicate details to display a nicer table.
if ($rowdetail == $lastdetail) {
$duplicates++;
} else {
$duplicates = 1;
}
$detailcell = new \html_table_cell($rowdetail);
$detailcell->rowspan = $duplicates;
$rows = count($table->data);
$table->data[$rows - $duplicates][5] = $detailcell;
$lastdetail = $rowdetail;
}
}
}
$details .= \html_writer::table($table);
return new result($status, $summary, $details);
}
/**
* Link to the dev docs for more info.
*
* @return \action_link|null
*/
public function get_action_link(): ?\action_link {
return new \action_link(
new \moodle_url(\get_docs_url('Installing_Moodle#Set_up_your_server')),
get_string('moodledocs'));
}
}
@@ -0,0 +1,80 @@
<?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/>.
/**
* Verifies fatal misconfiguration of dataroot
*
* @package core
* @category check
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @copyright 2008 petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\check\environment;
defined('MOODLE_INTERNAL') || die();
use core\check\result;
/**
* Verifies fatal misconfiguration of dataroot
*
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @copyright 2008 petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class unsecuredataroot extends \core\check\check {
/**
* Get the short check name
*
* @return string
*/
public function get_name(): string {
return get_string('check_unsecuredataroot_name', 'report_security');
}
/**
* Return result
* @return result
*/
public function get_result(): result {
global $CFG;
require_once($CFG->libdir.'/adminlib.php');
$details = get_string('check_unsecuredataroot_details', 'report_security');
$insecuredataroot = is_dataroot_insecure(true);
if ($insecuredataroot == INSECURE_DATAROOT_WARNING) {
$status = result::ERROR;
$summary = get_string('check_unsecuredataroot_warning', 'report_security', $CFG->dataroot);
} else if ($insecuredataroot == INSECURE_DATAROOT_ERROR) {
$status = result::CRITICAL;
$summary = get_string('check_unsecuredataroot_error', 'report_security', $CFG->dataroot);
} else {
$status = result::OK;
$summary = get_string('check_unsecuredataroot_ok', 'report_security');
}
return new result($status, $summary, $details);
}
}
@@ -0,0 +1,85 @@
<?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/>.
/**
* Upgrade check
*
* @package core
* @category check
* @copyright 2020 Brendan Heywood (brendan@catalyst-au.net)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\check\environment;
defined('MOODLE_INTERNAL') || die();
use core\check\check;
use core\check\result;
/**
* Upgrade check
*
* @package core
* @copyright 2020 Brendan Heywood (brendan@catalyst-au.net)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class upgradecheck extends check {
/**
* Get the short check name
*
* @return string
*/
public function get_name(): string {
return get_string('checkupgradepending', 'admin');
}
/**
* A link to a place to action this
*
* @return \action_link|null
*/
public function get_action_link(): ?\action_link {
return new \action_link(
new \moodle_url('/admin/index.php?cache=1'),
get_string('notifications', 'admin'));
}
/**
* Return result
* @return result
*/
public function get_result(): result {
global $CFG;
require("$CFG->dirroot/version.php");
$newversion = "$release ($version)";
if ($version < $CFG->version) {
$status = result::ERROR;
$summary = get_string('downgradedcore', 'error');
} else if (moodle_needs_upgrading()) {
$status = result::ERROR;
$summary = get_string('cliupgradepending', 'admin');
} else {
$status = result::OK;
$summary = get_string('cliupgradenoneed', 'core_admin', $newversion);
}
return new result($status, $summary);
}
}
+153
View File
@@ -0,0 +1,153 @@
<?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/>.
namespace core\check\external;
use admin_root;
use admin_setting_check;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_single_structure;
use core_external\external_value;
use context_system;
use invalid_parameter_exception;
/**
* Webservice to get result of a given check.
*
* @package core
* @category check
* @copyright 2023 Matthew Hilton <matthewhilton@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class get_result_admintree extends external_api {
/**
* Defines parameters
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'admintreeid' => new external_value(PARAM_TEXT, 'ID of node in admintree'),
'settingname' => new external_value(PARAM_TEXT, 'Name of setting'),
'includedetails' => new external_value(PARAM_BOOL, 'If the details should be included in the response.
Depending on the check, details could be slower to return.', VALUE_DEFAULT, false),
]);
}
/**
* Gets the result of the check and returns it.
* @param string $admintreeid ID of admin_setting to find check object from
* @param string $settingname Name of admin_setting to find check object from
* @param bool $includedetails If the details should be included in the response.
* @param admin_root|null $admintree Root of admin tree to use (for unit testing)
* @return array returned data
*/
public static function execute(string $admintreeid, string $settingname, bool $includedetails,
admin_root $admintree = null): array {
global $OUTPUT, $CFG;
// Validate parameters.
self::validate_parameters(self::execute_parameters(), [
'admintreeid' => $admintreeid,
'settingname' => $settingname,
'includedetails' => $includedetails,
]);
// Context and capability checks.
$context = context_system::instance();
self::validate_context($context);
require_admin();
require_once($CFG->libdir . '/adminlib.php');
// Find admin node so we can load the check object.
$check = self::get_check_from_setting($admintreeid, $settingname, $admintree);
if (empty($check)) {
throw new invalid_parameter_exception("Could not find check object using admin tree.");
}
// Execute the check and get the result.
$result = $check->get_result();
// Build the response.
$data = [
'status' => s($result->get_status()),
'summary' => s($result->get_summary()),
'html' => s($OUTPUT->check_full_result($check, $result, $includedetails)),
];
// Since details might be slower to obtain, we allow this to be optionally returned.
if ($includedetails) {
$data['details'] = s($result->get_details());
}
return $data;
}
/**
* Finds the check from the admin tree.
*
* @param string $settingid ID of the adming_setting
* @param string $settingname Name of the admin_setting
* @param admin_root|null $tree Admin tree to use (for unit testing). Null will default to the admin_get_root()
*/
private static function get_check_from_setting(string $settingid, string $settingname, admin_root $tree = null) {
// Since settings do not know exactly who their parents are in the tree, we must search for the setting.
if (empty($tree)) {
$tree = \admin_get_root();
}
// Search for the setting name.
// To do this, we must search in each category.
$categories = $tree->search($settingname);
$allsettings = array_map(function($c) {
return $c->settings;
}, array_values($categories));
// Flatten the array.
$allsettings = array_merge(...$allsettings);
// Find the one that matches the unique id exactly and are check settings.
$matchingsettings = array_filter($allsettings, function($s) use ($settingid) {
return $s->get_id() == $settingid && $s instanceof admin_setting_check;
});
// There was either none found or more than one found.
// In this case, we cannot determine which to use so just return null.
if (count($matchingsettings) != 1) {
return null;
}
$setting = current($matchingsettings);
return $setting->get_check();
}
/**
* Defines return structure.
* @return external_single_structure
*/
public static function execute_returns(): external_single_structure {
return new external_single_structure([
'status' => new external_value(PARAM_TEXT, 'Result status constant'),
'summary' => new external_value(PARAM_TEXT, 'Summary of result'),
'html' => new external_value(PARAM_TEXT, 'Rendered full html result', VALUE_OPTIONAL),
'details' => new external_value(PARAM_TEXT, 'Details of result (if includedetails was enabled)', VALUE_OPTIONAL),
]);
}
}
+90
View File
@@ -0,0 +1,90 @@
<?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/>.
/**
* Verifies if https enabled only secure cookies allowed
*
* This prevents redirections and sending of cookies to unsecure port.
*
* @package core
* @category check
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @copyright 2008 petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\check\http;
defined('MOODLE_INTERNAL') || die();
use core\check\check;
use core\check\result;
/**
* Verifies if https enabled only secure cookies allowed
*
* This prevents redirections and sending of cookies to unsecure port.
*
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @copyright 2008 petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cookiesecure extends check {
/**
* Get the short check name
*
* @return string
*/
public function get_name(): string {
return get_string('check_cookiesecure_name', 'report_security');
}
/**
* A link to a place to action this
*
* @return \action_link|null
*/
public function get_action_link(): ?\action_link {
return new \action_link(
new \moodle_url('/admin/settings.php?section=httpsecurity#admin-cookiesecure'),
get_string('httpsecurity', 'admin'));
}
/**
* Return result
* @return result
*/
public function get_result(): result {
global $CFG;
$details = get_string('check_cookiesecure_details', 'report_security');
if (!is_https()) {
$status = result::WARNING;
$summary = get_string('check_cookiesecure_http', 'report_security');
return new result($status, $summary, $details);
}
if (!is_moodle_cookie_secure()) {
$status = result::ERROR;
$summary = get_string('check_cookiesecure_error', 'report_security');
} else {
$status = result::OK;
$summary = get_string('check_cookiesecure_ok', 'report_security');
}
return new result($status, $summary, $details);
}
}
+157
View File
@@ -0,0 +1,157 @@
<?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/>.
/**
* Check API manager
*
* @package core
* @category check
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\check;
defined('MOODLE_INTERNAL') || die();
/**
* Check API manager
*
* @package core
* @category check
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class manager {
/**
* The list of valid check types
*/
public const TYPES = ['status', 'security', 'performance'];
/**
* Return all status checks
*
* @param string $type of checks to fetch
* @return array of check objects
*/
public static function get_checks(string $type): array {
if (!in_array($type, self::TYPES)) {
throw new \moodle_exception("Invalid check type '$type'");
}
$method = 'get_' . $type . '_checks';
$checks = self::$method();
return $checks;
}
/**
* Return all performance checks
*
* @return array of check objects
*/
public static function get_performance_checks(): array {
$checks = [
new performance\designermode(),
new performance\cachejs(),
new performance\debugging(),
new performance\backups(),
new performance\stats(),
new performance\dbschema(),
];
// Any plugin can add status checks to this report by implementing a callback
// <component>_status_checks() which returns a check object.
$morechecks = get_plugins_with_function('performance_checks', 'lib.php');
foreach ($morechecks as $plugintype => $plugins) {
foreach ($plugins as $plugin => $pluginfunction) {
$result = $pluginfunction();
foreach ($result as $check) {
$check->set_component($plugintype . '_' . $plugin);
$checks[] = $check;
}
}
}
return $checks;
}
/**
* Return all status checks
*
* @return array of check objects
*/
public static function get_status_checks(): array {
$checks = [
new environment\environment(),
new environment\upgradecheck(),
new environment\antivirus(),
];
// Any plugin can add status checks to this report by implementing a callback
// <component>_status_checks() which returns a check object.
$morechecks = get_plugins_with_function('status_checks', 'lib.php');
foreach ($morechecks as $plugintype => $plugins) {
foreach ($plugins as $plugin => $pluginfunction) {
$result = $pluginfunction();
foreach ($result as $check) {
$check->set_component($plugintype . '_' . $plugin);
$checks[] = $check;
}
}
}
return $checks;
}
/**
* Return all security checks
*
* @return array of check objects
*/
public static function get_security_checks(): array {
$checks = [
new environment\displayerrors(),
new environment\unsecuredataroot(),
new environment\publicpaths(),
new environment\configrw(),
new environment\preventexecpath(),
new security\embed(),
new security\openprofiles(),
new security\crawlers(),
new security\passwordpolicy(),
new security\emailchangeconfirmation(),
new security\webcron(),
new http\cookiesecure(),
new access\riskadmin(),
new access\riskxss(),
new access\riskbackup(),
new access\defaultuserrole(),
new access\guestrole(),
new access\frontpagerole(),
];
// Any plugin can add security checks to this report by implementing a callback
// <component>_security_checks() which returns a check object.
$morechecks = get_plugins_with_function('security_checks', 'lib.php');
foreach ($morechecks as $plugintype => $plugins) {
foreach ($plugins as $plugin => $pluginfunction) {
$result = $pluginfunction();
foreach ($result as $check) {
$check->set_component($plugintype . '_' . $plugin);
$checks[] = $check;
}
}
}
return $checks;
}
}
+84
View File
@@ -0,0 +1,84 @@
<?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/>.
/**
* Backups check
*
* @package core
* @category check
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\check\performance;
defined('MOODLE_INTERNAL') || die();
use core\check\check;
use core\check\result;
/**
* Backups check
*
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class backups extends check {
/**
* Get the short check name
*
* @return string
*/
public function get_name(): string {
return get_string('check_backup', 'report_performance');
}
/**
* A link to a place to action this
*
* @return \action_link|null
*/
public function get_action_link(): ?\action_link {
return new \action_link(
new \moodle_url('/admin/settings.php', ['section' => 'automated']),
get_string('automatedsetup', 'backup'));
}
/**
* Return result
* @return result
*/
public function get_result(): result {
global $CFG;
require_once($CFG->dirroot . '/backup/util/helper/backup_cron_helper.class.php');
$automatedbackupsenabled = get_config('backup', 'backup_auto_active');
if ($automatedbackupsenabled == \backup_cron_automated_helper::AUTO_BACKUP_ENABLED) {
$status = result::WARNING;
$summary = get_string('check_backup_comment_enable', 'report_performance');
} else {
$status = result::OK;
$summary = get_string('check_backup_comment_disable', 'report_performance');
}
$details = get_string('check_backup_details', 'report_performance');
return new result($status, $summary, $details);
}
}
+81
View File
@@ -0,0 +1,81 @@
<?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/>.
/**
* CacheJS check
*
* @package core
* @category check
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @copyright 2008 petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\check\performance;
defined('MOODLE_INTERNAL') || die();
use core\check\check;
use core\check\result;
/**
* CacheJS check
*
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cachejs extends check {
/**
* Get the short check name
*
* @return string
*/
public function get_name(): string {
return get_string('cachejs', 'admin');
}
/**
* A link to a place to action this
*
* @return \action_link|null
*/
public function get_action_link(): ?\action_link {
return new \action_link(
new \moodle_url('/admin/search.php', ['query' => 'cachejs']),
get_string('cachejs', 'admin'));
}
/**
* Return result
* @return result
*/
public function get_result(): result {
global $CFG;
if (empty($CFG->cachejs)) {
$status = result::CRITICAL;
$summary = get_string('check_cachejs_comment_disable', 'report_performance');
} else {
$status = result::OK;
$summary = get_string('check_cachejs_comment_enable', 'report_performance');
}
$details = get_string('check_cachejs_details', 'report_performance');
return new result($status, $summary, $details);
}
}
@@ -0,0 +1,85 @@
<?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/>.
/**
* DB schema performance check
*
* @package core
* @category check
* @copyright 2021 Brendan Heywood <brendan@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\check\performance;
defined('MOODLE_INTERNAL') || die();
use core\check\check;
use core\check\result;
/**
* DB schema performance check
*
* @copyright 2021 Brendan Heywood <brendan@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class dbschema extends check {
/**
* Get the short check name
*
* @return string
*/
public function get_name(): string {
return get_string('check_dbschema_name', 'report_performance');
}
/**
* A link to a place to action this
*
* @return \action_link|null
*/
public function get_action_link(): ?\action_link {
return new \action_link(
new \moodle_url(\get_docs_url('Verify_Database_Schema')),
get_string('moodledocs'));
}
/**
* Return result
* @return result
*/
public function get_result(): result {
global $DB;
$dbmanager = $DB->get_manager();
$schema = $dbmanager->get_install_xml_schema();
if (!$errors = $dbmanager->check_database_schema($schema)) {
return new result(result::OK, get_string('check_dbschema_ok', 'report_performance'), '');
}
$details = '';
foreach ($errors as $tablename => $items) {
$details .= \html_writer::tag('h4', $tablename);
foreach ($items as $item) {
$details .= \html_writer::tag('pre', $item);
}
}
return new result(result::ERROR, get_string('check_dbschema_errors', 'report_performance'), $details);
}
}
@@ -0,0 +1,81 @@
<?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/>.
/**
* Debugging check
*
* @package core
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @copyright 2008 petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\check\performance;
defined('MOODLE_INTERNAL') || die();
use core\check\check;
use core\check\result;
/**
* Debugging check
*
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class debugging extends check {
/**
* Get the short check name
*
* @return string
*/
public function get_name(): string {
return get_string('debug', 'admin');
}
/**
* A link to a place to action this
*
* @return \action_link|null
*/
public function get_action_link(): ?\action_link {
return new \action_link(
new \moodle_url('/admin/settings.php', ['section' => 'debugging']),
get_string('debug', 'admin'));
}
/**
* Return result
* @return result
*/
public function get_result(): result {
global $CFG;
if (!$CFG->debugdeveloper) {
$status = result::OK;
$summary = get_string('check_debugmsg_comment_nodeveloper', 'report_performance');
} else {
$status = result::WARNING;
$summary = get_string('check_debugmsg_comment_developer', 'report_performance');
}
$details = get_string('check_debugmsg_details', 'report_performance');
return new result($status, $summary, $details);
}
}
@@ -0,0 +1,82 @@
<?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/>.
/**
* Designer mode
*
* @package core
* @category check
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @copyright 2008 petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\check\performance;
defined('MOODLE_INTERNAL') || die();
use core\check\check;
use core\check\result;
/**
* Designer mode
*
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class designermode extends check {
/**
* Get the short check name
*
* @return string
*/
public function get_name(): string {
return get_string('themedesignermode', 'admin');
}
/**
* A link to a place to action this
*
* @return \action_link|null
*/
public function get_action_link(): ?\action_link {
return new \action_link(
new \moodle_url('/admin/search.php', ['query' => 'themedesignermode']),
get_string('themedesignermode', 'admin'));
}
/**
* Return result
* @return result
*/
public function get_result(): result {
global $DB, $CFG;
if (empty($CFG->themedesignermode)) {
$status = result::OK;
$summary = get_string('check_themedesignermode_comment_disable', 'report_performance');
} else {
$status = result::CRITICAL;
$summary = get_string('check_themedesignermode_comment_enable', 'report_performance');
}
$details = get_string('check_themedesignermode_details', 'report_performance');
return new result($status, $summary, $details);
}
}
+72
View File
@@ -0,0 +1,72 @@
<?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/>.
namespace core\check\performance;
use core\check\check;
use core\check\result;
/**
* Stats check
*
* @package core
* @category check
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @copyright 2008 petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class stats extends check {
/**
* Get the short check name
*
* @return string
*/
public function get_name(): string {
return get_string('stats');
}
/**
* A link to a place to action this
*
* @return \action_link|null
*/
public function get_action_link(): ?\action_link {
return new \action_link(
new \moodle_url('/admin/search.php', ['query' => 'enablestats']),
get_string('enablestats', 'admin'));
}
/**
* Return result
* @return result
*/
public function get_result(): result {
global $CFG;
if (!empty($CFG->enablestats)) {
$status = result::WARNING;
$summary = get_string('check_enablestats_comment_enable', 'report_performance');
} else {
$status = result::OK;
$summary = get_string('check_enablestats_comment_disable', 'report_performance');
}
$details = get_string('check_enablestats_details', 'report_performance');
return new result($status, $summary, $details);
}
}
+192
View File
@@ -0,0 +1,192 @@
<?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/>.
/**
* A check result class
*
* @package core
* @category check
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\check;
defined('MOODLE_INTERNAL') || die();
/**
* A check object returns a result object
*
* Most checks can use this an instance of this directly but if you have a
* 'details' which is computationally expensive then extend this and overide
* the get_details() method so that it is only called when it will be needed.
*
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class result implements \renderable {
/**
* This is used to notify if a check does not apply.
*
* In most cases if a check doesn't apply a check object shouldn't be made.
* This state exists for when you always want visibilty of the check itself.
* Can be useful for a check which depends on another check and it helps
* focus on the other check which matters more.
*/
const NA = 'na';
/**
* Ideally all checks should be ok.
*/
const OK = 'ok';
/**
* This is used to show info for a check.
*
* This is equivalent to OK but could be used for alerting to potential
* future warnings such as a deprecation in a service.
*/
const INFO = 'info';
/**
* This means we could not determine the state.
*
* An example might be an expensive check done via cron, and it has never run.
* It would be prudent to consider an unknown check as a warning or error.
*/
const UNKNOWN = 'unknown';
/**
* Warnings
*
* Something is not ideal and should be addressed, eg usability or the
* speed of the site may be affected, but it may self heal (eg a load spike)
*/
const WARNING = 'warning';
/**
* This is used to notify if a check failed.
*
* Something is wrong with a component and a feature is not working.
*/
const ERROR = 'error';
/**
* This is used to notify if a check is a major critical issue.
*
* An error which is affecting everyone in a major way.
*/
const CRITICAL = 'critical';
/**
* @var string $status - status
*/
protected $status = self::UNKNOWN;
/**
* @var string summary - should be roughly 1 line of plain text and may change depending on the state.
*/
protected $summary = '';
/**
* @var string details about check.
*
* This may be a large amount of preformatted html text, possibly describing all the
* different states and actions to address them.
*/
protected $details = '';
/**
* Get the check reference label
*
* @return string must be globally unique
*/
public function get_ref(): string {
$ref = $this->get_component();
if (!empty($ref)) {
$ref .= '_';
}
$ref .= $this->get_id();
return $ref;
}
/**
* Constructor
*
* @param string $status code
* @param string $summary a 1 liner summary
* @param string $details as a html chunk
*/
public function __construct($status, $summary, $details = '') {
$this->status = $status;
$this->summary = $summary;
$this->details = $details;
}
/**
* Get the check status
*
* @return string one of the consts eg result::OK
*/
public function get_status(): string {
return $this->status;
}
/**
* Summary of the check
* @return string formatted html
*/
public function get_summary(): string {
return $this->summary;
}
/**
* Get the check detailed info
* @return string formatted html
*/
public function get_details(): string {
return $this->details;
}
/**
* Export this data so it can be used as the context for a mustache template.
*
* @param \renderer_base $output typically, the renderer that's calling this function
* @return array data context for a mustache template
*/
public function export_for_template(\renderer_base $output) {
return array(
'status' => clean_text(get_string('status' . $this->status)),
'isna' => $this->status === self::NA,
'isok' => $this->status === self::OK,
'isinfo' => $this->status === self::INFO,
'isunknown' => $this->status === self::UNKNOWN,
'iswarning' => $this->status === self::WARNING,
'iserror' => $this->status === self::ERROR,
'iscritical' => $this->status === self::CRITICAL,
);
}
/**
* Which mustache template?
*
* @return string path to mustache template
*/
public function get_template_name(): string {
return 'core/check/result';
}
}
+89
View File
@@ -0,0 +1,89 @@
<?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/>.
/**
* Verifies web crawler (search engine) access
*
* Not combined with disabled guest access because attackers might gain guest
* access by modifying browser signature.
*
* @package core
* @category check
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @copyright 2008 petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\check\security;
defined('MOODLE_INTERNAL') || die();
use core\check\check;
use core\check\result;
/**
* Verifies web crawler (search engine) access
*
* Not combined with disabled guest access because attackers might gain guest
* access by modifying browser signature.
*
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @copyright 2008 petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class crawlers extends check {
/**
* Get the short check name
*
* @return string
*/
public function get_name(): string {
return get_string('check_crawlers_name', 'report_security');
}
/**
* A link to a place to action this
*
* @return \action_link|null
*/
public function get_action_link(): ?\action_link {
return new \action_link(
new \moodle_url('/admin/settings.php?section=sitepolicies#admin-opentowebcrawlers'),
get_string('sitepolicies', 'admin'));
}
/**
* Return result
* @return result
*/
public function get_result(): result {
global $CFG;
$details = get_string('check_crawlers_details', 'report_security');
if (empty($CFG->opentowebcrawlers)) {
$status = result::OK;
$summary = get_string('check_crawlers_ok', 'report_security');
} else if (!empty($CFG->guestloginbutton)) {
$status = result::INFO;
$summary = get_string('check_crawlers_info', 'report_security');
} else {
$status = result::ERROR;
$summary = get_string('check_crawlers_error', 'report_security');
}
return new result($status, $summary, $details);
}
}
@@ -0,0 +1,86 @@
<?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/>.
/**
* Verifies email confirmation - spammers were changing mails very often
*
* @package core
* @category check
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @copyright 2008 petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\check\security;
defined('MOODLE_INTERNAL') || die();
use core\check\check;
use core\check\result;
/**
* Verifies email confirmation - spammers were changing mails very often
*
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @copyright 2008 petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class emailchangeconfirmation extends check {
/**
* Get the short check name
*
* @return string
*/
public function get_name(): string {
return get_string('check_emailchangeconfirmation_name', 'report_security');
}
/**
* A link to a place to action this
*
* @return \action_link|null
*/
public function get_action_link(): ?\action_link {
return new \action_link(
new \moodle_url('/admin/settings.php?section=sitepolicies#admin-emailchangeconfirmation'),
get_string('sitepolicies', 'admin'));
}
/**
* Return result
* @return result
*/
public function get_result(): result {
global $CFG;
$details = get_string('check_emailchangeconfirmation_details', 'report_security');
if (empty($CFG->emailchangeconfirmation)) {
if (empty($CFG->allowemailaddresses)) {
$status = result::WARNING;
$summary = get_string('check_emailchangeconfirmation_error', 'report_security');
} else {
$status = result::INFO;
$summary = get_string('check_emailchangeconfirmation_info', 'report_security');
}
} else {
$status = result::OK;
$summary = get_string('check_emailchangeconfirmation_ok', 'report_security');
}
return new result($status, $summary, $details);
}
}
+80
View File
@@ -0,0 +1,80 @@
<?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/>.
/**
* Verifies sloppy embedding - this should have been removed long ago!!
*
* @package core
* @category check
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @copyright 2008 petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\check\security;
defined('MOODLE_INTERNAL') || die();
use core\check\check;
use core\check\result;
/**
* Verifies sloppy embedding - this should have been removed long ago!!
*
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @copyright 2008 petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class embed extends check {
/**
* Get the short check name
*
* @return string
*/
public function get_name(): string {
return get_string('check_embed_name', 'report_security');
}
/**
* A link to a place to action this
*
* @return \action_link|null
*/
public function get_action_link(): ?\action_link {
return new \action_link(
new \moodle_url('/admin/settings.php?section=sitepolicies#admin-allowobjectembed'),
get_string('sitepolicies', 'admin'));
}
/**
* Return result
* @return result
*/
public function get_result(): result {
global $CFG;
$details = get_string('check_embed_details', 'report_security');
if (!empty($CFG->allowobjectembed)) {
$status = result::ERROR;
$summary = get_string('check_embed_error', 'report_security');
} else {
$status = result::OK;
$summary = get_string('check_embed_ok', 'report_security');
}
return new result($status, $summary, $details);
}
}
@@ -0,0 +1,80 @@
<?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/>.
/**
* Verifies open profiles - originally open by default, not anymore because spammer abused it a lot
*
* @package core
* @category check
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @copyright 2008 petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\check\security;
defined('MOODLE_INTERNAL') || die();
use core\check\check;
use core\check\result;
/**
* Verifies open profiles - originally open by default, not anymore because spammer abused it a lot
*
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @copyright 2008 petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class openprofiles extends check {
/**
* Get the short check name
*
* @return string
*/
public function get_name(): string {
return get_string('check_openprofiles_name', 'report_security');
}
/**
* A link to a place to action this
*
* @return \action_link|null
*/
public function get_action_link(): ?\action_link {
return new \action_link(
new \moodle_url('/admin/settings.php?section=sitepolicies#admin-forcelogin'),
get_string('sitepolicies', 'admin'));
}
/**
* Return result
* @return result
*/
public function get_result(): result {
global $CFG;
$details = get_string('check_openprofiles_details', 'report_security');
if (empty($CFG->forcelogin) and empty($CFG->forceloginforprofiles)) {
$status = result::WARNING;
$summary = get_string('check_openprofiles_error', 'report_security');
} else {
$status = result::OK;
$summary = get_string('check_openprofiles_ok', 'report_security');
}
return new result($status, $summary, $details);
}
}
@@ -0,0 +1,80 @@
<?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/>.
/**
* Verifies if password policy set
*
* @package core
* @category check
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @copyright 2008 petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\check\security;
defined('MOODLE_INTERNAL') || die();
use core\check\check;
use core\check\result;
/**
* Verifies if password policy set
*
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @copyright 2008 petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class passwordpolicy extends check {
/**
* Get the short check name
*
* @return string
*/
public function get_name(): string {
return get_string('check_passwordpolicy_name', 'report_security');
}
/**
* A link to a place to action this
*
* @return \action_link|null
*/
public function get_action_link(): ?\action_link {
return new \action_link(
new \moodle_url('/admin/settings.php?section=sitepolicies#admin-passwordpolicy'),
get_string('sitepolicies', 'admin'));
}
/**
* Return result
* @return result
*/
public function get_result(): result {
global $CFG;
$details = get_string('check_passwordpolicy_details', 'report_security');
if (empty($CFG->passwordpolicy)) {
$status = result::WARNING;
$summary = get_string('check_passwordpolicy_error', 'report_security');
} else {
$status = result::OK;
$summary = get_string('check_passwordpolicy_ok', 'report_security');
}
return new result($status, $summary, $details);
}
}
+84
View File
@@ -0,0 +1,84 @@
<?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/>.
/**
* Verifies the status of web cron
*
* @package core
* @category check
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @copyright 2008 petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\check\security;
defined('MOODLE_INTERNAL') || die();
use core\check\check;
use core\check\result;
/**
* Verifies the status of web cron
*
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @copyright 2008 petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class webcron extends check {
/**
* Get the short check name
*
* @return string
*/
public function get_name(): string {
return get_string('check_webcron_name', 'report_security');
}
/**
* A link to a place to action this
*
* @return \action_link|null
*/
public function get_action_link(): ?\action_link {
return new \action_link(
new \moodle_url('/admin/settings.php?section=sitepolicies#admin-cronclionly'),
get_string('sitepolicies', 'admin'));
}
/**
* Return result
* @return result
*/
public function get_result(): result {
global $CFG;
$croncli = $CFG->cronclionly;
$cronremotepassword = $CFG->cronremotepassword;
if (empty($croncli) && empty($cronremotepassword)) {
$status = result::WARNING;
$summary = get_string('check_webcron_warning', 'report_security');
} else {
$status = result::OK;
$summary = get_string('check_webcron_ok', 'report_security');
}
$details = get_string('check_webcron_details', 'report_security');
return new result($status, $summary, $details);
}
}
+145
View File
@@ -0,0 +1,145 @@
<?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/>.
/**
* A table of check results
*
* @package core
* @category check
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\check;
defined('MOODLE_INTERNAL') || die();
/**
* A table of check results
*
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class table implements \renderable {
/**
* @var \moodle_url $url
*/
protected $url = '';
/**
* @var string $type What type of checks
*/
protected $type = '';
/**
* @var check $detail a specific check to focus on
*/
public $detail = '';
/**
* @var array $checks shown in this table
*/
public $checks = [];
/**
* Constructor
*
* @param string $type of check
* @param string $url of report
* @param string $detail check to focus on
*/
public function __construct($type, $url, $detail = '') {
// We may need a bit more memory and this may take a long time to process.
\raise_memory_limit(MEMORY_EXTRA);
\core_php_time_limit::raise();
$this->type = $type;
$this->url = $url;
$this->checks = \core\check\manager::get_checks($type);
if ($detail) {
$this->checks = array_filter($this->checks, function($check) use ($detail) {
return $detail == $check->get_ref();
});
if (!empty($this->checks)) {
$this->detail = reset($this->checks);
}
}
}
/**
* Render a table of checks
*
* @param renderer $output to use
* @return string html output
*/
public function render($output) {
$table = new \html_table();
$table->data = [];
$table->head = [
get_string('status'),
get_string('check'),
get_string('summary'),
get_string('action'),
];
$table->colclasses = [
'rightalign status',
'leftalign check',
'leftalign summary',
'leftalign action',
];
$table->id = $this->type . 'reporttable';
$table->attributes = ['class' => 'admintable ' . $this->type . 'report generaltable'];
foreach ($this->checks as $check) {
$ref = $check->get_ref();
$result = $check->get_result();
$component = $check->get_component();
$actionlink = $check->get_action_link();
$link = new \moodle_url($this->url, ['detail' => $ref]);
$row = [];
$row[] = $output->check_result($result);
$row[] = $output->action_link($link, $check->get_name());
$row[] = $result->get_summary()
. '<br>'
. \html_writer::start_tag('small')
. $output->action_link($link, get_string('moreinfo'))
. \html_writer::end_tag('small');
if ($actionlink) {
$row[] = $output->render($actionlink);
} else {
$row[] = '';
}
$table->data[] = $row;
}
$html = \html_writer::table($table);
if ($this->detail && $result) {
$html .= $output->heading(get_string('details'), 3);
$html .= $output->box($result->get_details(), 'generalbox boxwidthnormal boxaligncenter');
$html .= $output->continue_button($this->url);
}
return $html;
}
}
+33
View File
@@ -0,0 +1,33 @@
<?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/>.
namespace core;
/**
* Moodle Clock interface.
*
* @package core
* @copyright 2024 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
interface clock extends \Psr\Clock\ClockInterface {
/**
* Return the unix time stamp for the current representation of the time.
*
* @return int
*/
public function time(): int;
}
+349
View File
@@ -0,0 +1,349 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Defines string apis
*
* @package core
* @copyright 2011 Sam Hemelryk
* 2012 Petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* A collator class with static methods that can be used for sorting.
*
* @package core
* @copyright 2011 Sam Hemelryk
* 2012 Petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class core_collator {
/** @var int compare items using general PHP comparison, equivalent to Collator::SORT_REGULAR, this may bot be locale aware! */
const SORT_REGULAR = 0;
/** @var int compare items as strings, equivalent to Collator::SORT_STRING */
const SORT_STRING = 1;
/** @var int compare items as numbers, equivalent to Collator::SORT_NUMERIC */
const SORT_NUMERIC = 2;
/** @var int compare items like natsort(), equivalent to SORT_NATURAL */
const SORT_NATURAL = 6;
/** @var int do not ignore case when sorting, use bitwise "|" with SORT_NATURAL or SORT_STRING, equivalent to Collator::UPPER_FIRST */
const CASE_SENSITIVE = 64;
/** @var Collator|false|null **/
protected static $collator = null;
/** @var string|null The locale that was used in instantiating the current collator **/
protected static $locale = null;
/**
* Prevent class instances, all methods are static.
*/
private function __construct() {
}
/**
* Ensures that a collator is available and created
*
* @return bool Returns true if collation is available and ready
*/
protected static function ensure_collator_available() {
$locale = get_string('locale', 'langconfig');
if (is_null(self::$collator) || $locale != self::$locale) {
self::$collator = false;
self::$locale = $locale;
if (class_exists('Collator', false)) {
$collator = new Collator($locale);
if (!empty($collator) && $collator instanceof Collator) {
// Check for non fatal error messages. This has to be done immediately
// after instantiation as any further calls to collation will cause
// it to reset to 0 again (or another error code if one occurred)
$errorcode = $collator->getErrorCode();
$errormessage = $collator->getErrorMessage();
// Check for an error code, 0 means no error occurred
if ($errorcode !== 0) {
// Get the actual locale being used, e.g. en, he, zh
$localeinuse = $collator->getLocale(Locale::ACTUAL_LOCALE);
// Check for the common fallback warning error codes. If any of the two
// following errors occurred, there is normally little to worry about:
// * U_USING_FALLBACK_WARNING (-128) indicates that a fall back locale was
// used. For example, 'de_CH' was requested, but nothing was found
// there, so 'de' was used.
// * U_USING_DEFAULT_WARNING (-127) indicates that the default locale
// data was used; neither the requested locale nor any of its fall
// back locales could be found. For example, 'pt' was requested, but
// UCA was used (Unicode Collation Algorithm http://unicode.org/reports/tr10/).
// See http://www.icu-project.org/apiref/icu4c/classicu_1_1ResourceBundle.html
if ($errorcode === -127 || $errorcode === -128) {
// Check if the locale in use is UCA default one ('root') or
// if it is anything like the locale we asked for
if ($localeinuse !== 'root' && strpos($locale, $localeinuse) !== 0) {
// The locale we asked for is completely different to the locale
// we have received, let the user know via debugging
debugging('Locale warning (not fatal) '.$errormessage.': '.
'Requested locale "'.$locale.'" not found, locale "'.$localeinuse.'" used instead. '.
'The most specific locale supported by ICU relatively to the requested locale is "'.
$collator->getLocale(Locale::VALID_LOCALE).'".');
} else {
// Nothing to do here, this is expected!
// The Moodle locale setting isn't what the collator expected but
// it is smart enough to match the first characters of our locale
// to find the correct locale or to use UCA collation
}
} else {
// We've received some other sort of non fatal warning - let the
// user know about it via debugging.
debugging('Problem with locale: '.$errormessage.'. '.
'Requested locale: "'.$locale.'", actual locale "'.$localeinuse.'". '.
'The most specific locale supported by ICU relatively to the requested locale is "'.
$collator->getLocale(Locale::VALID_LOCALE).'".');
}
}
// Store the collator object now that we can be sure it is in a workable condition
self::$collator = $collator;
} else {
// Fatal error while trying to instantiate the collator... something went wrong
debugging('Error instantiating collator for locale: "' . $locale . '", with error [' .
intl_get_error_code() . '] ' . intl_get_error_message($collator));
}
}
}
return (self::$collator instanceof Collator);
}
/**
* Restore array contents keeping new keys.
* @static
* @param array $arr
* @param array $original
* @return void modifies $arr
*/
protected static function restore_array(array &$arr, array &$original) {
foreach ($arr as $key => $ignored) {
$arr[$key] = $original[$key];
}
}
/**
* Normalise numbers in strings for natural sorting comparisons.
* @static
* @param string $string
* @return string string with normalised numbers
*/
protected static function naturalise($string) {
return preg_replace_callback('/[0-9]+/', array('core_collator', 'callback_naturalise'), $string);
}
/**
* @internal
* @static
* @param array $matches
* @return string
*/
public static function callback_naturalise($matches) {
return str_pad($matches[0], 20, '0', STR_PAD_LEFT);
}
/**
* Locale aware sorting, the key associations are kept, values are sorted alphabetically.
*
* @param array $arr array to be sorted (reference)
* @param int $sortflag One of core_collator::SORT_NUMERIC, core_collator::SORT_STRING, core_collator::SORT_NATURAL, core_collator::SORT_REGULAR
* optionally "|" core_collator::CASE_SENSITIVE
* @return bool True on success
*/
public static function asort(array &$arr, $sortflag = core_collator::SORT_STRING) {
if (empty($arr)) {
// nothing to do
return true;
}
$original = null;
$casesensitive = (bool)($sortflag & core_collator::CASE_SENSITIVE);
$sortflag = ($sortflag & ~core_collator::CASE_SENSITIVE);
if ($sortflag != core_collator::SORT_NATURAL and $sortflag != core_collator::SORT_STRING) {
$casesensitive = false;
}
if (self::ensure_collator_available()) {
if ($sortflag == core_collator::SORT_NUMERIC) {
$flag = Collator::SORT_NUMERIC;
} else if ($sortflag == core_collator::SORT_REGULAR) {
$flag = Collator::SORT_REGULAR;
} else {
$flag = Collator::SORT_STRING;
}
if ($sortflag == core_collator::SORT_NATURAL) {
$original = $arr;
if ($sortflag == core_collator::SORT_NATURAL) {
foreach ($arr as $key => $value) {
$arr[$key] = self::naturalise((string)$value);
}
}
}
if ($casesensitive) {
self::$collator->setAttribute(Collator::CASE_FIRST, Collator::UPPER_FIRST);
} else {
self::$collator->setAttribute(Collator::CASE_FIRST, Collator::OFF);
}
$result = self::$collator->asort($arr, $flag);
if ($original) {
self::restore_array($arr, $original);
}
return $result;
}
// try some fallback that works at least for English
if ($sortflag == core_collator::SORT_NUMERIC) {
return asort($arr, SORT_NUMERIC);
} else if ($sortflag == core_collator::SORT_REGULAR) {
return asort($arr, SORT_REGULAR);
}
if (!$casesensitive) {
$original = $arr;
foreach ($arr as $key => $value) {
$arr[$key] = core_text::strtolower($value);
}
}
if ($sortflag == core_collator::SORT_NATURAL) {
$result = natsort($arr);
} else {
$result = asort($arr, SORT_LOCALE_STRING);
}
if ($original) {
self::restore_array($arr, $original);
}
return $result;
}
/**
* Locale aware sort of objects by a property in common to all objects
*
* @param array $objects An array of objects to sort (handled by reference)
* @param string $property The property to use for comparison
* @param int $sortflag One of core_collator::SORT_NUMERIC, core_collator::SORT_STRING, core_collator::SORT_NATURAL, core_collator::SORT_REGULAR
* optionally "|" core_collator::CASE_SENSITIVE
* @return bool True on success
*/
public static function asort_objects_by_property(array &$objects, $property, $sortflag = core_collator::SORT_STRING) {
$original = $objects;
foreach ($objects as $key => $object) {
$objects[$key] = $object->$property;
}
$result = self::asort($objects, $sortflag);
self::restore_array($objects, $original);
return $result;
}
/**
* Locale aware sort of objects by a method in common to all objects
*
* @param array $objects An array of objects to sort (handled by reference)
* @param string $method The method to call to generate a value for comparison
* @param int $sortflag One of core_collator::SORT_NUMERIC, core_collator::SORT_STRING, core_collator::SORT_NATURAL, core_collator::SORT_REGULAR
* optionally "|" core_collator::CASE_SENSITIVE
* @return bool True on success
*/
public static function asort_objects_by_method(array &$objects, $method, $sortflag = core_collator::SORT_STRING) {
$original = $objects;
foreach ($objects as $key => $object) {
$objects[$key] = $object->{$method}();
}
$result = self::asort($objects, $sortflag);
self::restore_array($objects, $original);
return $result;
}
/**
* Locale aware sort of array of arrays.
*
* Given an array like:
* $array = array(
* array('name' => 'bravo'),
* array('name' => 'charlie'),
* array('name' => 'alpha')
* );
*
* If you call:
* core_collator::asort_array_of_arrays_by_key($array, 'name')
*
* You will be returned $array sorted by the name key of the subarrays. e.g.
* $array = array(
* array('name' => 'alpha'),
* array('name' => 'bravo'),
* array('name' => 'charlie')
* );
*
* @param array $array An array of objects to sort (handled by reference)
* @param string $key The key to use for comparison
* @param int $sortflag One of
* core_collator::SORT_NUMERIC,
* core_collator::SORT_STRING,
* core_collator::SORT_NATURAL,
* core_collator::SORT_REGULAR
* optionally "|" core_collator::CASE_SENSITIVE
* @return bool True on success
*/
public static function asort_array_of_arrays_by_key(array &$array, $key, $sortflag = core_collator::SORT_STRING) {
$original = $array;
foreach ($array as $initkey => $item) {
$array[$initkey] = $item[$key];
}
$result = self::asort($array, $sortflag);
self::restore_array($array, $original);
return $result;
}
/**
* Locale aware sorting, the key associations are kept, keys are sorted alphabetically.
*
* @param array $arr array to be sorted (reference)
* @param int $sortflag One of core_collator::SORT_NUMERIC, core_collator::SORT_STRING, core_collator::SORT_NATURAL, core_collator::SORT_REGULAR
* optionally "|" core_collator::CASE_SENSITIVE
* @return bool True on success
*/
public static function ksort(array &$arr, $sortflag = core_collator::SORT_STRING) {
$keys = array_keys($arr);
if (!self::asort($keys, $sortflag)) {
return false;
}
// This is a bit slow, but we need to keep the references
$original = $arr;
$arr = array(); // Surprisingly this does not break references outside
foreach ($keys as $key) {
$arr[$key] = $original[$key];
}
return true;
}
}
File diff suppressed because it is too large Load Diff
+180
View File
@@ -0,0 +1,180 @@
<?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/>.
/**
* Content API File Area definition.
*
* @package core_files
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core;
use coding_exception;
use context;
use core\content\export\exporters\course_exporter;
use core\content\export\exporters\component_exporter;
use core\content\export\exporters\abstract_mod_exporter;
use core\content\export\zipwriter;
use core_component;
use moodle_url;
use stdClass;
use stored_file;
/**
* The Content API allows all parts of Moodle to determine details about content within a component, or plugintype.
*
* This includes the description of files.
*
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class content {
/**
* Check whether the specified user can export content for the specified context.
*
* @param context $currentcontext
* @param stdClass $user
* @return bool
*/
public static function can_export_context(context $currentcontext, stdClass $user): bool {
global $CFG;
$canexport = false;
if ($currentcontext->contextlevel == CONTEXT_COURSE) {
if ($CFG->downloadcoursecontentallowed &&
has_capability('moodle/course:downloadcoursecontent', $currentcontext, $user)) {
$courseinfo = get_fast_modinfo($currentcontext->instanceid)->get_course();
// If enabled/disabled explicitly set on course, use that as the course setting, otherwise use site default.
if (isset($courseinfo->downloadcontent) && $courseinfo->downloadcontent != DOWNLOAD_COURSE_CONTENT_SITE_DEFAULT) {
$canexport = $courseinfo->downloadcontent;
} else {
$canexport = get_config('moodlecourse')->downloadcontentsitedefault;
}
}
} else if ($currentcontext->contextlevel == CONTEXT_MODULE) {
$cm = get_fast_modinfo($currentcontext->get_course_context()->instanceid)->cms[$currentcontext->instanceid];
// Do not export course content if disabled at activity level.
if (isset($cm->downloadcontent) && $cm->downloadcontent == DOWNLOAD_COURSE_CONTENT_DISABLED) {
return false;
}
// Modules can only be exported if exporting is allowed in their course context.
$canexport = self::can_export_context($currentcontext->get_course_context(), $user);
}
return $canexport;
}
/**
* Export content for the specified context.
*
* @param context $requestedcontext The context to be exported
* @param stdClass $user The user being exported
* @param zipwriter $archive The Zip Archive to export to
*/
public static function export_context(context $requestedcontext, stdClass $user, zipwriter $archive): void {
global $USER;
if ($requestedcontext->contextlevel != CONTEXT_COURSE) {
throw new coding_exception('The Content Export API currently only supports the export of courses');
}
if ($USER->id != $user->id) {
throw new coding_exception('The Content Export API currently only supports export of the current user');
}
// Ensure that the zipwriter is aware of the requested context.
$archive->set_root_context($requestedcontext);
// Fetch all child contexts, indexed by path.
$contextlist = [
$requestedcontext->path => $requestedcontext,
];
foreach ($requestedcontext->get_child_contexts() as $context) {
$contextlist[$context->path] = $context;
}
// Reverse the order by key - this ensures that child contexts are processed before their parent.
krsort($contextlist);
// Get the course modinfo.
$modinfo = get_fast_modinfo($requestedcontext->instanceid);
// Filter out any context which cannot be exported.
$contextlist = array_filter($contextlist, function($context) use ($user, $modinfo): bool {
if ($context->contextlevel == CONTEXT_COURSE) {
return self::can_export_context($context, $user);
}
if ($context->contextlevel == CONTEXT_MODULE) {
if (empty($modinfo->cms[$context->instanceid])) {
// Unknown coursemodule in the course.
return false;
}
$cm = $modinfo->cms[$context->instanceid];
if (!$cm->uservisible) {
// This user cannot view the activity.
return false;
}
// Defer to setting checks.
return self::can_export_context($context, $user);
}
// Only course and activities are supported at this time.
return false;
});
// Export each context.
$exportedcontexts = [];
$coursecontroller = new course_exporter($requestedcontext->get_course_context(), $user, $archive);
foreach ($contextlist as $context) {
if ($context->contextlevel === CONTEXT_MODULE) {
$cm = $modinfo->cms[$context->instanceid];
$component = "mod_{$cm->modname}";
// Check for a specific implementation for this module.
// This will export any content specific to this activity.
// For example, in mod_folder it will export the list of folders.
$classname = component_exporter::get_classname_for_component($component);
$exportables = [];
if (class_exists($classname) && is_a($classname, abstract_mod_exporter::class, true)) {
$controller = new $classname($context, $component, $user, $archive);
$exportables = $controller->get_exportables();
}
// Pass the exportable content to the course controller for export.
$coursecontroller->export_mod_content($context, $exportables);
$exportedcontexts[$context->id] = $context;
} else if ($context->contextlevel === CONTEXT_COURSE) {
// Export the course content.
$coursecontroller->export_course($exportedcontexts);
}
}
$archive->finish();
}
}
@@ -0,0 +1,96 @@
<?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/>.
/**
* The definition of an item which can be exported.
*
* @package core
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
declare(strict_types=1);
namespace core\content\export;
use context;
use core\content\export\exported_item;
use core\content\export\zipwriter;
/**
* An object used to represent content which can be served.
*
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class exportable_item {
/** @var context The context associated with this exportable item */
protected $context = null;
/** @var string The component being exported */
protected $component = null;
/** @var string The name displayed to the user */
protected $uservisiblename = null;
/**
* Create a new exportable_item instance.
*
* @param context $context The context that this content belongs to
* @param string $component The component that this content relates to
* @param string $uservisiblename The name displayed in the export
*/
public function __construct(context $context, string $component, string $uservisiblename) {
$this->context = $context;
$this->component = $component;
$this->uservisiblename = $uservisiblename;
}
/**
* Get the context that this exportable item is for.
*
* @return context
*/
public function get_context(): context {
return $this->context;
}
/**
* Get the component that this exportable item relates to.
*
* @return string
*/
public function get_component(): string {
return $this->component;
}
/**
* Get the user visible name for the exportable item.
*
* @return string
*/
public function get_user_visible_name(): string {
return $this->uservisiblename;
}
/**
* Add the content to the archive.
*
* @param zipwriter $archive
*/
abstract public function add_to_archive(zipwriter $archive): ?exported_item;
}
@@ -0,0 +1,179 @@
<?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/>.
/**
* The definition of a set of files in a filearea to be exported.
*
* @package core
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
declare(strict_types=1);
namespace core\content\export\exportable_items;
use context;
use core\content\export\exportable_item;
use core\content\export\exported_item;
use core\content\export\zipwriter;
use moodle_url;
use stored_file;
/**
* The definition of a set of files in a filearea to be exported.
*
* All files mustbe in a single filearea and itemid combination.
*
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class exportable_filearea extends exportable_item {
/** @var string The destination path of the text content */
protected $folderpath;
/** @var string $filearea The file to be exported */
protected $filearea;
/** @var bool|int The itemid in the Files API */
protected $itemid;
/** @var int The itemid to use in the pluginfile URL */
protected $pluginfileitemid;
/**
* Create a new exportable_item instance.
*
* If no filearea or itemid is specified the no attempt will be made to export files.
*
* @param context $context The context that this content belongs to
* @param string $component
* @param string $uservisiblename The name displayed to the user when filtering
* @param string $filearea The file area in the Files API where these files are located
* @param int $itemid The itemid in the Files API where these files are located
* @param null|int $pluginfileitemid The itemid as used in the Pluginfile URL
* @param string $folderpath Any sub-directory to place files in
*/
public function __construct(
context $context,
string $component,
string $uservisiblename,
string $filearea,
int $itemid,
?int $pluginfileitemid = null,
string $folderpath = ''
) {
parent::__construct($context, $component, $uservisiblename);
$this->filearea = $filearea;
$this->itemid = $itemid;
$this->pluginfileitemid = $pluginfileitemid;
$this->folderpath = $folderpath;
}
/**
* Add the content to the archive.
*
* @param zipwriter $archive
*/
public function add_to_archive(zipwriter $archive): ?exported_item {
$fs = get_file_storage();
$files = $fs->get_area_files($this->context->id, $this->component, $this->filearea, $this->itemid);
$exporteditem = new exported_item();
$exporteditem->set_title($this->get_user_visible_name());
foreach ($files as $file) {
if ($file->is_directory()) {
// Skip folders. The zipwriter cannot handle them.
continue;
}
// Export the content to [contextpath]/[filepath].
$relativefilepath = $this->get_filepath_for_file($file);
$archive->add_file_from_stored_file(
$this->get_context(),
$relativefilepath,
$file
);
if ($archive->is_file_in_archive($this->context, $relativefilepath)) {
// The file was successfully added to the archive.
$exporteditem->add_file($relativefilepath, false);
} else {
// The file was not added. Link to the live version instead.
$exporteditem->add_file(
$relativefilepath,
false,
self::get_pluginfile_url_for_stored_file($file, $this->pluginfileitemid)
);
}
}
return $exporteditem;
}
/**
* Get the filepath for the specified stored_file.
*
* @param stored_file $file The file to get a filepath for
* @return string The generated filepath
*/
protected function get_filepath_for_file(stored_file $file): string {
$folderpath = rtrim($this->folderpath);
if (!empty($folderpath)) {
$folderpath .= '/';
}
return sprintf(
'%s%s%s%s',
$folderpath,
$file->get_filearea(),
$file->get_filepath(),
$file->get_filename()
);
}
/**
* Get the pluginfile URL for a stored file.
*
* Note: The itemid in the pluginfile may be omitted in some URLs, despite an itemid being present in the database.
* Equally, the itemid in the URL may not match the itemid in the files table.
*
* The pluginfileitemid argument provided to this function is the variant in the URL, and not the one in the files
* table.
*
* @param stored_file $file The file whose link will be generated
* @param null|int $pluginfileitemid The itemid of the file in pluginfile URL.
*
*/
protected static function get_pluginfile_url_for_stored_file(stored_file $file, ?int $pluginfileitemid): string {
$link = moodle_url::make_pluginfile_url(
$file->get_contextid(),
$file->get_component(),
$file->get_filearea(),
$pluginfileitemid,
$file->get_filepath(),
$file->get_filename(),
true,
true
);
return $link->out(false);
}
}
@@ -0,0 +1,202 @@
<?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/>.
/**
* The definition of an item which can be exported.
*
* @package core
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
declare(strict_types=1);
namespace core\content\export\exportable_items;
use context;
use core\content\export\exportable_item;
use core\content\export\exported_item;
use core\content\export\zipwriter;
use moodle_url;
use stored_file;
/**
* An object used to represent content which can be served.
*
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class exportable_stored_file extends exportable_item {
/** @var string The destination path of the text content */
protected $folderpath;
/** @var stored_file The file to be exported */
protected $file;
/** @var int The itemid to use in the pluginfile URL */
protected $pluginfileitemid;
/**
* Create a new exportable_item instance.
*
* If no filearea or itemid is specified the no attempt will be made to export files.
*
* @param context $context The context that this content belongs to
* @param string $component
* @param string $uservisiblename The name displayed to the user when filtering
* @param stored_file $file
* @param null|int $pluginfileitemid The itemid as used in the pluginfile URL.
* If no itemid is used, then a null value can be provided
* @param string $folderpath Any sub-directory to place files in
*/
public function __construct(
context $context,
string $component,
string $uservisiblename,
stored_file $file,
?int $pluginfileitemid = null,
string $folderpath = ''
) {
parent::__construct($context, $component, $uservisiblename);
$this->file = $file;
$this->folderpath = $folderpath;
$this->pluginfileitemid = $pluginfileitemid;
}
/**
* Create a set of exportable_items from a set of area paramaters as passed to get_areas_files().
*
* If no filearea or itemid is specified the no attempt will be made to export files.
*
* @param context $context The context that this content belongs to
* @param string $component
* @param string $filearea
* @param null|int $itemid
* @param null|int $pluginfileitemid The itemid as used in the pluginfile URL.
* If no itemid is used, then a null value can be provided
* @param string $folderpath Any sub-directory to place files in
* @return array
*/
public static function create_from_area_params(
context $context,
string $component,
string $filearea,
?int $itemid,
?int $pluginfileitemid = null,
string $folderpath = ''
): array {
$fs = get_file_storage();
if ($itemid === null) {
$itemid = false;
}
$exportables = [];
foreach ($fs->get_area_files($context->id, $component, $filearea, $itemid) as $file) {
if ($file->is_directory()) {
// Do not export directories.
// If they contain file contents the directory structure will be created in the zip file.
continue;
}
$filepath = $file->get_filepath() . $file->get_filename();
$exportables[] = new self($context, $component, $filepath, $file, $pluginfileitemid, $folderpath);
}
return $exportables;
}
/**
* Add the content to the archive.
*
* @param zipwriter $archive
*/
public function add_to_archive(zipwriter $archive): ?exported_item {
// Export the content to [contextpath]/[filepath].
$relativefilepath = $this->get_filepath_for_file();
$archive->add_file_from_stored_file(
$this->get_context(),
$relativefilepath,
$this->file
);
$exporteditem = new exported_item();
$exporteditem->set_title($this->get_user_visible_name());
if ($archive->is_file_in_archive($this->context, $relativefilepath)) {
// The file was successfully added to the archive.
$exporteditem->add_file($relativefilepath, false);
} else {
// The file was not added. Link to the live version instead.
$exporteditem->add_file(
$relativefilepath,
false,
self::get_pluginfile_url_for_stored_file($this->file, $this->pluginfileitemid)
);
}
return $exporteditem;
}
/**
* Get the filepath for the specified stored_file.
*
* @return string
*/
protected function get_filepath_for_file(): string {
$folderpath = rtrim($this->folderpath);
if (!empty($folderpath)) {
$folderpath .= '/';
}
return sprintf(
'%s%s%s%s',
$folderpath,
$this->file->get_filearea(),
$this->file->get_filepath(),
$this->file->get_filename()
);
}
/**
* Get the pluginfile URL for a stored file.
*
* Note: The itemid in the pluginfile may be omitted in some URLs, despite an itemid being present in the database.
* Equally, the itemid in the URL may not match the itemid in the files table.
*
* The pluginfileitemid argument provided to this function is the variant in the URL, and not the one in the files
* table.
*
* @param stored_file $file The file whose link will be generated
* @param null|int $pluginfileitemid The itemid of the file in pluginfile URL.
*
*/
protected static function get_pluginfile_url_for_stored_file(stored_file $file, ?int $pluginfileitemid): string {
$link = moodle_url::make_pluginfile_url(
$file->get_contextid(),
$file->get_component(),
$file->get_filearea(),
$pluginfileitemid,
$file->get_filepath(),
$file->get_filename(),
true,
true
);
return $link->out(false);
}
}
@@ -0,0 +1,172 @@
<?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/>.
/**
* The definition of a text area which can be exported.
*
* @package core
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
declare(strict_types=1);
namespace core\content\export\exportable_items;
use context;
use core\content\export\exportable_item;
use core\content\export\exported_item;
use core\content\export\zipwriter;
/**
* The definition of a text area which can be exported.
*
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class exportable_textarea extends exportable_item {
/** @var string The name of the table that ha the textarea within it */
protected $tablename;
/** @var int The id in the table */
protected $id;
/** @var string The name of the text field within the table */
protected $textfield;
/** @var null|string The name of the format field relating to the text field */
protected $textformatfield;
/** @var null|string The name of a file area for this content */
protected $filearea;
/** @var null|int The itemid for files in this text field */
protected $itemid;
/** @var null|int The itemid used for constructing pluginfiles */
protected $pluginfileitemid;
/**
* Create a new exportable_item instance.
*
* If no filearea or itemid is specified the no attempt will be made to export files.
*
* @param context $context The context that this content belongs to
* @param string $component The component that this textarea belongs to
* @param string $uservisiblename The name displayed to the user when filtering
* @param string $tablename The name of the table that this textarea is in
* @param string $textfield The field within the tbale
* @param int $id The id in the database
* @param null|string $textformatfield The field in the database relating to the format field if one is present
* @param null|string $filearea The name of the file area for files associated with this text area
* @param null|int $itemid The itemid for files associated with this text area
* @param null|int $pluginfileitemid The itemid to use when constructing the pluginfile URL
* Some fileareas do not use any itemid in the URL and should therefore provide a `null` value here.
*/
public function __construct(
context $context,
string $component,
string $uservisiblename,
string $tablename,
string $textfield,
int $id,
?string $textformatfield = null,
?string $filearea = null,
?int $itemid = null,
?int $pluginfileitemid = null
) {
parent::__construct($context, $component, $uservisiblename);
$this->tablename = $tablename;
$this->textfield = $textfield;
$this->textformatfield = $textformatfield;
$this->id = $id;
$this->filearea = $filearea;
$this->itemid = $itemid;
$this->pluginfileitemid = $pluginfileitemid;
}
/**
* Add the content to the archive.
*
* @param zipwriter $archive
*/
public function add_to_archive(zipwriter $archive): ?exported_item {
global $DB;
// Fetch the field.
$fields = [$this->textfield];
if (!empty($this->textformatfield)) {
$fields[] = $this->textformatfield;
}
$record = $DB->get_record($this->tablename, ['id' => $this->id], implode(', ', $fields));
if (empty($record)) {
return null;
}
// Export all of the files for this text area.
$text = $record->{$this->textfield};
if (empty($text)) {
$text = '';
}
if ($this->may_include_files()) {
// This content may include inline files.
$exporteditem = $archive->add_pluginfiles_for_content(
$this->get_context(),
"",
$text,
$this->component,
$this->filearea,
$this->itemid,
$this->pluginfileitemid
);
} else {
$exporteditem = new exported_item();
$exporteditem->set_content($text);
}
if (!empty($this->textformatfield)) {
$formattedcontent = format_text($exporteditem->get_content(), $record->{$this->textformatfield},
['context' => $this->get_context()]);
$exporteditem->set_content($formattedcontent);
}
$exporteditem->set_title($this->get_user_visible_name());
return $exporteditem;
}
/**
* Whether files may be included in this textarea.
*
* Both a filearea, and itemid are required for files to be exportable.
*
* @return bool
*/
protected function may_include_files(): bool {
if ($this->filearea === null) {
return false;
}
if ($this->itemid === null) {
return false;
}
return true;
}
}
@@ -0,0 +1,198 @@
<?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/>.
/**
* Exported Item.
*
* @package core
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
declare(strict_types=1);
namespace core\content\export;
use stdClass;
/**
* This class describes the files which were exported, and any text content that those files were contained in.
*
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class exported_item {
/** @var string A short, descriptive, name for this exported item */
protected $title = null;
/** @var string Any string content for export */
protected $content = '';
/** @var string[] A list of files which were exported and are not present in the content */
protected $files = [];
/** @var string[] A list of files which were exported and are present in the content */
protected $contentfiles = [];
/**
* Constructor for the exported_item.
*
* @param array $files A list of all files which were exported
*/
public function __construct(array $files = []) {
$this->add_files($files);
}
/**
* Set a title for this exported item.
*
* @param string $title
*/
public function set_title(string $title): void {
$this->title = $title;
}
/**
* Add a file to the list of exported files.
*
* @param string $relativefilepath The path to the content relative to the exported context
* @param bool $incontent Whether this file is included within the content
* @param null|string $url The URL to use of the live file where the file could not be stored in the archive
*/
public function add_file(string $relativefilepath, bool $incontent = false, ?string $url = null): void {
if ($url === null) {
$url = $relativefilepath;
}
$file = (object) [
'filepath' => $url,
'filename' => basename($relativefilepath),
];
$this->files[$relativefilepath] = $file;
if ($incontent) {
$this->contentfiles[$relativefilepath] = $file;
}
}
/**
* Add a list of files to the list of exported files.
*
* @param string[] $files The path to the content relative to the exported context
* @param bool $incontent Whether this file is included within the content
*/
public function add_files(array $files, bool $incontent = false): void {
foreach ($files as $relativefilepath) {
$this->add_file($relativefilepath, $incontent);
}
}
/**
* Set the rewritten content.
*
* @param string $content
*/
public function set_content(string $content): void {
$this->content = $content;
}
/**
* Fetch the rewritten content.
*
* @return string
*/
public function get_content(): string {
return $this->content;
}
/**
* Get a short, descriptive name associated with the exported content, if one is avaiable.
*
* @return null|string
*/
public function get_title(): ?string {
return $this->title;
}
/**
* Get all template data for this exported item.
*
* @return stdClass
*/
public function get_template_data(): stdClass {
return (object) [
'title' => $this->get_title(),
'files' => $this->get_noncontent_files(),
'content' => $this->content,
];
}
/**
* Get a list of all files in the exported item.
*
* @return array
*/
public function get_all_files(): array {
return $this->files;
}
/**
* Get a list of all files present in the content.
*
* That is those files which were exported, and which are referenced in some fashion.
* These files typically do not need to be listed separately.
*
* @return array
*/
public function get_content_files(): array {
return $this->contentfiles;
}
/**
* Get all files which are not already referenced in the content.
*
* These files will typically be displayed in a separate list.
*
* @return array
*/
public function get_noncontent_files(): array {
return array_values(array_diff_key(
$this->get_all_files(),
$this->get_content_files()
));
}
/**
* Check whether the exported_item includes any data.
*
* @return bool
*/
public function has_any_data(): bool {
if ($this->get_all_files()) {
// Some files are present.
return true;
}
if (trim(html_to_text($this->get_content())) !== '') {
// The content is not empty.
return true;
}
// No truthy conditions match.
return false;
}
}
@@ -0,0 +1,69 @@
<?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/>.
/**
* Activity module exporter for the content API.
*
* @package core
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\content\export\exporters;
use core\content\controllers\export\component_controller;
/**
* Activity module exporter for the content API.
*
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class abstract_mod_exporter extends component_exporter {
/** @var \cm_info The activity information for this course module */
protected $cm;
/**
* Constructor for the general activity exporter.
*/
public function __construct() {
parent::__construct(...func_get_args());
$coursecontext = $this->context->get_course_context();
$modinfo = get_fast_modinfo($coursecontext->instanceid);
$this->cm = $modinfo->get_cm($this->context->instanceid);
}
/**
* Get the exportable items for the user in the specified context.
*
* For activities which allow users to submit their own content which is not visible to all users, for example
* graded activities, the caller can request that this be either included, or excluded.
*
* @param bool $includeuserdata Whether to include user data, in addition to shared content.
* @return exportable_item[]
*/
abstract public function get_exportables(bool $includeuserdata = false): array;
/**
* Get the modname for the activity.
*
* @return string
*/
protected function get_modname(): string {
return $this->cm->modname;
}
}
@@ -0,0 +1,112 @@
<?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/>.
/**
* Content API Export definition.
*
* @package core
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\content\export\exporters;
use coding_exception;
use context;
use core\content\export\zipwriter;
use core_component;
use stdClass;
/**
* A class to help define, describe, and export content in a specific context.
*
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class component_exporter {
/** @var context The context to be exported */
protected $context = null;
/** @var string The component that this instance belongs to */
protected $component = null;
/** @var stdClass The user being exported */
protected $user;
/** @var zipwriter A reference to the zipwriter */
protected $archive;
/**
* Constructor for a new exporter.
*
* @param context $context The context to export
* @param string $component The component that this instance relates to
* @param stdClass $user The user to be exported
* @param zipwriter $archive
*/
public function __construct(context $context, string $component, stdClass $user, zipwriter $archive) {
$this->context = $context;
$this->component = $component;
$this->user = $user;
$this->archive = $archive;
}
/**
* Get the context being exported.
*
* @return context
*/
public function get_context(): context {
return $this->context;
}
/**
* Get the component name.
*
* @return string
*/
public function get_component(): string {
[$type, $component] = core_component::normalize_component($this->component);
if ($type === 'core') {
return $component;
}
return core_component::normalize_componentname($this->component);
}
/**
* Get the archive used for export.
*
* @return zipwriter
*/
public function get_archive(): zipwriter {
if ($this->archive === null) {
throw new coding_exception("Archive has not been set up yet");
}
return $this->archive;
}
/**
* Get the name of the exporter for the specified component.
*
* @param string $component The component to fetch a classname for
* @return string The classname for the component
*/
public static function get_classname_for_component(string $component): string {
return "{$component}\\content\\exporter";
}
}
@@ -0,0 +1,286 @@
<?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/>.
/**
* The course exporter.
*
* @package core
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\content\export\exporters;
use context_course;
use context_module;
use core\content\export\exported_item;
use core\content\export\zipwriter;
use section_info;
use stdClass;
/**
* The course exporter.
*
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_exporter extends component_exporter {
/** @var stdClass The course being exported */
protected $course;
/** @var \course_modinfo The course_modinfo instnace for this course */
protected $modinfo;
/**
* Constructor for the course exporter.
*
* @param context_course $context The context of the course to export
* @param stdClass $user
* @param zipwriter $archive
*/
public function __construct(context_course $context, stdClass $user, zipwriter $archive) {
$this->course = get_course($context->instanceid);
$this->modinfo = get_fast_modinfo($this->course, $user->id);
parent::__construct($context, 'core_course', $user, $archive);
}
/**
* Export the course.
*
* @param \context[] $exportedcontexts A list of contexts which were successfully exported
*/
public function export_course(array $exportedcontexts): void {
// A course export is composed of:
// - Course summary (including inline files)
// - Overview files
// - Section:
// -- Section name
// -- Section summary (including inline files)
// -- List of available activities.
$aboutpagelink = $this->add_course_about_page();
$templatedata = (object) [
'aboutpagelink' => $aboutpagelink,
'sections' => [],
];
// Add all sections.
foreach ($this->modinfo->get_section_info_all() as $number => $section) {
$templatedata->sections[] = $this->get_course_section($exportedcontexts, $section);
}
$this->get_archive()->add_file_from_template(
$this->get_context(),
'index.html',
'core/content/export/course_index',
$templatedata
);
}
/**
* Add course about page.
*
* @return null|string The URL to the about page if one was generated
*/
protected function add_course_about_page(): ?string {
$hascontent = false;
$templatedata = (object) [
'summary' => '',
'overviewfiles' => [],
];
// Fetch the course summary content.
if ($this->course->summary) {
$summarydata = $this->get_archive()->add_pluginfiles_for_content(
$this->get_context(),
'_course',
$this->course->summary,
'course',
'summary',
0,
null
);
if ($summarydata->has_any_data()) {
$hascontent = true;
$templatedata->summary = format_text($summarydata->get_content(), $this->course->summaryformat,
['context' => $this->get_context()]);
}
}
$files = $this->get_archive()->add_pluginfiles_for_content(
$this->get_context(),
'',
'',
'course',
'overviewfiles',
0,
null
)->get_noncontent_files();
if (count($files)) {
$templatedata->overviewfiles = $files;
$hascontent = true;
}
if ($hascontent) {
$this->get_archive()->add_file_from_template(
$this->get_context(),
'about.html',
'core/content/export/course_summary',
$templatedata
);
return $this->get_archive()->get_relative_context_path($this->get_context(), $this->get_context(), 'about.html');
}
return null;
}
/**
* Fetch data for the specified course section.
*
* @param \context[] $exportedcontexts A list of contexts which were successfully exported
* @param section_info $section The section being exported
* @return stdClass
*/
protected function get_course_section(array $exportedcontexts, section_info $section): stdClass {
$sectiondata = (object) [
'number' => $section->section,
'title' => format_string($section->name, true, ['context' => $this->get_context()]),
'summary' => '',
'activities' => [],
];
// Fetch the section summary content.
if ($section->summary) {
$summarydata = $this->get_archive()->add_pluginfiles_for_content(
$this->get_context(),
'_course',
$section->summary,
'course',
'section',
$section->id,
$section->id
);
if ($summarydata->has_any_data()) {
$sectiondata->summary = format_text($summarydata->get_content(), $section->summaryformat,
['context' => $this->get_context()]);
}
}
if (empty($this->modinfo->sections[$section->section])) {
return $sectiondata;
}
foreach ($this->modinfo->sections[$section->section] as $cmid) {
$cm = $this->modinfo->cms[$cmid];
if (!$cm->uservisible) {
continue;
}
if (array_key_exists($cm->context->id, $exportedcontexts)) {
// This activity was exported.
// The link to it from the course index should be a relative link.
$url = $this->get_archive()->get_relative_context_path($this->get_context(), $cm->context, 'index.html');
} else {
// This activity was not included in the export for some reason.
// Link to the live activity.
$url = $cm->url;
}
$sectiondata->activities[] = (object) [
'title' => $cm->get_formatted_name(),
'modname' => $cm->modfullname,
'link' => $url,
];
}
return $sectiondata;
}
/**
* Export all exportable content for an activity module.
*
* @param context_module $modcontect
* @param exportable_item[] $export_exportables
*/
public function export_mod_content(context_module $modcontext, array $exportables): void {
$cm = $this->modinfo->get_cm($modcontext->instanceid);
$modname = $cm->modname;
$templatedata = (object) [
'modulelink' => $cm->url,
'modulename' => $cm->get_formatted_name(),
'intro' => null,
'sections' => [],
];
if (plugin_supports('mod', $modname, FEATURE_MOD_INTRO, true)) {
$templatedata->intro = $this->get_mod_intro_data($modcontext);
}
$exporteditems = [];
foreach ($exportables as $exportable) {
$exporteditem = $exportable->add_to_archive($this->get_archive());
$templatedata->sections[] = $exporteditem->get_template_data();
}
// Add the index to the archive.
$this->get_archive()->add_file_from_template(
$modcontext,
'index.html',
'core/content/export/module_index',
$templatedata
);
}
/**
* Get the course_module introduction data.
*
* @param context_module $modcontect
* @return null|string The content of the intro area
*/
protected function get_mod_intro_data(context_module $modcontext): ?string {
global $DB;
$cm = $this->modinfo->get_cm($modcontext->instanceid);
$modname = $cm->modname;
$record = $DB->get_record($modname, ['id' => $cm->instance], 'intro, introformat');
// Fetch the module intro content.
if ($record->intro) {
$exporteditem = $this->get_archive()->add_pluginfiles_for_content(
$modcontext,
'',
$record->intro,
"mod_{$modname}",
'intro',
0,
null
);
if ($exporteditem->has_any_data()) {
return format_text($exporteditem->get_content(), $record->introformat, ['context' => $modcontext]);
}
}
return null;
}
}
+553
View File
@@ -0,0 +1,553 @@
<?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/>.
/**
* Zip writer wrapper.
*
* @package core
* @copyright 2020 Simey Lameze <simey@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\content\export;
use context;
use context_system;
use moodle_url;
use stdClass;
use stored_file;
/**
* Zip writer wrapper.
*
* @copyright 2020 Simey Lameze <simey@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class zipwriter {
/** @var int Maximum folder length name for a context */
const MAX_CONTEXT_NAME_LENGTH = 32;
/** @var \ZipStream\ZipStream */
protected $archive;
/** @var int Max file size of an individual file in the archive */
protected $maxfilesize = 1 * 1024 * 1024 * 10;
/** @var resource File resource for the file handle for a file-based zip stream */
protected $zipfilehandle = null;
/** @var string File path for a file-based zip stream */
protected $zipfilepath = null;
/** @var context The context to use as a base for export */
protected $rootcontext = null;
/** @var array The files in the zip */
protected $filesinzip = [];
/** @var bool Whether page requirements needed for HTML pages have been added */
protected $pagerequirementsadded = false;
/** @var stdClass The course relating to the root context */
protected $course;
/** @var context The context of the course for the root contect */
protected $coursecontext;
/**
* zipwriter constructor.
*
* @param \ZipStream\ZipStream $archive
* @param stdClass|null $options
*/
public function __construct(\ZipStream\ZipStream $archive, stdClass $options = null) {
$this->archive = $archive;
if ($options) {
$this->parse_options($options);
}
$this->rootcontext = context_system::instance();
}
/**
* Set a root context for use during the export.
*
* This is primarily used for creating paths within the archive relative to the root context.
*
* @param context $rootcontext
*/
public function set_root_context(context $rootcontext): void {
$this->rootcontext = $rootcontext;
}
/**
* Get the course object for the root context.
*
* @return stdClass
*/
protected function get_course(): stdClass {
if ($this->course && ($this->coursecontext !== $this->rootcontext->get_course_context())) {
$this->coursecontext = null;
$this->course = null;
}
if (empty($this->course)) {
$this->coursecontext = $this->rootcontext->get_course_context();
$this->course = get_course($this->coursecontext->instanceid);
}
return $this->course;
}
/**
* Parse options.
*
* @param stdClass $options
*/
protected function parse_options(stdClass $options): void {
if (property_exists($options, 'maxfilesize')) {
$this->maxfilesize = $options->maxfilesize;
}
}
/**
* Finish writing the zip footer.
*/
public function finish(): void {
$this->archive->finish();
if ($this->zipfilehandle) {
fclose($this->zipfilehandle);
}
}
/**
* Get the stream writer.
*
* @param string $filename
* @param stdClass|null $exportoptions
* @return static
*/
public static function get_stream_writer(string $filename, stdClass $exportoptions = null) {
$archive = new \ZipStream\ZipStream(
outputName: $filename,
);
$zipwriter = new static($archive, $exportoptions);
\core\session\manager::write_close();
return $zipwriter;
}
/**
* Get the file writer.
*
* @param string $filename
* @param stdClass|null $exportoptions
* @return static
*/
public static function get_file_writer(string $filename, stdClass $exportoptions = null) {
$dir = make_request_directory();
$filepath = $dir . "/$filename";
$fh = fopen($filepath, 'w');
$archive = new \ZipStream\ZipStream(
outputName: $filename,
outputStream: $fh,
sendHttpHeaders: false,
);
$zipwriter = new static($archive, $exportoptions);
$zipwriter->zipfilehandle = $fh;
$zipwriter->zipfilepath = $filepath;
\core\session\manager::write_close();
return $zipwriter;
}
/**
* Get the file path for a file-based zip writer.
*
* If this is not a file-based writer then no value is returned.
*
* @return null|string
*/
public function get_file_path(): ?string {
return $this->zipfilepath;
}
/**
* Add a file from the File Storage API.
*
* @param context $context
* @param string $filepathinzip
* @param stored_file $file The file to add
*/
public function add_file_from_stored_file(
context $context,
string $filepathinzip,
stored_file $file
): void {
$fullfilepathinzip = $this->get_context_path($context, $filepathinzip);
if ($file->get_filesize() <= $this->maxfilesize) {
$filehandle = $file->get_content_file_handle();
$this->archive->addFileFromStream($fullfilepathinzip, $filehandle);
fclose($filehandle);
$this->filesinzip[] = $fullfilepathinzip;
}
}
/**
* Add a file from string content.
*
* @param context $context
* @param string $filepathinzip
* @param string $content
*/
public function add_file_from_string(
context $context,
string $filepathinzip,
string $content
): void {
$fullfilepathinzip = $this->get_context_path($context, $filepathinzip);
$this->archive->addFile($fullfilepathinzip, $content);
$this->filesinzip[] = $fullfilepathinzip;
}
/**
* Create a file based on a Mustache Template and associated data.
*
* @param context $context
* @param string $filepathinzip
* @param string $template
* @param stdClass $templatedata
*/
public function add_file_from_template(
context $context,
string $filepathinzip,
string $template,
stdClass $templatedata
): void {
global $CFG, $PAGE, $SITE, $USER;
$exportedcourse = $this->get_course();
$courselink = (new moodle_url('/course/view.php', ['id' => $exportedcourse->id]))->out(false);
$coursename = format_string($exportedcourse->fullname, true, ['context' => $this->coursecontext]);
$this->add_template_requirements();
$templatedata->global = (object) [
'righttoleft' => right_to_left(),
'language' => get_html_lang_attribute_value(current_language()),
'sitename' => format_string($SITE->fullname, true, ['context' => context_system::instance()]),
'siteurl' => $CFG->wwwroot,
'pathtotop' => $this->get_relative_context_path($context, $this->rootcontext, '/'),
'contentexportfooter' => get_string('contentexport_footersummary', 'core', (object) [
'courselink' => $courselink,
'coursename' => $coursename,
'userfullname' => fullname($USER),
'date' => userdate(time()),
]),
'contentexportsummary' => get_string('contentexport_coursesummary', 'core', (object) [
'courselink' => $courselink,
'coursename' => $coursename,
'date' => userdate(time()),
]),
'coursename' => $coursename,
'courseshortname' => $exportedcourse->shortname,
'courselink' => $courselink,
'exportdate' => userdate(time()),
'maxfilesize' => display_size($this->maxfilesize, 0),
];
$renderer = $PAGE->get_renderer('core');
$this->add_file_from_string($context, $filepathinzip, $renderer->render_from_template($template, $templatedata));
}
/**
* Ensure that all requirements for a templated page are present.
*
* This includes CSS, and any other similar content.
*/
protected function add_template_requirements(): void {
if ($this->pagerequirementsadded) {
return;
}
// CSS required.
$this->add_content_from_dirroot('/theme/boost/style/moodle.css', 'shared/moodle.css');
$this->pagerequirementsadded = true;
}
/**
* Add content from the dirroot into the specified path in the zip file.
*
* @param string $dirrootpath
* @param string $pathinzip
*/
protected function add_content_from_dirroot(string $dirrootpath, string $pathinzip): void {
global $CFG;
$this->archive->addFileFromPath(
$this->get_context_path($this->rootcontext, $pathinzip),
"{$CFG->dirroot}/{$dirrootpath}"
);
}
/**
* Check whether the file was actually added to the archive.
*
* @param context $context
* @param string $filepathinzip
* @return bool
*/
public function is_file_in_archive(context $context, string $filepathinzip): bool {
$fullfilepathinzip = $this->get_context_path($context, $filepathinzip);
return in_array($fullfilepathinzip, $this->filesinzip);
}
/**
* Get the full path to the context within the zip.
*
* @param context $context
* @param string $filepathinzip
* @return string
*/
public function get_context_path(context $context, string $filepathinzip): string {
if (!$context->is_child_of($this->rootcontext, true)) {
throw new \coding_exception("Unexpected path requested");
}
// Fetch the path from the course down.
$parentcontexts = array_filter(
$context->get_parent_contexts(true),
function(context $curcontext): bool {
return $curcontext->is_child_of($this->rootcontext, true);
}
);
foreach (array_reverse($parentcontexts) as $curcontext) {
$path[] = $this->get_context_folder_name($curcontext);
}
$path[] = $filepathinzip;
$finalpath = implode(DIRECTORY_SEPARATOR, $path);
// Remove relative paths (./).
$finalpath = str_replace('./', '/', $finalpath);
// De-duplicate slashes.
$finalpath = str_replace('//', '/', $finalpath);
return $finalpath;
}
/**
* Get a relative path to the specified context path.
*
* @param context $rootcontext
* @param context $targetcontext
* @param string $filepathinzip
* @return string
*/
public function get_relative_context_path(context $rootcontext, context $targetcontext, string $filepathinzip): string {
$path = [];
if ($targetcontext === $rootcontext) {
$lookupcontexts = [];
} else if ($targetcontext->is_child_of($rootcontext, true)) {
// Fetch the path from the course down.
$lookupcontexts = array_filter(
$targetcontext->get_parent_contexts(true),
function(context $curcontext): bool {
return $curcontext->is_child_of($this->rootcontext, false);
}
);
foreach ($lookupcontexts as $curcontext) {
array_unshift($path, $this->get_context_folder_name($curcontext));
}
} else if ($targetcontext->is_parent_of($rootcontext, true)) {
$lookupcontexts = $targetcontext->get_parent_contexts(true);
$path[] = '..';
}
$path[] = $filepathinzip;
$relativepath = implode(DIRECTORY_SEPARATOR, $path);
// De-duplicate slashes and remove leading /.
$relativepath = ltrim(preg_replace('#/+#', '/', $relativepath), '/');
if (substr($relativepath, 0, 1) !== '.') {
$relativepath = "./{$relativepath}";
}
return $relativepath;
}
/**
* Get the name of the folder for the specified context.
*
* @param context $context
* @return string
*/
protected function get_context_folder_name(context $context): string {
// Replace spaces with underscores, or they will be removed completely when cleaning.
$contextname = str_replace(' ', '_', $context->get_context_name());
// Clean the context name of all but basic characters, as some systems don't support unicode within zip structure.
$shortenedname = shorten_text(
clean_param($contextname, PARAM_SAFEDIR),
self::MAX_CONTEXT_NAME_LENGTH,
true
);
return "{$shortenedname}_.{$context->id}";
}
/**
* Rewrite any pluginfile URLs in the content.
*
* @param context $context
* @param string $content
* @param string $component
* @param string $filearea
* @param null|int $pluginfileitemid The itemid to use in the pluginfile URL when composing any required URLs
* @return string
*/
protected function rewrite_other_pluginfile_urls(
context $context,
string $content,
string $component,
string $filearea,
?int $pluginfileitemid
): string {
// The pluginfile URLs should have been rewritten when the files were exported, but if any file was too large it
// may not have been included.
// In that situation use a tokenpluginfile URL.
if (strpos($content, '@@PLUGINFILE@@/') !== false) {
// Some files could not be rewritten.
// Use a tokenurl pluginfile for those.
$content = file_rewrite_pluginfile_urls(
$content,
'pluginfile.php',
$context->id,
$component,
$filearea,
$pluginfileitemid,
[
'includetoken' => true,
]
);
}
return $content;
}
/**
* Export files releating to this text area.
*
* @param context $context
* @param string $subdir The sub directory to export any files to
* @param string $content
* @param string $component
* @param string $filearea
* @param int $fileitemid The itemid as used in the Files API
* @param null|int $pluginfileitemid The itemid to use in the pluginfile URL when composing any required URLs
* @return exported_item
*/
public function add_pluginfiles_for_content(
context $context,
string $subdir,
string $content,
string $component,
string $filearea,
int $fileitemid,
?int $pluginfileitemid
): exported_item {
// Export all of the files for this text area.
$fs = get_file_storage();
$files = $fs->get_area_files($context->id, $component, $filearea, $fileitemid);
$result = new exported_item();
foreach ($files as $file) {
if ($file->is_directory()) {
continue;
}
$filepathinzip = self::get_filepath_for_file($file, $subdir, false);
$this->add_file_from_stored_file(
$context,
$filepathinzip,
$file
);
if ($this->is_file_in_archive($context, $filepathinzip)) {
// Attempt to rewrite any @@PLUGINFILE@@ URLs for this file in the content.
$searchpath = "@@PLUGINFILE@@" . $file->get_filepath() . rawurlencode($file->get_filename());
if (strpos($content, $searchpath) !== false) {
$content = str_replace($searchpath, self::get_filepath_for_file($file, $subdir, true), $content);
$result->add_file($filepathinzip, true);
} else {
$result->add_file($filepathinzip, false);
}
}
}
$content = $this->rewrite_other_pluginfile_urls($context, $content, $component, $filearea, $pluginfileitemid);
$result->set_content($content);
return $result;
}
/**
* Get the filepath for the specified stored_file.
*
* @param stored_file $file
* @param string $parentdir Any parent directory to place this file in
* @param bool $escape
* @return string
*/
protected static function get_filepath_for_file(stored_file $file, string $parentdir, bool $escape): string {
$path = [];
$filepath = sprintf(
'%s/%s/%s/%s',
$parentdir,
$file->get_filearea(),
$file->get_filepath(),
$file->get_filename()
);
if ($escape) {
foreach (explode('/', $filepath) as $dirname) {
$path[] = rawurlencode($dirname);
}
$filepath = implode('/', $path);
}
return ltrim(preg_replace('#/+#', '/', $filepath), '/');
}
}
File diff suppressed because it is too large Load Diff
+265
View File
@@ -0,0 +1,265 @@
<?php
// This file is part of Moodle - https://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 <https://www.gnu.org/licenses/>.
namespace core\context;
use core\context;
use stdClass;
use coding_exception, moodle_url;
/**
* Block context class
*
* @package core_access
* @category access
* @copyright Petr Skoda
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since Moodle 4.2
*/
class block extends context {
/** @var int numeric context level value matching legacy CONTEXT_BLOCK */
public const LEVEL = 80;
/**
* Please use \core\context\block::instance($blockinstanceid) if you need the instance of context.
* Alternatively if you know only the context id use \core\context::instance_by_id($contextid)
*
* @param stdClass $record
*/
protected function __construct(stdClass $record) {
parent::__construct($record);
if ($record->contextlevel != self::LEVEL) {
throw new coding_exception('Invalid $record->contextlevel in core\context\block constructor');
}
}
/**
* Returns short context name.
*
* @since Moodle 4.2
*
* @return string
*/
public static function get_short_name(): string {
return 'block';
}
/**
* Returns human readable context level name.
*
* @return string the human readable context level name.
*/
public static function get_level_name() {
return get_string('block');
}
/**
* Returns human readable context identifier.
*
* @param boolean $withprefix whether to prefix the name of the context with Block
* @param boolean $short does not apply to block context
* @param boolean $escape does not apply to block context
* @return string the human readable context name.
*/
public function get_context_name($withprefix = true, $short = false, $escape = true) {
global $DB;
$name = '';
if ($blockinstance = $DB->get_record('block_instances', array('id' => $this->_instanceid))) {
$blockobject = block_instance($blockinstance->blockname);
if ($blockobject) {
if ($withprefix) {
$name = get_string('block').': ';
}
$name .= $blockobject->title;
}
}
return $name;
}
/**
* Returns the most relevant URL for this context.
*
* @return moodle_url
*/
public function get_url() {
$parentcontexts = $this->get_parent_context();
return $parentcontexts->get_url();
}
/**
* Returns list of all possible parent context levels.
* @since Moodle 4.2
*
* @return int[]
*/
public static function get_possible_parent_levels(): array {
// Blocks may be added to any other context instance.
$alllevels = \core\context_helper::get_all_levels();
unset($alllevels[self::LEVEL]);
return array_keys($alllevels);
}
/**
* Returns array of relevant context capability records.
*
* @param string $sort
* @return array
*/
public function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT) {
global $DB;
$bi = $DB->get_record('block_instances', array('id' => $this->_instanceid));
$select = '(contextlevel = :level AND component = :component)';
$params = [
'level' => self::LEVEL,
'component' => 'block_' . $bi->blockname,
];
$extracaps = block_method_result($bi->blockname, 'get_extra_capabilities');
if ($extracaps) {
list($extra, $extraparams) = $DB->get_in_or_equal($extracaps, SQL_PARAMS_NAMED, 'cap');
$select .= " OR name $extra";
$params = array_merge($params, $extraparams);
}
return $DB->get_records_select('capabilities', $select, $params, $sort);
}
/**
* Is this context part of any course? If yes return course context.
*
* @param bool $strict true means throw exception if not found, false means return false if not found
* @return course context of the enclosing course, null if not found or exception
*/
public function get_course_context($strict = true) {
$parentcontext = $this->get_parent_context();
return $parentcontext->get_course_context($strict);
}
/**
* Returns block context instance.
*
* @param int $blockinstanceid id from {block_instances} table.
* @param int $strictness
* @return block|false context instance
*/
public static function instance($blockinstanceid, $strictness = MUST_EXIST) {
global $DB;
if ($context = context::cache_get(self::LEVEL, $blockinstanceid)) {
return $context;
}
if (!$record = $DB->get_record('context', array('contextlevel' => self::LEVEL, 'instanceid' => $blockinstanceid))) {
if ($bi = $DB->get_record('block_instances', array('id' => $blockinstanceid), 'id,parentcontextid', $strictness)) {
$parentcontext = context::instance_by_id($bi->parentcontextid);
$record = context::insert_context_record(self::LEVEL, $bi->id, $parentcontext->path);
}
}
if ($record) {
$context = new block($record);
context::cache_add($context);
return $context;
}
return false;
}
/**
* Block do not have child contexts...
* @return array
*/
public function get_child_contexts() {
return array();
}
/**
* Create missing context instances at block context level
*/
protected static function create_level_instances() {
global $DB;
$sql = <<<EOF
INSERT INTO {context} (
contextlevel,
instanceid
) SELECT
:contextlevel,
bi.id as instanceid
FROM {block_instances} bi
WHERE NOT EXISTS (
SELECT 'x' FROM {context} cx WHERE bi.id = cx.instanceid AND cx.contextlevel = :existingcontextlevel
)
EOF;
$DB->execute($sql, [
'contextlevel' => self::LEVEL,
'existingcontextlevel' => self::LEVEL,
]);
}
/**
* Returns sql necessary for purging of stale context instances.
*
* @return string cleanup SQL
*/
protected static function get_cleanup_sql() {
$sql = "
SELECT c.*
FROM {context} c
LEFT OUTER JOIN {block_instances} bi ON c.instanceid = bi.id
WHERE bi.id IS NULL AND c.contextlevel = ".self::LEVEL."
";
return $sql;
}
/**
* Rebuild context paths and depths at block context level.
*
* @param bool $force
*/
protected static function build_paths($force) {
global $DB;
if ($force || $DB->record_exists_select('context', "contextlevel = ".self::LEVEL." AND (depth = 0 OR path IS NULL)")) {
if ($force) {
$ctxemptyclause = '';
} else {
$ctxemptyclause = "AND (ctx.path IS NULL OR ctx.depth = 0)";
}
// The pctx.path IS NOT NULL prevents fatal problems with broken block instances that point to invalid context parent.
$sql = "INSERT INTO {context_temp} (id, path, depth, locked)
SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1, ctx.locked
FROM {context} ctx
JOIN {block_instances} bi ON (bi.id = ctx.instanceid AND ctx.contextlevel = " . self::LEVEL . ")
JOIN {context} pctx ON (pctx.id = bi.parentcontextid)
WHERE (pctx.path IS NOT NULL AND pctx.depth > 0)
$ctxemptyclause";
$trans = $DB->start_delegated_transaction();
$DB->delete_records('context_temp');
$DB->execute($sql);
context::merge_context_temp_table();
$DB->delete_records('context_temp');
$trans->allow_commit();
}
}
}
+298
View File
@@ -0,0 +1,298 @@
<?php
// This file is part of Moodle - https://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 <https://www.gnu.org/licenses/>.
namespace core\context;
use core\context;
use stdClass;
use coding_exception, moodle_url;
/**
* Course context class
*
* @package core_access
* @category access
* @copyright Petr Skoda
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since Moodle 4.2
*/
class course extends context {
/** @var int numeric context level value matching legacy CONTEXT_COURSE */
public const LEVEL = 50;
/**
* Please use \core\context\course::instance($courseid) if you need the instance of context.
* Alternatively if you know only the context id use \core\context::instance_by_id($contextid)
*
* @param stdClass $record
*/
protected function __construct(stdClass $record) {
parent::__construct($record);
if ($record->contextlevel != self::LEVEL) {
throw new coding_exception('Invalid $record->contextlevel in core\context\course constructor.');
}
}
/**
* Returns short context name.
*
* @since Moodle 4.2
*
* @return string
*/
public static function get_short_name(): string {
return 'course';
}
/**
* Returns human readable context level name.
*
* @return string the human readable context level name.
*/
public static function get_level_name() {
return get_string('course');
}
/**
* Returns human readable context identifier.
*
* @param boolean $withprefix whether to prefix the name of the context with Course
* @param boolean $short whether to use the short name of the thing.
* @param bool $escape Whether the returned category name is to be HTML escaped or not.
* @return string the human readable context name.
*/
public function get_context_name($withprefix = true, $short = false, $escape = true) {
global $DB;
$name = '';
if ($this->_instanceid == SITEID) {
$name = get_string('frontpage', 'admin');
} else {
if ($course = $DB->get_record('course', array('id' => $this->_instanceid))) {
if ($withprefix) {
$name = get_string('course').': ';
}
if ($short) {
if (!$escape) {
$name .= format_string($course->shortname, true, array('context' => $this, 'escape' => false));
} else {
$name .= format_string($course->shortname, true, array('context' => $this));
}
} else {
if (!$escape) {
$name .= format_string(get_course_display_name_for_list($course), true, array('context' => $this,
'escape' => false));
} else {
$name .= format_string(get_course_display_name_for_list($course), true, array('context' => $this));
}
}
}
}
return $name;
}
/**
* Returns the most relevant URL for this context.
*
* @return moodle_url
*/
public function get_url() {
if ($this->_instanceid != SITEID) {
return new moodle_url('/course/view.php', array('id' => $this->_instanceid));
}
return new moodle_url('/');
}
/**
* Returns context instance database name.
*
* @return string|null table name for all levels except system.
*/
protected static function get_instance_table(): ?string {
return 'course';
}
/**
* Returns list of columns that can be used from behat
* to look up context by reference.
*
* @return array list of column names from instance table
*/
protected static function get_behat_reference_columns(): array {
return ['shortname'];
}
/**
* Returns list of all role archetypes that are compatible
* with role assignments in context level.
* @since Moodle 4.2
*
* @return int[]
*/
protected static function get_compatible_role_archetypes(): array {
return ['manager', 'editingteacher', 'teacher', 'student'];
}
/**
* Returns list of all possible parent context levels.
* @since Moodle 4.2
*
* @return int[]
*/
public static function get_possible_parent_levels(): array {
return [coursecat::LEVEL];
}
/**
* Returns array of relevant context capability records.
*
* @param string $sort
* @return array
*/
public function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT) {
global $DB;
$levels = \core\context_helper::get_child_levels(self::LEVEL);
$levels[] = self::LEVEL;
return $DB->get_records_list('capabilities', 'contextlevel', $levels, $sort);
}
/**
* Is this context part of any course? If yes return course context.
*
* @param bool $strict true means throw exception if not found, false means return false if not found
* @return course context of the enclosing course, null if not found or exception
*/
public function get_course_context($strict = true) {
return $this;
}
/**
* Returns course context instance.
*
* @param int $courseid id from {course} table
* @param int $strictness
* @return course|false context instance
*/
public static function instance($courseid, $strictness = MUST_EXIST) {
global $DB;
if ($context = context::cache_get(self::LEVEL, $courseid)) {
return $context;
}
if (!$record = $DB->get_record('context', array('contextlevel' => self::LEVEL, 'instanceid' => $courseid))) {
if ($course = $DB->get_record('course', array('id' => $courseid), 'id,category', $strictness)) {
if ($course->category) {
$parentcontext = coursecat::instance($course->category);
$record = context::insert_context_record(self::LEVEL, $course->id, $parentcontext->path);
} else {
$record = context::insert_context_record(self::LEVEL, $course->id, '/'.SYSCONTEXTID, 0);
}
}
}
if ($record) {
$context = new course($record);
context::cache_add($context);
return $context;
}
return false;
}
/**
* Create missing context instances at course context level
*/
protected static function create_level_instances() {
global $DB;
$sql = "SELECT ".self::LEVEL.", c.id
FROM {course} c
WHERE NOT EXISTS (SELECT 'x'
FROM {context} cx
WHERE c.id = cx.instanceid AND cx.contextlevel=".self::LEVEL.")";
$contextdata = $DB->get_recordset_sql($sql);
foreach ($contextdata as $context) {
context::insert_context_record(self::LEVEL, $context->id, null);
}
$contextdata->close();
}
/**
* Returns sql necessary for purging of stale context instances.
*
* @return string cleanup SQL
*/
protected static function get_cleanup_sql() {
$sql = "
SELECT c.*
FROM {context} c
LEFT OUTER JOIN {course} co ON c.instanceid = co.id
WHERE co.id IS NULL AND c.contextlevel = ".self::LEVEL."
";
return $sql;
}
/**
* Rebuild context paths and depths at course context level.
*
* @param bool $force
*/
protected static function build_paths($force) {
global $DB;
if ($force || $DB->record_exists_select('context', "contextlevel = ".self::LEVEL." AND (depth = 0 OR path IS NULL)")) {
if ($force) {
$ctxemptyclause = $emptyclause = '';
} else {
$ctxemptyclause = "AND (ctx.path IS NULL OR ctx.depth = 0)";
$emptyclause = "AND ({context}.path IS NULL OR {context}.depth = 0)";
}
$base = '/'.SYSCONTEXTID;
// Standard frontpage.
$sql = "UPDATE {context}
SET depth = 2,
path = ".$DB->sql_concat("'$base/'", 'id')."
WHERE contextlevel = ".self::LEVEL."
AND EXISTS (SELECT 'x'
FROM {course} c
WHERE c.id = {context}.instanceid AND c.category = 0)
$emptyclause";
$DB->execute($sql);
// Standard courses.
$sql = "INSERT INTO {context_temp} (id, path, depth, locked)
SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1, ctx.locked
FROM {context} ctx
JOIN {course} c ON (c.id = ctx.instanceid AND ctx.contextlevel = ".self::LEVEL." AND c.category <> 0)
JOIN {context} pctx ON (pctx.instanceid = c.category AND pctx.contextlevel = ".coursecat::LEVEL.")
WHERE pctx.path IS NOT NULL AND pctx.depth > 0
$ctxemptyclause";
$trans = $DB->start_delegated_transaction();
$DB->delete_records('context_temp');
$DB->execute($sql);
context::merge_context_temp_table();
$DB->delete_records('context_temp');
$trans->allow_commit();
}
}
}
+304
View File
@@ -0,0 +1,304 @@
<?php
// This file is part of Moodle - https://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 <https://www.gnu.org/licenses/>.
namespace core\context;
use core\context;
use stdClass;
use coding_exception, moodle_url;
/**
* Course category context class
*
* @package core_access
* @category access
* @copyright Petr Skoda
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since Moodle 4.2
*/
class coursecat extends context {
/** @var int numeric context level value matching legacy CONTEXT_COURSECAT */
public const LEVEL = 40;
/**
* Please use \core\context\coursecat::instance($coursecatid) if you need the instance of context.
* Alternatively if you know only the context id use \core\context::instance_by_id($contextid)
*
* @param stdClass $record
*/
protected function __construct(stdClass $record) {
parent::__construct($record);
if ($record->contextlevel != self::LEVEL) {
throw new coding_exception('Invalid $record->contextlevel in core\context\coursecat constructor.');
}
}
/**
* Returns short context name.
*
* @since Moodle 4.2
*
* @return string
*/
public static function get_short_name(): string {
return 'coursecat';
}
/**
* Returns human readable context level name.
*
* @return string the human readable context level name.
*/
public static function get_level_name() {
return get_string('category');
}
/**
* Returns human readable context identifier.
*
* @param boolean $withprefix whether to prefix the name of the context with Category
* @param boolean $short does not apply to course categories
* @param boolean $escape Whether the returned name of the context is to be HTML escaped or not.
* @return string the human readable context name.
*/
public function get_context_name($withprefix = true, $short = false, $escape = true) {
global $DB;
$name = '';
if ($category = $DB->get_record('course_categories', array('id' => $this->_instanceid))) {
if ($withprefix) {
$name = get_string('category').': ';
}
if (!$escape) {
$name .= format_string($category->name, true, array('context' => $this, 'escape' => false));
} else {
$name .= format_string($category->name, true, array('context' => $this));
}
}
return $name;
}
/**
* Returns the most relevant URL for this context.
*
* @return moodle_url
*/
public function get_url() {
return new moodle_url('/course/index.php', array('categoryid' => $this->_instanceid));
}
/**
* Returns context instance database name.
*
* @return string|null table name for all levels except system.
*/
protected static function get_instance_table(): ?string {
return 'course_categories';
}
/**
* Returns list of columns that can be used from behat
* to look up context by reference.
*
* @return array list of column names from instance table
*/
protected static function get_behat_reference_columns(): array {
return ['idnumber'];
}
/**
* Returns list of all role archetypes that are compatible
* with role assignments in context level.
* @since Moodle 4.2
*
* @return int[]
*/
protected static function get_compatible_role_archetypes(): array {
return ['manager', 'coursecreator'];
}
/**
* Returns list of all possible parent context levels.
* @since Moodle 4.2
*
* @return int[]
*/
public static function get_possible_parent_levels(): array {
return [system::LEVEL, self::LEVEL];
}
/**
* Returns array of relevant context capability records.
*
* @param string $sort
* @return array
*/
public function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT) {
global $DB;
$levels = \core\context_helper::get_child_levels(self::LEVEL);
$levels[] = self::LEVEL;
return $DB->get_records_list('capabilities', 'contextlevel', $levels, $sort);
}
/**
* Returns course category context instance.
*
* @param int $categoryid id from {course_categories} table
* @param int $strictness
* @return coursecat|false context instance
*/
public static function instance($categoryid, $strictness = MUST_EXIST) {
global $DB;
if ($context = context::cache_get(self::LEVEL, $categoryid)) {
return $context;
}
if (!$record = $DB->get_record('context', array('contextlevel' => self::LEVEL, 'instanceid' => $categoryid))) {
if ($category = $DB->get_record('course_categories', array('id' => $categoryid), 'id,parent', $strictness)) {
if ($category->parent) {
$parentcontext = self::instance($category->parent);
$record = context::insert_context_record(self::LEVEL, $category->id, $parentcontext->path);
} else {
$record = context::insert_context_record(self::LEVEL, $category->id, '/'.SYSCONTEXTID, 0);
}
}
}
if ($record) {
$context = new coursecat($record);
context::cache_add($context);
return $context;
}
return false;
}
/**
* Returns immediate child contexts of category and all subcategories,
* children of subcategories and courses are not returned.
*
* @return array
*/
public function get_child_contexts() {
global $DB;
if (empty($this->_path) || empty($this->_depth)) {
debugging('Can not find child contexts of context '.$this->_id.' try rebuilding of context paths');
return array();
}
$sql = "SELECT ctx.*
FROM {context} ctx
WHERE ctx.path LIKE ? AND (ctx.depth = ? OR ctx.contextlevel = ?)";
$params = array($this->_path.'/%', $this->depth + 1, self::LEVEL);
$records = $DB->get_records_sql($sql, $params);
$result = array();
foreach ($records as $record) {
$result[$record->id] = context::create_instance_from_record($record);
}
return $result;
}
/**
* Create missing context instances at course category context level
*/
protected static function create_level_instances() {
global $DB;
$sql = "SELECT ".self::LEVEL.", cc.id
FROM {course_categories} cc
WHERE NOT EXISTS (SELECT 'x'
FROM {context} cx
WHERE cc.id = cx.instanceid AND cx.contextlevel=".self::LEVEL.")";
$contextdata = $DB->get_recordset_sql($sql);
foreach ($contextdata as $context) {
context::insert_context_record(self::LEVEL, $context->id, null);
}
$contextdata->close();
}
/**
* Returns sql necessary for purging of stale context instances.
*
* @return string cleanup SQL
*/
protected static function get_cleanup_sql() {
$sql = "
SELECT c.*
FROM {context} c
LEFT OUTER JOIN {course_categories} cc ON c.instanceid = cc.id
WHERE cc.id IS NULL AND c.contextlevel = ".self::LEVEL."
";
return $sql;
}
/**
* Rebuild context paths and depths at course category context level.
*
* @param bool $force
*/
protected static function build_paths($force) {
global $DB;
if ($force || $DB->record_exists_select('context', "contextlevel = ".self::LEVEL." AND (depth = 0 OR path IS NULL)")) {
if ($force) {
$ctxemptyclause = $emptyclause = '';
} else {
$ctxemptyclause = "AND (ctx.path IS NULL OR ctx.depth = 0)";
$emptyclause = "AND ({context}.path IS NULL OR {context}.depth = 0)";
}
$base = '/'.SYSCONTEXTID;
// Normal top level categories.
$sql = "UPDATE {context}
SET depth=2,
path=".$DB->sql_concat("'$base/'", 'id')."
WHERE contextlevel=".self::LEVEL."
AND EXISTS (SELECT 'x'
FROM {course_categories} cc
WHERE cc.id = {context}.instanceid AND cc.depth=1)
$emptyclause";
$DB->execute($sql);
// Deeper categories - one query per depthlevel.
$maxdepth = $DB->get_field_sql("SELECT MAX(depth) FROM {course_categories}");
for ($n = 2; $n <= $maxdepth; $n++) {
$sql = "INSERT INTO {context_temp} (id, path, depth, locked)
SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1, ctx.locked
FROM {context} ctx
JOIN {course_categories} cc ON (cc.id = ctx.instanceid AND ctx.contextlevel = ".self::LEVEL."
AND cc.depth = $n)
JOIN {context} pctx ON (pctx.instanceid = cc.parent AND pctx.contextlevel = ".self::LEVEL.")
WHERE pctx.path IS NOT NULL AND pctx.depth > 0
$ctxemptyclause";
$trans = $DB->start_delegated_transaction();
$DB->delete_records('context_temp');
$DB->execute($sql);
context::merge_context_temp_table();
$DB->delete_records('context_temp');
$trans->allow_commit();
}
}
}
}
+349
View File
@@ -0,0 +1,349 @@
<?php
// This file is part of Moodle - https://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 <https://www.gnu.org/licenses/>.
namespace core\context;
use core\context;
use stdClass;
use coding_exception, moodle_url;
/**
* Course module context class
*
* @package core_access
* @category access
* @copyright Petr Skoda
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since Moodle 4.2
*/
class module extends context {
/** @var int numeric context level value matching legacy CONTEXT_MODULE */
public const LEVEL = 70;
/**
* Please use \core\context\module::instance($cmid) if you need the instance of context.
* Alternatively if you know only the context id use \core\context::instance_by_id($contextid)
*
* @param stdClass $record
*/
protected function __construct(stdClass $record) {
parent::__construct($record);
if ($record->contextlevel != self::LEVEL) {
throw new coding_exception('Invalid $record->contextlevel in core\context\module constructor.');
}
}
/**
* Returns short context name.
*
* @since Moodle 4.2
*
* @return string
*/
public static function get_short_name(): string {
return 'module';
}
/**
* Returns human readable context level name.
*
* @return string the human readable context level name.
*/
public static function get_level_name() {
return get_string('activitymodule');
}
/**
* Returns human readable context identifier.
*
* @param boolean $withprefix whether to prefix the name of the context with the
* module name, e.g. Forum, Glossary, etc.
* @param boolean $short does not apply to module context
* @param boolean $escape Whether the returned name of the context is to be HTML escaped or not.
* @return string the human readable context name.
*/
public function get_context_name($withprefix = true, $short = false, $escape = true) {
global $DB;
$name = '';
if ($cm = $DB->get_record_sql("SELECT cm.*, md.name AS modname
FROM {course_modules} cm
JOIN {modules} md ON md.id = cm.module
WHERE cm.id = ?", array($this->_instanceid))) {
if ($mod = $DB->get_record($cm->modname, array('id' => $cm->instance))) {
if ($withprefix) {
$name = get_string('modulename', $cm->modname).': ';
}
if (!$escape) {
$name .= format_string($mod->name, true, array('context' => $this, 'escape' => false));
} else {
$name .= format_string($mod->name, true, array('context' => $this));
}
}
}
return $name;
}
/**
* Returns the most relevant URL for this context.
*
* @return moodle_url
*/
public function get_url() {
global $DB;
if ($modname = $DB->get_field_sql("SELECT md.name AS modname
FROM {course_modules} cm
JOIN {modules} md ON md.id = cm.module
WHERE cm.id = ?", array($this->_instanceid))) {
return new moodle_url('/mod/' . $modname . '/view.php', array('id' => $this->_instanceid));
}
return new moodle_url('/');
}
/**
* Returns context instance database name.
*
* @return string|null table name for all levels except system.
*/
protected static function get_instance_table(): ?string {
return 'course_modules';
}
/**
* Returns list of columns that can be used from behat
* to look up context by reference.
*
* @return array list of column names from instance table
*/
protected static function get_behat_reference_columns(): array {
return ['idnumber'];
}
/**
* Returns list of all role archetypes that are compatible
* with role assignments in context level.
* @since Moodle 4.2
*
* @return int[]
*/
protected static function get_compatible_role_archetypes(): array {
return ['editingteacher', 'teacher', 'student'];
}
/**
* Returns list of all possible parent context levels.
* @since Moodle 4.2
*
* @return int[]
*/
public static function get_possible_parent_levels(): array {
return [course::LEVEL];
}
/**
* Returns array of relevant context capability records.
*
* @param string $sort
* @return array
*/
public function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT) {
global $DB, $CFG;
$cm = $DB->get_record('course_modules', array('id' => $this->_instanceid));
$module = $DB->get_record('modules', array('id' => $cm->module));
$subcaps = array();
$modulepath = "{$CFG->dirroot}/mod/{$module->name}";
if (file_exists("{$modulepath}/db/subplugins.json")) {
$subplugins = (array) json_decode(file_get_contents("{$modulepath}/db/subplugins.json"))->plugintypes;
} else if (file_exists("{$modulepath}/db/subplugins.php")) {
debugging('Use of subplugins.php has been deprecated. ' .
'Please update your plugin to provide a subplugins.json file instead.',
DEBUG_DEVELOPER);
$subplugins = array(); // Should be redefined in the file.
include("{$modulepath}/db/subplugins.php");
}
if (!empty($subplugins)) {
foreach (array_keys($subplugins) as $subplugintype) {
foreach (array_keys(\core_component::get_plugin_list($subplugintype)) as $subpluginname) {
$subcaps = array_merge($subcaps, array_keys(load_capability_def($subplugintype.'_'.$subpluginname)));
}
}
}
$modfile = "{$modulepath}/lib.php";
$extracaps = array();
if (file_exists($modfile)) {
include_once($modfile);
$modfunction = $module->name.'_get_extra_capabilities';
if (function_exists($modfunction)) {
$extracaps = $modfunction();
}
}
$extracaps = array_merge($subcaps, $extracaps);
$extra = '';
list($extra, $params) = $DB->get_in_or_equal(
$extracaps, SQL_PARAMS_NAMED, 'cap0', true, '');
if (!empty($extra)) {
$extra = "OR name $extra";
}
// Fetch the list of modules, and remove this one.
$components = \core_component::get_component_list();
$componentnames = $components['mod'];
unset($componentnames["mod_{$module->name}"]);
$componentnames = array_keys($componentnames);
// Exclude all other modules.
list($notcompsql, $notcompparams) = $DB->get_in_or_equal($componentnames, SQL_PARAMS_NAMED, 'notcomp', false);
$params = array_merge($params, $notcompparams);
// Exclude other component submodules.
$i = 0;
$ignorecomponents = [];
foreach ($componentnames as $mod) {
if ($subplugins = \core_component::get_subplugins($mod)) {
foreach (array_keys($subplugins) as $subplugintype) {
$paramname = "notlike{$i}";
$ignorecomponents[] = $DB->sql_like('component', ":{$paramname}", true, true, true);
$params[$paramname] = "{$subplugintype}_%";
$i++;
}
}
}
$notlikesql = "(" . implode(' AND ', $ignorecomponents) . ")";
$sql = "SELECT *
FROM {capabilities}
WHERE (contextlevel = ".self::LEVEL."
AND component {$notcompsql}
AND {$notlikesql})
$extra
ORDER BY $sort";
return $DB->get_records_sql($sql, $params);
}
/**
* Is this context part of any course? If yes return course context.
*
* @param bool $strict true means throw exception if not found, false means return false if not found
* @return course|false context of the enclosing course, null if not found or exception
*/
public function get_course_context($strict = true) {
return $this->get_parent_context();
}
/**
* Returns module context instance.
*
* @param int $cmid id of the record from {course_modules} table; pass cmid there, NOT id in the instance column
* @param int $strictness
* @return module|false context instance
*/
public static function instance($cmid, $strictness = MUST_EXIST) {
global $DB;
if ($context = context::cache_get(self::LEVEL, $cmid)) {
return $context;
}
if (!$record = $DB->get_record('context', array('contextlevel' => self::LEVEL, 'instanceid' => $cmid))) {
if ($cm = $DB->get_record('course_modules', array('id' => $cmid), 'id,course', $strictness)) {
$parentcontext = course::instance($cm->course);
$record = context::insert_context_record(self::LEVEL, $cm->id, $parentcontext->path);
}
}
if ($record) {
$context = new module($record);
context::cache_add($context);
return $context;
}
return false;
}
/**
* Create missing context instances at module context level
*/
protected static function create_level_instances() {
global $DB;
$sql = "SELECT " . self::LEVEL . ", cm.id
FROM {course_modules} cm
WHERE NOT EXISTS (SELECT 'x'
FROM {context} cx
WHERE cm.id = cx.instanceid AND cx.contextlevel=" . self::LEVEL . ")";
$contextdata = $DB->get_recordset_sql($sql);
foreach ($contextdata as $context) {
context::insert_context_record(self::LEVEL, $context->id, null);
}
$contextdata->close();
}
/**
* Returns sql necessary for purging of stale context instances.
*
* @return string cleanup SQL
*/
protected static function get_cleanup_sql() {
$sql = "
SELECT c.*
FROM {context} c
LEFT OUTER JOIN {course_modules} cm ON c.instanceid = cm.id
WHERE cm.id IS NULL AND c.contextlevel = " . self::LEVEL . "
";
return $sql;
}
/**
* Rebuild context paths and depths at module context level.
*
* @param bool $force
*/
protected static function build_paths($force) {
global $DB;
if ($force || $DB->record_exists_select('context', "contextlevel = " . self::LEVEL . " AND (depth = 0 OR path IS NULL)")) {
if ($force) {
$ctxemptyclause = '';
} else {
$ctxemptyclause = "AND (ctx.path IS NULL OR ctx.depth = 0)";
}
$sql = "INSERT INTO {context_temp} (id, path, depth, locked)
SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1, ctx.locked
FROM {context} ctx
JOIN {course_modules} cm ON (cm.id = ctx.instanceid AND ctx.contextlevel = " . self::LEVEL . ")
JOIN {context} pctx ON (pctx.instanceid = cm.course AND pctx.contextlevel = " . course::LEVEL . ")
WHERE pctx.path IS NOT NULL AND pctx.depth > 0
$ctxemptyclause";
$trans = $DB->start_delegated_transaction();
$DB->delete_records('context_temp');
$DB->execute($sql);
context::merge_context_temp_table();
$DB->delete_records('context_temp');
$trans->allow_commit();
}
}
}
+307
View File
@@ -0,0 +1,307 @@
<?php
// This file is part of Moodle - https://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 <https://www.gnu.org/licenses/>.
namespace core\context;
use core\context;
use stdClass;
use coding_exception, moodle_url;
/**
* System context class
*
* @package core_access
* @category access
* @copyright Petr Skoda
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since Moodle 4.2
*/
class system extends context {
/** @var int numeric context level value matching legacy CONTEXT_SYSTEM */
public const LEVEL = 10;
/**
* Please use \core\context\system::instance() if you need the instance of context.
*
* @param stdClass $record
*/
protected function __construct(stdClass $record) {
parent::__construct($record);
if ($record->contextlevel != self::LEVEL) {
throw new coding_exception('Invalid $record->contextlevel in core\context\system constructor.');
}
}
/**
* Returns short context name.
*
* @since Moodle 4.2
*
* @return string
*/
public static function get_short_name(): string {
return 'system';
}
/**
* Returns human readable context level name.
*
* @return string the human readable context level name.
*/
public static function get_level_name() {
return get_string('coresystem');
}
/**
* Returns human readable context identifier.
*
* @param boolean $withprefix does not apply to system context
* @param boolean $short does not apply to system context
* @param boolean $escape does not apply to system context
* @return string the human readable context name.
*/
public function get_context_name($withprefix = true, $short = false, $escape = true) {
return self::get_level_name();
}
/**
* Returns the most relevant URL for this context.
*
* @return moodle_url
*/
public function get_url() {
return new moodle_url('/');
}
/**
* Returns list of all role archetypes that are compatible
* with role assignments in context level.
* @since Moodle 4.2
*
* @return int[]
*/
protected static function get_compatible_role_archetypes(): array {
return ['manager', 'coursecreator'];
}
/**
* Returns list of all possible parent context levels.
* @since Moodle 4.2
*
* @return int[]
*/
public static function get_possible_parent_levels(): array {
return [];
}
/**
* Returns array of relevant context capability records.
*
* @param string $sort
* @return array
*/
public function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT) {
global $DB;
return $DB->get_records('capabilities', [], $sort);
}
/**
* Create missing context instances at system context
*/
protected static function create_level_instances() {
// Nothing to do here, the system context is created automatically in installer.
self::instance(0);
}
/**
* Returns system context instance.
*
* @param int $instanceid should be 0
* @param int $strictness
* @param bool $cache
* @return system context instance
*/
public static function instance($instanceid = 0, $strictness = MUST_EXIST, $cache = true) {
global $DB;
if ($instanceid != 0) {
debugging('context_system::instance(): invalid $id parameter detected, should be 0');
}
// SYSCONTEXTID is cached in local cache to eliminate 1 query per page.
if (defined('SYSCONTEXTID') && $cache) {
if (!isset(context::$systemcontext)) {
$record = new stdClass();
$record->id = SYSCONTEXTID;
$record->contextlevel = self::LEVEL;
$record->instanceid = 0;
$record->path = '/'.SYSCONTEXTID;
$record->depth = 1;
$record->locked = 0;
context::$systemcontext = new system($record);
}
return context::$systemcontext;
}
try {
// We ignore the strictness completely because system context must exist except during install.
$record = $DB->get_record('context', array('contextlevel' => self::LEVEL), '*', MUST_EXIST);
} catch (\dml_exception $e) {
// Table or record does not exist.
if (!during_initial_install()) {
// Do not mess with system context after install, it simply must exist.
throw $e;
}
$record = null;
}
if (!$record) {
$record = new stdClass();
$record->contextlevel = self::LEVEL;
$record->instanceid = 0;
$record->depth = 1;
$record->path = null; // Not known before insert.
$record->locked = 0;
try {
if ($DB->count_records('context')) {
// Contexts already exist, this is very weird, system must be first!!!
return null;
}
if (defined('SYSCONTEXTID')) {
// This would happen only in unittest on sites that went through weird 1.7 upgrade.
$record->id = SYSCONTEXTID;
$DB->import_record('context', $record);
$DB->get_manager()->reset_sequence('context');
} else {
$record->id = $DB->insert_record('context', $record);
}
} catch (\dml_exception $e) {
// Can not create context - table does not exist yet, sorry.
return null;
}
}
if ($record->instanceid != 0) {
// This is very weird, somebody must be messing with context table.
debugging('Invalid system context detected');
}
if ($record->depth != 1 || $record->path != '/'.$record->id) {
// Fix path if necessary, initial install or path reset.
$record->depth = 1;
$record->path = '/'.$record->id;
$DB->update_record('context', $record);
}
if (empty($record->locked)) {
$record->locked = 0;
}
if (!defined('SYSCONTEXTID')) {
define('SYSCONTEXTID', $record->id);
}
context::$systemcontext = new system($record);
return context::$systemcontext;
}
/**
* Returns all site contexts except the system context, DO NOT call on production servers!!
*
* Contexts are not cached.
*
* @return array
*/
public function get_child_contexts() {
global $DB;
debugging('Fetching of system context child courses is strongly discouraged'
. ' on production servers (it may eat all available memory)!');
// Just get all the contexts except for system level
// and hope we don't OOM in the process - don't cache.
$sql = "SELECT c.*
FROM {context} c
WHERE contextlevel > " . self::LEVEL;
$records = $DB->get_records_sql($sql);
$result = array();
foreach ($records as $record) {
$result[$record->id] = context::create_instance_from_record($record);
}
return $result;
}
/**
* Returns sql necessary for purging of stale context instances.
*
* @return string cleanup SQL
*/
protected static function get_cleanup_sql() {
$sql = "
SELECT c.*
FROM {context} c
WHERE 1=2
";
return $sql;
}
/**
* Rebuild context paths and depths at system context level.
*
* @param bool $force
*/
protected static function build_paths($force) {
global $DB;
/* note: ignore $force here, we always do full test of system context */
// Exactly one record must exist.
$record = $DB->get_record('context', array('contextlevel' => self::LEVEL), '*', MUST_EXIST);
if ($record->instanceid != 0) {
debugging('Invalid system context detected');
}
if (defined('SYSCONTEXTID') && $record->id != SYSCONTEXTID) {
debugging('Invalid SYSCONTEXTID detected');
}
if ($record->depth != 1 || $record->path != '/'.$record->id) {
// Fix path if necessary, initial install or path reset.
$record->depth = 1;
$record->path = '/'.$record->id;
$DB->update_record('context', $record);
}
}
/**
* Set whether this context has been locked or not.
*
* @param bool $locked
* @return $this
*/
public function set_locked(bool $locked) {
if ($locked) {
throw new \coding_exception('It is not possible to lock the system context');
}
return parent::set_locked($locked);
}
}
+242
View File
@@ -0,0 +1,242 @@
<?php
// This file is part of Moodle - https://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 <https://www.gnu.org/licenses/>.
namespace core\context;
use core\context;
use stdClass;
use coding_exception, moodle_url;
/**
* User context class
*
* @package core_access
* @category access
* @copyright Petr Skoda
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since Moodle 4.2
*/
class user extends context {
/** @var int numeric context level value matching legacy CONTEXT_USER */
public const LEVEL = 30;
/**
* Please use \core\context\user::instance($userid) if you need the instance of context.
* Alternatively if you know only the context id use \core\context::instance_by_id($contextid)
*
* @param stdClass $record
*/
protected function __construct(stdClass $record) {
parent::__construct($record);
if ($record->contextlevel != self::LEVEL) {
throw new coding_exception('Invalid $record->contextlevel in core\context\user constructor.');
}
}
/**
* Returns short context name.
*
* @since Moodle 4.2
*
* @return string
*/
public static function get_short_name(): string {
return 'user';
}
/**
* Returns human readable context level name.
*
* @return string the human readable context level name.
*/
public static function get_level_name() {
return get_string('user');
}
/**
* Returns human readable context identifier.
*
* @param boolean $withprefix whether to prefix the name of the context with User
* @param boolean $short does not apply to user context
* @param boolean $escape does not apply to user context
* @return string the human readable context name.
*/
public function get_context_name($withprefix = true, $short = false, $escape = true) {
global $DB;
$name = '';
if ($user = $DB->get_record('user', array('id' => $this->_instanceid, 'deleted' => 0))) {
if ($withprefix) {
$name = get_string('user').': ';
}
$name .= fullname($user);
}
return $name;
}
/**
* Returns the most relevant URL for this context.
*
* @return moodle_url
*/
public function get_url() {
global $COURSE;
if ($COURSE->id == SITEID) {
$url = new moodle_url('/user/profile.php', array('id' => $this->_instanceid));
} else {
$url = new moodle_url('/user/view.php', array('id' => $this->_instanceid, 'courseid' => $COURSE->id));
}
return $url;
}
/**
* Returns list of all possible parent context levels.
* @since Moodle 4.2
*
* @return int[]
*/
public static function get_possible_parent_levels(): array {
return [system::LEVEL];
}
/**
* Returns context instance database name.
*
* @return string|null table name for all levels except system.
*/
protected static function get_instance_table(): ?string {
return 'user';
}
/**
* Returns list of columns that can be used from behat
* to look up context by reference.
*
* @return array list of column names from instance table
*/
protected static function get_behat_reference_columns(): array {
return ['username'];
}
/**
* Returns array of relevant context capability records.
*
* @param string $sort
* @return array
*/
public function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT) {
global $DB;
$extracaps = array('moodle/grade:viewall');
list($extra, $params) = $DB->get_in_or_equal($extracaps, SQL_PARAMS_NAMED, 'cap');
return $DB->get_records_select('capabilities', "contextlevel = :level OR name {$extra}",
$params + ['level' => self::LEVEL], $sort);
}
/**
* Returns user context instance.
*
* @param int $userid id from {user} table
* @param int $strictness
* @return user|false context instance
*/
public static function instance($userid, $strictness = MUST_EXIST) {
global $DB;
if ($context = context::cache_get(self::LEVEL, $userid)) {
return $context;
}
if (!$record = $DB->get_record('context', array('contextlevel' => self::LEVEL, 'instanceid' => $userid))) {
if ($user = $DB->get_record('user', array('id' => $userid, 'deleted' => 0), 'id', $strictness)) {
$record = context::insert_context_record(self::LEVEL, $user->id, '/'.SYSCONTEXTID, 0);
}
}
if ($record) {
$context = new user($record);
context::cache_add($context);
return $context;
}
return false;
}
/**
* Create missing context instances at user context level
*/
protected static function create_level_instances() {
global $DB;
$sql = "SELECT " . self::LEVEL . ", u.id
FROM {user} u
WHERE u.deleted = 0
AND NOT EXISTS (SELECT 'x'
FROM {context} cx
WHERE u.id = cx.instanceid AND cx.contextlevel=" . self::LEVEL . ")";
$contextdata = $DB->get_recordset_sql($sql);
foreach ($contextdata as $context) {
context::insert_context_record(self::LEVEL, $context->id, null);
}
$contextdata->close();
}
/**
* Returns sql necessary for purging of stale context instances.
*
* @return string cleanup SQL
*/
protected static function get_cleanup_sql() {
$sql = "
SELECT c.*
FROM {context} c
LEFT OUTER JOIN {user} u ON (c.instanceid = u.id AND u.deleted = 0)
WHERE u.id IS NULL AND c.contextlevel = " . self::LEVEL . "
";
return $sql;
}
/**
* Rebuild context paths and depths at user context level.
*
* @param bool $force
*/
protected static function build_paths($force) {
global $DB;
// First update normal users.
$path = $DB->sql_concat('?', 'id');
$pathstart = '/' . SYSCONTEXTID . '/';
$params = array($pathstart);
if ($force) {
$where = "depth <> 2 OR path IS NULL OR path <> ({$path})";
$params[] = $pathstart;
} else {
$where = "depth = 0 OR path IS NULL";
}
$sql = "UPDATE {context}
SET depth = 2,
path = {$path}
WHERE contextlevel = " . self::LEVEL . "
AND ($where)";
$DB->execute($sql, $params);
}
}

Some files were not shown because too many files have changed in this diff Show More