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