MDL-67749 ws: Set private token when reset or manual create token
[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),
1163 );
93ce0e82
JM
1164
1165 // Load the external settings with the web service settings.
1166 $settings = external_settings::get_instance();
bb14a488 1167 foreach ($externalsettings as $name => $settingdata) {
93ce0e82
JM
1168
1169 $wsparamname = 'moodlewssetting' . $name;
1170
1171 // Retrieve and remove the setting parameter from the request.
bb14a488 1172 $value = optional_param($wsparamname, $settingdata['default'], $settingdata['type']);
93ce0e82
JM
1173 unset($_GET[$wsparamname]);
1174 unset($_POST[$wsparamname]);
1175
1176 $functioname = 'set_' . $name;
1177 $settings->$functioname($value);
1178 }
1179
1180 }
01482a4a
PS
1181}
1182
886d7556 1183/**
a0a07014
JM
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
886d7556 1191 */
01482a4a 1192abstract class webservice_base_server extends webservice_server {
88098133 1193
a0a07014 1194 /** @var array The function parameters - the real values submitted in the request */
cc93c7da 1195 protected $parameters = null;
1196
a0a07014 1197 /** @var string The name of the function that is executed */
cc93c7da 1198 protected $functionname = null;
1199
a0a07014 1200 /** @var stdClass Full function description */
cc93c7da 1201 protected $function = null;
1202
a0a07014 1203 /** @var mixed Function return value */
cc93c7da 1204 protected $returns = null;
06e7fadc 1205
79c7fffc
JP
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
24350e06 1212 /**
cc93c7da 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
24350e06 1217 */
cc93c7da 1218 abstract protected function parse_request();
24350e06 1219
cc93c7da 1220 /**
1221 * Send the result of function call to the WS client.
cc93c7da 1222 */
1223 abstract protected function send_response();
24350e06 1224
fa0797ec 1225 /**
cc93c7da 1226 * Send the error information to the WS client.
a0a07014 1227 *
cc93c7da 1228 * @param exception $ex
fa0797ec 1229 */
cc93c7da 1230 abstract protected function send_error($ex=null);
fa0797ec 1231
cc93c7da 1232 /**
1233 * Process request from client.
a0a07014
JM
1234 *
1235 * @uses die
cc93c7da 1236 */
2458e30a 1237 public function run() {
bb14a488
JL
1238 global $CFG, $SESSION;
1239
cc93c7da 1240 // we will probably need a lot of memory in some functions
346c5887 1241 raise_memory_limit(MEMORY_EXTRA);
fa0797ec 1242
cc93c7da 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();
fa0797ec 1247
cc93c7da 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'));
06e7fadc 1252
cc93c7da 1253 // init all properties from the request data
1254 $this->parse_request();
06e7fadc 1255
cc93c7da 1256 // authenticate user, this has to be done after the request parsing
1257 // this also sets up $USER and $SESSION
1258 $this->authenticate_user();
06e7fadc 1259
cc93c7da 1260 // find all needed function info and make sure user may actually execute the function
1261 $this->load_function_info();
c924a469 1262
d733a8cc
FM
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();
f7631e73 1272
bb14a488
JL
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
cc93c7da 1290 // finally, execute the function - any errors are catched by the default exception handler
1291 $this->execute();
06e7fadc 1292
cc93c7da 1293 // send the results back in correct format
1294 $this->send_response();
06e7fadc 1295
cc93c7da 1296 // session cleanup
1297 $this->session_cleanup();
06e7fadc 1298
cc93c7da 1299 die;
f7631e73 1300 }
1301
cc93c7da 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
a0a07014 1307 * $uses exit
cc93c7da 1308 */
1309 public function exception_handler($ex) {
cc93c7da 1310 // detect active db transactions, rollback and log as error
3086dd59 1311 abort_all_db_transactions();
06e7fadc 1312
cc93c7da 1313 // some hacks might need a cleanup hook
1314 $this->session_cleanup($ex);
06e7fadc 1315
ca6340bf 1316 // now let the plugin send the exception to client
1317 $this->send_error($ex);
1318
cc93c7da 1319 // not much else we can do now, add some logging later
1320 exit(1);
f7631e73 1321 }
1322
1323 /**
cc93c7da 1324 * Future hook needed for emulated sessions.
a0a07014 1325 *
cc93c7da 1326 * @param exception $exception null means normal termination, $exception received when WS call failed
f7631e73 1327 */
cc93c7da 1328 protected function session_cleanup($exception=null) {
ad8b5ba2 1329 if ($this->authmethod == WEBSERVICE_AUTHMETHOD_USERNAME) {
cc93c7da 1330 // nothing needs to be done, there is no persistent session
1331 } else {
1332 // close emulated session if used
1333 }
f7631e73 1334 }
1335
24350e06 1336 /**
cc93c7da 1337 * Fetches the function description from database,
1338 * verifies user is allowed to use this function and
1339 * loads all paremeters and return descriptions.
24350e06 1340 */
cc93c7da 1341 protected function load_function_info() {
1342 global $DB, $USER, $CFG;
40f024c9 1343
cc93c7da 1344 if (empty($this->functionname)) {
1345 throw new invalid_parameter_exception('Missing function name');
1346 }
24350e06 1347
cc93c7da 1348 // function must exist
11c16f5f 1349 $function = external_api::external_function_info($this->functionname);
cc93c7da 1350
01482a4a
PS
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
cc93c7da 1361 // now let's verify access control
b8c5309e 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)
4c7f5363 1381 WHERE s.enabled = 1 AND (su.validuntil IS NULL OR su.validuntil < :now) $wscond2";
b8c5309e 1382 $params = array_merge($params, array('userid'=>$USER->id, 'name1'=>$function->name, 'name2'=>$function->name, 'now'=>time()));
88098133 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
cc93c7da 1391 }
88098133 1392 if ($service->iprestriction and !address_in_subnet($remoteaddr, $service->iprestriction)) {
1393 continue; // wrong request source ip, sorry
cc93c7da 1394 }
88098133 1395 $allowed = true;
1396 break; // one service is enough, no need to continue
1397 }
1398 $rs->close();
1399 if (!$allowed) {
96d3b93b
JM
1400 throw new webservice_access_exception(
1401 'Access to the function '.$this->functionname.'() is not allowed.
ca11d390
JM
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.');
cc93c7da 1413 }
9baf6825 1414
cc93c7da 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.
cc93c7da 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);
b5311ce4 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 }
9baf6825 1443
cc93c7da 1444 // execute - yay!
b5311ce4 1445 $this->returns = call_user_func_array(array($this->function->classname, $this->function->methodname), $params);
9baf6825 1446 }
79c7fffc
JP
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.
79c7fffc
JP
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 */
1530class $classname {
1531$methods
1532}
1533EOD;
1534 // Load the virtual class definition into memory.
1535 eval($code);
1536 }
1537
1538 /**
1539 * Generates a struct class.
1540 *
79c7fffc
JP
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 */
1570class $classname {
1571$fieldsstr
9baf6825 1572}
79c7fffc
JP
1573EOD;
1574 // Load into memory.
1575 eval($code);
9baf6825 1576
79c7fffc
JP
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 }
9baf6825 1586
79c7fffc
JP
1587 /**
1588 * Returns a virtual method code for a web service function.
1589 *
79c7fffc
JP
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) {
11c16f5f 1596 $function = external_api::external_function_info($function);
79c7fffc
JP
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 */
1674public function $function->name($paramanddefaults) {
1675$serviceclassmethodbody
1676}
1677EOD;
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 *
79c7fffc
JP
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));
1753EOD;
1754 return $methodbody;
1755 }
1756}
4ed6010a
PFO
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 */
1766function 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}