MDL-21874 - phisics are physics :-P
[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)) {
56 throw new coding_exception('Missing implementation method');
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
80 // easy to understand descriptiosn in admin UI,
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 /**
116 * Set context restriction for all folowing subsequent function calls.
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'));
382b9cea 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
210 * If a response attribut is unknown from the description, we just ignore the attribut.
211 * If a response attribut is incorrect, invalid_response_exception is thrown.
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) {
ab9a01f2 285 if (empty($context)) {
286 throw new invalid_parameter_exception('Context does not exist');
287 }
9a0df45a 288 if (empty(self::$contextrestriction)) {
289 self::$contextrestriction = get_context_instance(CONTEXT_SYSTEM);
290 }
291 $rcontext = self::$contextrestriction;
292
293 if ($rcontext->contextlevel == $context->contextlevel) {
aa7fbebd 294 if ($rcontext->id != $context->id) {
9a0df45a 295 throw new restricted_context_exception();
296 }
297 } else if ($rcontext->contextlevel > $context->contextlevel) {
298 throw new restricted_context_exception();
299 } else {
300 $parents = get_parent_contexts($context);
301 if (!in_array($rcontext->id, $parents)) {
302 throw new restricted_context_exception();
303 }
304 }
305
306 if ($context->contextlevel >= CONTEXT_COURSE) {
307 //TODO: temporary bloody hack, this needs to be replaced by
308 // proper enrolment and course visibility check
309 // similar to require_login() (which can not be used
310 // because it can be used only once and redirects)
c9c5cc81 311 // oh - did I say we need to rewrite enrolments in 2.0
9a0df45a 312 // to solve this bloody mess?
313 //
314 // missing: hidden courses and categories, groupmembersonly,
315 // conditional activities, etc.
316 require_capability('moodle/course:view', $context);
317 }
318 }
9a0df45a 319}
320
b038c32c 321/**
322 * Common ancestor of all parameter description classes
323 */
324abstract class external_description {
325 /** @property string $description description of element */
326 public $desc;
327 /** @property bool $required element value required, null not alowed */
328 public $required;
329
330 /**
331 * Contructor
332 * @param string $desc
333 * @param bool $required
334 */
c9c5cc81 335 public function __construct($desc, $required) {
b038c32c 336 $this->desc = $desc;
337 $this->required = $required;
338 }
339}
340
341/**
04d212ce 342 * Scalar alue description class
b038c32c 343 */
04d212ce 344class external_value extends external_description {
345 /** @property mixed $type value type PARAM_XX */
b038c32c 346 public $type;
347 /** @property mixed $default default value */
348 public $default;
349 /** @property bool $allownull allow null values */
350 public $allownull;
351
352 /**
353 * Constructor
354 * @param mixed $type
355 * @param string $desc
356 * @param bool $required
357 * @param mixed $default
358 * @param bool $allownull
359 */
5a1861ee 360 public function __construct($type, $desc='', $required=VALUE_REQUIRED, $default=null, $allownull=NULL_ALLOWED) {
c9c5cc81 361 parent::__construct($desc, $required);
b038c32c 362 $this->type = $type;
363 $this->default = $default;
364 $this->allownull = $allownull;
365 }
366}
367
368/**
369 * Associative array description class
370 */
371class external_single_structure extends external_description {
372 /** @property array $keys description of array keys key=>external_description */
373 public $keys;
374
375 /**
376 * Constructor
377 * @param array $keys
378 * @param string $desc
379 * @param bool $required
380 */
382b9cea 381 public function __construct(array $keys, $desc='', $required=VALUE_REQUIRED) {
c9c5cc81 382 parent::__construct($desc, $required);
b038c32c 383 $this->keys = $keys;
384 }
385}
386
387/**
388 * Bulk array description class.
389 */
390class external_multiple_structure extends external_description {
391 /** @property external_description $content */
392 public $content;
393
394 /**
395 * Constructor
396 * @param external_description $content
397 * @param string $desc
398 * @param bool $required
399 */
382b9cea 400 public function __construct(external_description $content, $desc='', $required=VALUE_REQUIRED) {
c9c5cc81 401 parent::__construct($desc, $required);
b038c32c 402 $this->content = $content;
403 }
404}
c29cca30 405
406/**
407 * Description of top level - PHP function parameters.
408 * @author skodak
409 *
410 */
411class external_function_parameters extends external_single_structure {
412}