3 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
19 * Web services utility functions and classes
22 * @copyright 2009 Moodle Pty Ltd (http://moodle.com)
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26 require_once($CFG->libdir
.'/externallib.php');
28 define('WEBSERVICE_AUTHMETHOD_USERNAME', 0);
29 define('WEBSERVICE_AUTHMETHOD_PERMANENT_TOKEN', 1);
30 define('WEBSERVICE_AUTHMETHOD_SESSION_TOKEN', 2);
33 * General web service library
38 * Add a user to the list of authorised user of a given service
41 public function add_ws_authorised_user($user) {
43 $user->timecreated
= mktime();
44 $DB->insert_record('external_services_users', $user);
48 * Remove a user from a list of allowed user of a service
50 * @param int $serviceid
52 public function remove_ws_authorised_user($user, $serviceid) {
54 $DB->delete_records('external_services_users',
55 array('externalserviceid' => $serviceid, 'userid' => $user->id
));
59 * Update service allowed user settings
62 public function update_ws_authorised_user($user) {
64 $DB->update_record('external_services_users', $user);
68 * Return list of allowed users with their options (ip/timecreated / validuntil...)
70 * @param int $serviceid
71 * @return array $users
73 public function get_ws_authorised_users($serviceid) {
75 $params = array($CFG->siteguest
, $serviceid);
76 $sql = " SELECT u.id as id, esu.id as serviceuserid, u.email as email, u.firstname as firstname,
77 u.lastname as lastname,
78 esu.iprestriction as iprestriction, esu.validuntil as validuntil,
79 esu.timecreated as timecreated
80 FROM {user} u, {external_services_users} esu
81 WHERE u.id <> ? AND u.deleted = 0 AND u.confirmed = 1
83 AND esu.externalserviceid = ?";
85 $users = $DB->get_records_sql($sql, $params);
90 * Return a authorised user with his options (ip/timecreated / validuntil...)
91 * @param int $serviceid
95 public function get_ws_authorised_user($serviceid, $userid) {
97 $params = array($CFG->siteguest
, $serviceid, $userid);
98 $sql = " SELECT u.id as id, esu.id as serviceuserid, u.email as email, u.firstname as firstname,
99 u.lastname as lastname,
100 esu.iprestriction as iprestriction, esu.validuntil as validuntil,
101 esu.timecreated as timecreated
102 FROM {user} u, {external_services_users} esu
103 WHERE u.id <> ? AND u.deleted = 0 AND u.confirmed = 1
104 AND esu.userid = u.id
105 AND esu.externalserviceid = ?
107 $user = $DB->get_record_sql($sql, $params);
112 * Generate all ws token needed by a user
115 public function generate_user_ws_tokens($userid) {
118 /// generate a token for non admin if web service are enable and the user has the capability to create a token
119 if (!is_siteadmin() && has_capability('moodle/webservice:createtoken', get_context_instance(CONTEXT_SYSTEM
), $userid) && !empty($CFG->enablewebservices
)) {
120 /// for every service than the user is authorised on, create a token (if it doesn't already exist)
122 ///get all services which are set to all user (no restricted to specific users)
123 $norestrictedservices = $DB->get_records('external_services', array('restrictedusers' => 0));
124 $serviceidlist = array();
125 foreach ($norestrictedservices as $service) {
126 $serviceidlist[] = $service->id
;
129 //get all services which are set to the current user (the current user is specified in the restricted user list)
130 $servicesusers = $DB->get_records('external_services_users', array('userid' => $userid));
131 foreach ($servicesusers as $serviceuser) {
132 if (!in_array($serviceuser->externalserviceid
,$serviceidlist)) {
133 $serviceidlist[] = $serviceuser->externalserviceid
;
137 //get all services which already have a token set for the current user
138 $usertokens = $DB->get_records('external_tokens', array('userid' => $userid, 'tokentype' => EXTERNAL_TOKEN_PERMANENT
));
139 $tokenizedservice = array();
140 foreach ($usertokens as $token) {
141 $tokenizedservice[] = $token->externalserviceid
;
144 //create a token for the service which have no token already
145 foreach ($serviceidlist as $serviceid) {
146 if (!in_array($serviceid, $tokenizedservice)) {
147 //create the token for this service
148 $newtoken = new stdClass();
149 $newtoken->token
= md5(uniqid(rand(),1));
150 //check that the user has capability on this service
151 $newtoken->tokentype
= EXTERNAL_TOKEN_PERMANENT
;
152 $newtoken->userid
= $userid;
153 $newtoken->externalserviceid
= $serviceid;
154 //TODO: find a way to get the context - UPDATE FOLLOWING LINE
155 $newtoken->contextid
= get_context_instance(CONTEXT_SYSTEM
)->id
;
156 $newtoken->creatorid
= $userid;
157 $newtoken->timecreated
= time();
159 $DB->insert_record('external_tokens', $newtoken);
168 * Return all ws user token with ws enabled/disabled and ws restricted users mode.
169 * @param integer $userid
170 * @return array of token
172 public function get_user_ws_tokens($userid) {
174 //here retrieve token list (including linked users firstname/lastname and linked services name)
176 t.id, t.creatorid, t.token, u.firstname, u.lastname, s.id as wsid, s.name, s.enabled, s.restrictedusers, t.validuntil
178 {external_tokens} t, {user} u, {external_services} s
180 t.userid=? AND t.tokentype = ".EXTERNAL_TOKEN_PERMANENT
." AND s.id = t.externalserviceid AND t.userid = u.id";
181 $tokens = $DB->get_records_sql($sql, array( $userid));
186 * Return a user token that has been created by the user
187 * If doesn't exist a exception is thrown
188 * @param integer $userid
189 * @param integer $tokenid
190 * @return object token
193 * ->firstname user firstname
195 * ->name service name
197 public function get_created_by_user_ws_token($userid, $tokenid) {
200 t.id, t.token, u.firstname, u.lastname, s.name
202 {external_tokens} t, {user} u, {external_services} s
204 t.creatorid=? AND t.id=? AND t.tokentype = "
205 . EXTERNAL_TOKEN_PERMANENT
206 . " AND s.id = t.externalserviceid AND t.userid = u.id";
207 //must be the token creator
208 $token = $DB->get_record_sql($sql, array($userid, $tokenid), MUST_EXIST
);
213 * Return a token for a given id
214 * @param integer $tokenid
215 * @return object token
217 public function get_token_by_id($tokenid) {
219 return $DB->get_record('external_tokens', array('id' => $tokenid));
223 * Delete a user token
224 * @param int $tokenid
226 public function delete_user_ws_token($tokenid) {
228 $DB->delete_records('external_tokens', array('id'=>$tokenid));
232 * Delete a service - it also delete the functions and users references to this service
233 * @param int $serviceid
235 public function delete_service($serviceid) {
237 $DB->delete_records('external_services_users', array('externalserviceid' => $serviceid));
238 $DB->delete_records('external_services_functions', array('externalserviceid' => $serviceid));
239 $DB->delete_records('external_tokens', array('externalserviceid' => $serviceid));
240 $DB->delete_records('external_services', array('id' => $serviceid));
244 * Get a user token by token
245 * @param string $token
246 * @throws moodle_exception if there is multiple result
248 public function get_user_ws_token($token) {
250 return $DB->get_record('external_tokens', array('token'=>$token), '*', MUST_EXIST
);
254 * Get the list of all functions for given service ids
255 * @param array $serviceids
256 * @return array functions
258 public function get_external_functions($serviceids) {
260 if (!empty($serviceids)) {
261 list($serviceids, $params) = $DB->get_in_or_equal($serviceids);
263 FROM {external_functions} f
264 WHERE f.name IN (SELECT sf.functionname
265 FROM {external_services_functions} sf
266 WHERE sf.externalserviceid $serviceids)";
267 $functions = $DB->get_records_sql($sql, $params);
269 $functions = array();
275 * Get the list of all functions not in the given service id
276 * @param int $serviceid
277 * @return array functions
279 public function get_not_associated_external_functions($serviceid) {
281 $select = "name NOT IN (SELECT s.functionname
282 FROM {external_services_functions} s
283 WHERE s.externalserviceid = :sid
286 $functions = $DB->get_records_select('external_functions',
287 $select, array('sid' => $serviceid), 'name');
293 * Get list of required capabilities of a service, sorted by functions
294 * @param integer $serviceid
296 * example of return value:
299 * [moodle_group_create_groups] => Array
301 * [0] => moodle/course:managegroups
304 * [moodle_enrol_get_enrolled_users] => Array
306 * [0] => moodle/site:viewparticipants
307 * [1] => moodle/course:viewparticipants
308 * [2] => moodle/role:review
309 * [3] => moodle/site:accessallgroups
310 * [4] => moodle/course:enrolreview
314 public function get_service_required_capabilities($serviceid) {
315 $functions = $this->get_external_functions(array($serviceid));
316 $requiredusercaps = array();
317 foreach ($functions as $function) {
318 $functioncaps = explode(',', $function->capabilities
);
319 if (!empty($functioncaps) and !empty($functioncaps[0])) {
320 foreach ($functioncaps as $functioncap) {
321 $requiredusercaps[$function->name
][] = trim($functioncap);
325 return $requiredusercaps;
329 * Get user capabilities (with context)
330 * Only usefull for documentation purpose
331 * @param integer $userid
334 public function get_user_capabilities($userid) {
336 //retrieve the user capabilities
337 $sql = "SELECT rc.id, rc.capability FROM {role_capabilities} rc, {role_assignments} ra
338 WHERE rc.roleid=ra.roleid AND ra.userid= ?";
339 $dbusercaps = $DB->get_records_sql($sql, array($userid));
341 foreach ($dbusercaps as $usercap) {
342 $usercaps[$usercap->capability
] = true;
348 * Get users missing capabilities for a given service
349 * @param array $users
350 * @param integer $serviceid
351 * @return array of missing capabilities, the key being the user id
353 public function get_missing_capabilities_by_users($users, $serviceid) {
355 $usersmissingcaps = array();
357 //retrieve capabilities required by the service
358 $servicecaps = $this->get_service_required_capabilities($serviceid);
360 //retrieve users missing capabilities
361 foreach ($users as $user) {
362 //cast user array into object to be a bit more flexible
363 if (is_array($user)) {
364 $user = (object) $user;
366 $usercaps = $this->get_user_capabilities($user->id
);
368 //detect the missing capabilities
369 foreach ($servicecaps as $functioname => $functioncaps) {
370 foreach ($functioncaps as $functioncap) {
371 if (!key_exists($functioncap, $usercaps)) {
372 if (!isset($usersmissingcaps[$user->id
])
373 or array_search($functioncap, $usersmissingcaps[$user->id
]) === false) {
374 $usersmissingcaps[$user->id
][] = $functioncap;
381 return $usersmissingcaps;
385 * Get a external service for a given id
386 * @param service id $serviceid
387 * @param integer $strictness IGNORE_MISSING, MUST_EXIST...
388 * @return object external service
390 public function get_external_service_by_id($serviceid, $strictness=IGNORE_MISSING
) {
392 $service = $DB->get_record('external_services',
393 array('id' => $serviceid), '*', $strictness);
398 * Get a external function for a given id
399 * @param function id $functionid
400 * @param integer $strictness IGNORE_MISSING, MUST_EXIST...
401 * @return object external function
403 public function get_external_function_by_id($functionid, $strictness=IGNORE_MISSING
) {
405 $function = $DB->get_record('external_functions',
406 array('id' => $functionid), '*', $strictness);
411 * Add a function to a service
412 * @param string $functionname
413 * @param integer $serviceid
415 public function add_external_function_to_service($functionname, $serviceid) {
417 $addedfunction = new stdClass();
418 $addedfunction->externalserviceid
= $serviceid;
419 $addedfunction->functionname
= $functionname;
420 $DB->insert_record('external_services_functions', $addedfunction);
425 * @param object $service
426 * @return serviceid integer
428 public function add_external_service($service) {
430 $service->timecreated
= mktime();
431 $serviceid = $DB->insert_record('external_services', $service);
437 * @param object $service
439 public function update_external_service($service) {
441 $service->timemodified
= mktime();
442 $DB->update_record('external_services', $service);
446 * Test whether a external function is already linked to a service
447 * @param string $functionname
448 * @param integer $serviceid
449 * @return bool true if a matching function exists for the service, else false.
450 * @throws dml_exception if error
452 public function service_function_exists($functionname, $serviceid) {
454 return $DB->record_exists('external_services_functions',
455 array('externalserviceid' => $serviceid,
456 'functionname' => $functionname));
459 public function remove_external_function_from_service($functionname, $serviceid) {
461 $DB->delete_records('external_services_functions',
462 array('externalserviceid' => $serviceid, 'functionname' => $functionname));
470 * Exception indicating access control problem in web service call
471 * @author Petr Skoda (skodak)
473 class webservice_access_exception
extends moodle_exception
{
477 function __construct($debuginfo) {
478 parent
::__construct('accessexception', 'webservice', '', null, $debuginfo);
483 * Is protocol enabled?
484 * @param string $protocol name of WS protocol
487 function webservice_protocol_is_enabled($protocol) {
490 if (empty($CFG->enablewebservices
)) {
494 $active = explode(',', $CFG->webserviceprotocols
);
496 return(in_array($protocol, $active));
502 * Mandatory interface for all test client classes.
503 * @author Petr Skoda (skodak)
505 interface webservice_test_client_interface
{
507 * Execute test client WS request
508 * @param string $serverurl
509 * @param string $function
510 * @param array $params
513 public function simpletest($serverurl, $function, $params);
517 * Mandatory interface for all web service protocol classes
518 * @author Petr Skoda (skodak)
520 interface webservice_server_interface
{
522 * Process request from client.
525 public function run();
529 * Abstract web service base class.
530 * @author Petr Skoda (skodak)
532 abstract class webservice_server
implements webservice_server_interface
{
534 /** @property string $wsname name of the web server plugin */
535 protected $wsname = null;
537 /** @property string $username name of local user */
538 protected $username = null;
540 /** @property string $password password of the local user */
541 protected $password = null;
543 /** @property int $userid the local user */
544 protected $userid = null;
546 /** @property integer $authmethod authentication method one of WEBSERVICE_AUTHMETHOD_* */
547 protected $authmethod;
549 /** @property string $token authentication token*/
550 protected $token = null;
552 /** @property object restricted context */
553 protected $restricted_context;
555 /** @property int restrict call to one service id*/
556 protected $restricted_serviceid = null;
560 * @param integer $authmethod authentication method one of WEBSERVICE_AUTHMETHOD_*
562 public function __construct($authmethod) {
563 $this->authmethod
= $authmethod;
568 * Authenticate user using username+password or token.
569 * This function sets up $USER global.
570 * It is safe to use has_capability() after this.
571 * This method also verifies user is allowed to use this
575 protected function authenticate_user() {
578 if (!NO_MOODLE_COOKIES
) {
579 throw new coding_exception('Cookies must be disabled in WS servers!');
582 if ($this->authmethod
== WEBSERVICE_AUTHMETHOD_USERNAME
) {
584 //we check that authentication plugin is enabled
585 //it is only required by simple authentication
586 if (!is_enabled_auth('webservice')) {
587 throw new webservice_access_exception(get_string('wsauthnotenabled', 'webservice'));
590 if (!$auth = get_auth_plugin('webservice')) {
591 throw new webservice_access_exception(get_string('wsauthmissing', 'webservice'));
594 $this->restricted_context
= get_context_instance(CONTEXT_SYSTEM
);
596 if (!$this->username
) {
597 throw new webservice_access_exception(get_string('missingusername', 'webservice'));
600 if (!$this->password
) {
601 throw new webservice_access_exception(get_string('missingpassword', 'webservice'));
604 if (!$auth->user_login_webservice($this->username
, $this->password
)) {
605 // log failed login attempts
606 add_to_log(SITEID
, 'webservice', get_string('simpleauthlog', 'webservice'), '' , get_string('failedtolog', 'webservice').": ".$this->username
."/".$this->password
." - ".getremoteaddr() , 0);
607 throw new webservice_access_exception(get_string('wrongusernamepassword', 'webservice'));
610 $user = $DB->get_record('user', array('username'=>$this->username
, 'mnethostid'=>$CFG->mnet_localhost_id
), '*', MUST_EXIST
);
612 } else if ($this->authmethod
== WEBSERVICE_AUTHMETHOD_PERMANENT_TOKEN
){
613 $user = $this->authenticate_by_token(EXTERNAL_TOKEN_PERMANENT
);
615 $user = $this->authenticate_by_token(EXTERNAL_TOKEN_EMBEDDED
);
618 //Non admin can not authenticate if maintenance mode
619 $hassiteconfig = has_capability('moodle/site:config', get_context_instance(CONTEXT_SYSTEM
), $user);
620 if (!empty($CFG->maintenance_enabled
) and !$hassiteconfig) {
621 throw new webservice_access_exception(get_string('sitemaintenance', 'admin'));
624 //only confirmed user should be able to call web service
625 if (!empty($user->deleted
)) {
626 add_to_log(SITEID
, '', '', '', get_string('wsaccessuserdeleted', 'webservice', $user->username
) . " - ".getremoteaddr(), 0, $user->id
);
627 throw new webservice_access_exception(get_string('wsaccessuserdeleted', 'webservice', $user->username
));
630 //only confirmed user should be able to call web service
631 if (empty($user->confirmed
)) {
632 add_to_log(SITEID
, '', '', '', get_string('wsaccessuserunconfirmed', 'webservice', $user->username
) . " - ".getremoteaddr(), 0, $user->id
);
633 throw new webservice_access_exception(get_string('wsaccessuserunconfirmed', 'webservice', $user->username
));
636 //check the user is suspended
637 if (!empty($user->suspended
)) {
638 add_to_log(SITEID
, '', '', '', get_string('wsaccessusersuspended', 'webservice', $user->username
) . " - ".getremoteaddr(), 0, $user->id
);
639 throw new webservice_access_exception(get_string('wsaccessusersuspended', 'webservice', $user->username
));
642 //retrieve the authentication plugin if no previously done
644 $auth = get_auth_plugin($user->auth
);
647 // check if credentials have expired
648 if (!empty($auth->config
->expiration
) and $auth->config
->expiration
== 1) {
649 $days2expire = $auth->password_expire($user->username
);
650 if (intval($days2expire) < 0 ) {
651 add_to_log(SITEID
, '', '', '', get_string('wsaccessuserexpired', 'webservice', $user->username
) . " - ".getremoteaddr(), 0, $user->id
);
652 throw new webservice_access_exception(get_string('wsaccessuserexpired', 'webservice', $user->username
));
656 //check if the auth method is nologin (in this case refuse connection)
657 if ($user->auth
=='nologin') {
658 add_to_log(SITEID
, '', '', '', get_string('wsaccessusernologin', 'webservice', $user->username
) . " - ".getremoteaddr(), 0, $user->id
);
659 throw new webservice_access_exception(get_string('wsaccessusernologin', 'webservice', $user->username
));
662 // now fake user login, the session is completely empty too
663 session_set_user($user);
664 $this->userid
= $user->id
;
666 if ($this->authmethod
!= WEBSERVICE_AUTHMETHOD_SESSION_TOKEN
&& !has_capability("webservice/$this->wsname:use", $this->restricted_context
)) {
667 throw new webservice_access_exception(get_string('accessnotallowed', 'webservice'));
670 external_api
::set_context_restriction($this->restricted_context
);
673 protected function authenticate_by_token($tokentype){
675 if (!$token = $DB->get_record('external_tokens', array('token'=>$this->token
, 'tokentype'=>$tokentype))) {
676 // log failed login attempts
677 add_to_log(SITEID
, 'webservice', get_string('tokenauthlog', 'webservice'), '' , get_string('failedtolog', 'webservice').": ".$this->token
. " - ".getremoteaddr() , 0);
678 throw new webservice_access_exception(get_string('invalidtoken', 'webservice'));
681 if ($token->validuntil
and $token->validuntil
< time()) {
682 $DB->delete_records('external_tokens', array('token'=>$this->token
, 'tokentype'=>$tokentype));
683 throw new webservice_access_exception(get_string('invalidtimedtoken', 'webservice'));
686 if ($token->sid
){//assumes that if sid is set then there must be a valid associated session no matter the token type
687 $session = session_get_instance();
688 if (!$session->session_exists($token->sid
)){
689 $DB->delete_records('external_tokens', array('sid'=>$token->sid
));
690 throw new webservice_access_exception(get_string('invalidtokensession', 'webservice'));
694 if ($token->iprestriction
and !address_in_subnet(getremoteaddr(), $token->iprestriction
)) {
695 add_to_log(SITEID
, 'webservice', get_string('tokenauthlog', 'webservice'), '' , get_string('failedtolog', 'webservice').": ".getremoteaddr() , 0);
696 throw new webservice_access_exception(get_string('invalidiptoken', 'webservice'));
699 $this->restricted_context
= get_context_instance_by_id($token->contextid
);
700 $this->restricted_serviceid
= $token->externalserviceid
;
702 $user = $DB->get_record('user', array('id'=>$token->userid
), '*', MUST_EXIST
);
705 $DB->set_field('external_tokens', 'lastaccess', time(), array('id'=>$token->id
));
713 * Special abstraction of our srvices that allows
714 * interaction with stock Zend ws servers.
715 * @author Petr Skoda (skodak)
717 abstract class webservice_zend_server
extends webservice_server
{
719 /** @property string name of the zend server class : Zend_XmlRpc_Server, Zend_Soap_Server, Zend_Soap_AutoDiscover, ...*/
720 protected $zend_class;
722 /** @property object Zend server instance */
723 protected $zend_server;
725 /** @property string $service_class virtual web service class with all functions user name execute, created on the fly */
726 protected $service_class;
730 * @param integer $authmethod authentication method - one of WEBSERVICE_AUTHMETHOD_*
732 public function __construct($authmethod, $zend_class) {
733 parent
::__construct($authmethod);
734 $this->zend_class
= $zend_class;
738 * Process request from client.
739 * @param bool $simple use simple authentication
742 public function run() {
743 // we will probably need a lot of memory in some functions
744 raise_memory_limit(MEMORY_EXTRA
);
746 // set some longer timeout, this script is not sending any output,
747 // this means we need to manually extend the timeout operations
748 // that need longer time to finish
749 external_api
::set_timeout();
751 // now create the instance of zend server
752 $this->init_zend_server();
754 // set up exception handler first, we want to sent them back in correct format that
755 // the other system understands
756 // we do not need to call the original default handler because this ws handler does everything
757 set_exception_handler(array($this, 'exception_handler'));
759 // init all properties from the request data
760 $this->parse_request();
762 // this sets up $USER and $SESSION and context restrictions
763 $this->authenticate_user();
765 // make a list of all functions user is allowed to excecute
766 $this->init_service_class();
768 // tell server what functions are available
769 $this->zend_server
->setClass($this->service_class
);
771 //log the web service request
772 add_to_log(SITEID
, 'webservice', '', '' , $this->zend_class
." ".getremoteaddr() , 0, $this->userid
);
775 $this->send_headers();
777 // execute and return response, this sends some headers too
778 $response = $this->zend_server
->handle();
781 $this->session_cleanup();
783 //finally send the result
789 * Load virtual class needed for Zend api
792 protected function init_service_class() {
795 // first ofall get a complete list of services user is allowed to access
797 if ($this->restricted_serviceid
) {
798 $params = array('sid1'=>$this->restricted_serviceid
, 'sid2'=>$this->restricted_serviceid
);
799 $wscond1 = 'AND s.id = :sid1';
800 $wscond2 = 'AND s.id = :sid2';
807 // now make sure the function is listed in at least one service user is allowed to use
808 // allow access only if:
809 // 1/ entry in the external_services_users table if required
810 // 2/ validuntil not reached
811 // 3/ has capability if specified in service desc
814 $sql = "SELECT s.*, NULL AS iprestriction
815 FROM {external_services} s
816 JOIN {external_services_functions} sf ON (sf.externalserviceid = s.id AND s.restrictedusers = 0)
817 WHERE s.enabled = 1 $wscond1
821 SELECT s.*, su.iprestriction
822 FROM {external_services} s
823 JOIN {external_services_functions} sf ON (sf.externalserviceid = s.id AND s.restrictedusers = 1)
824 JOIN {external_services_users} su ON (su.externalserviceid = s.id AND su.userid = :userid)
825 WHERE s.enabled = 1 AND su.validuntil IS NULL OR su.validuntil < :now $wscond2";
827 $params = array_merge($params, array('userid'=>$USER->id
, 'now'=>time()));
829 $serviceids = array();
830 $rs = $DB->get_recordset_sql($sql, $params);
832 // now make sure user may access at least one service
833 $remoteaddr = getremoteaddr();
835 foreach ($rs as $service) {
836 if (isset($serviceids[$service->id
])) {
839 if ($service->requiredcapability
and !has_capability($service->requiredcapability
, $this->restricted_context
)) {
840 continue; // cap required, sorry
842 if ($service->iprestriction
and !address_in_subnet($remoteaddr, $service->iprestriction
)) {
843 continue; // wrong request source ip, sorry
845 $serviceids[$service->id
] = $service->id
;
849 // now get the list of all functions
850 $wsmanager = new webservice();
851 $functions = $wsmanager->get_external_functions($serviceids);
853 // now make the virtual WS class with all the fuctions for this particular user
855 foreach ($functions as $function) {
856 $methods .= $this->get_virtual_method_code($function);
859 // let's use unique class name, there might be problem in unit tests
860 $classname = 'webservices_virtual_class_000000';
861 while(class_exists($classname)) {
867 * Virtual class web services for user id '.$USER->id
.' in context '.$this->restricted_context
->id
.'.
869 class '.$classname.' {
874 // load the virtual class definition into memory
876 $this->service_class
= $classname;
880 * returns virtual method code
881 * @param object $function
882 * @return string PHP code
884 protected function get_virtual_method_code($function) {
887 $function = external_function_info($function);
889 //arguments in function declaration line with defaults.
890 $paramanddefaults = array();
891 //arguments used as parameters for external lib call.
893 $params_desc = array();
894 foreach ($function->parameters_desc
->keys
as $name=>$keydesc) {
896 $paramanddefault = $param;
897 //need to generate the default if there is any
898 if ($keydesc instanceof external_value
) {
899 if ($keydesc->required
== VALUE_DEFAULT
) {
900 if ($keydesc->default===null) {
901 $paramanddefault .= '=null';
903 switch($keydesc->type
) {
905 $paramanddefault .= '='.$keydesc->default; break;
907 $paramanddefault .= '='.$keydesc->default; break;
909 $paramanddefault .= '='.$keydesc->default; break;
911 $paramanddefault .= '=\''.$keydesc->default.'\'';
914 } else if ($keydesc->required
== VALUE_OPTIONAL
) {
915 //it does make sens to declare a parameter VALUE_OPTIONAL
916 //VALUE_OPTIONAL is used only for array/object key
917 throw new moodle_exception('parametercannotbevalueoptional');
919 } else { //for the moment we do not support default for other structure types
920 if ($keydesc->required
== VALUE_DEFAULT
) {
921 //accept empty array as default
922 if (isset($keydesc->default) and is_array($keydesc->default)
923 and empty($keydesc->default)) {
924 $paramanddefault .= '=array()';
926 throw new moodle_exception('errornotemptydefaultparamarray', 'webservice', '', $name);
929 if ($keydesc->required
== VALUE_OPTIONAL
) {
930 throw new moodle_exception('erroroptionalparamarray', 'webservice', '', $name);
934 $paramanddefaults[] = $paramanddefault;
936 if ($keydesc instanceof external_value
) {
937 switch($keydesc->type
) {
938 case PARAM_BOOL
: // 0 or 1 only for now
940 $type = 'int'; break;
942 $type = 'double'; break;
946 } else if ($keydesc instanceof external_single_structure
) {
947 $type = 'object|struct';
948 } else if ($keydesc instanceof external_multiple_structure
) {
951 $params_desc[] = ' * @param '.$type.' $'.$name.' '.$keydesc->desc
;
953 $params = implode(', ', $params);
954 $paramanddefaults = implode(', ', $paramanddefaults);
955 $params_desc = implode("\n", $params_desc);
957 $serviceclassmethodbody = $this->service_class_method_body($function, $params);
959 if (is_null($function->returns_desc
)) {
960 $return = ' * @return void';
963 if ($function->returns_desc
instanceof external_value
) {
964 switch($function->returns_desc
->type
) {
965 case PARAM_BOOL
: // 0 or 1 only for now
967 $type = 'int'; break;
969 $type = 'double'; break;
973 } else if ($function->returns_desc
instanceof external_single_structure
) {
974 $type = 'object|struct'; //only 'object' is supported by SOAP, 'struct' by XML-RPC MDL-23083
975 } else if ($function->returns_desc
instanceof external_multiple_structure
) {
978 $return = ' * @return '.$type.' '.$function->returns_desc
->desc
;
981 // now crate the virtual method that calls the ext implementation
985 * '.$function->description
.'
990 public function '.$function->name
.'('.$paramanddefaults.') {
991 '.$serviceclassmethodbody.'
998 * You can override this function in your child class to add extra code into the dynamically
999 * created service class. For example it is used in the amf server to cast types of parameters and to
1000 * cast the return value to the types as specified in the return value description.
1001 * @param stdClass $function
1002 * @param array $params
1003 * @return string body of the method for $function ie. everything within the {} of the method declaration.
1005 protected function service_class_method_body($function, $params){
1006 //cast the param from object to array (validate_parameters except array only)
1009 $paramstocast = explode(',', $params);
1010 foreach ($paramstocast as $paramtocast) {
1011 //clean the parameter from any white space
1012 $paramtocast = trim($paramtocast);
1013 $castingcode .= $paramtocast .
1014 '=webservice_zend_server::cast_objects_to_array('.$paramtocast.');';
1019 $descriptionmethod = $function->methodname
.'_returns()';
1020 $callforreturnvaluedesc = $function->classname
.'::'.$descriptionmethod;
1021 return $castingcode . ' if ('.$callforreturnvaluedesc.' == null) {'.
1022 $function->classname
.'::'.$function->methodname
.'('.$params.');
1025 return external_api::clean_returnvalue('.$callforreturnvaluedesc.', '.$function->classname
.'::'.$function->methodname
.'('.$params.'));';
1029 * Recursive function to recurse down into a complex variable and convert all
1030 * objects to arrays.
1031 * @param mixed $param value to cast
1032 * @return mixed Cast value
1034 public static function cast_objects_to_array($param){
1035 if (is_object($param)){
1036 $param = (array)$param;
1038 if (is_array($param)){
1039 $toreturn = array();
1040 foreach ($param as $key=> $param){
1041 $toreturn[$key] = self
::cast_objects_to_array($param);
1050 * Set up zend service class
1053 protected function init_zend_server() {
1054 $this->zend_server
= new $this->zend_class();
1058 * This method parses the $_REQUEST superglobal and looks for
1059 * the following information:
1060 * 1/ user authentication - username+password or token (wsusername, wspassword and wstoken parameters)
1064 protected function parse_request() {
1065 if ($this->authmethod
== WEBSERVICE_AUTHMETHOD_USERNAME
) {
1066 //note: some clients have problems with entity encoding :-(
1067 if (isset($_REQUEST['wsusername'])) {
1068 $this->username
= $_REQUEST['wsusername'];
1070 if (isset($_REQUEST['wspassword'])) {
1071 $this->password
= $_REQUEST['wspassword'];
1074 if (isset($_REQUEST['wstoken'])) {
1075 $this->token
= $_REQUEST['wstoken'];
1081 * Internal implementation - sending of page headers.
1084 protected function send_headers() {
1085 header('Cache-Control: private, must-revalidate, pre-check=0, post-check=0, max-age=0');
1086 header('Expires: '. gmdate('D, d M Y H:i:s', 0) .' GMT');
1087 header('Pragma: no-cache');
1088 header('Accept-Ranges: none');
1092 * Specialised exception handler, we can not use the standard one because
1093 * it can not just print html to output.
1095 * @param exception $ex
1096 * @return void does not return
1098 public function exception_handler($ex) {
1099 // detect active db transactions, rollback and log as error
1100 abort_all_db_transactions();
1102 // some hacks might need a cleanup hook
1103 $this->session_cleanup($ex);
1105 // now let the plugin send the exception to client
1106 $this->send_error($ex);
1108 // not much else we can do now, add some logging later
1113 * Send the error information to the WS client
1114 * formatted as XML document.
1115 * @param exception $ex
1118 protected function send_error($ex=null) {
1119 $this->send_headers();
1120 echo $this->zend_server
->fault($ex);
1124 * Future hook needed for emulated sessions.
1125 * @param exception $exception null means normal termination, $exception received when WS call failed
1128 protected function session_cleanup($exception=null) {
1129 if ($this->authmethod
== WEBSERVICE_AUTHMETHOD_USERNAME
) {
1130 // nothing needs to be done, there is no persistent session
1132 // close emulated session if used
1139 * Web Service server base class, this class handles both
1140 * simple and token authentication.
1141 * @author Petr Skoda (skodak)
1143 abstract class webservice_base_server
extends webservice_server
{
1145 /** @property array $parameters the function parameters - the real values submitted in the request */
1146 protected $parameters = null;
1148 /** @property string $functionname the name of the function that is executed */
1149 protected $functionname = null;
1151 /** @property object $function full function description */
1152 protected $function = null;
1154 /** @property mixed $returns function return value */
1155 protected $returns = null;
1158 * This method parses the request input, it needs to get:
1159 * 1/ user authentication - username+password or token
1161 * 3/ function parameters
1165 abstract protected function parse_request();
1168 * Send the result of function call to the WS client.
1171 abstract protected function send_response();
1174 * Send the error information to the WS client.
1175 * @param exception $ex
1178 abstract protected function send_error($ex=null);
1181 * Process request from client.
1184 public function run() {
1185 // we will probably need a lot of memory in some functions
1186 raise_memory_limit(MEMORY_EXTRA
);
1188 // set some longer timeout, this script is not sending any output,
1189 // this means we need to manually extend the timeout operations
1190 // that need longer time to finish
1191 external_api
::set_timeout();
1193 // set up exception handler first, we want to sent them back in correct format that
1194 // the other system understands
1195 // we do not need to call the original default handler because this ws handler does everything
1196 set_exception_handler(array($this, 'exception_handler'));
1198 // init all properties from the request data
1199 $this->parse_request();
1201 // authenticate user, this has to be done after the request parsing
1202 // this also sets up $USER and $SESSION
1203 $this->authenticate_user();
1205 // find all needed function info and make sure user may actually execute the function
1206 $this->load_function_info();
1208 //log the web service request
1209 add_to_log(SITEID
, 'webservice', $this->functionname
, '' , getremoteaddr() , 0, $this->userid
);
1211 // finally, execute the function - any errors are catched by the default exception handler
1214 // send the results back in correct format
1215 $this->send_response();
1218 $this->session_cleanup();
1224 * Specialised exception handler, we can not use the standard one because
1225 * it can not just print html to output.
1227 * @param exception $ex
1228 * @return void does not return
1230 public function exception_handler($ex) {
1231 // detect active db transactions, rollback and log as error
1232 abort_all_db_transactions();
1234 // some hacks might need a cleanup hook
1235 $this->session_cleanup($ex);
1237 // now let the plugin send the exception to client
1238 $this->send_error($ex);
1240 // not much else we can do now, add some logging later
1245 * Future hook needed for emulated sessions.
1246 * @param exception $exception null means normal termination, $exception received when WS call failed
1249 protected function session_cleanup($exception=null) {
1250 if ($this->authmethod
== WEBSERVICE_AUTHMETHOD_USERNAME
) {
1251 // nothing needs to be done, there is no persistent session
1253 // close emulated session if used
1258 * Fetches the function description from database,
1259 * verifies user is allowed to use this function and
1260 * loads all paremeters and return descriptions.
1263 protected function load_function_info() {
1264 global $DB, $USER, $CFG;
1266 if (empty($this->functionname
)) {
1267 throw new invalid_parameter_exception('Missing function name');
1270 // function must exist
1271 $function = external_function_info($this->functionname
);
1273 if ($this->restricted_serviceid
) {
1274 $params = array('sid1'=>$this->restricted_serviceid
, 'sid2'=>$this->restricted_serviceid
);
1275 $wscond1 = 'AND s.id = :sid1';
1276 $wscond2 = 'AND s.id = :sid2';
1283 // now let's verify access control
1285 // now make sure the function is listed in at least one service user is allowed to use
1286 // allow access only if:
1287 // 1/ entry in the external_services_users table if required
1288 // 2/ validuntil not reached
1289 // 3/ has capability if specified in service desc
1292 $sql = "SELECT s.*, NULL AS iprestriction
1293 FROM {external_services} s
1294 JOIN {external_services_functions} sf ON (sf.externalserviceid = s.id AND s.restrictedusers = 0 AND sf.functionname = :name1)
1295 WHERE s.enabled = 1 $wscond1
1299 SELECT s.*, su.iprestriction
1300 FROM {external_services} s
1301 JOIN {external_services_functions} sf ON (sf.externalserviceid = s.id AND s.restrictedusers = 1 AND sf.functionname = :name2)
1302 JOIN {external_services_users} su ON (su.externalserviceid = s.id AND su.userid = :userid)
1303 WHERE s.enabled = 1 AND su.validuntil IS NULL OR su.validuntil < :now $wscond2";
1304 $params = array_merge($params, array('userid'=>$USER->id
, 'name1'=>$function->name
, 'name2'=>$function->name
, 'now'=>time()));
1306 $rs = $DB->get_recordset_sql($sql, $params);
1307 // now make sure user may access at least one service
1308 $remoteaddr = getremoteaddr();
1310 foreach ($rs as $service) {
1311 if ($service->requiredcapability
and !has_capability($service->requiredcapability
, $this->restricted_context
)) {
1312 continue; // cap required, sorry
1314 if ($service->iprestriction
and !address_in_subnet($remoteaddr, $service->iprestriction
)) {
1315 continue; // wrong request source ip, sorry
1318 break; // one service is enough, no need to continue
1322 throw new webservice_access_exception('Access to external function not allowed');
1325 // we have all we need now
1326 $this->function = $function;
1330 * Execute previously loaded function using parameters parsed from the request data.
1333 protected function execute() {
1334 // validate params, this also sorts the params properly, we need the correct order in the next part
1335 $params = call_user_func(array($this->function->classname
, 'validate_parameters'), $this->function->parameters_desc
, $this->parameters
);
1338 $this->returns
= call_user_func_array(array($this->function->classname
, $this->function->methodname
), array_values($params));