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