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