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 context; 20 use context_course; 21 use context_helper; 22 use context_system; 23 use core_user; 24 use moodle_exception; 25 use moodle_url; 26 use stdClass; 27 28 /** 29 * Utility functions for the external API. 30 * 31 * @package core_webservice 32 * @copyright 2015 Juan Leyva 33 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 34 */ 35 class util { 36 /** 37 * Validate a list of courses, returning the complete course objects for valid courses. 38 * 39 * Each course has an additional 'contextvalidated' field, this will be set to true unless 40 * you set $keepfails, in which case it will be false if validation fails for a course. 41 * 42 * @param array $courseids A list of course ids 43 * @param array $courses An array of courses already pre-fetched, indexed by course id. 44 * @param bool $addcontext True if the returned course object should include the full context object. 45 * @param bool $keepfails True to keep all the course objects even if validation fails 46 * @return array An array of courses and the validation warnings 47 */ 48 public static function validate_courses( 49 $courseids, 50 $courses = [], 51 $addcontext = false, 52 $keepfails = false 53 ) { 54 global $DB; 55 56 // Delete duplicates. 57 $courseids = array_unique($courseids); 58 $warnings = []; 59 60 // Remove courses which are not even requested. 61 $courses = array_intersect_key($courses, array_flip($courseids)); 62 63 // For any courses NOT loaded already, get them in a single query (and preload contexts) 64 // for performance. Preserve ordering because some tests depend on it. 65 $newcourseids = []; 66 foreach ($courseids as $cid) { 67 if (!array_key_exists($cid, $courses)) { 68 $newcourseids[] = $cid; 69 } 70 } 71 if ($newcourseids) { 72 [$listsql, $listparams] = $DB->get_in_or_equal($newcourseids); 73 74 // Load list of courses, and preload associated contexts. 75 $contextselect = context_helper::get_preload_record_columns_sql('x'); 76 $newcourses = $DB->get_records_sql( 77 " 78 SELECT c.*, $contextselect 79 FROM {course} c 80 JOIN {context} x ON x.instanceid = c.id 81 WHERE x.contextlevel = ? AND c.id $listsql", 82 array_merge([CONTEXT_COURSE], $listparams) 83 ); 84 foreach ($newcourseids as $cid) { 85 if (array_key_exists($cid, $newcourses)) { 86 $course = $newcourses[$cid]; 87 context_helper::preload_from_record($course); 88 $courses[$course->id] = $course; 89 } 90 } 91 } 92 93 foreach ($courseids as $cid) { 94 // Check the user can function in this context. 95 try { 96 $context = context_course::instance($cid); 97 external_api::validate_context($context); 98 99 if ($addcontext) { 100 $courses[$cid]->context = $context; 101 } 102 $courses[$cid]->contextvalidated = true; 103 } catch (\Exception $e) { 104 if ($keepfails) { 105 $courses[$cid]->contextvalidated = false; 106 } else { 107 unset($courses[$cid]); 108 } 109 $warnings[] = [ 110 'item' => 'course', 111 'itemid' => $cid, 112 'warningcode' => '1', 113 'message' => 'No access rights in course context', 114 ]; 115 } 116 } 117 118 return [$courses, $warnings]; 119 } 120 121 /** 122 * Returns all area files (optionally limited by itemid). 123 * 124 * @param int $contextid context ID 125 * @param string $component component 126 * @param string $filearea file area 127 * @param int $itemid item ID or all files if not specified 128 * @param bool $useitemidinurl wether to use the item id in the file URL (modules intro don't use it) 129 * @return array of files, compatible with the external_files structure. 130 * @since Moodle 3.2 131 */ 132 public static function get_area_files($contextid, $component, $filearea, $itemid = false, $useitemidinurl = true) { 133 $files = []; 134 $fs = get_file_storage(); 135 136 if ($areafiles = $fs->get_area_files($contextid, $component, $filearea, $itemid, 'itemid, filepath, filename', false)) { 137 foreach ($areafiles as $areafile) { 138 $file = []; 139 $file['filename'] = $areafile->get_filename(); 140 $file['filepath'] = $areafile->get_filepath(); 141 $file['mimetype'] = $areafile->get_mimetype(); 142 $file['filesize'] = $areafile->get_filesize(); 143 $file['timemodified'] = $areafile->get_timemodified(); 144 $file['isexternalfile'] = $areafile->is_external_file(); 145 if ($file['isexternalfile']) { 146 $file['repositorytype'] = $areafile->get_repository_type(); 147 } 148 $fileitemid = $useitemidinurl ? $areafile->get_itemid() : null; 149 $file['fileurl'] = moodle_url::make_webservice_pluginfile_url( 150 $contextid, 151 $component, 152 $filearea, 153 $fileitemid, 154 $areafile->get_filepath(), 155 $areafile->get_filename() 156 )->out(false); 157 $files[] = $file; 158 } 159 } 160 return $files; 161 } 162 163 164 /** 165 * Create and return a session linked token. Token to be used for html embedded client apps that want to communicate 166 * with the Moodle server through web services. The token is linked to the current session for the current page request. 167 * It is expected this will be called in the script generating the html page that is embedding the client app and that the 168 * returned token will be somehow passed into the client app being embedded in the page. 169 * 170 * @param int $tokentype EXTERNAL_TOKEN_EMBEDDED|EXTERNAL_TOKEN_PERMANENT 171 * @param stdClass $service service linked to the token 172 * @param int $userid user linked to the token 173 * @param context $context 174 * @param int $validuntil date when the token expired 175 * @param string $iprestriction allowed ip - if 0 or empty then all ips are allowed 176 * @param string $name token name as a note or token identity at the table view. 177 * @return string generated token 178 */ 179 public static function generate_token( 180 int $tokentype, 181 stdClass $service, 182 int $userid, 183 context $context, 184 int $validuntil = 0, 185 string $iprestriction = '', 186 string $name = '' 187 ): string { 188 global $DB, $USER, $SESSION; 189 190 // Make sure the token doesn't exist (even if it should be almost impossible with the random generation). 191 $numtries = 0; 192 do { 193 $numtries++; 194 $generatedtoken = md5(uniqid((string) rand(), true)); 195 if ($numtries > 5) { 196 throw new moodle_exception('tokengenerationfailed'); 197 } 198 } while ($DB->record_exists('external_tokens', ['token' => $generatedtoken])); 199 $newtoken = (object) [ 200 'token' => $generatedtoken, 201 ]; 202 203 if (empty($service->requiredcapability) || has_capability($service->requiredcapability, $context, $userid)) { 204 $newtoken->externalserviceid = $service->id; 205 } else { 206 throw new moodle_exception('nocapabilitytousethisservice'); 207 } 208 209 $newtoken->tokentype = $tokentype; 210 $newtoken->userid = $userid; 211 if ($tokentype == EXTERNAL_TOKEN_EMBEDDED) { 212 $newtoken->sid = session_id(); 213 } 214 215 $newtoken->contextid = $context->id; 216 $newtoken->creatorid = $USER->id; 217 $newtoken->timecreated = time(); 218 $newtoken->validuntil = $validuntil; 219 if (!empty($iprestriction)) { 220 $newtoken->iprestriction = $iprestriction; 221 } 222 223 // Generate the private token, it must be transmitted only via https. 224 $newtoken->privatetoken = random_string(64); 225 226 if (!$name) { 227 // Generate a token name. 228 $name = self::generate_token_name(); 229 } 230 $newtoken->name = $name; 231 232 $tokenid = $DB->insert_record('external_tokens', $newtoken); 233 // Create new session to hold newly created token ID. 234 $SESSION->webservicenewlycreatedtoken = $tokenid; 235 236 return $newtoken->token; 237 } 238 239 /** 240 * Get a service by its id. 241 * 242 * @param int $serviceid 243 * @return stdClass 244 */ 245 public static function get_service_by_id(int $serviceid): stdClass { 246 global $DB; 247 248 return $DB->get_record('external_services', ['id' => $serviceid], '*', MUST_EXIST); 249 } 250 251 /** 252 * Get a service by its name. 253 * 254 * @param string $name The service name. 255 * @return stdClass 256 */ 257 public static function get_service_by_name(string $name): stdClass { 258 global $DB; 259 260 return $DB->get_record('external_services', ['name' => $name], '*', MUST_EXIST); 261 } 262 263 /** 264 * Set the last time a token was sent and trigger the \core\event\webservice_token_sent event. 265 * 266 * This function is used when a token is generated by the user via login/token.php or admin/tool/mobile/launch.php. 267 * In order to protect the privatetoken, we remove it from the event params. 268 * 269 * @param stdClass $token token object 270 */ 271 public static function log_token_request(stdClass $token): void { 272 global $DB, $USER; 273 274 $token->privatetoken = null; 275 276 // Log token access. 277 $DB->set_field('external_tokens', 'lastaccess', time(), ['id' => $token->id]); 278 279 $event = \core\event\webservice_token_sent::create([ 280 'objectid' => $token->id, 281 ]); 282 $event->add_record_snapshot('external_tokens', $token); 283 $event->trigger(); 284 285 // Check if we need to notify the user about the new login via token. 286 $loginip = getremoteaddr(); 287 if ($USER->lastip === $loginip) { 288 return; 289 } 290 291 $shouldskip = WS_SERVER || CLI_SCRIPT || !NO_MOODLE_COOKIES; 292 if ($shouldskip && !PHPUNIT_TEST) { 293 return; 294 } 295 296 // Schedule adhoc task to sent a login notification to the user. 297 $task = new \core\task\send_login_notifications(); 298 $task->set_userid($USER->id); 299 $logintime = time(); 300 $task->set_custom_data([ 301 'useragent' => \core_useragent::get_user_agent_string(), 302 'ismoodleapp' => \core_useragent::is_moodle_app(), 303 'loginip' => $loginip, 304 'logintime' => $logintime, 305 ]); 306 $task->set_component('core'); 307 // We need sometime so the mobile app will send to Moodle the device information after login. 308 $task->set_next_run_time(time() + (2 * MINSECS)); 309 \core\task\manager::reschedule_or_queue_adhoc_task($task); 310 } 311 312 /** 313 * Generate or return an existing token for the current authenticated user. 314 * This function is used for creating a valid token for users authenticathing via places, including: 315 * - login/token.php 316 * - admin/tool/mobile/launch.php. 317 * 318 * @param stdClass $service external service object 319 * @return stdClass token object 320 * @throws moodle_exception 321 */ 322 public static function generate_token_for_current_user(stdClass $service) { 323 global $DB, $USER, $CFG; 324 325 core_user::require_active_user($USER, true, true); 326 327 // Check if there is any required system capability. 328 if ($service->requiredcapability && !has_capability($service->requiredcapability, context_system::instance())) { 329 throw new moodle_exception('missingrequiredcapability', 'webservice', '', $service->requiredcapability); 330 } 331 332 // Specific checks related to user restricted service. 333 if ($service->restrictedusers) { 334 $authoriseduser = $DB->get_record('external_services_users', [ 335 'externalserviceid' => $service->id, 336 'userid' => $USER->id, 337 ]); 338 339 if (empty($authoriseduser)) { 340 throw new moodle_exception('usernotallowed', 'webservice', '', $service->shortname); 341 } 342 343 if (!empty($authoriseduser->validuntil) && $authoriseduser->validuntil < time()) { 344 throw new moodle_exception('invalidtimedtoken', 'webservice'); 345 } 346 347 if (!empty($authoriseduser->iprestriction) && !address_in_subnet(getremoteaddr(), $authoriseduser->iprestriction)) { 348 throw new moodle_exception('invalidiptoken', 'webservice'); 349 } 350 } 351 352 // Check if a token has already been created for this user and this service. 353 $conditions = [ 354 'userid' => $USER->id, 355 'externalserviceid' => $service->id, 356 'tokentype' => EXTERNAL_TOKEN_PERMANENT, 357 ]; 358 $tokens = $DB->get_records('external_tokens', $conditions, 'timecreated ASC'); 359 360 // A bit of sanity checks. 361 foreach ($tokens as $key => $token) { 362 // Checks related to a specific token. (script execution continue). 363 $unsettoken = false; 364 // If sid is set then there must be a valid associated session no matter the token type. 365 if (!empty($token->sid)) { 366 if (!\core\session\manager::session_exists($token->sid)) { 367 // This token will never be valid anymore, delete it. 368 $DB->delete_records('external_tokens', ['sid' => $token->sid]); 369 $unsettoken = true; 370 } 371 } 372 373 // Remove token is not valid anymore. 374 if (!empty($token->validuntil) && $token->validuntil < time()) { 375 $DB->delete_records('external_tokens', ['token' => $token->token, 'tokentype' => EXTERNAL_TOKEN_PERMANENT]); 376 $unsettoken = true; 377 } 378 379 // Remove token if its IP is restricted. 380 if (isset($token->iprestriction) && !address_in_subnet(getremoteaddr(), $token->iprestriction)) { 381 $unsettoken = true; 382 } 383 384 if ($unsettoken) { 385 unset($tokens[$key]); 386 } 387 } 388 389 // If some valid tokens exist then use the most recent. 390 if (count($tokens) > 0) { 391 $token = array_pop($tokens); 392 } else { 393 $context = context_system::instance(); 394 $isofficialservice = $service->shortname == MOODLE_OFFICIAL_MOBILE_SERVICE; 395 396 if ( 397 ($isofficialservice && has_capability('moodle/webservice:createmobiletoken', $context)) || 398 (!is_siteadmin($USER) && has_capability('moodle/webservice:createtoken', $context)) 399 ) { 400 // Create a new token. 401 $token = new stdClass(); 402 $token->token = md5(uniqid((string) rand(), true)); 403 $token->userid = $USER->id; 404 $token->tokentype = EXTERNAL_TOKEN_PERMANENT; 405 $token->contextid = context_system::instance()->id; 406 $token->creatorid = $USER->id; 407 $token->timecreated = time(); 408 $token->externalserviceid = $service->id; 409 // By default tokens are valid for 12 weeks. 410 $token->validuntil = $token->timecreated + $CFG->tokenduration; 411 $token->iprestriction = null; 412 $token->sid = null; 413 $token->lastaccess = null; 414 $token->name = self::generate_token_name(); 415 // Generate the private token, it must be transmitted only via https. 416 $token->privatetoken = random_string(64); 417 $token->id = $DB->insert_record('external_tokens', $token); 418 419 $eventtoken = clone $token; 420 $eventtoken->privatetoken = null; 421 $params = [ 422 'objectid' => $eventtoken->id, 423 'relateduserid' => $USER->id, 424 'other' => [ 425 'auto' => true, 426 ], 427 ]; 428 $event = \core\event\webservice_token_created::create($params); 429 $event->add_record_snapshot('external_tokens', $eventtoken); 430 $event->trigger(); 431 } else { 432 throw new moodle_exception('cannotcreatetoken', 'webservice', '', $service->shortname); 433 } 434 } 435 return $token; 436 } 437 438 /** 439 * Format the string to be returned properly as requested by the either the web service server, 440 * either by an internally call. 441 * The caller can change the format (raw) with the settings singleton 442 * All web service servers must set this singleton when parsing the $_GET and $_POST. 443 * 444 * <pre> 445 * Options are the same that in {@see format_string()} with some changes: 446 * filter : Can be set to false to force filters off, else observes {@see settings}. 447 * </pre> 448 * 449 * @param string|null $content The string to be filtered. Should be plain text, expect 450 * possibly for multilang tags. 451 * @param context $context The id of the context for the string or the context (affects filters). 452 * @param boolean $striplinks To strip any link in the result text. Moodle 1.8 default changed from false to true! MDL-8713 453 * @param array $options options array/object or courseid 454 * @return string text 455 */ 456 public static function format_string( 457 $content, 458 $context, 459 $striplinks = true, 460 $options = [] 461 ) { 462 if ($content === null || $content === '') { 463 // Nothing to return. 464 // Note: It's common for the DB to return null, so we allow format_string to take a null, 465 // even though it is counter-intuitive. 466 return ''; 467 } 468 469 // Get settings (singleton). 470 $settings = external_settings::get_instance(); 471 472 if (!$settings->get_raw()) { 473 $options['context'] = $context; 474 $options['filter'] = isset($options['filter']) && !$options['filter'] ? false : $settings->get_filter(); 475 return format_string($content, $striplinks, $options); 476 } 477 478 return $content; 479 } 480 481 /** 482 * Format the text to be returned properly as requested by the either the web service server, 483 * either by an internally call. 484 * The caller can change the format (raw, filter, file, fileurl) with the \core_external\settings singleton 485 * All web service servers must set this singleton when parsing the $_GET and $_POST. 486 * 487 * <pre> 488 * Options are the same that in {@see format_text()} with some changes in defaults to provide backwards compatibility: 489 * trusted : If true the string won't be cleaned. Default false. 490 * noclean : If true the string won't be cleaned only if trusted is also true. Default false. 491 * nocache : If true the string will not be cached and will be formatted every call. Default false. 492 * filter : Can be set to false to force filters off, else observes {@see \core_external\settings}. 493 * para : If true then the returned string will be wrapped in div tags. 494 * Default (different from format_text) false. 495 * Default changed because div tags are not commonly needed. 496 * newlines : If true then lines newline breaks will be converted to HTML newline breaks. Default true. 497 * context : Not used! Using contextid parameter instead. 498 * overflowdiv : If set to true the formatted text will be encased in a div with the class no-overflow before being 499 * returned. Default false. 500 * allowid : If true then id attributes will not be removed, even when using htmlpurifier. Default (different from 501 * format_text) true. Default changed id attributes are commonly needed. 502 * blanktarget : If true all <a> tags will have target="_blank" added unless target is explicitly specified. 503 * </pre> 504 * 505 * @param string|null $text The content that may contain ULRs in need of rewriting. 506 * @param string|int|null $textformat The text format. 507 * @param context $context This parameter and the next two identify the file area to use. 508 * @param string|null $component 509 * @param string|null $filearea helps identify the file area. 510 * @param int|string|null $itemid helps identify the file area. 511 * @param array|stdClass|null $options text formatting options 512 * @return array text + textformat 513 */ 514 public static function format_text( 515 $text, 516 $textformat, 517 $context, 518 $component = null, 519 $filearea = null, 520 $itemid = null, 521 $options = null 522 ) { 523 global $CFG; 524 525 if ($text === null || $text === '') { 526 // Nothing to return. 527 // Note: It's common for the DB to return null, so we allow format_string to take nulls, 528 // even though it is counter-intuitive. 529 return ['', $textformat ?? FORMAT_MOODLE]; 530 } 531 532 if (empty($itemid)) { 533 $itemid = null; 534 } 535 536 // Get settings (singleton). 537 $settings = external_settings::get_instance(); 538 539 if ($component && $filearea && $settings->get_fileurl()) { 540 require_once($CFG->libdir . "/filelib.php"); 541 $text = file_rewrite_pluginfile_urls($text, $settings->get_file(), $context->id, $component, $filearea, $itemid); 542 } 543 544 // Note that $CFG->forceclean does not apply here if the client requests for the raw database content. 545 // This is consistent with web clients that are still able to load non-cleaned text into editors, too. 546 547 if (!$settings->get_raw()) { 548 $options = (array) $options; 549 550 // If context is passed in options, check that is the same to show a debug message. 551 if (isset($options['context'])) { 552 if (is_int($options['context'])) { 553 if ($options['context'] != $context->id) { 554 debugging( 555 'Different contexts found in external_format_text parameters. $options[\'context\'] not allowed. ' . 556 'Using $contextid parameter...', 557 DEBUG_DEVELOPER 558 ); 559 } 560 } else if ($options['context'] instanceof context) { 561 if ($options['context']->id != $context->id) { 562 debugging( 563 'Different contexts found in external_format_text parameters. $options[\'context\'] not allowed. ' . 564 'Using $contextid parameter...', 565 DEBUG_DEVELOPER 566 ); 567 } 568 } 569 } 570 571 $options['filter'] = isset($options['filter']) && !$options['filter'] ? false : $settings->get_filter(); 572 $options['para'] = isset($options['para']) ? $options['para'] : false; 573 $options['context'] = $context; 574 $options['allowid'] = isset($options['allowid']) ? $options['allowid'] : true; 575 576 $text = format_text($text, $textformat, $options); 577 // Once converted to html (from markdown, plain... lets inform consumer this is already HTML). 578 $textformat = FORMAT_HTML; 579 } 580 581 // Note: The formats defined in weblib are strings. 582 return [$text, $textformat]; 583 } 584 585 /** 586 * Validate text field format against known FORMAT_XXX 587 * 588 * @param string $format the format to validate 589 * @return string the validated format 590 * @throws \moodle_exception 591 * @since Moodle 2.3 592 */ 593 public static function validate_format($format) { 594 $allowedformats = [FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN]; 595 if (!in_array($format, $allowedformats)) { 596 throw new moodle_exception( 597 'formatnotsupported', 598 'webservice', 599 '', 600 null, 601 "The format with value={$format} is not supported by this Moodle site" 602 ); 603 } 604 return $format; 605 } 606 607 /** 608 * Delete all pre-built services, related tokens, and external functions information defined for the specified component. 609 * 610 * @param string $component The frankenstyle component name 611 */ 612 public static function delete_service_descriptions(string $component): void { 613 global $DB; 614 615 $params = [$component]; 616 617 $DB->delete_records_select( 618 'external_tokens', 619 "externalserviceid IN (SELECT id FROM {external_services} WHERE component = ?)", 620 $params 621 ); 622 $DB->delete_records_select( 623 'external_services_users', 624 "externalserviceid IN (SELECT id FROM {external_services} WHERE component = ?)", 625 $params 626 ); 627 $DB->delete_records_select( 628 'external_services_functions', 629 "functionname IN (SELECT name FROM {external_functions} WHERE component = ?)", 630 $params 631 ); 632 $DB->delete_records('external_services', ['component' => $component]); 633 $DB->delete_records('external_functions', ['component' => $component]); 634 } 635 636 /** 637 * Generate token name. 638 * 639 * @return string 640 */ 641 public static function generate_token_name(): string { 642 return get_string( 643 'tokennameprefix', 644 'webservice', 645 random_string(5) 646 ); 647 } 648 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body