From 56bcaf409f795f60db4e078e895c933ca7e958e1 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Wed, 7 Dec 2022 10:32:02 +0800 Subject: [PATCH] MDL-76583 core_external: Migrate external_api to core_external\external_api --- lib/external/classes/external_api.php | 565 ++++++++++++++++++++++++++ lib/externallib.php | 532 +----------------------- lib/tests/externallib_test.php | 34 +- 3 files changed, 583 insertions(+), 548 deletions(-) create mode 100644 lib/external/classes/external_api.php diff --git a/lib/external/classes/external_api.php b/lib/external/classes/external_api.php new file mode 100644 index 00000000000..5124161e508 --- /dev/null +++ b/lib/external/classes/external_api.php @@ -0,0 +1,565 @@ +. + +namespace core_external; + +use coding_exception; +use context; +use context_helper; +use context_system; +use core_component; +use core_php_time_limit; +use external_description; +use external_function_parameters; +use external_multiple_structure; +use external_single_structure; +use external_value; +use invalid_parameter_exception; +use invalid_response_exception; +use moodle_exception; +use restricted_context_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|object $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 + * @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', array('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'); + } + 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(array($function->classname, $function->deprecated_method)) === true) { + $function->deprecated = true; + } + } + $function->allowed_from_ajax = false; + + // Fetch the parameters description. + $function->parameters_desc = call_user_func(array($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(array($function->classname, $function->returns_method)); + // Null means void result or result is ignored. + if (!is_null($function->returns_desc) and !($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(array($function->classname, $function->ajax_method)) === true) { + debugging('External function ' . $function->ajax_method . '() function is deprecated.' . + 'Set ajax=>true in db/service.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 = array(); + + 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 = array($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 ($callbacks as $plugintype => $plugins) { + foreach ($plugins as $plugin => $callback) { + $result = $callback($externalfunctioninfo, $params); + if ($result !== false) { + break 2; + } + } + } + + // If the function was not overridden, call the real one. + if ($result === false) { + $callable = array($externalfunctioninfo->classname, $externalfunctioninfo->methodname); + $result = call_user_func_array($callable, $params); + } + + // Validate the return parameters. + if ($externalfunctioninfo->returns_desc !== null) { + $callable = array($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 ($description instanceof external_value) { + if (is_array($params) or 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) or $params === 0 or $params === 1 or $params === '0' or $params === '1') { + return (bool)$params; + } + } + $debuginfo = 'Invalid external api parameter: the value is "' . $params . + '", 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('Only arrays accepted. The bad value is: \'' + . print_r($params, true) . '\''); + } + $result = array(); + 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() + //(in order to build the path to the faulty attribut) + 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() + //(in order to build the path to the faulty attribut) + 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: \'' + . print_r($params, true) . '\''); + } + $result = array(); + 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 ($description instanceof external_value) { + if (is_array($response) or 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) or $response === 0 or $response === 1 or $response === '0' or $response === '1') { + return (bool)$response; + } + } + $responsetype = gettype($response); + $debuginfo = 'Invalid external api response: the value is "' . $response . + '" of PHP type "' . $responsetype . '", 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 attribut + 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('Only arrays/objects accepted. The bad value is: \'' . + print_r($response, true) . '\''); + } + + // Cast objects into arrays. + if (is_object($response)) { + $response = (array) $response; + } + + $result = array(); + 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 attribut + 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 attribut + 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('Only arrays accepted. The bad value is: \'' . + print_r($response, true) . '\''); + } + $result = array(); + 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 \stdClass $context + * @since Moodle 2.0 + */ + public static function validate_context($context) { + global $CFG, $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(); + list($unused, $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 context levels. + * + * @param array $param + * @since Moodle 2.6 + * @throws invalid_parameter_exception + * @return context + */ + protected static function get_context_from_params($param) { + $levels = context_helper::get_all_levels(); + if (!empty($param['contextid'])) { + return context::instance_by_id($param['contextid'], IGNORE_MISSING); + } else if (!empty($param['contextlevel']) && isset($param['instanceid'])) { + $contextlevel = "context_".$param['contextlevel']; + if (!array_search($contextlevel, $levels)) { + throw new invalid_parameter_exception('Invalid context level = '.$param['contextlevel']); + } + return $contextlevel::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_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 + ); + return new external_single_structure(array( + 'contextid' => $id, + 'contextlevel' => $level, + 'instanceid' => $instanceid, + )); + } + +} diff --git a/lib/externallib.php b/lib/externallib.php index 666b49940cf..f5b975446f9 100644 --- a/lib/externallib.php +++ b/lib/externallib.php @@ -44,537 +44,7 @@ class restricted_context_exception extends 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|object $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 description or false if not found or exception thrown - * @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', array('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'); - } - 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(array($function->classname, $function->deprecated_method)) === true) { - $function->deprecated = true; - } - } - $function->allowed_from_ajax = false; - - // Fetch the parameters description. - $function->parameters_desc = call_user_func(array($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(array($function->classname, $function->returns_method)); - // Null means void result or result is ignored. - if (!is_null($function->returns_desc) and !($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(array($function->classname, $function->ajax_method)) === true) { - debugging('External function ' . $function->ajax_method . '() function is deprecated.' . - 'Set ajax=>true in db/service.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 = array(); - - 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 = array($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 ($callbacks as $plugintype => $plugins) { - foreach ($plugins as $plugin => $callback) { - $result = $callback($externalfunctioninfo, $params); - if ($result !== false) { - break 2; - } - } - } - - // If the function was not overridden, call the real one. - if ($result === false) { - $callable = array($externalfunctioninfo->classname, $externalfunctioninfo->methodname); - $result = call_user_func_array($callable, $params); - } - - // Validate the return parameters. - if ($externalfunctioninfo->returns_desc !== null) { - $callable = array($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 ($description instanceof external_value) { - if (is_array($params) or 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) or $params === 0 or $params === 1 or $params === '0' or $params === '1') { - return (bool)$params; - } - } - $debuginfo = 'Invalid external api parameter: the value is "' . $params . - '", 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('Only arrays accepted. The bad value is: \'' - . print_r($params, true) . '\''); - } - $result = array(); - 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() - //(in order to build the path to the faulty attribut) - 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() - //(in order to build the path to the faulty attribut) - 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: \'' - . print_r($params, true) . '\''); - } - $result = array(); - 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 ($description instanceof external_value) { - if (is_array($response) or 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) or $response === 0 or $response === 1 or $response === '0' or $response === '1') { - return (bool)$response; - } - } - $responsetype = gettype($response); - $debuginfo = 'Invalid external api response: the value is "' . $response . - '" of PHP type "' . $responsetype . '", 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 attribut - 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('Only arrays/objects accepted. The bad value is: \'' . - print_r($response, true) . '\''); - } - - // Cast objects into arrays. - if (is_object($response)) { - $response = (array) $response; - } - - $result = array(); - 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 attribut - 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 attribut - 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('Only arrays accepted. The bad value is: \'' . - print_r($response, true) . '\''); - } - $result = array(); - 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 stdClass $context - * @since Moodle 2.0 - */ - public static function validate_context($context) { - global $CFG, $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(); - list($unused, $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 context levels. - * - * @param array $param - * @since Moodle 2.6 - * @throws invalid_parameter_exception - * @return context - */ - protected static function get_context_from_params($param) { - $levels = context_helper::get_all_levels(); - if (!empty($param['contextid'])) { - return context::instance_by_id($param['contextid'], IGNORE_MISSING); - } else if (!empty($param['contextlevel']) && isset($param['instanceid'])) { - $contextlevel = "context_".$param['contextlevel']; - if (!array_search($contextlevel, $levels)) { - throw new invalid_parameter_exception('Invalid context level = '.$param['contextlevel']); - } - return $contextlevel::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_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 - ); - return new external_single_structure(array( - 'contextid' => $id, - 'contextlevel' => $level, - 'instanceid' => $instanceid, - )); - } - -} +class_alias(\core_external\external_api::class, 'external_api'); /** * Common ancestor of all parameter description classes diff --git a/lib/tests/externallib_test.php b/lib/tests/externallib_test.php index 68fb84280d9..94bcb0109fb 100644 --- a/lib/tests/externallib_test.php +++ b/lib/tests/externallib_test.php @@ -77,7 +77,7 @@ class externallib_test extends \advanced_testcase { $params = array('text'=>'aaa', 'someid'=>'6'); $description = new \external_function_parameters(array('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); + $result = \core_external\external_api::validate_parameters($description, $params); $this->assertCount(2, $result); reset($result); $this->assertSame('someid', key($result)); @@ -87,7 +87,7 @@ class externallib_test extends \advanced_testcase { $params = array('someids'=>array('1', 2, 'a'=>'3'), 'scalar'=>666); $description = new \external_function_parameters(array('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); + $result = \core_external\external_api::validate_parameters($description, $params); $this->assertCount(2, $result); reset($result); $this->assertSame('someids', key($result)); @@ -97,7 +97,7 @@ class externallib_test extends \advanced_testcase { $params = array('text'=>'aaa'); $description = new \external_function_parameters(array('someid' => new \external_value(PARAM_INT, 'Some int value', false), 'text' => new \external_value(PARAM_ALPHA, 'Some text value'))); - $result = \external_api::validate_parameters($description, $params); + $result = \core_external\external_api::validate_parameters($description, $params); $this->assertCount(2, $result); reset($result); $this->assertSame('someid', key($result)); @@ -107,7 +107,7 @@ class externallib_test extends \advanced_testcase { $params = array('text'=>'aaa'); $description = new \external_function_parameters(array('someid' => new \external_value(PARAM_INT, 'Some int value', false, 6), 'text' => new \external_value(PARAM_ALPHA, 'Some text value'))); - $result = \external_api::validate_parameters($description, $params); + $result = \core_external\external_api::validate_parameters($description, $params); $this->assertCount(2, $result); reset($result); $this->assertSame('someid', key($result)); @@ -270,7 +270,7 @@ class externallib_test extends \advanced_testcase { // Check return type on exception because the external values does not allow NULL values. $testdata = array('value' => null); try { - $cleanedvalue = \external_api::clean_returnvalue($returndesc, $testdata); + $cleanedvalue = \core_external\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); @@ -297,7 +297,7 @@ class externallib_test extends \advanced_testcase { $singlestructure['object'] = $object; $singlestructure['value2'] = 'Some text'; $testdata = array($singlestructure); - $cleanedvalue = \external_api::clean_returnvalue($returndesc, $testdata); + $cleanedvalue = \core_external\external_api::clean_returnvalue($returndesc, $testdata); $cleanedsinglestructure = array_pop($cleanedvalue); $this->assertSame($object->value1, $cleanedsinglestructure['object']['value1']); $this->assertSame($singlestructure['value2'], $cleanedsinglestructure['value2']); @@ -308,7 +308,7 @@ class externallib_test extends \advanced_testcase { $singlestructure = new \stdClass(); $singlestructure->object = $object; $testdata = array($singlestructure); - $cleanedvalue = \external_api::clean_returnvalue($returndesc, $testdata); + $cleanedvalue = \core_external\external_api::clean_returnvalue($returndesc, $testdata); $cleanedsinglestructure = array_pop($cleanedvalue); $this->assertSame($object->value1, $cleanedsinglestructure['object']['value1']); $this->assertArrayNotHasKey('value2', $cleanedsinglestructure); @@ -321,7 +321,7 @@ class externallib_test extends \advanced_testcase { $singlestructure['value2'] = 'Some text'; $singlestructure['unknownvalue'] = 'Some text to ignore'; $testdata = array($singlestructure); - $cleanedvalue = \external_api::clean_returnvalue($returndesc, $testdata); + $cleanedvalue = \core_external\external_api::clean_returnvalue($returndesc, $testdata); $cleanedsinglestructure = array_pop($cleanedvalue); $this->assertSame($object['value1'], $cleanedsinglestructure['object']['value1']); $this->assertSame($singlestructure['value2'], $cleanedsinglestructure['value2']); @@ -334,10 +334,10 @@ class externallib_test extends \advanced_testcase { $singlestructure['value2'] = 'Some text'; $testdata = array($singlestructure); $this->expectException('invalid_response_exception'); - $cleanedvalue = \external_api::clean_returnvalue($returndesc, $testdata); + $cleanedvalue = \core_external\external_api::clean_returnvalue($returndesc, $testdata); } /* - * Test \external_api::get_context_from_params(). + * Test \core_external\external_api::get_context_from_params(). */ public function test_get_context_from_params() { $this->resetAfterTest(true); @@ -385,7 +385,7 @@ class externallib_test extends \advanced_testcase { } /* - * Test \external_api::get_context()_from_params parameter validation. + * Test \core_external\external_api::get_context()_from_params parameter validation. */ public function test_get_context_params() { global $USER; @@ -396,7 +396,7 @@ class externallib_test extends \advanced_testcase { } /* - * Test \external_api::get_context()_from_params parameter validation. + * Test \core_external\external_api::get_context()_from_params parameter validation. */ public function test_get_context_params2() { global $USER; @@ -407,7 +407,7 @@ class externallib_test extends \advanced_testcase { } /* - * Test \external_api::get_context()_from_params parameter validation. + * Test \core_external\external_api::get_context()_from_params parameter validation. */ public function test_get_context_params3() { global $USER; @@ -437,7 +437,7 @@ class externallib_test extends \advanced_testcase { * @dataProvider all_external_info_provider */ public function test_all_external_info($f) { - $desc = \external_api::external_function_info($f); + $desc = \core_external\external_api::external_function_info($f); $this->assertNotEmpty($desc->name); $this->assertNotEmpty($desc->classname); $this->assertNotEmpty($desc->methodname); @@ -555,7 +555,7 @@ class externallib_test extends \advanced_testcase { $beforepage = $PAGE; $beforecourse = $COURSE; $params = array('cohortids' => array($cohort1->id, $cohort2->id)); - $result = \external_api::call_external_function('core_cohort_get_cohorts', $params); + $result = \core_external\external_api::call_external_function('core_cohort_get_cohorts', $params); $this->assertSame($beforepage, $PAGE); $this->assertSame($beforecourse, $COURSE); @@ -566,7 +566,7 @@ class externallib_test extends \advanced_testcase { $beforepage = $PAGE; $beforecourse = $COURSE; $params = array('courseid' => $course->id, 'options' => array()); - $result = \external_api::call_external_function('core_enrol_get_enrolled_users', $params); + $result = \core_external\external_api::call_external_function('core_enrol_get_enrolled_users', $params); $this->assertSame($beforepage, $PAGE); $this->assertSame($beforecourse, $COURSE); @@ -686,7 +686,7 @@ class externallib_test extends \advanced_testcase { /* * Just a wrapper to access protected apis for testing */ -class test_exernal_api extends \external_api { +class test_exernal_api extends \core_external\external_api { public static function get_context_wrapper($params) { return self::get_context_from_params($params);