reordering strings
[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
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'));
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
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) {
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
PS
309 list($context, $course, $cm) = get_context_info_array($context->id);
310 // must be enrolled or viewing
311 if (!is_enrolled($context) and !is_viewing($context)) {
312 throw new invalid_parameter_exception('Must be enrolled in course or be allowed to inspect it.');
313 }
314 // make sure the course is actually visible
315 if (!($course->visible && course_parent_visible($COURSE)) && !has_capability('moodle/course:viewhiddencourses', get_context_instance(CONTEXT_COURSE, $course->id))) {
316 throw new invalid_parameter_exception('Invalid course.');
317 }
318 // make sure the activity is actually visible
319 if ($cm && !$cm->visible && !has_capability('moodle/course:viewhiddenactivities', get_context_instance(CONTEXT_MODULE, $cm->id))) {
320 throw new invalid_parameter_exception('Invalid activity.');
321 }
322 // verify group memebers
98da6021 323 if (!empty($CFG->enablegroupmembersonly) and $cm and $cm->groupmembersonly and !has_capability('moodle/site:accessallgroups', get_context_instance(CONTEXT_MODULE, $cm->id))) {
4f0c2d00
PS
324 if (!groups_has_membership($cm)) {
325 throw new invalid_parameter_exception('Must be member of at least one group.');
326 }
327 }
328 //TODO: verify course completion
9a0df45a 329 }
330 }
9a0df45a 331}
332
b038c32c 333/**
334 * Common ancestor of all parameter description classes
335 */
336abstract class external_description {
337 /** @property string $description description of element */
338 public $desc;
339 /** @property bool $required element value required, null not alowed */
340 public $required;
341
342 /**
343 * Contructor
344 * @param string $desc
345 * @param bool $required
346 */
c9c5cc81 347 public function __construct($desc, $required) {
b038c32c 348 $this->desc = $desc;
349 $this->required = $required;
350 }
351}
352
353/**
04d212ce 354 * Scalar alue description class
b038c32c 355 */
04d212ce 356class external_value extends external_description {
357 /** @property mixed $type value type PARAM_XX */
b038c32c 358 public $type;
359 /** @property mixed $default default value */
360 public $default;
361 /** @property bool $allownull allow null values */
362 public $allownull;
363
364 /**
365 * Constructor
366 * @param mixed $type
367 * @param string $desc
368 * @param bool $required
369 * @param mixed $default
370 * @param bool $allownull
371 */
5a1861ee 372 public function __construct($type, $desc='', $required=VALUE_REQUIRED, $default=null, $allownull=NULL_ALLOWED) {
c9c5cc81 373 parent::__construct($desc, $required);
b038c32c 374 $this->type = $type;
375 $this->default = $default;
376 $this->allownull = $allownull;
377 }
378}
379
380/**
381 * Associative array description class
382 */
383class external_single_structure extends external_description {
384 /** @property array $keys description of array keys key=>external_description */
385 public $keys;
386
387 /**
388 * Constructor
389 * @param array $keys
390 * @param string $desc
391 * @param bool $required
392 */
382b9cea 393 public function __construct(array $keys, $desc='', $required=VALUE_REQUIRED) {
c9c5cc81 394 parent::__construct($desc, $required);
b038c32c 395 $this->keys = $keys;
396 }
397}
398
399/**
400 * Bulk array description class.
401 */
402class external_multiple_structure extends external_description {
403 /** @property external_description $content */
404 public $content;
405
406 /**
407 * Constructor
408 * @param external_description $content
409 * @param string $desc
410 * @param bool $required
411 */
382b9cea 412 public function __construct(external_description $content, $desc='', $required=VALUE_REQUIRED) {
c9c5cc81 413 parent::__construct($desc, $required);
b038c32c 414 $this->content = $content;
415 }
416}
c29cca30 417
418/**
419 * Description of top level - PHP function parameters.
420 * @author skodak
421 *
422 */
423class external_function_parameters extends external_single_structure {
424}
2822f40a
JP
425
426function external_generate_token($tokentype, $serviceorid, $userid, $contextorid, $validuntil=0, $iprestriction=''){
427 global $DB, $USER;
428 // make sure the token doesn't exist (even if it should be almost impossible with the random generation)
429 $numtries = 0;
430 do {
431 $numtries ++;
432 $generatedtoken = md5(uniqid(rand(),1));
433 if ($numtries > 5){
434 throw new moodle_exception('tokengenerationfailed');
435 }
436 } while ($DB->record_exists('external_tokens', array('token'=>$generatedtoken)));
437 $newtoken = new object();
438 $newtoken->token = $generatedtoken;
439 if (!is_object($serviceorid)){
440 $service = $DB->get_record('external_services', array('id' => $serviceorid));
441 } else {
442 $service = $serviceorid;
443 }
444 if (!is_object($contextorid)){
445 $context = get_context_instance_by_id($contextorid, MUST_EXIST);
446 } else {
447 $context = $contextorid;
448 }
449 if (empty($service->requiredcapability) || has_capability($service->requiredcapability, $context, $userid)) {
450 $newtoken->externalserviceid = $service->id;
451 } else {
452 throw new moodle_exception('nocapabilitytousethisservice');
453 }
454 $newtoken->tokentype = $tokentype;
455 $newtoken->userid = $userid;
4f0c2d00
PS
456
457 $newtoken->contextid = $context->id;
2822f40a
JP
458 $newtoken->creatorid = $USER->id;
459 $newtoken->timecreated = time();
460 $newtoken->validuntil = $validuntil;
461 if (!empty($iprestriction)) {
462 $newtoken->iprestriction = $iprestriction;
463 }
464 $DB->insert_record('external_tokens', $newtoken);
465 return $newtoken->token;
466}