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