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