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