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