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