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
+586
View File
@@ -0,0 +1,586 @@
<?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;
use coding_exception;
use context;
use context_helper;
use context_system;
use core_component;
use core_php_time_limit;
use invalid_parameter_exception;
use invalid_response_exception;
use moodle_exception;
/**
* Base class for external api methods.
*
* @package core_webservice
* @copyright 2009 Petr Skodak
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since Moodle 2.0
*/
class external_api {
/** @var \stdClass context where the function calls will be restricted */
private static $contextrestriction;
/**
* Returns detailed function information
*
* @param string|\stdClass $function name of external function or record from external_function
* @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found;
* MUST_EXIST means throw exception if no record or multiple records found
* @return \stdClass|bool description or false if not found or exception thrown
* @throws coding_exception for any property and/or method that is missing or invalid
* @since Moodle 2.0
*/
public static function external_function_info($function, $strictness = MUST_EXIST) {
global $DB, $CFG;
if (!is_object($function)) {
if (!$function = $DB->get_record('external_functions', ['name' => $function], '*', $strictness)) {
return false;
}
}
// First try class autoloading.
if (!class_exists($function->classname)) {
// Fallback to explicit include of externallib.php.
if (empty($function->classpath)) {
$function->classpath = core_component::get_component_directory($function->component) . '/externallib.php';
} else {
$function->classpath = "{$CFG->dirroot}/{$function->classpath}";
}
if (!file_exists($function->classpath)) {
throw new coding_exception(
"Cannot find file {$function->classpath} with external function implementation " .
"for {$function->classname}::{$function->methodname}"
);
}
require_once($function->classpath);
if (!class_exists($function->classname)) {
throw new coding_exception("Cannot find external class {$function->classname}");
}
}
$function->ajax_method = "{$function->methodname}_is_allowed_from_ajax";
$function->parameters_method = "{$function->methodname}_parameters";
$function->returns_method = "{$function->methodname}_returns";
$function->deprecated_method = "{$function->methodname}_is_deprecated";
// Make sure the implementaion class is ok.
if (!method_exists($function->classname, $function->methodname)) {
throw new coding_exception(
"Missing implementation method {$function->classname}::{$function->methodname}"
);
}
if (!method_exists($function->classname, $function->parameters_method)) {
throw new coding_exception(
"Missing parameters description method {$function->classname}::{$function->parameters_method}"
);
}
if (!method_exists($function->classname, $function->returns_method)) {
throw new coding_exception(
"Missing returned values description method {$function->classname}::{$function->returns_method}"
);
}
if (method_exists($function->classname, $function->deprecated_method)) {
if (call_user_func([$function->classname, $function->deprecated_method]) === true) {
$function->deprecated = true;
}
}
$function->allowed_from_ajax = false;
// Fetch the parameters description.
$function->parameters_desc = call_user_func([$function->classname, $function->parameters_method]);
if (!($function->parameters_desc instanceof external_function_parameters)) {
throw new coding_exception(
"{$function->classname}::{$function->parameters_method} did not return a valid external_function_parameters object"
);
}
// Fetch the return values description.
$function->returns_desc = call_user_func([$function->classname, $function->returns_method]);
// Null means void result or result is ignored.
if (!is_null($function->returns_desc) && !($function->returns_desc instanceof external_description)) {
throw new coding_exception(
"{$function->classname}::{$function->returns_method} did not return a valid external_description object"
);
}
// Now get the function description.
// TODO MDL-31115 use localised lang pack descriptions, it would be nice to have
// easy to understand descriptions in admin UI,
// on the other hand this is still a bit in a flux and we need to find some new naming
// conventions for these descriptions in lang packs.
$function->description = null;
$servicesfile = core_component::get_component_directory($function->component) . '/db/services.php';
if (file_exists($servicesfile)) {
$functions = null;
include($servicesfile);
if (isset($functions[$function->name]['description'])) {
$function->description = $functions[$function->name]['description'];
}
if (isset($functions[$function->name]['testclientpath'])) {
$function->testclientpath = $functions[$function->name]['testclientpath'];
}
if (isset($functions[$function->name]['type'])) {
$function->type = $functions[$function->name]['type'];
}
if (isset($functions[$function->name]['ajax'])) {
$function->allowed_from_ajax = $functions[$function->name]['ajax'];
} else if (method_exists($function->classname, $function->ajax_method)) {
if (call_user_func([$function->classname, $function->ajax_method]) === true) {
debugging('External function ' . $function->ajax_method . '() function is deprecated.' .
'Set ajax=>true in db/services.php instead.', DEBUG_DEVELOPER);
$function->allowed_from_ajax = true;
}
}
if (isset($functions[$function->name]['loginrequired'])) {
$function->loginrequired = $functions[$function->name]['loginrequired'];
} else {
$function->loginrequired = true;
}
if (isset($functions[$function->name]['readonlysession'])) {
$function->readonlysession = $functions[$function->name]['readonlysession'];
} else {
$function->readonlysession = false;
}
}
return $function;
}
/**
* Call an external function validating all params/returns correctly.
*
* Note that an external function may modify the state of the current page, so this wrapper
* saves and restores tha PAGE and COURSE global variables before/after calling the external function.
*
* @param string $function A webservice function name.
* @param array $args Params array (named params)
* @param boolean $ajaxonly If true, an extra check will be peformed to see if ajax is required.
* @return array containing keys for error (bool), exception and data.
*/
public static function call_external_function($function, $args, $ajaxonly = false) {
global $PAGE, $COURSE, $CFG, $SITE;
require_once("{$CFG->libdir}/pagelib.php");
$externalfunctioninfo = static::external_function_info($function);
// Eventually this should shift into the various handlers and not be handled via config.
$readonlysession = $externalfunctioninfo->readonlysession ?? false;
if (!$readonlysession || empty($CFG->enable_read_only_sessions)) {
\core\session\manager::restart_with_write_lock($readonlysession);
}
$currentpage = $PAGE;
$currentcourse = $COURSE;
$response = [];
try {
// Taken straight from from setup.php.
if (!empty($CFG->moodlepageclass)) {
if (!empty($CFG->moodlepageclassfile)) {
require_once($CFG->moodlepageclassfile);
}
$classname = $CFG->moodlepageclass;
} else {
$classname = 'moodle_page';
}
$PAGE = new $classname();
$COURSE = clone($SITE);
if ($ajaxonly && !$externalfunctioninfo->allowed_from_ajax) {
throw new moodle_exception('servicenotavailable', 'webservice');
}
// Do not allow access to write or delete webservices as a public user.
if ($externalfunctioninfo->loginrequired && !WS_SERVER) {
if (defined('NO_MOODLE_COOKIES') && NO_MOODLE_COOKIES && !PHPUNIT_TEST) {
throw new moodle_exception('servicerequireslogin', 'webservice');
}
if (!isloggedin()) {
throw new moodle_exception('servicerequireslogin', 'webservice');
} else {
require_sesskey();
}
}
// Validate params, this also sorts the params properly, we need the correct order in the next part.
$callable = [$externalfunctioninfo->classname, 'validate_parameters'];
$params = call_user_func(
$callable,
$externalfunctioninfo->parameters_desc,
$args
);
$params = array_values($params);
// Allow any Moodle plugin a chance to override this call. This is a convenient spot to
// make arbitrary behaviour customisations. The overriding plugin could call the 'real'
// function first and then modify the results, or it could do a completely separate
// thing.
$callbacks = get_plugins_with_function('override_webservice_execution');
$result = false;
foreach (array_values($callbacks) as $plugins) {
foreach (array_values($plugins) as $callback) {
$result = $callback($externalfunctioninfo, $params);
if ($result !== false) {
break 2;
}
}
}
// If the function was not overridden, call the real one.
if ($result === false) {
$callable = [$externalfunctioninfo->classname, $externalfunctioninfo->methodname];
$result = call_user_func_array($callable, $params);
}
// Validate the return parameters.
if ($externalfunctioninfo->returns_desc !== null) {
$callable = [$externalfunctioninfo->classname, 'clean_returnvalue'];
$result = call_user_func($callable, $externalfunctioninfo->returns_desc, $result);
}
$response['error'] = false;
$response['data'] = $result;
} catch (\Throwable $e) {
$exception = get_exception_info($e);
unset($exception->a);
$exception->backtrace = format_backtrace($exception->backtrace, true);
if (!debugging('', DEBUG_DEVELOPER)) {
unset($exception->debuginfo);
unset($exception->backtrace);
}
$response['error'] = true;
$response['exception'] = $exception;
// Do not process the remaining requests.
}
$PAGE = $currentpage;
$COURSE = $currentcourse;
return $response;
}
/**
* Set context restriction for all following subsequent function calls.
*
* @param \stdClass $context the context restriction
* @since Moodle 2.0
*/
public static function set_context_restriction($context) {
self::$contextrestriction = $context;
}
/**
* This method has to be called before every operation
* that takes a longer time to finish!
*
* @param int $seconds max expected time the next operation needs
* @since Moodle 2.0
*/
public static function set_timeout($seconds = 360) {
$seconds = ($seconds < 300) ? 300 : $seconds;
core_php_time_limit::raise($seconds);
}
/**
* Validates submitted function parameters, if anything is incorrect
* invalid_parameter_exception is thrown.
* This is a simple recursive method which is intended to be called from
* each implementation method of external API.
*
* @param external_description $description description of parameters
* @param mixed $params the actual parameters
* @return mixed params with added defaults for optional items, invalid_parameters_exception thrown if any problem found
* @since Moodle 2.0
*/
public static function validate_parameters(external_description $description, $params) {
if ($params === null && $description->allownull == NULL_ALLOWED) {
return null;
}
if ($description instanceof external_value) {
if (is_array($params) || is_object($params)) {
throw new invalid_parameter_exception('Scalar type expected, array or object received.');
}
if ($description->type == PARAM_BOOL) {
// Special case for PARAM_BOOL - we want true/false instead of the usual 1/0 - we can not be too strict here.
if (is_bool($params) || $params === 0 || $params === 1 || $params === '0' || $params === '1') {
return (bool) $params;
}
}
$debuginfo = "Invalid external api parameter: the value is \"{$params}\", ";
$debuginfo .= "the server was expecting \"{$description->type}\" type";
return validate_param($params, $description->type, $description->allownull, $debuginfo);
} else if ($description instanceof external_single_structure) {
if (!is_array($params)) {
throw new invalid_parameter_exception(
// phpcs:ignore moodle.PHP.ForbiddenFunctions.Found
"Only arrays accepted. The bad value is: '" . print_r($params, true) . "'"
);
}
$result = [];
foreach ($description->keys as $key => $subdesc) {
if (!array_key_exists($key, $params)) {
if ($subdesc->required == VALUE_REQUIRED) {
throw new invalid_parameter_exception("Missing required key in single structure: {$key}");
}
if ($subdesc->required == VALUE_DEFAULT) {
try {
$result[$key] = static::validate_parameters($subdesc, $subdesc->default);
} catch (invalid_parameter_exception $e) {
// We are only interested by exceptions returned by validate_param() and validate_parameters().
// This is used to build the path to the faulty attribute.
throw new invalid_parameter_exception("{$key} => " . $e->getMessage() . ': ' . $e->debuginfo);
}
}
} else {
try {
$result[$key] = static::validate_parameters($subdesc, $params[$key]);
} catch (invalid_parameter_exception $e) {
// We are only interested by exceptions returned by validate_param() and validate_parameters().
// This is used to build the path to the faulty attribute.
throw new invalid_parameter_exception($key . " => " . $e->getMessage() . ': ' . $e->debuginfo);
}
}
unset($params[$key]);
}
if (!empty($params)) {
throw new invalid_parameter_exception(
'Unexpected keys (' . implode(', ', array_keys($params)) . ') detected in parameter array.'
);
}
return $result;
} else if ($description instanceof external_multiple_structure) {
if (!is_array($params)) {
throw new invalid_parameter_exception(
'Only arrays accepted. The bad value is: \'' .
// phpcs:ignore moodle.PHP.ForbiddenFunctions.Found
print_r($params, true) .
"'"
);
}
$result = [];
foreach ($params as $param) {
$result[] = static::validate_parameters($description->content, $param);
}
return $result;
} else {
throw new invalid_parameter_exception('Invalid external api description');
}
}
/**
* Clean response
* If a response attribute is unknown from the description, we just ignore the attribute.
* If a response attribute is incorrect, invalid_response_exception is thrown.
* Note: this function is similar to validate parameters, however it is distinct because
* parameters validation must be distinct from cleaning return values.
*
* @param external_description $description description of the return values
* @param mixed $response the actual response
* @return mixed response with added defaults for optional items, invalid_response_exception thrown if any problem found
* @author 2010 Jerome Mouneyrac
* @since Moodle 2.0
*/
public static function clean_returnvalue(external_description $description, $response) {
if ($response === null && $description->allownull == NULL_ALLOWED) {
return null;
}
if ($description instanceof external_value) {
if (is_array($response) || is_object($response)) {
throw new invalid_response_exception('Scalar type expected, array or object received.');
}
if ($description->type == PARAM_BOOL) {
// Special case for PARAM_BOOL - we want true/false instead of the usual 1/0 - we can not be too strict here.
if (is_bool($response) || $response === 0 || $response === 1 || $response === '0' || $response === '1') {
return (bool) $response;
}
}
$responsetype = gettype($response);
$debuginfo = "Invalid external api response: the value is \"{$response}\" of PHP type \"{$responsetype}\", ";
$debuginfo .= "the server was expecting \"{$description->type}\" type";
try {
return validate_param($response, $description->type, $description->allownull, $debuginfo);
} catch (invalid_parameter_exception $e) {
// Proper exception name, to be recursively catched to build the path to the faulty attribute.
throw new invalid_response_exception($e->debuginfo);
}
} else if ($description instanceof external_single_structure) {
if (!is_array($response) && !is_object($response)) {
throw new invalid_response_exception(
// phpcs:ignore moodle.PHP.ForbiddenFunctions.Found
"Only arrays/objects accepted. The bad value is: '" . print_r($response, true) . "'"
);
}
// Cast objects into arrays.
if (is_object($response)) {
$response = (array) $response;
}
$result = [];
foreach ($description->keys as $key => $subdesc) {
if (!array_key_exists($key, $response)) {
if ($subdesc->required == VALUE_REQUIRED) {
throw new invalid_response_exception(
"Error in response - Missing following required key in a single structure: {$key}"
);
}
if ($subdesc instanceof external_value) {
if ($subdesc->required == VALUE_DEFAULT) {
try {
$result[$key] = static::clean_returnvalue($subdesc, $subdesc->default);
} catch (invalid_response_exception $e) {
// Build the path to the faulty attribute.
throw new invalid_response_exception("{$key} => " . $e->getMessage() . ': ' . $e->debuginfo);
}
}
}
} else {
try {
$result[$key] = static::clean_returnvalue($subdesc, $response[$key]);
} catch (invalid_response_exception $e) {
// Build the path to the faulty attribute.
throw new invalid_response_exception("{$key} => " . $e->getMessage() . ': ' . $e->debuginfo);
}
}
unset($response[$key]);
}
return $result;
} else if ($description instanceof external_multiple_structure) {
if (!is_array($response)) {
throw new invalid_response_exception(
// phpcs:ignore moodle.PHP.ForbiddenFunctions.Found
"Only arrays accepted. The bad value is: '" . print_r($response, true) . "'"
);
}
$result = [];
foreach ($response as $param) {
$result[] = static::clean_returnvalue($description->content, $param);
}
return $result;
} else {
throw new invalid_response_exception('Invalid external api response description');
}
}
/**
* Makes sure user may execute functions in this context.
*
* @param context $context
* @since Moodle 2.0
*/
public static function validate_context($context) {
global $PAGE;
if (empty($context)) {
throw new invalid_parameter_exception('Context does not exist');
}
if (empty(self::$contextrestriction)) {
self::$contextrestriction = context_system::instance();
}
$rcontext = self::$contextrestriction;
if ($rcontext->contextlevel == $context->contextlevel) {
if ($rcontext->id != $context->id) {
throw new restricted_context_exception();
}
} else if ($rcontext->contextlevel > $context->contextlevel) {
throw new restricted_context_exception();
} else {
$parents = $context->get_parent_context_ids();
if (!in_array($rcontext->id, $parents)) {
throw new restricted_context_exception();
}
}
$PAGE->reset_theme_and_output();
[, $course, $cm] = get_context_info_array($context->id);
require_login($course, false, $cm, false, true);
$PAGE->set_context($context);
}
/**
* Get context from passed parameters.
* The passed array must either contain a contextid or a combination of context level and instance id to fetch the context.
* For example, the context level can be "course" and instanceid can be courseid.
*
* See context_helper::get_all_levels() for a list of valid numeric context levels,
* legacy short names such as 'system', 'user', 'course' are not supported in new
* plugin capabilities.
*
* @param array $param
* @since Moodle 2.6
* @throws invalid_parameter_exception
* @return context
*/
protected static function get_context_from_params($param) {
if (!empty($param['contextid'])) {
return context::instance_by_id($param['contextid'], IGNORE_MISSING);
} else if (!empty($param['contextlevel']) && isset($param['instanceid'])) {
// Numbers and short names are supported since Moodle 4.2.
$classname = \core\context_helper::parse_external_level($param['contextlevel']);
if (!$classname) {
throw new invalid_parameter_exception('Invalid context level = '.$param['contextlevel']);
}
return $classname::instance($param['instanceid'], IGNORE_MISSING);
} else {
// No valid context info was found.
throw new invalid_parameter_exception(
'Missing parameters, please provide either context level with instance id or contextid'
);
}
}
/**
* Returns a prepared structure to use a context parameters.
* @return external_single_structure
*/
protected static function get_context_parameters() {
$id = new external_value(
PARAM_INT,
'Context ID. Either use this value, or level and instanceid.',
VALUE_DEFAULT,
0
);
$level = new external_value(
PARAM_ALPHANUM, // Since Moodle 4.2 numeric context level values are supported too.
'Context level. To be used with instanceid.',
VALUE_DEFAULT,
''
);
$instanceid = new external_value(
PARAM_INT,
'Context instance ID. To be used with level',
VALUE_DEFAULT,
0
);
return new external_single_structure([
'contextid' => $id,
'contextlevel' => $level,
'instanceid' => $instanceid,
]);
}
}
+61
View File
@@ -0,0 +1,61 @@
<?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;
/**
* Common ancestor of all parameter description classes
*
* @package core_external
* @copyright 2009 Petr Skodak
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class external_description {
/** @var string Description of element */
public $desc;
/** @var bool Element value required, null not allowed */
public $required;
/** @var mixed Default value */
public $default;
/** @var bool Allow null values */
public $allownull;
/**
* Contructor.
*
* @param string $desc Description of element
* @param int $required Whether the element value is required. Valid values are VALUE_DEFAULT, VALUE_REQUIRED, VALUE_OPTIONAL.
* @param mixed $default The default value
* @param bool $allownull Allow null value
*/
public function __construct($desc, $required, $default, $allownull = NULL_NOT_ALLOWED) {
if (!in_array($required, [VALUE_DEFAULT, VALUE_REQUIRED, VALUE_OPTIONAL], true)) {
$requiredstr = $required;
if (is_array($required)) {
$requiredstr = "Array: " . implode(" ", $required);
}
debugging("Invalid \$required parameter value: '{$requiredstr}' .
It must be either VALUE_DEFAULT, VALUE_REQUIRED, or VALUE_OPTIONAL", DEBUG_DEVELOPER);
}
$this->desc = $desc;
$this->required = $required;
$this->default = $default;
$this->allownull = (bool)$allownull;
}
}
+115
View File
@@ -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_external;
/**
* External structure representing a set of files.
*
* @package core_external
* @copyright 2016 Juan Leyva
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class external_files extends external_multiple_structure {
/**
* Constructor
* @param string $desc Description for the multiple structure.
* @param int $required The type of value (VALUE_REQUIRED OR VALUE_OPTIONAL).
*/
public function __construct($desc = 'List of files.', $required = VALUE_REQUIRED) {
parent::__construct(
new external_single_structure([
'filename' => new external_value(PARAM_FILE, 'File name.', VALUE_OPTIONAL),
'filepath' => new external_value(PARAM_PATH, 'File path.', VALUE_OPTIONAL),
'filesize' => new external_value(PARAM_INT, 'File size.', VALUE_OPTIONAL),
'fileurl' => new external_value(PARAM_URL, 'Downloadable file url.', VALUE_OPTIONAL),
'timemodified' => new external_value(PARAM_INT, 'Time modified.', VALUE_OPTIONAL),
'mimetype' => new external_value(PARAM_RAW, 'File mime type.', VALUE_OPTIONAL),
'isexternalfile' => new external_value(PARAM_BOOL, 'Whether is an external file.', VALUE_OPTIONAL),
'repositorytype' => new external_value(PARAM_PLUGIN, 'The repository type for external files.', VALUE_OPTIONAL),
'icon' => new external_value(PARAM_RAW,
'The relative path to the relevant file type icon based on the file\'s mime type.', VALUE_OPTIONAL),
], 'File.'),
$desc,
$required,
);
}
/**
* Return the properties ready to be used by an exporter.
*
* @return array properties
* @since Moodle 3.3
*/
public static function get_properties_for_exporter() {
return [
'filename' => [
'type' => PARAM_FILE,
'description' => 'File name.',
'optional' => true,
'null' => NULL_NOT_ALLOWED,
],
'filepath' => [
'type' => PARAM_PATH,
'description' => 'File path.',
'optional' => true,
'null' => NULL_NOT_ALLOWED,
],
'filesize' => [
'type' => PARAM_INT,
'description' => 'File size.',
'optional' => true,
'null' => NULL_NOT_ALLOWED,
],
'fileurl' => [
'type' => PARAM_URL,
'description' => 'Downloadable file url.',
'optional' => true,
'null' => NULL_NOT_ALLOWED,
],
'timemodified' => [
'type' => PARAM_INT,
'description' => 'Time modified.',
'optional' => true,
'null' => NULL_NOT_ALLOWED,
],
'mimetype' => [
'type' => PARAM_RAW,
'description' => 'File mime type.',
'optional' => true,
'null' => NULL_NOT_ALLOWED,
],
'isexternalfile' => [
'type' => PARAM_BOOL,
'description' => 'Whether is an external file.',
'optional' => true,
'null' => NULL_NOT_ALLOWED,
],
'repositorytype' => [
'type' => PARAM_PLUGIN,
'description' => 'The repository type for the external files.',
'optional' => true,
'null' => NULL_ALLOWED,
],
'icon' => [
'type' => PARAM_RAW,
'description' => 'Relative path to the relevant file type icon based on the file\'s mime type.',
'optional' => true,
'null' => NULL_ALLOWED,
],
];
}
}
+63
View File
@@ -0,0 +1,63 @@
<?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;
/**
* A pre-filled external_value class for text format.
*
* Default is FORMAT_HTML
* This should be used all the time in external xxx_params()/xxx_returns functions
* as it is the standard way to implement text format param/return values.
*
* @package core_webservice
* @copyright 2012 Jerome Mouneyrac
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since Moodle 2.3
*/
class external_format_value extends external_value {
/**
* Constructor
*
* @param string $textfieldname Name of the text field
* @param int $required if VALUE_REQUIRED then set standard default FORMAT_HTML
* @param int $default Default value.
*/
public function __construct($textfieldname, $required = VALUE_REQUIRED, $default = null) {
// Make sure the default format's value is correct.
if ($default !== null && !in_array($default, [FORMAT_MOODLE, FORMAT_HTML, FORMAT_PLAIN, FORMAT_MARKDOWN])) {
debugging("Invalid default format for $textfieldname: $default. " .
"It must be either FORMAT_MOODLE, FORMAT_HTML, FORMAT_PLAIN, or FORMAT_MARKDOWN.", DEBUG_DEVELOPER);
$default = null;
}
if ($default == null && $required == VALUE_DEFAULT) {
$default = FORMAT_HTML;
}
$desc = sprintf(
"%s format (%s = HTML, %s = MOODLE, %s = PLAIN, or %s = MARKDOWN)",
$textfieldname,
FORMAT_HTML,
FORMAT_MOODLE,
FORMAT_PLAIN,
FORMAT_MARKDOWN,
);
parent::__construct(PARAM_INT, $desc, $required, $default);
}
}
+55
View File
@@ -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/>.
namespace core_external;
/**
* Description of top level - PHP function parameters.
*
* @package core_external
* @copyright 2009 Petr Skodak
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class external_function_parameters extends external_single_structure {
/**
* Constructor - does extra checking to prevent top level optional parameters.
*
* @param array $keys
* @param string $desc
* @param int $required
* @param array $default
*/
public function __construct(
array $keys,
$desc = '',
$required = VALUE_REQUIRED,
$default = null
) {
global $CFG;
if ($CFG->debugdeveloper) {
foreach (array_values($keys) as $value) {
if ($value instanceof external_value) {
if ($value->required == VALUE_OPTIONAL) {
debugging('External function parameters: invalid OPTIONAL value specified.', DEBUG_DEVELOPER);
break;
}
}
}
}
parent::__construct($keys, $desc, $required, $default, NULL_NOT_ALLOWED);
}
}
+50
View File
@@ -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/>.
namespace core_external;
/**
* Bulk array description class.
*
* @package core_external
* @copyright 2009 Petr Skodak
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class external_multiple_structure extends external_description {
/** @var external_description content */
public $content;
/**
* Constructor
*
* @param external_description $content
* @param string $desc
* @param int $required
* @param array $default
* @param bool $allownull
*/
public function __construct(
external_description $content,
$desc = '',
$required = VALUE_REQUIRED,
$default = null,
$allownull = NULL_NOT_ALLOWED
) {
parent::__construct($desc, $required, $default, $allownull);
$this->content = $content;
}
}
+190
View File
@@ -0,0 +1,190 @@
<?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;
/**
* Singleton to handle the external settings..
*
* We use singleton to encapsulate the "logic".
*
* @package core_external
* @copyright 2012 Jerome Mouneyrac
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class external_settings {
/** @var settings|null the singleton instance */
public static $instance = null;
/** @var bool Should the external function return raw text or formatted */
private $raw = false;
/** @var bool Should the external function filter the text */
private $filter = false;
/** @var bool Should the external function rewrite plugin file url */
private $fileurl = true;
/** @var string In which file should the urls be rewritten */
private $file = 'webservice/pluginfile.php';
/** @var string The session lang */
private $lang = '';
/** @var string The timezone to use during this WS request */
private $timezone = '';
/**
* Constructor - protected - can not be instanciated
*/
protected function __construct() {
if ((AJAX_SCRIPT == false) && (CLI_SCRIPT == false) && (WS_SERVER == false)) {
// For normal pages, the default should match the default for format_text.
$this->filter = true;
// Use pluginfile.php for web requests.
$this->file = 'pluginfile.php';
}
}
/**
* Return only one instance
*
* @return self
*/
public static function get_instance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Reset the singleton instance.
*/
public static function reset(): void {
self::$instance = null;
}
/**
* Set raw
*
* @param boolean $raw
*/
public function set_raw($raw) {
$this->raw = $raw;
}
/**
* Get raw
*
* @return boolean
*/
public function get_raw() {
return $this->raw;
}
/**
* Set filter
*
* @param boolean $filter
*/
public function set_filter($filter) {
$this->filter = $filter;
}
/**
* Get filter
*
* @return boolean
*/
public function get_filter() {
return $this->filter;
}
/**
* Set fileurl
*
* @param bool $fileurl
*/
public function set_fileurl($fileurl) {
$this->fileurl = $fileurl;
}
/**
* Get fileurl
*
* @return bool
*/
public function get_fileurl() {
return $this->fileurl;
}
/**
* Set file
*
* @param string $file
*/
public function set_file($file) {
$this->file = $file;
}
/**
* Get file
*
* @return string
*/
public function get_file() {
return $this->file;
}
/**
* Set lang
*
* @param string $lang
*/
public function set_lang($lang) {
$this->lang = $lang;
}
/**
* Get lang
*
* @return string
*/
public function get_lang() {
return $this->lang;
}
/**
* Set timezone
*
* @param string $timezone
*/
public function set_timezone($timezone) {
$this->timezone = $timezone;
}
/**
* Get timezone
*
* @return string
*/
public function get_timezone() {
return $this->timezone;
}
}
+50
View File
@@ -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/>.
namespace core_external;
/**
* Associative array description class
*
* @package core_external
* @copyright 2009 Petr Skodak
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class external_single_structure extends external_description {
/** @var array Description of array keys key=>external_description */
public $keys;
/**
* Constructor
*
* @param array $keys
* @param string $desc
* @param int $required
* @param array $default
* @param bool $allownull
*/
public function __construct(
array $keys,
$desc = '',
$required = VALUE_REQUIRED,
$default = null,
$allownull = NULL_NOT_ALLOWED
) {
parent::__construct($desc, $required, $default, $allownull);
$this->keys = $keys;
}
}
+50
View File
@@ -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/>.
namespace core_external;
/**
* Scalar value description class.
*
* @package core_external
* @copyright 2009 Petr Skodak
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class external_value extends external_description {
/** @var mixed Value type PARAM_XX */
public $type;
/**
* Constructor for the external_value class.
*
* @param mixed $type
* @param string $desc
* @param int $required
* @param mixed $default
* @param bool $allownull
*/
public function __construct(
$type,
$desc = '',
$required = VALUE_REQUIRED,
$default = null,
$allownull = NULL_ALLOWED
) {
parent::__construct($desc, $required, $default, $allownull);
$this->type = $type;
}
}
+51
View File
@@ -0,0 +1,51 @@
<?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;
/**
* Standard Moodle web service warnings.
*
* @package core_external
* @copyright 2012 Jerome Mouneyrac
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class external_warnings extends external_multiple_structure {
/**
* Constructor
*
* @param string $itemdesc
* @param string $itemiddesc
* @param string $warningcodedesc
*/
public function __construct(
$itemdesc = 'item',
$itemiddesc = 'item id',
$warningcodedesc = 'the warning code can be used by the client app to implement specific behaviour'
) {
parent::__construct(
new external_single_structure([
'item' => new external_value(PARAM_TEXT, $itemdesc, VALUE_OPTIONAL),
'itemid' => new external_value(PARAM_INT, $itemiddesc, VALUE_OPTIONAL),
'warningcode' => new external_value(PARAM_ALPHANUM, $warningcodedesc),
'message' => new external_value(PARAM_RAW, 'untranslated english message to explain the warning'),
], 'warning'),
'list of warnings',
VALUE_OPTIONAL
);
}
}
+331
View File
@@ -0,0 +1,331 @@
<?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\privacy;
use context;
use context_user;
use core_privacy\local\metadata\collection;
use core_privacy\local\request\approved_contextlist;
use core_privacy\local\request\transform;
use core_privacy\local\request\writer;
use core_privacy\local\request\userlist;
use core_privacy\local\request\approved_userlist;
/**
* Data provider class.
*
* @package core_external
* @copyright 2018 Frédéric Massart
* @author Frédéric Massart <fred@branchup.tech>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements
\core_privacy\local\metadata\provider,
\core_privacy\local\request\core_userlist_provider,
\core_privacy\local\request\subsystem\provider {
/**
* Returns metadata.
*
* @param collection $collection The initialised collection to add items to.
* @return collection A listing of user data stored through this system.
*/
public static function get_metadata(collection $collection): collection {
$collection->add_database_table('external_tokens', [
'token' => 'privacy:metadata:tokens:token',
'privatetoken' => 'privacy:metadata:tokens:privatetoken',
'tokentype' => 'privacy:metadata:tokens:tokentype',
'userid' => 'privacy:metadata:tokens:userid',
'creatorid' => 'privacy:metadata:tokens:creatorid',
'iprestriction' => 'privacy:metadata:tokens:iprestriction',
'validuntil' => 'privacy:metadata:tokens:validuntil',
'timecreated' => 'privacy:metadata:tokens:timecreated',
'lastaccess' => 'privacy:metadata:tokens:lastaccess',
'name' => 'privacy:metadata:tokens:name',
], 'privacy:metadata:tokens');
$collection->add_database_table('external_services_users', [
'userid' => 'privacy:metadata:serviceusers:userid',
'iprestriction' => 'privacy:metadata:serviceusers:iprestriction',
'validuntil' => 'privacy:metadata:serviceusers:validuntil',
'timecreated' => 'privacy:metadata:serviceusers:timecreated',
], 'privacy:metadata:serviceusers');
return $collection;
}
/**
* Get the list of contexts that contain user information for the specified user.
*
* @param int $userid The user to search.
* @return \core_privacy\local\request\contextlist $contextlist The contextlist containing the list of contexts
* used in this plugin.
*/
public static function get_contexts_for_userid(int $userid): \core_privacy\local\request\contextlist {
$contextlist = new \core_privacy\local\request\contextlist();
$sql = "
SELECT ctx.id
FROM {external_tokens} t
JOIN {context} ctx
ON ctx.instanceid = t.userid
AND ctx.contextlevel = :userlevel
WHERE t.userid = :userid1
OR t.creatorid = :userid2";
$contextlist->add_from_sql($sql, ['userlevel' => CONTEXT_USER, 'userid1' => $userid, 'userid2' => $userid]);
$sql = "
SELECT ctx.id
FROM {external_services_users} su
JOIN {context} ctx
ON ctx.instanceid = su.userid
AND ctx.contextlevel = :userlevel
WHERE su.userid = :userid";
$contextlist->add_from_sql($sql, ['userlevel' => CONTEXT_USER, 'userid' => $userid]);
return $contextlist;
}
/**
* Get the list of users within a specific context.
*
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
*/
public static function get_users_in_context(userlist $userlist) {
global $DB;
$context = $userlist->get_context();
if (!$context instanceof \context_user) {
return;
}
$userid = $context->instanceid;
$hasdata = false;
$hasdata = $hasdata || $DB->record_exists_select('external_tokens', 'userid = ? OR creatorid = ?', [$userid, $userid]);
$hasdata = $hasdata || $DB->record_exists('external_services_users', ['userid' => $userid]);
if ($hasdata) {
$userlist->add_user($userid);
}
}
/**
* Export all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts to export information for.
*/
public static function export_user_data(approved_contextlist $contextlist) {
global $DB;
$userid = $contextlist->get_user()->id;
$contexts = array_reduce($contextlist->get_contexts(), function($carry, $context) use ($userid) {
if ($context->contextlevel == CONTEXT_USER) {
if ($context->instanceid == $userid) {
$carry['has_mine'] = true;
} else {
$carry['others'][] = $context->instanceid;
}
}
return $carry;
}, [
'has_mine' => false,
'others' => []
]);
$path = [get_string('services', 'core_external')];
// Exporting my stuff.
if ($contexts['has_mine']) {
$data = [];
// Exporting my tokens.
$sql = "
SELECT t.*, s.name as externalservicename
FROM {external_tokens} t
JOIN {external_services} s
ON s.id = t.externalserviceid
WHERE t.userid = :userid
ORDER BY t.id";
$recordset = $DB->get_recordset_sql($sql, ['userid' => $userid]);
foreach ($recordset as $record) {
if (!isset($data['tokens'])) {
$data['tokens'] = [];
}
$data['tokens'][] = static::transform_token($record);
}
$recordset->close();
// Exporting the services I have access to.
$sql = "
SELECT su.*, s.name as externalservicename
FROM {external_services_users} su
JOIN {external_services} s
ON s.id = su.externalserviceid
WHERE su.userid = :userid
ORDER BY su.id";
$recordset = $DB->get_recordset_sql($sql, ['userid' => $userid]);
foreach ($recordset as $record) {
if (!isset($data['services_user'])) {
$data['services_user'] = [];
}
$data['services_user'][] = [
'external_service' => $record->externalservicename,
'ip_restriction' => $record->iprestriction,
'valid_until' => $record->validuntil ? transform::datetime($record->validuntil) : null,
'created_on' => transform::datetime($record->timecreated),
];
}
$recordset->close();
if (!empty($data)) {
writer::with_context(context_user::instance($userid))->export_data($path, (object) $data);
};
}
// Exporting the tokens I created.
if (!empty($contexts['others'])) {
list($insql, $inparams) = $DB->get_in_or_equal($contexts['others'], SQL_PARAMS_NAMED);
$sql = "
SELECT t.*, s.name as externalservicename
FROM {external_tokens} t
JOIN {external_services} s
ON s.id = t.externalserviceid
WHERE t.userid $insql
AND t.creatorid = :userid1
AND t.userid <> :userid2
ORDER BY t.userid, t.id";
$params = array_merge($inparams, ['userid1' => $userid, 'userid2' => $userid]);
$recordset = $DB->get_recordset_sql($sql, $params);
static::recordset_loop_and_export($recordset, 'userid', [], function($carry, $record) {
$carry[] = static::transform_token($record);
return $carry;
}, function($userid, $data) use ($path) {
writer::with_context(context_user::instance($userid))->export_related_data($path, 'created_by_you', (object) [
'tokens' => $data
]);
});
}
}
/**
* Delete all data for all users in the specified context.
*
* @param context $context The specific context to delete data for.
*/
public static function delete_data_for_all_users_in_context(context $context) {
if ($context->contextlevel != CONTEXT_USER) {
return;
}
static::delete_user_data($context->instanceid);
}
/**
* Delete multiple users within a single context.
*
* @param approved_userlist $userlist The approved context and user information to delete information for.
*/
public static function delete_data_for_users(approved_userlist $userlist) {
$context = $userlist->get_context();
if ($context instanceof \context_user) {
static::delete_user_data($context->instanceid);
}
}
/**
* Delete all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
*/
public static function delete_data_for_user(approved_contextlist $contextlist) {
$userid = $contextlist->get_user()->id;
foreach ($contextlist as $context) {
if ($context->contextlevel == CONTEXT_USER && $context->instanceid == $userid) {
static::delete_user_data($context->instanceid);
break;
}
}
}
/**
* Delete user data.
*
* @param int $userid The user ID.
* @return void
*/
protected static function delete_user_data($userid) {
global $DB;
$DB->delete_records('external_tokens', ['userid' => $userid]);
$DB->delete_records('external_services_users', ['userid' => $userid]);
}
/**
* Transform a token entry.
*
* @param object $record The token record.
* @return array
*/
protected static function transform_token($record) {
$notexportedstr = get_string('privacy:request:notexportedsecurity', 'core_external');
return [
'external_service' => $record->externalservicename,
'token' => $notexportedstr,
'private_token' => $record->privatetoken ? $notexportedstr : null,
'ip_restriction' => $record->iprestriction,
'valid_until' => $record->validuntil ? transform::datetime($record->validuntil) : null,
'created_on' => transform::datetime($record->timecreated),
'last_access' => $record->lastaccess ? transform::datetime($record->lastaccess) : null,
'name' => $record->name,
];
}
/**
* Loop and export from a recordset.
*
* @param \moodle_recordset $recordset The recordset.
* @param string $splitkey The record key to determine when to export.
* @param mixed $initial The initial data to reduce from.
* @param callable $reducer The function to return the dataset, receives current dataset, and the current record.
* @param callable $export The function to export the dataset, receives the last value from $splitkey and the dataset.
* @return void
*/
protected static function recordset_loop_and_export(\moodle_recordset $recordset, $splitkey, $initial,
callable $reducer, callable $export) {
$data = $initial;
$lastid = null;
foreach ($recordset as $record) {
if ($lastid && $record->{$splitkey} != $lastid) {
$export($lastid, $data);
$data = $initial;
}
$data = $reducer($data, $record);
$lastid = $record->{$splitkey};
}
$recordset->close();
if (!empty($lastid)) {
$export($lastid, $data);
}
}
}
+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_external;
/**
* Exception indicating user is not allowed to use external function in the current context.
*
* @package core_external
* @copyright 2009 Petr Skodak
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class restricted_context_exception extends \moodle_exception {
/**
* Constructor
*/
public function __construct() {
parent::__construct('restrictedcontextexception', 'error');
}
}
+662
View File
@@ -0,0 +1,662 @@
<?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;
use context;
use context_course;
use context_helper;
use context_system;
use core_user;
use moodle_exception;
use moodle_url;
use stdClass;
/**
* Utility functions for the external API.
*
* @package core_webservice
* @copyright 2015 Juan Leyva
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class util {
/**
* Validate a list of courses, returning the complete course objects for valid courses.
*
* Each course has an additional 'contextvalidated' field, this will be set to true unless
* you set $keepfails, in which case it will be false if validation fails for a course.
*
* @param array $courseids A list of course ids
* @param array $courses An array of courses already pre-fetched, indexed by course id.
* @param bool $addcontext True if the returned course object should include the full context object.
* @param bool $keepfails True to keep all the course objects even if validation fails
* @return array An array of courses and the validation warnings
*/
public static function validate_courses(
$courseids,
$courses = [],
$addcontext = false,
$keepfails = false
) {
global $DB;
// Delete duplicates.
$courseids = array_unique($courseids);
$warnings = [];
// Remove courses which are not even requested.
$courses = array_intersect_key($courses, array_flip($courseids));
// For any courses NOT loaded already, get them in a single query (and preload contexts)
// for performance. Preserve ordering because some tests depend on it.
$newcourseids = [];
foreach ($courseids as $cid) {
if (!array_key_exists($cid, $courses)) {
$newcourseids[] = $cid;
}
}
if ($newcourseids) {
[$listsql, $listparams] = $DB->get_in_or_equal($newcourseids);
// Load list of courses, and preload associated contexts.
$contextselect = context_helper::get_preload_record_columns_sql('x');
$newcourses = $DB->get_records_sql(
"
SELECT c.*, $contextselect
FROM {course} c
JOIN {context} x ON x.instanceid = c.id
WHERE x.contextlevel = ? AND c.id $listsql",
array_merge([CONTEXT_COURSE], $listparams)
);
foreach ($newcourseids as $cid) {
if (array_key_exists($cid, $newcourses)) {
$course = $newcourses[$cid];
context_helper::preload_from_record($course);
$courses[$course->id] = $course;
}
}
}
foreach ($courseids as $cid) {
// Check the user can function in this context.
try {
$context = context_course::instance($cid);
external_api::validate_context($context);
if ($addcontext) {
$courses[$cid]->context = $context;
}
$courses[$cid]->contextvalidated = true;
} catch (\Exception $e) {
if ($keepfails) {
$courses[$cid]->contextvalidated = false;
} else {
unset($courses[$cid]);
}
$warnings[] = [
'item' => 'course',
'itemid' => $cid,
'warningcode' => '1',
'message' => 'No access rights in course context',
];
}
}
return [$courses, $warnings];
}
/**
* Returns all area files (optionally limited by itemid).
*
* @param int $contextid context ID
* @param string $component component
* @param string $filearea file area
* @param int $itemid item ID or all files if not specified
* @param bool $useitemidinurl wether to use the item id in the file URL (modules intro don't use it)
* @return array of files, compatible with the external_files structure.
* @since Moodle 3.2
*/
public static function get_area_files($contextid, $component, $filearea, $itemid = false, $useitemidinurl = true) {
$files = [];
$fs = get_file_storage();
if ($areafiles = $fs->get_area_files($contextid, $component, $filearea, $itemid, 'itemid, filepath, filename', false)) {
foreach ($areafiles as $areafile) {
$file = [];
$file['filename'] = $areafile->get_filename();
$file['filepath'] = $areafile->get_filepath();
$file['mimetype'] = $areafile->get_mimetype();
$file['filesize'] = $areafile->get_filesize();
$file['timemodified'] = $areafile->get_timemodified();
$file['isexternalfile'] = $areafile->is_external_file();
if ($file['isexternalfile']) {
$file['repositorytype'] = $areafile->get_repository_type();
}
$fileitemid = $useitemidinurl ? $areafile->get_itemid() : null;
// If AJAX request, generate a standard plugin file url.
if (AJAX_SCRIPT) {
$fileurl = moodle_url::make_pluginfile_url(
$contextid,
$component,
$filearea,
$fileitemid,
$areafile->get_filepath(),
$areafile->get_filename()
);
} else { // Otherwise, generate a webservice plugin file url.
$fileurl = moodle_url::make_webservice_pluginfile_url(
$contextid,
$component,
$filearea,
$fileitemid,
$areafile->get_filepath(),
$areafile->get_filename()
);
}
$file['fileurl'] = $fileurl->out(false);
$file['icon'] = file_file_icon($areafile);
$files[] = $file;
}
}
return $files;
}
/**
* Create and return a session linked token. Token to be used for html embedded client apps that want to communicate
* with the Moodle server through web services. The token is linked to the current session for the current page request.
* It is expected this will be called in the script generating the html page that is embedding the client app and that the
* returned token will be somehow passed into the client app being embedded in the page.
*
* @param int $tokentype EXTERNAL_TOKEN_EMBEDDED|EXTERNAL_TOKEN_PERMANENT
* @param stdClass $service service linked to the token
* @param int $userid user linked to the token
* @param context $context
* @param int $validuntil date when the token expired
* @param string $iprestriction allowed ip - if 0 or empty then all ips are allowed
* @param string $name token name as a note or token identity at the table view.
* @return string generated token
*/
public static function generate_token(
int $tokentype,
stdClass $service,
int $userid,
context $context,
int $validuntil = 0,
string $iprestriction = '',
string $name = ''
): string {
global $DB, $USER, $SESSION;
// Make sure the token doesn't exist (even if it should be almost impossible with the random generation).
$numtries = 0;
do {
$numtries++;
$generatedtoken = md5(uniqid((string) rand(), true));
if ($numtries > 5) {
throw new moodle_exception('tokengenerationfailed');
}
} while ($DB->record_exists('external_tokens', ['token' => $generatedtoken]));
$newtoken = (object) [
'token' => $generatedtoken,
];
if (empty($service->requiredcapability) || has_capability($service->requiredcapability, $context, $userid)) {
$newtoken->externalserviceid = $service->id;
} else {
throw new moodle_exception('nocapabilitytousethisservice');
}
$newtoken->tokentype = $tokentype;
$newtoken->userid = $userid;
if ($tokentype == EXTERNAL_TOKEN_EMBEDDED) {
$newtoken->sid = session_id();
}
$newtoken->contextid = $context->id;
$newtoken->creatorid = $USER->id;
$newtoken->timecreated = time();
$newtoken->validuntil = $validuntil;
if (!empty($iprestriction)) {
$newtoken->iprestriction = $iprestriction;
}
// Generate the private token, it must be transmitted only via https.
$newtoken->privatetoken = random_string(64);
if (!$name) {
// Generate a token name.
$name = self::generate_token_name();
}
$newtoken->name = $name;
$tokenid = $DB->insert_record('external_tokens', $newtoken);
// Create new session to hold newly created token ID.
$SESSION->webservicenewlycreatedtoken = $tokenid;
return $newtoken->token;
}
/**
* Get a service by its id.
*
* @param int $serviceid
* @return stdClass
*/
public static function get_service_by_id(int $serviceid): stdClass {
global $DB;
return $DB->get_record('external_services', ['id' => $serviceid], '*', MUST_EXIST);
}
/**
* Get a service by its name.
*
* @param string $name The service name.
* @return stdClass
*/
public static function get_service_by_name(string $name): stdClass {
global $DB;
return $DB->get_record('external_services', ['name' => $name], '*', MUST_EXIST);
}
/**
* Set the last time a token was sent and trigger the \core\event\webservice_token_sent event.
*
* This function is used when a token is generated by the user via login/token.php or admin/tool/mobile/launch.php.
* In order to protect the privatetoken, we remove it from the event params.
*
* @param stdClass $token token object
*/
public static function log_token_request(stdClass $token): void {
global $DB, $USER;
$token->privatetoken = null;
// Log token access.
$DB->set_field('external_tokens', 'lastaccess', time(), ['id' => $token->id]);
$event = \core\event\webservice_token_sent::create([
'objectid' => $token->id,
]);
$event->add_record_snapshot('external_tokens', $token);
$event->trigger();
// Check if we need to notify the user about the new login via token.
$loginip = getremoteaddr();
if ($USER->lastip === $loginip) {
return;
}
$shouldskip = WS_SERVER || CLI_SCRIPT || !NO_MOODLE_COOKIES;
if ($shouldskip && !PHPUNIT_TEST) {
return;
}
// Schedule adhoc task to sent a login notification to the user.
$task = new \core\task\send_login_notifications();
$task->set_userid($USER->id);
$logintime = time();
$task->set_custom_data([
'useragent' => \core_useragent::get_user_agent_string(),
'ismoodleapp' => \core_useragent::is_moodle_app(),
'loginip' => $loginip,
'logintime' => $logintime,
]);
$task->set_component('core');
// We need sometime so the mobile app will send to Moodle the device information after login.
$task->set_next_run_time(time() + (2 * MINSECS));
\core\task\manager::reschedule_or_queue_adhoc_task($task);
}
/**
* Generate or return an existing token for the current authenticated user.
* This function is used for creating a valid token for users authenticathing via places, including:
* - login/token.php
* - admin/tool/mobile/launch.php.
*
* @param stdClass $service external service object
* @return stdClass token object
* @throws moodle_exception
*/
public static function generate_token_for_current_user(stdClass $service) {
global $DB, $USER, $CFG;
core_user::require_active_user($USER, true, true);
// Check if there is any required system capability.
if ($service->requiredcapability && !has_capability($service->requiredcapability, context_system::instance())) {
throw new moodle_exception('missingrequiredcapability', 'webservice', '', $service->requiredcapability);
}
// Specific checks related to user restricted service.
if ($service->restrictedusers) {
$authoriseduser = $DB->get_record('external_services_users', [
'externalserviceid' => $service->id,
'userid' => $USER->id,
]);
if (empty($authoriseduser)) {
throw new moodle_exception('usernotallowed', 'webservice', '', $service->shortname);
}
if (!empty($authoriseduser->validuntil) && $authoriseduser->validuntil < time()) {
throw new moodle_exception('invalidtimedtoken', 'webservice');
}
if (!empty($authoriseduser->iprestriction) && !address_in_subnet(getremoteaddr(), $authoriseduser->iprestriction)) {
throw new moodle_exception('invalidiptoken', 'webservice');
}
}
// Check if a token has already been created for this user and this service.
$conditions = [
'userid' => $USER->id,
'externalserviceid' => $service->id,
'tokentype' => EXTERNAL_TOKEN_PERMANENT,
];
$tokens = $DB->get_records('external_tokens', $conditions, 'timecreated ASC');
// A bit of sanity checks.
foreach ($tokens as $key => $token) {
// Checks related to a specific token. (script execution continue).
$unsettoken = false;
// If sid is set then there must be a valid associated session no matter the token type.
if (!empty($token->sid)) {
if (!\core\session\manager::session_exists($token->sid)) {
// This token will never be valid anymore, delete it.
$DB->delete_records('external_tokens', ['sid' => $token->sid]);
$unsettoken = true;
}
}
// Remove token is not valid anymore.
if (!empty($token->validuntil) && $token->validuntil < time()) {
$DB->delete_records('external_tokens', ['token' => $token->token, 'tokentype' => EXTERNAL_TOKEN_PERMANENT]);
$unsettoken = true;
}
// Remove token if its IP is restricted.
if (isset($token->iprestriction) && !address_in_subnet(getremoteaddr(), $token->iprestriction)) {
$unsettoken = true;
}
if ($unsettoken) {
unset($tokens[$key]);
}
}
// If some valid tokens exist then use the most recent.
if (count($tokens) > 0) {
$token = array_pop($tokens);
} else {
$context = context_system::instance();
$isofficialservice = $service->shortname == MOODLE_OFFICIAL_MOBILE_SERVICE;
if (
($isofficialservice && has_capability('moodle/webservice:createmobiletoken', $context)) ||
(!is_siteadmin($USER) && has_capability('moodle/webservice:createtoken', $context))
) {
// Create a new token.
$token = new stdClass();
$token->token = md5(uniqid((string) rand(), true));
$token->userid = $USER->id;
$token->tokentype = EXTERNAL_TOKEN_PERMANENT;
$token->contextid = context_system::instance()->id;
$token->creatorid = $USER->id;
$token->timecreated = time();
$token->externalserviceid = $service->id;
// By default tokens are valid for 12 weeks.
$token->validuntil = $token->timecreated + $CFG->tokenduration;
$token->iprestriction = null;
$token->sid = null;
$token->lastaccess = null;
$token->name = self::generate_token_name();
// Generate the private token, it must be transmitted only via https.
$token->privatetoken = random_string(64);
$token->id = $DB->insert_record('external_tokens', $token);
$eventtoken = clone $token;
$eventtoken->privatetoken = null;
$params = [
'objectid' => $eventtoken->id,
'relateduserid' => $USER->id,
'other' => [
'auto' => true,
],
];
$event = \core\event\webservice_token_created::create($params);
$event->add_record_snapshot('external_tokens', $eventtoken);
$event->trigger();
} else {
throw new moodle_exception('cannotcreatetoken', 'webservice', '', $service->shortname);
}
}
return $token;
}
/**
* Format the string to be returned properly as requested by the either the web service server,
* either by an internally call.
* The caller can change the format (raw) with the settings singleton
* All web service servers must set this singleton when parsing the $_GET and $_POST.
*
* <pre>
* Options are the same that in {@see format_string()} with some changes:
* filter : Can be set to false to force filters off, else observes {@see settings}.
* </pre>
*
* @param string|null $content The string to be filtered. Should be plain text, expect
* possibly for multilang tags.
* @param int|context $context The id of the context for the string or the context (affects filters).
* @param boolean $striplinks To strip any link in the result text. Moodle 1.8 default changed from false to true! MDL-8713
* @param array $options options array/object or courseid
* @return string text
*/
public static function format_string(
$content,
$context,
$striplinks = true,
$options = []
) {
if ($content === null || $content === '') {
// Nothing to return.
// Note: It's common for the DB to return null, so we allow format_string to take a null,
// even though it is counter-intuitive.
return '';
}
// Get settings (singleton).
$settings = external_settings::get_instance();
if (!$settings->get_raw()) {
$options['context'] = $context;
$options['filter'] = isset($options['filter']) && !$options['filter'] ? false : $settings->get_filter();
return format_string($content, $striplinks, $options);
}
return $content;
}
/**
* Format the text to be returned properly as requested by the either the web service server,
* either by an internally call.
* The caller can change the format (raw, filter, file, fileurl) with the \core_external\settings singleton
* All web service servers must set this singleton when parsing the $_GET and $_POST.
*
* <pre>
* Options are the same that in {@see format_text()} with some changes in defaults to provide backwards compatibility:
* trusted : If true the string won't be cleaned. Default false.
* noclean : If true the string won't be cleaned only if trusted is also true. Default false.
* nocache : If true the string will not be cached and will be formatted every call. Default false.
* filter : Can be set to false to force filters off, else observes {@see \core_external\settings}.
* para : If true then the returned string will be wrapped in div tags.
* Default (different from format_text) false.
* Default changed because div tags are not commonly needed.
* newlines : If true then lines newline breaks will be converted to HTML newline breaks. Default true.
* context : Not used! Using contextid parameter instead.
* overflowdiv : If set to true the formatted text will be encased in a div with the class no-overflow before being
* returned. Default false.
* allowid : If true then id attributes will not be removed, even when using htmlpurifier. Default (different from
* format_text) true. Default changed id attributes are commonly needed.
* blanktarget : If true all <a> tags will have target="_blank" added unless target is explicitly specified.
* </pre>
*
* @param string|null $text The content that may contain ULRs in need of rewriting.
* @param string|int|null $textformat The text format.
* @param context $context This parameter and the next two identify the file area to use.
* @param string|null $component
* @param string|null $filearea helps identify the file area.
* @param int|string|null $itemid helps identify the file area.
* @param array|stdClass|null $options text formatting options
* @return array text + textformat
*/
public static function format_text(
$text,
$textformat,
$context,
$component = null,
$filearea = null,
$itemid = null,
$options = null
) {
global $CFG;
if ($text === null || $text === '') {
// Nothing to return.
// Note: It's common for the DB to return null, so we allow format_string to take nulls,
// even though it is counter-intuitive.
return ['', $textformat ?? FORMAT_MOODLE];
}
if (empty($itemid)) {
$itemid = null;
}
// Get settings (singleton).
$settings = external_settings::get_instance();
if ($component && $filearea && $settings->get_fileurl()) {
require_once($CFG->libdir . "/filelib.php");
$text = file_rewrite_pluginfile_urls($text, $settings->get_file(), $context->id, $component, $filearea, $itemid);
}
// Note that $CFG->forceclean does not apply here if the client requests for the raw database content.
// This is consistent with web clients that are still able to load non-cleaned text into editors, too.
if (!$settings->get_raw()) {
$options = (array) $options;
// If context is passed in options, check that is the same to show a debug message.
if (isset($options['context'])) {
if (is_int($options['context'])) {
if ($options['context'] != $context->id) {
debugging(
'Different contexts found in external_format_text parameters. $options[\'context\'] not allowed. ' .
'Using $contextid parameter...',
DEBUG_DEVELOPER
);
}
} else if ($options['context'] instanceof context) {
if ($options['context']->id != $context->id) {
debugging(
'Different contexts found in external_format_text parameters. $options[\'context\'] not allowed. ' .
'Using $contextid parameter...',
DEBUG_DEVELOPER
);
}
}
}
$options['filter'] = isset($options['filter']) && !$options['filter'] ? false : $settings->get_filter();
$options['para'] = isset($options['para']) ? $options['para'] : false;
$options['context'] = $context;
$options['allowid'] = isset($options['allowid']) ? $options['allowid'] : true;
$text = format_text($text, $textformat, $options);
// Once converted to html (from markdown, plain... lets inform consumer this is already HTML).
$textformat = FORMAT_HTML;
}
// Note: The formats defined in weblib are strings.
return [$text, $textformat];
}
/**
* Validate text field format against known FORMAT_XXX
*
* @param string $format the format to validate
* @return string the validated format
* @throws \moodle_exception
* @since Moodle 2.3
*/
public static function validate_format($format) {
$allowedformats = [FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN];
if (!in_array($format, $allowedformats)) {
throw new moodle_exception(
'formatnotsupported',
'webservice',
'',
null,
"The format with value={$format} is not supported by this Moodle site"
);
}
return $format;
}
/**
* Delete all pre-built services, related tokens, and external functions information defined for the specified component.
*
* @param string $component The frankenstyle component name
*/
public static function delete_service_descriptions(string $component): void {
global $DB;
$params = [$component];
$DB->delete_records_select(
'external_tokens',
"externalserviceid IN (SELECT id FROM {external_services} WHERE component = ?)",
$params
);
$DB->delete_records_select(
'external_services_users',
"externalserviceid IN (SELECT id FROM {external_services} WHERE component = ?)",
$params
);
$DB->delete_records_select(
'external_services_functions',
"functionname IN (SELECT name FROM {external_functions} WHERE component = ?)",
$params
);
$DB->delete_records('external_services', ['component' => $component]);
$DB->delete_records('external_functions', ['component' => $component]);
}
/**
* Generate token name.
*
* @return string
*/
public static function generate_token_name(): string {
return get_string(
'tokennameprefix',
'webservice',
random_string(5)
);
}
}
+558
View File
@@ -0,0 +1,558 @@
<?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/>.
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_multiple_structure;
use core_external\external_single_structure;
use core_external\external_value;
/**
* Web service related functions
*
* @package core
* @category external
* @copyright 2012 Jerome Mouneyrac <jerome@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since Moodle 2.4
*/
class core_external extends external_api {
/**
* Format the received string parameters to be sent to the core get_string() function.
*
* @param array $stringparams
* @return object|string
* @since Moodle 2.4
*/
public static function format_string_parameters($stringparams) {
// Check if there are some string params.
$strparams = new stdClass();
if (!empty($stringparams)) {
// There is only one string parameter.
if (count($stringparams) == 1) {
$stringparam = array_pop($stringparams);
if (isset($stringparam['name'])) {
$strparams->{$stringparam['name']} = $stringparam['value'];
} else {
// It is a not named string parameter.
$strparams = $stringparam['value'];
}
} else {
// There are more than one parameter.
foreach ($stringparams as $stringparam) {
// If a parameter is unnamed throw an exception
// unnamed param is only possible if one only param is sent.
if (empty($stringparam['name'])) {
throw new moodle_exception('unnamedstringparam', 'webservice');
}
$strparams->{$stringparam['name']} = $stringparam['value'];
}
}
}
return $strparams;
}
/**
* Returns description of get_string parameters
*
* @return external_function_parameters
* @since Moodle 2.4
*/
public static function get_string_parameters() {
return new external_function_parameters(
array('stringid' => new external_value(PARAM_STRINGID, 'string identifier'),
'component' => new external_value(PARAM_COMPONENT,'component', VALUE_DEFAULT, 'moodle'),
'lang' => new external_value(PARAM_LANG, 'lang', VALUE_DEFAULT, null),
'stringparams' => new external_multiple_structure (
new external_single_structure(array(
'name' => new external_value(PARAM_ALPHANUMEXT, 'param name
- if the string expect only one $a parameter then don\'t send this field, just send the value.', VALUE_OPTIONAL),
'value' => new external_value(PARAM_RAW,'param value'))),
'the definition of a string param (i.e. {$a->name})', VALUE_DEFAULT, array()
)
)
);
}
/**
* Return a core get_string() call
*
* @param string $identifier string identifier
* @param string $component string component
* @param array $stringparams the string params
* @return string
* @since Moodle 2.4
*/
public static function get_string($stringid, $component = 'moodle', $lang = null, $stringparams = array()) {
$params = self::validate_parameters(self::get_string_parameters(),
array('stringid'=>$stringid, 'component' => $component, 'lang' => $lang, 'stringparams' => $stringparams));
$stringmanager = get_string_manager();
return $stringmanager->get_string($params['stringid'], $params['component'],
core_external::format_string_parameters($params['stringparams']), $params['lang']);
}
/**
* Returns description of get_string() result value
*
* @return \core_external\external_description
* @since Moodle 2.4
*/
public static function get_string_returns() {
return new external_value(PARAM_RAW, 'translated string');
}
/**
* Returns description of get_string parameters
*
* @return external_function_parameters
* @since Moodle 2.4
*/
public static function get_strings_parameters() {
return new external_function_parameters(
array('strings' => new external_multiple_structure (
new external_single_structure (array(
'stringid' => new external_value(PARAM_STRINGID, 'string identifier'),
'component' => new external_value(PARAM_COMPONENT, 'component', VALUE_DEFAULT, 'moodle'),
'lang' => new external_value(PARAM_LANG, 'lang', VALUE_DEFAULT, null),
'stringparams' => new external_multiple_structure (
new external_single_structure(array(
'name' => new external_value(PARAM_ALPHANUMEXT, 'param name
- if the string expect only one $a parameter then don\'t send this field, just send the value.', VALUE_OPTIONAL),
'value' => new external_value(PARAM_RAW, 'param value'))),
'the definition of a string param (i.e. {$a->name})', VALUE_DEFAULT, array()
))
)
)
)
);
}
/**
* Return multiple call to core get_string()
*
* @param array $strings strings to translate
* @return array
*
* @since Moodle 2.4
*/
public static function get_strings($strings) {
$params = self::validate_parameters(self::get_strings_parameters(),
array('strings'=>$strings));
$stringmanager = get_string_manager();
$translatedstrings = array();
foreach($params['strings'] as $string) {
if (!empty($string['lang'])) {
$lang = $string['lang'];
} else {
$lang = current_language();
}
$translatedstrings[] = array(
'stringid' => $string['stringid'],
'component' => $string['component'],
'lang' => $lang,
'string' => $stringmanager->get_string($string['stringid'], $string['component'],
core_external::format_string_parameters($string['stringparams']), $lang));
}
return $translatedstrings;
}
/**
* Returns description of get_string() result value
*
* @return \core_external\external_description
* @since Moodle 2.4
*/
public static function get_strings_returns() {
return new external_multiple_structure(
new external_single_structure(array(
'stringid' => new external_value(PARAM_STRINGID, 'string id'),
'component' => new external_value(PARAM_COMPONENT, 'string component'),
'lang' => new external_value(PARAM_LANG, 'lang'),
'string' => new external_value(PARAM_RAW, 'translated string'))
));
}
/**
* Returns description of get_user_dates parameters
*
* @return external_function_parameters
*/
public static function get_user_dates_parameters() {
return new external_function_parameters(
[
'contextid' => new external_value(
PARAM_INT,
'Context ID. Either use this value, or level and instanceid.',
VALUE_DEFAULT,
0
),
'contextlevel' => new external_value(
PARAM_ALPHA,
'Context level. To be used with instanceid.',
VALUE_DEFAULT,
''
),
'instanceid' => new external_value(
PARAM_INT,
'Context instance ID. To be used with level',
VALUE_DEFAULT,
0
),
'timestamps' => new external_multiple_structure (
new external_single_structure (
[
'timestamp' => new external_value(PARAM_INT, 'unix timestamp'),
'format' => new external_value(PARAM_TEXT, 'format string'),
'type' => new external_value(PARAM_PLUGIN, 'The calendar type', VALUE_DEFAULT),
'fixday' => new external_value(PARAM_INT, 'Remove leading zero for day', VALUE_DEFAULT, 1),
'fixhour' => new external_value(PARAM_INT, 'Remove leading zero for hour', VALUE_DEFAULT, 1),
]
)
)
]
);
}
/**
* Format an array of timestamps.
*
* @param int|null $contextid The contenxt id
* @param string|null $contextlevel The context level
* @param int|null $instanceid The instnace id for the context level
* @param array $timestamps Timestamps to format
* @return array
*/
public static function get_user_dates($contextid, $contextlevel, $instanceid, $timestamps) {
$params = self::validate_parameters(
self::get_user_dates_parameters(),
[
'contextid' => $contextid,
'contextlevel' => $contextlevel,
'instanceid' => $instanceid,
'timestamps' => $timestamps,
]
);
$context = self::get_context_from_params($params);
self::validate_context($context);
$formatteddates = array_map(function($timestamp) {
$calendartype = $timestamp['type'];
$fixday = !empty($timestamp['fixday']);
$fixhour = !empty($timestamp['fixhour']);
$calendar = \core_calendar\type_factory::get_calendar_instance($calendartype);
return $calendar->timestamp_to_date_string($timestamp['timestamp'], $timestamp['format'], 99, $fixday, $fixhour);
}, $params['timestamps']);
return ['dates' => $formatteddates];
}
/**
* Returns description of get_user_dates() result value
*
* @return \core_external\external_description
*/
public static function get_user_dates_returns() {
return new external_single_structure(
[
'dates' => new external_multiple_structure (
new external_value(PARAM_TEXT, 'formatted dates strings')
)
]
);
}
/**
* Returns description of get_component_strings parameters
*
* @return external_function_parameters
* @since Moodle 2.4
*/
public static function get_component_strings_parameters() {
return new external_function_parameters(
array('component' => new external_value(PARAM_COMPONENT, 'component'),
'lang' => new external_value(PARAM_LANG, 'lang', VALUE_DEFAULT, null),
)
);
}
/**
* Return all lang strings of a component - call to core get_component_strings().
*
* @param string $component component name
* @return array
*
* @since Moodle 2.4
*/
public static function get_component_strings($component, $lang = null) {
if (empty($lang)) {
$lang = current_language();
}
$params = self::validate_parameters(self::get_component_strings_parameters(),
array('component'=>$component, 'lang' => $lang));
$stringmanager = get_string_manager();
$wsstrings = array();
$componentstrings = $stringmanager->load_component_strings($params['component'], $params['lang']);
foreach($componentstrings as $stringid => $string) {
$wsstring = array();
$wsstring['stringid'] = $stringid;
$wsstring['string'] = $string;
$wsstrings[] = $wsstring;
}
return $wsstrings;
}
/**
* Returns description of get_component_strings() result value
*
* @return \core_external\external_description
* @since Moodle 2.4
*/
public static function get_component_strings_returns() {
return new external_multiple_structure(
new external_single_structure(array(
'stringid' => new external_value(PARAM_STRINGID, 'string id'),
'string' => new external_value(PARAM_RAW, 'translated string'))
));
}
/**
* Returns description of get_fragment parameters
*
* @return external_function_parameters
* @since Moodle 3.1
*/
public static function get_fragment_parameters() {
return new external_function_parameters(
array(
'component' => new external_value(PARAM_COMPONENT, 'Component for the callback e.g. mod_assign'),
'callback' => new external_value(PARAM_ALPHANUMEXT, 'Name of the callback to execute'),
'contextid' => new external_value(PARAM_INT, 'Context ID that the fragment is from'),
'args' => new external_multiple_structure(
new external_single_structure(
array(
'name' => new external_value(PARAM_ALPHANUMEXT, 'param name'),
'value' => new external_value(PARAM_RAW, 'param value')
)
), 'args for the callback are optional', VALUE_OPTIONAL
)
)
);
}
/**
* Get a HTML fragment for inserting into something. Initial use is for inserting mforms into
* a page using AJAX.
* This web service is designed to be called only via AJAX and not directly.
* Callbacks that are called by this web service are responsible for doing the appropriate security checks
* to access the information returned. This only does minimal validation on the context.
*
* @param string $component Name of the component.
* @param string $callback Function callback name.
* @param int $contextid Context ID this fragment is in.
* @param array $args optional arguments for the callback.
* @return array HTML and JavaScript fragments for insertion into stuff.
* @since Moodle 3.1
*/
public static function get_fragment($component, $callback, $contextid, $args = null) {
global $OUTPUT, $PAGE;
$params = self::validate_parameters(self::get_fragment_parameters(),
array(
'component' => $component,
'callback' => $callback,
'contextid' => $contextid,
'args' => $args
)
);
// Reformat arguments into something less unwieldy.
$arguments = array();
foreach ($params['args'] as $paramargument) {
$arguments[$paramargument['name']] = $paramargument['value'];
}
$context = context::instance_by_id($contextid);
self::validate_context($context);
$arguments['context'] = $context;
// Hack alert: Set a default URL to stop the annoying debug.
$PAGE->set_url('/');
// Hack alert: Forcing bootstrap_renderer to initiate moodle page.
$OUTPUT->header();
// Overwriting page_requirements_manager with the fragment one so only JS included from
// this point is returned to the user.
$PAGE->start_collecting_javascript_requirements();
$data = component_callback($params['component'], 'output_fragment_' . $params['callback'], array($arguments));
$jsfooter = $PAGE->requires->get_end_code();
$output = array('html' => $data, 'javascript' => $jsfooter);
return $output;
}
/**
* Returns description of get_fragment() result value
*
* @return \core_external\external_description
* @since Moodle 3.1
*/
public static function get_fragment_returns() {
return new external_single_structure(
array(
'html' => new external_value(PARAM_RAW, 'HTML fragment.'),
'javascript' => new external_value(PARAM_RAW, 'JavaScript fragment')
)
);
}
/**
* Parameters for function update_inplace_editable()
*
* @since Moodle 3.1
* @return external_function_parameters
*/
public static function update_inplace_editable_parameters() {
return new external_function_parameters(
array(
'component' => new external_value(PARAM_COMPONENT, 'component responsible for the update', VALUE_REQUIRED),
'itemtype' => new external_value(PARAM_NOTAGS, 'type of the updated item inside the component', VALUE_REQUIRED),
'itemid' => new external_value(PARAM_RAW, 'identifier of the updated item', VALUE_REQUIRED),
'value' => new external_value(PARAM_RAW, 'new value', VALUE_REQUIRED),
));
}
/**
* Update any component's editable value assuming that component implements necessary callback
*
* @since Moodle 3.1
* @param string $component
* @param string $itemtype
* @param string $itemid
* @param string $value
*/
public static function update_inplace_editable($component, $itemtype, $itemid, $value) {
global $PAGE;
// Validate and normalize parameters.
$params = self::validate_parameters(self::update_inplace_editable_parameters(),
array('component' => $component, 'itemtype' => $itemtype, 'itemid' => $itemid, 'value' => $value));
if (!$functionname = component_callback_exists($component, 'inplace_editable')) {
throw new \moodle_exception('inplaceeditableerror');
}
$tmpl = component_callback($params['component'], 'inplace_editable',
array($params['itemtype'], $params['itemid'], $params['value']));
if (!$tmpl || !($tmpl instanceof \core\output\inplace_editable)) {
throw new \moodle_exception('inplaceeditableerror');
}
return $tmpl->export_for_template($PAGE->get_renderer('core'));
}
/**
* Return structure for update_inplace_editable()
*
* @since Moodle 3.1
* @return \core_external\external_description
*/
public static function update_inplace_editable_returns() {
return new external_single_structure(
array(
'displayvalue' => new external_value(PARAM_RAW, 'display value (may contain link or other html tags)'),
'component' => new external_value(PARAM_NOTAGS, 'component responsible for the update', VALUE_OPTIONAL),
'itemtype' => new external_value(PARAM_NOTAGS, 'itemtype', VALUE_OPTIONAL),
'value' => new external_value(PARAM_RAW, 'value of the item as it is stored', VALUE_OPTIONAL),
'itemid' => new external_value(PARAM_RAW, 'identifier of the updated item', VALUE_OPTIONAL),
'edithint' => new external_value(PARAM_NOTAGS, 'hint for editing element', VALUE_OPTIONAL),
'editlabel' => new external_value(PARAM_RAW, 'label for editing element', VALUE_OPTIONAL),
'editicon' => new external_single_structure([
'key' => new external_value(PARAM_RAW, 'Edit icon key', VALUE_OPTIONAL),
'component' => new external_value(PARAM_COMPONENT, 'Edit icon component', VALUE_OPTIONAL),
'title' => new external_value(PARAM_NOTAGS, 'Edit icon title', VALUE_OPTIONAL),
], 'Edit icon', VALUE_OPTIONAL),
'type' => new external_value(PARAM_ALPHA, 'type of the element (text, toggle, select)', VALUE_OPTIONAL),
'options' => new external_value(PARAM_RAW, 'options of the element, format depends on type', VALUE_OPTIONAL),
'linkeverything' => new external_value(PARAM_INT, 'Should everything be wrapped in the edit link or link displayed separately', VALUE_OPTIONAL),
)
);
}
/**
* Returns description of fetch_notifications() parameters.
*
* @return external_function_parameters
* @since Moodle 3.1
*/
public static function fetch_notifications_parameters() {
return new external_function_parameters(
array(
'contextid' => new external_value(PARAM_INT, 'Context ID', VALUE_REQUIRED),
));
}
/**
* Returns description of fetch_notifications() result value.
*
* @return \core_external\external_description
* @since Moodle 3.1
*/
public static function fetch_notifications_returns() {
return new external_multiple_structure(
new external_single_structure(
array(
'template' => new external_value(PARAM_RAW, 'Name of the template'),
'variables' => new external_single_structure(array(
'message' => new external_value(PARAM_RAW, 'HTML content of the Notification'),
'extraclasses' => new external_value(PARAM_RAW, 'Extra classes to provide to the tmeplate'),
'announce' => new external_value(PARAM_RAW, 'Whether to announce'),
'closebutton' => new external_value(PARAM_RAW, 'Whether to close'),
)),
)
)
);
}
/**
* Returns the list of notifications against the current session.
*
* @return array
* @since Moodle 3.1
*/
public static function fetch_notifications($contextid) {
global $PAGE;
self::validate_parameters(self::fetch_notifications_parameters(), [
'contextid' => $contextid,
]);
$context = \context::instance_by_id($contextid);
self::validate_context($context);
return \core\notification::fetch_as_array($PAGE->get_renderer('core'));
}
}
+474
View File
@@ -0,0 +1,474 @@
<?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;
/**
* Unit tests for core_external\external_api.
*
* @package core_external
* @category test
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
* @covers \core_external\external_api
*/
class external_api_test extends \advanced_testcase {
/**
* Test the validate_parameters method.
*
* @covers \core_external\external_api::validate_parameters
*/
public function test_validate_params(): void {
$params = ['text' => 'aaa', 'someid' => '6'];
$description = new external_function_parameters([
'someid' => new external_value(PARAM_INT, 'Some int value'),
'text' => new external_value(PARAM_ALPHA, 'Some text value'),
]);
$result = external_api::validate_parameters($description, $params);
$this->assertCount(2, $result);
reset($result);
$this->assertSame('someid', key($result));
$this->assertSame(6, $result['someid']);
$this->assertSame('aaa', $result['text']);
$params = [
'someids' => ['1', 2, 'a' => '3'],
'scalar' => 666,
];
$description = new external_function_parameters([
'someids' => new external_multiple_structure(new external_value(PARAM_INT, 'Some ID')),
'scalar' => new external_value(PARAM_ALPHANUM, 'Some text value'),
]);
$result = external_api::validate_parameters($description, $params);
$this->assertCount(2, $result);
reset($result);
$this->assertSame('someids', key($result));
$this->assertEquals([0 => 1, 1 => 2, 2 => 3], $result['someids']);
$this->assertSame('666', $result['scalar']);
$params = ['text' => 'aaa'];
$description = new external_function_parameters([
'someid' => new external_value(PARAM_INT, 'Some int value', VALUE_DEFAULT),
'text' => new external_value(PARAM_ALPHA, 'Some text value'),
]);
$result = external_api::validate_parameters($description, $params);
$this->assertCount(2, $result);
reset($result);
$this->assertSame('someid', key($result));
$this->assertNull($result['someid']);
$this->assertSame('aaa', $result['text']);
$params = ['text' => 'aaa'];
$description = new external_function_parameters([
'someid' => new external_value(PARAM_INT, 'Some int value', VALUE_DEFAULT, 6),
'text' => new external_value(PARAM_ALPHA, 'Some text value'),
]);
$result = external_api::validate_parameters($description, $params);
$this->assertCount(2, $result);
reset($result);
$this->assertSame('someid', key($result));
$this->assertSame(6, $result['someid']);
$this->assertSame('aaa', $result['text']);
// Missing required value (an exception is thrown).
$testdata = [];
try {
external_api::clean_returnvalue($description, $testdata);
$this->fail('Exception expected');
} catch (\moodle_exception $ex) {
$this->assertInstanceOf(\invalid_response_exception::class, $ex);
$this->assertSame('Invalid response value detected (Error in response - '
. 'Missing following required key in a single structure: text)', $ex->getMessage());
}
// Test nullable external_value may optionally return data.
$description = new external_function_parameters([
'value' => new external_value(PARAM_INT, '', VALUE_REQUIRED, null, NULL_ALLOWED)
]);
$testdata = ['value' => null];
$cleanedvalue = external_api::clean_returnvalue($description, $testdata);
$this->assertSame($testdata, $cleanedvalue);
$testdata = ['value' => 1];
$cleanedvalue = external_api::clean_returnvalue($description, $testdata);
$this->assertSame($testdata, $cleanedvalue);
// Test nullable external_single_structure may optionally return data.
$description = new external_function_parameters([
'value' => new external_single_structure(['value2' => new external_value(PARAM_INT)],
'', VALUE_REQUIRED, null, NULL_ALLOWED)
]);
$testdata = ['value' => null];
$cleanedvalue = external_api::clean_returnvalue($description, $testdata);
$this->assertSame($testdata, $cleanedvalue);
$testdata = ['value' => ['value2' => 1]];
$cleanedvalue = external_api::clean_returnvalue($description, $testdata);
$this->assertSame($testdata, $cleanedvalue);
// Test nullable external_multiple_structure may optionally return data.
$description = new external_function_parameters([
'value' => new external_multiple_structure(
new external_value(PARAM_INT), '', VALUE_REQUIRED, null, NULL_ALLOWED)
]);
$testdata = ['value' => null];
$cleanedvalue = external_api::clean_returnvalue($description, $testdata);
$this->assertSame($testdata, $cleanedvalue);
$testdata = ['value' => [1]];
$cleanedvalue = external_api::clean_returnvalue($description, $testdata);
$this->assertSame($testdata, $cleanedvalue);
}
/**
* Test for clean_returnvalue() for testing that returns the PHP type.
*
* @covers \core_external\external_api::clean_returnvalue
*/
public function test_clean_returnvalue_return_php_type(): void {
$returndesc = new external_single_structure([
'value' => new external_value(PARAM_RAW, 'Some text', VALUE_OPTIONAL, null, NULL_NOT_ALLOWED),
]);
// Check return type on exception because the external values does not allow NULL values.
$testdata = ['value' => null];
try {
$cleanedvalue = external_api::clean_returnvalue($returndesc, $testdata);
} catch (\moodle_exception $e) {
$this->assertInstanceOf(\invalid_response_exception::class, $e);
$this->assertStringContainsString('of PHP type "NULL"', $e->debuginfo);
}
}
/**
* Test for clean_returnvalue().
*
* @covers \core_external\external_api::clean_returnvalue
*/
public function test_clean_returnvalue(): void {
// Build some return value decription.
$returndesc = new external_multiple_structure(
new external_single_structure(
[
'object' => new external_single_structure(
['value1' => new external_value(PARAM_INT, 'this is a int')]),
'value2' => new external_value(PARAM_TEXT, 'some text', VALUE_OPTIONAL),
]
));
// Clean an object (it should be cast into an array).
$object = new \stdClass();
$object->value1 = 1;
$singlestructure['object'] = $object;
$singlestructure['value2'] = 'Some text';
$testdata = [$singlestructure];
$cleanedvalue = external_api::clean_returnvalue($returndesc, $testdata);
$cleanedsinglestructure = array_pop($cleanedvalue);
$this->assertSame($object->value1, $cleanedsinglestructure['object']['value1']);
$this->assertSame($singlestructure['value2'], $cleanedsinglestructure['value2']);
// Missing VALUE_OPTIONAL.
$object = new \stdClass();
$object->value1 = 1;
$singlestructure = new \stdClass();
$singlestructure->object = $object;
$testdata = [$singlestructure];
$cleanedvalue = external_api::clean_returnvalue($returndesc, $testdata);
$cleanedsinglestructure = array_pop($cleanedvalue);
$this->assertSame($object->value1, $cleanedsinglestructure['object']['value1']);
$this->assertArrayNotHasKey('value2', $cleanedsinglestructure);
// Unknown attribute (the value should be ignored).
$object = [];
$object['value1'] = 1;
$singlestructure = [];
$singlestructure['object'] = $object;
$singlestructure['value2'] = 'Some text';
$singlestructure['unknownvalue'] = 'Some text to ignore';
$testdata = [$singlestructure];
$cleanedvalue = external_api::clean_returnvalue($returndesc, $testdata);
$cleanedsinglestructure = array_pop($cleanedvalue);
$this->assertSame($object['value1'], $cleanedsinglestructure['object']['value1']);
$this->assertSame($singlestructure['value2'], $cleanedsinglestructure['value2']);
$this->assertArrayNotHasKey('unknownvalue', $cleanedsinglestructure);
// Missing required value (an exception is thrown).
$object = [];
$singlestructure = [];
$singlestructure['object'] = $object;
$singlestructure['value2'] = 'Some text';
$testdata = [$singlestructure];
try {
external_api::clean_returnvalue($returndesc, $testdata);
$this->fail('Exception expected');
} catch (\moodle_exception $ex) {
$this->assertInstanceOf(\invalid_response_exception::class, $ex);
$this->assertSame('Invalid response value detected (object => Invalid response value detected '
. '(Error in response - Missing following required key in a single structure: value1): Error in response - '
. 'Missing following required key in a single structure: value1)', $ex->getMessage());
}
// Fail if no data provided when value required.
$testdata = null;
try {
external_api::clean_returnvalue($returndesc, $testdata);
$this->fail('Exception expected');
} catch (\moodle_exception $ex) {
$this->assertInstanceOf(\invalid_response_exception::class, $ex);
$this->assertSame('Invalid response value detected (Only arrays accepted. The bad value is: \'\')',
$ex->getMessage());
}
// Test nullable external_multiple_structure may optionally return data.
$returndesc = new external_multiple_structure(
new external_value(PARAM_INT),
'', VALUE_REQUIRED, null, NULL_ALLOWED);
$testdata = null;
$cleanedvalue = external_api::clean_returnvalue($returndesc, $testdata);
$this->assertSame($testdata, $cleanedvalue);
$testdata = [1];
$cleanedvalue = external_api::clean_returnvalue($returndesc, $testdata);
$this->assertSame($testdata, $cleanedvalue);
// Test nullable external_single_structure may optionally return data.
$returndesc = new external_single_structure(['value' => new external_value(PARAM_INT)],
'', VALUE_REQUIRED, null, NULL_ALLOWED);
$testdata = null;
$cleanedvalue = external_api::clean_returnvalue($returndesc, $testdata);
$this->assertSame($testdata, $cleanedvalue);
$testdata = ['value' => 1];
$cleanedvalue = external_api::clean_returnvalue($returndesc, $testdata);
$this->assertSame($testdata, $cleanedvalue);
// Test nullable external_value may optionally return data.
$returndesc = new external_value(PARAM_INT, '', VALUE_REQUIRED, null, NULL_ALLOWED);
$testdata = null;
$cleanedvalue = external_api::clean_returnvalue($returndesc, $testdata);
$this->assertSame($testdata, $cleanedvalue);
$testdata = 1;
$cleanedvalue = external_api::clean_returnvalue($returndesc, $testdata);
$this->assertSame($testdata, $cleanedvalue);
}
/**
* Test \core_external\external_api::get_context_from_params().
*
* @covers \core_external\external_api::get_context_from_params
*/
public function test_get_context_from_params(): void {
$this->resetAfterTest(true);
$course = $this->getDataGenerator()->create_course();
$realcontext = \context_course::instance($course->id);
// Use context id.
$fetchedcontext = $this->get_context_from_params(["contextid" => $realcontext->id]);
$this->assertEquals($realcontext, $fetchedcontext);
// Use context level and instance id.
$fetchedcontext = $this->get_context_from_params(["contextlevel" => "course", "instanceid" => $course->id]);
$this->assertEquals($realcontext, $fetchedcontext);
// Use context level numbers instead of legacy short level names.
$fetchedcontext = $this->get_context_from_params(
["contextlevel" => \core\context\course::LEVEL, "instanceid" => $course->id]);
$this->assertEquals($realcontext, $fetchedcontext);
// Passing empty values.
try {
$fetchedcontext = $this->get_context_from_params(["contextid" => 0]);
$this->fail('Exception expected from get_context_wrapper()');
} catch (\moodle_exception $e) {
$this->assertInstanceOf(\invalid_parameter_exception::class, $e);
}
try {
$fetchedcontext = $this->get_context_from_params(["instanceid" => 0]);
$this->fail('Exception expected from get_context_wrapper()');
} catch (\moodle_exception $e) {
$this->assertInstanceOf(\invalid_parameter_exception::class, $e);
}
try {
$fetchedcontext = $this->get_context_from_params(["contextid" => null]);
$this->fail('Exception expected from get_context_wrapper()');
} catch (\moodle_exception $e) {
$this->assertInstanceOf(\invalid_parameter_exception::class, $e);
}
// Tests for context with instanceid equal to 0 (System context).
$realcontext = \context_system::instance();
$fetchedcontext = $this->get_context_from_params(["contextlevel" => "system", "instanceid" => 0]);
$this->assertEquals($realcontext, $fetchedcontext);
// Passing wrong level name.
try {
$fetchedcontext = $this->get_context_from_params(["contextlevel" => "random", "instanceid" => $course->id]);
$this->fail('exception expected when level name is invalid');
} catch (\moodle_exception $e) {
$this->assertInstanceOf('invalid_parameter_exception', $e);
$this->assertSame('Invalid parameter value detected (Invalid context level = random)', $e->getMessage());
}
// Passing wrong level number.
try {
$fetchedcontext = $this->get_context_from_params(["contextlevel" => -10, "instanceid" => $course->id]);
$this->fail('exception expected when level name is invalid');
} catch (\moodle_exception $e) {
$this->assertInstanceOf('invalid_parameter_exception', $e);
$this->assertSame('Invalid parameter value detected (Invalid context level = -10)', $e->getMessage());
}
}
/**
* Test \core_external\external_api::get_context()_from_params parameter validation.
*
* @covers \core_external\external_api::get_context
*/
public function test_get_context_params(): void {
global $USER;
// Call without correct context details.
$this->expectException('invalid_parameter_exception');
$this->get_context_from_params(['roleid' => 3, 'userid' => $USER->id]);
}
/**
* Test \core_external\external_api::get_context()_from_params parameter validation.
*
* @covers \core_external\external_api::get_context
*/
public function test_get_context_params2(): void {
global $USER;
// Call without correct context details.
$this->expectException('invalid_parameter_exception');
$this->get_context_from_params(['roleid' => 3, 'userid' => $USER->id, 'contextlevel' => "course"]);
}
/**
* Test \core_external\external_api::get_context()_from_params parameter validation.
* @covers \core_external\external_api::get_context
*/
public function test_get_context_params3(): void {
global $USER;
// Call without correct context details.
$this->resetAfterTest(true);
$course = self::getDataGenerator()->create_course();
$this->expectException('invalid_parameter_exception');
$this->get_context_from_params(['roleid' => 3, 'userid' => $USER->id, 'instanceid' => $course->id]);
}
/**
* Data provider for the test_all_external_info test.
*
* @return array
*/
public function all_external_info_provider(): array {
global $DB;
// We are testing here that all the external function descriptions can be generated without
// producing warnings. E.g. misusing optional params will generate a debugging message which
// will fail this test.
$functions = $DB->get_records('external_functions', [], 'name');
$return = [];
foreach ($functions as $f) {
$return[$f->name] = [$f];
}
return $return;
}
/**
* Test \core_external\external_api::external_function_info.
*
* @runInSeparateProcess
* @dataProvider all_external_info_provider
* @covers \core_external\external_api::external_function_info
* @param \stdClass $definition
*/
public function test_all_external_info(\stdClass $definition): void {
$desc = external_api::external_function_info($definition);
$this->assertNotEmpty($desc->name);
$this->assertNotEmpty($desc->classname);
$this->assertNotEmpty($desc->methodname);
$this->assertEquals($desc->component, clean_param($desc->component, PARAM_COMPONENT));
$this->assertInstanceOf(external_function_parameters::class, $desc->parameters_desc);
if ($desc->returns_desc != null) {
$this->assertInstanceOf(external_description::class, $desc->returns_desc);
}
}
/**
* Test the \core_external\external_api::call_external_function() function.
*
* @covers \core_external\external_api::call_external_function
*/
public function test_call_external_function(): void {
global $PAGE, $COURSE, $CFG;
$this->resetAfterTest(true);
// Call some webservice functions and verify they are correctly handling $PAGE and $COURSE.
// First test a function that calls validate_context outside a course.
$this->setAdminUser();
$category = $this->getDataGenerator()->create_category();
$params = [
'contextid' => \context_coursecat::instance($category->id)->id,
'name' => 'aaagrrryyy',
'idnumber' => '',
'description' => '',
];
$cohort1 = $this->getDataGenerator()->create_cohort($params);
$cohort2 = $this->getDataGenerator()->create_cohort();
$beforepage = $PAGE;
$beforecourse = $COURSE;
$params = ['cohortids' => [$cohort1->id, $cohort2->id]];
$result = external_api::call_external_function('core_cohort_get_cohorts', $params);
$this->assertSame($beforepage, $PAGE);
$this->assertSame($beforecourse, $COURSE);
// Now test a function that calls validate_context inside a course.
$course = $this->getDataGenerator()->create_course();
$beforepage = $PAGE;
$beforecourse = $COURSE;
$params = ['courseid' => $course->id, 'options' => []];
$result = external_api::call_external_function('core_enrol_get_enrolled_users', $params);
$this->assertSame($beforepage, $PAGE);
$this->assertSame($beforecourse, $COURSE);
// Test a function that triggers a PHP exception.
require_once($CFG->dirroot . '/lib/tests/fixtures/test_external_function_throwable.php');
// Call our test function.
$result = \test_external_function_throwable::call_external_function('core_throw_exception', [], false);
$this->assertTrue($result['error']);
$this->assertArrayHasKey('exception', $result);
$this->assertEquals($result['exception']->message, 'Exception - Modulo by zero');
}
/**
* Call the get_contect_from_params methods on the api class.
*
* @return mixed
*/
protected function get_context_from_params() {
$rc = new \ReflectionClass(external_api::class);
$method = $rc->getMethod('get_context_from_params');
return $method->invokeArgs(null, func_get_args());
}
}
+280
View File
@@ -0,0 +1,280 @@
<?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 core_external\external_api;
use externallib_advanced_testcase;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/lib/external/externallib.php');
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
/**
* External library functions unit tests
*
* @package core
* @category phpunit
* @copyright 2012 Jerome Mouneyrac
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class external_externallib_test extends externallib_advanced_testcase {
/**
* Test get_string
*/
public function test_get_string(): void {
$this->resetAfterTest(true);
$service = new \stdClass();
$service->name = 'Dummy Service';
$service->id = 12;
// String with two parameters.
$returnedstring = \core_external::get_string('addservice', 'webservice', null,
array(array('name' => 'name', 'value' => $service->name),
array('name' => 'id', 'value' => $service->id)));
// We need to execute the return values cleaning process to simulate the web service server.
$returnedstring = external_api::clean_returnvalue(\core_external::get_string_returns(), $returnedstring);
$corestring = get_string('addservice', 'webservice', $service);
$this->assertSame($corestring, $returnedstring);
// String with one parameter.
$acapname = 'A capability name';
$returnedstring = \core_external::get_string('missingrequiredcapability', 'webservice', null,
array(array('value' => $acapname)));
// We need to execute the return values cleaning process to simulate the web service server.
$returnedstring = external_api::clean_returnvalue(\core_external::get_string_returns(), $returnedstring);
$corestring = get_string('missingrequiredcapability', 'webservice', $acapname);
$this->assertSame($corestring, $returnedstring);
// String without parameters.
$returnedstring = \core_external::get_string('missingpassword', 'webservice');
// We need to execute the return values cleaning process to simulate the web service server.
$returnedstring = external_api::clean_returnvalue(\core_external::get_string_returns(), $returnedstring);
$corestring = get_string('missingpassword', 'webservice');
$this->assertSame($corestring, $returnedstring);
// String with two parameter but one is invalid (not named).
$this->expectException('moodle_exception');
$returnedstring = \core_external::get_string('addservice', 'webservice', null,
array(array('value' => $service->name),
array('name' => 'id', 'value' => $service->id)));
}
/**
* Test get_string with HTML.
*/
public function test_get_string_containing_html(): void {
$result = \core_external::get_string('registrationinfo');
$actual = external_api::clean_returnvalue(\core_external::get_string_returns(), $result);
$expected = get_string('registrationinfo', 'moodle');
$this->assertSame($expected, $actual);
}
/**
* Test get_string with arguments containing HTML.
*/
public function test_get_string_with_args_containing_html(): void {
$result = \core_external::get_string('added', 'moodle', null, [['value' => '<strong>Test</strong>']]);
$actual = external_api::clean_returnvalue(\core_external::get_string_returns(), $result);
$expected = get_string('added', 'moodle', '<strong>Test</strong>');
$this->assertSame($expected, $actual);
}
/**
* Test get_strings
*/
public function test_get_strings(): void {
$this->resetAfterTest(true);
$stringmanager = get_string_manager();
$service = new \stdClass();
$service->name = 'Dummy Service';
$service->id = 12;
$returnedstrings = \core_external::get_strings(
array(
array(
'stringid' => 'addservice', 'component' => 'webservice',
'stringparams' => array(array('name' => 'name', 'value' => $service->name),
array('name' => 'id', 'value' => $service->id)
),
'lang' => 'en'
),
array('stringid' => 'addaservice', 'component' => 'webservice', 'lang' => 'en')
));
// We need to execute the return values cleaning process to simulate the web service server.
$returnedstrings = external_api::clean_returnvalue(\core_external::get_strings_returns(), $returnedstrings);
foreach($returnedstrings as $returnedstring) {
$corestring = $stringmanager->get_string($returnedstring['stringid'],
$returnedstring['component'],
$service,
'en');
$this->assertSame($corestring, $returnedstring['string']);
}
}
/**
* Test get_strings with HTML.
*/
public function test_get_strings_containing_html(): void {
$result = \core_external::get_strings([['stringid' => 'registrationinfo'], ['stringid' => 'loginaspasswordexplain']]);
$actual = external_api::clean_returnvalue(\core_external::get_strings_returns(), $result);
$this->assertSame(get_string('registrationinfo', 'moodle'), $actual[0]['string']);
$this->assertSame(get_string('loginaspasswordexplain', 'moodle'), $actual[1]['string']);
}
/**
* Test get_strings with arguments containing HTML.
*/
public function test_get_strings_with_args_containing_html(): void {
$result = \core_external::get_strings([
['stringid' => 'added', 'stringparams' => [['value' => '<strong>Test</strong>']]],
['stringid' => 'loggedinas', 'stringparams' => [['value' => '<strong>Test</strong>']]]]
);
$actual = external_api::clean_returnvalue(\core_external::get_strings_returns(), $result);
$this->assertSame(get_string('added', 'moodle', '<strong>Test</strong>'), $actual[0]['string']);
$this->assertSame(get_string('loggedinas', 'moodle', '<strong>Test</strong>'), $actual[1]['string']);
}
/**
* Test get_component_strings
*/
public function test_get_component_strings(): void {
global $USER;
$this->resetAfterTest(true);
$stringmanager = get_string_manager();
$wsstrings = $stringmanager->load_component_strings('webservice', current_language());
$componentstrings = \core_external::get_component_strings('webservice');
// We need to execute the return values cleaning process to simulate the web service server.
$componentstrings = external_api::clean_returnvalue(\core_external::get_component_strings_returns(), $componentstrings);
$this->assertEquals(count($componentstrings), count($wsstrings));
foreach($componentstrings as $string) {
$this->assertSame($string['string'], $wsstrings[$string['stringid']]);
}
}
/**
* Test update_inplace_editable()
*/
public function test_update_inplace_editable(): void {
$this->resetAfterTest(true);
// Call service for component that does not have inplace_editable callback.
try {
\core_external::update_inplace_editable('tool_log', 'itemtype', 1, 'newvalue');
$this->fail('Exception expected');
} catch (\moodle_exception $e) {
$this->assertEquals('Error calling update processor', $e->getMessage());
}
// This is a very basic test for the return value of the external function.
// More detailed test for tag updating can be found in core_tag component.
$this->setAdminUser();
$tag = $this->getDataGenerator()->create_tag();
$res = \core_external::update_inplace_editable('core_tag', 'tagname', $tag->id, 'new tag name');
$res = external_api::clean_returnvalue(\core_external::update_inplace_editable_returns(), $res);
$this->assertEquals('new tag name', $res['value']);
}
/**
* Test update_inplace_editable with mathjax.
*/
public function test_update_inplace_editable_with_mathjax(): void {
$this->resetAfterTest(true);
$this->setAdminUser();
// Enable MathJax filter in content and headings.
$this->configure_filters([
['name' => 'mathjaxloader', 'state' => TEXTFILTER_ON, 'move' => -1, 'applytostrings' => true],
]);
// Create a forum.
$course = $this->getDataGenerator()->create_course();
$forum = self::getDataGenerator()->create_module('forum', array('course' => $course->id, 'name' => 'forum name'));
// Change the forum name.
$newname = 'New forum name $$(a+b)=2$$';
$res = \core_external::update_inplace_editable('core_course', 'activityname', $forum->cmid, $newname);
$res = external_api::clean_returnvalue(\core_external::update_inplace_editable_returns(), $res);
// Format original data.
$context = \context_module::instance($forum->cmid);
$newname = \core_external\util::format_string($newname, $context);
$editlabel = get_string('newactivityname', '', $newname);
// Check editlabel is the same and has mathjax.
$this->assertStringContainsString('<span class="filter_mathjaxloader_equation">', $res['editlabel']);
$this->assertEquals($editlabel, $res['editlabel']);
}
public function test_get_user_dates(): void {
$this->resetAfterTest();
$this->setAdminUser();
// Set default timezone to Australia/Perth, else time calculated
// will not match expected values.
$this->setTimezone(99, 'Australia/Perth');
$context = \context_system::instance();
$request = [
[
'timestamp' => 1293876000,
'format' => '%A, %d %B %Y, %I:%M'
],
[
'timestamp' => 1293876000,
'format' => '%d %m %Y'
],
[
'timestamp' => 1293876000,
'format' => '%d %m %Y',
'type' => 'gregorian'
],
[
'timestamp' => 1293876000,
'format' => 'some invalid format'
],
];
$result = \core_external::get_user_dates($context->id, null, null, $request);
$result = external_api::clean_returnvalue(\core_external::get_user_dates_returns(), $result);
$this->assertEquals('Saturday, 1 January 2011, 6:00', $result['dates'][0]);
$this->assertEquals('1 01 2011', $result['dates'][1]);
$this->assertEquals('1 01 2011', $result['dates'][2]);
$this->assertEquals('some invalid format', $result['dates'][3]);
}
}
+51
View File
@@ -0,0 +1,51 @@
<?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;
/**
* Unit tests for core_external\external_files.
*
* @package core_external
* @category test
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
* @covers \core_external\external_files
*/
class external_files_test extends \advanced_testcase {
/**
* Text external files structure.
*
* @covers \core_external\external_files
*/
public function test_files_structure(): void {
$description = new external_files();
// First check that the expected default values and keys are returned.
$expectedkeys = array_flip([
'filename', 'filepath', 'filesize', 'fileurl', 'timemodified', 'mimetype',
'isexternalfile', 'repositorytype', 'icon',
]);
$returnedkeys = array_flip(array_keys($description->content->keys));
$this->assertEquals($expectedkeys, $returnedkeys);
$this->assertEquals('List of files.', $description->desc);
$this->assertEquals(VALUE_REQUIRED, $description->required);
foreach ($description->content->keys as $key) {
$this->assertEquals(VALUE_OPTIONAL, $key->required);
}
}
}
+120
View File
@@ -0,0 +1,120 @@
<?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;
/**
* Unit tests for core_external\external_settings.
*
* @package core_external
* @category test
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
* @covers \core_external\external_settings
*/
class external_settings_test extends \advanced_testcase {
/**
* Reset the singleton between tests.
*/
public function tearDown(): void {
external_settings::reset();
}
/**
* Tests for external_settings class.
*
* @covers \core_external\external_settings::get_instance
*/
public function test_external_settings(): void {
$settings = external_settings::get_instance();
$this->assertInstanceOf(external_settings::class, $settings);
}
/**
* Check external_settings defaults.
*
* @covers \core_external\external_settings::get_instance
*/
public function test_external_settings_defaults(): void {
$settings = external_settings::get_instance();
$currentraw = $settings->get_raw();
$currentfilter = $settings->get_filter();
$currentfile = $settings->get_file();
$currentfileurl = $settings->get_fileurl();
$this->assertInstanceOf(external_settings::class, $settings);
// Check apis.
$settings->set_file('plugin.php');
$this->assertEquals('plugin.php', $settings->get_file());
$settings->set_filter(false);
$this->assertFalse($settings->get_filter());
$settings->set_fileurl(false);
$this->assertFalse($settings->get_fileurl());
$settings->set_raw(true);
$this->assertTrue($settings->get_raw());
}
/**
* Check external_settings file API calls.
*
* @covers \core_external\external_settings::set_file
* @covers \core_external\external_settings::get_file
*/
public function test_external_settings_set_file(): void {
$settings = external_settings::get_instance();
$settings->set_file('plugin.php');
$this->assertEquals('plugin.php', $settings->get_file());
}
/**
* Check external_settings filter API calls.
*
* @covers \core_external\external_settings::set_filter
* @covers \core_external\external_settings::get_filter
*/
public function test_external_settings_set_filter(): void {
$settings = external_settings::get_instance();
$settings->set_filter(false);
$this->assertFalse($settings->get_filter());
}
/**
* Check external_settings file API calls.
*
* @covers \core_external\external_settings::set_fileurl
* @covers \core_external\external_settings::get_fileurl
*/
public function test_external_settings_set_fileurl(): void {
$settings = external_settings::get_instance();
$settings->set_fileurl(false);
$this->assertFalse($settings->get_fileurl());
}
/**
* Check external_settings raw API calls.
*
* @covers \core_external\external_settings::set_raw
* @covers \core_external\external_settings::get_raw
*/
public function test_external_settings_set_raw(): void {
$settings = external_settings::get_instance();
$settings->set_raw(true);
$this->assertTrue($settings->get_raw());
}
}
+67
View File
@@ -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/>.
namespace core_external;
use advanced_testcase;
/**
* Unit tests for core_external\external_description.
*
* @package core
* @category test
* @copyright 2023 Jun Pataleta
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass external_value
*/
class external_value_test extends advanced_testcase {
/**
* Data provider for the required param test.
*
* @return array[]
*/
public function required_param_provider(): array {
return [
[ VALUE_DEFAULT, false ],
[ VALUE_REQUIRED, false ],
[ VALUE_OPTIONAL, false ],
[ 'aaa', true, 'aaa' ],
[ [VALUE_OPTIONAL], true, 'Array: ' . VALUE_OPTIONAL ],
[ -1000, true, -1000 ],
];
}
/**
* Tests the constructor for the $required parameter validation.
*
* @dataProvider required_param_provider
* @param int $required The required param being tested.
* @param bool $debuggingexpected Whether debugging is expected.
* @param mixed $requiredstr The string value of the $required param in the debugging message.
* @return void
*/
public function test_required_param_validation($required, $debuggingexpected, $requiredstr = ''): void {
$externalvalue = new external_value(PARAM_INT, 'Cool description', $required);
if ($debuggingexpected) {
$this->assertDebuggingCalled("Invalid \$required parameter value: '{$requiredstr}' .
It must be either VALUE_DEFAULT, VALUE_REQUIRED, or VALUE_OPTIONAL", DEBUG_DEVELOPER);
}
$this->assertEquals(PARAM_INT, $externalvalue->type);
$this->assertEquals('Cool description', $externalvalue->desc);
$this->assertEquals($required, $externalvalue->required);
}
}
+512
View File
@@ -0,0 +1,512 @@
<?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/>.
/**
* Data provider tests.
*
* @package core_external
* @category test
* @copyright 2018 Frédéric Massart
* @author Frédéric Massart <fred@branchup.tech>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_external\privacy;
use core_external\privacy\provider;
use core_privacy\local\request\approved_contextlist;
use core_privacy\local\request\approved_userlist;
use core_privacy\local\request\transform;
use core_privacy\local\request\writer;
use core_privacy\tests\provider_testcase;
/**
* External subsytem testcase class.
*
* @package core_external
* @category test
* @copyright 2018 Frédéric Massart
* @author Frédéric Massart <fred@branchup.tech>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider_test extends provider_testcase {
public function setUp(): void {
$this->resetAfterTest();
}
/**
* Test the external service get_contexts_for_userid function.
*
* @covers \core_external\privacy\provider::get_contexts_for_userid
*/
public function test_get_contexts_for_userid(): void {
$dg = $this->getDataGenerator();
$u1 = $dg->create_user();
$u2 = $dg->create_user();
$u3 = $dg->create_user();
$u4 = $dg->create_user();
$u5 = $dg->create_user();
$u1ctx = \context_user::instance($u1->id);
$u2ctx = \context_user::instance($u2->id);
$u3ctx = \context_user::instance($u3->id);
$u5ctx = \context_user::instance($u5->id);
$s = $this->create_service();
$this->create_token(['userid' => $u1->id]);
$this->create_token(['userid' => $u1->id]);
$this->create_token(['userid' => $u2->id, 'creatorid' => $u3->id]);
$this->create_service_user(['externalserviceid' => $s->id, 'userid' => $u5->id]);
$contextids = provider::get_contexts_for_userid($u1->id)->get_contextids();
$this->assertCount(1, $contextids);
$this->assertTrue(in_array($u1ctx->id, $contextids));
$contextids = provider::get_contexts_for_userid($u2->id)->get_contextids();
$this->assertCount(1, $contextids);
$this->assertTrue(in_array($u2ctx->id, $contextids));
$contextids = provider::get_contexts_for_userid($u3->id)->get_contextids();
$this->assertCount(1, $contextids);
$this->assertTrue(in_array($u2ctx->id, $contextids));
$contextids = provider::get_contexts_for_userid($u4->id)->get_contextids();
$this->assertCount(0, $contextids);
$contextids = provider::get_contexts_for_userid($u5->id)->get_contextids();
$this->assertCount(1, $contextids);
$this->assertTrue(in_array($u5ctx->id, $contextids));
}
/**
* Test delete_data_for_user
*
* @covers \core_external\privacy\provider::delete_data_for_user
*/
public function test_delete_data_for_user(): void {
global $DB;
$dg = $this->getDataGenerator();
$u1 = $dg->create_user();
$u2 = $dg->create_user();
$u1ctx = \context_user::instance($u1->id);
$u2ctx = \context_user::instance($u2->id);
$s = $this->create_service();
$this->create_token(['userid' => $u1->id, 'creatorid' => $u2->id]);
$this->create_token(['userid' => $u1->id]);
$this->create_token(['userid' => $u2->id]);
$this->create_service_user(['externalserviceid' => $s->id, 'userid' => $u1->id]);
$this->create_service_user(['externalserviceid' => $s->id, 'userid' => $u2->id]);
$this->assertEquals(2, $DB->count_records('external_tokens', ['userid' => $u1->id]));
$this->assertEquals(1, $DB->count_records('external_tokens', ['userid' => $u2->id]));
$this->assertTrue($DB->record_exists('external_services_users', ['userid' => $u1->id]));
$this->assertTrue($DB->record_exists('external_services_users', ['userid' => $u2->id]));
// Delete in another context, nothing happens.
provider::delete_data_for_user(new approved_contextlist($u2, 'core_external', [$u1ctx->id]));
$this->assertEquals(2, $DB->count_records('external_tokens', ['userid' => $u1->id]));
$this->assertEquals(1, $DB->count_records('external_tokens', ['userid' => $u2->id]));
$this->assertTrue($DB->record_exists('external_services_users', ['userid' => $u1->id]));
$this->assertTrue($DB->record_exists('external_services_users', ['userid' => $u2->id]));
// Delete in my context.
provider::delete_data_for_user(new approved_contextlist($u2, 'core_external', [$u2ctx->id]));
$this->assertEquals(2, $DB->count_records('external_tokens', ['userid' => $u1->id]));
$this->assertEquals(0, $DB->count_records('external_tokens', ['userid' => $u2->id]));
$this->assertTrue($DB->record_exists('external_services_users', ['userid' => $u1->id]));
$this->assertFalse($DB->record_exists('external_services_users', ['userid' => $u2->id]));
}
/**
* Test delete_data_for_all_users_in_context
*
* @covers \core_external\privacy\provider::delete_data_for_all_users_in_context
*/
public function test_delete_data_for_all_users_in_context(): void {
global $DB;
$dg = $this->getDataGenerator();
$u1 = $dg->create_user();
$u2 = $dg->create_user();
$u1ctx = \context_user::instance($u1->id);
$u2ctx = \context_user::instance($u2->id);
$s = $this->create_service();
$this->create_token(['userid' => $u1->id, 'creatorid' => $u2->id]);
$this->create_token(['userid' => $u1->id]);
$this->create_token(['userid' => $u2->id]);
$this->create_service_user(['externalserviceid' => $s->id, 'userid' => $u1->id]);
$this->create_service_user(['externalserviceid' => $s->id, 'userid' => $u2->id]);
$this->assertEquals(2, $DB->count_records('external_tokens', ['userid' => $u1->id]));
$this->assertEquals(1, $DB->count_records('external_tokens', ['userid' => $u2->id]));
$this->assertTrue($DB->record_exists('external_services_users', ['userid' => $u1->id]));
$this->assertTrue($DB->record_exists('external_services_users', ['userid' => $u2->id]));
provider::delete_data_for_all_users_in_context($u2ctx);
$this->assertEquals(2, $DB->count_records('external_tokens', ['userid' => $u1->id]));
$this->assertEquals(0, $DB->count_records('external_tokens', ['userid' => $u2->id]));
$this->assertTrue($DB->record_exists('external_services_users', ['userid' => $u1->id]));
$this->assertFalse($DB->record_exists('external_services_users', ['userid' => $u2->id]));
provider::delete_data_for_all_users_in_context($u1ctx);
$this->assertEquals(0, $DB->count_records('external_tokens', ['userid' => $u1->id]));
$this->assertEquals(0, $DB->count_records('external_tokens', ['userid' => $u2->id]));
$this->assertFalse($DB->record_exists('external_services_users', ['userid' => $u1->id]));
$this->assertFalse($DB->record_exists('external_services_users', ['userid' => $u2->id]));
}
/**
* Test the export_user_data function.
* @covers \core_external\privacy\provider::export_user_data
*/
public function test_export_data_for_user(): void {
global $DB;
$dg = $this->getDataGenerator();
$u1 = $dg->create_user();
$u2 = $dg->create_user();
$u1ctx = \context_user::instance($u1->id);
$u2ctx = \context_user::instance($u2->id);
$path = [get_string('services', 'core_external')];
$yearago = time() - YEARSECS;
$hourago = time() - HOURSECS;
$s = $this->create_service(['name' => 'Party time!']);
$this->create_token(['userid' => $u1->id, 'timecreated' => $yearago]);
$this->create_token([
'userid' => $u1->id,
'creatorid' => $u2->id,
'iprestriction' => '127.0.0.1',
'lastaccess' => $hourago,
]);
$this->create_token([
'userid' => $u2->id,
'iprestriction' => '192.168.1.0/24',
'lastaccess' => $yearago,
'externalserviceid' => $s->id,
]);
$this->create_service_user(['externalserviceid' => $s->id, 'userid' => $u2->id]);
// User 1 exporting user 2 context does not give anything.
writer::reset();
provider::export_user_data(new approved_contextlist($u1, 'core_external', [$u2ctx->id]));
$data = writer::with_context($u1ctx)->get_data($path);
$this->assertEmpty($data);
$data = writer::with_context($u1ctx)->get_related_data($path, 'created_by_you');
$this->assertEmpty($data);
$data = writer::with_context($u2ctx)->get_data($path);
$this->assertEmpty($data);
$data = writer::with_context($u2ctx)->get_related_data($path, 'created_by_you');
$this->assertEmpty($data);
// User 1 exporting their context.
writer::reset();
provider::export_user_data(new approved_contextlist($u1, 'core_external', [$u1ctx->id, $u2ctx->id]));
$data = writer::with_context($u1ctx)->get_data($path);
$this->assertFalse(isset($data->services_user));
$this->assertCount(2, $data->tokens);
$this->assertEquals(transform::datetime($yearago), $data->tokens[0]['created_on']);
$this->assertEquals(null, $data->tokens[0]['ip_restriction']);
$this->assertEquals(transform::datetime($hourago), $data->tokens[1]['last_access']);
$this->assertEquals('127.0.0.1', $data->tokens[1]['ip_restriction']);
$data = writer::with_context($u1ctx)->get_related_data($path, 'created_by_you');
$this->assertEmpty($data);
$data = writer::with_context($u2ctx)->get_data($path);
$this->assertEmpty($data);
$data = writer::with_context($u2ctx)->get_related_data($path, 'created_by_you');
$this->assertEmpty($data);
// User 2 exporting their context.
writer::reset();
provider::export_user_data(new approved_contextlist($u2, 'core_external', [$u1ctx->id, $u2ctx->id]));
$data = writer::with_context($u2ctx)->get_data($path);
$this->assertCount(1, $data->tokens);
$this->assertEquals('Party time!', $data->tokens[0]['external_service']);
$this->assertEquals(transform::datetime($yearago), $data->tokens[0]['last_access']);
$this->assertEquals('192.168.1.0/24', $data->tokens[0]['ip_restriction']);
$this->assertCount(1, $data->services_user);
$this->assertEquals('Party time!', $data->services_user[0]['external_service']);
$data = writer::with_context($u1ctx)->get_related_data($path, 'created_by_you');
$this->assertCount(1, $data->tokens);
$this->assertEquals(transform::datetime($hourago), $data->tokens[0]['last_access']);
$this->assertEquals('127.0.0.1', $data->tokens[0]['ip_restriction']);
$data = writer::with_context($u1ctx)->get_data($path);
$this->assertEmpty($data);
$data = writer::with_context($u2ctx)->get_related_data($path, 'created_by_you');
$this->assertEmpty($data);
}
/**
* Test that only users with a user context are fetched.
*
* @covers \core_external\privacy\provider::get_users_in_context
*/
public function test_get_users_in_context(): void {
$component = 'core_external';
// Create user u1.
$u1 = $this->getDataGenerator()->create_user();
$u1ctx = \context_user::instance($u1->id);
// Create user u2.
$u2 = $this->getDataGenerator()->create_user();
$u2ctx = \context_user::instance($u2->id);
// Create user u3.
$u3 = $this->getDataGenerator()->create_user();
$u3ctx = \context_user::instance($u3->id);
// Create user u4.
$u4 = $this->getDataGenerator()->create_user();
$u4ctx = \context_user::instance($u4->id);
// Create user u5.
$u5 = $this->getDataGenerator()->create_user();
$u5ctx = \context_user::instance($u5->id);
// The lists of users for each user context ($u1ctx, $u2ctx, etc.) should be empty.
// Related user data have not been created yet.
$userlist1 = new \core_privacy\local\request\userlist($u1ctx, $component);
provider::get_users_in_context($userlist1);
$this->assertCount(0, $userlist1);
$userlist2 = new \core_privacy\local\request\userlist($u2ctx, $component);
provider::get_users_in_context($userlist2);
$this->assertCount(0, $userlist2);
$userlist3 = new \core_privacy\local\request\userlist($u3ctx, $component);
provider::get_users_in_context($userlist3);
$this->assertCount(0, $userlist3);
$userlist4 = new \core_privacy\local\request\userlist($u4ctx, $component);
provider::get_users_in_context($userlist4);
$this->assertCount(0, $userlist4);
$userlist5 = new \core_privacy\local\request\userlist($u5ctx, $component);
provider::get_users_in_context($userlist5);
$this->assertCount(0, $userlist5);
// Create a service.
$s = $this->create_service();
// Create a ws token for u1.
$this->create_token(['userid' => $u1->id]);
// Create a ws token for u2, and u3 as the creator of the token.
$this->create_token(['userid' => $u2->id, 'creatorid' => $u3->id]);
// Create a service user (u4).
$this->create_service_user(['externalserviceid' => $s->id, 'userid' => $u4->id]);
// The list of users for userlist1 should return one user (u1).
provider::get_users_in_context($userlist1);
$this->assertCount(1, $userlist1);
$expected = [$u1->id];
$actual = $userlist1->get_userids();
$this->assertEquals($expected, $actual);
// The list of users for userlist2 should return one user (u2).
provider::get_users_in_context($userlist2);
$this->assertCount(1, $userlist2);
$expected = [$u2->id];
$actual = $userlist2->get_userids();
$this->assertEquals($expected, $actual);
// The list of users for userlist3 should return one user (u3).
provider::get_users_in_context($userlist3);
$this->assertCount(1, $userlist3);
$expected = [$u3->id];
$actual = $userlist3->get_userids();
$this->assertEquals($expected, $actual);
// The list of users for userlist4 should return one user (u4).
provider::get_users_in_context($userlist4);
$this->assertCount(1, $userlist4);
$expected = [$u4->id];
$actual = $userlist4->get_userids();
$this->assertEquals($expected, $actual);
// The list of users for userlist5 should not return any users.
provider::get_users_in_context($userlist5);
$this->assertCount(0, $userlist5);
// The list of users should only return users in the user context.
$systemcontext = \context_system::instance();
$userlist6 = new \core_privacy\local\request\userlist($systemcontext, $component);
provider::get_users_in_context($userlist6);
$this->assertCount(0, $userlist6);
}
/**
* Test that data for users in approved userlist is deleted.
*
* @covers \core_external\privacy\provider::delete_data_for_users
*/
public function test_delete_data_for_users(): void {
$component = 'core_external';
// Create user u1.
$u1 = $this->getDataGenerator()->create_user();
$u1ctx = \context_user::instance($u1->id);
// Create user u2.
$u2 = $this->getDataGenerator()->create_user();
$u2ctx = \context_user::instance($u2->id);
// Create user u3.
$u3 = $this->getDataGenerator()->create_user();
$u3ctx = \context_user::instance($u3->id);
// Create user u4.
$u4 = $this->getDataGenerator()->create_user();
$u4ctx = \context_user::instance($u4->id);
// Create user u5.
$u5 = $this->getDataGenerator()->create_user();
$u5ctx = \context_user::instance($u5->id);
// Create a service.
$s = $this->create_service();
// Create a ws token for u1.
$this->create_token(['userid' => $u1->id]);
// Create a ws token for u2, and u3 as the creator of the token.
$this->create_token(['userid' => $u2->id, 'creatorid' => $u3->id]);
// Create a service user (u4).
$this->create_service_user(['externalserviceid' => $s->id, 'userid' => $u4->id]);
// Create a service user (u5).
$this->create_service_user(['externalserviceid' => $s->id, 'userid' => $u5->id]);
// The list of users for u1ctx should return one user (u1).
$userlist1 = new \core_privacy\local\request\userlist($u1ctx, $component);
provider::get_users_in_context($userlist1);
$this->assertCount(1, $userlist1);
// The list of users for u2ctx should return one user (u2).
$userlist2 = new \core_privacy\local\request\userlist($u2ctx, $component);
provider::get_users_in_context($userlist2);
$this->assertCount(1, $userlist2);
// The list of users for u3ctx should return one user (u3).
$userlist3 = new \core_privacy\local\request\userlist($u3ctx, $component);
provider::get_users_in_context($userlist3);
$this->assertCount(1, $userlist3);
// The list of users for u4ctx should return one user (u4).
$userlist4 = new \core_privacy\local\request\userlist($u4ctx, $component);
provider::get_users_in_context($userlist4);
$this->assertCount(1, $userlist4);
$approvedlist = new approved_userlist($u1ctx, $component, $userlist1->get_userids());
// Delete using delete_data_for_user.
provider::delete_data_for_users($approvedlist);
// Re-fetch users in u1ctx - the user data should now be empty.
$userlist1 = new \core_privacy\local\request\userlist($u1ctx, $component);
provider::get_users_in_context($userlist1);
$this->assertCount(0, $userlist1);
$approvedlist = new approved_userlist($u2ctx, $component, $userlist2->get_userids());
// Delete using delete_data_for_user.
provider::delete_data_for_users($approvedlist);
// Re-fetch users in u2ctx - the user data should now be empty.
$userlist2 = new \core_privacy\local\request\userlist($u2ctx, $component);
provider::get_users_in_context($userlist2);
$this->assertCount(0, $userlist2);
$approvedlist = new approved_userlist($u3ctx, $component, $userlist3->get_userids());
// Delete using delete_data_for_user.
provider::delete_data_for_users($approvedlist);
// Re-fetch users in u3ctx - the user data should now be empty.
$userlist3 = new \core_privacy\local\request\userlist($u3ctx, $component);
provider::get_users_in_context($userlist3);
$this->assertCount(0, $userlist3);
$approvedlist = new approved_userlist($u4ctx, $component, $userlist3->get_userids());
// Delete using delete_data_for_user.
provider::delete_data_for_users($approvedlist);
// Re-fetch users in u4ctx - the user data should now be empty.
$userlist4 = new \core_privacy\local\request\userlist($u4ctx, $component);
provider::get_users_in_context($userlist4);
$this->assertCount(0, $userlist4);
// The list of users for u5ctx should still return one user (u5).
$userlist5 = new \core_privacy\local\request\userlist($u5ctx, $component);
provider::get_users_in_context($userlist5);
$this->assertCount(1, $userlist5);
// User data should only be removed in the user context.
$systemcontext = \context_system::instance();
$approvedlist = new approved_userlist($systemcontext, $component, $userlist5->get_userids());
// Delete using delete_data_for_user.
provider::delete_data_for_users($approvedlist);
// Re-fetch users in u5ctx - the user data should still be present.
$userlist5 = new \core_privacy\local\request\userlist($u5ctx, $component);
provider::get_users_in_context($userlist5);
$this->assertCount(1, $userlist5);
}
/**
* Create a service.
*
* @param array $params The params.
* @return \stdClass
*/
protected function create_service(array $params = []) {
global $DB;
static $i = 0;
$record = (object) array_merge([
'name' => 'Some service',
'enabled' => '1',
'requiredcapability' => '',
'restrictedusers' => '0',
'component' => 'core_external',
'timecreated' => time(),
'timemodified' => time(),
'shortname' => 'service' . $i,
'downloadfiles' => '1',
'uploadfiles' => '1',
], $params);
$record->id = $DB->insert_record('external_services', $record);
return $record;
}
/**
* Create a service user.
*
* @param array $params The params.
* @return \stdClass
*/
protected function create_service_user(array $params) {
global $DB, $USER;
static $i = 0;
$record = (object) array_merge([
'externalserviceid' => null,
'userid' => $USER->id,
'validuntil' => time() + YEARSECS,
'iprestriction' => '',
'timecreated' => time(),
], $params);
$record->id = $DB->insert_record('external_services_users', $record);
return $record;
}
/**
* Create a token.
*
* @param array $params The params.
* @return stdClass
*/
protected function create_token(array $params) {
global $DB, $USER;
$service = $DB->get_record('external_services', ['shortname' => MOODLE_OFFICIAL_MOBILE_SERVICE]);
$record = (object) array_merge([
'token' => random_string(64),
'privatetoken' => random_string(64),
'tokentype' => EXTERNAL_TOKEN_PERMANENT,
'contextid' => SYSCONTEXTID,
'externalserviceid' => $service->id,
'userid' => $USER->id,
'validuntil' => time() + YEARSECS,
'iprestriction' => null,
'sid' => null,
'timecreated' => time(),
'lastaccess' => time(),
'creatorid' => $USER->id,
], $params);
$record->id = $DB->insert_record('external_tokens', $record);
return $record;
}
}
+415
View File
@@ -0,0 +1,415 @@
<?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;
/**
* Unit tests for core_external\util.
*
* @package core_external
* @category test
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
* @covers \core_external\util
*/
class util_test extends \advanced_testcase {
/** @var \moodle_database The database connection */
protected $db;
/**
* Store the global DB for restore between tests.
*/
public function setUp(): void {
global $DB;
$this->db = $DB;
external_settings::reset();
}
/**
* A helper to include the legacy external functions.
*/
protected function include_legacy_functions(): void {
global $CFG;
$this->assertTrue(
$this->isInIsolation(),
'Inclusion of the legacy test functions requires the test to be run in isolation.',
);
// Note: This is retained for testing of the old functions.
require_once("{$CFG->libdir}/externallib.php");
}
/**
* Reset the global DB between tests.
*/
public function tearDown(): void {
global $DB;
if ($this->db !== null) {
$DB = $this->db;
}
external_settings::reset();
}
/**
* Validate courses, but still return courses even if they fail validation.
*
* @covers \core_external\util::validate_courses
*/
public function test_validate_courses_keepfails(): void {
$this->resetAfterTest(true);
$c1 = $this->getDataGenerator()->create_course();
$c2 = $this->getDataGenerator()->create_course();
$c3 = $this->getDataGenerator()->create_course();
$u1 = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($u1->id, $c1->id);
$courseids = [$c1->id, $c2->id, $c3->id];
$this->setUser($u1);
[$courses, $warnings] = util::validate_courses($courseids, [], false, true);
$this->assertCount(2, $warnings);
$this->assertEquals($c2->id, $warnings[0]['itemid']);
$this->assertEquals($c3->id, $warnings[1]['itemid']);
$this->assertCount(3, $courses);
$this->assertTrue($courses[$c1->id]->contextvalidated);
$this->assertFalse($courses[$c2->id]->contextvalidated);
$this->assertFalse($courses[$c3->id]->contextvalidated);
}
/**
* Validate courses can re-use an array of prefetched courses.
*
* @covers \core_external\util::validate_courses
*/
public function test_validate_courses_prefetch(): void {
$this->resetAfterTest(true);
$c1 = $this->getDataGenerator()->create_course();
$c2 = $this->getDataGenerator()->create_course();
$c3 = $this->getDataGenerator()->create_course();
$c4 = $this->getDataGenerator()->create_course();
$u1 = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($u1->id, $c1->id);
$this->getDataGenerator()->enrol_user($u1->id, $c2->id);
$courseids = [$c1->id, $c2->id, $c3->id];
$courses = [$c2->id => $c2, $c3->id => $c3, $c4->id => $c4];
$this->setUser($u1);
[$courses, $warnings] = util::validate_courses($courseids, $courses);
$this->assertCount(2, $courses);
$this->assertCount(1, $warnings);
$this->assertArrayHasKey($c1->id, $courses);
$this->assertSame($c2, $courses[$c2->id]);
$this->assertArrayNotHasKey($c3->id, $courses);
// The extra course passed is not returned.
$this->assertArrayNotHasKey($c4->id, $courses);
}
/**
* Test the Validate courses standard functionality.
*
* @covers \core_external\util::validate_courses
*/
public function test_validate_courses(): void {
$this->resetAfterTest(true);
$c1 = $this->getDataGenerator()->create_course();
$c2 = $this->getDataGenerator()->create_course();
$c3 = $this->getDataGenerator()->create_course();
$u1 = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($u1->id, $c1->id);
$courseids = [$c1->id, $c2->id, $c3->id];
$this->setAdminUser();
[$courses, $warnings] = util::validate_courses($courseids);
$this->assertEmpty($warnings);
$this->assertCount(3, $courses);
$this->assertArrayHasKey($c1->id, $courses);
$this->assertArrayHasKey($c2->id, $courses);
$this->assertArrayHasKey($c3->id, $courses);
$this->assertEquals($c1->id, $courses[$c1->id]->id);
$this->assertEquals($c2->id, $courses[$c2->id]->id);
$this->assertEquals($c3->id, $courses[$c3->id]->id);
$this->setUser($u1);
[$courses, $warnings] = util::validate_courses($courseids);
$this->assertCount(2, $warnings);
$this->assertEquals($c2->id, $warnings[0]['itemid']);
$this->assertEquals($c3->id, $warnings[1]['itemid']);
$this->assertCount(1, $courses);
$this->assertArrayHasKey($c1->id, $courses);
$this->assertArrayNotHasKey($c2->id, $courses);
$this->assertArrayNotHasKey($c3->id, $courses);
$this->assertEquals($c1->id, $courses[$c1->id]->id);
}
/**
* Text util::get_area_files
*
* @covers \core_external\util::get_area_files
*/
public function test_get_area_files(): void {
global $CFG, $DB;
$this->db = $DB;
$DB = $this->getMockBuilder('moodle_database')->getMock();
$content = base64_encode("Let us create a nice simple file.");
$timemodified = 102030405;
$itemid = 42;
$filesize = strlen($content);
$DB->method('get_records_sql')->willReturn([
(object) [
'filename' => 'example.txt',
'filepath' => '/',
'mimetype' => 'text/plain',
'filesize' => $filesize,
'timemodified' => $timemodified,
'itemid' => $itemid,
'pathnamehash' => sha1('/example.txt'),
],
]);
$component = 'mod_foo';
$filearea = 'area';
$context = 12345;
$expectedfiles = [[
'filename' => 'example.txt',
'filepath' => '/',
'fileurl' => "{$CFG->wwwroot}/webservice/pluginfile.php/{$context}/{$component}/{$filearea}/{$itemid}/example.txt",
'timemodified' => $timemodified,
'filesize' => $filesize,
'mimetype' => 'text/plain',
'isexternalfile' => false,
'icon' => 'f/text',
],
];
// Get all the files for the area.
$files = util::get_area_files($context, $component, $filearea, false);
$this->assertEquals($expectedfiles, $files);
$DB->method('get_in_or_equal')->willReturn([
'= :mock1',
['mock1' => $itemid],
]);
// Get just the file indicated by $itemid.
$files = util::get_area_files($context, $component, $filearea, $itemid);
$this->assertEquals($expectedfiles, $files);
}
/**
* Test default time for user created tokens.
*
* @covers \core_external\util::generate_token_for_current_user
*/
public function test_user_created_tokens_duration(): void {
global $CFG, $DB;
$this->resetAfterTest(true);
$CFG->enablewebservices = 1;
$CFG->enablemobilewebservice = 1;
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
$service = $DB->get_record('external_services', ['shortname' => MOODLE_OFFICIAL_MOBILE_SERVICE, 'enabled' => 1]);
$this->setUser($user1);
$timenow = time();
$token = util::generate_token_for_current_user($service);
$this->assertGreaterThanOrEqual($timenow + $CFG->tokenduration, $token->validuntil);
// Change token default time.
$this->setUser($user2);
set_config('tokenduration', DAYSECS);
$token = util::generate_token_for_current_user($service);
$timenow = time();
$this->assertLessThanOrEqual($timenow + DAYSECS, $token->validuntil);
}
/**
* Test the format_text function.
*
* @covers \core_external\util::format_text
* @runInSeparateProcess
*/
public function test_format_text(): void {
$this->include_legacy_functions();
$settings = external_settings::get_instance();
$settings->set_raw(true);
$settings->set_filter(false);
$context = \context_system::instance();
$test = '$$ \pi $$';
$testformat = FORMAT_MARKDOWN;
$correct = [$test, $testformat];
$this->assertSame($correct, util::format_text($test, $testformat, $context, 'core', '', 0));
// Function external_format_text should work with context id or context instance.
$this->assertSame(external_format_text($test, $testformat, $context->id, 'core', '', 0), $correct);
$this->assertSame(external_format_text($test, $testformat, $context, 'core', '', 0), $correct);
$settings->set_raw(false);
$settings->set_filter(true);
$test = '$$ \pi $$';
$testformat = FORMAT_MARKDOWN;
$correct = ['<span class="filter_mathjaxloader_equation"><p><span class="nolink">$$ \pi $$</span></p>
</span>', FORMAT_HTML,
];
$this->assertSame(util::format_text($test, $testformat, $context, 'core', '', 0), $correct);
// Function external_format_text should work with context id or context instance.
$this->assertSame(external_format_text($test, $testformat, $context->id, 'core', '', 0), $correct);
$this->assertSame(external_format_text($test, $testformat, $context, 'core', '', 0), $correct);
// Filters can be opted out from by the developer.
$test = '$$ \pi $$';
$testformat = FORMAT_MARKDOWN;
$correct = ['<p>$$ \pi $$</p>
', FORMAT_HTML,
];
$this->assertSame(util::format_text($test, $testformat, $context, 'core', '', 0, ['filter' => false]), $correct);
// Function external_format_text should work with context id or context instance.
$this->assertSame(external_format_text($test, $testformat, $context->id, 'core', '', 0, ['filter' => false]), $correct);
$this->assertSame(external_format_text($test, $testformat, $context, 'core', '', 0, ['filter' => false]), $correct);
$test = '<p><a id="test"></a><a href="#test">Text</a></p>';
$testformat = FORMAT_HTML;
$correct = [$test, FORMAT_HTML];
$options = ['allowid' => true];
$this->assertSame(util::format_text($test, $testformat, $context, 'core', '', 0, $options), $correct);
// Function external_format_text should work with context id or context instance.
$this->assertSame(external_format_text($test, $testformat, $context->id, 'core', '', 0, $options), $correct);
$this->assertSame(external_format_text($test, $testformat, $context, 'core', '', 0, $options), $correct);
$test = '<p><a id="test"></a><a href="#test">Text</a></p>';
$testformat = FORMAT_HTML;
$correct = ['<p><a></a><a href="#test">Text</a></p>', FORMAT_HTML];
$options = new \stdClass();
$options->allowid = false;
$this->assertSame(util::format_text($test, $testformat, $context, 'core', '', 0, $options), $correct);
// Function external_format_text should work with context id or context instance.
$this->assertSame(external_format_text($test, $testformat, $context->id, 'core', '', 0, $options), $correct);
$this->assertSame(external_format_text($test, $testformat, $context, 'core', '', 0, $options), $correct);
$test = '<p><a id="test"></a><a href="#test">Text</a></p>' . "\n" . 'Newline';
$testformat = FORMAT_MOODLE;
$correct = ['<p><a id="test"></a><a href="#test">Text</a></p> Newline', FORMAT_HTML];
$options = new \stdClass();
$options->newlines = false;
$this->assertSame(util::format_text($test, $testformat, $context, 'core', '', 0, $options), $correct);
// Function external_format_text should work with context id or context instance.
$this->assertSame(external_format_text($test, $testformat, $context->id, 'core', '', 0, $options), $correct);
$this->assertSame(external_format_text($test, $testformat, $context, 'core', '', 0, $options), $correct);
$test = '<p><a id="test"></a><a href="#test">Text</a></p>';
$testformat = FORMAT_MOODLE;
$correct = ['<div class="text_to_html">' . $test . '</div>', FORMAT_HTML];
$options = new \stdClass();
$options->para = true;
$this->assertSame(util::format_text($test, $testformat, $context, 'core', '', 0, $options), $correct);
// Function external_format_text should work with context id or context instance.
$this->assertSame(external_format_text($test, $testformat, $context->id, 'core', '', 0, $options), $correct);
$this->assertSame(external_format_text($test, $testformat, $context, 'core', '', 0, $options), $correct);
$test = '<p><a id="test"></a><a href="#test">Text</a></p>';
$testformat = FORMAT_MOODLE;
$correct = [$test, FORMAT_HTML];
$options = new \stdClass();
$options->context = $context;
$this->assertSame(util::format_text($test, $testformat, $context, 'core', '', 0, $options), $correct);
// Function external_format_text should work with context id or context instance.
$this->assertSame(external_format_text($test, $testformat, $context->id, 'core', '', 0, $options), $correct);
$this->assertSame(external_format_text($test, $testformat, $context, 'core', '', 0, $options), $correct);
}
/**
* Teset the format_string function.
*
* @covers \core_external\util::format_string
* @runInSeparateProcess
*/
public function test_external_format_string(): void {
$this->resetAfterTest();
$this->include_legacy_functions();
$settings = external_settings::get_instance();
// Enable multilang filter to on content and heading.
filter_set_global_state('multilang', TEXTFILTER_ON);
filter_set_applies_to_strings('multilang', 1);
$filtermanager = \filter_manager::instance();
$filtermanager->reset_caches();
$settings->set_raw(true);
$settings->set_filter(true);
$context = \context_system::instance();
$test = '<span lang="en" class="multilang">EN</span><span lang="fr" class="multilang">FR</span> ';
$test .= '<script>hi</script> <h3>there</h3>!';
$correct = $test;
$this->assertSame($correct, util::format_string($test, $context));
// Function external_format_string should work with context id or context instance.
$this->assertSame($correct, external_format_string($test, $context));
$this->assertSame($correct, external_format_string($test, $context->id));
$settings->set_raw(false);
$settings->set_filter(false);
$test = '<span lang="en" class="multilang">EN</span><span lang="fr" class="multilang">FR</span> ';
$test .= '<script>hi</script> <h3>there</h3>?';
$correct = 'ENFR hi there?';
$this->assertSame($correct, util::format_string($test, $context));
// Function external_format_string should work with context id or context instance.
$this->assertSame($correct, external_format_string($test, $context));
$this->assertSame($correct, external_format_string($test, $context->id));
$settings->set_filter(true);
$test = '<span lang="en" class="multilang">EN</span><span lang="fr" class="multilang">FR</span> ';
$test .= '<script>hi</script> <h3>there</h3>@';
$correct = 'EN hi there@';
$this->assertSame($correct, util::format_string($test, $context));
// Function external_format_string should work with context id or context instance.
$this->assertSame($correct, external_format_string($test, $context));
$this->assertSame($correct, external_format_string($test, $context->id));
// Filters can be opted out.
$test = '<span lang="en" class="multilang">EN</span><span lang="fr" class="multilang">FR</span> ';
$test .= '<script>hi</script> <h3>there</h3>%';
$correct = 'ENFR hi there%';
$this->assertSame($correct, util::format_string($test, $context, false, ['filter' => false]));
// Function external_format_string should work with context id or context instance.
$this->assertSame($correct, external_format_string($test, $context->id, false, ['filter' => false]));
$this->assertSame($correct, external_format_string($test, $context, false, ['filter' => false]));
$this->assertSame("& < > \" '", format_string("& < > \" '", true, ['escape' => false]));
}
}