MDL-23062 reverting incorrect fix, this needs to be fixed directly in accesslib
[moodle.git] / lib / externallib.php
CommitLineData
9a0df45a 1<?php
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
18/**
19 * Support for external API
20 *
21 * @package moodlecore
22 * @subpackage webservice
551f4420 23 * @copyright 2009 Moodle Pty Ltd (http://moodle.com)
9a0df45a 24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 */
26
1942103f 27
5593d2dc 28/**
bff11d29 29 * Returns detailed function information
5593d2dc 30 * @param string|object $function name of external function or record from external_function
31 * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found;
32 * MUST_EXIST means throw exception if no record or multiple records found
33 * @return object description or false if not found or exception thrown
34 */
35function external_function_info($function, $strictness=MUST_EXIST) {
36 global $DB, $CFG;
37
38 if (!is_object($function)) {
39 if (!$function = $DB->get_record('external_functions', array('name'=>$function), '*', $strictness)) {
40 return false;
41 }
42 }
43
44 //first find and include the ext implementation class
45 $function->classpath = empty($function->classpath) ? get_component_directory($function->component).'/externallib.php' : $CFG->dirroot.'/'.$function->classpath;
46 if (!file_exists($function->classpath)) {
47 throw new coding_exception('Can not find file with external function implementation');
48 }
49 require_once($function->classpath);
50
51 $function->parameters_method = $function->methodname.'_parameters';
52 $function->returns_method = $function->methodname.'_returns';
53
54 // make sure the implementaion class is ok
55 if (!method_exists($function->classname, $function->methodname)) {
203fda8a 56 throw new coding_exception('Missing implementation method of '.$function->classname.'::'.$function->methodname);
5593d2dc 57 }
58 if (!method_exists($function->classname, $function->parameters_method)) {
59 throw new coding_exception('Missing parameters description');
60 }
61 if (!method_exists($function->classname, $function->returns_method)) {
62 throw new coding_exception('Missing returned values description');
63 }
64
65 // fetch the parameters description
66 $function->parameters_desc = call_user_func(array($function->classname, $function->parameters_method));
67 if (!($function->parameters_desc instanceof external_function_parameters)) {
68 throw new coding_exception('Invalid parameters description');
69 }
70
71 // fetch the return values description
72 $function->returns_desc = call_user_func(array($function->classname, $function->returns_method));
73 // null means void result or result is ignored
74 if (!is_null($function->returns_desc) and !($function->returns_desc instanceof external_description)) {
75 throw new coding_exception('Invalid return description');
76 }
77
78 //now get the function description
79 //TODO: use localised lang pack descriptions, it would be nice to have
23e7b7cc 80 // easy to understand descriptions in admin UI,
5593d2dc 81 // on the other hand this is still a bit in a flux and we need to find some new naming
82 // conventions for these descriptions in lang packs
83 $function->description = null;
84 $servicesfile = get_component_directory($function->component).'/db/services.php';
85 if (file_exists($servicesfile)) {
86 $functions = null;
87 include($servicesfile);
88 if (isset($functions[$function->name]['description'])) {
89 $function->description = $functions[$function->name]['description'];
90 }
91 }
92
93 return $function;
94}
95
9a0df45a 96/**
97 * Exception indicating user is not allowed to use external function in
98 * the current context.
99 */
100class restricted_context_exception extends moodle_exception {
101 /**
102 * Constructor
103 */
104 function __construct() {
105 parent::__construct('restrictedcontextexception', 'error');
106 }
107}
108
109/**
110 * Base class for external api methods.
111 */
112class external_api {
9a0df45a 113 private static $contextrestriction;
114
1bea0c27 115 /**
23e7b7cc 116 * Set context restriction for all following subsequent function calls.
1bea0c27 117 * @param stdClass $contex
118 * @return void
119 */
2965d271 120 public static function set_context_restriction($context) {
9a0df45a 121 self::$contextrestriction = $context;
122 }
123
2965d271 124 /**
125 * This method has to be called before every operation
126 * that takes a longer time to finish!
127 *
128 * @param int $seconds max expected time the next operation needs
129 * @return void
130 */
131 public static function set_timeout($seconds=360) {
132 $seconds = ($seconds < 300) ? 300 : $seconds;
133 set_time_limit($seconds);
134 }
135
1bea0c27 136 /**
c9c5cc81 137 * Validates submitted function parameters, if anything is incorrect
1bea0c27 138 * invalid_parameter_exception is thrown.
1d7db36f 139 * This is a simple recursive method which is intended to be called from
140 * each implementation method of external API.
c9c5cc81 141 * @param external_description $description description of parameters
142 * @param mixed $params the actual parameters
143 * @return mixed params with added defaults for optional items, invalid_parameters_exception thrown if any problem found
1bea0c27 144 */
c9c5cc81 145 public static function validate_parameters(external_description $description, $params) {
04d212ce 146 if ($description instanceof external_value) {
c9c5cc81 147 if (is_array($params) or is_object($params)) {
eae18ab6 148 throw new invalid_parameter_exception(get_string('errorscalartype', 'webservice'));
c9c5cc81 149 }
4f0c6ad1
PS
150
151 if ($description->type == PARAM_BOOL) {
152 // special case for PARAM_BOOL - we want true/false instead of the usual 1/0 - we can not be too strict here ;-)
153 if (is_bool($params) or $params === 0 or $params === 1 or $params === '0' or $params === '1') {
154 return (bool)$params;
155 }
156 }
eae18ab6 157 return validate_param($params, $description->type, $description->allownull, get_string('errorinvalidparamsapi', 'webservice'));
4f0c2d00 158
c9c5cc81 159 } else if ($description instanceof external_single_structure) {
160 if (!is_array($params)) {
eae18ab6 161 throw new invalid_parameter_exception(get_string('erroronlyarray', 'webservice'));
c9c5cc81 162 }
163 $result = array();
164 foreach ($description->keys as $key=>$subdesc) {
165 if (!array_key_exists($key, $params)) {
382b9cea 166 if ($subdesc->required == VALUE_REQUIRED) {
eae18ab6 167 throw new invalid_parameter_exception(get_string('errormissingkey', 'webservice', $key));
c9c5cc81 168 }
04d212ce 169 if ($subdesc instanceof external_value) {
382b9cea 170 if ($subdesc->required == VALUE_DEFAULT) {
559a5dbd 171 try {
172 $result[$key] = self::validate_parameters($subdesc, $subdesc->default);
173 } catch (invalid_parameter_exception $e) {
174 throw new webservice_parameter_exception('invalidextparam',$key);
175 }
382b9cea 176 }
177 }
c9c5cc81 178 } else {
559a5dbd 179 try {
180 $result[$key] = self::validate_parameters($subdesc, $params[$key]);
181 } catch (invalid_parameter_exception $e) {
e5752b7d 182 //it's ok to display debug info as here the information is useful for ws client/dev
183 throw new webservice_parameter_exception('invalidextparam',$key." (".$e->debuginfo.")");
559a5dbd 184 }
c9c5cc81 185 }
186 unset($params[$key]);
187 }
188 if (!empty($params)) {
eae18ab6 189 throw new invalid_parameter_exception(get_string('errorunexpectedkey', 'webservice'));
c9c5cc81 190 }
191 return $result;
1bea0c27 192
c9c5cc81 193 } else if ($description instanceof external_multiple_structure) {
194 if (!is_array($params)) {
eae18ab6 195 throw new invalid_parameter_exception(get_string('erroronlyarray', 'webservice'));
c9c5cc81 196 }
197 $result = array();
198 foreach ($params as $param) {
199 $result[] = self::validate_parameters($description->content, $param);
200 }
201 return $result;
202
203 } else {
eae18ab6 204 throw new invalid_parameter_exception(get_string('errorinvalidparamsdesc', 'webservice'));
c9c5cc81 205 }
1bea0c27 206 }
207
d07ff72d 208 /**
209 * Clean response
23e7b7cc
PS
210 * If a response attribute is unknown from the description, we just ignore the attribute.
211 * If a response attribute is incorrect, invalid_response_exception is thrown.
d07ff72d 212 * Note: this function is similar to validate parameters, however it is distinct because
213 * parameters validation must be distinct from cleaning return values.
214 * @param external_description $description description of the return values
215 * @param mixed $response the actual response
216 * @return mixed response with added defaults for optional items, invalid_response_exception thrown if any problem found
217 */
218 public static function clean_returnvalue(external_description $description, $response) {
219 if ($description instanceof external_value) {
220 if (is_array($response) or is_object($response)) {
221 throw new invalid_response_exception(get_string('errorscalartype', 'webservice'));
222 }
223
224 if ($description->type == PARAM_BOOL) {
225 // special case for PARAM_BOOL - we want true/false instead of the usual 1/0 - we can not be too strict here ;-)
226 if (is_bool($response) or $response === 0 or $response === 1 or $response === '0' or $response === '1') {
227 return (bool)$response;
228 }
229 }
230 return validate_param($response, $description->type, $description->allownull, get_string('errorinvalidresponseapi', 'webservice'));
231
232 } else if ($description instanceof external_single_structure) {
233 if (!is_array($response)) {
234 throw new invalid_response_exception(get_string('erroronlyarray', 'webservice'));
235 }
236 $result = array();
237 foreach ($description->keys as $key=>$subdesc) {
238 if (!array_key_exists($key, $response)) {
239 if ($subdesc->required == VALUE_REQUIRED) {
240 throw new invalid_response_exception(get_string('errormissingkey', 'webservice', $key));
241 }
242 if ($subdesc instanceof external_value) {
243 if ($subdesc->required == VALUE_DEFAULT) {
244 try {
245 $result[$key] = self::clean_returnvalue($subdesc, $subdesc->default);
246 } catch (invalid_response_exception $e) {
247 throw new webservice_parameter_exception('invalidextresponse',$key);
248 }
249 }
250 }
251 } else {
252 try {
253 $result[$key] = self::clean_returnvalue($subdesc, $response[$key]);
254 } catch (invalid_response_exception $e) {
255 //it's ok to display debug info as here the information is useful for ws client/dev
256 throw new webservice_parameter_exception('invalidextresponse',$key." (".$e->debuginfo.")");
257 }
258 }
259 unset($response[$key]);
260 }
261
262 return $result;
263
264 } else if ($description instanceof external_multiple_structure) {
265 if (!is_array($response)) {
266 throw new invalid_response_exception(get_string('erroronlyarray', 'webservice'));
267 }
268 $result = array();
269 foreach ($response as $param) {
270 $result[] = self::clean_returnvalue($description->content, $param);
271 }
272 return $result;
273
274 } else {
275 throw new invalid_response_exception(get_string('errorinvalidresponsedesc', 'webservice'));
276 }
277 }
278
9a0df45a 279 /**
280 * Makes sure user may execute functions in this context.
281 * @param object $context
282 * @return void
283 */
284 protected static function validate_context($context) {
4f0c2d00
PS
285 global $CFG;
286
ab9a01f2 287 if (empty($context)) {
288 throw new invalid_parameter_exception('Context does not exist');
289 }
9a0df45a 290 if (empty(self::$contextrestriction)) {
291 self::$contextrestriction = get_context_instance(CONTEXT_SYSTEM);
292 }
293 $rcontext = self::$contextrestriction;
294
295 if ($rcontext->contextlevel == $context->contextlevel) {
aa7fbebd 296 if ($rcontext->id != $context->id) {
9a0df45a 297 throw new restricted_context_exception();
298 }
299 } else if ($rcontext->contextlevel > $context->contextlevel) {
300 throw new restricted_context_exception();
301 } else {
302 $parents = get_parent_contexts($context);
303 if (!in_array($rcontext->id, $parents)) {
304 throw new restricted_context_exception();
305 }
306 }
307
308 if ($context->contextlevel >= CONTEXT_COURSE) {
4f0c2d00 309 list($context, $course, $cm) = get_context_info_array($context->id);
df997f84 310 require_login($course, false, $cm, false, true);
9a0df45a 311 }
312 }
9a0df45a 313}
314
b038c32c 315/**
316 * Common ancestor of all parameter description classes
317 */
318abstract class external_description {
319 /** @property string $description description of element */
320 public $desc;
23e7b7cc 321 /** @property bool $required element value required, null not allowed */
b038c32c 322 public $required;
323
324 /**
325 * Contructor
326 * @param string $desc
327 * @param bool $required
328 */
c9c5cc81 329 public function __construct($desc, $required) {
b038c32c 330 $this->desc = $desc;
331 $this->required = $required;
332 }
333}
334
335/**
04d212ce 336 * Scalar alue description class
b038c32c 337 */
04d212ce 338class external_value extends external_description {
339 /** @property mixed $type value type PARAM_XX */
b038c32c 340 public $type;
341 /** @property mixed $default default value */
342 public $default;
343 /** @property bool $allownull allow null values */
344 public $allownull;
345
346 /**
347 * Constructor
348 * @param mixed $type
349 * @param string $desc
350 * @param bool $required
351 * @param mixed $default
352 * @param bool $allownull
353 */
5a1861ee 354 public function __construct($type, $desc='', $required=VALUE_REQUIRED, $default=null, $allownull=NULL_ALLOWED) {
c9c5cc81 355 parent::__construct($desc, $required);
b038c32c 356 $this->type = $type;
357 $this->default = $default;
358 $this->allownull = $allownull;
359 }
360}
361
362/**
363 * Associative array description class
364 */
365class external_single_structure extends external_description {
366 /** @property array $keys description of array keys key=>external_description */
367 public $keys;
368
369 /**
370 * Constructor
371 * @param array $keys
372 * @param string $desc
373 * @param bool $required
374 */
382b9cea 375 public function __construct(array $keys, $desc='', $required=VALUE_REQUIRED) {
c9c5cc81 376 parent::__construct($desc, $required);
b038c32c 377 $this->keys = $keys;
378 }
379}
380
381/**
382 * Bulk array description class.
383 */
384class external_multiple_structure extends external_description {
385 /** @property external_description $content */
386 public $content;
387
388 /**
389 * Constructor
390 * @param external_description $content
391 * @param string $desc
392 * @param bool $required
393 */
382b9cea 394 public function __construct(external_description $content, $desc='', $required=VALUE_REQUIRED) {
c9c5cc81 395 parent::__construct($desc, $required);
b038c32c 396 $this->content = $content;
397 }
398}
c29cca30 399
400/**
401 * Description of top level - PHP function parameters.
402 * @author skodak
403 *
404 */
405class external_function_parameters extends external_single_structure {
406}
2822f40a
JP
407
408function external_generate_token($tokentype, $serviceorid, $userid, $contextorid, $validuntil=0, $iprestriction=''){
409 global $DB, $USER;
410 // make sure the token doesn't exist (even if it should be almost impossible with the random generation)
411 $numtries = 0;
412 do {
413 $numtries ++;
414 $generatedtoken = md5(uniqid(rand(),1));
415 if ($numtries > 5){
416 throw new moodle_exception('tokengenerationfailed');
417 }
418 } while ($DB->record_exists('external_tokens', array('token'=>$generatedtoken)));
419 $newtoken = new object();
420 $newtoken->token = $generatedtoken;
421 if (!is_object($serviceorid)){
422 $service = $DB->get_record('external_services', array('id' => $serviceorid));
423 } else {
424 $service = $serviceorid;
425 }
426 if (!is_object($contextorid)){
427 $context = get_context_instance_by_id($contextorid, MUST_EXIST);
428 } else {
429 $context = $contextorid;
430 }
431 if (empty($service->requiredcapability) || has_capability($service->requiredcapability, $context, $userid)) {
432 $newtoken->externalserviceid = $service->id;
433 } else {
434 throw new moodle_exception('nocapabilitytousethisservice');
435 }
436 $newtoken->tokentype = $tokentype;
437 $newtoken->userid = $userid;
2d0acbd5
JP
438 if ($tokentype == EXTERNAL_TOKEN_EMBEDDED){
439 $newtoken->sid = session_id();
440 }
4f0c2d00
PS
441
442 $newtoken->contextid = $context->id;
2822f40a
JP
443 $newtoken->creatorid = $USER->id;
444 $newtoken->timecreated = time();
445 $newtoken->validuntil = $validuntil;
446 if (!empty($iprestriction)) {
447 $newtoken->iprestriction = $iprestriction;
448 }
449 $DB->insert_record('external_tokens', $newtoken);
450 return $newtoken->token;
2d0acbd5
JP
451}
452/**
df997f84 453 * Create and return a session linked token. Token to be used for html embedded client apps that want to communicate
2d0acbd5
JP
454 * with the Moodle server through web services. The token is linked to the current session for the current page request.
455 * It is expected this will be called in the script generating the html page that is embedding the client app and that the
456 * returned token will be somehow passed into the client app being embedded in the page.
457 * @param string $servicename name of the web service. Service name as defined in db/services.php
458 * @param int $context context within which the web service can operate.
459 * @return int returns token id.
460 */
461function external_create_service_token($servicename, $context){
462 global $USER, $DB;
463 $service = $DB->get_record('external_services', array('name'=>$servicename), '*', MUST_EXIST);
464 return external_generate_token(EXTERNAL_TOKEN_EMBEDDED, $service, $USER->id, $context, 0);
2822f40a 465}