Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]
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 18 /** 19 * Support for external API 20 * 21 * @package core_webservice 22 * @copyright 2009 Petr Skodak 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 defined('MOODLE_INTERNAL') || die(); 27 28 /** 29 * Exception indicating user is not allowed to use external function in the current context. 30 * 31 * @package core_webservice 32 * @copyright 2009 Petr Skodak 33 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 34 * @since Moodle 2.0 35 */ 36 class restricted_context_exception extends moodle_exception { 37 /** 38 * Constructor 39 * 40 * @since Moodle 2.0 41 */ 42 function __construct() { 43 parent::__construct('restrictedcontextexception', 'error'); 44 } 45 } 46 47 /** 48 * Base class for external api methods. 49 * 50 * @package core_webservice 51 * @copyright 2009 Petr Skodak 52 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 53 * @since Moodle 2.0 54 */ 55 class external_api { 56 57 /** @var stdClass context where the function calls will be restricted */ 58 private static $contextrestriction; 59 60 /** 61 * Returns detailed function information 62 * 63 * @param string|object $function name of external function or record from external_function 64 * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found; 65 * MUST_EXIST means throw exception if no record or multiple records found 66 * @return stdClass description or false if not found or exception thrown 67 * @since Moodle 2.0 68 */ 69 public static function external_function_info($function, $strictness=MUST_EXIST) { 70 global $DB, $CFG; 71 72 if (!is_object($function)) { 73 if (!$function = $DB->get_record('external_functions', array('name' => $function), '*', $strictness)) { 74 return false; 75 } 76 } 77 78 // First try class autoloading. 79 if (!class_exists($function->classname)) { 80 // Fallback to explicit include of externallib.php. 81 if (empty($function->classpath)) { 82 $function->classpath = core_component::get_component_directory($function->component).'/externallib.php'; 83 } else { 84 $function->classpath = $CFG->dirroot.'/'.$function->classpath; 85 } 86 if (!file_exists($function->classpath)) { 87 throw new coding_exception('Cannot find file ' . $function->classpath . 88 ' with external function implementation'); 89 } 90 require_once($function->classpath); 91 if (!class_exists($function->classname)) { 92 throw new coding_exception('Cannot find external class ' . $function->classname); 93 } 94 } 95 96 $function->ajax_method = $function->methodname.'_is_allowed_from_ajax'; 97 $function->parameters_method = $function->methodname.'_parameters'; 98 $function->returns_method = $function->methodname.'_returns'; 99 $function->deprecated_method = $function->methodname.'_is_deprecated'; 100 101 // Make sure the implementaion class is ok. 102 if (!method_exists($function->classname, $function->methodname)) { 103 throw new coding_exception('Missing implementation method ' . 104 $function->classname . '::' . $function->methodname); 105 } 106 if (!method_exists($function->classname, $function->parameters_method)) { 107 throw new coding_exception('Missing parameters description method ' . 108 $function->classname . '::' . $function->parameters_method); 109 } 110 if (!method_exists($function->classname, $function->returns_method)) { 111 throw new coding_exception('Missing returned values description method ' . 112 $function->classname . '::' . $function->returns_method); 113 } 114 if (method_exists($function->classname, $function->deprecated_method)) { 115 if (call_user_func(array($function->classname, $function->deprecated_method)) === true) { 116 $function->deprecated = true; 117 } 118 } 119 $function->allowed_from_ajax = false; 120 121 // Fetch the parameters description. 122 $function->parameters_desc = call_user_func(array($function->classname, $function->parameters_method)); 123 if (!($function->parameters_desc instanceof external_function_parameters)) { 124 throw new coding_exception($function->classname . '::' . $function->parameters_method . 125 ' did not return a valid external_function_parameters object.'); 126 } 127 128 // Fetch the return values description. 129 $function->returns_desc = call_user_func(array($function->classname, $function->returns_method)); 130 // Null means void result or result is ignored. 131 if (!is_null($function->returns_desc) and !($function->returns_desc instanceof external_description)) { 132 throw new coding_exception($function->classname . '::' . $function->returns_method . 133 ' did not return a valid external_description object'); 134 } 135 136 // Now get the function description. 137 138 // TODO MDL-31115 use localised lang pack descriptions, it would be nice to have 139 // easy to understand descriptions in admin UI, 140 // on the other hand this is still a bit in a flux and we need to find some new naming 141 // conventions for these descriptions in lang packs. 142 $function->description = null; 143 $servicesfile = core_component::get_component_directory($function->component).'/db/services.php'; 144 if (file_exists($servicesfile)) { 145 $functions = null; 146 include($servicesfile); 147 if (isset($functions[$function->name]['description'])) { 148 $function->description = $functions[$function->name]['description']; 149 } 150 if (isset($functions[$function->name]['testclientpath'])) { 151 $function->testclientpath = $functions[$function->name]['testclientpath']; 152 } 153 if (isset($functions[$function->name]['type'])) { 154 $function->type = $functions[$function->name]['type']; 155 } 156 if (isset($functions[$function->name]['ajax'])) { 157 $function->allowed_from_ajax = $functions[$function->name]['ajax']; 158 } else if (method_exists($function->classname, $function->ajax_method)) { 159 if (call_user_func(array($function->classname, $function->ajax_method)) === true) { 160 debugging('External function ' . $function->ajax_method . '() function is deprecated.' . 161 'Set ajax=>true in db/service.php instead.', DEBUG_DEVELOPER); 162 $function->allowed_from_ajax = true; 163 } 164 } 165 if (isset($functions[$function->name]['loginrequired'])) { 166 $function->loginrequired = $functions[$function->name]['loginrequired']; 167 } else { 168 $function->loginrequired = true; 169 } 170 if (isset($functions[$function->name]['readonlysession'])) { 171 $function->readonlysession = $functions[$function->name]['readonlysession']; 172 } else { 173 $function->readonlysession = false; 174 } 175 } 176 177 return $function; 178 } 179 180 /** 181 * Call an external function validating all params/returns correctly. 182 * 183 * Note that an external function may modify the state of the current page, so this wrapper 184 * saves and restores tha PAGE and COURSE global variables before/after calling the external function. 185 * 186 * @param string $function A webservice function name. 187 * @param array $args Params array (named params) 188 * @param boolean $ajaxonly If true, an extra check will be peformed to see if ajax is required. 189 * @return array containing keys for error (bool), exception and data. 190 */ 191 public static function call_external_function($function, $args, $ajaxonly=false) { 192 global $PAGE, $COURSE, $CFG, $SITE; 193 194 require_once($CFG->libdir . "/pagelib.php"); 195 196 $externalfunctioninfo = static::external_function_info($function); 197 198 // Eventually this should shift into the various handlers and not be handled via config. 199 $readonlysession = $externalfunctioninfo->readonlysession ?? false; 200 if (!$readonlysession || empty($CFG->enable_read_only_sessions)) { 201 \core\session\manager::restart_with_write_lock(); 202 } 203 204 $currentpage = $PAGE; 205 $currentcourse = $COURSE; 206 $response = array(); 207 208 try { 209 // Taken straight from from setup.php. 210 if (!empty($CFG->moodlepageclass)) { 211 if (!empty($CFG->moodlepageclassfile)) { 212 require_once($CFG->moodlepageclassfile); 213 } 214 $classname = $CFG->moodlepageclass; 215 } else { 216 $classname = 'moodle_page'; 217 } 218 $PAGE = new $classname(); 219 $COURSE = clone($SITE); 220 221 if ($ajaxonly && !$externalfunctioninfo->allowed_from_ajax) { 222 throw new moodle_exception('servicenotavailable', 'webservice'); 223 } 224 225 // Do not allow access to write or delete webservices as a public user. 226 if ($externalfunctioninfo->loginrequired && !WS_SERVER) { 227 if (defined('NO_MOODLE_COOKIES') && NO_MOODLE_COOKIES && !PHPUNIT_TEST) { 228 throw new moodle_exception('servicerequireslogin', 'webservice'); 229 } 230 if (!isloggedin()) { 231 throw new moodle_exception('servicerequireslogin', 'webservice'); 232 } else { 233 require_sesskey(); 234 } 235 } 236 // Validate params, this also sorts the params properly, we need the correct order in the next part. 237 $callable = array($externalfunctioninfo->classname, 'validate_parameters'); 238 $params = call_user_func($callable, 239 $externalfunctioninfo->parameters_desc, 240 $args); 241 $params = array_values($params); 242 243 // Allow any Moodle plugin a chance to override this call. This is a convenient spot to 244 // make arbitrary behaviour customisations. The overriding plugin could call the 'real' 245 // function first and then modify the results, or it could do a completely separate 246 // thing. 247 $callbacks = get_plugins_with_function('override_webservice_execution'); 248 $result = false; 249 foreach ($callbacks as $plugintype => $plugins) { 250 foreach ($plugins as $plugin => $callback) { 251 $result = $callback($externalfunctioninfo, $params); 252 if ($result !== false) { 253 break 2; 254 } 255 } 256 } 257 258 // If the function was not overridden, call the real one. 259 if ($result === false) { 260 $callable = array($externalfunctioninfo->classname, $externalfunctioninfo->methodname); 261 $result = call_user_func_array($callable, $params); 262 } 263 264 // Validate the return parameters. 265 if ($externalfunctioninfo->returns_desc !== null) { 266 $callable = array($externalfunctioninfo->classname, 'clean_returnvalue'); 267 $result = call_user_func($callable, $externalfunctioninfo->returns_desc, $result); 268 } 269 270 $response['error'] = false; 271 $response['data'] = $result; 272 } catch (Throwable $e) { 273 $exception = get_exception_info($e); 274 unset($exception->a); 275 $exception->backtrace = format_backtrace($exception->backtrace, true); 276 if (!debugging('', DEBUG_DEVELOPER)) { 277 unset($exception->debuginfo); 278 unset($exception->backtrace); 279 } 280 $response['error'] = true; 281 $response['exception'] = $exception; 282 // Do not process the remaining requests. 283 } 284 285 $PAGE = $currentpage; 286 $COURSE = $currentcourse; 287 288 return $response; 289 } 290 291 /** 292 * Set context restriction for all following subsequent function calls. 293 * 294 * @param stdClass $context the context restriction 295 * @since Moodle 2.0 296 */ 297 public static function set_context_restriction($context) { 298 self::$contextrestriction = $context; 299 } 300 301 /** 302 * This method has to be called before every operation 303 * that takes a longer time to finish! 304 * 305 * @param int $seconds max expected time the next operation needs 306 * @since Moodle 2.0 307 */ 308 public static function set_timeout($seconds=360) { 309 $seconds = ($seconds < 300) ? 300 : $seconds; 310 core_php_time_limit::raise($seconds); 311 } 312 313 /** 314 * Validates submitted function parameters, if anything is incorrect 315 * invalid_parameter_exception is thrown. 316 * This is a simple recursive method which is intended to be called from 317 * each implementation method of external API. 318 * 319 * @param external_description $description description of parameters 320 * @param mixed $params the actual parameters 321 * @return mixed params with added defaults for optional items, invalid_parameters_exception thrown if any problem found 322 * @since Moodle 2.0 323 */ 324 public static function validate_parameters(external_description $description, $params) { 325 if ($description instanceof external_value) { 326 if (is_array($params) or is_object($params)) { 327 throw new invalid_parameter_exception('Scalar type expected, array or object received.'); 328 } 329 330 if ($description->type == PARAM_BOOL) { 331 // special case for PARAM_BOOL - we want true/false instead of the usual 1/0 - we can not be too strict here ;-) 332 if (is_bool($params) or $params === 0 or $params === 1 or $params === '0' or $params === '1') { 333 return (bool)$params; 334 } 335 } 336 $debuginfo = 'Invalid external api parameter: the value is "' . $params . 337 '", the server was expecting "' . $description->type . '" type'; 338 return validate_param($params, $description->type, $description->allownull, $debuginfo); 339 340 } else if ($description instanceof external_single_structure) { 341 if (!is_array($params)) { 342 throw new invalid_parameter_exception('Only arrays accepted. The bad value is: \'' 343 . print_r($params, true) . '\''); 344 } 345 $result = array(); 346 foreach ($description->keys as $key=>$subdesc) { 347 if (!array_key_exists($key, $params)) { 348 if ($subdesc->required == VALUE_REQUIRED) { 349 throw new invalid_parameter_exception('Missing required key in single structure: '. $key); 350 } 351 if ($subdesc->required == VALUE_DEFAULT) { 352 try { 353 $result[$key] = static::validate_parameters($subdesc, $subdesc->default); 354 } catch (invalid_parameter_exception $e) { 355 //we are only interested by exceptions returned by validate_param() and validate_parameters() 356 //(in order to build the path to the faulty attribut) 357 throw new invalid_parameter_exception($key." => ".$e->getMessage() . ': ' .$e->debuginfo); 358 } 359 } 360 } else { 361 try { 362 $result[$key] = static::validate_parameters($subdesc, $params[$key]); 363 } catch (invalid_parameter_exception $e) { 364 //we are only interested by exceptions returned by validate_param() and validate_parameters() 365 //(in order to build the path to the faulty attribut) 366 throw new invalid_parameter_exception($key." => ".$e->getMessage() . ': ' .$e->debuginfo); 367 } 368 } 369 unset($params[$key]); 370 } 371 if (!empty($params)) { 372 throw new invalid_parameter_exception('Unexpected keys (' . implode(', ', array_keys($params)) . ') detected in parameter array.'); 373 } 374 return $result; 375 376 } else if ($description instanceof external_multiple_structure) { 377 if (!is_array($params)) { 378 throw new invalid_parameter_exception('Only arrays accepted. The bad value is: \'' 379 . print_r($params, true) . '\''); 380 } 381 $result = array(); 382 foreach ($params as $param) { 383 $result[] = static::validate_parameters($description->content, $param); 384 } 385 return $result; 386 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 ($description instanceof external_value) { 407 if (is_array($response) or is_object($response)) { 408 throw new invalid_response_exception('Scalar type expected, array or object received.'); 409 } 410 411 if ($description->type == PARAM_BOOL) { 412 // special case for PARAM_BOOL - we want true/false instead of the usual 1/0 - we can not be too strict here ;-) 413 if (is_bool($response) or $response === 0 or $response === 1 or $response === '0' or $response === '1') { 414 return (bool)$response; 415 } 416 } 417 $responsetype = gettype($response); 418 $debuginfo = 'Invalid external api response: the value is "' . $response . 419 '" of PHP type "' . $responsetype . '", the server was expecting "' . $description->type . '" type'; 420 try { 421 return validate_param($response, $description->type, $description->allownull, $debuginfo); 422 } catch (invalid_parameter_exception $e) { 423 //proper exception name, to be recursively catched to build the path to the faulty attribut 424 throw new invalid_response_exception($e->debuginfo); 425 } 426 427 } else if ($description instanceof external_single_structure) { 428 if (!is_array($response) && !is_object($response)) { 429 throw new invalid_response_exception('Only arrays/objects accepted. The bad value is: \'' . 430 print_r($response, true) . '\''); 431 } 432 433 // Cast objects into arrays. 434 if (is_object($response)) { 435 $response = (array) $response; 436 } 437 438 $result = array(); 439 foreach ($description->keys as $key=>$subdesc) { 440 if (!array_key_exists($key, $response)) { 441 if ($subdesc->required == VALUE_REQUIRED) { 442 throw new invalid_response_exception('Error in response - Missing following required key in a single structure: ' . $key); 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 attribut 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 attribut 459 throw new invalid_response_exception($key." => ".$e->getMessage() . ': ' . $e->debuginfo); 460 } 461 } 462 unset($response[$key]); 463 } 464 465 return $result; 466 467 } else if ($description instanceof external_multiple_structure) { 468 if (!is_array($response)) { 469 throw new invalid_response_exception('Only arrays accepted. The bad value is: \'' . 470 print_r($response, true) . '\''); 471 } 472 $result = array(); 473 foreach ($response as $param) { 474 $result[] = static::clean_returnvalue($description->content, $param); 475 } 476 return $result; 477 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 stdClass $context 487 * @since Moodle 2.0 488 */ 489 public static function validate_context($context) { 490 global $CFG, $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 list($unused, $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 context levels. 525 * 526 * @param array $param 527 * @since Moodle 2.6 528 * @throws invalid_parameter_exception 529 * @return context 530 */ 531 protected static function get_context_from_params($param) { 532 $levels = context_helper::get_all_levels(); 533 if (!empty($param['contextid'])) { 534 return context::instance_by_id($param['contextid'], IGNORE_MISSING); 535 } else if (!empty($param['contextlevel']) && isset($param['instanceid'])) { 536 $contextlevel = "context_".$param['contextlevel']; 537 if (!array_search($contextlevel, $levels)) { 538 throw new invalid_parameter_exception('Invalid context level = '.$param['contextlevel']); 539 } 540 return $contextlevel::instance($param['instanceid'], IGNORE_MISSING); 541 } else { 542 // No valid context info was found. 543 throw new invalid_parameter_exception('Missing parameters, please provide either context level with instance id or contextid'); 544 } 545 } 546 547 /** 548 * Returns a prepared structure to use a context parameters. 549 * @return external_single_structure 550 */ 551 protected static function get_context_parameters() { 552 $id = new external_value( 553 PARAM_INT, 554 'Context ID. Either use this value, or level and instanceid.', 555 VALUE_DEFAULT, 556 0 557 ); 558 $level = new external_value( 559 PARAM_ALPHA, 560 'Context level. To be used with instanceid.', 561 VALUE_DEFAULT, 562 '' 563 ); 564 $instanceid = new external_value( 565 PARAM_INT, 566 'Context instance ID. To be used with level', 567 VALUE_DEFAULT, 568 0 569 ); 570 return new external_single_structure(array( 571 'contextid' => $id, 572 'contextlevel' => $level, 573 'instanceid' => $instanceid, 574 )); 575 } 576 577 } 578 579 /** 580 * Common ancestor of all parameter description classes 581 * 582 * @package core_webservice 583 * @copyright 2009 Petr Skodak 584 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 585 * @since Moodle 2.0 586 */ 587 abstract class external_description { 588 /** @var string Description of element */ 589 public $desc; 590 591 /** @var bool Element value required, null not allowed */ 592 public $required; 593 594 /** @var mixed Default value */ 595 public $default; 596 597 /** 598 * Contructor 599 * 600 * @param string $desc 601 * @param bool $required 602 * @param mixed $default 603 * @since Moodle 2.0 604 */ 605 public function __construct($desc, $required, $default) { 606 $this->desc = $desc; 607 $this->required = $required; 608 $this->default = $default; 609 } 610 } 611 612 /** 613 * Scalar value description class 614 * 615 * @package core_webservice 616 * @copyright 2009 Petr Skodak 617 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 618 * @since Moodle 2.0 619 */ 620 class external_value extends external_description { 621 622 /** @var mixed Value type PARAM_XX */ 623 public $type; 624 625 /** @var bool Allow null values */ 626 public $allownull; 627 628 /** 629 * Constructor 630 * 631 * @param mixed $type 632 * @param string $desc 633 * @param bool $required 634 * @param mixed $default 635 * @param bool $allownull 636 * @since Moodle 2.0 637 */ 638 public function __construct($type, $desc='', $required=VALUE_REQUIRED, 639 $default=null, $allownull=NULL_ALLOWED) { 640 parent::__construct($desc, $required, $default); 641 $this->type = $type; 642 $this->allownull = $allownull; 643 } 644 } 645 646 /** 647 * Associative array description class 648 * 649 * @package core_webservice 650 * @copyright 2009 Petr Skodak 651 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 652 * @since Moodle 2.0 653 */ 654 class external_single_structure extends external_description { 655 656 /** @var array Description of array keys key=>external_description */ 657 public $keys; 658 659 /** 660 * Constructor 661 * 662 * @param array $keys 663 * @param string $desc 664 * @param bool $required 665 * @param array $default 666 * @since Moodle 2.0 667 */ 668 public function __construct(array $keys, $desc='', 669 $required=VALUE_REQUIRED, $default=null) { 670 parent::__construct($desc, $required, $default); 671 $this->keys = $keys; 672 } 673 } 674 675 /** 676 * Bulk array description class. 677 * 678 * @package core_webservice 679 * @copyright 2009 Petr Skodak 680 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 681 * @since Moodle 2.0 682 */ 683 class external_multiple_structure extends external_description { 684 685 /** @var external_description content */ 686 public $content; 687 688 /** 689 * Constructor 690 * 691 * @param external_description $content 692 * @param string $desc 693 * @param bool $required 694 * @param array $default 695 * @since Moodle 2.0 696 */ 697 public function __construct(external_description $content, $desc='', 698 $required=VALUE_REQUIRED, $default=null) { 699 parent::__construct($desc, $required, $default); 700 $this->content = $content; 701 } 702 } 703 704 /** 705 * Description of top level - PHP function parameters. 706 * 707 * @package core_webservice 708 * @copyright 2009 Petr Skodak 709 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 710 * @since Moodle 2.0 711 */ 712 class external_function_parameters extends external_single_structure { 713 714 /** 715 * Constructor - does extra checking to prevent top level optional parameters. 716 * 717 * @param array $keys 718 * @param string $desc 719 * @param bool $required 720 * @param array $default 721 */ 722 public function __construct(array $keys, $desc='', $required=VALUE_REQUIRED, $default=null) { 723 global $CFG; 724 725 if ($CFG->debugdeveloper) { 726 foreach ($keys as $key => $value) { 727 if ($value instanceof external_value) { 728 if ($value->required == VALUE_OPTIONAL) { 729 debugging('External function parameters: invalid OPTIONAL value specified.', DEBUG_DEVELOPER); 730 break; 731 } 732 } 733 } 734 } 735 parent::__construct($keys, $desc, $required, $default); 736 } 737 } 738 739 /** 740 * Generate a token 741 * 742 * @param string $tokentype EXTERNAL_TOKEN_EMBEDDED|EXTERNAL_TOKEN_PERMANENT 743 * @param stdClass|int $serviceorid service linked to the token 744 * @param int $userid user linked to the token 745 * @param stdClass|int $contextorid 746 * @param int $validuntil date when the token expired 747 * @param string $iprestriction allowed ip - if 0 or empty then all ips are allowed 748 * @return string generated token 749 * @author 2010 Jamie Pratt 750 * @since Moodle 2.0 751 */ 752 function external_generate_token($tokentype, $serviceorid, $userid, $contextorid, $validuntil=0, $iprestriction=''){ 753 global $DB, $USER; 754 // make sure the token doesn't exist (even if it should be almost impossible with the random generation) 755 $numtries = 0; 756 do { 757 $numtries ++; 758 $generatedtoken = md5(uniqid(rand(),1)); 759 if ($numtries > 5){ 760 throw new moodle_exception('tokengenerationfailed'); 761 } 762 } while ($DB->record_exists('external_tokens', array('token'=>$generatedtoken))); 763 $newtoken = new stdClass(); 764 $newtoken->token = $generatedtoken; 765 if (!is_object($serviceorid)){ 766 $service = $DB->get_record('external_services', array('id' => $serviceorid)); 767 } else { 768 $service = $serviceorid; 769 } 770 if (!is_object($contextorid)){ 771 $context = context::instance_by_id($contextorid, MUST_EXIST); 772 } else { 773 $context = $contextorid; 774 } 775 if (empty($service->requiredcapability) || has_capability($service->requiredcapability, $context, $userid)) { 776 $newtoken->externalserviceid = $service->id; 777 } else { 778 throw new moodle_exception('nocapabilitytousethisservice'); 779 } 780 $newtoken->tokentype = $tokentype; 781 $newtoken->userid = $userid; 782 if ($tokentype == EXTERNAL_TOKEN_EMBEDDED){ 783 $newtoken->sid = session_id(); 784 } 785 786 $newtoken->contextid = $context->id; 787 $newtoken->creatorid = $USER->id; 788 $newtoken->timecreated = time(); 789 $newtoken->validuntil = $validuntil; 790 if (!empty($iprestriction)) { 791 $newtoken->iprestriction = $iprestriction; 792 } 793 // Generate the private token, it must be transmitted only via https. 794 $newtoken->privatetoken = random_string(64); 795 $DB->insert_record('external_tokens', $newtoken); 796 return $newtoken->token; 797 } 798 799 /** 800 * Create and return a session linked token. Token to be used for html embedded client apps that want to communicate 801 * with the Moodle server through web services. The token is linked to the current session for the current page request. 802 * It is expected this will be called in the script generating the html page that is embedding the client app and that the 803 * returned token will be somehow passed into the client app being embedded in the page. 804 * 805 * @param string $servicename name of the web service. Service name as defined in db/services.php 806 * @param int $context context within which the web service can operate. 807 * @return int returns token id. 808 * @since Moodle 2.0 809 */ 810 function external_create_service_token($servicename, $context){ 811 global $USER, $DB; 812 $service = $DB->get_record('external_services', array('name'=>$servicename), '*', MUST_EXIST); 813 return external_generate_token(EXTERNAL_TOKEN_EMBEDDED, $service, $USER->id, $context, 0); 814 } 815 816 /** 817 * Delete all pre-built services (+ related tokens) and external functions information defined in the specified component. 818 * 819 * @param string $component name of component (moodle, mod_assignment, etc.) 820 */ 821 function external_delete_descriptions($component) { 822 global $DB; 823 824 $params = array($component); 825 826 $DB->delete_records_select('external_tokens', 827 "externalserviceid IN (SELECT id FROM {external_services} WHERE component = ?)", $params); 828 $DB->delete_records_select('external_services_users', 829 "externalserviceid IN (SELECT id FROM {external_services} WHERE component = ?)", $params); 830 $DB->delete_records_select('external_services_functions', 831 "functionname IN (SELECT name FROM {external_functions} WHERE component = ?)", $params); 832 $DB->delete_records('external_services', array('component'=>$component)); 833 $DB->delete_records('external_functions', array('component'=>$component)); 834 } 835 836 /** 837 * Standard Moodle web service warnings 838 * 839 * @package core_webservice 840 * @copyright 2012 Jerome Mouneyrac 841 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 842 * @since Moodle 2.3 843 */ 844 class external_warnings extends external_multiple_structure { 845 846 /** 847 * Constructor 848 * 849 * @since Moodle 2.3 850 */ 851 public function __construct($itemdesc = 'item', $itemiddesc = 'item id', 852 $warningcodedesc = 'the warning code can be used by the client app to implement specific behaviour') { 853 854 parent::__construct( 855 new external_single_structure( 856 array( 857 'item' => new external_value(PARAM_TEXT, $itemdesc, VALUE_OPTIONAL), 858 'itemid' => new external_value(PARAM_INT, $itemiddesc, VALUE_OPTIONAL), 859 'warningcode' => new external_value(PARAM_ALPHANUM, $warningcodedesc), 860 'message' => new external_value(PARAM_TEXT, 861 'untranslated english message to explain the warning') 862 ), 'warning'), 863 'list of warnings', VALUE_OPTIONAL); 864 } 865 } 866 867 /** 868 * A pre-filled external_value class for text format. 869 * 870 * Default is FORMAT_HTML 871 * This should be used all the time in external xxx_params()/xxx_returns functions 872 * as it is the standard way to implement text format param/return values. 873 * 874 * @package core_webservice 875 * @copyright 2012 Jerome Mouneyrac 876 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 877 * @since Moodle 2.3 878 */ 879 class external_format_value extends external_value { 880 881 /** 882 * Constructor 883 * 884 * @param string $textfieldname Name of the text field 885 * @param int $required if VALUE_REQUIRED then set standard default FORMAT_HTML 886 * @param int $default Default value. 887 * @since Moodle 2.3 888 */ 889 public function __construct($textfieldname, $required = VALUE_REQUIRED, $default = null) { 890 891 if ($default == null && $required == VALUE_DEFAULT) { 892 $default = FORMAT_HTML; 893 } 894 895 $desc = $textfieldname . ' format (' . FORMAT_HTML . ' = HTML, ' 896 . FORMAT_MOODLE . ' = MOODLE, ' 897 . FORMAT_PLAIN . ' = PLAIN or ' 898 . FORMAT_MARKDOWN . ' = MARKDOWN)'; 899 900 parent::__construct(PARAM_INT, $desc, $required, $default); 901 } 902 } 903 904 /** 905 * Validate text field format against known FORMAT_XXX 906 * 907 * @param array $format the format to validate 908 * @return the validated format 909 * @throws coding_exception 910 * @since Moodle 2.3 911 */ 912 function external_validate_format($format) { 913 $allowedformats = array(FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN); 914 if (!in_array($format, $allowedformats)) { 915 throw new moodle_exception('formatnotsupported', 'webservice', '' , null, 916 'The format with value=' . $format . ' is not supported by this Moodle site'); 917 } 918 return $format; 919 } 920 921 /** 922 * Format the string to be returned properly as requested by the either the web service server, 923 * either by an internally call. 924 * The caller can change the format (raw) with the external_settings singleton 925 * All web service servers must set this singleton when parsing the $_GET and $_POST. 926 * 927 * <pre> 928 * Options are the same that in {@link format_string()} with some changes: 929 * filter : Can be set to false to force filters off, else observes {@link external_settings}. 930 * </pre> 931 * 932 * @param string $str The string to be filtered. Should be plain text, expect 933 * possibly for multilang tags. 934 * @param boolean $striplinks To strip any link in the result text. Moodle 1.8 default changed from false to true! MDL-8713 935 * @param context|int $contextorid The id of the context for the string or the context (affects filters). 936 * @param array $options options array/object or courseid 937 * @return string text 938 * @since Moodle 3.0 939 */ 940 function external_format_string($str, $contextorid, $striplinks = true, $options = array()) { 941 942 // Get settings (singleton). 943 $settings = external_settings::get_instance(); 944 if (empty($contextorid)) { 945 throw new coding_exception('contextid is required'); 946 } 947 948 if (!$settings->get_raw()) { 949 if (is_object($contextorid) && is_a($contextorid, 'context')) { 950 $context = $contextorid; 951 } else { 952 $context = context::instance_by_id($contextorid); 953 } 954 $options['context'] = $context; 955 $options['filter'] = isset($options['filter']) && !$options['filter'] ? false : $settings->get_filter(); 956 $str = format_string($str, $striplinks, $options); 957 } 958 959 return $str; 960 } 961 962 /** 963 * Format the text to be returned properly as requested by the either the web service server, 964 * either by an internally call. 965 * The caller can change the format (raw, filter, file, fileurl) with the external_settings singleton 966 * All web service servers must set this singleton when parsing the $_GET and $_POST. 967 * 968 * <pre> 969 * Options are the same that in {@link format_text()} with some changes in defaults to provide backwards compatibility: 970 * trusted : If true the string won't be cleaned. Default false. 971 * noclean : If true the string won't be cleaned only if trusted is also true. Default false. 972 * nocache : If true the string will not be cached and will be formatted every call. Default false. 973 * filter : Can be set to false to force filters off, else observes {@link external_settings}. 974 * para : If true then the returned string will be wrapped in div tags. Default (different from format_text) false. 975 * Default changed because div tags are not commonly needed. 976 * newlines : If true then lines newline breaks will be converted to HTML newline breaks. Default true. 977 * context : Not used! Using contextid parameter instead. 978 * overflowdiv : If set to true the formatted text will be encased in a div with the class no-overflow before being 979 * returned. Default false. 980 * allowid : If true then id attributes will not be removed, even when using htmlpurifier. Default (different from 981 * format_text) true. Default changed id attributes are commonly needed. 982 * blanktarget : If true all <a> tags will have target="_blank" added unless target is explicitly specified. 983 * </pre> 984 * 985 * @param string $text The content that may contain ULRs in need of rewriting. 986 * @param int $textformat The text format. 987 * @param context|int $contextorid This parameter and the next two identify the file area to use. 988 * @param string $component 989 * @param string $filearea helps identify the file area. 990 * @param int $itemid helps identify the file area. 991 * @param object/array $options text formatting options 992 * @return array text + textformat 993 * @since Moodle 2.3 994 * @since Moodle 3.2 component, filearea and itemid are optional parameters 995 */ 996 function external_format_text($text, $textformat, $contextorid, $component = null, $filearea = null, $itemid = null, 997 $options = null) { 998 global $CFG; 999 1000 // Get settings (singleton). 1001 $settings = external_settings::get_instance(); 1002 1003 if (is_object($contextorid) && is_a($contextorid, 'context')) { 1004 $context = $contextorid; 1005 $contextid = $context->id; 1006 } else { 1007 $context = null; 1008 $contextid = $contextorid; 1009 } 1010 1011 if ($component and $filearea and $settings->get_fileurl()) { 1012 require_once($CFG->libdir . "/filelib.php"); 1013 $text = file_rewrite_pluginfile_urls($text, $settings->get_file(), $contextid, $component, $filearea, $itemid); 1014 } 1015 1016 // Note that $CFG->forceclean does not apply here if the client requests for the raw database content. 1017 // This is consistent with web clients that are still able to load non-cleaned text into editors, too. 1018 1019 if (!$settings->get_raw()) { 1020 $options = (array)$options; 1021 1022 // If context is passed in options, check that is the same to show a debug message. 1023 if (isset($options['context'])) { 1024 if ((is_object($options['context']) && $options['context']->id != $contextid) 1025 || (!is_object($options['context']) && $options['context'] != $contextid)) { 1026 debugging('Different contexts found in external_format_text parameters. $options[\'context\'] not allowed. 1027 Using $contextid parameter...', DEBUG_DEVELOPER); 1028 } 1029 } 1030 1031 $options['filter'] = isset($options['filter']) && !$options['filter'] ? false : $settings->get_filter(); 1032 $options['para'] = isset($options['para']) ? $options['para'] : false; 1033 $options['context'] = !is_null($context) ? $context : context::instance_by_id($contextid); 1034 $options['allowid'] = isset($options['allowid']) ? $options['allowid'] : true; 1035 1036 $text = format_text($text, $textformat, $options); 1037 $textformat = FORMAT_HTML; // Once converted to html (from markdown, plain... lets inform consumer this is already HTML). 1038 } 1039 1040 return array($text, $textformat); 1041 } 1042 1043 /** 1044 * Generate or return an existing token for the current authenticated user. 1045 * This function is used for creating a valid token for users authenticathing via login/token.php or admin/tool/mobile/launch.php. 1046 * 1047 * @param stdClass $service external service object 1048 * @return stdClass token object 1049 * @since Moodle 3.2 1050 * @throws moodle_exception 1051 */ 1052 function external_generate_token_for_current_user($service) { 1053 global $DB, $USER, $CFG; 1054 1055 core_user::require_active_user($USER, true, true); 1056 1057 // Check if there is any required system capability. 1058 if ($service->requiredcapability and !has_capability($service->requiredcapability, context_system::instance())) { 1059 throw new moodle_exception('missingrequiredcapability', 'webservice', '', $service->requiredcapability); 1060 } 1061 1062 // Specific checks related to user restricted service. 1063 if ($service->restrictedusers) { 1064 $authoriseduser = $DB->get_record('external_services_users', 1065 array('externalserviceid' => $service->id, 'userid' => $USER->id)); 1066 1067 if (empty($authoriseduser)) { 1068 throw new moodle_exception('usernotallowed', 'webservice', '', $service->shortname); 1069 } 1070 1071 if (!empty($authoriseduser->validuntil) and $authoriseduser->validuntil < time()) { 1072 throw new moodle_exception('invalidtimedtoken', 'webservice'); 1073 } 1074 1075 if (!empty($authoriseduser->iprestriction) and !address_in_subnet(getremoteaddr(), $authoriseduser->iprestriction)) { 1076 throw new moodle_exception('invalidiptoken', 'webservice'); 1077 } 1078 } 1079 1080 // Check if a token has already been created for this user and this service. 1081 $conditions = array( 1082 'userid' => $USER->id, 1083 'externalserviceid' => $service->id, 1084 'tokentype' => EXTERNAL_TOKEN_PERMANENT 1085 ); 1086 $tokens = $DB->get_records('external_tokens', $conditions, 'timecreated ASC'); 1087 1088 // A bit of sanity checks. 1089 foreach ($tokens as $key => $token) { 1090 1091 // Checks related to a specific token. (script execution continue). 1092 $unsettoken = false; 1093 // If sid is set then there must be a valid associated session no matter the token type. 1094 if (!empty($token->sid)) { 1095 if (!\core\session\manager::session_exists($token->sid)) { 1096 // This token will never be valid anymore, delete it. 1097 $DB->delete_records('external_tokens', array('sid' => $token->sid)); 1098 $unsettoken = true; 1099 } 1100 } 1101 1102 // Remove token is not valid anymore. 1103 if (!empty($token->validuntil) and $token->validuntil < time()) { 1104 $DB->delete_records('external_tokens', array('token' => $token->token, 'tokentype' => EXTERNAL_TOKEN_PERMANENT)); 1105 $unsettoken = true; 1106 } 1107 1108 // Remove token if its IP is restricted. 1109 if (isset($token->iprestriction) and !address_in_subnet(getremoteaddr(), $token->iprestriction)) { 1110 $unsettoken = true; 1111 } 1112 1113 if ($unsettoken) { 1114 unset($tokens[$key]); 1115 } 1116 } 1117 1118 // If some valid tokens exist then use the most recent. 1119 if (count($tokens) > 0) { 1120 $token = array_pop($tokens); 1121 } else { 1122 $context = context_system::instance(); 1123 $isofficialservice = $service->shortname == MOODLE_OFFICIAL_MOBILE_SERVICE; 1124 1125 if (($isofficialservice and has_capability('moodle/webservice:createmobiletoken', $context)) or 1126 (!is_siteadmin($USER) && has_capability('moodle/webservice:createtoken', $context))) { 1127 1128 // Create a new token. 1129 $token = new stdClass; 1130 $token->token = md5(uniqid(rand(), 1)); 1131 $token->userid = $USER->id; 1132 $token->tokentype = EXTERNAL_TOKEN_PERMANENT; 1133 $token->contextid = context_system::instance()->id; 1134 $token->creatorid = $USER->id; 1135 $token->timecreated = time(); 1136 $token->externalserviceid = $service->id; 1137 // By default tokens are valid for 12 weeks. 1138 $token->validuntil = $token->timecreated + $CFG->tokenduration; 1139 $token->iprestriction = null; 1140 $token->sid = null; 1141 $token->lastaccess = null; 1142 // Generate the private token, it must be transmitted only via https. 1143 $token->privatetoken = random_string(64); 1144 $token->id = $DB->insert_record('external_tokens', $token); 1145 1146 $eventtoken = clone $token; 1147 $eventtoken->privatetoken = null; 1148 $params = array( 1149 'objectid' => $eventtoken->id, 1150 'relateduserid' => $USER->id, 1151 'other' => array( 1152 'auto' => true 1153 ) 1154 ); 1155 $event = \core\event\webservice_token_created::create($params); 1156 $event->add_record_snapshot('external_tokens', $eventtoken); 1157 $event->trigger(); 1158 } else { 1159 throw new moodle_exception('cannotcreatetoken', 'webservice', '', $service->shortname); 1160 } 1161 } 1162 return $token; 1163 } 1164 1165 /** 1166 * Set the last time a token was sent and trigger the \core\event\webservice_token_sent event. 1167 * 1168 * This function is used when a token is generated by the user via login/token.php or admin/tool/mobile/launch.php. 1169 * In order to protect the privatetoken, we remove it from the event params. 1170 * 1171 * @param stdClass $token token object 1172 * @since Moodle 3.2 1173 */ 1174 function external_log_token_request($token) { 1175 global $DB; 1176 1177 $token->privatetoken = null; 1178 1179 // Log token access. 1180 $DB->set_field('external_tokens', 'lastaccess', time(), array('id' => $token->id)); 1181 1182 $params = array( 1183 'objectid' => $token->id, 1184 ); 1185 $event = \core\event\webservice_token_sent::create($params); 1186 $event->add_record_snapshot('external_tokens', $token); 1187 $event->trigger(); 1188 } 1189 1190 /** 1191 * Singleton to handle the external settings. 1192 * 1193 * We use singleton to encapsulate the "logic" 1194 * 1195 * @package core_webservice 1196 * @copyright 2012 Jerome Mouneyrac 1197 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 1198 * @since Moodle 2.3 1199 */ 1200 class external_settings { 1201 1202 /** @var object the singleton instance */ 1203 public static $instance = null; 1204 1205 /** @var boolean Should the external function return raw text or formatted */ 1206 private $raw = false; 1207 1208 /** @var boolean Should the external function filter the text */ 1209 private $filter = false; 1210 1211 /** @var boolean Should the external function rewrite plugin file url */ 1212 private $fileurl = true; 1213 1214 /** @var string In which file should the urls be rewritten */ 1215 private $file = 'webservice/pluginfile.php'; 1216 1217 /** @var string The session lang */ 1218 private $lang = ''; 1219 1220 /** @var string The timezone to use during this WS request */ 1221 private $timezone = ''; 1222 1223 /** 1224 * Constructor - protected - can not be instanciated 1225 */ 1226 protected function __construct() { 1227 if ((AJAX_SCRIPT == false) && (CLI_SCRIPT == false) && (WS_SERVER == false)) { 1228 // For normal pages, the default should match the default for format_text. 1229 $this->filter = true; 1230 // Use pluginfile.php for web requests. 1231 $this->file = 'pluginfile.php'; 1232 } 1233 } 1234 1235 /** 1236 * Clone - private - can not be cloned 1237 */ 1238 private final function __clone() { 1239 } 1240 1241 /** 1242 * Return only one instance 1243 * 1244 * @return \external_settings 1245 */ 1246 public static function get_instance() { 1247 if (self::$instance === null) { 1248 self::$instance = new external_settings; 1249 } 1250 1251 return self::$instance; 1252 } 1253 1254 /** 1255 * Set raw 1256 * 1257 * @param boolean $raw 1258 */ 1259 public function set_raw($raw) { 1260 $this->raw = $raw; 1261 } 1262 1263 /** 1264 * Get raw 1265 * 1266 * @return boolean 1267 */ 1268 public function get_raw() { 1269 return $this->raw; 1270 } 1271 1272 /** 1273 * Set filter 1274 * 1275 * @param boolean $filter 1276 */ 1277 public function set_filter($filter) { 1278 $this->filter = $filter; 1279 } 1280 1281 /** 1282 * Get filter 1283 * 1284 * @return boolean 1285 */ 1286 public function get_filter() { 1287 return $this->filter; 1288 } 1289 1290 /** 1291 * Set fileurl 1292 * 1293 * @param boolean $fileurl 1294 */ 1295 public function set_fileurl($fileurl) { 1296 $this->fileurl = $fileurl; 1297 } 1298 1299 /** 1300 * Get fileurl 1301 * 1302 * @return boolean 1303 */ 1304 public function get_fileurl() { 1305 return $this->fileurl; 1306 } 1307 1308 /** 1309 * Set file 1310 * 1311 * @param string $file 1312 */ 1313 public function set_file($file) { 1314 $this->file = $file; 1315 } 1316 1317 /** 1318 * Get file 1319 * 1320 * @return string 1321 */ 1322 public function get_file() { 1323 return $this->file; 1324 } 1325 1326 /** 1327 * Set lang 1328 * 1329 * @param string $lang 1330 */ 1331 public function set_lang($lang) { 1332 $this->lang = $lang; 1333 } 1334 1335 /** 1336 * Get lang 1337 * 1338 * @return string 1339 */ 1340 public function get_lang() { 1341 return $this->lang; 1342 } 1343 1344 /** 1345 * Set timezone 1346 * 1347 * @param string $timezone 1348 */ 1349 public function set_timezone($timezone) { 1350 $this->timezone = $timezone; 1351 } 1352 1353 /** 1354 * Get timezone 1355 * 1356 * @return string 1357 */ 1358 public function get_timezone() { 1359 return $this->timezone; 1360 } 1361 } 1362 1363 /** 1364 * Utility functions for the external API. 1365 * 1366 * @package core_webservice 1367 * @copyright 2015 Juan Leyva 1368 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 1369 * @since Moodle 3.0 1370 */ 1371 class external_util { 1372 1373 /** 1374 * Validate a list of courses, returning the complete course objects for valid courses. 1375 * 1376 * Each course has an additional 'contextvalidated' field, this will be set to true unless 1377 * you set $keepfails, in which case it will be false if validation fails for a course. 1378 * 1379 * @param array $courseids A list of course ids 1380 * @param array $courses An array of courses already pre-fetched, indexed by course id. 1381 * @param bool $addcontext True if the returned course object should include the full context object. 1382 * @param bool $keepfails True to keep all the course objects even if validation fails 1383 * @return array An array of courses and the validation warnings 1384 */ 1385 public static function validate_courses($courseids, $courses = array(), $addcontext = false, 1386 $keepfails = false) { 1387 global $DB; 1388 1389 // Delete duplicates. 1390 $courseids = array_unique($courseids); 1391 $warnings = array(); 1392 1393 // Remove courses which are not even requested. 1394 $courses = array_intersect_key($courses, array_flip($courseids)); 1395 1396 // For any courses NOT loaded already, get them in a single query (and preload contexts) 1397 // for performance. Preserve ordering because some tests depend on it. 1398 $newcourseids = []; 1399 foreach ($courseids as $cid) { 1400 if (!array_key_exists($cid, $courses)) { 1401 $newcourseids[] = $cid; 1402 } 1403 } 1404 if ($newcourseids) { 1405 list ($listsql, $listparams) = $DB->get_in_or_equal($newcourseids); 1406 1407 // Load list of courses, and preload associated contexts. 1408 $contextselect = context_helper::get_preload_record_columns_sql('x'); 1409 $newcourses = $DB->get_records_sql(" 1410 SELECT c.*, $contextselect 1411 FROM {course} c 1412 JOIN {context} x ON x.instanceid = c.id 1413 WHERE x.contextlevel = ? AND c.id $listsql", 1414 array_merge([CONTEXT_COURSE], $listparams)); 1415 foreach ($newcourseids as $cid) { 1416 if (array_key_exists($cid, $newcourses)) { 1417 $course = $newcourses[$cid]; 1418 context_helper::preload_from_record($course); 1419 $courses[$course->id] = $course; 1420 } 1421 } 1422 } 1423 1424 foreach ($courseids as $cid) { 1425 // Check the user can function in this context. 1426 try { 1427 $context = context_course::instance($cid); 1428 external_api::validate_context($context); 1429 1430 if ($addcontext) { 1431 $courses[$cid]->context = $context; 1432 } 1433 $courses[$cid]->contextvalidated = true; 1434 } catch (Exception $e) { 1435 if ($keepfails) { 1436 $courses[$cid]->contextvalidated = false; 1437 } else { 1438 unset($courses[$cid]); 1439 } 1440 $warnings[] = array( 1441 'item' => 'course', 1442 'itemid' => $cid, 1443 'warningcode' => '1', 1444 'message' => 'No access rights in course context' 1445 ); 1446 } 1447 } 1448 1449 return array($courses, $warnings); 1450 } 1451 1452 /** 1453 * Returns all area files (optionally limited by itemid). 1454 * 1455 * @param int $contextid context ID 1456 * @param string $component component 1457 * @param string $filearea file area 1458 * @param int $itemid item ID or all files if not specified 1459 * @param bool $useitemidinurl wether to use the item id in the file URL (modules intro don't use it) 1460 * @return array of files, compatible with the external_files structure. 1461 * @since Moodle 3.2 1462 */ 1463 public static function get_area_files($contextid, $component, $filearea, $itemid = false, $useitemidinurl = true) { 1464 $files = array(); 1465 $fs = get_file_storage(); 1466 1467 if ($areafiles = $fs->get_area_files($contextid, $component, $filearea, $itemid, 'itemid, filepath, filename', false)) { 1468 foreach ($areafiles as $areafile) { 1469 $file = array(); 1470 $file['filename'] = $areafile->get_filename(); 1471 $file['filepath'] = $areafile->get_filepath(); 1472 $file['mimetype'] = $areafile->get_mimetype(); 1473 $file['filesize'] = $areafile->get_filesize(); 1474 $file['timemodified'] = $areafile->get_timemodified(); 1475 $file['isexternalfile'] = $areafile->is_external_file(); 1476 if ($file['isexternalfile']) { 1477 $file['repositorytype'] = $areafile->get_repository_type(); 1478 } 1479 $fileitemid = $useitemidinurl ? $areafile->get_itemid() : null; 1480 $file['fileurl'] = moodle_url::make_webservice_pluginfile_url($contextid, $component, $filearea, 1481 $fileitemid, $areafile->get_filepath(), $areafile->get_filename())->out(false); 1482 $files[] = $file; 1483 } 1484 } 1485 return $files; 1486 } 1487 } 1488 1489 /** 1490 * External structure representing a set of files. 1491 * 1492 * @package core_webservice 1493 * @copyright 2016 Juan Leyva 1494 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 1495 * @since Moodle 3.2 1496 */ 1497 class external_files extends external_multiple_structure { 1498 1499 /** 1500 * Constructor 1501 * @param string $desc Description for the multiple structure. 1502 * @param int $required The type of value (VALUE_REQUIRED OR VALUE_OPTIONAL). 1503 */ 1504 public function __construct($desc = 'List of files.', $required = VALUE_REQUIRED) { 1505 1506 parent::__construct( 1507 new external_single_structure( 1508 array( 1509 'filename' => new external_value(PARAM_FILE, 'File name.', VALUE_OPTIONAL), 1510 'filepath' => new external_value(PARAM_PATH, 'File path.', VALUE_OPTIONAL), 1511 'filesize' => new external_value(PARAM_INT, 'File size.', VALUE_OPTIONAL), 1512 'fileurl' => new external_value(PARAM_URL, 'Downloadable file url.', VALUE_OPTIONAL), 1513 'timemodified' => new external_value(PARAM_INT, 'Time modified.', VALUE_OPTIONAL), 1514 'mimetype' => new external_value(PARAM_RAW, 'File mime type.', VALUE_OPTIONAL), 1515 'isexternalfile' => new external_value(PARAM_BOOL, 'Whether is an external file.', VALUE_OPTIONAL), 1516 'repositorytype' => new external_value(PARAM_PLUGIN, 'The repository type for external files.', VALUE_OPTIONAL), 1517 ), 1518 'File.' 1519 ), 1520 $desc, 1521 $required 1522 ); 1523 } 1524 1525 /** 1526 * Return the properties ready to be used by an exporter. 1527 * 1528 * @return array properties 1529 * @since Moodle 3.3 1530 */ 1531 public static function get_properties_for_exporter() { 1532 return [ 1533 'filename' => array( 1534 'type' => PARAM_FILE, 1535 'description' => 'File name.', 1536 'optional' => true, 1537 'null' => NULL_NOT_ALLOWED, 1538 ), 1539 'filepath' => array( 1540 'type' => PARAM_PATH, 1541 'description' => 'File path.', 1542 'optional' => true, 1543 'null' => NULL_NOT_ALLOWED, 1544 ), 1545 'filesize' => array( 1546 'type' => PARAM_INT, 1547 'description' => 'File size.', 1548 'optional' => true, 1549 'null' => NULL_NOT_ALLOWED, 1550 ), 1551 'fileurl' => array( 1552 'type' => PARAM_URL, 1553 'description' => 'Downloadable file url.', 1554 'optional' => true, 1555 'null' => NULL_NOT_ALLOWED, 1556 ), 1557 'timemodified' => array( 1558 'type' => PARAM_INT, 1559 'description' => 'Time modified.', 1560 'optional' => true, 1561 'null' => NULL_NOT_ALLOWED, 1562 ), 1563 'mimetype' => array( 1564 'type' => PARAM_RAW, 1565 'description' => 'File mime type.', 1566 'optional' => true, 1567 'null' => NULL_NOT_ALLOWED, 1568 ), 1569 'isexternalfile' => array( 1570 'type' => PARAM_BOOL, 1571 'description' => 'Whether is an external file.', 1572 'optional' => true, 1573 'null' => NULL_NOT_ALLOWED, 1574 ), 1575 'repositorytype' => array( 1576 'type' => PARAM_PLUGIN, 1577 'description' => 'The repository type for the external files.', 1578 'optional' => true, 1579 'null' => NULL_ALLOWED, 1580 ), 1581 ]; 1582 } 1583 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body