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