MDL-67898 check: Add check admin setting

This admin setting allows you to display a check anywhere in the admin
tree. It uses a webservice to execute the check, so the impact on the
admin tree performance is as low as possible.

Checks do not necessarily need to be registered in the plugins callback
to be shown here, allowing customisation of what is shown in the
settings versus the reports.
This commit is contained in:
Matthew Hilton 2023-09-29 09:32:29 +10:00
parent bc4df2af71
commit 9b8acd44d1
No known key found for this signature in database
GPG Key ID: DEE897B8DA89460B
13 changed files with 721 additions and 1 deletions

View File

@ -1063,6 +1063,7 @@ $string['changepassword'] = 'Change password';
$string['changessaved'] = 'Changes saved';
$string['check'] = 'Check';
$string['checkactual'] = 'Actual';
$string['checkerror'] = 'Error getting result of check \'{$a}\'. Check the browser console for more information.';
$string['checkexpected'] = 'Expected';
$string['checks'] = 'Checks';
$string['checksok'] = 'All \'{$a}\' checks OK';
@ -1073,6 +1074,7 @@ $string['checkingforbbexport'] = 'Checking for BlackBoard export';
$string['checkinginstances'] = 'Checking instances';
$string['checkingsections'] = 'Checking sections';
$string['checklanguage'] = 'Check language';
$string['checkloading'] = 'Getting the result of check \'{$a}\'';
$string['checknone'] = 'Check none';
$string['childcoursenotfound'] = 'Child course not found!';
$string['childcourses'] = 'Child courses';

View File

@ -11679,3 +11679,100 @@ class admin_settings_h5plib_handler_select extends admin_setting_configselect {
return true;
}
}
/**
* Displays the result of a check via ajax.
*
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @author Matthew Hilton <matthewhilton@catalyst-au.net>
* @copyright Catalyst IT, 2023
*/
class admin_setting_check extends admin_setting {
/** @var \core\check\check $check the check to use **/
private $check;
/** @var bool $includedetails if the details of result are included. **/
private $includedetails;
/**
* Creates check setting.
*
* @param string $name name of setting
* @param \core\check\check $check The check linked to this setting.
* @param bool $includedetails if the details of the result are included
*/
public function __construct(string $name, \core\check\check $check, bool $includedetails = false) {
$this->check = $check;
$this->includedetails = $includedetails;
$heading = $check->get_name();
parent::__construct($name, $heading, '', '');
}
/**
* Returns the check linked to this setting.
*
* @return \core\check\check
*/
public function get_check() {
return $this->check;
}
/**
* Returns setting (unused)
*
* @return true
*/
public function get_setting() {
return true;
}
/**
* Writes the setting (unused)
*
* @param mixed $data
*/
public function write_setting($data) {
return '';
}
/**
* Outputs the admin setting HTML to be rendered.
*
* @param mixed $data
* @param string $query
* @return string html
*/
public function output_html($data, $query = '') {
global $PAGE, $OUTPUT;
$domref = uniqid($this->check->get_ref());
// The actual result is obtained via ajax,
// since its likely somewhat slow to obtain.
$context = [
'domselector' => '[data-check-reference="' . $domref . '"]',
'admintreeid' => $this->get_id(),
'settingname' => $this->name,
'includedetails' => $this->includedetails,
];
$PAGE->requires->js_call_amd('core/check/check_result', 'getAndRender', $context);
// Render a generic loading icon while waiting for ajax.
$loadingstr = get_string('checkloading', '', $this->check->get_name());
$loadingicon = $OUTPUT->pix_icon('i/loading', $loadingstr);
// Wrap it in a notification so we reduce style changes when loading is finished.
$output = $OUTPUT->notification($loadingicon . $loadingstr, \core\output\notification::NOTIFY_INFO, false);
// Add the action link.
$output .= $OUTPUT->render($this->check->get_action_link());
// Wrap in a div with a reference. The JS getAndRender will replace this with the response from the webservice.
$statusdiv = \html_writer::div($output, '', ['data-check-reference' => $domref]);
return format_admin_setting($this, $this->visiblename, '', $statusdiv);
}
}

12
lib/amd/build/check/check_result.min.js vendored Normal file
View File

