weekly release 3.10.1+
[moodle.git] / webservice / lib.php
CommitLineData
06e7fadc 1<?php
cc93c7da 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
a0a07014 17
06e7fadc 18/**
cc93c7da 19 * Web services utility functions and classes
06e7fadc 20 *
a0a07014
JM
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
06e7fadc 24 */
25
cc93c7da 26require_once($CFG->libdir.'/externallib.php');
893d7f0f 27
a0a07014
JM
28/**
29 * WEBSERVICE_AUTHMETHOD_USERNAME - username/password authentication (also called simple authentication)
30 */
2d0acbd5 31define('WEBSERVICE_AUTHMETHOD_USERNAME', 0);
a0a07014
JM
32
33/**
34 * WEBSERVICE_AUTHMETHOD_PERMANENT_TOKEN - most common token authentication (external app, mobile app...)
35 */
2d0acbd5 36define('WEBSERVICE_AUTHMETHOD_PERMANENT_TOKEN', 1);
a0a07014
JM
37
38/**
39 * WEBSERVICE_AUTHMETHOD_SESSION_TOKEN - token for embedded application (requires Moodle session)
40 */
2d0acbd5
JP
41define('WEBSERVICE_AUTHMETHOD_SESSION_TOKEN', 2);
42
229a7099 43/**
44 * General web service library
a0a07014
JM
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
229a7099 49 */
50class webservice {
bb044f53 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;
229a7099 59
07cc3d11
JM
60 /**
61 * Authenticate user (used by download/upload file scripts)
a0a07014 62 *
07cc3d11
JM
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) {
96d3b93b 71 throw new webservice_access_exception('Web services are not enabled in Advanced features.');
07cc3d11
JM
72 }
73
74 // Obtain token record
75 if (!$token = $DB->get_record('external_tokens', array('token' => $token))) {
96d3b93b
JM
76 //client may want to display login form => moodle_exception
77 throw new moodle_exception('invalidtoken', 'webservice');
07cc3d11
JM
78 }
79
d733a8cc
FM
80 $loginfaileddefaultparams = array(
81 'other' => array(
82 'method' => WEBSERVICE_AUTHMETHOD_PERMANENT_TOKEN,
83 'reason' => null,
84 'tokenid' => $token->id
85 )
86 );
87
07cc3d11
JM
88 // Validate token date
89 if ($token->validuntil and $token->validuntil < time()) {
d733a8cc
FM
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();
07cc3d11 97 $DB->delete_records('external_tokens', array('token' => $token->token));
96d3b93b 98 throw new webservice_access_exception('Invalid token - token expired - check validuntil time for the token');
07cc3d11
JM
99 }
100
101 // Check ip
102 if ($token->iprestriction and !address_in_subnet(getremoteaddr(), $token->iprestriction)) {
d733a8cc
FM
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();
96d3b93b
JM
110 throw new webservice_access_exception('Invalid token - IP:' . getremoteaddr()
111 . ' is not supported');
07cc3d11
JM
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
d79d5ac2 121 \core\session\manager::set_user($user);
1f2e3279 122 set_login_session_preferences();
07cc3d11
JM
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) {
d79d5ac2 126 if (!\core\session\manager::session_exists($token->sid)) {
07cc3d11 127 $DB->delete_records('external_tokens', array('sid' => $token->sid));
96d3b93b 128 throw new webservice_access_exception('Invalid session based token - session not found or expired');
07cc3d11
JM
129 }
130 }
131
59c66f92
MD
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) {
96d3b93b
JM
135 //this is usually temporary, client want to implement code logic => moodle_exception
136 throw new moodle_exception('sitemaintenance', 'admin');
07cc3d11
JM
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
96d3b93b 143 throw new webservice_access_exception('Web service is not available (it doesn\'t exist or might be disabled)');
07cc3d11
JM
144 }
145
146 //check if there is any required system capability
43731030 147 if ($service->requiredcapability and !has_capability($service->requiredcapability, context_system::instance(), $user)) {
96d3b93b 148 throw new webservice_access_exception('The capability ' . $service->requiredcapability . ' is required.');
07cc3d11
JM
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)) {
96d3b93b
JM
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.');
07cc3d11
JM
159 }
160
161 if (!empty($authoriseduser->validuntil) and $authoriseduser->validuntil < time()) {
96d3b93b 162 throw new webservice_access_exception('Invalid service - service expired - check validuntil time for this allowed user');
07cc3d11
JM
163 }
164
165 if (!empty($authoriseduser->iprestriction) and !address_in_subnet(getremoteaddr(), $authoriseduser->iprestriction)) {
96d3b93b
JM
166 throw new webservice_access_exception('Invalid service - IP:' . getremoteaddr()
167 . ' is not supported - check this allowed user');
07cc3d11
JM
168 }
169 }
170
171 //only confirmed user should be able to call web service
172 if (empty($user->confirmed)) {
d733a8cc
FM
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();
96d3b93b 179 throw new moodle_exception('usernotconfirmed', 'moodle', '', $user->username);
07cc3d11
JM
180 }
181
182 //check the user is suspended
183 if (!empty($user->suspended)) {
d733a8cc
FM
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();
96d3b93b 190 throw new webservice_access_exception('Refused web service access for suspended username: ' . $user->username);
07cc3d11
JM
191 }
192
193 //check if the auth method is nologin (in this case refuse connection)
194 if ($user->auth == 'nologin') {
d733a8cc
FM
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();
96d3b93b 201 throw new webservice_access_exception('Refused web service access for nologin authentication username: ' . $user->username);
07cc3d11
JM
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) {
d733a8cc
FM
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();
96d3b93b 215 throw new moodle_exception('passwordisexpired', 'webservice');
07cc3d11
JM
216 }
217 }
218
219 // log token access
bb044f53 220 self::update_token_lastaccess($token);
07cc3d11
JM
221
222 return array('user' => $user, 'token' => $token, 'service' => $service);
223 }
224
bb044f53 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
86dcc6f0 246 /**
a0a07014
JM
247 * Allow user to call a service
248 *
249 * @param stdClass $user a user
86dcc6f0 250 */
251 public function add_ws_authorised_user($user) {
252 global $DB;
caee6e6c 253 $user->timecreated = time();
86dcc6f0 254 $DB->insert_record('external_services_users', $user);
255 }
256
257 /**
a0a07014
JM
258 * Disallow a user to call a service
259 *
260 * @param stdClass $user a user
86dcc6f0 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 /**
a0a07014
JM
270 * Update allowed user settings (ip restriction, valid until...)
271 *
272 * @param stdClass $user
86dcc6f0 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
a0a07014
JM
282 *
283 * @param int $serviceid the service id to search against
86dcc6f0 284 * @return array $users
285 */
286 public function get_ws_authorised_users($serviceid) {
c924a469
PS
287 global $DB, $CFG;
288 $params = array($CFG->siteguest, $serviceid);
86dcc6f0 289 $sql = " SELECT u.id as id, esu.id as serviceuserid, u.email as email, u.firstname as firstname,
c924a469
PS
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
86dcc6f0 295 AND esu.userid = u.id
296 AND esu.externalserviceid = ?";
86dcc6f0 297
298 $users = $DB->get_records_sql($sql, $params);
299 return $users;
300 }
301
302 /**
a0a07014
JM
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
86dcc6f0 308 */
309 public function get_ws_authorised_user($serviceid, $userid) {
c924a469
PS
310 global $DB, $CFG;
311 $params = array($CFG->siteguest, $serviceid, $userid);
86dcc6f0 312 $sql = " SELECT u.id as id, esu.id as serviceuserid, u.email as email, u.firstname as firstname,
c924a469
PS
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
86dcc6f0 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
229a7099 325 /**
a0a07014
JM
326 * Generate all tokens of a specific user
327 *
328 * @param int $userid user id
229a7099 329 */
330 public function generate_user_ws_tokens($userid) {
331 global $CFG, $DB;
c924a469 332
a0a07014 333 // generate a token for non admin if web service are enable and the user has the capability to create a token
43731030 334 if (!is_siteadmin() && has_capability('moodle/webservice:createtoken', context_system::instance(), $userid) && !empty($CFG->enablewebservices)) {
a0a07014 335 // for every service than the user is authorised on, create a token (if it doesn't already exist)
229a7099 336
a0a07014 337 // get all services which are set to all user (no restricted to specific users)
229a7099 338 $norestrictedservices = $DB->get_records('external_services', array('restrictedusers' => 0));
339 $serviceidlist = array();
340 foreach ($norestrictedservices as $service) {
341 $serviceidlist[] = $service->id;
342 }
343
a0a07014 344 // get all services which are set to the current user (the current user is specified in the restricted user list)
229a7099 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
a0a07014 352 // get all services which already have a token set for the current user
229a7099 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
a0a07014 359 // create a token for the service which have no token already
229a7099 360 foreach ($serviceidlist as $serviceid) {
361 if (!in_array($serviceid, $tokenizedservice)) {
a0a07014 362 // create the token for this service
7a424cc4 363 $newtoken = new stdClass();
229a7099 364 $newtoken->token = md5(uniqid(rand(),1));
a0a07014 365 // check that the user has capability on this service
229a7099 366 $newtoken->tokentype = EXTERNAL_TOKEN_PERMANENT;
367 $newtoken->userid = $userid;
368 $newtoken->externalserviceid = $serviceid;
a0a07014 369 // TODO MDL-31190 find a way to get the context - UPDATE FOLLOWING LINE
43731030 370 $newtoken->contextid = context_system::instance()->id;
229a7099 371 $newtoken->creatorid = $userid;
372 $newtoken->timecreated = time();
956232db
DP
373 // Generate the private token, it must be transmitted only via https.
374 $newtoken->privatetoken = random_string(64);
229a7099 375
376 $DB->insert_record('external_tokens', $newtoken);
377 }
378 }
379
380
381 }
382 }
383
384 /**
a0a07014
JM
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
229a7099 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
e88c193f 396 t.id, t.creatorid, t.token, u.firstname, u.lastname, s.id as wsid, s.name, s.enabled, s.restrictedusers, t.validuntil
229a7099 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 /**
a0a07014
JM
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:
9c954e88 410 * ->id token id
411 * ->token
412 * ->firstname user firstname
413 * ->lastname
414 * ->name service name
a0a07014
JM
415 *
416 * @param int $userid user id
417 * @param int $tokenid token id
418 * @return stdClass
229a7099 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
9c954e88 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);
229a7099 432 return $token;
229a7099 433 }
434
0c62ca25
JD
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;
ad2ae6e3
JO
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);
0c62ca25
JD
455 return $token;
456 }
457
9ef728d6 458 /**
a0a07014
JM
459 * Return a database token record for a token id
460 *
461 * @param int $tokenid token id
9ef728d6 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
229a7099 469 /**
a0a07014
JM
470 * Delete a token
471 *
472 * @param int $tokenid token id
229a7099 473 */
474 public function delete_user_ws_token($tokenid) {
475 global $DB;
476 $DB->delete_records('external_tokens', array('id'=>$tokenid));
477 }
bb98a5b2 478
753504fb
JL
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
c25662b0 489 /**
a0a07014
JM
490 * Delete a service
491 * Also delete function references and authorised user references.
492 *
493 * @param int $serviceid service id
c25662b0 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));
fc0fcb27 500 $DB->delete_records('external_services', array('id' => $serviceid));
c25662b0 501 }
502
bb98a5b2 503 /**
a0a07014
JM
504 * Get a full database token record for a given token value
505 *
bb98a5b2 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 }
229a7099 513
72f68b51 514 /**
a0a07014
JM
515 * Get the functions list of a service list (by id)
516 *
517 * @param array $serviceids service ids
518 * @return array of functions
72f68b51 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
89823b1a
JL
528 WHERE sf.externalserviceid $serviceids)
529 ORDER BY f.name ASC";
72f68b51 530 $functions = $DB->get_records_sql($sql, $params);
531 } else {
532 $functions = array();
533 }
534 return $functions;
535 }
536
0bf486a6 537 /**
a0a07014
JM
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
0bf486a6
JM
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;
72f68b51 561 }
562
9c954e88 563 /**
a0a07014
JM
564 * Get functions not included in a service
565 *
566 * @param int $serviceid service id
9c954e88 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
72f68b51 582 /**
583 * Get list of required capabilities of a service, sorted by functions
a0a07014 584 * Example of returned value:
72f68b51 585 * Array
586 * (
f23e9b6b 587 * [core_group_create_groups] => Array
72f68b51 588 * (
589 * [0] => moodle/course:managegroups
590 * )
591 *
f23e9b6b 592 * [core_enrol_get_enrolled_users] => Array
72f68b51 593 * (
f23e9b6b
CB
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
72f68b51 599 * )
600 * )
a0a07014
JM
601 * @param int $serviceid service id
602 * @return array
72f68b51 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) {
8819c47b 608 $functioncaps = explode(',', $function->capabilities);
72f68b51 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)
a0a07014 620 * Only useful for documentation purpose
b449d3b7
JM
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
a0a07014
JM
625 *
626 * @param int $userid user id
72f68b51 627 * @return array
628 */
629 public function get_user_capabilities($userid) {
630 global $DB;
631 //retrieve the user capabilities
fbf6cfe6
JM
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));
72f68b51 635 $usercaps = array();
636 foreach ($dbusercaps as $usercap) {
637 $usercaps[$usercap->capability] = true;
638 }
639 return $usercaps;
640 }
641
642 /**
a0a07014 643 * Get missing user capabilities for a given service
b449d3b7
JM
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
a0a07014
JM
648 *
649 * @param array $users users
650 * @param int $serviceid service id
651 * @return array of missing capabilities, keys being the user ids
72f68b51 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) {
12fc8acf 671 if (!array_key_exists($functioncap, $usercaps)) {
72f68b51 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
c25662b0 684 /**
a0a07014
JM
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
c25662b0 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
c1b65883 698 /**
a0a07014
JM
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
c1b65883
JM
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
72f68b51 712 /**
a0a07014
JM
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
72f68b51 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
a0a07014
JM
728 *
729 * @param string $functionname function name
730 * @param int $serviceid service id
72f68b51 731 */
732 public function add_external_function_to_service($functionname, $serviceid) {
733 global $DB;
7a424cc4 734 $addedfunction = new stdClass();
72f68b51 735 $addedfunction->externalserviceid = $serviceid;
736 $addedfunction->functionname = $functionname;
737 $DB->insert_record('external_services_functions', $addedfunction);
738 }
739
c25662b0 740 /**
741 * Add a service
a0a07014
JM
742 * It generates the timecreated field automatically.
743 *
744 * @param stdClass $service
c25662b0 745 * @return serviceid integer
746 */
747 public function add_external_service($service) {
748 global $DB;
caee6e6c 749 $service->timecreated = time();
c25662b0 750 $serviceid = $DB->insert_record('external_services', $service);
751 return $serviceid;
752 }
753
a0a07014 754 /**
c25662b0 755 * Update a service
a0a07014
JM
756 * It modifies the timemodified automatically.
757 *
758 * @param stdClass $service
c25662b0 759 */
760 public function update_external_service($service) {
761 global $DB;
caee6e6c 762 $service->timemodified = time();
c25662b0 763 $DB->update_record('external_services', $service);
764 }
765
72f68b51 766 /**
a0a07014
JM
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
72f68b51 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
a0a07014
JM
781 /**
782 * Remove a function from a service
783 *
784 * @param string $functionname function name
785 * @param int $serviceid service id
786 */
72f68b51 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
9d382a94
JL
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;
72f68b51 803
9d382a94
JL
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 }
72f68b51 810}
229a7099 811
5593d2dc 812/**
813 * Exception indicating access control problem in web service call
96d3b93b
JM
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.
a0a07014
JM
818 *
819 * @package core_webservice
820 * @copyright 2009 Petr Skodak
821 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
5593d2dc 822 */
823class webservice_access_exception extends moodle_exception {
a0a07014 824
5593d2dc 825 /**
826 * Constructor
a0a07014
JM
827 *
828 * @param string $debuginfo the debug info
5593d2dc 829 */
830 function __construct($debuginfo) {
e8b21670 831 parent::__construct('accessexception', 'webservice', '', null, $debuginfo);
5593d2dc 832 }
833}
834
f0dafb3c 835/**
a0a07014
JM
836 * Check if a protocol is enabled
837 *
13ae7db2 838 * @param string $protocol name of WS protocol ('rest', 'soap', 'xmlrpc'...)
a0a07014 839 * @return bool true if the protocol is enabled
f0dafb3c 840 */
cc93c7da 841function webservice_protocol_is_enabled($protocol) {
842 global $CFG;
893d7f0f 843
cc93c7da 844 if (empty($CFG->enablewebservices)) {
845 return false;
893d7f0f 846 }
847
cc93c7da 848 $active = explode(',', $CFG->webserviceprotocols);
893d7f0f 849
cc93c7da 850 return(in_array($protocol, $active));
851}
893d7f0f 852
f0dafb3c 853/**
3f599588 854 * Mandatory interface for all test client classes.
a0a07014
JM
855 *
856 * @package core_webservice
857 * @copyright 2009 Petr Skodak
858 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
f0dafb3c 859 */
860interface webservice_test_client_interface {
a0a07014 861
f0dafb3c 862 /**
863 * Execute test client WS request
a0a07014
JM
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
f0dafb3c 868 * @return mixed
869 */
870 public function simpletest($serverurl, $function, $params);
871}
872
06e7fadc 873/**
3f599588 874 * Mandatory interface for all web service protocol classes
a0a07014
JM
875 *
876 * @package core_webservice
877 * @copyright 2009 Petr Skodak
878 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
06e7fadc 879 */
01482a4a 880interface webservice_server_interface {
a0a07014 881
01482a4a
PS
882 /**
883 * Process request from client.
01482a4a
PS
884 */
885 public function run();
886}
88098133 887
01482a4a
PS
888/**
889 * Abstract web service base class.
a0a07014
JM
890 *
891 * @package core_webservice
892 * @copyright 2009 Petr Skodak
893 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
01482a4a
PS
894 */
895abstract class webservice_server implements webservice_server_interface {
88098133 896
a0a07014 897 /** @var string Name of the web server plugin */
88098133 898 protected $wsname = null;
899
a0a07014 900 /** @var string Name of local user */
c187722c
PS
901 protected $username = null;
902
a0a07014 903 /** @var string Password of the local user */
c187722c 904 protected $password = null;
b107e647 905
a0a07014 906 /** @var int The local user */
c3517f05 907 protected $userid = null;
908
a0a07014 909 /** @var integer Authentication method one of WEBSERVICE_AUTHMETHOD_* */
2d0acbd5 910 protected $authmethod;
88098133 911
a0a07014 912 /** @var string Authentication token*/
01482a4a 913 protected $token = null;
88098133 914
a0a07014 915 /** @var stdClass Restricted context */
88098133 916 protected $restricted_context;
917
a0a07014 918 /** @var int Restrict call to one service id*/
01482a4a
PS
919 protected $restricted_serviceid = null;
920
2d0acbd5 921 /**
a0a07014
JM
922 * Constructor
923 *
c924a469 924 * @param integer $authmethod authentication method one of WEBSERVICE_AUTHMETHOD_*
2d0acbd5
JP
925 */
926 public function __construct($authmethod) {
927 $this->authmethod = $authmethod;
c924a469
PS
928 }
929
930
01482a4a
PS
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.
01482a4a
PS
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
d733a8cc 945 $loginfaileddefaultparams = array(
d733a8cc
FM
946 'other' => array(
947 'method' => $this->authmethod,
ee2df1a8 948 'reason' => null
d733a8cc
FM
949 )
950 );
951
2d0acbd5 952 if ($this->authmethod == WEBSERVICE_AUTHMETHOD_USERNAME) {
01482a4a 953
1839e2f0 954 //we check that authentication plugin is enabled
955 //it is only required by simple authentication
956 if (!is_enabled_auth('webservice')) {
96d3b93b 957 throw new webservice_access_exception('The web service authentication plugin is disabled.');
1839e2f0 958 }
01482a4a 959
1839e2f0 960 if (!$auth = get_auth_plugin('webservice')) {
96d3b93b 961 throw new webservice_access_exception('The web service authentication plugin is missing.');
1839e2f0 962 }
01482a4a 963
43731030 964 $this->restricted_context = context_system::instance();
01482a4a
PS
965
966 if (!$this->username) {
96d3b93b 967 throw new moodle_exception('missingusername', 'webservice');
01482a4a
PS
968 }
969
970 if (!$this->password) {
96d3b93b 971 throw new moodle_exception('missingpassword', 'webservice');
01482a4a
PS
972 }
973
974 if (!$auth->user_login_webservice($this->username, $this->password)) {
d733a8cc
FM
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
96d3b93b 985 throw new moodle_exception('wrongusernamepassword', 'webservice');
01482a4a
PS
986 }
987
07a90ec3 988 $user = $DB->get_record('user', array('username'=>$this->username, 'mnethostid'=>$CFG->mnet_localhost_id), '*', MUST_EXIST);
01482a4a 989
2d0acbd5
JP
990 } else if ($this->authmethod == WEBSERVICE_AUTHMETHOD_PERMANENT_TOKEN){
991 $user = $this->authenticate_by_token(EXTERNAL_TOKEN_PERMANENT);
01482a4a 992 } else {
2d0acbd5 993 $user = $this->authenticate_by_token(EXTERNAL_TOKEN_EMBEDDED);
01482a4a 994 }
c924a469 995
59c66f92
MD
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) {
96d3b93b 999 throw new moodle_exception('sitemaintenance', 'admin');
07a90ec3
JM
1000 }
1001
1002 //only confirmed user should be able to call web service
1003 if (!empty($user->deleted)) {
d733a8cc
FM
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();
96d3b93b 1011 throw new webservice_access_exception('Refused web service access for deleted username: ' . $user->username);
07a90ec3
JM
1012 }
1013
1014 //only confirmed user should be able to call web service
1015 if (empty($user->confirmed)) {
d733a8cc
FM
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();
96d3b93b 1023 throw new moodle_exception('wsaccessuserunconfirmed', 'webservice', '', $user->username);
07a90ec3
JM
1024 }
1025
1026 //check the user is suspended
1027 if (!empty($user->suspended)) {
d733a8cc
FM
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();
96d3b93b 1035 throw new webservice_access_exception('Refused web service access for suspended username: ' . $user->username);
07a90ec3
JM
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 ) {
d733a8cc
FM
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();
96d3b93b 1054 throw new webservice_access_exception('Refused web service access for password expired username: ' . $user->username);
07a90ec3
JM
1055 }
1056 }
1057
1058 //check if the auth method is nologin (in this case refuse connection)
1059 if ($user->auth=='nologin') {
d733a8cc
FM
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();
96d3b93b 1067 throw new webservice_access_exception('Refused web service access for nologin authentication username: ' . $user->username);
07a90ec3
JM
1068 }
1069
01482a4a 1070 // now fake user login, the session is completely empty too
e922fe23 1071 enrol_check_plugins($user);
d79d5ac2 1072 \core\session\manager::set_user($user);
1f2e3279 1073 set_login_session_preferences();
c3517f05 1074 $this->userid = $user->id;
01482a4a 1075
0f1e3914 1076 if ($this->authmethod != WEBSERVICE_AUTHMETHOD_SESSION_TOKEN && !has_capability("webservice/$this->wsname:use", $this->restricted_context)) {
96d3b93b 1077 throw new webservice_access_exception('You are not allowed to use the {$a} protocol (missing capability: webservice/' . $this->wsname . ':use)');
01482a4a
PS
1078 }
1079
1080 external_api::set_context_restriction($this->restricted_context);
1081 }
c924a469 1082
a0a07014
JM
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 */
2d0acbd5
JP
1090 protected function authenticate_by_token($tokentype){
1091 global $DB;
d733a8cc
FM
1092
1093 $loginfaileddefaultparams = array(
d733a8cc
FM
1094 'other' => array(
1095 'method' => $this->authmethod,
ee2df1a8 1096 'reason' => null
d733a8cc
FM
1097 )
1098 );
1099
2d0acbd5 1100 if (!$token = $DB->get_record('external_tokens', array('token'=>$this->token, 'tokentype'=>$tokentype))) {
d733a8cc
FM
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();
96d3b93b 1108 throw new moodle_exception('invalidtoken', 'webservice');
2d0acbd5 1109 }
c924a469 1110
2d0acbd5
JP
1111 if ($token->validuntil and $token->validuntil < time()) {
1112 $DB->delete_records('external_tokens', array('token'=>$this->token, 'tokentype'=>$tokentype));
96d3b93b 1113 throw new webservice_access_exception('Invalid token - token expired - check validuntil time for the token');
2d0acbd5 1114 }
c924a469 1115
2d0acbd5 1116 if ($token->sid){//assumes that if sid is set then there must be a valid associated session no matter the token type
d79d5ac2 1117 if (!\core\session\manager::session_exists($token->sid)){
2d0acbd5 1118 $DB->delete_records('external_tokens', array('sid'=>$token->sid));
96d3b93b 1119 throw new webservice_access_exception('Invalid session based token - session not found or expired');
2d0acbd5
JP
1120 }
1121 }
1122
1123 if ($token->iprestriction and !address_in_subnet(getremoteaddr(), $token->iprestriction)) {
d733a8cc
FM
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();
96d3b93b
JM
1132 throw new webservice_access_exception('Invalid service - IP:' . getremoteaddr()
1133 . ' is not supported - check this allowed user');
2d0acbd5
JP
1134 }
1135
d197ea43 1136 $this->restricted_context = context::instance_by_id($token->contextid);
2d0acbd5
JP
1137 $this->restricted_serviceid = $token->externalserviceid;
1138
07a90ec3 1139 $user = $DB->get_record('user', array('id'=>$token->userid), '*', MUST_EXIST);
2d0acbd5
JP
1140
1141 // log token access
bb044f53 1142 webservice::update_token_lastaccess($token);
c924a469 1143
2d0acbd5 1144 return $user;
c924a469 1145
2d0acbd5 1146 }
93ce0e82
JM
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(
bb14a488
JL
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),
9b64c178 1163 'timezone' => array('default' => '', 'type' => PARAM_TIMEZONE),
bb14a488 1164 );
93ce0e82
JM
1165
1166 // Load the external settings with the web service settings.
1167 $settings = external_settings::get_instance();
bb14a488 1168 foreach ($externalsettings as $name => $settingdata) {
93ce0e82
JM
1169
1170 $wsparamname = 'moodlewssetting' . $name;
1171
1172 // Retrieve and remove the setting parameter from the request.
bb14a488 1173 $value = optional_param($wsparamname, $settingdata['default'], $settingdata['type']);
93ce0e82
JM
1174 unset($_GET[$wsparamname]);
1175 unset($_POST[$wsparamname]);
1176
1177 $functioname = 'set_' . $name;
1178 $settings->$functioname($value);
1179 }
1180
1181 }
01482a4a
PS
1182}
1183
886d7556 1184/**
a0a07014
JM
1185 * Web Service server base class.
1186 *
1187 * This class handles both simple and token authentication.
1188 *
1189 * @package core_webservice
1190 * @copyright 2009 Petr Skodak
1191 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
886d7556 1192 */
01482a4a 1193abstract class webservice_base_server extends webservice_server {
88098133 1194
a0a07014 1195 /** @var array The function parameters - the real values submitted in the request */
cc93c7da 1196 protected $parameters = null;
1197
a0a07014 1198 /** @var string The name of the function that is executed */
cc93c7da 1199 protected $functionname = null;
1200
a0a07014 1201 /** @var stdClass Full function description */
cc93c7da 1202 protected $function = null;
1203
a0a07014 1204 /** @var mixed Function return value */
cc93c7da 1205 protected $returns = null;
06e7fadc 1206
79c7fffc
JP
1207 /** @var array List of methods and their information provided by the web service. */
1208 protected $servicemethods;
1209
1210 /** @var array List of struct classes generated for the web service methods. */
1211 protected $servicestructs;
1212
24350e06 1213 /**
cc93c7da 1214 * This method parses the request input, it needs to get:
1215 * 1/ user authentication - username+password or token
1216 * 2/ function name
1217 * 3/ function parameters
24350e06 1218 */
cc93c7da 1219 abstract protected function parse_request();
24350e06 1220
cc93c7da 1221 /**
1222 * Send the result of function call to the WS client.
cc93c7da 1223 */
1224 abstract protected function send_response();
24350e06 1225
fa0797ec 1226 /**
cc93c7da 1227 * Send the error information to the WS client.
a0a07014 1228 *
cc93c7da 1229 * @param exception $ex
fa0797ec 1230 */
cc93c7da 1231 abstract protected function send_error($ex=null);
fa0797ec 1232
cc93c7da 1233 /**
1234 * Process request from client.
a0a07014
JM
1235 *
1236 * @uses die
cc93c7da 1237 */
2458e30a 1238 public function run() {
9b64c178 1239 global $CFG, $USER, $SESSION;
bb14a488 1240
cc93c7da 1241 // we will probably need a lot of memory in some functions
346c5887 1242 raise_memory_limit(MEMORY_EXTRA);
fa0797ec 1243
cc93c7da 1244 // set some longer timeout, this script is not sending any output,
1245 // this means we need to manually extend the timeout operations
1246 // that need longer time to finish
1247 external_api::set_timeout();
fa0797ec 1248
cc93c7da 1249 // set up exception handler first, we want to sent them back in correct format that
1250 // the other system understands
1251 // we do not need to call the original default handler because this ws handler does everything
1252 set_exception_handler(array($this, 'exception_handler'));
06e7fadc 1253
cc93c7da 1254 // init all properties from the request data
1255 $this->parse_request();
06e7fadc 1256
cc93c7da 1257 // authenticate user, this has to be done after the request parsing
1258 // this also sets up $USER and $SESSION
1259 $this->authenticate_user();
06e7fadc 1260
cc93c7da 1261 // find all needed function info and make sure user may actually execute the function
1262 $this->load_function_info();
c924a469 1263
d733a8cc
FM
1264 // Log the web service request.
1265 $params = array(
1266 'other' => array(
1267 'function' => $this->functionname
1268 )
1269 );
1270 $event = \core\event\webservice_function_called::create($params);
1271 $event->set_legacy_logdata(array(SITEID, 'webservice', $this->functionname, '' , getremoteaddr() , 0, $this->userid));
1272 $event->trigger();
f7631e73 1273
bb14a488
JL
1274 // Do additional setup stuff.
1275 $settings = external_settings::get_instance();
1276 $sessionlang = $settings->get_lang();
1277 if (!empty($sessionlang)) {
1278 $SESSION->lang = $sessionlang;
1279 }
1280
1281 setup_lang_from_browser();
1282
1283 if (empty($CFG->lang)) {
1284 if (empty($SESSION->lang)) {
1285 $CFG->lang = 'en';
1286 } else {
1287 $CFG->lang = $SESSION->lang;
1288 }
1289 }
1290
9b64c178
JL
1291 // Change timezone only in sites where it isn't forced.
1292 $newtimezone = $settings->get_timezone();
1293 if (!empty($newtimezone) && (!isset($CFG->forcetimezone) || $CFG->forcetimezone == 99)) {
1294 $USER->timezone = $newtimezone;
1295 }
1296
cc93c7da 1297 // finally, execute the function - any errors are catched by the default exception handler
1298 $this->execute();
06e7fadc 1299
cc93c7da 1300 // send the results back in correct format
1301 $this->send_response();
06e7fadc 1302
cc93c7da 1303 // session cleanup
1304 $this->session_cleanup();
06e7fadc 1305
cc93c7da 1306 die;
f7631e73 1307 }
1308
cc93c7da 1309 /**
1310 * Specialised exception handler, we can not use the standard one because
1311 * it can not just print html to output.
1312 *
1313 * @param exception $ex
a0a07014 1314 * $uses exit
cc93c7da 1315 */
1316 public function exception_handler($ex) {
cc93c7da 1317 // detect active db transactions, rollback and log as error
3086dd59 1318 abort_all_db_transactions();
06e7fadc 1319
cc93c7da 1320 // some hacks might need a cleanup hook
1321 $this->session_cleanup($ex);
06e7fadc 1322
ca6340bf 1323 // now let the plugin send the exception to client
1324 $this->send_error($ex);
1325
cc93c7da 1326 // not much else we can do now, add some logging later
1327 exit(1);
f7631e73 1328 }
1329
1330 /**
cc93c7da 1331 * Future hook needed for emulated sessions.
a0a07014 1332 *
cc93c7da 1333 * @param exception $exception null means normal termination, $exception received when WS call failed
f7631e73 1334 */
cc93c7da 1335 protected function session_cleanup($exception=null) {
ad8b5ba2 1336 if ($this->authmethod == WEBSERVICE_AUTHMETHOD_USERNAME) {
cc93c7da 1337 // nothing needs to be done, there is no persistent session
1338 } else {
1339 // close emulated session if used
1340 }
f7631e73 1341 }
1342
24350e06 1343 /**
cc93c7da 1344 * Fetches the function description from database,
1345 * verifies user is allowed to use this function and
1346 * loads all paremeters and return descriptions.
24350e06 1347 */
cc93c7da 1348 protected function load_function_info() {
1349 global $DB, $USER, $CFG;
40f024c9 1350
cc93c7da 1351 if (empty($this->functionname)) {
1352 throw new invalid_parameter_exception('Missing function name');
1353 }
24350e06 1354
cc93c7da 1355 // function must exist
11c16f5f 1356 $function = external_api::external_function_info($this->functionname);
cc93c7da 1357
01482a4a
PS
1358 if ($this->restricted_serviceid) {
1359 $params = array('sid1'=>$this->restricted_serviceid, 'sid2'=>$this->restricted_serviceid);
1360 $wscond1 = 'AND s.id = :sid1';
1361 $wscond2 = 'AND s.id = :sid2';
1362 } else {
1363 $params = array();
1364 $wscond1 = '';
1365 $wscond2 = '';
1366 }
1367
cc93c7da 1368 // now let's verify access control
b8c5309e 1369
1370 // now make sure the function is listed in at least one service user is allowed to use
1371 // allow access only if:
1372 // 1/ entry in the external_services_users table if required
1373 // 2/ validuntil not reached
1374 // 3/ has capability if specified in service desc
1375 // 4/ iprestriction
1376
1377 $sql = "SELECT s.*, NULL AS iprestriction
1378 FROM {external_services} s
1379 JOIN {external_services_functions} sf ON (sf.externalserviceid = s.id AND s.restrictedusers = 0 AND sf.functionname = :name1)
1380 WHERE s.enabled = 1 $wscond1
1381
1382 UNION
1383
1384 SELECT s.*, su.iprestriction
1385 FROM {external_services} s
1386 JOIN {external_services_functions} sf ON (sf.externalserviceid = s.id AND s.restrictedusers = 1 AND sf.functionname = :name2)
1387 JOIN {external_services_users} su ON (su.externalserviceid = s.id AND su.userid = :userid)
4c7f5363 1388 WHERE s.enabled = 1 AND (su.validuntil IS NULL OR su.validuntil < :now) $wscond2";
b8c5309e 1389 $params = array_merge($params, array('userid'=>$USER->id, 'name1'=>$function->name, 'name2'=>$function->name, 'now'=>time()));
88098133 1390
1391 $rs = $DB->get_recordset_sql($sql, $params);
1392 // now make sure user may access at least one service
1393 $remoteaddr = getremoteaddr();
1394 $allowed = false;
1395 foreach ($rs as $service) {
1396 if ($service->requiredcapability and !has_capability($service->requiredcapability, $this->restricted_context)) {
1397 continue; // cap required, sorry
cc93c7da 1398 }
88098133 1399 if ($service->iprestriction and !address_in_subnet($remoteaddr, $service->iprestriction)) {
1400 continue; // wrong request source ip, sorry
cc93c7da 1401 }
88098133 1402 $allowed = true;
1403 break; // one service is enough, no need to continue
1404 }
1405 $rs->close();
1406 if (!$allowed) {
96d3b93b
JM
1407 throw new webservice_access_exception(
1408 'Access to the function '.$this->functionname.'() is not allowed.
ca11d390
JM
1409 There could be multiple reasons for this:
1410 1. The service linked to the user token does not contain the function.
1411 2. The service is user-restricted and the user is not listed.
1412 3. The service is IP-restricted and the user IP is not listed.
1413 4. The service is time-restricted and the time has expired.
1414 5. The token is time-restricted and the time has expired.
1415 6. The service requires a specific capability which the user does not have.
1416 7. The function is called with username/password (no user token is sent)
1417 and none of the services has the function to allow the user.
1418 These settings can be found in Administration > Site administration
1419 > Plugins > Web services > External services and Manage tokens.');
cc93c7da 1420 }
9baf6825 1421
cc93c7da 1422 // we have all we need now
1423 $this->function = $function;
1424 }
1425
1426 /**
1427 * Execute previously loaded function using parameters parsed from the request data.
cc93c7da 1428 */
1429 protected function execute() {
1430 // validate params, this also sorts the params properly, we need the correct order in the next part
1431 $params = call_user_func(array($this->function->classname, 'validate_parameters'), $this->function->parameters_desc, $this->parameters);
b5311ce4 1432 $params = array_values($params);
1433
1434 // Allow any Moodle plugin a chance to override this call. This is a convenient spot to
1435 // make arbitrary behaviour customisations, for example to affect the mobile app behaviour.
1436 // The overriding plugin could call the 'real' function first and then modify the results,
1437 // or it could do a completely separate thing.
1438 $callbacks = get_plugins_with_function('override_webservice_execution');
1439 foreach ($callbacks as $plugintype => $plugins) {
1440 foreach ($plugins as $plugin => $callback) {
1441 $result = $callback($this->function, $params);
1442 if ($result !== false) {
1443 // If the callback returns anything other than false, we assume it replaces the
1444 // real function.
1445 $this->returns = $result;
1446 return;
1447 }
1448 }
1449 }
9baf6825 1450
cc93c7da 1451 // execute - yay!
b5311ce4 1452 $this->returns = call_user_func_array(array($this->function->classname, $this->function->methodname), $params);
9baf6825 1453 }
79c7fffc
JP
1454
1455 /**
1456 * Load the virtual class needed for the web service.
1457 *
1458 * Initialises the virtual class that contains the web service functions that the user is allowed to use.
1459 * The web service function will be available if the user:
1460 * - is validly registered in the external_services_users table.
1461 * - has the required capability.
1462 * - meets the IP restriction requirement.
1463 * This virtual class can be used by web service protocols such as SOAP, especially when generating WSDL.
79c7fffc
JP
1464 */
1465 protected function init_service_class() {
1466 global $USER, $DB;
1467
1468 // Initialise service methods and struct classes.
1469 $this->servicemethods = array();
1470 $this->servicestructs = array();
1471
1472 $params = array();
1473 $wscond1 = '';
1474 $wscond2 = '';
1475 if ($this->restricted_serviceid) {
1476 $params = array('sid1' => $this->restricted_serviceid, 'sid2' => $this->restricted_serviceid);
1477 $wscond1 = 'AND s.id = :sid1';
1478 $wscond2 = 'AND s.id = :sid2';
1479 }
1480
1481 $sql = "SELECT s.*, NULL AS iprestriction
1482 FROM {external_services} s
1483 JOIN {external_services_functions} sf ON (sf.externalserviceid = s.id AND s.restrictedusers = 0)
1484 WHERE s.enabled = 1 $wscond1
1485
1486 UNION
1487
1488 SELECT s.*, su.iprestriction
1489 FROM {external_services} s
1490 JOIN {external_services_functions} sf ON (sf.externalserviceid = s.id AND s.restrictedusers = 1)
1491 JOIN {external_services_users} su ON (su.externalserviceid = s.id AND su.userid = :userid)
1492 WHERE s.enabled = 1 AND (su.validuntil IS NULL OR su.validuntil < :now) $wscond2";
1493 $params = array_merge($params, array('userid' => $USER->id, 'now' => time()));
1494
1495 $serviceids = array();
1496 $remoteaddr = getremoteaddr();
1497
1498 // Query list of external services for the user.
1499 $rs = $DB->get_recordset_sql($sql, $params);
1500
1501 // Check which service ID to include.
1502 foreach ($rs as $service) {
1503 if (isset($serviceids[$service->id])) {
1504 continue; // Service already added.
1505 }
1506 if ($service->requiredcapability and !has_capability($service->requiredcapability, $this->restricted_context)) {
1507 continue; // Cap required, sorry.
1508 }
1509 if ($service->iprestriction and !address_in_subnet($remoteaddr, $service->iprestriction)) {
1510 continue; // Wrong request source ip, sorry.
1511 }
1512 $serviceids[$service->id] = $service->id;
1513 }
1514 $rs->close();
1515
1516 // Generate the virtual class name.
1517 $classname = 'webservices_virtual_class_000000';
1518 while (class_exists($classname)) {
1519 $classname++;
1520 }
1521 $this->serviceclass = $classname;
1522
1523 // Get the list of all available external functions.
1524 $wsmanager = new webservice();
1525 $functions = $wsmanager->get_external_functions($serviceids);
1526
1527 // Generate code for the virtual methods for this web service.
1528 $methods = '';
1529 foreach ($functions as $function) {
1530 $methods .= $this->get_virtual_method_code($function);
1531 }
1532
1533 $code = <<<EOD
1534/**
1535 * Virtual class web services for user id $USER->id in context {$this->restricted_context->id}.
1536 */
1537class $classname {
1538$methods
1539}
1540EOD;
1541 // Load the virtual class definition into memory.
1542 eval($code);
1543 }
1544
1545 /**
1546 * Generates a struct class.
1547 *
79c7fffc
JP
1548 * @param external_single_structure $structdesc The basis of the struct class to be generated.
1549 * @return string The class name of the generated struct class.
1550 */
1551 protected function generate_simple_struct_class(external_single_structure $structdesc) {
1552 global $USER;
1553
1554 $propeties = array();
1555 $fields = array();
1556 foreach ($structdesc->keys as $name => $fieldsdesc) {
1557 $type = $this->get_phpdoc_type($fieldsdesc);
1558 $propertytype = array('type' => $type);
1559 if (empty($fieldsdesc->allownull) || $fieldsdesc->allownull == NULL_ALLOWED) {
1560 $propertytype['nillable'] = true;
1561 }
1562 $propeties[$name] = $propertytype;
1563 $fields[] = ' /** @var ' . $type . ' $' . $name . '*/';
1564 $fields[] = ' public $' . $name .';';
1565 }
1566 $fieldsstr = implode("\n", $fields);
1567
1568 // We do this after the call to get_phpdoc_type() to avoid duplicate class creation.
1569 $classname = 'webservices_struct_class_000000';
1570 while (class_exists($classname)) {
1571 $classname++;
1572 }
1573 $code = <<<EOD
1574/**
1575 * Virtual struct class for web services for user id $USER->id in context {$this->restricted_context->id}.
1576 */
1577class $classname {
1578$fieldsstr
9baf6825 1579}
79c7fffc
JP
1580EOD;
1581 // Load into memory.
1582 eval($code);
9baf6825 1583
79c7fffc
JP
1584 // Prepare struct info.
1585 $structinfo = new stdClass();
1586 $structinfo->classname = $classname;
1587 $structinfo->properties = $propeties;
1588 // Add the struct info the the list of service struct classes.
1589 $this->servicestructs[] = $structinfo;
1590
1591 return $classname;
1592 }
9baf6825 1593
79c7fffc
JP
1594 /**
1595 * Returns a virtual method code for a web service function.
1596 *
79c7fffc
JP
1597 * @param stdClass $function a record from external_function
1598 * @return string The PHP code of the virtual method.
1599 * @throws coding_exception
1600 * @throws moodle_exception
1601 */
1602 protected function get_virtual_method_code($function) {
11c16f5f 1603 $function = external_api::external_function_info($function);
79c7fffc
JP
1604
1605 // Parameters and their defaults for the method signature.
1606 $paramanddefaults = array();
1607 // Parameters for external lib call.
1608 $params = array();
1609 $paramdesc = array();
1610 // The method's input parameters and their respective types.
1611 $inputparams = array();
1612 // The method's output parameters and their respective types.
1613 $outputparams = array();
1614
1615 foreach ($function->parameters_desc->keys as $name => $keydesc) {
1616 $param = '$' . $name;
1617 $paramanddefault = $param;
1618 if ($keydesc->required == VALUE_OPTIONAL) {
1619 // It does not make sense to declare a parameter VALUE_OPTIONAL. VALUE_OPTIONAL is used only for array/object key.
1620 throw new moodle_exception('erroroptionalparamarray', 'webservice', '', $name);
1621 } else if ($keydesc->required == VALUE_DEFAULT) {
1622 // Need to generate the default, if there is any.
1623 if ($keydesc instanceof external_value) {
1624 if ($keydesc->default === null) {
1625 $paramanddefault .= ' = null';
1626 } else {
1627 switch ($keydesc->type) {
1628 case PARAM_BOOL:
1629 $default = (int)$keydesc->default;
1630 break;
1631 case PARAM_INT:
1632 $default = $keydesc->default;
1633 break;
1634 case PARAM_FLOAT;
1635 $default = $keydesc->default;
1636 break;
1637 default:
1638 $default = "'$keydesc->default'";
1639 }
1640 $paramanddefault .= " = $default";
1641 }
1642 } else {
1643 // Accept empty array as default.
1644 if (isset($keydesc->default) && is_array($keydesc->default) && empty($keydesc->default)) {
1645 $paramanddefault .= ' = array()';
1646 } else {
1647 // For the moment we do not support default for other structure types.
1648 throw new moodle_exception('errornotemptydefaultparamarray', 'webservice', '', $name);
1649 }
1650 }
1651 }
1652
1653 $params[] = $param;
1654 $paramanddefaults[] = $paramanddefault;
1655 $type = $this->get_phpdoc_type($keydesc);
1656 $inputparams[$name]['type'] = $type;
1657
1658 $paramdesc[] = '* @param ' . $type . ' $' . $name . ' ' . $keydesc->desc;
1659 }
1660 $paramanddefaults = implode(', ', $paramanddefaults);
1661 $paramdescstr = implode("\n ", $paramdesc);
1662
1663 $serviceclassmethodbody = $this->service_class_method_body($function, $params);
1664
1665 if (empty($function->returns_desc)) {
1666 $return = '* @return void';
1667 } else {
1668 $type = $this->get_phpdoc_type($function->returns_desc);
1669 $outputparams['return']['type'] = $type;
1670 $return = '* @return ' . $type . ' ' . $function->returns_desc->desc;
1671 }
1672
1673 // Now create the virtual method that calls the ext implementation.
1674 $code = <<<EOD
1675/**
1676 * $function->description.
1677 *
1678 $paramdescstr
1679 $return
1680 */
1681public function $function->name($paramanddefaults) {
1682$serviceclassmethodbody
1683}
1684EOD;
1685
1686 // Prepare the method information.
1687 $methodinfo = new stdClass();
1688 $methodinfo->name = $function->name;
1689 $methodinfo->inputparams = $inputparams;
1690 $methodinfo->outputparams = $outputparams;
1691 $methodinfo->description = $function->description;
1692 // Add the method information into the list of service methods.
1693 $this->servicemethods[] = $methodinfo;
1694
1695 return $code;
1696 }
1697
1698 /**
1699 * Get the phpdoc type for an external_description object.
1700 * external_value => int, double or string
1701 * external_single_structure => object|struct, on-fly generated stdClass name.
1702 * external_multiple_structure => array
1703 *
1704 * @param mixed $keydesc The type description.
1705 * @return string The PHP doc type of the external_description object.
1706 */
1707 protected function get_phpdoc_type($keydesc) {
1708 $type = null;
1709 if ($keydesc instanceof external_value) {
1710 switch ($keydesc->type) {
1711 case PARAM_BOOL: // 0 or 1 only for now.
1712 case PARAM_INT:
1713 $type = 'int';
1714 break;
1715 case PARAM_FLOAT;
1716 $type = 'double';
1717 break;
1718 default:
1719 $type = 'string';
1720 }
1721 } else if ($keydesc instanceof external_single_structure) {
1722 $type = $this->generate_simple_struct_class($keydesc);
1723 } else if ($keydesc instanceof external_multiple_structure) {
1724 $type = 'array';
1725 }
1726
1727 return $type;
1728 }
1729
1730 /**
1731 * Generates the method body of the virtual external function.
1732 *
79c7fffc
JP
1733 * @param stdClass $function a record from external_function.
1734 * @param array $params web service function parameters.
1735 * @return string body of the method for $function ie. everything within the {} of the method declaration.
1736 */
1737 protected function service_class_method_body($function, $params) {
1738 // Cast the param from object to array (validate_parameters except array only).
1739 $castingcode = '';
1740 $paramsstr = '';
1741 if (!empty($params)) {
1742 foreach ($params as $paramtocast) {
1743 // Clean the parameter from any white space.
1744 $paramtocast = trim($paramtocast);
1745 $castingcode .= " $paramtocast = json_decode(json_encode($paramtocast), true);\n";
1746 }
1747 $paramsstr = implode(', ', $params);
1748 }
1749
1750 $descriptionmethod = $function->methodname . '_returns()';
1751 $callforreturnvaluedesc = $function->classname . '::' . $descriptionmethod;
1752
1753 $methodbody = <<<EOD
1754$castingcode
1755 if ($callforreturnvaluedesc == null) {
1756 $function->classname::$function->methodname($paramsstr);
1757 return null;
1758 }
1759 return external_api::clean_returnvalue($callforreturnvaluedesc, $function->classname::$function->methodname($paramsstr));
1760EOD;
1761 return $methodbody;
1762 }
1763}
4ed6010a
PFO
1764
1765/**
1766 * Early WS exception handler.
1767 * It handles exceptions during setup and returns the Exception text in the WS format.
1768 * If a raise function is found nothing is returned. Throws Exception otherwise.
1769 *
1770 * @param Exception $ex Raised exception.
1771 * @throws Exception
1772 */
1773function early_ws_exception_handler(Exception $ex): void {
1774 if (function_exists('raise_early_ws_exception')) {
1775 raise_early_ws_exception($ex);
1776 die;
1777 }
1778
1779 throw $ex;
1780}