Differences Between: [Versions 402 and 403]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 namespace core_external; 18 19 use coding_exception; 20 use context; 21 use context_helper; 22 use context_system; 23 use core_component; 24 use core_php_time_limit; 25 use invalid_parameter_exception; 26 use invalid_response_exception; 27 use moodle_exception; 28 29 /** 30 * Base class for external api methods. 31 * 32 * @package core_webservice 33 * @copyright 2009 Petr Skodak 34 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 35 * @since Moodle 2.0 36 */ 37 class external_api { 38 39 /** @var \stdClass context where the function calls will be restricted */ 40 private static $contextrestriction; 41 42 /** 43 * Returns detailed function information 44 * 45 * @param string|\stdClass $function name of external function or record from external_function 46 * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found; 47 * MUST_EXIST means throw exception if no record or multiple records found 48 * @return \stdClass|bool description or false if not found or exception thrown 49 * @throws coding_exception for any property and/or method that is missing or invalid 50 * @since Moodle 2.0 51 */ 52 public static function external_function_info($function, $strictness = MUST_EXIST) { 53 global $DB, $CFG; 54 55 if (!is_object($function)) { 56 if (!$function = $DB->get_record('external_functions', ['name' => $function], '*', $strictness)) { 57 return false; 58 } 59 } 60 61 // First try class autoloading. 62 if (!class_exists($function->classname)) { 63 // Fallback to explicit include of externallib.php. 64 if (empty($function->classpath)) { 65 $function->classpath = core_component::get_component_directory($function->component) . '/externallib.php'; 66 } else { 67 $function->classpath = "{$CFG->dirroot}/{$function->classpath}"; 68 } 69 if (!file_exists($function->classpath)) { 70 throw new coding_exception( 71 "Cannot find file {$function->classpath} with external function implementation " . 72 "for {$function->classname}::{$function->methodname}" 73 ); 74 } 75 require_once($function->classpath); 76 if (!class_exists($function->classname)) { 77 throw new coding_exception("Cannot find external class {$function->classname}"); 78 } 79 } 80 81 $function->ajax_method = "{$function->methodname}_is_allowed_from_ajax"; 82 $function->parameters_method = "{$function->methodname}_parameters"; 83 $function->returns_method = "{$function->methodname}_returns"; 84 $function->deprecated_method = "{$function->methodname}_is_deprecated"; 85 86 // Make sure the implementaion class is ok. 87 if (!method_exists($function->classname, $function->methodname)) { 88 throw new coding_exception( 89 "Missing implementation method {$function->classname}::{$function->methodname}" 90 ); 91 } 92 if (!method_exists($function->classname, $function->parameters_method)) { 93 throw new coding_exception( 94 "Missing parameters description method {$function->classname}::{$function->parameters_method}" 95 ); 96 } 97 if (!method_exists($function->classname, $function->returns_method)) { 98 throw new coding_exception( 99 "Missing returned values description method {$function->classname}::{$function->returns_method}" 100 ); 101 } 102 if (method_exists($function->classname, $function->deprecated_method)) { 103 if (call_user_func([$function->classname, $function->deprecated_method]) === true) { 104 $function->deprecated = true; 105 } 106 } 107 $function->allowed_from_ajax = false; 108 109 // Fetch the parameters description. 110 $function->parameters_desc = call_user_func([$function->classname, $function->parameters_method]); 111 if (!($function->parameters_desc instanceof external_function_parameters)) { 112 throw new coding_exception( 113 "{$function->classname}::{$function->parameters_method} did not return a valid external_function_parameters object" 114 ); 115 } 116 117 // Fetch the return values description. 118 $function->returns_desc = call_user_func([$function->classname, $function->returns_method]); 119 // Null means void result or result is ignored. 120 if (!is_null($function->returns_desc) && !($function->returns_desc instanceof external_description)) { 121 throw new coding_exception( 122 "{$function->classname}::{$function->returns_method} did not return a valid external_description object" 123 ); 124 } 125 126 // Now get the function description. 127 128 // TODO MDL-31115 use localised lang pack descriptions, it would be nice to have 129 // easy to understand descriptions in admin UI, 130 // on the other hand this is still a bit in a flux and we need to find some new naming 131 // conventions for these descriptions in lang packs. 132 $function->description = null; 133 $servicesfile = core_component::get_component_directory($function->component) . '/db/services.php'; 134 if (file_exists($servicesfile)) { 135 $functions = null; 136 include($servicesfile); 137 if (isset($functions[$function->name]['description'])) { 138 $function->description = $functions[$function->name]['description']; 139 } 140 if (isset($functions[$function->name]['testclientpath'])) { 141 $function->testclientpath = $functions[$function->name]['testclientpath']; 142 } 143 if (isset($functions[$function->name]['type'])) { 144 $function->type = $functions[$function->name]['type']; 145 } 146 if (isset($functions[$function->name]['ajax'])) { 147 $function->allowed_from_ajax = $functions[$function->name]['ajax']; 148 } else if (method_exists($function->classname, $function->ajax_method)) { 149 if (call_user_func([$function->classname, $function->ajax_method]) === true) { 150 debugging('External function ' . $function->ajax_method . '() function is deprecated.' . 151 'Set ajax=>true in db/service.php instead.', DEBUG_DEVELOPER); 152 $function->allowed_from_ajax = true; 153 } 154 } 155 if (isset($functions[$function->name]['loginrequired'])) { 156 $function->loginrequired = $functions[$function->name]['loginrequired']; 157 } else { 158 $function->loginrequired = true; 159 } 160 if (isset($functions[$function->name]['readonlysession'])) { 161 $function->readonlysession = $functions[$function->name]['readonlysession']; 162 } else { 163 $function->readonlysession = false; 164 } 165 } 166 167 return $function; 168 } 169 170 /** 171 * Call an external function validating all params/returns correctly. 172 * 173 * Note that an external function may modify the state of the current page, so this wrapper 174 * saves and restores tha PAGE and COURSE global variables before/after calling the external function. 175 * 176 * @param string $function A webservice function name. 177 * @param array $args Params array (named params) 178 * @param boolean $ajaxonly If true, an extra check will be peformed to see if ajax is required. 179 * @return array containing keys for error (bool), exception and data. 180 */ 181 public static function call_external_function($function, $args, $ajaxonly = false) { 182 global $PAGE, $COURSE, $CFG, $SITE; 183 184 require_once("{$CFG->libdir}/pagelib.php"); 185 186 $externalfunctioninfo = static::external_function_info($function); 187 188 // Eventually this should shift into the various handlers and not be handled via config. 189 $readonlysession = $externalfunctioninfo->readonlysession ?? false; 190 if (!$readonlysession || empty($CFG->enable_read_only_sessions)) { 191 \core\session\manager::restart_with_write_lock($readonlysession); 192 } 193 194 $currentpage = $PAGE; 195 $currentcourse = $COURSE; 196 $response = []; 197 198 try { 199 // Taken straight from from setup.php. 200 if (!empty($CFG->moodlepageclass)) { 201 if (!empty($CFG->moodlepageclassfile)) { 202 require_once($CFG->moodlepageclassfile); 203 } 204 $classname = $CFG->moodlepageclass; 205 } else { 206 $classname = 'moodle_page'; 207 } 208 $PAGE = new $classname(); 209 $COURSE = clone($SITE); 210 211 if ($ajaxonly && !$externalfunctioninfo->allowed_from_ajax) { 212 throw new moodle_exception('servicenotavailable', 'webservice'); 213 } 214 215 // Do not allow access to write or delete webservices as a public user. 216 if ($externalfunctioninfo->loginrequired && !WS_SERVER) { 217 if (defined('NO_MOODLE_COOKIES') && NO_MOODLE_COOKIES && !PHPUNIT_TEST) { 218 throw new moodle_exception('servicerequireslogin', 'webservice'); 219 } 220 if (!isloggedin()) { 221 throw new moodle_exception('servicerequireslogin', 'webservice'); 222 } else { 223 require_sesskey(); 224 } 225 } 226 // Validate params, this also sorts the params properly, we need the correct order in the next part. 227 $callable = [$externalfunctioninfo->classname, 'validate_parameters']; 228 $params = call_user_func( 229 $callable, 230 $externalfunctioninfo->parameters_desc, 231 $args 232 ); 233 $params = array_values($params); 234 235 // Allow any Moodle plugin a chance to override this call. This is a convenient spot to 236 // make arbitrary behaviour customisations. The overriding plugin could call the 'real' 237 // function first and then modify the results, or it could do a completely separate 238 // thing. 239 $callbacks = get_plugins_with_function('override_webservice_execution'); 240 $result = false; 241 foreach (array_values($callbacks) as $plugins) { 242 foreach (array_values($plugins) as $callback) { 243 $result = $callback($externalfunctioninfo, $params); 244 if ($result !== false) { 245 break 2; 246 } 247 } 248 } 249 250 // If the function was not overridden, call the real one. 251 if ($result === false) { 252 $callable = [$externalfunctioninfo->classname, $externalfunctioninfo->methodname]; 253 $result = call_user_func_array($callable, $params); 254 } 255 256 // Validate the return parameters. 257 if ($externalfunctioninfo->returns_desc !== null) { 258 $callable = [$externalfunctioninfo->classname, 'clean_returnvalue']; 259 $result = call_user_func($callable, $externalfunctioninfo->returns_desc, $result); 260 } 261 262 $response['error'] = false; 263 $response['data'] = $result; 264 } catch (\Throwable $e) { 265 $exception = get_exception_info($e); 266 unset($exception->a); 267 $exception->backtrace = format_backtrace($exception->backtrace, true); 268 if (!debugging('', DEBUG_DEVELOPER)) { 269 unset($exception->debuginfo); 270 unset($exception->backtrace); 271 } 272 $response['error'] = true; 273 $response['exception'] = $exception; 274 // Do not process the remaining requests. 275 } 276 277 $PAGE = $currentpage; 278 $COURSE = $currentcourse; 279 280 return $response; 281 } 282 283 /** 284 * Set context restriction for all following subsequent function calls. 285 * 286 * @param \stdClass $context the context restriction 287 * @since Moodle 2.0 288 */ 289 public static function set_context_restriction($context) { 290 self::$contextrestriction = $context; 291 } 292 293 /** 294 * This method has to be called before every operation 295 * that takes a longer time to finish! 296 * 297 * @param int $seconds max expected time the next operation needs 298 * @since Moodle 2.0 299 */ 300 public static function set_timeout($seconds = 360) { 301 $seconds = ($seconds < 300) ? 300 : $seconds; 302 core_php_time_limit::raise($seconds); 303 } 304 305 /** 306 * Validates submitted function parameters, if anything is incorrect 307 * invalid_parameter_exception is thrown. 308 * This is a simple recursive method which is intended to be called from 309 * each implementation method of external API. 310 * 311 * @param external_description $description description of parameters 312 * @param mixed $params the actual parameters 313 * @return mixed params with added defaults for optional items, invalid_parameters_exception thrown if any problem found 314 * @since Moodle 2.0 315 */ 316 public static function validate_parameters(external_description $description, $params) { 317 if ($description instanceof external_value) { 318 if (is_array($params) || is_object($params)) { 319 throw new invalid_parameter_exception('Scalar type expected, array or object received.'); 320 } 321 322 if ($description->type == PARAM_BOOL) { 323 // Special case for PARAM_BOOL - we want true/false instead of the usual 1/0 - we can not be too strict here. 324 if (is_bool($params) || $params === 0 || $params === 1 || $params === '0' || $params === '1') { 325 return (bool) $params; 326 } 327 } 328 $debuginfo = "Invalid external api parameter: the value is \"{$params}\", "; 329 $debuginfo .= "the server was expecting \"{$description->type}\" type"; 330 return validate_param($params, $description->type, $description->allownull, $debuginfo); 331 } else if ($description instanceof external_single_structure) { 332 if (!is_array($params)) { 333 throw new invalid_parameter_exception( 334 // phpcs:ignore moodle.PHP.ForbiddenFunctions.Found 335 "Only arrays accepted. The bad value is: '" . print_r($params, true) . "'" 336 ); 337 } 338 $result = []; 339 foreach ($description->keys as $key => $subdesc) { 340 if (!array_key_exists($key, $params)) { 341 if ($subdesc->required == VALUE_REQUIRED) { 342 throw new invalid_parameter_exception("Missing required key in single structure: {$key}"); 343 } 344 if ($subdesc->required == VALUE_DEFAULT) { 345 try { 346 $result[$key] = static::validate_parameters($subdesc, $subdesc->default); 347 } catch (invalid_parameter_exception $e) { 348 // We are only interested by exceptions returned by validate_param() and validate_parameters(). 349 // This is used to build the path to the faulty attribute. 350 throw new invalid_parameter_exception("{$key} => " . $e->getMessage() . ': ' . $e->debuginfo); 351 } 352 } 353 } else { 354 try { 355 $result[$key] = static::validate_parameters($subdesc, $params[$key]); 356 } catch (invalid_parameter_exception $e) { 357 // We are only interested by exceptions returned by validate_param() and validate_parameters(). 358 // This is used to build the path to the faulty attribute. 359 throw new invalid_parameter_exception($key . " => " . $e->getMessage() . ': ' . $e->debuginfo); 360 } 361 } 362 unset($params[$key]); 363 } 364 if (!empty($params)) { 365 throw new invalid_parameter_exception( 366 'Unexpected keys (' . implode(', ', array_keys($params)) . ') detected in parameter array.' 367 ); 368 } 369 return $result; 370 } else if ($description instanceof external_multiple_structure) { 371 if (!is_array($params)) { 372 throw new invalid_parameter_exception( 373 'Only arrays accepted. The bad value is: \'' . 374 // phpcs:ignore moodle.PHP.ForbiddenFunctions.Found 375 print_r($params, true) . 376 "'" 377 ); 378 } 379 $result = []; 380 foreach ($params as $param) { 381 $result[] = static::validate_parameters($description->content, $param); 382 } 383 return $result; 384 } else { 385 throw new invalid_parameter_exception('Invalid external api description'); 386 } 387 } 388 389 /** 390 * Clean response 391 * If a response attribute is unknown from the description, we just ignore the attribute. 392 * If a response attribute is incorrect, invalid_response_exception is thrown. 393 * Note: this function is similar to validate parameters, however it is distinct because 394 * parameters validation must be distinct from cleaning return values. 395 * 396 * @param external_description $description description of the return values 397 * @param mixed $response the actual response 398 * @return mixed response with added defaults for optional items, invalid_response_exception thrown if any problem found 399 * @author 2010 Jerome Mouneyrac 400 * @since Moodle 2.0 401 */ 402 public static function clean_returnvalue(external_description $description, $response) { 403 if ($description instanceof external_value) { 404 if (is_array($response) || is_object($response)) { 405 throw new invalid_response_exception('Scalar type expected, array or object received.'); 406 } 407 408 if ($description->type == PARAM_BOOL) { 409 // Special case for PARAM_BOOL - we want true/false instead of the usual 1/0 - we can not be too strict here. 410 if (is_bool($response) || $response === 0 || $response === 1 || $response === '0' || $response === '1') { 411 return (bool) $response; 412 } 413 } 414 $responsetype = gettype($response); 415 $debuginfo = "Invalid external api response: the value is \"{$response}\" of PHP type \"{$responsetype}\", "; 416 $debuginfo .= "the server was expecting \"{$description->type}\" type"; 417 try { 418 return validate_param($response, $description->type, $description->allownull, $debuginfo); 419 } catch (invalid_parameter_exception $e) { 420 // Proper exception name, to be recursively catched to build the path to the faulty attribute. 421 throw new invalid_response_exception($e->debuginfo); 422 } 423 } else if ($description instanceof external_single_structure) { 424 if (!is_array($response) && !is_object($response)) { 425 throw new invalid_response_exception( 426 // phpcs:ignore moodle.PHP.ForbiddenFunctions.Found 427 "Only arrays/objects accepted. The bad value is: '" . print_r($response, true) . "'" 428 ); 429 } 430 431 // Cast objects into arrays. 432 if (is_object($response)) { 433 $response = (array) $response; 434 } 435 436 $result = []; 437 foreach ($description->keys as $key => $subdesc) { 438 if (!array_key_exists($key, $response)) { 439 if ($subdesc->required == VALUE_REQUIRED) { 440 throw new invalid_response_exception( 441 "Error in response - Missing following required key in a single structure: {$key}" 442 ); 443 } 444 if ($subdesc instanceof external_value) { 445 if ($subdesc->required == VALUE_DEFAULT) { 446 try { 447 $result[$key] = static::clean_returnvalue($subdesc, $subdesc->default); 448 } catch (invalid_response_exception $e) { 449 // Build the path to the faulty attribute. 450 throw new invalid_response_exception("{$key} => " . $e->getMessage() . ': ' . $e->debuginfo); 451 } 452 } 453 } 454 } else { 455 try { 456 $result[$key] = static::clean_returnvalue($subdesc, $response[$key]); 457 } catch (invalid_response_exception $e) { 458 // Build the path to the faulty attribute. 459 throw new invalid_response_exception("{$key} => " . $e->getMessage() . ': ' . $e->debuginfo); 460 } 461 } 462 unset($response[$key]); 463 } 464 465 return $result; 466 } else if ($description instanceof external_multiple_structure) { 467 if (!is_array($response)) { 468 throw new invalid_response_exception( 469 // phpcs:ignore moodle.PHP.ForbiddenFunctions.Found 470 "Only arrays accepted. The bad value is: '" . print_r($response, true) . "'" 471 ); 472 } 473 $result = []; 474 foreach ($response as $param) { 475 $result[] = static::clean_returnvalue($description->content, $param); 476 } 477 return $result; 478 } else { 479 throw new invalid_response_exception('Invalid external api response description'); 480 } 481 } 482 483 /** 484 * Makes sure user may execute functions in this context. 485 * 486 * @param context $context 487 * @since Moodle 2.0 488 */ 489 public static function validate_context($context) { 490 global $PAGE; 491 492 if (empty($context)) { 493 throw new invalid_parameter_exception('Context does not exist'); 494 } 495 if (empty(self::$contextrestriction)) { 496 self::$contextrestriction = context_system::instance(); 497 } 498 $rcontext = self::$contextrestriction; 499 500 if ($rcontext->contextlevel == $context->contextlevel) { 501 if ($rcontext->id != $context->id) { 502 throw new restricted_context_exception(); 503 } 504 } else if ($rcontext->contextlevel > $context->contextlevel) { 505 throw new restricted_context_exception(); 506 } else { 507 $parents = $context->get_parent_context_ids(); 508 if (!in_array($rcontext->id, $parents)) { 509 throw new restricted_context_exception(); 510 } 511 } 512 513 $PAGE->reset_theme_and_output(); 514 [, $course, $cm] = get_context_info_array($context->id); 515 require_login($course, false, $cm, false, true); 516 $PAGE->set_context($context); 517 } 518 519 /** 520 * Get context from passed parameters. 521 * The passed array must either contain a contextid or a combination of context level and instance id to fetch the context. 522 * For example, the context level can be "course" and instanceid can be courseid. 523 * 524 * See context_helper::get_all_levels() for a list of valid numeric context levels, 525 * legacy short names such as 'system', 'user', 'course' are not supported in new 526 * plugin capabilities. 527 * 528 * @param array $param 529 * @since Moodle 2.6 530 * @throws invalid_parameter_exception 531 * @return context 532 */ 533 protected static function get_context_from_params($param) { 534 if (!empty($param['contextid'])) { 535 return context::instance_by_id($param['contextid'], IGNORE_MISSING); 536 } else if (!empty($param['contextlevel']) && isset($param['instanceid'])) { 537 // Numbers and short names are supported since Moodle 4.2. 538 $classname = \core\context_helper::parse_external_level($param['contextlevel']); 539 if (!$classname) { 540 throw new invalid_parameter_exception('Invalid context level = '.$param['contextlevel']); 541 } 542 return $classname::instance($param['instanceid'], IGNORE_MISSING); 543 } else { 544 // No valid context info was found. 545 throw new invalid_parameter_exception( 546 'Missing parameters, please provide either context level with instance id or contextid' 547 ); 548 } 549 } 550 551 /** 552 * Returns a prepared structure to use a context parameters. 553 * @return external_single_structure 554 */ 555 protected static function get_context_parameters() { 556 $id = new external_value( 557 PARAM_INT, 558 'Context ID. Either use this value, or level and instanceid.', 559 VALUE_DEFAULT, 560 0 561 ); 562 $level = new external_value( 563 PARAM_ALPHANUM, // Since Moodle 4.2 numeric context level values are supported too. 564 'Context level. To be used with instanceid.', 565 VALUE_DEFAULT, 566 '' 567 ); 568 $instanceid = new external_value( 569 PARAM_INT, 570 'Context instance ID. To be used with level', 571 VALUE_DEFAULT, 572 0 573 ); 574 return new external_single_structure([ 575 'contextid' => $id, 576 'contextlevel' => $level, 577 'instanceid' => $instanceid, 578 ]); 579 } 580 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body