MDL-51664 externalib: clarify crucial comment
[moodle.git] / lib / externallib.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
18 /**
19  * Support for external API
20  *
21  * @package    core_webservice
22  * @copyright  2009 Petr Skodak
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
26 defined('MOODLE_INTERNAL') || die();
28 /**
29  * Returns detailed function information
30  *
31  * @param string|object $function name of external function or record from external_function
32  * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found;
33  *                        MUST_EXIST means throw exception if no record or multiple records found
34  * @return stdClass description or false if not found or exception thrown
35  * @since Moodle 2.0
36  */
37 function external_function_info($function, $strictness=MUST_EXIST) {
38     global $DB, $CFG;
40     if (!is_object($function)) {
41         if (!$function = $DB->get_record('external_functions', array('name'=>$function), '*', $strictness)) {
42             return false;
43         }
44     }
46     // First try class autoloading.
47     if (!class_exists($function->classname)) {
48         // Fallback to explicit include of externallib.php.
49         $function->classpath = empty($function->classpath) ? core_component::get_component_directory($function->component).'/externallib.php' : $CFG->dirroot.'/'.$function->classpath;
50         if (!file_exists($function->classpath)) {
51             throw new coding_exception('Cannot find file with external function implementation: ' . $function->classname);
52         }
53         require_once($function->classpath);
54         if (!class_exists($function->classname)) {
55             throw new coding_exception('Cannot find external class');
56         }
57     }
59     $function->ajax_method = $function->methodname.'_is_allowed_from_ajax';
60     $function->parameters_method = $function->methodname.'_parameters';
61     $function->returns_method    = $function->methodname.'_returns';
62     $function->deprecated_method = $function->methodname.'_is_deprecated';
64     // make sure the implementaion class is ok
65     if (!method_exists($function->classname, $function->methodname)) {
66         throw new coding_exception('Missing implementation method of '.$function->classname.'::'.$function->methodname);
67     }
68     if (!method_exists($function->classname, $function->parameters_method)) {
69         throw new coding_exception('Missing parameters description');
70     }
71     if (!method_exists($function->classname, $function->returns_method)) {
72         throw new coding_exception('Missing returned values description');
73     }
74     if (method_exists($function->classname, $function->deprecated_method)) {
75         if (call_user_func(array($function->classname, $function->deprecated_method)) === true) {
76             $function->deprecated = true;
77         }
78     }
79     $function->allowed_from_ajax = false;
81     // fetch the parameters description
82     $function->parameters_desc = call_user_func(array($function->classname, $function->parameters_method));
83     if (!($function->parameters_desc instanceof external_function_parameters)) {
84         throw new coding_exception('Invalid parameters description');
85     }
87     // fetch the return values description
88     $function->returns_desc = call_user_func(array($function->classname, $function->returns_method));
89     // null means void result or result is ignored
90     if (!is_null($function->returns_desc) and !($function->returns_desc instanceof external_description)) {
91         throw new coding_exception('Invalid return description');
92     }
94     //now get the function description
95     //TODO MDL-31115 use localised lang pack descriptions, it would be nice to have
96     //      easy to understand descriptions in admin UI,
97     //      on the other hand this is still a bit in a flux and we need to find some new naming
98     //      conventions for these descriptions in lang packs
99     $function->description = null;
100     $servicesfile = core_component::get_component_directory($function->component).'/db/services.php';
101     if (file_exists($servicesfile)) {
102         $functions = null;
103         include($servicesfile);
104         if (isset($functions[$function->name]['description'])) {
105             $function->description = $functions[$function->name]['description'];
106         }
107         if (isset($functions[$function->name]['testclientpath'])) {
108             $function->testclientpath = $functions[$function->name]['testclientpath'];
109         }
110         if (isset($functions[$function->name]['type'])) {
111             $function->type = $functions[$function->name]['type'];
112         }
113         if (isset($functions[$function->name]['ajax'])) {
114             $function->allowed_from_ajax = $functions[$function->name]['ajax'];
115         } else if (method_exists($function->classname, $function->ajax_method)) {
116             if (call_user_func(array($function->classname, $function->ajax_method)) === true) {
117                 debugging('External function ' . $function->ajax_method . '() function is deprecated.' .
118                           'Set ajax=>true in db/service.php instead.', DEBUG_DEVELOPER);
119                 $function->allowed_from_ajax = true;
120             }
121         }
122         if (isset($functions[$function->name]['loginrequired'])) {
123             $function->loginrequired = $functions[$function->name]['loginrequired'];
124         } else {
125             $function->loginrequired = true;
126         }
127     }
129     return $function;
132 /**
133  * Exception indicating user is not allowed to use external function in the current context.
134  *
135  * @package    core_webservice
136  * @copyright  2009 Petr Skodak
137  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
138  * @since Moodle 2.0
139  */
140 class restricted_context_exception extends moodle_exception {
141     /**
142      * Constructor
143      *
144      * @since Moodle 2.0
145      */
146     function __construct() {
147         parent::__construct('restrictedcontextexception', 'error');
148     }
151 /**
152  * Base class for external api methods.
153  *
154  * @package    core_webservice
155  * @copyright  2009 Petr Skodak
156  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
157  * @since Moodle 2.0
158  */
159 class external_api {
161     /** @var stdClass context where the function calls will be restricted */
162     private static $contextrestriction;
164     /**
165      * Set context restriction for all following subsequent function calls.
166      *
167      * @param stdClass $context the context restriction
168      * @since Moodle 2.0
169      */
170     public static function set_context_restriction($context) {
171         self::$contextrestriction = $context;
172     }
174     /**
175      * This method has to be called before every operation
176      * that takes a longer time to finish!
177      *
178      * @param int $seconds max expected time the next operation needs
179      * @since Moodle 2.0
180      */
181     public static function set_timeout($seconds=360) {
182         $seconds = ($seconds < 300) ? 300 : $seconds;
183         core_php_time_limit::raise($seconds);
184     }
186     /**
187      * Validates submitted function parameters, if anything is incorrect
188      * invalid_parameter_exception is thrown.
189      * This is a simple recursive method which is intended to be called from
190      * each implementation method of external API.
191      *
192      * @param external_description $description description of parameters
193      * @param mixed $params the actual parameters
194      * @return mixed params with added defaults for optional items, invalid_parameters_exception thrown if any problem found
195      * @since Moodle 2.0
196      */
197     public static function validate_parameters(external_description $description, $params) {
198         if ($description instanceof external_value) {
199             if (is_array($params) or is_object($params)) {
200                 throw new invalid_parameter_exception('Scalar type expected, array or object received.');
201             }
203             if ($description->type == PARAM_BOOL) {
204                 // special case for PARAM_BOOL - we want true/false instead of the usual 1/0 - we can not be too strict here ;-)
205                 if (is_bool($params) or $params === 0 or $params === 1 or $params === '0' or $params === '1') {
206                     return (bool)$params;
207                 }
208             }
209             $debuginfo = 'Invalid external api parameter: the value is "' . $params .
210                     '", the server was expecting "' . $description->type . '" type';
211             return validate_param($params, $description->type, $description->allownull, $debuginfo);
213         } else if ($description instanceof external_single_structure) {
214             if (!is_array($params)) {
215                 throw new invalid_parameter_exception('Only arrays accepted. The bad value is: \''
216                         . print_r($params, true) . '\'');
217             }
218             $result = array();
219             foreach ($description->keys as $key=>$subdesc) {
220                 if (!array_key_exists($key, $params)) {
221                     if ($subdesc->required == VALUE_REQUIRED) {
222                         throw new invalid_parameter_exception('Missing required key in single structure: '. $key);
223                     }
224                     if ($subdesc->required == VALUE_DEFAULT) {
225                         try {
226                             $result[$key] = self::validate_parameters($subdesc, $subdesc->default);
227                         } catch (invalid_parameter_exception $e) {
228                             //we are only interested by exceptions returned by validate_param() and validate_parameters()
229                             //(in order to build the path to the faulty attribut)
230                             throw new invalid_parameter_exception($key." => ".$e->getMessage() . ': ' .$e->debuginfo);
231                         }
232                     }
233                 } else {
234                     try {
235                         $result[$key] = self::validate_parameters($subdesc, $params[$key]);
236                     } catch (invalid_parameter_exception $e) {
237                         //we are only interested by exceptions returned by validate_param() and validate_parameters()
238                         //(in order to build the path to the faulty attribut)
239                         throw new invalid_parameter_exception($key." => ".$e->getMessage() . ': ' .$e->debuginfo);
240                     }
241                 }
242                 unset($params[$key]);
243             }
244             if (!empty($params)) {
245                 throw new invalid_parameter_exception('Unexpected keys (' . implode(', ', array_keys($params)) . ') detected in parameter array.');
246             }
247             return $result;
249         } else if ($description instanceof external_multiple_structure) {
250             if (!is_array($params)) {
251                 throw new invalid_parameter_exception('Only arrays accepted. The bad value is: \''
252                         . print_r($params, true) . '\'');
253             }
254             $result = array();
255             foreach ($params as $param) {
256                 $result[] = self::validate_parameters($description->content, $param);
257             }
258             return $result;
260         } else {
261             throw new invalid_parameter_exception('Invalid external api description');
262         }
263     }
265     /**
266      * Clean response
267      * If a response attribute is unknown from the description, we just ignore the attribute.
268      * If a response attribute is incorrect, invalid_response_exception is thrown.
269      * Note: this function is similar to validate parameters, however it is distinct because
270      * parameters validation must be distinct from cleaning return values.
271      *
272      * @param external_description $description description of the return values
273      * @param mixed $response the actual response
274      * @return mixed response with added defaults for optional items, invalid_response_exception thrown if any problem found
275      * @author 2010 Jerome Mouneyrac
276      * @since Moodle 2.0
277      */
278     public static function clean_returnvalue(external_description $description, $response) {
279         if ($description instanceof external_value) {
280             if (is_array($response) or is_object($response)) {
281                 throw new invalid_response_exception('Scalar type expected, array or object received.');
282             }
284             if ($description->type == PARAM_BOOL) {
285                 // special case for PARAM_BOOL - we want true/false instead of the usual 1/0 - we can not be too strict here ;-)
286                 if (is_bool($response) or $response === 0 or $response === 1 or $response === '0' or $response === '1') {
287                     return (bool)$response;
288                 }
289             }
290             $debuginfo = 'Invalid external api response: the value is "' . $response .
291                     '", the server was expecting "' . $description->type . '" type';
292             try {
293                 return validate_param($response, $description->type, $description->allownull, $debuginfo);
294             } catch (invalid_parameter_exception $e) {
295                 //proper exception name, to be recursively catched to build the path to the faulty attribut
296                 throw new invalid_response_exception($e->debuginfo);
297             }
299         } else if ($description instanceof external_single_structure) {
300             if (!is_array($response) && !is_object($response)) {
301                 throw new invalid_response_exception('Only arrays/objects accepted. The bad value is: \'' .
302                         print_r($response, true) . '\'');
303             }
305             // Cast objects into arrays.
306             if (is_object($response)) {
307                 $response = (array) $response;
308             }
310             $result = array();
311             foreach ($description->keys as $key=>$subdesc) {
312                 if (!array_key_exists($key, $response)) {
313                     if ($subdesc->required == VALUE_REQUIRED) {
314                         throw new invalid_response_exception('Error in response - Missing following required key in a single structure: ' . $key);
315                     }
316                     if ($subdesc instanceof external_value) {
317                         if ($subdesc->required == VALUE_DEFAULT) {
318                             try {
319                                     $result[$key] = self::clean_returnvalue($subdesc, $subdesc->default);
320                             } catch (invalid_response_exception $e) {
321                                 //build the path to the faulty attribut
322                                 throw new invalid_response_exception($key." => ".$e->getMessage() . ': ' . $e->debuginfo);
323                             }
324                         }
325                     }
326                 } else {
327                     try {
328                         $result[$key] = self::clean_returnvalue($subdesc, $response[$key]);
329                     } catch (invalid_response_exception $e) {
330                         //build the path to the faulty attribut
331                         throw new invalid_response_exception($key." => ".$e->getMessage() . ': ' . $e->debuginfo);
332                     }
333                 }
334                 unset($response[$key]);
335             }
337             return $result;
339         } else if ($description instanceof external_multiple_structure) {
340             if (!is_array($response)) {
341                 throw new invalid_response_exception('Only arrays accepted. The bad value is: \'' .
342                         print_r($response, true) . '\'');
343             }
344             $result = array();
345             foreach ($response as $param) {
346                 $result[] = self::clean_returnvalue($description->content, $param);
347             }
348             return $result;
350         } else {
351             throw new invalid_response_exception('Invalid external api response description');
352         }
353     }
355     /**
356      * Makes sure user may execute functions in this context.
357      *
358      * @param stdClass $context
359      * @since Moodle 2.0
360      */
361     public static function validate_context($context) {
362         global $CFG;
364         if (empty($context)) {
365             throw new invalid_parameter_exception('Context does not exist');
366         }
367         if (empty(self::$contextrestriction)) {
368             self::$contextrestriction = context_system::instance();
369         }
370         $rcontext = self::$contextrestriction;
372         if ($rcontext->contextlevel == $context->contextlevel) {
373             if ($rcontext->id != $context->id) {
374                 throw new restricted_context_exception();
375             }
376         } else if ($rcontext->contextlevel > $context->contextlevel) {
377             throw new restricted_context_exception();
378         } else {
379             $parents = $context->get_parent_context_ids();
380             if (!in_array($rcontext->id, $parents)) {
381                 throw new restricted_context_exception();
382             }
383         }
385         if ($context->contextlevel >= CONTEXT_COURSE) {
386             list($context, $course, $cm) = get_context_info_array($context->id);
387             require_login($course, false, $cm, false, true);
388         }
389     }
391     /**
392      * Get context from passed parameters.
393      * The passed array must either contain a contextid or a combination of context level and instance id to fetch the context.
394      * For example, the context level can be "course" and instanceid can be courseid.
395      *
396      * See context_helper::get_all_levels() for a list of valid context levels.
397      *
398      * @param array $param
399      * @since Moodle 2.6
400      * @throws invalid_parameter_exception
401      * @return context
402      */
403     protected static function get_context_from_params($param) {
404         $levels = context_helper::get_all_levels();
405         if (!empty($param['contextid'])) {
406             return context::instance_by_id($param['contextid'], IGNORE_MISSING);
407         } else if (!empty($param['contextlevel']) && isset($param['instanceid'])) {
408             $contextlevel = "context_".$param['contextlevel'];
409             if (!array_search($contextlevel, $levels)) {
410                 throw new invalid_parameter_exception('Invalid context level = '.$param['contextlevel']);
411             }
412            return $contextlevel::instance($param['instanceid'], IGNORE_MISSING);
413         } else {
414             // No valid context info was found.
415             throw new invalid_parameter_exception('Missing parameters, please provide either context level with instance id or contextid');
416         }
417     }
420 /**
421  * Common ancestor of all parameter description classes
422  *
423  * @package    core_webservice
424  * @copyright  2009 Petr Skodak
425  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
426  * @since Moodle 2.0
427  */
428 abstract class external_description {
429     /** @var string Description of element */
430     public $desc;
432     /** @var bool Element value required, null not allowed */
433     public $required;
435     /** @var mixed Default value */
436     public $default;
438     /**
439      * Contructor
440      *
441      * @param string $desc
442      * @param bool $required
443      * @param mixed $default
444      * @since Moodle 2.0
445      */
446     public function __construct($desc, $required, $default) {
447         $this->desc = $desc;
448         $this->required = $required;
449         $this->default = $default;
450     }
453 /**
454  * Scalar value description class
455  *
456  * @package    core_webservice
457  * @copyright  2009 Petr Skodak
458  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
459  * @since Moodle 2.0
460  */
461 class external_value extends external_description {
463     /** @var mixed Value type PARAM_XX */
464     public $type;
466     /** @var bool Allow null values */
467     public $allownull;
469     /**
470      * Constructor
471      *
472      * @param mixed $type
473      * @param string $desc
474      * @param bool $required
475      * @param mixed $default
476      * @param bool $allownull
477      * @since Moodle 2.0
478      */
479     public function __construct($type, $desc='', $required=VALUE_REQUIRED,
480             $default=null, $allownull=NULL_ALLOWED) {
481         parent::__construct($desc, $required, $default);
482         $this->type      = $type;
483         $this->allownull = $allownull;
484     }
487 /**
488  * Associative array description class
489  *
490  * @package    core_webservice
491  * @copyright  2009 Petr Skodak
492  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
493  * @since Moodle 2.0
494  */
495 class external_single_structure extends external_description {
497      /** @var array Description of array keys key=>external_description */
498     public $keys;
500     /**
501      * Constructor
502      *
503      * @param array $keys
504      * @param string $desc
505      * @param bool $required
506      * @param array $default
507      * @since Moodle 2.0
508      */
509     public function __construct(array $keys, $desc='',
510             $required=VALUE_REQUIRED, $default=null) {
511         parent::__construct($desc, $required, $default);
512         $this->keys = $keys;
513     }
516 /**
517  * Bulk array description class.
518  *
519  * @package    core_webservice
520  * @copyright  2009 Petr Skodak
521  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
522  * @since Moodle 2.0
523  */
524 class external_multiple_structure extends external_description {
526      /** @var external_description content */
527     public $content;
529     /**
530      * Constructor
531      *
532      * @param external_description $content
533      * @param string $desc
534      * @param bool $required
535      * @param array $default
536      * @since Moodle 2.0
537      */
538     public function __construct(external_description $content, $desc='',
539             $required=VALUE_REQUIRED, $default=null) {
540         parent::__construct($desc, $required, $default);
541         $this->content = $content;
542     }
545 /**
546  * Description of top level - PHP function parameters.
547  *
548  * @package    core_webservice
549  * @copyright  2009 Petr Skodak
550  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
551  * @since Moodle 2.0
552  */
553 class external_function_parameters extends external_single_structure {
555     /**
556      * Constructor - does extra checking to prevent top level optional parameters.
557      *
558      * @param array $keys
559      * @param string $desc
560      * @param bool $required
561      * @param array $default
562      */
563     public function __construct(array $keys, $desc='', $required=VALUE_REQUIRED, $default=null) {
564         global $CFG;
566         if ($CFG->debugdeveloper) {
567             foreach ($keys as $key => $value) {
568                 if ($value instanceof external_value) {
569                     if ($value->required == VALUE_OPTIONAL) {
570                         debugging('External function parameters: invalid OPTIONAL value specified.', DEBUG_DEVELOPER);
571                         break;
572                     }
573                 }
574             }
575         }
576         parent::__construct($keys, $desc, $required, $default);
577     }
580 /**
581  * Generate a token
582  *
583  * @param string $tokentype EXTERNAL_TOKEN_EMBEDDED|EXTERNAL_TOKEN_PERMANENT
584  * @param stdClass|int $serviceorid service linked to the token
585  * @param int $userid user linked to the token
586  * @param stdClass|int $contextorid
587  * @param int $validuntil date when the token expired
588  * @param string $iprestriction allowed ip - if 0 or empty then all ips are allowed
589  * @return string generated token
590  * @author  2010 Jamie Pratt
591  * @since Moodle 2.0
592  */
593 function external_generate_token($tokentype, $serviceorid, $userid, $contextorid, $validuntil=0, $iprestriction=''){
594     global $DB, $USER;
595     // make sure the token doesn't exist (even if it should be almost impossible with the random generation)
596     $numtries = 0;
597     do {
598         $numtries ++;
599         $generatedtoken = md5(uniqid(rand(),1));
600         if ($numtries > 5){
601             throw new moodle_exception('tokengenerationfailed');
602         }
603     } while ($DB->record_exists('external_tokens', array('token'=>$generatedtoken)));
604     $newtoken = new stdClass();
605     $newtoken->token = $generatedtoken;
606     if (!is_object($serviceorid)){
607         $service = $DB->get_record('external_services', array('id' => $serviceorid));
608     } else {
609         $service = $serviceorid;
610     }
611     if (!is_object($contextorid)){
612         $context = context::instance_by_id($contextorid, MUST_EXIST);
613     } else {
614         $context = $contextorid;
615     }
616     if (empty($service->requiredcapability) || has_capability($service->requiredcapability, $context, $userid)) {
617         $newtoken->externalserviceid = $service->id;
618     } else {
619         throw new moodle_exception('nocapabilitytousethisservice');
620     }
621     $newtoken->tokentype = $tokentype;
622     $newtoken->userid = $userid;
623     if ($tokentype == EXTERNAL_TOKEN_EMBEDDED){
624         $newtoken->sid = session_id();
625     }
627     $newtoken->contextid = $context->id;
628     $newtoken->creatorid = $USER->id;
629     $newtoken->timecreated = time();
630     $newtoken->validuntil = $validuntil;
631     if (!empty($iprestriction)) {
632         $newtoken->iprestriction = $iprestriction;
633     }
634     $DB->insert_record('external_tokens', $newtoken);
635     return $newtoken->token;
638 /**
639  * Create and return a session linked token. Token to be used for html embedded client apps that want to communicate
640  * with the Moodle server through web services. The token is linked to the current session for the current page request.
641  * It is expected this will be called in the script generating the html page that is embedding the client app and that the
642  * returned token will be somehow passed into the client app being embedded in the page.
643  *
644  * @param string $servicename name of the web service. Service name as defined in db/services.php
645  * @param int $context context within which the web service can operate.
646  * @return int returns token id.
647  * @since Moodle 2.0
648  */
649 function external_create_service_token($servicename, $context){
650     global $USER, $DB;
651     $service = $DB->get_record('external_services', array('name'=>$servicename), '*', MUST_EXIST);
652     return external_generate_token(EXTERNAL_TOKEN_EMBEDDED, $service, $USER->id, $context, 0);
655 /**
656  * Delete all pre-built services (+ related tokens) and external functions information defined in the specified component.
657  *
658  * @param string $component name of component (moodle, mod_assignment, etc.)
659  */
660 function external_delete_descriptions($component) {
661     global $DB;
663     $params = array($component);
665     $DB->delete_records_select('external_tokens',
666             "externalserviceid IN (SELECT id FROM {external_services} WHERE component = ?)", $params);
667     $DB->delete_records_select('external_services_users',
668             "externalserviceid IN (SELECT id FROM {external_services} WHERE component = ?)", $params);
669     $DB->delete_records_select('external_services_functions',
670             "functionname IN (SELECT name FROM {external_functions} WHERE component = ?)", $params);
671     $DB->delete_records('external_services', array('component'=>$component));
672     $DB->delete_records('external_functions', array('component'=>$component));
675 /**
676  * Standard Moodle web service warnings
677  *
678  * @package    core_webservice
679  * @copyright  2012 Jerome Mouneyrac
680  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
681  * @since Moodle 2.3
682  */
683 class external_warnings extends external_multiple_structure {
685     /**
686      * Constructor
687      *
688      * @since Moodle 2.3
689      */
690     public function __construct($itemdesc = 'item', $itemiddesc = 'item id',
691         $warningcodedesc = 'the warning code can be used by the client app to implement specific behaviour') {
693         parent::__construct(
694             new external_single_structure(
695                 array(
696                     'item' => new external_value(PARAM_TEXT, $itemdesc, VALUE_OPTIONAL),
697                     'itemid' => new external_value(PARAM_INT, $itemiddesc, VALUE_OPTIONAL),
698                     'warningcode' => new external_value(PARAM_ALPHANUM, $warningcodedesc),
699                     'message' => new external_value(PARAM_TEXT,
700                             'untranslated english message to explain the warning')
701                 ), 'warning'),
702             'list of warnings', VALUE_OPTIONAL);
703     }
706 /**
707  * A pre-filled external_value class for text format.
708  *
709  * Default is FORMAT_HTML
710  * This should be used all the time in external xxx_params()/xxx_returns functions
711  * as it is the standard way to implement text format param/return values.
712  *
713  * @package    core_webservice
714  * @copyright  2012 Jerome Mouneyrac
715  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
716  * @since Moodle 2.3
717  */
718 class external_format_value extends external_value {
720     /**
721      * Constructor
722      *
723      * @param string $textfieldname Name of the text field
724      * @param int $required if VALUE_REQUIRED then set standard default FORMAT_HTML
725      * @since Moodle 2.3
726      */
727     public function __construct($textfieldname, $required = VALUE_REQUIRED) {
729         $default = ($required == VALUE_DEFAULT) ? FORMAT_HTML : null;
731         $desc = $textfieldname . ' format (' . FORMAT_HTML . ' = HTML, '
732                 . FORMAT_MOODLE . ' = MOODLE, '
733                 . FORMAT_PLAIN . ' = PLAIN or '
734                 . FORMAT_MARKDOWN . ' = MARKDOWN)';
736         parent::__construct(PARAM_INT, $desc, $required, $default);
737     }
740 /**
741  * Validate text field format against known FORMAT_XXX
742  *
743  * @param array $format the format to validate
744  * @return the validated format
745  * @throws coding_exception
746  * @since Moodle 2.3
747  */
748 function external_validate_format($format) {
749     $allowedformats = array(FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN);
750     if (!in_array($format, $allowedformats)) {
751         throw new moodle_exception('formatnotsupported', 'webservice', '' , null,
752                 'The format with value=' . $format . ' is not supported by this Moodle site');
753     }
754     return $format;
757 /**
758  * Format the string to be returned properly as requested by the either the web service server,
759  * either by an internally call.
760  * The caller can change the format (raw) with the external_settings singleton
761  * All web service servers must set this singleton when parsing the $_GET and $_POST.
762  *
763  * @param string $str The string to be filtered. Should be plain text, expect
764  * possibly for multilang tags.
765  * @param boolean $striplinks To strip any link in the result text. Moodle 1.8 default changed from false to true! MDL-8713
766  * @param int $contextid The id of the context for the string (affects filters).
767  * @param array $options options array/object or courseid
768  * @return string text
769  * @since Moodle 3.0
770  */
771 function external_format_string($str, $contextid, $striplinks = true, $options = array()) {
773     // Get settings (singleton).
774     $settings = external_settings::get_instance();
775     if (empty($contextid)) {
776         throw new coding_exception('contextid is required');
777     }
779     if (!$settings->get_raw()) {
780         $context = context::instance_by_id($contextid);
781         $options['context'] = $context;
782         $options['filter'] = $settings->get_filter();
783         $str = format_string($str, $striplinks, $options);
784     }
786     return $str;
789 /**
790  * Format the text to be returned properly as requested by the either the web service server,
791  * either by an internally call.
792  * The caller can change the format (raw, filter, file, fileurl) with the external_settings singleton
793  * All web service servers must set this singleton when parsing the $_GET and $_POST.
794  *
795  * @param string $text The content that may contain ULRs in need of rewriting.
796  * @param int $textformat The text format.
797  * @param int $contextid This parameter and the next two identify the file area to use.
798  * @param string $component
799  * @param string $filearea helps identify the file area.
800  * @param int $itemid helps identify the file area.
801  * @return array text + textformat
802  * @since Moodle 2.3
803  */
804 function external_format_text($text, $textformat, $contextid, $component, $filearea, $itemid) {
805     global $CFG;
807     // Get settings (singleton).
808     $settings = external_settings::get_instance();
810     if ($settings->get_fileurl()) {
811         require_once($CFG->libdir . "/filelib.php");
812         $text = file_rewrite_pluginfile_urls($text, $settings->get_file(), $contextid, $component, $filearea, $itemid);
813     }
815     if (!$settings->get_raw()) {
816         $context = context::instance_by_id($contextid);
817         $text = format_text($text, $textformat, array('para' => false, 'filter' => $settings->get_filter(), 'context' => $context));
818         $textformat = FORMAT_HTML; // Once converted to html (from markdown, plain... lets inform consumer this is already HTML).
819     }
821     return array($text, $textformat);
824 /**
825  * Singleton to handle the external settings.
826  *
827  * We use singleton to encapsulate the "logic"
828  *
829  * @package    core_webservice
830  * @copyright  2012 Jerome Mouneyrac
831  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
832  * @since Moodle 2.3
833  */
834 class external_settings {
836     /** @var object the singleton instance */
837     public static $instance = null;
839     /** @var boolean Should the external function return raw text or formatted */
840     private $raw = false;
842     /** @var boolean Should the external function filter the text */
843     private $filter = false;
845     /** @var boolean Should the external function rewrite plugin file url */
846     private $fileurl = true;
848     /** @var string In which file should the urls be rewritten */
849     private $file = 'webservice/pluginfile.php';
851     /**
852      * Constructor - protected - can not be instanciated
853      */
854     protected function __construct() {
855         if (!defined('AJAX_SCRIPT') && !defined('CLI_SCRIPT') && !defined('WS_SERVER')) {
856             // For normal pages, the default should match the default for format_text.
857             $this->filter = true;
858         }
859     }
861     /**
862      * Clone - private - can not be cloned
863      */
864     private final function __clone() {
865     }
867     /**
868      * Return only one instance
869      *
870      * @return object
871      */
872     public static function get_instance() {
873         if (self::$instance === null) {
874             self::$instance = new external_settings;
875         }
877         return self::$instance;
878     }
880     /**
881      * Set raw
882      *
883      * @param boolean $raw
884      */
885     public function set_raw($raw) {
886         $this->raw = $raw;
887     }
889     /**
890      * Get raw
891      *
892      * @return boolean
893      */
894     public function get_raw() {
895         return $this->raw;
896     }
898     /**
899      * Set filter
900      *
901      * @param boolean $filter
902      */
903     public function set_filter($filter) {
904         $this->filter = $filter;
905     }
907     /**
908      * Get filter
909      *
910      * @return boolean
911      */
912     public function get_filter() {
913         return $this->filter;
914     }
916     /**
917      * Set fileurl
918      *
919      * @param boolean $fileurl
920      */
921     public function set_fileurl($fileurl) {
922         $this->fileurl = $fileurl;
923     }
925     /**
926      * Get fileurl
927      *
928      * @return boolean
929      */
930     public function get_fileurl() {
931         return $this->fileurl;
932     }
934     /**
935      * Set file
936      *
937      * @param string $file
938      */
939     public function set_file($file) {
940         $this->file = $file;
941     }
943     /**
944      * Get file
945      *
946      * @return string
947      */
948     public function get_file() {
949         return $this->file;
950     }
953 /**
954  * Utility functions for the external API.
955  *
956  * @package    core_webservice
957  * @copyright  2015 Juan Leyva
958  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
959  * @since Moodle 3.0
960  */
961 class external_util {
963     /**
964      * Validate a list of courses, returning the complete course objects for valid courses.
965      *
966      * @param  array $courseids A list of course ids
967      * @param  array $courses   An array of courses already pre-fetched, indexed by course id.
968      * @return array            An array of courses and the validation warnings
969      */
970     public static function validate_courses($courseids, $courses = array()) {
971         // Delete duplicates.
972         $courseids = array_unique($courseids);
973         $warnings = array();
975         // Remove courses which are not even requested.
976         $courses =  array_intersect_key($courses, array_flip($courseids));
978         foreach ($courseids as $cid) {
979             // Check the user can function in this context.
980             try {
981                 $context = context_course::instance($cid);
982                 external_api::validate_context($context);
984                 if (!isset($courses[$cid])) {
985                     $courses[$cid] = get_course($cid);
986                 }
987             } catch (Exception $e) {
988                 unset($courses[$cid]);
989                 $warnings[] = array(
990                     'item' => 'course',
991                     'itemid' => $cid,
992                     'warningcode' => '1',
993                     'message' => 'No access rights in course context'
994                 );
995             }
996         }
998         return array($courses, $warnings);
999     }