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 ($params === null && $description->allownull == NULL_ALLOWED) { 318 return null; 319 } 320 if ($description instanceof external_value) { 321 if (is_array($params) || is_object($params)) { 322 throw new invalid_parameter_exception('Scalar type expected, array or object received.'); 323 } 324 325 if ($description->type == PARAM_BOOL) { 326 // Special case for PARAM_BOOL - we want true/false instead of the usual 1/0 - we can not be too strict here. 327 if (is_bool($params) || $params === 0 || $params === 1 || $params === '0' || $params === '1') { 328 return (bool) $params; 329 } 330 } 331 $debuginfo = "Invalid external api parameter: the value is \"{$params}\", "; 332 $debuginfo .= "the server was expecting \"{$description->type}\" type"; 333 return validate_param($params, $description->type, $description->allownull, $debuginfo); 334 } else if ($description instanceof external_single_structure) { 335 if (!is_array($params)) { 336 throw new invalid_parameter_exception( 337 // phpcs:ignore moodle.PHP.ForbiddenFunctions.Found 338 "Only arrays accepted. The bad value is: '" . print_r($params, true) . "'" 339 ); 340 } 341 $result = []; 342 foreach ($description->keys as $key => $subdesc) { 343 if (!array_key_exists($key, $params)) { 344 if ($subdesc->required == VALUE_REQUIRED) { 345 throw new invalid_parameter_exception("Missing required key in single structure: {$key}"); 346 } 347 if ($subdesc->required == VALUE_DEFAULT) { 348 try { 349 $result[$key] = static::validate_parameters($subdesc, $subdesc->default); 350 } catch (invalid_parameter_exception $e) { 351 // We are only interested by exceptions returned by validate_param() and validate_parameters(). 352 // This is used to build the path to the faulty attribute. 353 throw new invalid_parameter_exception("{$key} => " . $e->getMessage() . ': ' . $e->debuginfo); 354 } 355 } 356 } else { 357 try { 358 $result[$key] = static::validate_parameters($subdesc, $params[$key]); 359 } catch (invalid_parameter_exception $e) { 360 // We are only interested by exceptions returned by validate_param() and validate_parameters(). 361 // This is used to build the path to the faulty attribute. 362 throw new invalid_parameter_exception($key . " => " . $e->getMessage() . ': ' . $e->debuginfo); 363 } 364 } 365 unset($params[$key]); 366 } 367 if (!empty($params)) { 368 throw new invalid_parameter_exception( 369 'Unexpected keys (' . implode(', ', array_keys($params)) . ') detected in parameter array.' 370 ); 371 } 372 return $result; 373 } else if ($description instanceof external_multiple_structure) { 374 if (!is_array($params)) { 375 throw new invalid_parameter_exception( 376 'Only arrays accepted. The bad value is: \'' . 377 // phpcs:ignore moodle.PHP.ForbiddenFunctions.Found 378 print_r($params, true) . 379 "'" 380 ); 381 } 382 $result = []; 383 foreach ($params as $param) { 384 $result[] = static::validate_parameters($description->content, $param); 385 } 386 return $result; 387 } else { 388 throw new invalid_parameter_exception('Invalid external api description'); 389 } 390 } 391 392 /** 393 * Clean response 394 * If a response attribute is unknown from the description, we just ignore the attribute. 395 * If a response attribute is incorrect, invalid_response_exception is thrown. 396 * Note: this function is similar to validate parameters, however it is distinct because 397 * parameters validation must be distinct from cleaning return values. 398 * 399 * @param external_description $description description of the return values 400 * @param mixed $response the actual response 401 * @return mixed response with added defaults for optional items, invalid_response_exception thrown if any problem found 402 * @author 2010 Jerome Mouneyrac 403 * @since Moodle 2.0 404 */ 405 public static function clean_returnvalue(external_description $description, $response) { 406 if ($response === null && $description->allownull == NULL_ALLOWED) { 407 return null; 408 } 409 if ($description instanceof external_value) { 410 if (is_array($response) || is_object($response)) { 411 throw new invalid_response_exception('Scalar type expected, array or object received.'); 412 } 413 414 if ($description->type == PARAM_BOOL) { 415 // Special case for PARAM_BOOL - we want true/false instead of the usual 1/0 - we can not be too strict here. 416 if (is_bool($response) || $response === 0 || $response === 1 || $response === '0' || $response === '1') { 417 return (bool) $response; 418 } 419 } 420 $responsetype = gettype($response); 421 $debuginfo = "Invalid external api response: the value is \"{$response}\" of PHP type \"{$responsetype}\", "; 422 $debuginfo .= "the server was expecting \"{$description->type}\" type"; 423 try { 424 return validate_param($response, $description->type, $description->allownull, $debuginfo); 425 } catch (invalid_parameter_exception $e) { 426 // Proper exception name, to be recursively catched to build the path to the faulty attribute. 427 throw new invalid_response_exception($e->debuginfo); 428 } 429 } else if ($description instanceof external_single_structure) { 430 if (!is_array($response) && !is_object($response)) { 431 throw new invalid_response_exception( 432 // phpcs:ignore moodle.PHP.ForbiddenFunctions.Found 433 "Only arrays/objects accepted. The bad value is: '" . print_r($response, true) . "'" 434 ); 435 } 436 437 // Cast objects into arrays. 438 if (is_object($response)) { 439 $response = (array) $response; 440 } 441 442 $result = []; 443 foreach ($description->keys as $key => $subdesc) { 444 if (!array_key_exists($key, $response)) { 445 if ($subdesc->required == VALUE_REQUIRED) { 446 throw new invalid_response_exception( 447 "Error in response - Missing following required key in a single structure: {$key}" 448 ); 449 } 450 if ($subdesc instanceof external_value) { 451 if ($subdesc->required == VALUE_DEFAULT) { 452 try { 453 $result[$key] = static::clean_returnvalue($subdesc, $subdesc->default); 454 } catch (invalid_response_exception $e) { 455 // Build the path to the faulty attribute. 456 throw new invalid_response_exception("{$key} => " . $e->getMessage() . ': ' . $e->debuginfo); 457 } 458 } 459 } 460 } else { 461 try { 462 $result[$key] = static::clean_returnvalue($subdesc, $response[$key]); 463 } catch (invalid_response_exception $e) { 464 // Build the path to the faulty attribute. 465 throw new invalid_response_exception("{$key} => " . $e->getMessage() . ': ' . $e->debuginfo); 466 } 467 } 468 unset($response[$key]); 469 } 470 471 return $result; 472 } else if ($description instanceof external_multiple_structure) { 473 if (!is_array($response)) { 474 throw new invalid_response_exception( 475 // phpcs:ignore moodle.PHP.ForbiddenFunctions.Found 476 "Only arrays accepted. The bad value is: '" . print_r($response, true) . "'" 477 ); 478 } 479 $result = []; 480 foreach ($response as $param) { 481 $result[] = static::clean_returnvalue($description->content, $param); 482 } 483 return $result; 484 } else { 485 throw new invalid_response_exception('Invalid external api response description'); 486 } 487 } 488 489 /** 490 * Makes sure user may execute functions in this context. 491 * 492 * @param context $context 493 * @since Moodle 2.0 494 */ 495 public static function validate_context($context) { 496 global $PAGE; 497 498 if (empty($context)) { 499 throw new invalid_parameter_exception('Context does not exist'); 500 } 501 if (empty(self::$contextrestriction)) { 502 self::$contextrestriction = context_system::instance(); 503 } 504 $rcontext = self::$contextrestriction; 505 506 if ($rcontext->contextlevel == $context->contextlevel) { 507 if ($rcontext->id != $context->id) { 508 throw new restricted_context_exception(); 509 } 510 } else if ($rcontext->contextlevel > $context->contextlevel) { 511 throw new restricted_context_exception(); 512 } else { 513 $parents = $context->get_parent_context_ids(); 514 if (!in_array($rcontext->id, $parents)) { 515 throw new restricted_context_exception(); 516 } 517 } 518 519 $PAGE->reset_theme_and_output(); 520 [, $course, $cm] = get_context_info_array($context->id); 521 require_login($course, false, $cm, false, true); 522 $PAGE->set_context($context); 523 } 524 525 /** 526 * Get context from passed parameters. 527 * The passed array must either contain a contextid or a combination of context level and instance id to fetch the context. 528 * For example, the context level can be "course" and instanceid can be courseid. 529 * 530 * See context_helper::get_all_levels() for a list of valid numeric context levels, 531 * legacy short names such as 'system', 'user', 'course' are not supported in new 532 * plugin capabilities. 533 * 534 * @param array $param 535 * @since Moodle 2.6 536 * @throws invalid_parameter_exception 537 * @return context 538 */ 539 protected static function get_context_from_params($param) { 540 if (!empty($param['contextid'])) { 541 return context::instance_by_id($param['contextid'], IGNORE_MISSING); 542 } else if (!empty($param['contextlevel']) && isset($param['instanceid'])) { 543 // Numbers and short names are supported since Moodle 4.2. 544 $classname = \core\context_helper::parse_external_level($param['contextlevel']); 545 if (!$classname) { 546 throw new invalid_parameter_exception('Invalid context level = '.$param['contextlevel']); 547 } 548 return $classname::instance($param['instanceid'], IGNORE_MISSING); 549 } else { 550 // No valid context info was found. 551 throw new invalid_parameter_exception( 552 'Missing parameters, please provide either context level with instance id or contextid' 553 ); 554 } 555 } 556 557 /** 558 * Returns a prepared structure to use a context parameters. 559 * @return external_single_structure 560 */ 561 protected static function get_context_parameters() { 562 $id = new external_value( 563 PARAM_INT, 564 'Context ID. Either use this value, or level and instanceid.', 565 VALUE_DEFAULT, 566 0 567 ); 568 $level = new external_value( 569 PARAM_ALPHANUM, // Since Moodle 4.2 numeric context level values are supported too. 570 'Context level. To be used with instanceid.', 571 VALUE_DEFAULT, 572 '' 573 ); 574 $instanceid = new external_value( 575 PARAM_INT, 576 'Context instance ID. To be used with level', 577 VALUE_DEFAULT, 578 0 579 ); 580 return new external_single_structure([ 581 'contextid' => $id, 582 'contextlevel' => $level, 583 'instanceid' => $instanceid, 584 ]); 585 } 586 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body