@ -0,0 +1,12 @@
define("core/check/check_result",["exports","./repository","core/str","core/templates"],(function(_exports,_repository,_str,Templates){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.getAndRender=
/**
* Check API result functions
*
* @module core/check
* @author Matthew Hilton <matthewhilton@catalyst-au.net>
* @copyright Catalyst IT, 2023
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
async function(domSelector,adminTreeId,settingName,includeDetails){const element=document.querySelector(domSelector);if(!element)return void window.console.error("Check selector not found");try{const result=await(0,_repository.getCheckResult)(adminTreeId,settingName,includeDetails),decoded=(new DOMParser).parseFromString(result.html,"text/html").documentElement.textContent;element.innerHTML=decoded}catch(e){window.console.error(e),element.innerHTML=await Templates.render("core/notification",{iserror:!0,closebutton:!1,announce:0,extraclasses:"",message:await(0,_str.getString)("checkerror","core",adminTreeId)})}},Templates=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Templates)}));
//# sourceMappingURL=check_result.min.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"check_result.min.js","sources":["../../src/check/check_result.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Check API result functions\n *\n * @module core/check\n * @author Matthew Hilton <matthewhilton@catalyst-au.net>\n * @copyright Catalyst IT, 2023\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getCheckResult} from './repository';\nimport {getString} from 'core/str';\nimport * as Templates from 'core/templates';\n\n/**\n * Get the result of a check and replace a given DOM element with the result.\n *\n * @method getAndRender\n * @param {String} domSelector A CSS selector for a dom element to replace the the HTML for.\n * @param {String} adminTreeId Id of the admin_setting that called this webservice. Used to retrieve the check registered to it.\n * @param {String} settingName Name of setting (used to find the parent node in the admin tree)\n * @param {Boolean} includeDetails If true, details will be included in the check.\n * By default only the status and the summary is returned.\n */\nexport async function getAndRender(domSelector, adminTreeId, settingName, includeDetails) {\n const element = document.querySelector(domSelector);\n\n if (!element) {\n window.console.error('Check selector not found');\n return;\n }\n\n try {\n const result = await getCheckResult(adminTreeId, settingName, includeDetails);\n const decoded = new DOMParser().parseFromString(result.html, \"text/html\").documentElement.textContent;\n element.innerHTML = decoded;\n } catch (e) {\n window.console.error(e);\n\n // Render error as a red notification.\n element.innerHTML = await Templates.render('core/notification', {\n iserror: true,\n closebutton: false,\n announce: 0,\n extraclasses: '',\n message: await getString('checkerror', 'core', adminTreeId)\n });\n }\n}\n"],"names":["domSelector","adminTreeId","settingName","includeDetails","element","document","querySelector","window","console","error","result","decoded","DOMParser","parseFromString","html","documentElement","textContent","innerHTML","e","Templates","render","iserror","closebutton","announce","extraclasses","message"],"mappings":";;;;;;;;;eAsCmCA,YAAaC,YAAaC,YAAaC,sBAChEC,QAAUC,SAASC,cAAcN,iBAElCI,oBACDG,OAAOC,QAAQC,MAAM,sCAKfC,aAAe,8BAAeT,YAAaC,YAAaC,gBACxDQ,SAAU,IAAIC,WAAYC,gBAAgBH,OAAOI,KAAM,aAAaC,gBAAgBC,YAC1FZ,QAAQa,UAAYN,QACtB,MAAOO,GACLX,OAAOC,QAAQC,MAAMS,GAGrBd,QAAQa,gBAAkBE,UAAUC,OAAO,oBAAqB,CAC5DC,SAAS,EACTC,aAAa,EACbC,SAAU,EACVC,aAAc,GACdC,cAAe,kBAAU,aAAc,OAAQxB"}

3
lib/amd/build/check/repository.min.js vendored Normal file
View File

@ -0,0 +1,3 @@
define("core/check/repository",["exports","core/ajax"],(function(_exports,_ajax){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.getCheckResult=void 0;_exports.getCheckResult=(adminTreeId,settingName,includeDetails)=>(0,_ajax.call)([{methodname:"core_check_get_result_admintree",args:{admintreeid:adminTreeId,settingname:settingName,includedetails:includeDetails}}])[0]}));
//# sourceMappingURL=repository.min.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"repository.min.js","sources":["../../src/check/repository.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Check API webservice repository\n *\n * @module core/check\n * @author Matthew Hilton <matthewhilton@catalyst-au.net>\n * @copyright Catalyst IT, 2023\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {call as fetchMany} from 'core/ajax';\n\n/**\n * Call check_get_result webservice function\n *\n * @param {String} adminTreeId Id of the admin_setting that called this webservice. Used to retrieve the check registered to it.\n * @param {String} settingName Setting name (used to find it's parent)\n * @param {Boolean} includeDetails If details should be included in the response\n */\nexport const getCheckResult = (adminTreeId, settingName, includeDetails) => fetchMany([{\n methodname: 'core_check_get_result_admintree',\n args: {\n admintreeid: adminTreeId,\n settingname: settingName,\n includedetails: includeDetails,\n },\n}])[0];\n\n"],"names":["adminTreeId","settingName","includeDetails","methodname","args","admintreeid","settingname","includedetails"],"mappings":"gMAiC8B,CAACA,YAAaC,YAAaC,kBAAmB,cAAU,CAAC,CACnFC,WAAY,kCACZC,KAAM,CACFC,YAAaL,YACbM,YAAaL,YACbM,eAAgBL,mBAEpB"}

View File

@ -0,0 +1,63 @@
// 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 result functions
*
* @module core/check
* @author Matthew Hilton <matthewhilton@catalyst-au.net>
* @copyright Catalyst IT, 2023
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {getCheckResult} from './repository';
import {getString} from 'core/str';
import * as Templates from 'core/templates';
/**
* Get the result of a check and replace a given DOM element with the result.
*
* @method getAndRender
* @param {String} domSelector A CSS selector for a dom element to replace the the HTML for.
* @param {String} adminTreeId Id of the admin_setting that called this webservice. Used to retrieve the check registered to it.
* @param {String} settingName Name of setting (used to find the parent node in the admin tree)
* @param {Boolean} includeDetails If true, details will be included in the check.
* By default only the status and the summary is returned.
*/
export async function getAndRender(domSelector, adminTreeId, settingName, includeDetails) {
const element = document.querySelector(domSelector);
if (!element) {
window.console.error('Check selector not found');
return;
}
try {
const result = await getCheckResult(adminTreeId, settingName, includeDetails);
const decoded = new DOMParser().parseFromString(result.html, "text/html").documentElement.textContent;
element.innerHTML = decoded;
} catch (e) {
window.console.error(e);
// Render error as a red notification.
element.innerHTML = await Templates.render('core/notification', {
iserror: true,
closebutton: false,
announce: 0,
extraclasses: '',
message: await getString('checkerror', 'core', adminTreeId)
});
}
}

View File

@ -0,0 +1,42 @@
// 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 webservice repository
*
* @module core/check
* @author Matthew Hilton <matthewhilton@catalyst-au.net>
* @copyright Catalyst IT, 2023
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {call as fetchMany} from 'core/ajax';
/**
* Call check_get_result webservice function
*
* @param {String} adminTreeId Id of the admin_setting that called this webservice. Used to retrieve the check registered to it.
* @param {String} settingName Setting name (used to find it's parent)
* @param {Boolean} includeDetails If details should be included in the response
*/
export const getCheckResult = (adminTreeId, settingName, includeDetails) => fetchMany([{
methodname: 'core_check_get_result_admintree',
args: {
admintreeid: adminTreeId,
settingname: settingName,
includedetails: includeDetails,
},
}])[0];

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),
]);
}
}

