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