Fixed passing 'itemid' param in case of no item
[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
5593d2dc 28/**
29 * Exception indicating access control problem in web service call
30 */
31class webservice_access_exception extends moodle_exception {
32 /**
33 * Constructor
34 */
35 function __construct($debuginfo) {
e8b21670 36 parent::__construct('accessexception', 'webservice', '', null, $debuginfo);
5593d2dc 37 }
38}
39
f0dafb3c 40/**
41 * Is protocol enabled?
42 * @param string $protocol name of WS protocol
43 * @return bool
44 */
cc93c7da 45function webservice_protocol_is_enabled($protocol) {
46 global $CFG;
893d7f0f 47
cc93c7da 48 if (empty($CFG->enablewebservices)) {
49 return false;
893d7f0f 50 }
51
cc93c7da 52 $active = explode(',', $CFG->webserviceprotocols);
893d7f0f 53
cc93c7da 54 return(in_array($protocol, $active));
55}
893d7f0f 56
cc93c7da 57/**
58 * Mandatory web service server interface
59 * @author Petr Skoda (skodak)
60 */
61interface webservice_server {
62 /**
63 * Process request from client.
64 * @param bool $simple use simple authentication
65 * @return void
66 */
67 public function run($simple);
893d7f0f 68}
69
f0dafb3c 70/**
71 * Mandatory test client interface.
72 */
73interface webservice_test_client_interface {
74 /**
75 * Execute test client WS request
76 * @param string $serverurl
77 * @param string $function
78 * @param array $params
79 * @return mixed
80 */
81 public function simpletest($serverurl, $function, $params);
82}
83
06e7fadc 84/**
cc93c7da 85 * Special abstraction of our srvices that allows
86 * interaction with stock Zend ws servers.
87 * @author skodak
06e7fadc 88 */
cc93c7da 89abstract class webservice_zend_server implements webservice_server {
88098133 90
91 /** @property string name of the zend server class */
92 protected $zend_class;
93
94 /** @property object Zend server instance */
95 protected $zend_server;
96
97 /** @property string $wsname name of the web server plugin */
98 protected $wsname = null;
99
100 /** @property bool $simple true if simple auth used */
101 protected $simple;
102
103 /** @property string $service_class virtual web service class with all functions user name execute, created on the fly */
104 protected $service_class;
105
106 /** @property object restricted context */
107 protected $restricted_context;
108
109 /**
110 * Contructor
111 */
112 public function __construct($zend_class) {
113 $this->zend_class = $zend_class;
114 }
115
116 /**
117 * Process request from client.
118 * @param bool $simple use simple authentication
119 * @return void
120 */
121 public function run($simple) {
122 $this->simple = $simple;
123
124 // we will probably need a lot of memory in some functions
125 @raise_memory_limit('128M');
126
127 // set some longer timeout, this script is not sending any output,
128 // this means we need to manually extend the timeout operations
129 // that need longer time to finish
130 external_api::set_timeout();
131
e8b21670 132 // now create the instance of zend server
133 $this->init_zend_server();
134
88098133 135 // set up exception handler first, we want to sent them back in correct format that
136 // the other system understands
137 // we do not need to call the original default handler because this ws handler does everything
138 set_exception_handler(array($this, 'exception_handler'));
139
88098133 140 // this sets up $USER and $SESSION and context restrictions
141 $this->authenticate_user();
142
143 // make a list of all functions user is allowed to excecute
144 $this->init_service_class();
145
146 // start the server
147 $this->zend_server->setClass($this->service_class);
148 $response = $this->zend_server->handle();
5593d2dc 149/*
150 $grrr = ob_get_clean();
151 error_log($grrr);
152 error_log($response);
153*/
88098133 154 // session cleanup
155 $this->session_cleanup();
156
ca6340bf 157 //finally send the result
158 $this->send_headers();
88098133 159 echo $response;
160 die;
161 }
162
163 /**
164 * Load virtual class needed for Zend api
165 * @return void
166 */
167 protected function init_service_class() {
168 global $USER, $DB;
169
170 // first ofall get a complete list of services user is allowed to access
171 if ($this->simple) {
172 // now make sure the function is listed in at least one service user is allowed to use
173 // allow access only if:
174 // 1/ entry in the external_services_users table if required
175 // 2/ validuntil not reached
176 // 3/ has capability if specified in service desc
177 // 4/ iprestriction
178
179 $sql = "SELECT s.*, NULL AS iprestriction
180 FROM {external_services} s
181 JOIN {external_services_functions} sf ON (sf.externalserviceid = s.id AND s.restrictedusers = 0)
182 WHERE s.enabled = 1
183
184 UNION
185
186 SELECT s.*, su.iprestriction
187 FROM {external_services} s
188 JOIN {external_services_functions} sf ON (sf.externalserviceid = s.id AND s.restrictedusers = 1)
189 JOIN {external_services_users} su ON (su.externalserviceid = s.id AND su.userid = :userid)
190 WHERE s.enabled = 1 AND su.validuntil IS NULL OR su.validuntil < :now";
191 $params = array('userid'=>$USER->id, 'now'=>time());
192 } else {
193
194 //TODO: token may restrict access to one service only
195 die('not implemented yet');
196 }
197
198 $serviceids = array();
199 $rs = $DB->get_recordset_sql($sql, $params);
200
201 // now make sure user may access at least one service
202 $remoteaddr = getremoteaddr();
203 $allowed = false;
204 foreach ($rs as $service) {
205 if (isset($serviceids[$service->id])) {
206 continue;
207 }
208 if ($service->requiredcapability and !has_capability($service->requiredcapability, $this->restricted_context)) {
209 continue; // cap required, sorry
210 }
211 if ($service->iprestriction and !address_in_subnet($remoteaddr, $service->iprestriction)) {
212 continue; // wrong request source ip, sorry
213 }
214 $serviceids[$service->id] = $service->id;
215 }
216 $rs->close();
217
218 // now get the list of all functions
219 if ($serviceids) {
220 list($serviceids, $params) = $DB->get_in_or_equal($serviceids);
221 $sql = "SELECT f.*
222 FROM {external_functions} f
223 WHERE f.name IN (SELECT sf.functionname
224 FROM {external_services_functions} sf
225 WHERE sf.externalserviceid $serviceids)";
226 $functions = $DB->get_records_sql($sql, $params);
227 } else {
228 $functions = array();
229 }
230
231 // now make the virtual WS class with all the fuctions for this particular user
232 $methods = '';
233 foreach ($functions as $function) {
234 $methods .= $this->get_virtual_method_code($function);
235 }
236
5593d2dc 237 // let's use unique class name, there might be problem in unit tests
88098133 238 $classname = 'webservices_virtual_class_000000';
239 while(class_exists($classname)) {
240 $classname++;
241 }
242
243 $code = '
244/**
245 * Virtual class web services for user id '.$USER->id.' in context '.$this->restricted_context->id.'.
246 */
247class '.$classname.' {
248'.$methods.'
249}
250';
f0dafb3c 251
88098133 252 // load the virtual class definition into memory
253 eval($code);
88098133 254 $this->service_class = $classname;
255 }
256
257 /**
258 * returns virtual method code
259 * @param object $function
260 * @return string PHP code
261 */
262 protected function get_virtual_method_code($function) {
263 global $CFG;
264
5593d2dc 265 $function = external_function_info($function);
88098133 266
267 $params = array();
268 $params_desc = array();
453a7a85 269 foreach ($function->parameters_desc->keys as $name=>$keydesc) {
88098133 270 $params[] = '$'.$name;
453a7a85 271 $type = 'string';
272 if ($keydesc instanceof external_value) {
273 switch($keydesc->type) {
274 case PARAM_BOOL: // 0 or 1 only for now
275 case PARAM_INT:
276 $type = 'int'; break;
277 case PARAM_FLOAT;
278 $type = 'double'; break;
279 default:
280 $type = 'string';
281 }
282 } else if ($keydesc instanceof external_single_structure) {
283 $type = 'struct';
284 } else if ($keydesc instanceof external_multiple_structure) {
285 $type = 'array';
286 }
287 $params_desc[] = ' * @param '.$type.' $'.$name.' '.$keydesc->desc;
88098133 288 }
289 $params = implode(', ', $params);
290 $params_desc = implode("\n", $params_desc);
291
453a7a85 292 if (is_null($function->returns_desc)) {
293 $return = ' * @return void';
294 } else {
295 $type = 'string';
296 if ($function->returns_desc instanceof external_value) {
297 switch($function->returns_desc->type) {
298 case PARAM_BOOL: // 0 or 1 only for now
299 case PARAM_INT:
300 $type = 'int'; break;
301 case PARAM_FLOAT;
302 $type = 'double'; break;
303 default:
304 $type = 'string';
305 }
306 } else if ($function->returns_desc instanceof external_single_structure) {
307 $type = 'struct';
308 } else if ($function->returns_desc instanceof external_multiple_structure) {
309 $type = 'array';
310 }
311 $return = ' * @return '.$type.' '.$function->returns_desc->desc;
312 }
d4e764ab 313
88098133 314 // now crate a virtual method that calls the ext implemenation
315 // TODO: add PHP docs and all missing info here
316
317 $code = '
318 /**
5593d2dc 319 * '.$function->description.'
320 *
88098133 321'.$params_desc.'
453a7a85 322'.$return.'
88098133 323 */
324 public function '.$function->name.'('.$params.') {
325 return '.$function->classname.'::'.$function->methodname.'('.$params.');
326 }
327';
328 return $code;
329 }
330
331 /**
332 * Set up zend serice class
333 * @return void
334 */
335 protected function init_zend_server() {
336 include "Zend/Loader.php";
337 Zend_Loader::registerAutoload();
338 //TODO: set up some server options and debugging too - maybe a new method
339 //TODO: add some zend exeption handler too
340 $this->zend_server = new $this->zend_class();
88098133 341
e8b21670 342 // TODO: solve debugging level somehow
343 Zend_XmlRpc_Server_Fault::attachFaultException('moodle_exception');
88098133 344 }
345
346 /**
347 * Authenticate user using username+password or token.
348 * This function sets up $USER global.
349 * It is safe to use has_capability() after this.
350 * This method also verifies user is allowed to use this
351 * server.
352 * @return void
353 */
354 protected function authenticate_user() {
355 global $CFG, $DB;
356
357 if (!NO_MOODLE_COOKIES) {
358 throw new coding_exception('Cookies must be disabled in WS servers!');
359 }
360
361 if ($this->simple) {
362 $this->restricted_context = get_context_instance(CONTEXT_SYSTEM);
363
364 if (!is_enabled_auth('webservice')) {
5593d2dc 365 throw new webservice_access_exception('WS auth not enabled');
88098133 366 }
367
368 if (!$auth = get_auth_plugin('webservice')) {
5593d2dc 369 throw new webservice_access_exception('WS auth missing');
88098133 370 }
371
372 // the username is hardcoded as URL parameter because we can not easily parse the request data :-(
373 if (!$username = optional_param('wsusername', '', PARAM_RAW)) {
5593d2dc 374 throw new webservice_access_exception('Missing username');
88098133 375 }
376
377 // the password is hardcoded as URL parameter because we can not easily parse the request data :-(
378 if (!$password = optional_param('wspassword', '', PARAM_RAW)) {
5593d2dc 379 throw new webservice_access_exception('Missing password');
88098133 380 }
381
382 if (!$auth->user_login_webservice($username, $password)) {
5593d2dc 383 throw new webservice_access_exception('Wrong username or password');
88098133 384 }
385
386 $user = $DB->get_record('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id, 'deleted'=>0), '*', MUST_EXIST);
387
388 // now fake user login, the session is completely empty too
389 session_set_user($user);
390
391 } else {
392
393 //TODO: not implemented yet
394 die('token login not implemented yet');
395 //TODO: $this->restricted_context is derived from the token context
396 }
397
398 if (!has_capability("webservice/$this->wsname:use", $this->restricted_context)) {
5593d2dc 399 throw new webservice_access_exception('Access to web service not allowed');
88098133 400 }
401
402 external_api::set_context_restriction($this->restricted_context);
403 }
404
ca6340bf 405 /**
406 * Internal implementation - sending of page headers.
407 * @return void
408 */
409 protected function send_headers() {
410 header('Cache-Control: private, must-revalidate, pre-check=0, post-check=0, max-age=0');
411 header('Expires: '. gmdate('D, d M Y H:i:s', 0) .' GMT');
412 header('Pragma: no-cache');
413 header('Accept-Ranges: none');
414 }
415
88098133 416 /**
417 * Specialised exception handler, we can not use the standard one because
418 * it can not just print html to output.
419 *
420 * @param exception $ex
421 * @return void does not return
422 */
423 public function exception_handler($ex) {
424 global $CFG, $DB, $SCRIPT;
425
426 // detect active db transactions, rollback and log as error
427 if ($DB->is_transaction_started()) {
428 error_log('Database transaction aborted by exception in ' . $CFG->dirroot . $SCRIPT);
429 try {
430 // note: transaction blocks should never change current $_SESSION
431 $DB->rollback_sql();
432 } catch (Exception $ignored) {
433 }
434 }
435
88098133 436 // some hacks might need a cleanup hook
437 $this->session_cleanup($ex);
438
ca6340bf 439 // now let the plugin send the exception to client
440 $this->send_headers();
441 echo $this->zend_server->fault($ex);
442
88098133 443 // not much else we can do now, add some logging later
444 exit(1);
445 }
446
447 /**
448 * Future hook needed for emulated sessions.
449 * @param exception $exception null means normal termination, $exception received when WS call failed
450 * @return void
451 */
452 protected function session_cleanup($exception=null) {
453 if ($this->simple) {
454 // nothing needs to be done, there is no persistent session
455 } else {
456 // close emulated session if used
457 }
458 }
459
cc93c7da 460}
461
06e7fadc 462
886d7556 463/**
cc93c7da 464 * Web Service server base class, this class handles both
465 * simple and token authentication.
466 * @author Petr Skoda (skodak)
886d7556 467 */
cc93c7da 468abstract class webservice_base_server implements webservice_server {
469
470 /** @property string $wsname name of the web server plugin */
471 protected $wsname = null;
472
473 /** @property bool $simple true if simple auth used */
474 protected $simple;
475
476 /** @property string $username name of local user */
477 protected $username = null;
478
479 /** @property string $password password of the local user */
480 protected $password = null;
481
482 /** @property string $token authentication token*/
483 protected $token = null;
484
88098133 485 /** @property object restricted context */
486 protected $restricted_context;
487
cc93c7da 488 /** @property array $parameters the function parameters - the real values submitted in the request */
489 protected $parameters = null;
490
491 /** @property string $functionname the name of the function that is executed */
492 protected $functionname = null;
493
494 /** @property object $function full function description */
495 protected $function = null;
496
497 /** @property mixed $returns function return value */
498 protected $returns = null;
06e7fadc 499
500 /**
cc93c7da 501 * Contructor
06e7fadc 502 */
cc93c7da 503 public function __construct() {
06e7fadc 504 }
505
24350e06 506 /**
cc93c7da 507 * This method parses the request input, it needs to get:
508 * 1/ user authentication - username+password or token
509 * 2/ function name
510 * 3/ function parameters
511 *
512 * @return void
24350e06 513 */
cc93c7da 514 abstract protected function parse_request();
24350e06 515
cc93c7da 516 /**
517 * Send the result of function call to the WS client.
518 * @return void
519 */
520 abstract protected function send_response();
24350e06 521
fa0797ec 522 /**
cc93c7da 523 * Send the error information to the WS client.
524 * @param exception $ex
525 * @return void
fa0797ec 526 */
cc93c7da 527 abstract protected function send_error($ex=null);
fa0797ec 528
cc93c7da 529 /**
530 * Process request from client.
531 * @param bool $simple use simple authentication
532 * @return void
533 */
534 public function run($simple) {
535 $this->simple = $simple;
fa0797ec 536
cc93c7da 537 // we will probably need a lot of memory in some functions
538 @raise_memory_limit('128M');
fa0797ec 539
cc93c7da 540 // set some longer timeout, this script is not sending any output,
541 // this means we need to manually extend the timeout operations
542 // that need longer time to finish
543 external_api::set_timeout();
fa0797ec 544
cc93c7da 545 // set up exception handler first, we want to sent them back in correct format that
546 // the other system understands
547 // we do not need to call the original default handler because this ws handler does everything
548 set_exception_handler(array($this, 'exception_handler'));
06e7fadc 549
cc93c7da 550 // init all properties from the request data
551 $this->parse_request();
06e7fadc 552
cc93c7da 553 // authenticate user, this has to be done after the request parsing
554 // this also sets up $USER and $SESSION
555 $this->authenticate_user();
06e7fadc 556
cc93c7da 557 // find all needed function info and make sure user may actually execute the function
558 $this->load_function_info();
f7631e73 559
cc93c7da 560 // finally, execute the function - any errors are catched by the default exception handler
561 $this->execute();
06e7fadc 562
cc93c7da 563 // send the results back in correct format
564 $this->send_response();
06e7fadc 565
cc93c7da 566 // session cleanup
567 $this->session_cleanup();
06e7fadc 568
cc93c7da 569 die;
f7631e73 570 }
571
cc93c7da 572 /**
573 * Specialised exception handler, we can not use the standard one because
574 * it can not just print html to output.
575 *
576 * @param exception $ex
577 * @return void does not return
578 */
579 public function exception_handler($ex) {
580 global $CFG, $DB, $SCRIPT;
581
582 // detect active db transactions, rollback and log as error
583 if ($DB->is_transaction_started()) {
584 error_log('Database transaction aborted by exception in ' . $CFG->dirroot . $SCRIPT);
585 try {
586 // note: transaction blocks should never change current $_SESSION
587 $DB->rollback_sql();
588 } catch (Exception $ignored) {
589 }
590 }
06e7fadc 591
cc93c7da 592 // some hacks might need a cleanup hook
593 $this->session_cleanup($ex);
06e7fadc 594
ca6340bf 595 // now let the plugin send the exception to client
596 $this->send_error($ex);
597
cc93c7da 598 // not much else we can do now, add some logging later
599 exit(1);
f7631e73 600 }
601
602 /**
cc93c7da 603 * Future hook needed for emulated sessions.
604 * @param exception $exception null means normal termination, $exception received when WS call failed
605 * @return void
f7631e73 606 */
cc93c7da 607 protected function session_cleanup($exception=null) {
608 if ($this->simple) {
609 // nothing needs to be done, there is no persistent session
610 } else {
611 // close emulated session if used
612 }
f7631e73 613 }
614
cc93c7da 615 /**
616 * Authenticate user using username+password or token.
617 * This function sets up $USER global.
618 * It is safe to use has_capability() after this.
619 * This method also verifies user is allowed to use this
620 * server.
621 * @return void
622 */
623 protected function authenticate_user() {
624 global $CFG, $DB;
06e7fadc 625
cc93c7da 626 if (!NO_MOODLE_COOKIES) {
627 throw new coding_exception('Cookies must be disabled in WS servers!');
628 }
629
630 if ($this->simple) {
88098133 631 $this->restricted_context = get_context_instance(CONTEXT_SYSTEM);
632
cc93c7da 633 if (!is_enabled_auth('webservice')) {
5593d2dc 634 throw new webservice_access_exception('WS auth not enabled');
cc93c7da 635 }
636
637 if (!$auth = get_auth_plugin('webservice')) {
5593d2dc 638 throw new webservice_access_exception('WS auth missing');
cc93c7da 639 }
640
641 if (!$this->username) {
5593d2dc 642 throw new webservice_access_exception('Missing username');
cc93c7da 643 }
644
645 if (!$this->password) {
5593d2dc 646 throw new webservice_access_exception('Missing password');
cc93c7da 647 }
648
649 if (!$auth->user_login_webservice($this->username, $this->password)) {
5593d2dc 650 throw new webservice_access_exception('Wrong username or password');
cc93c7da 651 }
652
653 $user = $DB->get_record('user', array('username'=>$this->username, 'mnethostid'=>$CFG->mnet_localhost_id, 'deleted'=>0), '*', MUST_EXIST);
654
655 // now fake user login, the session is completely empty too
656 session_set_user($user);
06e7fadc 657 } else {
88098133 658
cc93c7da 659 //TODO: not implemented yet
660 die('token login not implemented yet');
88098133 661 //TODO: $this->restricted_context is derived from the token context
662 }
cc93c7da 663
88098133 664 if (!has_capability("webservice/$this->wsname:use", $this->restricted_context)) {
5593d2dc 665 throw new webservice_access_exception('Access to web service not allowed');
886d7556 666 }
88098133 667
668 external_api::set_context_restriction($this->restricted_context);
06e7fadc 669 }
24350e06 670
671 /**
cc93c7da 672 * Fetches the function description from database,
673 * verifies user is allowed to use this function and
674 * loads all paremeters and return descriptions.
675 * @return void
24350e06 676 */
cc93c7da 677 protected function load_function_info() {
678 global $DB, $USER, $CFG;
40f024c9 679
cc93c7da 680 if (empty($this->functionname)) {
681 throw new invalid_parameter_exception('Missing function name');
682 }
24350e06 683
cc93c7da 684 // function must exist
5593d2dc 685 $function = external_function_info($this->functionname);
cc93c7da 686
687 // now let's verify access control
688 if ($this->simple) {
689 // now make sure the function is listed in at least one service user is allowed to use
690 // allow access only if:
691 // 1/ entry in the external_services_users table if required
692 // 2/ validuntil not reached
693 // 3/ has capability if specified in service desc
694 // 4/ iprestriction
695
696 $sql = "SELECT s.*, NULL AS iprestriction
697 FROM {external_services} s
698 JOIN {external_services_functions} sf ON (sf.externalserviceid = s.id AND s.restrictedusers = 0 AND sf.functionname = :name1)
699 WHERE s.enabled = 1
700
701 UNION
702
703 SELECT s.*, su.iprestriction
704 FROM {external_services} s
705 JOIN {external_services_functions} sf ON (sf.externalserviceid = s.id AND s.restrictedusers = 1 AND sf.functionname = :name2)
706 JOIN {external_services_users} su ON (su.externalserviceid = s.id AND su.userid = :userid)
707 WHERE s.enabled = 1 AND su.validuntil IS NULL OR su.validuntil < :now";
88098133 708 $params = array('userid'=>$USER->id, 'name1'=>$function->name, 'name2'=>$function->name, 'now'=>time());
709 } else {
cc93c7da 710
88098133 711 //TODO: token may restrict access to one service only
712 die('not implemented yet');
713 }
714
715 $rs = $DB->get_recordset_sql($sql, $params);
716 // now make sure user may access at least one service
717 $remoteaddr = getremoteaddr();
718 $allowed = false;
719 foreach ($rs as $service) {
720 if ($service->requiredcapability and !has_capability($service->requiredcapability, $this->restricted_context)) {
721 continue; // cap required, sorry
cc93c7da 722 }
88098133 723 if ($service->iprestriction and !address_in_subnet($remoteaddr, $service->iprestriction)) {
724 continue; // wrong request source ip, sorry
cc93c7da 725 }
88098133 726 $allowed = true;
727 break; // one service is enough, no need to continue
728 }
729 $rs->close();
730 if (!$allowed) {
c91cc5ef 731 throw new webservice_access_exception('Access to external function not allowed');
cc93c7da 732 }
9baf6825 733
cc93c7da 734 // we have all we need now
735 $this->function = $function;
736 }
737
738 /**
739 * Execute previously loaded function using parameters parsed from the request data.
740 * @return void
741 */
742 protected function execute() {
743 // validate params, this also sorts the params properly, we need the correct order in the next part
744 $params = call_user_func(array($this->function->classname, 'validate_parameters'), $this->function->parameters_desc, $this->parameters);
9baf6825 745
cc93c7da 746 // execute - yay!
747 $this->returns = call_user_func_array(array($this->function->classname, $this->function->methodname), array_values($params));
9baf6825 748 }
749}
750
751