View File

@ -306,6 +306,13 @@ $functions = array(
'type' => 'write',
'ajax' => true
],
'core_check_get_result_admintree' => [
'classname' => 'core\check\external\get_result_admintree',
'description' => 'Executes a check stored in the admin tree and returns the result',
'type' => 'read',
'ajax' => true,
'readonlysession' => true,
],
'core_cohort_add_cohort_members' => array(
'classname' => 'core_cohort_external',
'methodname' => 'add_cohort_members',

View File

@ -1836,6 +1836,59 @@ class core_renderer extends renderer_base {
return $this->render_from_template('core/action_menu', $context);
}
/**
* Renders a full check API result including summary and details
*
* @param core\check\check $check the check that was run to get details from
* @param core\check\result $result the result of a check
* @param bool $includedetails if true, details are included as well
* @return string rendered html
*/
protected function render_check_full_result(core\check\check $check, core\check\result $result, bool $includedetails): string {
// Initially render just badge itself.
$renderedresult = $this->render_from_template($result->get_template_name(), $result->export_for_template($this));
// Add summary.
$renderedresult .= ' ' . $result->get_summary();
// Wrap in notificaiton.
$notificationmap = [
\core\check\result::NA => \core\output\notification::NOTIFY_INFO,
\core\check\result::OK => \core\output\notification::NOTIFY_SUCCESS,
\core\check\result::INFO => \core\output\notification::NOTIFY_INFO,
\core\check\result::UNKNOWN => \core\output\notification::NOTIFY_WARNING,
\core\check\result::WARNING => \core\output\notification::NOTIFY_WARNING,
\core\check\result::ERROR => \core\output\notification::NOTIFY_ERROR,
\core\check\result::CRITICAL => \core\output\notification::NOTIFY_ERROR,
];
// Get type, or default to error.
$notificationtype = $notificationmap[$result->get_status()] ?? \core\output\notification::NOTIFY_ERROR;
$renderedresult = $this->notification($renderedresult, $notificationtype, false);
// If adding details, add on new line.
if ($includedetails) {
$renderedresult .= $result->get_details();
}
// Add the action link.
$renderedresult .= $this->render_action_link($check->get_action_link());
return $renderedresult;
}
/**
* Renders a full check API result including summary and details
*
* @param core\check\check $check the check that was run to get details from
* @param core\check\result $result the result of a check
* @param bool $includedetails if details should be included
* @return string HTML fragment
*/
public function check_full_result(core\check\check $check, core\check\result $result, bool $includedetails = false) {
return $this->render_check_full_result($check, $result, $includedetails);
}
/**
* Renders a Check API result
*

View File

@ -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/>.
namespace core\external\check;
defined('MOODLE_INTERNAL') || die();
use admin_category;
use admin_root;
use admin_setting_check;
use admin_settingpage;
use core\check\result;
use externallib_advanced_testcase;
use required_capability_exception;
use context_system;
use core\check\access\guestrole;
use core\check\check;
use core\check\external\get_result_admintree;
use core\check\security\passwordpolicy;
use ReflectionMethod;
global $CFG;
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
require_once($CFG->libdir . '/adminlib.php');
/**
* Unit tests check API get_result webservice
*
* @package core
* @covers \core\check\external\get_result_admintree
* @author Matthew Hilton <matthewhilton@catalyst-au.net>
* @copyright Catalyst IT, 2023
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class get_result_admintree_test extends externallib_advanced_testcase {
/**
* Sets up admin tree for the given settings.
*
* @param array $settings array of admin_settings. Each will be placed into a single category, but each on their own page.
* @return admin_root admin root that was created.
*/
private function setup_admin_tree(array $settings) {
$root = new admin_root(true);
$category = new admin_category('testcategory', 'testcategory');
$root->add('root', $category);
foreach ($settings as $i => $setting) {
$page = new admin_settingpage('testpage_' . $i, 'testpage');
$page->add($setting);
$root->add('testcategory', $page);
}
return $root;
}
/**
* Provides values to execute_test
*
* @return array
*/
public static function execute_options_provider() : array {
return [
'get check result (ok, no details)' => [
'triggererror' => false,
'check' => new passwordpolicy(),
'includedetails' => false,
'expectedreturn' => [
'status' => result::OK,
'summary' => get_string('check_passwordpolicy_ok', 'report_security'),
// Note for details and html, null = not returned and anything else will simply check something was returned.
'details' => null,
'html' => '',
],
],
'get check result (ok, with details)' => [
'triggererror' => false,
'check' => new passwordpolicy(),
'includedetails' => true,
'expectedreturn' => [
'status' => result::OK,
'summary' => get_string('check_passwordpolicy_ok', 'report_security'),
// Note for details and html, null = not returned and anything else will simply check something was returned.
'details' => '',
'html' => '',
],
],
'get check result (error, no details)' => [
'triggererror' => true,
'check' => new passwordpolicy(),
'includedetails' => false,
'expectedreturn' => [
'status' => result::WARNING,
'summary' => get_string('check_passwordpolicy_error', 'report_security'),
// Note for details and html, null = not returned and anything else will simply check something was returned.
'details' => null,
'html' => '',
],
],
'get check result (error, with details)' => [
'triggererror' => true,
'check' => new passwordpolicy(),
'includedetails' => true,
'expectedreturn' => [
'status' => result::WARNING,
'summary' => get_string('check_passwordpolicy_error', 'report_security'),
// Note for details and html, null = not returned and anything else will simply check something was returned.
'details' => '',
'html' => '',
],
],
];
}
/**
* Tests the execute function
*
* @param bool $triggererror If the test should setup the conditions so that the check will fail
* @param check $check Check to use
* @param bool $includedetails if details are included
* @param array $expectedreturn an array of key value pairs. For each key, if the value is null it expects the
* webservice to not return it. If it has a value, it checks that that value was inside what was returned from the webservice.
* @dataProvider execute_options_provider
*/
public function test_execute_options(bool $triggererror, check $check, bool $includedetails, array $expectedreturn): void {
global $CFG;
$this->resetAfterTest(true);
$this->setAdminUser();
// This makes the check we test (password policy) warn or be OK depending on the test.
$CFG->passwordpolicy = $triggererror ? false : true;
// Add the admin setting.
$checksetting = new admin_setting_check('core/testcheck', $check);
$root = $this->setup_admin_tree([$checksetting]);
// Execute the ws function.
$this->setAdminUser();
$wsresult = (object) get_result_admintree::execute($checksetting->get_id(), $checksetting->name, $includedetails, $root);
foreach ($expectedreturn as $key => $expectedvalue) {
// If the expected result is null, ensure the return value was also null.
if (is_null($expectedvalue)) {
$this->assertTrue(empty($wsresult->$key));
}
// If the expected result is set, ensure it is contained in the return value.
if (!is_null($expectedvalue)) {
$this->assertTrue(!empty($wsresult->$key));
$this->assertStringContainsString($expectedvalue, $wsresult->$key);
}
}
}
/**
* Provides values to test_find_check_from_settings_tree
*
* @return array
*/
public static function find_check_from_setting_tree_provider(): array {
$testsetting1 = new admin_setting_check('testsetting', new passwordpolicy());
return [
'setting is in tree, correctly linked' => [
'settings' => [
$testsetting1,
new admin_setting_check('testsetting2', new guestrole()),
new admin_setting_check('testsetting3', new guestrole()),
],
'searchname' => $testsetting1->name,
'searchid' => $testsetting1->get_id(),
'expectedcheck' => passwordpolicy::class,
],
'setting is not in tree' => [
'settings' => [],
'searchname' => $testsetting1->name,
'searchid' => $testsetting1->get_id(),
'expectedcheck' => '',
],
'setting in tree, but name has conflict' => [
'settings' => [
$testsetting1,
new admin_setting_check('testsetting', new guestrole()),
],
'searchname' => $testsetting1->name,
'searchid' => $testsetting1->get_id(),
// Because the two settings have the same name + id, its impossible to tell them apart.
// So the check should not be returned.
'expectedcheck' => '',
],
];
}
/**
* Tests finding the check using the admin tree in various situations.
*
* @param array $settings array of admin_settings to setup
* @param string $searchname name of setting to search for
* @param string $searchid id of setting to search for
* @param string $expectedcheck class name of expected check to be found. If empty, expects that none was found.
* @dataProvider find_check_from_setting_tree_provider
*/
public function test_find_check_from_setting_tree(array $settings, string $searchname, string $searchid,
string $expectedcheck): void {
$this->resetAfterTest(true);
$this->setAdminUser();
$root = $this->setup_admin_tree($settings);
$method = new ReflectionMethod(get_result_admintree::class, 'get_check_from_setting');
$method->setAccessible(true);
$result = $method->invoke(new get_result_admintree(), $searchid, $searchname, $root);
if (!empty($expectedcheck)) {
$this->assertInstanceOf($expectedcheck, $result);
} else {
$this->assertEmpty($result);
}
}
/**
* Provides values to test_capability_check
*
* @return array
*/
public static function capability_check_provider(): array {
return [
'has permission' => [
'permission' => CAP_ALLOW,
'expectedexception' => null,
],
'does not have permission' => [
'permission' => CAP_PROHIBIT,
'expectedexception' => required_capability_exception::class,
],
];
}
/**
* Tests that capabilites are being checked correctly by the webservice.
*
* @param int $permission the permission level to assign the capability to the role for.
* @param string|null $expectedexception Exception class expected, or null if none is expected.
* @dataProvider capability_check_provider
*/
public function test_capability_check($permission, $expectedexception): void {
$this->resetAfterTest(true);
$user = $this->getDataGenerator()->create_user();
$role = $this->getDataGenerator()->create_role();
role_assign($role, $user->id, context_system::instance()->id);
role_change_permission($role, context_system::instance(), 'moodle/site:config', $permission);
if (!empty($expectedexception)) {
$this->expectException($expectedexception);
}
// Setup check setting as admin.
$this->setAdminUser();
$checksetting = new admin_setting_check('core/testcheck', new passwordpolicy());
$root = $this->setup_admin_tree([$checksetting]);
$this->setUser($user);
get_result_admintree::execute($checksetting->get_id(), $checksetting->name, false, $root);
}
}

View File

@ -29,7 +29,7 @@
defined('MOODLE_INTERNAL') || die();
$version = 2023111600.00; // YYYYMMDD = weekly release date of this DEV branch.
$version = 2023111600.01; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
$release = '4.4dev (Build: 20231116)'; // Human-friendly version name