See Release Notes
Long Term Support Release
Differences Between: [Versions 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 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 18 /** 19 * Web services utility functions and classes 20 * 21 * @package core_webservice 22 * @copyright 2009 Jerome Mouneyrac <jerome@moodle.com> 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 require_once($CFG->libdir.'/externallib.php'); 27 28 /** 29 * WEBSERVICE_AUTHMETHOD_USERNAME - username/password authentication (also called simple authentication) 30 */ 31 define('WEBSERVICE_AUTHMETHOD_USERNAME', 0); 32 33 /** 34 * WEBSERVICE_AUTHMETHOD_PERMANENT_TOKEN - most common token authentication (external app, mobile app...) 35 */ 36 define('WEBSERVICE_AUTHMETHOD_PERMANENT_TOKEN', 1); 37 38 /** 39 * WEBSERVICE_AUTHMETHOD_SESSION_TOKEN - token for embedded application (requires Moodle session) 40 */ 41 define('WEBSERVICE_AUTHMETHOD_SESSION_TOKEN', 2); 42 43 /** 44 * General web service library 45 * 46 * @package core_webservice 47 * @copyright 2010 Jerome Mouneyrac <jerome@moodle.com> 48 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 49 */ 50 class webservice { 51 /** 52 * Only update token last access once per this many seconds. (This constant controls update of 53 * the external tokens last access field. There is a similar define LASTACCESS_UPDATE_SECS 54 * which controls update of the web site last access fields.) 55 * 56 * @var int 57 */ 58 const TOKEN_LASTACCESS_UPDATE_SECS = 60; 59 60 /** 61 * Authenticate user (used by download/upload file scripts) 62 * 63 * @param string $token 64 * @return array - contains the authenticated user, token and service objects 65 */ 66 public function authenticate_user($token) { 67 global $DB, $CFG; 68 69 // web service must be enabled to use this script 70 if (!$CFG->enablewebservices) { 71 throw new webservice_access_exception('Web services are not enabled in Advanced features.'); 72 } 73 74 // Obtain token record 75 if (!$token = $DB->get_record('external_tokens', array('token' => $token))) { 76 //client may want to display login form => moodle_exception 77 throw new moodle_exception('invalidtoken', 'webservice'); 78 } 79 80 $loginfaileddefaultparams = array( 81 'other' => array( 82 'method' => WEBSERVICE_AUTHMETHOD_PERMANENT_TOKEN, 83 'reason' => null, 84 'tokenid' => $token->id 85 ) 86 ); 87 88 // Validate token date 89 if ($token->validuntil and $token->validuntil < time()) { 90 $params = $loginfaileddefaultparams; 91 $params['other']['reason'] = 'token_expired'; 92 $event = \core\event\webservice_login_failed::create($params); 93 $event->add_record_snapshot('external_tokens', $token); 94 $event->set_legacy_logdata(array(SITEID, 'webservice', get_string('tokenauthlog', 'webservice'), '', 95 get_string('invalidtimedtoken', 'webservice'), 0)); 96 $event->trigger(); 97 $DB->delete_records('external_tokens', array('token' => $token->token)); 98 throw new webservice_access_exception('Invalid token - token expired - check validuntil time for the token'); 99 } 100 101 // Check ip 102 if ($token->iprestriction and !address_in_subnet(getremoteaddr(), $token->iprestriction)) { 103 $params = $loginfaileddefaultparams; 104 $params['other']['reason'] = 'ip_restricted'; 105 $event = \core\event\webservice_login_failed::create($params); 106 $event->add_record_snapshot('external_tokens', $token); 107 $event->set_legacy_logdata(array(SITEID, 'webservice', get_string('tokenauthlog', 'webservice'), '', 108 get_string('failedtolog', 'webservice') . ": " . getremoteaddr(), 0)); 109 $event->trigger(); 110 throw new webservice_access_exception('Invalid token - IP:' . getremoteaddr() 111 . ' is not supported'); 112 } 113 114 //retrieve user link to the token 115 $user = $DB->get_record('user', array('id' => $token->userid, 'deleted' => 0), '*', MUST_EXIST); 116 117 // let enrol plugins deal with new enrolments if necessary 118 enrol_check_plugins($user); 119 120 // setup user session to check capability 121 \core\session\manager::set_user($user); 122 set_login_session_preferences(); 123 124 //assumes that if sid is set then there must be a valid associated session no matter the token type 125 if ($token->sid) { 126 if (!\core\session\manager::session_exists($token->sid)) { 127 $DB->delete_records('external_tokens', array('sid' => $token->sid)); 128 throw new webservice_access_exception('Invalid session based token - session not found or expired'); 129 } 130 } 131 132 // Cannot authenticate unless maintenance access is granted. 133 $hasmaintenanceaccess = has_capability('moodle/site:maintenanceaccess', context_system::instance(), $user); 134 if (!empty($CFG->maintenance_enabled) and !$hasmaintenanceaccess) { 135 //this is usually temporary, client want to implement code logic => moodle_exception 136 throw new moodle_exception('sitemaintenance', 'admin'); 137 } 138 139 //retrieve web service record 140 $service = $DB->get_record('external_services', array('id' => $token->externalserviceid, 'enabled' => 1)); 141 if (empty($service)) { 142 // will throw exception if no token found 143 throw new webservice_access_exception('Web service is not available (it doesn\'t exist or might be disabled)'); 144 } 145 146 //check if there is any required system capability 147 if ($service->requiredcapability and !has_capability($service->requiredcapability, context_system::instance(), $user)) { 148 throw new webservice_access_exception('The capability ' . $service->requiredcapability . ' is required.'); 149 } 150 151 //specific checks related to user restricted service 152 if ($service->restrictedusers) { 153 $authoriseduser = $DB->get_record('external_services_users', array('externalserviceid' => $service->id, 'userid' => $user->id)); 154 155 if (empty($authoriseduser)) { 156 throw new webservice_access_exception( 157 'The user is not allowed for this service. First you need to allow this user on the ' 158 . $service->name . '\'s allowed users administration page.'); 159 } 160 161 if (!empty($authoriseduser->validuntil) and $authoriseduser->validuntil < time()) { 162 throw new webservice_access_exception('Invalid service - service expired - check validuntil time for this allowed user'); 163 } 164 165 if (!empty($authoriseduser->iprestriction) and !address_in_subnet(getremoteaddr(), $authoriseduser->iprestriction)) { 166 throw new webservice_access_exception('Invalid service - IP:' . getremoteaddr() 167 . ' is not supported - check this allowed user'); 168 } 169 } 170 171 //only confirmed user should be able to call web service 172 if (empty($user->confirmed)) { 173 $params = $loginfaileddefaultparams; 174 $params['other']['reason'] = 'user_unconfirmed'; 175 $event = \core\event\webservice_login_failed::create($params); 176 $event->add_record_snapshot('external_tokens', $token); 177 $event->set_legacy_logdata(array(SITEID, 'webservice', 'user unconfirmed', '', $user->username)); 178 $event->trigger(); 179 throw new moodle_exception('usernotconfirmed', 'moodle', '', $user->username); 180 } 181 182 //check the user is suspended 183 if (!empty($user->suspended)) { 184 $params = $loginfaileddefaultparams; 185 $params['other']['reason'] = 'user_suspended'; 186 $event = \core\event\webservice_login_failed::create($params); 187 $event->add_record_snapshot('external_tokens', $token); 188 $event->set_legacy_logdata(array(SITEID, 'webservice', 'user suspended', '', $user->username)); 189 $event->trigger(); 190 throw new webservice_access_exception('Refused web service access for suspended username: ' . $user->username); 191 } 192 193 //check if the auth method is nologin (in this case refuse connection) 194 if ($user->auth == 'nologin') { 195 $params = $loginfaileddefaultparams; 196 $params['other']['reason'] = 'nologin'; 197 $event = \core\event\webservice_login_failed::create($params); 198 $event->add_record_snapshot('external_tokens', $token); 199 $event->set_legacy_logdata(array(SITEID, 'webservice', 'nologin auth attempt with web service', '', $user->username)); 200 $event->trigger(); 201 throw new webservice_access_exception('Refused web service access for nologin authentication username: ' . $user->username); 202 } 203 204 //Check if the user password is expired 205 $auth = get_auth_plugin($user->auth); 206 if (!empty($auth->config->expiration) and $auth->config->expiration == 1) { 207 $days2expire = $auth->password_expire($user->username); 208 if (intval($days2expire) < 0) { 209 $params = $loginfaileddefaultparams; 210 $params['other']['reason'] = 'password_expired'; 211 $event = \core\event\webservice_login_failed::create($params); 212 $event->add_record_snapshot('external_tokens', $token); 213 $event->set_legacy_logdata(array(SITEID, 'webservice', 'expired password', '', $user->username)); 214 $event->trigger(); 215 throw new moodle_exception('passwordisexpired', 'webservice'); 216 } 217 } 218 219 // log token access 220 self::update_token_lastaccess($token); 221 222 return array('user' => $user, 'token' => $token, 'service' => $service); 223 } 224 225 /** 226 * Updates the last access time for a token. 227 * 228 * @param \stdClass $token Token object (must include id, lastaccess fields) 229 * @param int $time Time of access (0 = use current time) 230 * @throws dml_exception If database error 231 */ 232 public static function update_token_lastaccess($token, int $time = 0) { 233 global $DB; 234 235 if (!$time) { 236 $time = time(); 237 } 238 239 // Only update the field if it is a different time from previous request, 240 // so as not to waste database effort. 241 if ($time >= $token->lastaccess + self::TOKEN_LASTACCESS_UPDATE_SECS) { 242 $DB->set_field('external_tokens', 'lastaccess', $time, array('id' => $token->id)); 243 } 244 } 245 246 /** 247 * Allow user to call a service 248 * 249 * @param stdClass $user a user 250 */ 251 public function add_ws_authorised_user($user) { 252 global $DB; 253 $user->timecreated = time(); 254 $DB->insert_record('external_services_users', $user); 255 } 256 257 /** 258 * Disallow a user to call a service 259 * 260 * @param stdClass $user a user 261 * @param int $serviceid 262 */ 263 public function remove_ws_authorised_user($user, $serviceid) { 264 global $DB; 265 $DB->delete_records('external_services_users', 266 array('externalserviceid' => $serviceid, 'userid' => $user->id)); 267 } 268 269 /** 270 * Update allowed user settings (ip restriction, valid until...) 271 * 272 * @param stdClass $user 273 */ 274 public function update_ws_authorised_user($user) { 275 global $DB; 276 $DB->update_record('external_services_users', $user); 277 } 278 279 /** 280 * Return list of allowed users with their options (ip/timecreated / validuntil...) 281 * for a given service 282 * 283 * @param int $serviceid the service id to search against 284 * @return array $users 285 */ 286 public function get_ws_authorised_users($serviceid) { 287 global $DB, $CFG; 288 $params = array($CFG->siteguest, $serviceid); 289 $sql = " SELECT u.id as id, esu.id as serviceuserid, u.email as email, u.firstname as firstname, 290 u.lastname as lastname, 291 esu.iprestriction as iprestriction, esu.validuntil as validuntil, 292 esu.timecreated as timecreated 293 FROM {user} u, {external_services_users} esu 294 WHERE u.id <> ? AND u.deleted = 0 AND u.confirmed = 1 295 AND esu.userid = u.id 296 AND esu.externalserviceid = ?"; 297 298 $users = $DB->get_records_sql($sql, $params); 299 return $users; 300 } 301 302 /** 303 * Return an authorised user with their options (ip/timecreated / validuntil...) 304 * 305 * @param int $serviceid the service id to search against 306 * @param int $userid the user to search against 307 * @return stdClass 308 */ 309 public function get_ws_authorised_user($serviceid, $userid) { 310 global $DB, $CFG; 311 $params = array($CFG->siteguest, $serviceid, $userid); 312 $sql = " SELECT u.id as id, esu.id as serviceuserid, u.email as email, u.firstname as firstname, 313 u.lastname as lastname, 314 esu.iprestriction as iprestriction, esu.validuntil as validuntil, 315 esu.timecreated as timecreated 316 FROM {user} u, {external_services_users} esu 317 WHERE u.id <> ? AND u.deleted = 0 AND u.confirmed = 1 318 AND esu.userid = u.id 319 AND esu.externalserviceid = ? 320 AND u.id = ?"; 321 $user = $DB->get_record_sql($sql, $params); 322 return $user; 323 } 324 325 /** 326 * Generate all tokens of a specific user 327 * 328 * @param int $userid user id 329 */ 330 public function generate_user_ws_tokens($userid) { 331 global $CFG, $DB; 332 333 // generate a token for non admin if web service are enable and the user has the capability to create a token 334 if (!is_siteadmin() && has_capability('moodle/webservice:createtoken', context_system::instance(), $userid) && !empty($CFG->enablewebservices)) { 335 // for every service than the user is authorised on, create a token (if it doesn't already exist) 336 337 // get all services which are set to all user (no restricted to specific users) 338 $norestrictedservices = $DB->get_records('external_services', array('restrictedusers' => 0)); 339 $serviceidlist = array(); 340 foreach ($norestrictedservices as $service) { 341 $serviceidlist[] = $service->id; 342 } 343 344 // get all services which are set to the current user (the current user is specified in the restricted user list) 345 $servicesusers = $DB->get_records('external_services_users', array('userid' => $userid)); 346 foreach ($servicesusers as $serviceuser) { 347 if (!in_array($serviceuser->externalserviceid,$serviceidlist)) { 348 $serviceidlist[] = $serviceuser->externalserviceid; 349 } 350 } 351 352 // get all services which already have a token set for the current user 353 $usertokens = $DB->get_records('external_tokens', array('userid' => $userid, 'tokentype' => EXTERNAL_TOKEN_PERMANENT)); 354 $tokenizedservice = array(); 355 foreach ($usertokens as $token) { 356 $tokenizedservice[] = $token->externalserviceid; 357 } 358 359 // create a token for the service which have no token already 360 foreach ($serviceidlist as $serviceid) { 361 if (!in_array($serviceid, $tokenizedservice)) { 362 // create the token for this service 363 $newtoken = new stdClass(); 364 $newtoken->token = md5(uniqid(rand(),1)); 365 // check that the user has capability on this service 366 $newtoken->tokentype = EXTERNAL_TOKEN_PERMANENT; 367 $newtoken->userid = $userid; 368 $newtoken->externalserviceid = $serviceid; 369 // TODO MDL-31190 find a way to get the context - UPDATE FOLLOWING LINE 370 $newtoken->contextid = context_system::instance()->id; 371 $newtoken->creatorid = $userid; 372 $newtoken->timecreated = time(); 373 // Generate the private token, it must be transmitted only via https. 374 $newtoken->privatetoken = random_string(64); 375 376 $DB->insert_record('external_tokens', $newtoken); 377 } 378 } 379 380 381 } 382 } 383 384 /** 385 * Return all tokens of a specific user 386 * + the service state (enabled/disabled) 387 * + the authorised user mode (restricted/not restricted) 388 * 389 * @param int $userid user id 390 * @return array 391 */ 392 public function get_user_ws_tokens($userid) { 393 global $DB; 394 //here retrieve token list (including linked users firstname/lastname and linked services name) 395 $sql = "SELECT 396 t.id, t.creatorid, t.token, u.firstname, u.lastname, s.id as wsid, s.name, s.enabled, s.restrictedusers, t.validuntil 397 FROM 398 {external_tokens} t, {user} u, {external_services} s 399 WHERE 400 t.userid=? AND t.tokentype = ".EXTERNAL_TOKEN_PERMANENT." AND s.id = t.externalserviceid AND t.userid = u.id"; 401 $tokens = $DB->get_records_sql($sql, array( $userid)); 402 return $tokens; 403 } 404 405 /** 406 * Return a token that has been created by the user (i.e. to created by an admin) 407 * If no tokens exist an exception is thrown 408 * 409 * The returned value is a stdClass: 410 * ->id token id 411 * ->token 412 * ->firstname user firstname 413 * ->lastname 414 * ->name service name 415 * 416 * @param int $userid user id 417 * @param int $tokenid token id 418 * @return stdClass 419 */ 420 public function get_created_by_user_ws_token($userid, $tokenid) { 421 global $DB; 422 $sql = "SELECT 423 t.id, t.token, u.firstname, u.lastname, s.name 424 FROM 425 {external_tokens} t, {user} u, {external_services} s 426 WHERE 427 t.creatorid=? AND t.id=? AND t.tokentype = " 428 . EXTERNAL_TOKEN_PERMANENT 429 . " AND s.id = t.externalserviceid AND t.userid = u.id"; 430 //must be the token creator 431 $token = $DB->get_record_sql($sql, array($userid, $tokenid), MUST_EXIST); 432 return $token; 433 } 434 435 /** 436 * Return a token of an arbitrary user by tokenid, including details of the associated user and the service name. 437 * If no tokens exist an exception is thrown 438 * 439 * The returned value is a stdClass: 440 * ->id token id 441 * ->token 442 * ->firstname user firstname 443 * ->lastname 444 * ->name service name 445 * 446 * @param int $tokenid token id 447 * @return stdClass 448 */ 449 public function get_token_by_id_with_details($tokenid) { 450 global $DB; 451 $sql = "SELECT t.id, t.token, u.id AS userid, u.firstname, u.lastname, s.name, t.creatorid 452 FROM {external_tokens} t, {user} u, {external_services} s 453 WHERE t.id=? AND t.tokentype = ? AND s.id = t.externalserviceid AND t.userid = u.id"; 454 $token = $DB->get_record_sql($sql, array($tokenid, EXTERNAL_TOKEN_PERMANENT), MUST_EXIST); 455 return $token; 456 } 457 458 /** 459 * Return a database token record for a token id 460 * 461 * @param int $tokenid token id 462 * @return object token 463 */ 464 public function get_token_by_id($tokenid) { 465 global $DB; 466 return $DB->get_record('external_tokens', array('id' => $tokenid)); 467 } 468 469 /** 470 * Delete a token 471 * 472 * @param int $tokenid token id 473 */ 474 public function delete_user_ws_token($tokenid) { 475 global $DB; 476 $DB->delete_records('external_tokens', array('id'=>$tokenid)); 477 } 478 479 /** 480 * Delete all the tokens belonging to a user. 481 * 482 * @param int $userid the user id whose tokens must be deleted 483 */ 484 public static function delete_user_ws_tokens($userid) { 485 global $DB; 486 $DB->delete_records('external_tokens', array('userid' => $userid)); 487 } 488 489 /** 490 * Delete a service 491 * Also delete function references and authorised user references. 492 * 493 * @param int $serviceid service id 494 */ 495 public function delete_service($serviceid) { 496 global $DB; 497 $DB->delete_records('external_services_users', array('externalserviceid' => $serviceid)); 498 $DB->delete_records('external_services_functions', array('externalserviceid' => $serviceid)); 499 $DB->delete_records('external_tokens', array('externalserviceid' => $serviceid)); 500 $DB->delete_records('external_services', array('id' => $serviceid)); 501 } 502 503 /** 504 * Get a full database token record for a given token value 505 * 506 * @param string $token 507 * @throws moodle_exception if there is multiple result 508 */ 509 public function get_user_ws_token($token) { 510 global $DB; 511 return $DB->get_record('external_tokens', array('token'=>$token), '*', MUST_EXIST); 512 } 513 514 /** 515 * Get the functions list of a service list (by id) 516 * 517 * @param array $serviceids service ids 518 * @return array of functions 519 */ 520 public function get_external_functions($serviceids) { 521 global $DB; 522 if (!empty($serviceids)) { 523 list($serviceids, $params) = $DB->get_in_or_equal($serviceids); 524 $sql = "SELECT f.* 525 FROM {external_functions} f 526 WHERE f.name IN (SELECT sf.functionname 527 FROM {external_services_functions} sf 528 WHERE sf.externalserviceid $serviceids) 529 ORDER BY f.name ASC"; 530 $functions = $DB->get_records_sql($sql, $params); 531 } else { 532 $functions = array(); 533 } 534 return $functions; 535 } 536 537 /** 538 * Get the functions of a service list (by shortname). It can return only enabled functions if required. 539 * 540 * @param array $serviceshortnames service shortnames 541 * @param bool $enabledonly if true then only return functions for services that have been enabled 542 * @return array functions 543 */ 544 public function get_external_functions_by_enabled_services($serviceshortnames, $enabledonly = true) { 545 global $DB; 546 if (!empty($serviceshortnames)) { 547 $enabledonlysql = $enabledonly?' AND s.enabled = 1 ':''; 548 list($serviceshortnames, $params) = $DB->get_in_or_equal($serviceshortnames); 549 $sql = "SELECT f.* 550 FROM {external_functions} f 551 WHERE f.name IN (SELECT sf.functionname 552 FROM {external_services_functions} sf, {external_services} s 553 WHERE s.shortname $serviceshortnames 554 AND sf.externalserviceid = s.id 555 " . $enabledonlysql . ")"; 556 $functions = $DB->get_records_sql($sql, $params); 557 } else { 558 $functions = array(); 559 } 560 return $functions; 561 } 562 563 /** 564 * Get functions not included in a service 565 * 566 * @param int $serviceid service id 567 * @return array functions 568 */ 569 public function get_not_associated_external_functions($serviceid) { 570 global $DB; 571 $select = "name NOT IN (SELECT s.functionname 572 FROM {external_services_functions} s 573 WHERE s.externalserviceid = :sid 574 )"; 575 576 $functions = $DB->get_records_select('external_functions', 577 $select, array('sid' => $serviceid), 'name'); 578 579 return $functions; 580 } 581 582 /** 583 * Get list of required capabilities of a service, sorted by functions 584 * Example of returned value: 585 * Array 586 * ( 587 * [core_group_create_groups] => Array 588 * ( 589 * [0] => moodle/course:managegroups 590 * ) 591 * 592 * [core_enrol_get_enrolled_users] => Array 593 * ( 594 * [0] => moodle/user:viewdetails 595 * [1] => moodle/user:viewhiddendetails 596 * [2] => moodle/course:useremail 597 * [3] => moodle/user:update 598 * [4] => moodle/site:accessallgroups 599 * ) 600 * ) 601 * @param int $serviceid service id 602 * @return array 603 */ 604 public function get_service_required_capabilities($serviceid) { 605 $functions = $this->get_external_functions(array($serviceid)); 606 $requiredusercaps = array(); 607 foreach ($functions as $function) { 608 $functioncaps = explode(',', $function->capabilities); 609 if (!empty($functioncaps) and !empty($functioncaps[0])) { 610 foreach ($functioncaps as $functioncap) { 611 $requiredusercaps[$function->name][] = trim($functioncap); 612 } 613 } 614 } 615 return $requiredusercaps; 616 } 617 618 /** 619 * Get user capabilities (with context) 620 * Only useful for documentation purpose 621 * WARNING: do not use this "broken" function. It was created in the goal to display some capabilities 622 * required by users. In theory we should not need to display this kind of information 623 * as the front end does not display it itself. In pratice, 624 * admins would like the info, for more info you can follow: MDL-29962 625 * 626 * @param int $userid user id 627 * @return array 628 */ 629 public function get_user_capabilities($userid) { 630 global $DB; 631 //retrieve the user capabilities 632 $sql = "SELECT DISTINCT rc.id, rc.capability FROM {role_capabilities} rc, {role_assignments} ra 633 WHERE rc.roleid=ra.roleid AND ra.userid= ? AND rc.permission = ?"; 634 $dbusercaps = $DB->get_records_sql($sql, array($userid, CAP_ALLOW)); 635 $usercaps = array(); 636 foreach ($dbusercaps as $usercap) { 637 $usercaps[$usercap->capability] = true; 638 } 639 return $usercaps; 640 } 641 642 /** 643 * Get missing user capabilities for a given service 644 * WARNING: do not use this "broken" function. It was created in the goal to display some capabilities 645 * required by users. In theory we should not need to display this kind of information 646 * as the front end does not display it itself. In pratice, 647 * admins would like the info, for more info you can follow: MDL-29962 648 * 649 * @param array $users users 650 * @param int $serviceid service id 651 * @return array of missing capabilities, keys being the user ids 652 */ 653 public function get_missing_capabilities_by_users($users, $serviceid) { 654 global $DB; 655 $usersmissingcaps = array(); 656 657 //retrieve capabilities required by the service 658 $servicecaps = $this->get_service_required_capabilities($serviceid); 659 660 //retrieve users missing capabilities 661 foreach ($users as $user) { 662 //cast user array into object to be a bit more flexible 663 if (is_array($user)) { 664 $user = (object) $user; 665 } 666 $usercaps = $this->get_user_capabilities($user->id); 667 668 //detect the missing capabilities 669 foreach ($servicecaps as $functioname => $functioncaps) { 670 foreach ($functioncaps as $functioncap) { 671 if (!array_key_exists($functioncap, $usercaps)) { 672 if (!isset($usersmissingcaps[$user->id]) 673 or array_search($functioncap, $usersmissingcaps[$user->id]) === false) { 674 $usersmissingcaps[$user->id][] = $functioncap; 675 } 676 } 677 } 678 } 679 } 680 681 return $usersmissingcaps; 682 } 683 684 /** 685 * Get an external service for a given service id 686 * 687 * @param int $serviceid service id 688 * @param int $strictness IGNORE_MISSING, MUST_EXIST... 689 * @return stdClass external service 690 */ 691 public function get_external_service_by_id($serviceid, $strictness=IGNORE_MISSING) { 692 global $DB; 693 $service = $DB->get_record('external_services', 694 array('id' => $serviceid), '*', $strictness); 695 return $service; 696 } 697 698 /** 699 * Get an external service for a given shortname 700 * 701 * @param string $shortname service shortname 702 * @param int $strictness IGNORE_MISSING, MUST_EXIST... 703 * @return stdClass external service 704 */ 705 public function get_external_service_by_shortname($shortname, $strictness=IGNORE_MISSING) { 706 global $DB; 707 $service = $DB->get_record('external_services', 708 array('shortname' => $shortname), '*', $strictness); 709 return $service; 710 } 711 712 /** 713 * Get an external function for a given function id 714 * 715 * @param int $functionid function id 716 * @param int $strictness IGNORE_MISSING, MUST_EXIST... 717 * @return stdClass external function 718 */ 719 public function get_external_function_by_id($functionid, $strictness=IGNORE_MISSING) { 720 global $DB; 721 $function = $DB->get_record('external_functions', 722 array('id' => $functionid), '*', $strictness); 723 return $function; 724 } 725 726 /** 727 * Add a function to a service 728 * 729 * @param string $functionname function name 730 * @param int $serviceid service id 731 */ 732 public function add_external_function_to_service($functionname, $serviceid) { 733 global $DB; 734 $addedfunction = new stdClass(); 735 $addedfunction->externalserviceid = $serviceid; 736 $addedfunction->functionname = $functionname; 737 $DB->insert_record('external_services_functions', $addedfunction); 738 } 739 740 /** 741 * Add a service 742 * It generates the timecreated field automatically. 743 * 744 * @param stdClass $service 745 * @return serviceid integer 746 */ 747 public function add_external_service($service) { 748 global $DB; 749 $service->timecreated = time(); 750 $serviceid = $DB->insert_record('external_services', $service); 751 return $serviceid; 752 } 753 754 /** 755 * Update a service 756 * It modifies the timemodified automatically. 757 * 758 * @param stdClass $service 759 */ 760 public function update_external_service($service) { 761 global $DB; 762 $service->timemodified = time(); 763 $DB->update_record('external_services', $service); 764 } 765 766 /** 767 * Test whether an external function is already linked to a service 768 * 769 * @param string $functionname function name 770 * @param int $serviceid service id 771 * @return bool true if a matching function exists for the service, else false. 772 * @throws dml_exception if error 773 */ 774 public function service_function_exists($functionname, $serviceid) { 775 global $DB; 776 return $DB->record_exists('external_services_functions', 777 array('externalserviceid' => $serviceid, 778 'functionname' => $functionname)); 779 } 780 781 /** 782 * Remove a function from a service 783 * 784 * @param string $functionname function name 785 * @param int $serviceid service id 786 */ 787 public function remove_external_function_from_service($functionname, $serviceid) { 788 global $DB; 789 $DB->delete_records('external_services_functions', 790 array('externalserviceid' => $serviceid, 'functionname' => $functionname)); 791 792 } 793 794 /** 795 * Return a list with all the valid user tokens for the given user, it only excludes expired tokens. 796 * 797 * @param string $userid user id to retrieve tokens from 798 * @return array array of token entries 799 * @since Moodle 3.2 800 */ 801 public static function get_active_tokens($userid) { 802 global $DB; 803 804 $sql = 'SELECT t.*, s.name as servicename FROM {external_tokens} t JOIN 805 {external_services} s ON t.externalserviceid = s.id WHERE 806 t.userid = :userid AND (t.validuntil IS NULL OR t.validuntil > :now)'; 807 $params = array('userid' => $userid, 'now' => time()); 808 return $DB->get_records_sql($sql, $params); 809 } 810 } 811 812 /** 813 * Exception indicating access control problem in web service call 814 * This exception should return general errors about web service setup. 815 * Errors related to the user like wrong username/password should not use it, 816 * you should not use this exception if you want to let the client implement 817 * some code logic against an access error. 818 * 819 * @package core_webservice 820 * @copyright 2009 Petr Skodak 821 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 822 */ 823 class webservice_access_exception extends moodle_exception { 824 825 /** 826 * Constructor 827 * 828 * @param string $debuginfo the debug info 829 */ 830 function __construct($debuginfo) { 831 parent::__construct('accessexception', 'webservice', '', null, $debuginfo); 832 } 833 } 834 835 /** 836 * Check if a protocol is enabled 837 * 838 * @param string $protocol name of WS protocol ('rest', 'soap', 'xmlrpc'...) 839 * @return bool true if the protocol is enabled 840 */ 841 function webservice_protocol_is_enabled($protocol) { 842 global $CFG; 843 844 if (empty($CFG->enablewebservices)) { 845 return false; 846 } 847 848 $active = explode(',', $CFG->webserviceprotocols); 849 850 return(in_array($protocol, $active)); 851 } 852 853 /** 854 * Mandatory interface for all test client classes. 855 * 856 * @package core_webservice 857 * @copyright 2009 Petr Skodak 858 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 859 */ 860 interface webservice_test_client_interface { 861 862 /** 863 * Execute test client WS request 864 * 865 * @param string $serverurl server url (including the token param) 866 * @param string $function web service function name 867 * @param array $params parameters of the web service function 868 * @return mixed 869 */ 870 public function simpletest($serverurl, $function, $params); 871 } 872 873 /** 874 * Mandatory interface for all web service protocol classes 875 * 876 * @package core_webservice 877 * @copyright 2009 Petr Skodak 878 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 879 */ 880 interface webservice_server_interface { 881 882 /** 883 * Process request from client. 884 */ 885 public function run(); 886 } 887 888 /** 889 * Abstract web service base class. 890 * 891 * @package core_webservice 892 * @copyright 2009 Petr Skodak 893 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 894 */ 895 abstract class webservice_server implements webservice_server_interface { 896 897 /** @var string Name of the web server plugin */ 898 protected $wsname = null; 899 900 /** @var string Name of local user */ 901 protected $username = null; 902 903 /** @var string Password of the local user */ 904 protected $password = null; 905 906 /** @var int The local user */ 907 protected $userid = null; 908 909 /** @var integer Authentication method one of WEBSERVICE_AUTHMETHOD_* */ 910 protected $authmethod; 911 912 /** @var string Authentication token*/ 913 protected $token = null; 914 915 /** @var stdClass Restricted context */ 916 protected $restricted_context; 917 918 /** @var int Restrict call to one service id*/ 919 protected $restricted_serviceid = null; 920 921 /** 922 * Constructor 923 * 924 * @param integer $authmethod authentication method one of WEBSERVICE_AUTHMETHOD_* 925 */ 926 public function __construct($authmethod) { 927 $this->authmethod = $authmethod; 928 } 929 930 931 /** 932 * Authenticate user using username+password or token. 933 * This function sets up $USER global. 934 * It is safe to use has_capability() after this. 935 * This method also verifies user is allowed to use this 936 * server. 937 */ 938 protected function authenticate_user() { 939 global $CFG, $DB; 940 941 if (!NO_MOODLE_COOKIES) { 942 throw new coding_exception('Cookies must be disabled in WS servers!'); 943 } 944 945 $loginfaileddefaultparams = array( 946 'other' => array( 947 'method' => $this->authmethod, 948 'reason' => null 949 ) 950 ); 951 952 if ($this->authmethod == WEBSERVICE_AUTHMETHOD_USERNAME) { 953 954 //we check that authentication plugin is enabled 955 //it is only required by simple authentication 956 if (!is_enabled_auth('webservice')) { 957 throw new webservice_access_exception('The web service authentication plugin is disabled.'); 958 } 959 960 if (!$auth = get_auth_plugin('webservice')) { 961 throw new webservice_access_exception('The web service authentication plugin is missing.'); 962 } 963 964 $this->restricted_context = context_system::instance(); 965 966 if (!$this->username) { 967 throw new moodle_exception('missingusername', 'webservice'); 968 } 969 970 if (!$this->password) { 971 throw new moodle_exception('missingpassword', 'webservice'); 972 } 973 974 if (!$auth->user_login_webservice($this->username, $this->password)) { 975 976 // Log failed login attempts. 977 $params = $loginfaileddefaultparams; 978 $params['other']['reason'] = 'password'; 979 $params['other']['username'] = $this->username; 980 $event = \core\event\webservice_login_failed::create($params); 981 $event->set_legacy_logdata(array(SITEID, 'webservice', get_string('simpleauthlog', 'webservice'), '' , 982 get_string('failedtolog', 'webservice').": ".$this->username."/".$this->password." - ".getremoteaddr() , 0)); 983 $event->trigger(); 984 985 throw new moodle_exception('wrongusernamepassword', 'webservice'); 986 } 987 988 $user = $DB->get_record('user', array('username'=>$this->username, 'mnethostid'=>$CFG->mnet_localhost_id), '*', MUST_EXIST); 989 990 } else if ($this->authmethod == WEBSERVICE_AUTHMETHOD_PERMANENT_TOKEN){ 991 $user = $this->authenticate_by_token(EXTERNAL_TOKEN_PERMANENT); 992 } else { 993 $user = $this->authenticate_by_token(EXTERNAL_TOKEN_EMBEDDED); 994 } 995 996 // Cannot authenticate unless maintenance access is granted. 997 $hasmaintenanceaccess = has_capability('moodle/site:maintenanceaccess', context_system::instance(), $user); 998 if (!empty($CFG->maintenance_enabled) and !$hasmaintenanceaccess) { 999 throw new moodle_exception('sitemaintenance', 'admin'); 1000 } 1001 1002 //only confirmed user should be able to call web service 1003 if (!empty($user->deleted)) { 1004 $params = $loginfaileddefaultparams; 1005 $params['other']['reason'] = 'user_deleted'; 1006 $params['other']['username'] = $user->username; 1007 $event = \core\event\webservice_login_failed::create($params); 1008 $event->set_legacy_logdata(array(SITEID, '', '', '', get_string('wsaccessuserdeleted', 'webservice', 1009 $user->username) . " - ".getremoteaddr(), 0, $user->id)); 1010 $event->trigger(); 1011 throw new webservice_access_exception('Refused web service access for deleted username: ' . $user->username); 1012 } 1013 1014 //only confirmed user should be able to call web service 1015 if (empty($user->confirmed)) { 1016 $params = $loginfaileddefaultparams; 1017 $params['other']['reason'] = 'user_unconfirmed'; 1018 $params['other']['username'] = $user->username; 1019 $event = \core\event\webservice_login_failed::create($params); 1020 $event->set_legacy_logdata(array(SITEID, '', '', '', get_string('wsaccessuserunconfirmed', 'webservice', 1021 $user->username) . " - ".getremoteaddr(), 0, $user->id)); 1022 $event->trigger(); 1023 throw new moodle_exception('wsaccessuserunconfirmed', 'webservice', '', $user->username); 1024 } 1025 1026 //check the user is suspended 1027 if (!empty($user->suspended)) { 1028 $params = $loginfaileddefaultparams; 1029 $params['other']['reason'] = 'user_unconfirmed'; 1030 $params['other']['username'] = $user->username; 1031 $event = \core\event\webservice_login_failed::create($params); 1032 $event->set_legacy_logdata(array(SITEID, '', '', '', get_string('wsaccessusersuspended', 'webservice', 1033 $user->username) . " - ".getremoteaddr(), 0, $user->id)); 1034 $event->trigger(); 1035 throw new webservice_access_exception('Refused web service access for suspended username: ' . $user->username); 1036 } 1037 1038 //retrieve the authentication plugin if no previously done 1039 if (empty($auth)) { 1040 $auth = get_auth_plugin($user->auth); 1041 } 1042 1043 // check if credentials have expired 1044 if (!empty($auth->config->expiration) and $auth->config->expiration == 1) { 1045 $days2expire = $auth->password_expire($user->username); 1046 if (intval($days2expire) < 0 ) { 1047 $params = $loginfaileddefaultparams; 1048 $params['other']['reason'] = 'password_expired'; 1049 $params['other']['username'] = $user->username; 1050 $event = \core\event\webservice_login_failed::create($params); 1051 $event->set_legacy_logdata(array(SITEID, '', '', '', get_string('wsaccessuserexpired', 'webservice', 1052 $user->username) . " - ".getremoteaddr(), 0, $user->id)); 1053 $event->trigger(); 1054 throw new webservice_access_exception('Refused web service access for password expired username: ' . $user->username); 1055 } 1056 } 1057 1058 //check if the auth method is nologin (in this case refuse connection) 1059 if ($user->auth=='nologin') { 1060 $params = $loginfaileddefaultparams; 1061 $params['other']['reason'] = 'login'; 1062 $params['other']['username'] = $user->username; 1063 $event = \core\event\webservice_login_failed::create($params); 1064 $event->set_legacy_logdata(array(SITEID, '', '', '', get_string('wsaccessusernologin', 'webservice', 1065 $user->username) . " - ".getremoteaddr(), 0, $user->id)); 1066 $event->trigger(); 1067 throw new webservice_access_exception('Refused web service access for nologin authentication username: ' . $user->username); 1068 } 1069 1070 // now fake user login, the session is completely empty too 1071 enrol_check_plugins($user); 1072 \core\session\manager::set_user($user); 1073 set_login_session_preferences(); 1074 $this->userid = $user->id; 1075 1076 if ($this->authmethod != WEBSERVICE_AUTHMETHOD_SESSION_TOKEN && !has_capability("webservice/$this->wsname:use", $this->restricted_context)) { 1077 throw new webservice_access_exception('You are not allowed to use the {$a} protocol (missing capability: webservice/' . $this->wsname . ':use)'); 1078 } 1079 1080 external_api::set_context_restriction($this->restricted_context); 1081 } 1082 1083 /** 1084 * User authentication by token 1085 * 1086 * @param string $tokentype token type (EXTERNAL_TOKEN_EMBEDDED or EXTERNAL_TOKEN_PERMANENT) 1087 * @return stdClass the authenticated user 1088 * @throws webservice_access_exception 1089 */ 1090 protected function authenticate_by_token($tokentype){ 1091 global $DB; 1092 1093 $loginfaileddefaultparams = array( 1094 'other' => array( 1095 'method' => $this->authmethod, 1096 'reason' => null 1097 ) 1098 ); 1099 1100 if (!$token = $DB->get_record('external_tokens', array('token'=>$this->token, 'tokentype'=>$tokentype))) { 1101 // Log failed login attempts. 1102 $params = $loginfaileddefaultparams; 1103 $params['other']['reason'] = 'invalid_token'; 1104 $event = \core\event\webservice_login_failed::create($params); 1105 $event->set_legacy_logdata(array(SITEID, 'webservice', get_string('tokenauthlog', 'webservice'), '' , 1106 get_string('failedtolog', 'webservice').": ".$this->token. " - ".getremoteaddr() , 0)); 1107 $event->trigger(); 1108 throw new moodle_exception('invalidtoken', 'webservice'); 1109 } 1110 1111 if ($token->validuntil and $token->validuntil < time()) { 1112 $DB->delete_records('external_tokens', array('token'=>$this->token, 'tokentype'=>$tokentype)); 1113 throw new webservice_access_exception('Invalid token - token expired - check validuntil time for the token'); 1114 } 1115 1116 if ($token->sid){//assumes that if sid is set then there must be a valid associated session no matter the token type 1117 if (!\core\session\manager::session_exists($token->sid)){ 1118 $DB->delete_records('external_tokens', array('sid'=>$token->sid)); 1119 throw new webservice_access_exception('Invalid session based token - session not found or expired'); 1120 } 1121 } 1122 1123 if ($token->iprestriction and !address_in_subnet(getremoteaddr(), $token->iprestriction)) { 1124 $params = $loginfaileddefaultparams; 1125 $params['other']['reason'] = 'ip_restricted'; 1126 $params['other']['tokenid'] = $token->id; 1127 $event = \core\event\webservice_login_failed::create($params); 1128 $event->add_record_snapshot('external_tokens', $token); 1129 $event->set_legacy_logdata(array(SITEID, 'webservice', get_string('tokenauthlog', 'webservice'), '' , 1130 get_string('failedtolog', 'webservice').": ".getremoteaddr() , 0)); 1131 $event->trigger(); 1132 throw new webservice_access_exception('Invalid service - IP:' . getremoteaddr() 1133 . ' is not supported - check this allowed user'); 1134 } 1135 1136 $this->restricted_context = context::instance_by_id($token->contextid); 1137 $this->restricted_serviceid = $token->externalserviceid; 1138 1139 $user = $DB->get_record('user', array('id'=>$token->userid), '*', MUST_EXIST); 1140 1141 // log token access 1142 webservice::update_token_lastaccess($token); 1143 1144 return $user; 1145 1146 } 1147 1148 /** 1149 * Intercept some moodlewssettingXXX $_GET and $_POST parameter 1150 * that are related to the web service call and are not the function parameters 1151 */ 1152 protected function set_web_service_call_settings() { 1153 global $CFG; 1154 1155 // Default web service settings. 1156 // Must be the same XXX key name as the external_settings::set_XXX function. 1157 // Must be the same XXX ws parameter name as 'moodlewssettingXXX'. 1158 $externalsettings = array( 1159 'raw' => array('default' => false, 'type' => PARAM_BOOL), 1160 'fileurl' => array('default' => true, 'type' => PARAM_BOOL), 1161 'filter' => array('default' => false, 'type' => PARAM_BOOL), 1162 'lang' => array('default' => '', 'type' => PARAM_LANG), 1163 ); 1164 1165 // Load the external settings with the web service settings. 1166 $settings = external_settings::get_instance(); 1167 foreach ($externalsettings as $name => $settingdata) { 1168 1169 $wsparamname = 'moodlewssetting' . $name; 1170 1171 // Retrieve and remove the setting parameter from the request. 1172 $value = optional_param($wsparamname, $settingdata['default'], $settingdata['type']); 1173 unset($_GET[$wsparamname]); 1174 unset($_POST[$wsparamname]); 1175 1176 $functioname = 'set_' . $name; 1177 $settings->$functioname($value); 1178 } 1179 1180 } 1181 } 1182 1183 /** 1184 * Web Service server base class. 1185 * 1186 * This class handles both simple and token authentication. 1187 * 1188 * @package core_webservice 1189 * @copyright 2009 Petr Skodak 1190 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 1191 */ 1192 abstract class webservice_base_server extends webservice_server { 1193 1194 /** @var array The function parameters - the real values submitted in the request */ 1195 protected $parameters = null; 1196 1197 /** @var string The name of the function that is executed */ 1198 protected $functionname = null; 1199 1200 /** @var stdClass Full function description */ 1201 protected $function = null; 1202 1203 /** @var mixed Function return value */ 1204 protected $returns = null; 1205 1206 /** @var array List of methods and their information provided by the web service. */ 1207 protected $servicemethods; 1208 1209 /** @var array List of struct classes generated for the web service methods. */ 1210 protected $servicestructs; 1211 1212 /** 1213 * This method parses the request input, it needs to get: 1214 * 1/ user authentication - username+password or token 1215 * 2/ function name 1216 * 3/ function parameters 1217 */ 1218 abstract protected function parse_request(); 1219 1220 /** 1221 * Send the result of function call to the WS client. 1222 */ 1223 abstract protected function send_response(); 1224 1225 /** 1226 * Send the error information to the WS client. 1227 * 1228 * @param exception $ex 1229 */ 1230 abstract protected function send_error($ex=null); 1231 1232 /** 1233 * Process request from client. 1234 * 1235 * @uses die 1236 */ 1237 public function run() { 1238 global $CFG, $SESSION; 1239 1240 // we will probably need a lot of memory in some functions 1241 raise_memory_limit(MEMORY_EXTRA); 1242 1243 // set some longer timeout, this script is not sending any output, 1244 // this means we need to manually extend the timeout operations 1245 // that need longer time to finish 1246 external_api::set_timeout(); 1247 1248 // set up exception handler first, we want to sent them back in correct format that 1249 // the other system understands 1250 // we do not need to call the original default handler because this ws handler does everything 1251 set_exception_handler(array($this, 'exception_handler')); 1252 1253 // init all properties from the request data 1254 $this->parse_request(); 1255 1256 // authenticate user, this has to be done after the request parsing 1257 // this also sets up $USER and $SESSION 1258 $this->authenticate_user(); 1259 1260 // find all needed function info and make sure user may actually execute the function 1261 $this->load_function_info(); 1262 1263 // Log the web service request. 1264 $params = array( 1265 'other' => array( 1266 'function' => $this->functionname 1267 ) 1268 ); 1269 $event = \core\event\webservice_function_called::create($params); 1270 $event->set_legacy_logdata(array(SITEID, 'webservice', $this->functionname, '' , getremoteaddr() , 0, $this->userid)); 1271 $event->trigger(); 1272 1273 // Do additional setup stuff. 1274 $settings = external_settings::get_instance(); 1275 $sessionlang = $settings->get_lang(); 1276 if (!empty($sessionlang)) { 1277 $SESSION->lang = $sessionlang; 1278 } 1279 1280 setup_lang_from_browser(); 1281 1282 if (empty($CFG->lang)) { 1283 if (empty($SESSION->lang)) { 1284 $CFG->lang = 'en'; 1285 } else { 1286 $CFG->lang = $SESSION->lang; 1287 } 1288 } 1289 1290 // finally, execute the function - any errors are catched by the default exception handler 1291 $this->execute(); 1292 1293 // send the results back in correct format 1294 $this->send_response(); 1295 1296 // session cleanup 1297 $this->session_cleanup(); 1298 1299 die; 1300 } 1301 1302 /** 1303 * Specialised exception handler, we can not use the standard one because 1304 * it can not just print html to output. 1305 * 1306 * @param exception $ex 1307 * $uses exit 1308 */ 1309 public function exception_handler($ex) { 1310 // detect active db transactions, rollback and log as error 1311 abort_all_db_transactions(); 1312 1313 // some hacks might need a cleanup hook 1314 $this->session_cleanup($ex); 1315 1316 // now let the plugin send the exception to client 1317 $this->send_error($ex); 1318 1319 // not much else we can do now, add some logging later 1320 exit(1); 1321 } 1322 1323 /** 1324 * Future hook needed for emulated sessions. 1325 * 1326 * @param exception $exception null means normal termination, $exception received when WS call failed 1327 */ 1328 protected function session_cleanup($exception=null) { 1329 if ($this->authmethod == WEBSERVICE_AUTHMETHOD_USERNAME) { 1330 // nothing needs to be done, there is no persistent session 1331 } else { 1332 // close emulated session if used 1333 } 1334 } 1335 1336 /** 1337 * Fetches the function description from database, 1338 * verifies user is allowed to use this function and 1339 * loads all paremeters and return descriptions. 1340 */ 1341 protected function load_function_info() { 1342 global $DB, $USER, $CFG; 1343 1344 if (empty($this->functionname)) { 1345 throw new invalid_parameter_exception('Missing function name'); 1346 } 1347 1348 // function must exist 1349 $function = external_api::external_function_info($this->functionname); 1350 1351 if ($this->restricted_serviceid) { 1352 $params = array('sid1'=>$this->restricted_serviceid, 'sid2'=>$this->restricted_serviceid); 1353 $wscond1 = 'AND s.id = :sid1'; 1354 $wscond2 = 'AND s.id = :sid2'; 1355 } else { 1356 $params = array(); 1357 $wscond1 = ''; 1358 $wscond2 = ''; 1359 } 1360 1361 // now let's verify access control 1362 1363 // now make sure the function is listed in at least one service user is allowed to use 1364 // allow access only if: 1365 // 1/ entry in the external_services_users table if required 1366 // 2/ validuntil not reached 1367 // 3/ has capability if specified in service desc 1368 // 4/ iprestriction 1369 1370 $sql = "SELECT s.*, NULL AS iprestriction 1371 FROM {external_services} s 1372 JOIN {external_services_functions} sf ON (sf.externalserviceid = s.id AND s.restrictedusers = 0 AND sf.functionname = :name1) 1373 WHERE s.enabled = 1 $wscond1 1374 1375 UNION 1376 1377 SELECT s.*, su.iprestriction 1378 FROM {external_services} s 1379 JOIN {external_services_functions} sf ON (sf.externalserviceid = s.id AND s.restrictedusers = 1 AND sf.functionname = :name2) 1380 JOIN {external_services_users} su ON (su.externalserviceid = s.id AND su.userid = :userid) 1381 WHERE s.enabled = 1 AND (su.validuntil IS NULL OR su.validuntil < :now) $wscond2"; 1382 $params = array_merge($params, array('userid'=>$USER->id, 'name1'=>$function->name, 'name2'=>$function->name, 'now'=>time())); 1383 1384 $rs = $DB->get_recordset_sql($sql, $params); 1385 // now make sure user may access at least one service 1386 $remoteaddr = getremoteaddr(); 1387 $allowed = false; 1388 foreach ($rs as $service) { 1389 if ($service->requiredcapability and !has_capability($service->requiredcapability, $this->restricted_context)) { 1390 continue; // cap required, sorry 1391 } 1392 if ($service->iprestriction and !address_in_subnet($remoteaddr, $service->iprestriction)) { 1393 continue; // wrong request source ip, sorry 1394 } 1395 $allowed = true; 1396 break; // one service is enough, no need to continue 1397 } 1398 $rs->close(); 1399 if (!$allowed) { 1400 throw new webservice_access_exception( 1401 'Access to the function '.$this->functionname.'() is not allowed. 1402 There could be multiple reasons for this: 1403 1. The service linked to the user token does not contain the function. 1404 2. The service is user-restricted and the user is not listed. 1405 3. The service is IP-restricted and the user IP is not listed. 1406 4. The service is time-restricted and the time has expired. 1407 5. The token is time-restricted and the time has expired. 1408 6. The service requires a specific capability which the user does not have. 1409 7. The function is called with username/password (no user token is sent) 1410 and none of the services has the function to allow the user. 1411 These settings can be found in Administration > Site administration 1412 > Plugins > Web services > External services and Manage tokens.'); 1413 } 1414 1415 // we have all we need now 1416 $this->function = $function; 1417 } 1418 1419 /** 1420 * Execute previously loaded function using parameters parsed from the request data. 1421 */ 1422 protected function execute() { 1423 // validate params, this also sorts the params properly, we need the correct order in the next part 1424 $params = call_user_func(array($this->function->classname, 'validate_parameters'), $this->function->parameters_desc, $this->parameters); 1425 $params = array_values($params); 1426 1427 // Allow any Moodle plugin a chance to override this call. This is a convenient spot to 1428 // make arbitrary behaviour customisations, for example to affect the mobile app behaviour. 1429 // The overriding plugin could call the 'real' function first and then modify the results, 1430 // or it could do a completely separate thing. 1431 $callbacks = get_plugins_with_function('override_webservice_execution'); 1432 foreach ($callbacks as $plugintype => $plugins) { 1433 foreach ($plugins as $plugin => $callback) { 1434 $result = $callback($this->function, $params); 1435 if ($result !== false) { 1436 // If the callback returns anything other than false, we assume it replaces the 1437 // real function. 1438 $this->returns = $result; 1439 return; 1440 } 1441 } 1442 } 1443 1444 // execute - yay! 1445 $this->returns = call_user_func_array(array($this->function->classname, $this->function->methodname), $params); 1446 } 1447 1448 /** 1449 * Load the virtual class needed for the web service. 1450 * 1451 * Initialises the virtual class that contains the web service functions that the user is allowed to use. 1452 * The web service function will be available if the user: 1453 * - is validly registered in the external_services_users table. 1454 * - has the required capability. 1455 * - meets the IP restriction requirement. 1456 * This virtual class can be used by web service protocols such as SOAP, especially when generating WSDL. 1457 */ 1458 protected function init_service_class() { 1459 global $USER, $DB; 1460 1461 // Initialise service methods and struct classes. 1462 $this->servicemethods = array(); 1463 $this->servicestructs = array(); 1464 1465 $params = array(); 1466 $wscond1 = ''; 1467 $wscond2 = ''; 1468 if ($this->restricted_serviceid) { 1469 $params = array('sid1' => $this->restricted_serviceid, 'sid2' => $this->restricted_serviceid); 1470 $wscond1 = 'AND s.id = :sid1'; 1471 $wscond2 = 'AND s.id = :sid2'; 1472 } 1473 1474 $sql = "SELECT s.*, NULL AS iprestriction 1475 FROM {external_services} s 1476 JOIN {external_services_functions} sf ON (sf.externalserviceid = s.id AND s.restrictedusers = 0) 1477 WHERE s.enabled = 1 $wscond1 1478 1479 UNION 1480 1481 SELECT s.*, su.iprestriction 1482 FROM {external_services} s 1483 JOIN {external_services_functions} sf ON (sf.externalserviceid = s.id AND s.restrictedusers = 1) 1484 JOIN {external_services_users} su ON (su.externalserviceid = s.id AND su.userid = :userid) 1485 WHERE s.enabled = 1 AND (su.validuntil IS NULL OR su.validuntil < :now) $wscond2"; 1486 $params = array_merge($params, array('userid' => $USER->id, 'now' => time())); 1487 1488 $serviceids = array(); 1489 $remoteaddr = getremoteaddr(); 1490 1491 // Query list of external services for the user. 1492 $rs = $DB->get_recordset_sql($sql, $params); 1493 1494 // Check which service ID to include. 1495 foreach ($rs as $service) { 1496 if (isset($serviceids[$service->id])) { 1497 continue; // Service already added. 1498 } 1499 if ($service->requiredcapability and !has_capability($service->requiredcapability, $this->restricted_context)) { 1500 continue; // Cap required, sorry. 1501 } 1502 if ($service->iprestriction and !address_in_subnet($remoteaddr, $service->iprestriction)) { 1503 continue; // Wrong request source ip, sorry. 1504 } 1505 $serviceids[$service->id] = $service->id; 1506 } 1507 $rs->close(); 1508 1509 // Generate the virtual class name. 1510 $classname = 'webservices_virtual_class_000000'; 1511 while (class_exists($classname)) { 1512 $classname++; 1513 } 1514 $this->serviceclass = $classname; 1515 1516 // Get the list of all available external functions. 1517 $wsmanager = new webservice(); 1518 $functions = $wsmanager->get_external_functions($serviceids); 1519 1520 // Generate code for the virtual methods for this web service. 1521 $methods = ''; 1522 foreach ($functions as $function) { 1523 $methods .= $this->get_virtual_method_code($function); 1524 } 1525 1526 $code = <<<EOD 1527 /** 1528 * Virtual class web services for user id $USER->id in context {$this->restricted_context->id}. 1529 */ 1530 class $classname { 1531 $methods 1532 } 1533 EOD; 1534 // Load the virtual class definition into memory. 1535 eval($code); 1536 } 1537 1538 /** 1539 * Generates a struct class. 1540 * 1541 * @param external_single_structure $structdesc The basis of the struct class to be generated. 1542 * @return string The class name of the generated struct class. 1543 */ 1544 protected function generate_simple_struct_class(external_single_structure $structdesc) { 1545 global $USER; 1546 1547 $propeties = array(); 1548 $fields = array(); 1549 foreach ($structdesc->keys as $name => $fieldsdesc) { 1550 $type = $this->get_phpdoc_type($fieldsdesc); 1551 $propertytype = array('type' => $type); 1552 if (empty($fieldsdesc->allownull) || $fieldsdesc->allownull == NULL_ALLOWED) { 1553 $propertytype['nillable'] = true; 1554 } 1555 $propeties[$name] = $propertytype; 1556 $fields[] = ' /** @var ' . $type . ' $' . $name . '*/'; 1557 $fields[] = ' public $' . $name .';'; 1558 } 1559 $fieldsstr = implode("\n", $fields); 1560 1561 // We do this after the call to get_phpdoc_type() to avoid duplicate class creation. 1562 $classname = 'webservices_struct_class_000000'; 1563 while (class_exists($classname)) { 1564 $classname++; 1565 } 1566 $code = <<<EOD 1567 /** 1568 * Virtual struct class for web services for user id $USER->id in context {$this->restricted_context->id}. 1569 */ 1570 class $classname { 1571 $fieldsstr 1572 } 1573 EOD; 1574 // Load into memory. 1575 eval($code); 1576 1577 // Prepare struct info. 1578 $structinfo = new stdClass(); 1579 $structinfo->classname = $classname; 1580 $structinfo->properties = $propeties; 1581 // Add the struct info the the list of service struct classes. 1582 $this->servicestructs[] = $structinfo; 1583 1584 return $classname; 1585 } 1586 1587 /** 1588 * Returns a virtual method code for a web service function. 1589 * 1590 * @param stdClass $function a record from external_function 1591 * @return string The PHP code of the virtual method. 1592 * @throws coding_exception 1593 * @throws moodle_exception 1594 */ 1595 protected function get_virtual_method_code($function) { 1596 $function = external_api::external_function_info($function); 1597 1598 // Parameters and their defaults for the method signature. 1599 $paramanddefaults = array(); 1600 // Parameters for external lib call. 1601 $params = array(); 1602 $paramdesc = array(); 1603 // The method's input parameters and their respective types. 1604 $inputparams = array(); 1605 // The method's output parameters and their respective types. 1606 $outputparams = array(); 1607 1608 foreach ($function->parameters_desc->keys as $name => $keydesc) { 1609 $param = '$' . $name; 1610 $paramanddefault = $param; 1611 if ($keydesc->required == VALUE_OPTIONAL) { 1612 // It does not make sense to declare a parameter VALUE_OPTIONAL. VALUE_OPTIONAL is used only for array/object key. 1613 throw new moodle_exception('erroroptionalparamarray', 'webservice', '', $name); 1614 } else if ($keydesc->required == VALUE_DEFAULT) { 1615 // Need to generate the default, if there is any. 1616 if ($keydesc instanceof external_value) { 1617 if ($keydesc->default === null) { 1618 $paramanddefault .= ' = null'; 1619 } else { 1620 switch ($keydesc->type) { 1621 case PARAM_BOOL: 1622 $default = (int)$keydesc->default; 1623 break; 1624 case PARAM_INT: 1625 $default = $keydesc->default; 1626 break; 1627 case PARAM_FLOAT; 1628 $default = $keydesc->default; 1629 break; 1630 default: 1631 $default = "'$keydesc->default'"; 1632 } 1633 $paramanddefault .= " = $default"; 1634 } 1635 } else { 1636 // Accept empty array as default. 1637 if (isset($keydesc->default) && is_array($keydesc->default) && empty($keydesc->default)) { 1638 $paramanddefault .= ' = array()'; 1639 } else { 1640 // For the moment we do not support default for other structure types. 1641 throw new moodle_exception('errornotemptydefaultparamarray', 'webservice', '', $name); 1642 } 1643 } 1644 } 1645 1646 $params[] = $param; 1647 $paramanddefaults[] = $paramanddefault; 1648 $type = $this->get_phpdoc_type($keydesc); 1649 $inputparams[$name]['type'] = $type; 1650 1651 $paramdesc[] = '* @param ' . $type . ' $' . $name . ' ' . $keydesc->desc; 1652 } 1653 $paramanddefaults = implode(', ', $paramanddefaults); 1654 $paramdescstr = implode("\n ", $paramdesc); 1655 1656 $serviceclassmethodbody = $this->service_class_method_body($function, $params); 1657 1658 if (empty($function->returns_desc)) { 1659 $return = '* @return void'; 1660 } else { 1661 $type = $this->get_phpdoc_type($function->returns_desc); 1662 $outputparams['return']['type'] = $type; 1663 $return = '* @return ' . $type . ' ' . $function->returns_desc->desc; 1664 } 1665 1666 // Now create the virtual method that calls the ext implementation. 1667 $code = <<<EOD 1668 /** 1669 * $function->description. 1670 * 1671 $paramdescstr 1672 $return 1673 */ 1674 public function $function->name($paramanddefaults) { 1675 $serviceclassmethodbody 1676 } 1677 EOD; 1678 1679 // Prepare the method information. 1680 $methodinfo = new stdClass(); 1681 $methodinfo->name = $function->name; 1682 $methodinfo->inputparams = $inputparams; 1683 $methodinfo->outputparams = $outputparams; 1684 $methodinfo->description = $function->description; 1685 // Add the method information into the list of service methods. 1686 $this->servicemethods[] = $methodinfo; 1687 1688 return $code; 1689 } 1690 1691 /** 1692 * Get the phpdoc type for an external_description object. 1693 * external_value => int, double or string 1694 * external_single_structure => object|struct, on-fly generated stdClass name. 1695 * external_multiple_structure => array 1696 * 1697 * @param mixed $keydesc The type description. 1698 * @return string The PHP doc type of the external_description object. 1699 */ 1700 protected function get_phpdoc_type($keydesc) { 1701 $type = null; 1702 if ($keydesc instanceof external_value) { 1703 switch ($keydesc->type) { 1704 case PARAM_BOOL: // 0 or 1 only for now. 1705 case PARAM_INT: 1706 $type = 'int'; 1707 break; 1708 case PARAM_FLOAT; 1709 $type = 'double'; 1710 break; 1711 default: 1712 $type = 'string'; 1713 } 1714 } else if ($keydesc instanceof external_single_structure) { 1715 $type = $this->generate_simple_struct_class($keydesc); 1716 } else if ($keydesc instanceof external_multiple_structure) { 1717 $type = 'array'; 1718 } 1719 1720 return $type; 1721 } 1722 1723 /** 1724 * Generates the method body of the virtual external function. 1725 * 1726 * @param stdClass $function a record from external_function. 1727 * @param array $params web service function parameters. 1728 * @return string body of the method for $function ie. everything within the {} of the method declaration. 1729 */ 1730 protected function service_class_method_body($function, $params) { 1731 // Cast the param from object to array (validate_parameters except array only). 1732 $castingcode = ''; 1733 $paramsstr = ''; 1734 if (!empty($params)) { 1735 foreach ($params as $paramtocast) { 1736 // Clean the parameter from any white space. 1737 $paramtocast = trim($paramtocast); 1738 $castingcode .= " $paramtocast = json_decode(json_encode($paramtocast), true);\n"; 1739 } 1740 $paramsstr = implode(', ', $params); 1741 } 1742 1743 $descriptionmethod = $function->methodname . '_returns()'; 1744 $callforreturnvaluedesc = $function->classname . '::' . $descriptionmethod; 1745 1746 $methodbody = <<<EOD 1747 $castingcode 1748 if ($callforreturnvaluedesc == null) { 1749 $function->classname::$function->methodname($paramsstr); 1750 return null; 1751 } 1752 return external_api::clean_returnvalue($callforreturnvaluedesc, $function->classname::$function->methodname($paramsstr)); 1753 EOD; 1754 return $methodbody; 1755 } 1756 } 1757 1758 /** 1759 * Early WS exception handler. 1760 * It handles exceptions during setup and returns the Exception text in the WS format. 1761 * If a raise function is found nothing is returned. Throws Exception otherwise. 1762 * 1763 * @param Exception $ex Raised exception. 1764 * @throws Exception 1765 */ 1766 function early_ws_exception_handler(Exception $ex): void { 1767 if (function_exists('raise_early_ws_exception')) { 1768 raise_early_ws_exception($ex); 1769 die; 1770 } 1771 1772 throw $ex; 1773 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body