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