Merge branch 'MDL-41660-imscc-tempdir' of https://github.com/mpetrowi/moodle
[moodle.git] / webservice / lib.php
blob1723d723740d29d249be5d14bb087dec6ece24d5
1 <?php
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.
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/>.
18 /**
19 * Web services utility functions and classes
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
26 require_once($CFG->libdir.'/externallib.php');
28 /**
29 * WEBSERVICE_AUTHMETHOD_USERNAME - username/password authentication (also called simple authentication)
31 define('WEBSERVICE_AUTHMETHOD_USERNAME', 0);
33 /**
34 * WEBSERVICE_AUTHMETHOD_PERMANENT_TOKEN - most common token authentication (external app, mobile app...)
36 define('WEBSERVICE_AUTHMETHOD_PERMANENT_TOKEN', 1);
38 /**
39 * WEBSERVICE_AUTHMETHOD_SESSION_TOKEN - token for embedded application (requires Moodle session)
41 define('WEBSERVICE_AUTHMETHOD_SESSION_TOKEN', 2);
43 /**
44 * General web service library
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
50 class webservice {
52 /**
53 * Authenticate user (used by download/upload file scripts)
55 * @param string $token
56 * @return array - contains the authenticated user, token and service objects
58 public function authenticate_user($token) {
59 global $DB, $CFG;
61 // web service must be enabled to use this script
62 if (!$CFG->enablewebservices) {
63 throw new webservice_access_exception('Web services are not enabled in Advanced features.');
66 // Obtain token record
67 if (!$token = $DB->get_record('external_tokens', array('token' => $token))) {
68 //client may want to display login form => moodle_exception
69 throw new moodle_exception('invalidtoken', 'webservice');
72 // Validate token date
73 if ($token->validuntil and $token->validuntil < time()) {
74 add_to_log(SITEID, 'webservice', get_string('tokenauthlog', 'webservice'), '', get_string('invalidtimedtoken', 'webservice'), 0);
75 $DB->delete_records('external_tokens', array('token' => $token->token));
76 throw new webservice_access_exception('Invalid token - token expired - check validuntil time for the token');
79 // Check ip
80 if ($token->iprestriction and !address_in_subnet(getremoteaddr(), $token->iprestriction)) {
81 add_to_log(SITEID, 'webservice', get_string('tokenauthlog', 'webservice'), '', get_string('failedtolog', 'webservice') . ": " . getremoteaddr(), 0);
82 throw new webservice_access_exception('Invalid token - IP:' . getremoteaddr()
83 . ' is not supported');
86 //retrieve user link to the token
87 $user = $DB->get_record('user', array('id' => $token->userid, 'deleted' => 0), '*', MUST_EXIST);
89 // let enrol plugins deal with new enrolments if necessary
90 enrol_check_plugins($user);
92 // setup user session to check capability
93 \core\session\manager::set_user($user);
95 //assumes that if sid is set then there must be a valid associated session no matter the token type
96 if ($token->sid) {
97 if (!\core\session\manager::session_exists($token->sid)) {
98 $DB->delete_records('external_tokens', array('sid' => $token->sid));
99 throw new webservice_access_exception('Invalid session based token - session not found or expired');
103 //Non admin can not authenticate if maintenance mode
104 $hassiteconfig = has_capability('moodle/site:config', context_system::instance(), $user);
105 if (!empty($CFG->maintenance_enabled) and !$hassiteconfig) {
106 //this is usually temporary, client want to implement code logic => moodle_exception
107 throw new moodle_exception('sitemaintenance', 'admin');
110 //retrieve web service record
111 $service = $DB->get_record('external_services', array('id' => $token->externalserviceid, 'enabled' => 1));
112 if (empty($service)) {
113 // will throw exception if no token found
114 throw new webservice_access_exception('Web service is not available (it doesn\'t exist or might be disabled)');
117 //check if there is any required system capability
118 if ($service->requiredcapability and !has_capability($service->requiredcapability, context_system::instance(), $user)) {
119 throw new webservice_access_exception('The capability ' . $service->requiredcapability . ' is required.');
122 //specific checks related to user restricted service
123 if ($service->restrictedusers) {
124 $authoriseduser = $DB->get_record('external_services_users', array('externalserviceid' => $service->id, 'userid' => $user->id));
126 if (empty($authoriseduser)) {
127 throw new webservice_access_exception(
128 'The user is not allowed for this service. First you need to allow this user on the '
129 . $service->name . '\'s allowed users administration page.');
132 if (!empty($authoriseduser->validuntil) and $authoriseduser->validuntil < time()) {
133 throw new webservice_access_exception('Invalid service - service expired - check validuntil time for this allowed user');
136 if (!empty($authoriseduser->iprestriction) and !address_in_subnet(getremoteaddr(), $authoriseduser->iprestriction)) {
137 throw new webservice_access_exception('Invalid service - IP:' . getremoteaddr()
138 . ' is not supported - check this allowed user');
142 //only confirmed user should be able to call web service
143 if (empty($user->confirmed)) {
144 add_to_log(SITEID, 'webservice', 'user unconfirmed', '', $user->username);
145 throw new moodle_exception('usernotconfirmed', 'moodle', '', $user->username);
148 //check the user is suspended
149 if (!empty($user->suspended)) {
150 add_to_log(SITEID, 'webservice', 'user suspended', '', $user->username);
151 throw new webservice_access_exception('Refused web service access for suspended username: ' . $user->username);
154 //check if the auth method is nologin (in this case refuse connection)
155 if ($user->auth == 'nologin') {
156 add_to_log(SITEID, 'webservice', 'nologin auth attempt with web service', '', $user->username);
157 throw new webservice_access_exception('Refused web service access for nologin authentication username: ' . $user->username);
160 //Check if the user password is expired
161 $auth = get_auth_plugin($user->auth);
162 if (!empty($auth->config->expiration) and $auth->config->expiration == 1) {
163 $days2expire = $auth->password_expire($user->username);
164 if (intval($days2expire) < 0) {
165 add_to_log(SITEID, 'webservice', 'expired password', '', $user->username);
166 throw new moodle_exception('passwordisexpired', 'webservice');
170 // log token access
171 $DB->set_field('external_tokens', 'lastaccess', time(), array('id' => $token->id));
173 return array('user' => $user, 'token' => $token, 'service' => $service);
177 * Allow user to call a service
179 * @param stdClass $user a user
181 public function add_ws_authorised_user($user) {
182 global $DB;
183 $user->timecreated = time();
184 $DB->insert_record('external_services_users', $user);
188 * Disallow a user to call a service
190 * @param stdClass $user a user
191 * @param int $serviceid
193 public function remove_ws_authorised_user($user, $serviceid) {
194 global $DB;
195 $DB->delete_records('external_services_users',
196 array('externalserviceid' => $serviceid, 'userid' => $user->id));
200 * Update allowed user settings (ip restriction, valid until...)
202 * @param stdClass $user
204 public function update_ws_authorised_user($user) {
205 global $DB;
206 $DB->update_record('external_services_users', $user);
210 * Return list of allowed users with their options (ip/timecreated / validuntil...)
211 * for a given service
213 * @param int $serviceid the service id to search against
214 * @return array $users
216 public function get_ws_authorised_users($serviceid) {
217 global $DB, $CFG;
218 $params = array($CFG->siteguest, $serviceid);
219 $sql = " SELECT u.id as id, esu.id as serviceuserid, u.email as email, u.firstname as firstname,
220 u.lastname as lastname,
221 esu.iprestriction as iprestriction, esu.validuntil as validuntil,
222 esu.timecreated as timecreated
223 FROM {user} u, {external_services_users} esu
224 WHERE u.id <> ? AND u.deleted = 0 AND u.confirmed = 1
225 AND esu.userid = u.id
226 AND esu.externalserviceid = ?";
228 $users = $DB->get_records_sql($sql, $params);
229 return $users;
233 * Return an authorised user with their options (ip/timecreated / validuntil...)
235 * @param int $serviceid the service id to search against
236 * @param int $userid the user to search against
237 * @return stdClass
239 public function get_ws_authorised_user($serviceid, $userid) {
240 global $DB, $CFG;
241 $params = array($CFG->siteguest, $serviceid, $userid);
242 $sql = " SELECT u.id as id, esu.id as serviceuserid, u.email as email, u.firstname as firstname,
243 u.lastname as lastname,
244 esu.iprestriction as iprestriction, esu.validuntil as validuntil,
245 esu.timecreated as timecreated
246 FROM {user} u, {external_services_users} esu
247 WHERE u.id <> ? AND u.deleted = 0 AND u.confirmed = 1
248 AND esu.userid = u.id
249 AND esu.externalserviceid = ?
250 AND u.id = ?";
251 $user = $DB->get_record_sql($sql, $params);
252 return $user;
256 * Generate all tokens of a specific user
258 * @param int $userid user id
260 public function generate_user_ws_tokens($userid) {
261 global $CFG, $DB;
263 // generate a token for non admin if web service are enable and the user has the capability to create a token
264 if (!is_siteadmin() && has_capability('moodle/webservice:createtoken', context_system::instance(), $userid) && !empty($CFG->enablewebservices)) {
265 // for every service than the user is authorised on, create a token (if it doesn't already exist)
267 // get all services which are set to all user (no restricted to specific users)
268 $norestrictedservices = $DB->get_records('external_services', array('restrictedusers' => 0));
269 $serviceidlist = array();
270 foreach ($norestrictedservices as $service) {
271 $serviceidlist[] = $service->id;
274 // get all services which are set to the current user (the current user is specified in the restricted user list)
275 $servicesusers = $DB->get_records('external_services_users', array('userid' => $userid));
276 foreach ($servicesusers as $serviceuser) {
277 if (!in_array($serviceuser->externalserviceid,$serviceidlist)) {
278 $serviceidlist[] = $serviceuser->externalserviceid;
282 // get all services which already have a token set for the current user
283 $usertokens = $DB->get_records('external_tokens', array('userid' => $userid, 'tokentype' => EXTERNAL_TOKEN_PERMANENT));
284 $tokenizedservice = array();
285 foreach ($usertokens as $token) {
286 $tokenizedservice[] = $token->externalserviceid;
289 // create a token for the service which have no token already
290 foreach ($serviceidlist as $serviceid) {
291 if (!in_array($serviceid, $tokenizedservice)) {
292 // create the token for this service
293 $newtoken = new stdClass();
294 $newtoken->token = md5(uniqid(rand(),1));
295 // check that the user has capability on this service
296 $newtoken->tokentype = EXTERNAL_TOKEN_PERMANENT;
297 $newtoken->userid = $userid;
298 $newtoken->externalserviceid = $serviceid;
299 // TODO MDL-31190 find a way to get the context - UPDATE FOLLOWING LINE
300 $newtoken->contextid = context_system::instance()->id;
301 $newtoken->creatorid = $userid;
302 $newtoken->timecreated = time();
304 $DB->insert_record('external_tokens', $newtoken);
313 * Return all tokens of a specific user
314 * + the service state (enabled/disabled)
315 * + the authorised user mode (restricted/not restricted)
317 * @param int $userid user id
318 * @return array
320 public function get_user_ws_tokens($userid) {
321 global $DB;
322 //here retrieve token list (including linked users firstname/lastname and linked services name)
323 $sql = "SELECT
324 t.id, t.creatorid, t.token, u.firstname, u.lastname, s.id as wsid, s.name, s.enabled, s.restrictedusers, t.validuntil
325 FROM
326 {external_tokens} t, {user} u, {external_services} s
327 WHERE
328 t.userid=? AND t.tokentype = ".EXTERNAL_TOKEN_PERMANENT." AND s.id = t.externalserviceid AND t.userid = u.id";
329 $tokens = $DB->get_records_sql($sql, array( $userid));
330 return $tokens;
334 * Return a token that has been created by the user (i.e. to created by an admin)
335 * If no tokens exist an exception is thrown
337 * The returned value is a stdClass:
338 * ->id token id
339 * ->token
340 * ->firstname user firstname
341 * ->lastname
342 * ->name service name
344 * @param int $userid user id
345 * @param int $tokenid token id
346 * @return stdClass
348 public function get_created_by_user_ws_token($userid, $tokenid) {
349 global $DB;
350 $sql = "SELECT
351 t.id, t.token, u.firstname, u.lastname, s.name
352 FROM
353 {external_tokens} t, {user} u, {external_services} s
354 WHERE
355 t.creatorid=? AND t.id=? AND t.tokentype = "
356 . EXTERNAL_TOKEN_PERMANENT
357 . " AND s.id = t.externalserviceid AND t.userid = u.id";
358 //must be the token creator
359 $token = $DB->get_record_sql($sql, array($userid, $tokenid), MUST_EXIST);
360 return $token;
364 * Return a database token record for a token id
366 * @param int $tokenid token id
367 * @return object token
369 public function get_token_by_id($tokenid) {
370 global $DB;
371 return $DB->get_record('external_tokens', array('id' => $tokenid));
375 * Delete a token
377 * @param int $tokenid token id
379 public function delete_user_ws_token($tokenid) {
380 global $DB;
381 $DB->delete_records('external_tokens', array('id'=>$tokenid));
385 * Delete a service
386 * Also delete function references and authorised user references.
388 * @param int $serviceid service id
390 public function delete_service($serviceid) {
391 global $DB;
392 $DB->delete_records('external_services_users', array('externalserviceid' => $serviceid));
393 $DB->delete_records('external_services_functions', array('externalserviceid' => $serviceid));
394 $DB->delete_records('external_tokens', array('externalserviceid' => $serviceid));
395 $DB->delete_records('external_services', array('id' => $serviceid));
399 * Get a full database token record for a given token value
401 * @param string $token
402 * @throws moodle_exception if there is multiple result
404 public function get_user_ws_token($token) {
405 global $DB;
406 return $DB->get_record('external_tokens', array('token'=>$token), '*', MUST_EXIST);
410 * Get the functions list of a service list (by id)
412 * @param array $serviceids service ids
413 * @return array of functions
415 public function get_external_functions($serviceids) {
416 global $DB;
417 if (!empty($serviceids)) {
418 list($serviceids, $params) = $DB->get_in_or_equal($serviceids);
419 $sql = "SELECT f.*
420 FROM {external_functions} f
421 WHERE f.name IN (SELECT sf.functionname
422 FROM {external_services_functions} sf
423 WHERE sf.externalserviceid $serviceids)";
424 $functions = $DB->get_records_sql($sql, $params);
425 } else {
426 $functions = array();
428 return $functions;
432 * Get the functions of a service list (by shortname). It can return only enabled functions if required.
434 * @param array $serviceshortnames service shortnames
435 * @param bool $enabledonly if true then only return functions for services that have been enabled
436 * @return array functions
438 public function get_external_functions_by_enabled_services($serviceshortnames, $enabledonly = true) {
439 global $DB;
440 if (!empty($serviceshortnames)) {
441 $enabledonlysql = $enabledonly?' AND s.enabled = 1 ':'';
442 list($serviceshortnames, $params) = $DB->get_in_or_equal($serviceshortnames);
443 $sql = "SELECT f.*
444 FROM {external_functions} f
445 WHERE f.name IN (SELECT sf.functionname
446 FROM {external_services_functions} sf, {external_services} s
447 WHERE s.shortname $serviceshortnames
448 AND sf.externalserviceid = s.id
449 " . $enabledonlysql . ")";
450 $functions = $DB->get_records_sql($sql, $params);
451 } else {
452 $functions = array();
454 return $functions;
458 * Get functions not included in a service
460 * @param int $serviceid service id
461 * @return array functions
463 public function get_not_associated_external_functions($serviceid) {
464 global $DB;
465 $select = "name NOT IN (SELECT s.functionname
466 FROM {external_services_functions} s
467 WHERE s.externalserviceid = :sid
470 $functions = $DB->get_records_select('external_functions',
471 $select, array('sid' => $serviceid), 'name');
473 return $functions;
477 * Get list of required capabilities of a service, sorted by functions
478 * Example of returned value:
479 * Array
481 * [moodle_group_create_groups] => Array
483 * [0] => moodle/course:managegroups
486 * [moodle_enrol_get_enrolled_users] => Array
488 * [0] => moodle/site:viewparticipants
489 * [1] => moodle/course:viewparticipants
490 * [2] => moodle/role:review
491 * [3] => moodle/site:accessallgroups
492 * [4] => moodle/course:enrolreview
496 * @param int $serviceid service id
497 * @return array
499 public function get_service_required_capabilities($serviceid) {
500 $functions = $this->get_external_functions(array($serviceid));
501 $requiredusercaps = array();
502 foreach ($functions as $function) {
503 $functioncaps = explode(',', $function->capabilities);
504 if (!empty($functioncaps) and !empty($functioncaps[0])) {
505 foreach ($functioncaps as $functioncap) {
506 $requiredusercaps[$function->name][] = trim($functioncap);
510 return $requiredusercaps;
514 * Get user capabilities (with context)
515 * Only useful for documentation purpose
516 * WARNING: do not use this "broken" function. It was created in the goal to display some capabilities
517 * required by users. In theory we should not need to display this kind of information
518 * as the front end does not display it itself. In pratice,
519 * admins would like the info, for more info you can follow: MDL-29962
521 * @param int $userid user id
522 * @return array
524 public function get_user_capabilities($userid) {
525 global $DB;
526 //retrieve the user capabilities
527 $sql = "SELECT DISTINCT rc.id, rc.capability FROM {role_capabilities} rc, {role_assignments} ra
528 WHERE rc.roleid=ra.roleid AND ra.userid= ? AND rc.permission = ?";
529 $dbusercaps = $DB->get_records_sql($sql, array($userid, CAP_ALLOW));
530 $usercaps = array();
531 foreach ($dbusercaps as $usercap) {
532 $usercaps[$usercap->capability] = true;
534 return $usercaps;
538 * Get missing user capabilities for a given service
539 * WARNING: do not use this "broken" function. It was created in the goal to display some capabilities
540 * required by users. In theory we should not need to display this kind of information
541 * as the front end does not display it itself. In pratice,
542 * admins would like the info, for more info you can follow: MDL-29962
544 * @param array $users users
545 * @param int $serviceid service id
546 * @return array of missing capabilities, keys being the user ids
548 public function get_missing_capabilities_by_users($users, $serviceid) {
549 global $DB;
550 $usersmissingcaps = array();
552 //retrieve capabilities required by the service
553 $servicecaps = $this->get_service_required_capabilities($serviceid);
555 //retrieve users missing capabilities
556 foreach ($users as $user) {
557 //cast user array into object to be a bit more flexible
558 if (is_array($user)) {
559 $user = (object) $user;
561 $usercaps = $this->get_user_capabilities($user->id);
563 //detect the missing capabilities
564 foreach ($servicecaps as $functioname => $functioncaps) {
565 foreach ($functioncaps as $functioncap) {
566 if (!array_key_exists($functioncap, $usercaps)) {
567 if (!isset($usersmissingcaps[$user->id])
568 or array_search($functioncap, $usersmissingcaps[$user->id]) === false) {
569 $usersmissingcaps[$user->id][] = $functioncap;
576 return $usersmissingcaps;
580 * Get an external service for a given service id
582 * @param int $serviceid service id
583 * @param int $strictness IGNORE_MISSING, MUST_EXIST...
584 * @return stdClass external service
586 public function get_external_service_by_id($serviceid, $strictness=IGNORE_MISSING) {
587 global $DB;
588 $service = $DB->get_record('external_services',
589 array('id' => $serviceid), '*', $strictness);
590 return $service;
594 * Get an external service for a given shortname
596 * @param string $shortname service shortname
597 * @param int $strictness IGNORE_MISSING, MUST_EXIST...
598 * @return stdClass external service
600 public function get_external_service_by_shortname($shortname, $strictness=IGNORE_MISSING) {
601 global $DB;
602 $service = $DB->get_record('external_services',
603 array('shortname' => $shortname), '*', $strictness);
604 return $service;
608 * Get an external function for a given function id
610 * @param int $functionid function id
611 * @param int $strictness IGNORE_MISSING, MUST_EXIST...
612 * @return stdClass external function
614 public function get_external_function_by_id($functionid, $strictness=IGNORE_MISSING) {
615 global $DB;
616 $function = $DB->get_record('external_functions',
617 array('id' => $functionid), '*', $strictness);
618 return $function;
622 * Add a function to a service
624 * @param string $functionname function name
625 * @param int $serviceid service id
627 public function add_external_function_to_service($functionname, $serviceid) {
628 global $DB;
629 $addedfunction = new stdClass();
630 $addedfunction->externalserviceid = $serviceid;
631 $addedfunction->functionname = $functionname;
632 $DB->insert_record('external_services_functions', $addedfunction);
636 * Add a service
637 * It generates the timecreated field automatically.
639 * @param stdClass $service
640 * @return serviceid integer
642 public function add_external_service($service) {
643 global $DB;
644 $service->timecreated = time();
645 $serviceid = $DB->insert_record('external_services', $service);
646 return $serviceid;
650 * Update a service
651 * It modifies the timemodified automatically.
653 * @param stdClass $service
655 public function update_external_service($service) {
656 global $DB;
657 $service->timemodified = time();
658 $DB->update_record('external_services', $service);
662 * Test whether an external function is already linked to a service
664 * @param string $functionname function name
665 * @param int $serviceid service id
666 * @return bool true if a matching function exists for the service, else false.
667 * @throws dml_exception if error
669 public function service_function_exists($functionname, $serviceid) {
670 global $DB;
671 return $DB->record_exists('external_services_functions',
672 array('externalserviceid' => $serviceid,
673 'functionname' => $functionname));
677 * Remove a function from a service
679 * @param string $functionname function name
680 * @param int $serviceid service id
682 public function remove_external_function_from_service($functionname, $serviceid) {
683 global $DB;
684 $DB->delete_records('external_services_functions',
685 array('externalserviceid' => $serviceid, 'functionname' => $functionname));
693 * Exception indicating access control problem in web service call
694 * This exception should return general errors about web service setup.
695 * Errors related to the user like wrong username/password should not use it,
696 * you should not use this exception if you want to let the client implement
697 * some code logic against an access error.
699 * @package core_webservice
700 * @copyright 2009 Petr Skodak
701 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
703 class webservice_access_exception extends moodle_exception {
706 * Constructor
708 * @param string $debuginfo the debug info
710 function __construct($debuginfo) {
711 parent::__construct('accessexception', 'webservice', '', null, $debuginfo);
716 * Check if a protocol is enabled
718 * @param string $protocol name of WS protocol ('rest', 'soap', 'xmlrpc', 'amf'...)
719 * @return bool true if the protocol is enabled
721 function webservice_protocol_is_enabled($protocol) {
722 global $CFG;
724 if (empty($CFG->enablewebservices)) {
725 return false;
728 $active = explode(',', $CFG->webserviceprotocols);
730 return(in_array($protocol, $active));
734 * Mandatory interface for all test client classes.
736 * @package core_webservice
737 * @copyright 2009 Petr Skodak
738 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
740 interface webservice_test_client_interface {
743 * Execute test client WS request
745 * @param string $serverurl server url (including the token param)
746 * @param string $function web service function name
747 * @param array $params parameters of the web service function
748 * @return mixed
750 public function simpletest($serverurl, $function, $params);
754 * Mandatory interface for all web service protocol classes
756 * @package core_webservice
757 * @copyright 2009 Petr Skodak
758 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
760 interface webservice_server_interface {
763 * Process request from client.
765 public function run();
769 * Abstract web service base class.
771 * @package core_webservice
772 * @copyright 2009 Petr Skodak
773 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
775 abstract class webservice_server implements webservice_server_interface {
777 /** @var string Name of the web server plugin */
778 protected $wsname = null;
780 /** @var string Name of local user */
781 protected $username = null;
783 /** @var string Password of the local user */
784 protected $password = null;
786 /** @var int The local user */
787 protected $userid = null;
789 /** @var integer Authentication method one of WEBSERVICE_AUTHMETHOD_* */
790 protected $authmethod;
792 /** @var string Authentication token*/
793 protected $token = null;
795 /** @var stdClass Restricted context */
796 protected $restricted_context;
798 /** @var int Restrict call to one service id*/
799 protected $restricted_serviceid = null;
802 * Constructor
804 * @param integer $authmethod authentication method one of WEBSERVICE_AUTHMETHOD_*
806 public function __construct($authmethod) {
807 $this->authmethod = $authmethod;
812 * Authenticate user using username+password or token.
813 * This function sets up $USER global.
814 * It is safe to use has_capability() after this.
815 * This method also verifies user is allowed to use this
816 * server.
818 protected function authenticate_user() {
819 global $CFG, $DB;
821 if (!NO_MOODLE_COOKIES) {
822 throw new coding_exception('Cookies must be disabled in WS servers!');
825 if ($this->authmethod == WEBSERVICE_AUTHMETHOD_USERNAME) {
827 //we check that authentication plugin is enabled
828 //it is only required by simple authentication
829 if (!is_enabled_auth('webservice')) {
830 throw new webservice_access_exception('The web service authentication plugin is disabled.');
833 if (!$auth = get_auth_plugin('webservice')) {
834 throw new webservice_access_exception('The web service authentication plugin is missing.');
837 $this->restricted_context = context_system::instance();
839 if (!$this->username) {
840 throw new moodle_exception('missingusername', 'webservice');
843 if (!$this->password) {
844 throw new moodle_exception('missingpassword', 'webservice');
847 if (!$auth->user_login_webservice($this->username, $this->password)) {
848 // log failed login attempts
849 add_to_log(SITEID, 'webservice', get_string('simpleauthlog', 'webservice'), '' , get_string('failedtolog', 'webservice').": ".$this->username."/".$this->password." - ".getremoteaddr() , 0);
850 throw new moodle_exception('wrongusernamepassword', 'webservice');
853 $user = $DB->get_record('user', array('username'=>$this->username, 'mnethostid'=>$CFG->mnet_localhost_id), '*', MUST_EXIST);
855 } else if ($this->authmethod == WEBSERVICE_AUTHMETHOD_PERMANENT_TOKEN){
856 $user = $this->authenticate_by_token(EXTERNAL_TOKEN_PERMANENT);
857 } else {
858 $user = $this->authenticate_by_token(EXTERNAL_TOKEN_EMBEDDED);
861 //Non admin can not authenticate if maintenance mode
862 $hassiteconfig = has_capability('moodle/site:config', context_system::instance(), $user);
863 if (!empty($CFG->maintenance_enabled) and !$hassiteconfig) {
864 throw new moodle_exception('sitemaintenance', 'admin');
867 //only confirmed user should be able to call web service
868 if (!empty($user->deleted)) {
869 add_to_log(SITEID, '', '', '', get_string('wsaccessuserdeleted', 'webservice', $user->username) . " - ".getremoteaddr(), 0, $user->id);
870 throw new webservice_access_exception('Refused web service access for deleted username: ' . $user->username);
873 //only confirmed user should be able to call web service
874 if (empty($user->confirmed)) {
875 add_to_log(SITEID, '', '', '', get_string('wsaccessuserunconfirmed', 'webservice', $user->username) . " - ".getremoteaddr(), 0, $user->id);
876 throw new moodle_exception('wsaccessuserunconfirmed', 'webservice', '', $user->username);
879 //check the user is suspended
880 if (!empty($user->suspended)) {
881 add_to_log(SITEID, '', '', '', get_string('wsaccessusersuspended', 'webservice', $user->username) . " - ".getremoteaddr(), 0, $user->id);
882 throw new webservice_access_exception('Refused web service access for suspended username: ' . $user->username);
885 //retrieve the authentication plugin if no previously done
886 if (empty($auth)) {
887 $auth = get_auth_plugin($user->auth);
890 // check if credentials have expired
891 if (!empty($auth->config->expiration) and $auth->config->expiration == 1) {
892 $days2expire = $auth->password_expire($user->username);
893 if (intval($days2expire) < 0 ) {
894 add_to_log(SITEID, '', '', '', get_string('wsaccessuserexpired', 'webservice', $user->username) . " - ".getremoteaddr(), 0, $user->id);
895 throw new webservice_access_exception('Refused web service access for password expired username: ' . $user->username);
899 //check if the auth method is nologin (in this case refuse connection)
900 if ($user->auth=='nologin') {
901 add_to_log(SITEID, '', '', '', get_string('wsaccessusernologin', 'webservice', $user->username) . " - ".getremoteaddr(), 0, $user->id);
902 throw new webservice_access_exception('Refused web service access for nologin authentication username: ' . $user->username);
905 // now fake user login, the session is completely empty too
906 enrol_check_plugins($user);
907 \core\session\manager::set_user($user);
908 $this->userid = $user->id;
910 if ($this->authmethod != WEBSERVICE_AUTHMETHOD_SESSION_TOKEN && !has_capability("webservice/$this->wsname:use", $this->restricted_context)) {
911 throw new webservice_access_exception('You are not allowed to use the {$a} protocol (missing capability: webservice/' . $this->wsname . ':use)');
914 external_api::set_context_restriction($this->restricted_context);
918 * User authentication by token
920 * @param string $tokentype token type (EXTERNAL_TOKEN_EMBEDDED or EXTERNAL_TOKEN_PERMANENT)
921 * @return stdClass the authenticated user
922 * @throws webservice_access_exception
924 protected function authenticate_by_token($tokentype){
925 global $DB;
926 if (!$token = $DB->get_record('external_tokens', array('token'=>$this->token, 'tokentype'=>$tokentype))) {
927 // log failed login attempts
928 add_to_log(SITEID, 'webservice', get_string('tokenauthlog', 'webservice'), '' , get_string('failedtolog', 'webservice').": ".$this->token. " - ".getremoteaddr() , 0);
929 throw new moodle_exception('invalidtoken', 'webservice');
932 if ($token->validuntil and $token->validuntil < time()) {
933 $DB->delete_records('external_tokens', array('token'=>$this->token, 'tokentype'=>$tokentype));
934 throw new webservice_access_exception('Invalid token - token expired - check validuntil time for the token');
937 if ($token->sid){//assumes that if sid is set then there must be a valid associated session no matter the token type
938 if (!\core\session\manager::session_exists($token->sid)){
939 $DB->delete_records('external_tokens', array('sid'=>$token->sid));
940 throw new webservice_access_exception('Invalid session based token - session not found or expired');
944 if ($token->iprestriction and !address_in_subnet(getremoteaddr(), $token->iprestriction)) {
945 add_to_log(SITEID, 'webservice', get_string('tokenauthlog', 'webservice'), '' , get_string('failedtolog', 'webservice').": ".getremoteaddr() , 0);
946 throw new webservice_access_exception('Invalid service - IP:' . getremoteaddr()
947 . ' is not supported - check this allowed user');
950 $this->restricted_context = context::instance_by_id($token->contextid);
951 $this->restricted_serviceid = $token->externalserviceid;
953 $user = $DB->get_record('user', array('id'=>$token->userid), '*', MUST_EXIST);
955 // log token access
956 $DB->set_field('external_tokens', 'lastaccess', time(), array('id'=>$token->id));
958 return $user;
963 * Intercept some moodlewssettingXXX $_GET and $_POST parameter
964 * that are related to the web service call and are not the function parameters
966 protected function set_web_service_call_settings() {
967 global $CFG;
969 // Default web service settings.
970 // Must be the same XXX key name as the external_settings::set_XXX function.
971 // Must be the same XXX ws parameter name as 'moodlewssettingXXX'.
972 $externalsettings = array(
973 'raw' => false,
974 'fileurl' => true,
975 'filter' => false);
977 // Load the external settings with the web service settings.
978 $settings = external_settings::get_instance();
979 foreach ($externalsettings as $name => $default) {
981 $wsparamname = 'moodlewssetting' . $name;
983 // Retrieve and remove the setting parameter from the request.
984 $value = optional_param($wsparamname, $default, PARAM_BOOL);
985 unset($_GET[$wsparamname]);
986 unset($_POST[$wsparamname]);
988 $functioname = 'set_' . $name;
989 $settings->$functioname($value);
996 * Special abstraction of our services that allows interaction with stock Zend ws servers.
998 * @package core_webservice
999 * @copyright 2009 Jerome Mouneyrac <jerome@moodle.com>
1000 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1002 abstract class webservice_zend_server extends webservice_server {
1004 /** @var string Name of the zend server class : Zend_Amf_Server, moodle_zend_soap_server, Zend_Soap_AutoDiscover, ...*/
1005 protected $zend_class;
1007 /** @var stdClass Zend server instance */
1008 protected $zend_server;
1010 /** @var string Virtual web service class with all functions user name execute, created on the fly */
1011 protected $service_class;
1014 * Constructor
1016 * @param int $authmethod authentication method - one of WEBSERVICE_AUTHMETHOD_*
1017 * @param string $zend_class Name of the zend server class
1019 public function __construct($authmethod, $zend_class) {
1020 parent::__construct($authmethod);
1021 $this->zend_class = $zend_class;
1025 * Process request from client.
1027 * @uses die
1029 public function run() {
1030 // we will probably need a lot of memory in some functions
1031 raise_memory_limit(MEMORY_EXTRA);
1033 // set some longer timeout, this script is not sending any output,
1034 // this means we need to manually extend the timeout operations
1035 // that need longer time to finish
1036 external_api::set_timeout();
1038 // now create the instance of zend server
1039 $this->init_zend_server();
1041 // set up exception handler first, we want to sent them back in correct format that
1042 // the other system understands
1043 // we do not need to call the original default handler because this ws handler does everything
1044 set_exception_handler(array($this, 'exception_handler'));
1046 // init all properties from the request data
1047 $this->parse_request();
1049 // this sets up $USER and $SESSION and context restrictions
1050 $this->authenticate_user();
1052 // make a list of all functions user is allowed to excecute
1053 $this->init_service_class();
1055 // tell server what functions are available
1056 $this->zend_server->setClass($this->service_class);
1058 //log the web service request
1059 add_to_log(SITEID, 'webservice', '', '' , $this->zend_class." ".getremoteaddr() , 0, $this->userid);
1061 //send headers
1062 $this->send_headers();
1064 // execute and return response, this sends some headers too
1065 $response = $this->zend_server->handle();
1067 // session cleanup
1068 $this->session_cleanup();
1070 //finally send the result
1071 echo $response;
1072 die;
1076 * Load virtual class needed for Zend api
1078 protected function init_service_class() {
1079 global $USER, $DB;
1081 // first ofall get a complete list of services user is allowed to access
1083 if ($this->restricted_serviceid) {
1084 $params = array('sid1'=>$this->restricted_serviceid, 'sid2'=>$this->restricted_serviceid);
1085 $wscond1 = 'AND s.id = :sid1';
1086 $wscond2 = 'AND s.id = :sid2';
1087 } else {
1088 $params = array();
1089 $wscond1 = '';
1090 $wscond2 = '';
1093 // now make sure the function is listed in at least one service user is allowed to use
1094 // allow access only if:
1095 // 1/ entry in the external_services_users table if required
1096 // 2/ validuntil not reached
1097 // 3/ has capability if specified in service desc
1098 // 4/ iprestriction
1100 $sql = "SELECT s.*, NULL AS iprestriction
1101 FROM {external_services} s
1102 JOIN {external_services_functions} sf ON (sf.externalserviceid = s.id AND s.restrictedusers = 0)
1103 WHERE s.enabled = 1 $wscond1
1105 UNION
1107 SELECT s.*, su.iprestriction
1108 FROM {external_services} s
1109 JOIN {external_services_functions} sf ON (sf.externalserviceid = s.id AND s.restrictedusers = 1)
1110 JOIN {external_services_users} su ON (su.externalserviceid = s.id AND su.userid = :userid)
1111 WHERE s.enabled = 1 AND (su.validuntil IS NULL OR su.validuntil < :now) $wscond2";
1113 $params = array_merge($params, array('userid'=>$USER->id, 'now'=>time()));
1115 $serviceids = array();
1116 $rs = $DB->get_recordset_sql($sql, $params);
1118 // now make sure user may access at least one service
1119 $remoteaddr = getremoteaddr();
1120 $allowed = false;
1121 foreach ($rs as $service) {
1122 if (isset($serviceids[$service->id])) {
1123 continue;
1125 if ($service->requiredcapability and !has_capability($service->requiredcapability, $this->restricted_context)) {
1126 continue; // cap required, sorry
1128 if ($service->iprestriction and !address_in_subnet($remoteaddr, $service->iprestriction)) {
1129 continue; // wrong request source ip, sorry
1131 $serviceids[$service->id] = $service->id;
1133 $rs->close();
1135 // now get the list of all functions
1136 $wsmanager = new webservice();
1137 $functions = $wsmanager->get_external_functions($serviceids);
1139 // now make the virtual WS class with all the fuctions for this particular user
1140 $methods = '';
1141 foreach ($functions as $function) {
1142 $methods .= $this->get_virtual_method_code($function);
1145 // let's use unique class name, there might be problem in unit tests
1146 $classname = 'webservices_virtual_class_000000';
1147 while(class_exists($classname)) {
1148 $classname++;
1151 $code = '
1153 * Virtual class web services for user id '.$USER->id.' in context '.$this->restricted_context->id.'.
1155 class '.$classname.' {
1156 '.$methods.'
1160 // load the virtual class definition into memory
1161 eval($code);
1162 $this->service_class = $classname;
1166 * returns virtual method code
1168 * @param stdClass $function a record from external_function
1169 * @return string PHP code
1171 protected function get_virtual_method_code($function) {
1172 global $CFG;
1174 $function = external_function_info($function);
1176 //arguments in function declaration line with defaults.
1177 $paramanddefaults = array();
1178 //arguments used as parameters for external lib call.
1179 $params = array();
1180 $params_desc = array();
1181 foreach ($function->parameters_desc->keys as $name=>$keydesc) {
1182 $param = '$'.$name;
1183 $paramanddefault = $param;
1184 //need to generate the default if there is any
1185 if ($keydesc instanceof external_value) {
1186 if ($keydesc->required == VALUE_DEFAULT) {
1187 if ($keydesc->default===null) {
1188 $paramanddefault .= '=null';
1189 } else {
1190 switch($keydesc->type) {
1191 case PARAM_BOOL:
1192 $paramanddefault .= '='. (int) $keydesc->default; break;
1193 case PARAM_INT:
1194 $paramanddefault .= '='.$keydesc->default; break;
1195 case PARAM_FLOAT;
1196 $paramanddefault .= '='.$keydesc->default; break;
1197 default:
1198 $paramanddefault .= '=\''.$keydesc->default.'\'';
1201 } else if ($keydesc->required == VALUE_OPTIONAL) {
1202 //it does make sens to declare a parameter VALUE_OPTIONAL
1203 //VALUE_OPTIONAL is used only for array/object key
1204 throw new moodle_exception('parametercannotbevalueoptional');
1206 } else { //for the moment we do not support default for other structure types
1207 if ($keydesc->required == VALUE_DEFAULT) {
1208 //accept empty array as default
1209 if (isset($keydesc->default) and is_array($keydesc->default)
1210 and empty($keydesc->default)) {
1211 $paramanddefault .= '=array()';
1212 } else {
1213 throw new moodle_exception('errornotemptydefaultparamarray', 'webservice', '', $name);
1216 if ($keydesc->required == VALUE_OPTIONAL) {
1217 throw new moodle_exception('erroroptionalparamarray', 'webservice', '', $name);
1220 $params[] = $param;
1221 $paramanddefaults[] = $paramanddefault;
1222 $type = $this->get_phpdoc_type($keydesc);
1223 $params_desc[] = ' * @param '.$type.' $'.$name.' '.$keydesc->desc;
1225 $params = implode(', ', $params);
1226 $paramanddefaults = implode(', ', $paramanddefaults);
1227 $params_desc = implode("\n", $params_desc);
1229 $serviceclassmethodbody = $this->service_class_method_body($function, $params);
1231 if (is_null($function->returns_desc)) {
1232 $return = ' * @return void';
1233 } else {
1234 $type = $this->get_phpdoc_type($function->returns_desc);
1235 $return = ' * @return '.$type.' '.$function->returns_desc->desc;
1238 // now crate the virtual method that calls the ext implementation
1240 $code = '
1242 * '.$function->description.'
1244 '.$params_desc.'
1245 '.$return.'
1247 public function '.$function->name.'('.$paramanddefaults.') {
1248 '.$serviceclassmethodbody.'
1251 return $code;
1255 * Get the phpdoc type for an external_description
1256 * external_value => int, double or string
1257 * external_single_structure => object|struct, on-fly generated stdClass name, ...
1258 * external_multiple_structure => array
1260 * @param string $keydesc any of PARAM_*
1261 * @return string phpdoc type (string, double, int, array...)
1263 protected function get_phpdoc_type($keydesc) {
1264 if ($keydesc instanceof external_value) {
1265 switch($keydesc->type) {
1266 case PARAM_BOOL: // 0 or 1 only for now
1267 case PARAM_INT:
1268 $type = 'int'; break;
1269 case PARAM_FLOAT;
1270 $type = 'double'; break;
1271 default:
1272 $type = 'string';
1275 } else if ($keydesc instanceof external_single_structure) {
1276 $classname = $this->generate_simple_struct_class($keydesc);
1277 $type = $classname;
1279 } else if ($keydesc instanceof external_multiple_structure) {
1280 $type = 'array';
1283 return $type;
1287 * Generate 'struct'/'object' type name
1288 * Some servers (our Zend ones) parse the phpdoc to know the parameter types.
1289 * The purpose to this function is to be overwritten when the common object|struct type are not understood by the server.
1290 * See webservice/soap/locallib.php - the SOAP server requires detailed structure)
1292 * @param external_single_structure $structdesc the structure for which we generate the phpdoc type
1293 * @return string the phpdoc type
1295 protected function generate_simple_struct_class(external_single_structure $structdesc) {
1296 return 'object|struct'; //only 'object' is supported by SOAP, 'struct' by XML-RPC MDL-23083
1300 * You can override this function in your child class to add extra code into the dynamically
1301 * created service class. For example it is used in the amf server to cast types of parameters and to
1302 * cast the return value to the types as specified in the return value description.
1304 * @param stdClass $function a record from external_function
1305 * @param array $params web service function parameters
1306 * @return string body of the method for $function ie. everything within the {} of the method declaration.
1308 protected function service_class_method_body($function, $params){
1309 //cast the param from object to array (validate_parameters except array only)
1310 $castingcode = '';
1311 if ($params){
1312 $paramstocast = explode(',', $params);
1313 foreach ($paramstocast as $paramtocast) {
1314 //clean the parameter from any white space
1315 $paramtocast = trim($paramtocast);
1316 $castingcode .= $paramtocast .
1317 '=webservice_zend_server::cast_objects_to_array('.$paramtocast.');';
1322 $descriptionmethod = $function->methodname.'_returns()';
1323 $callforreturnvaluedesc = $function->classname.'::'.$descriptionmethod;
1324 return $castingcode . ' if ('.$callforreturnvaluedesc.' == null) {'.
1325 $function->classname.'::'.$function->methodname.'('.$params.');
1326 return null;
1328 return external_api::clean_returnvalue('.$callforreturnvaluedesc.', '.$function->classname.'::'.$function->methodname.'('.$params.'));';
1332 * Recursive function to recurse down into a complex variable and convert all
1333 * objects to arrays.
1335 * @param mixed $param value to cast
1336 * @return mixed Cast value
1338 public static function cast_objects_to_array($param){
1339 if (is_object($param)){
1340 $param = (array)$param;
1342 if (is_array($param)){
1343 $toreturn = array();
1344 foreach ($param as $key=> $param){
1345 $toreturn[$key] = self::cast_objects_to_array($param);
1347 return $toreturn;
1348 } else {
1349 return $param;
1354 * Set up zend service class
1356 protected function init_zend_server() {
1357 $this->zend_server = new $this->zend_class();
1361 * This method parses the $_POST and $_GET superglobals and looks for
1362 * the following information:
1363 * 1/ user authentication - username+password or token (wsusername, wspassword and wstoken parameters)
1365 * @return void
1367 protected function parse_request() {
1369 // We are going to clean the POST/GET parameters from the parameters specific to the server.
1370 parent::set_web_service_call_settings();
1372 // Get GET and POST paramters.
1373 $methodvariables = array_merge($_GET,$_POST);
1375 if ($this->authmethod == WEBSERVICE_AUTHMETHOD_USERNAME) {
1376 //note: some clients have problems with entity encoding :-(
1377 if (isset($methodvariables['wsusername'])) {
1378 $this->username = $methodvariables['wsusername'];
1380 if (isset($methodvariables['wspassword'])) {
1381 $this->password = $methodvariables['wspassword'];
1383 } else {
1384 if (isset($methodvariables['wstoken'])) {
1385 $this->token = $methodvariables['wstoken'];
1391 * Internal implementation - sending of page headers.
1393 protected function send_headers() {
1394 header('Cache-Control: private, must-revalidate, pre-check=0, post-check=0, max-age=0');
1395 header('Expires: '. gmdate('D, d M Y H:i:s', 0) .' GMT');
1396 header('Pragma: no-cache');
1397 header('Accept-Ranges: none');
1401 * Specialised exception handler, we can not use the standard one because
1402 * it can not just print html to output.
1404 * @param exception $ex
1405 * @uses exit
1407 public function exception_handler($ex) {
1408 // detect active db transactions, rollback and log as error
1409 abort_all_db_transactions();
1411 // some hacks might need a cleanup hook
1412 $this->session_cleanup($ex);
1414 // now let the plugin send the exception to client
1415 $this->send_error($ex);
1417 // not much else we can do now, add some logging later
1418 exit(1);
1422 * Send the error information to the WS client
1423 * formatted as XML document.
1425 * @param exception $ex
1427 protected function send_error($ex=null) {
1428 $this->send_headers();
1429 echo $this->zend_server->fault($ex);
1433 * Future hook needed for emulated sessions.
1435 * @param exception $exception null means normal termination, $exception received when WS call failed
1437 protected function session_cleanup($exception=null) {
1438 if ($this->authmethod == WEBSERVICE_AUTHMETHOD_USERNAME) {
1439 // nothing needs to be done, there is no persistent session
1440 } else {
1441 // close emulated session if used
1448 * Web Service server base class.
1450 * This class handles both simple and token authentication.
1452 * @package core_webservice
1453 * @copyright 2009 Petr Skodak
1454 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1456 abstract class webservice_base_server extends webservice_server {
1458 /** @var array The function parameters - the real values submitted in the request */
1459 protected $parameters = null;
1461 /** @var string The name of the function that is executed */
1462 protected $functionname = null;
1464 /** @var stdClass Full function description */
1465 protected $function = null;
1467 /** @var mixed Function return value */
1468 protected $returns = null;
1471 * This method parses the request input, it needs to get:
1472 * 1/ user authentication - username+password or token
1473 * 2/ function name
1474 * 3/ function parameters
1476 abstract protected function parse_request();
1479 * Send the result of function call to the WS client.
1481 abstract protected function send_response();
1484 * Send the error information to the WS client.
1486 * @param exception $ex
1488 abstract protected function send_error($ex=null);
1491 * Process request from client.
1493 * @uses die
1495 public function run() {
1496 // we will probably need a lot of memory in some functions
1497 raise_memory_limit(MEMORY_EXTRA);
1499 // set some longer timeout, this script is not sending any output,
1500 // this means we need to manually extend the timeout operations
1501 // that need longer time to finish
1502 external_api::set_timeout();
1504 // set up exception handler first, we want to sent them back in correct format that
1505 // the other system understands
1506 // we do not need to call the original default handler because this ws handler does everything
1507 set_exception_handler(array($this, 'exception_handler'));
1509 // init all properties from the request data
1510 $this->parse_request();
1512 // authenticate user, this has to be done after the request parsing
1513 // this also sets up $USER and $SESSION
1514 $this->authenticate_user();
1516 // find all needed function info and make sure user may actually execute the function
1517 $this->load_function_info();
1519 //log the web service request
1520 add_to_log(SITEID, 'webservice', $this->functionname, '' , getremoteaddr() , 0, $this->userid);
1522 // finally, execute the function - any errors are catched by the default exception handler
1523 $this->execute();
1525 // send the results back in correct format
1526 $this->send_response();
1528 // session cleanup
1529 $this->session_cleanup();
1531 die;
1535 * Specialised exception handler, we can not use the standard one because
1536 * it can not just print html to output.
1538 * @param exception $ex
1539 * $uses exit
1541 public function exception_handler($ex) {
1542 // detect active db transactions, rollback and log as error
1543 abort_all_db_transactions();
1545 // some hacks might need a cleanup hook
1546 $this->session_cleanup($ex);
1548 // now let the plugin send the exception to client
1549 $this->send_error($ex);
1551 // not much else we can do now, add some logging later
1552 exit(1);
1556 * Future hook needed for emulated sessions.
1558 * @param exception $exception null means normal termination, $exception received when WS call failed
1560 protected function session_cleanup($exception=null) {
1561 if ($this->authmethod == WEBSERVICE_AUTHMETHOD_USERNAME) {
1562 // nothing needs to be done, there is no persistent session
1563 } else {
1564 // close emulated session if used
1569 * Fetches the function description from database,
1570 * verifies user is allowed to use this function and
1571 * loads all paremeters and return descriptions.
1573 protected function load_function_info() {
1574 global $DB, $USER, $CFG;
1576 if (empty($this->functionname)) {
1577 throw new invalid_parameter_exception('Missing function name');
1580 // function must exist
1581 $function = external_function_info($this->functionname);
1583 if ($this->restricted_serviceid) {
1584 $params = array('sid1'=>$this->restricted_serviceid, 'sid2'=>$this->restricted_serviceid);
1585 $wscond1 = 'AND s.id = :sid1';
1586 $wscond2 = 'AND s.id = :sid2';
1587 } else {
1588 $params = array();
1589 $wscond1 = '';
1590 $wscond2 = '';
1593 // now let's verify access control
1595 // now make sure the function is listed in at least one service user is allowed to use
1596 // allow access only if:
1597 // 1/ entry in the external_services_users table if required
1598 // 2/ validuntil not reached
1599 // 3/ has capability if specified in service desc
1600 // 4/ iprestriction
1602 $sql = "SELECT s.*, NULL AS iprestriction
1603 FROM {external_services} s
1604 JOIN {external_services_functions} sf ON (sf.externalserviceid = s.id AND s.restrictedusers = 0 AND sf.functionname = :name1)
1605 WHERE s.enabled = 1 $wscond1
1607 UNION
1609 SELECT s.*, su.iprestriction
1610 FROM {external_services} s
1611 JOIN {external_services_functions} sf ON (sf.externalserviceid = s.id AND s.restrictedusers = 1 AND sf.functionname = :name2)
1612 JOIN {external_services_users} su ON (su.externalserviceid = s.id AND su.userid = :userid)
1613 WHERE s.enabled = 1 AND (su.validuntil IS NULL OR su.validuntil < :now) $wscond2";
1614 $params = array_merge($params, array('userid'=>$USER->id, 'name1'=>$function->name, 'name2'=>$function->name, 'now'=>time()));
1616 $rs = $DB->get_recordset_sql($sql, $params);
1617 // now make sure user may access at least one service
1618 $remoteaddr = getremoteaddr();
1619 $allowed = false;
1620 foreach ($rs as $service) {
1621 if ($service->requiredcapability and !has_capability($service->requiredcapability, $this->restricted_context)) {
1622 continue; // cap required, sorry
1624 if ($service->iprestriction and !address_in_subnet($remoteaddr, $service->iprestriction)) {
1625 continue; // wrong request source ip, sorry
1627 $allowed = true;
1628 break; // one service is enough, no need to continue
1630 $rs->close();
1631 if (!$allowed) {
1632 throw new webservice_access_exception(
1633 'Access to the function '.$this->functionname.'() is not allowed.
1634 There could be multiple reasons for this:
1635 1. The service linked to the user token does not contain the function.
1636 2. The service is user-restricted and the user is not listed.
1637 3. The service is IP-restricted and the user IP is not listed.
1638 4. The service is time-restricted and the time has expired.
1639 5. The token is time-restricted and the time has expired.
1640 6. The service requires a specific capability which the user does not have.
1641 7. The function is called with username/password (no user token is sent)
1642 and none of the services has the function to allow the user.
1643 These settings can be found in Administration > Site administration
1644 > Plugins > Web services > External services and Manage tokens.');
1647 // we have all we need now
1648 $this->function = $function;
1652 * Execute previously loaded function using parameters parsed from the request data.
1654 protected function execute() {
1655 // validate params, this also sorts the params properly, we need the correct order in the next part
1656 $params = call_user_func(array($this->function->classname, 'validate_parameters'), $this->function->parameters_desc, $this->parameters);
1658 // execute - yay!
1659 $this->returns = call_user_func_array(array($this->function->classname, $this->function->methodname), array_values($params));