MDL-65075 tool_mobile: Check UserAgent only in WS requests
[moodle.git] / admin / tool / mobile / classes / external.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/>.
17 /**
18  * This is the external API for this tool.
19  *
20  * @package    tool_mobile
21  * @copyright  2016 Juan Leyva
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 namespace tool_mobile;
26 defined('MOODLE_INTERNAL') || die();
28 require_once("$CFG->libdir/externallib.php");
29 require_once("$CFG->dirroot/webservice/lib.php");
31 use external_api;
32 use external_files;
33 use external_function_parameters;
34 use external_value;
35 use external_single_structure;
36 use external_multiple_structure;
37 use external_warnings;
38 use context_system;
39 use moodle_exception;
40 use moodle_url;
41 use core_text;
42 use coding_exception;
44 /**
45  * This is the external API for this tool.
46  *
47  * @copyright  2016 Juan Leyva
48  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
49  */
50 class external extends external_api {
52     /**
53      * Returns description of get_plugins_supporting_mobile() parameters.
54      *
55      * @return external_function_parameters
56      * @since  Moodle 3.1
57      */
58     public static function get_plugins_supporting_mobile_parameters() {
59         return new external_function_parameters(array());
60     }
62     /**
63      * Returns a list of Moodle plugins supporting the mobile app.
64      *
65      * @return array an array of warnings and objects containing the plugin information
66      * @since  Moodle 3.1
67      */
68     public static function get_plugins_supporting_mobile() {
69         return array(
70             'plugins' => api::get_plugins_supporting_mobile(),
71             'warnings' => array(),
72         );
73     }
75     /**
76      * Returns description of get_plugins_supporting_mobile() result value.
77      *
78      * @return external_description
79      * @since  Moodle 3.1
80      */
81     public static function get_plugins_supporting_mobile_returns() {
82         return new external_single_structure(
83             array(
84                 'plugins' => new external_multiple_structure(
85                     new external_single_structure(
86                         array(
87                             'component' => new external_value(PARAM_COMPONENT, 'The plugin component name.'),
88                             'version' => new external_value(PARAM_NOTAGS, 'The plugin version number.'),
89                             'addon' => new external_value(PARAM_COMPONENT, 'The Mobile addon (package) name.'),
90                             'dependencies' => new external_multiple_structure(
91                                                 new external_value(PARAM_COMPONENT, 'Mobile addon name.'),
92                                                 'The list of Mobile addons this addon depends on.'
93                                                ),
94                             'fileurl' => new external_value(PARAM_URL, 'The addon package url for download
95                                                             or empty if it doesn\'t exist.'),
96                             'filehash' => new external_value(PARAM_RAW, 'The addon package hash or empty if it doesn\'t exist.'),
97                             'filesize' => new external_value(PARAM_INT, 'The addon package size or empty if it doesn\'t exist.'),
98                             'handlers' => new external_value(PARAM_RAW, 'Handlers definition (JSON)', VALUE_OPTIONAL),
99                             'lang' => new external_value(PARAM_RAW, 'Language strings used by the handlers (JSON)', VALUE_OPTIONAL),
100                         )
101                     )
102                 ),
103                 'warnings' => new external_warnings(),
104             )
105         );
106     }
108     /**
109      * Returns description of get_public_config() parameters.
110      *
111      * @return external_function_parameters
112      * @since  Moodle 3.2
113      */
114     public static function get_public_config_parameters() {
115         return new external_function_parameters(array());
116     }
118     /**
119      * Returns a list of the site public settings, those not requiring authentication.
120      *
121      * @return array with the settings and warnings
122      * @since  Moodle 3.2
123      */
124     public static function get_public_config() {
125         $result = api::get_public_config();
126         $result['warnings'] = array();
127         return $result;
128     }
130     /**
131      * Returns description of get_public_config() result value.
132      *
133      * @return external_description
134      * @since  Moodle 3.2
135      */
136     public static function get_public_config_returns() {
137         return new external_single_structure(
138             array(
139                 'wwwroot' => new external_value(PARAM_RAW, 'Site URL.'),
140                 'httpswwwroot' => new external_value(PARAM_RAW, 'Site https URL (if httpslogin is enabled).'),
141                 'sitename' => new external_value(PARAM_TEXT, 'Site name.'),
142                 'guestlogin' => new external_value(PARAM_INT, 'Whether guest login is enabled.'),
143                 'rememberusername' => new external_value(PARAM_INT, 'Values: 0 for No, 1 for Yes, 2 for optional.'),
144                 'authloginviaemail' => new external_value(PARAM_INT, 'Whether log in via email is enabled.'),
145                 'registerauth' => new external_value(PARAM_PLUGIN, 'Authentication method for user registration.'),
146                 'forgottenpasswordurl' => new external_value(PARAM_URL, 'Forgotten password URL.'),
147                 'authinstructions' => new external_value(PARAM_RAW, 'Authentication instructions.'),
148                 'authnoneenabled' => new external_value(PARAM_INT, 'Whether auth none is enabled.'),
149                 'enablewebservices' => new external_value(PARAM_INT, 'Whether Web Services are enabled.'),
150                 'enablemobilewebservice' => new external_value(PARAM_INT, 'Whether the Mobile service is enabled.'),
151                 'maintenanceenabled' => new external_value(PARAM_INT, 'Whether site maintenance is enabled.'),
152                 'maintenancemessage' => new external_value(PARAM_RAW, 'Maintenance message.'),
153                 'logourl' => new external_value(PARAM_URL, 'The site logo URL', VALUE_OPTIONAL),
154                 'compactlogourl' => new external_value(PARAM_URL, 'The site compact logo URL', VALUE_OPTIONAL),
155                 'typeoflogin' => new external_value(PARAM_INT, 'The type of login. 1 for app, 2 for browser, 3 for embedded.'),
156                 'launchurl' => new external_value(PARAM_URL, 'SSO login launch URL.', VALUE_OPTIONAL),
157                 'mobilecssurl' => new external_value(PARAM_URL, 'Mobile custom CSS theme', VALUE_OPTIONAL),
158                 'tool_mobile_disabledfeatures' => new external_value(PARAM_RAW, 'Disabled features in the app', VALUE_OPTIONAL),
159                 'identityproviders' => new external_multiple_structure(
160                     new external_single_structure(
161                         array(
162                             'name' => new external_value(PARAM_TEXT, 'The identity provider name.'),
163                             'iconurl' => new external_value(PARAM_URL, 'The icon URL for the provider.'),
164                             'url' => new external_value(PARAM_URL, 'The URL of the provider.'),
165                         )
166                     ),
167                     'Identity providers', VALUE_OPTIONAL
168                 ),
169                 'country' => new external_value(PARAM_NOTAGS, 'Default site country', VALUE_OPTIONAL),
170                 'agedigitalconsentverification' => new external_value(PARAM_BOOL, 'Whether age digital consent verification
171                     is enabled.', VALUE_OPTIONAL),
172                 'supportname' => new external_value(PARAM_NOTAGS, 'Site support contact name
173                     (only if age verification is enabled).', VALUE_OPTIONAL),
174                 'supportemail' => new external_value(PARAM_EMAIL, 'Site support contact email
175                     (only if age verification is enabled).', VALUE_OPTIONAL),
176                 'autolang' => new external_value(PARAM_INT, 'Whether to detect default language
177                     from browser setting.', VALUE_OPTIONAL),
178                 'lang' => new external_value(PARAM_LANG, 'Default language for the site.', VALUE_OPTIONAL),
179                 'langmenu' => new external_value(PARAM_INT, 'Whether the language menu should be displayed.', VALUE_OPTIONAL),
180                 'langlist' => new external_value(PARAM_RAW, 'Languages on language menu.', VALUE_OPTIONAL),
181                 'locale' => new external_value(PARAM_RAW, 'Sitewide locale.', VALUE_OPTIONAL),
182                 'warnings' => new external_warnings(),
183             )
184         );
185     }
187     /**
188      * Returns description of get_config() parameters.
189      *
190      * @return external_function_parameters
191      * @since  Moodle 3.2
192      */
193     public static function get_config_parameters() {
194         return new external_function_parameters(
195             array(
196                 'section' => new external_value(PARAM_ALPHANUMEXT, 'Settings section name.', VALUE_DEFAULT, ''),
197             )
198         );
199     }
201     /**
202      * Returns a list of site settings, filtering by section.
203      *
204      * @param string $section settings section name
205      * @return array with the settings and warnings
206      * @since  Moodle 3.2
207      */
208     public static function get_config($section = '') {
210         $params = self::validate_parameters(self::get_config_parameters(), array('section' => $section));
212         $settings = api::get_config($params['section']);
213         $result['settings'] = array();
214         foreach ($settings as $name => $value) {
215             $result['settings'][] = array(
216                 'name' => $name,
217                 'value' => $value,
218             );
219         }
221         $result['warnings'] = array();
222         return $result;
223     }
225     /**
226      * Returns description of get_config() result value.
227      *
228      * @return external_description
229      * @since  Moodle 3.2
230      */
231     public static function get_config_returns() {
232         return new external_single_structure(
233             array(
234                 'settings' => new external_multiple_structure(
235                     new external_single_structure(
236                         array(
237                             'name' => new external_value(PARAM_RAW, 'The name of the setting'),
238                             'value' => new external_value(PARAM_RAW, 'The value of the setting'),
239                         )
240                     ),
241                     'Settings'
242                 ),
243                 'warnings' => new external_warnings(),
244             )
245         );
246     }
248     /**
249      * Returns description of get_autologin_key() parameters.
250      *
251      * @return external_function_parameters
252      * @since  Moodle 3.2
253      */
254     public static function get_autologin_key_parameters() {
255         return new external_function_parameters (
256             array(
257                 'privatetoken' => new external_value(PARAM_ALPHANUM, 'Private token, usually generated by login/token.php'),
258             )
259         );
260     }
262     /**
263      * Creates an auto-login key for the current user. Is created only in https sites and is restricted by time and ip address.
264      *
265      * Please note that it only works if the request comes from the Moodle mobile or desktop app.
266      *
267      * @param string $privatetoken the user private token for validating the request
268      * @return array with the settings and warnings
269      * @since  Moodle 3.2
270      */
271     public static function get_autologin_key($privatetoken) {
272         global $CFG, $DB, $USER;
274         $params = self::validate_parameters(self::get_autologin_key_parameters(), array('privatetoken' => $privatetoken));
275         $privatetoken = $params['privatetoken'];
277         $context = context_system::instance();
279         // We must toletare these two exceptions: forcepasswordchangenotice and usernotfullysetup.
280         try {
281             self::validate_context($context);
282         } catch (moodle_exception $e) {
283             if ($e->errorcode != 'usernotfullysetup' && $e->errorcode != 'forcepasswordchangenotice') {
284                 // In case we receive a different exception, throw it.
285                 throw $e;
286             }
287         }
290         // Only requests from the Moodle mobile or desktop app. This enhances security to avoid any type of XSS attack.
291         // This code goes intentionally here and not inside the check_autologin_prerequisites() function because it
292         // is used by other PHP scripts that can be opened in any browser.
293         if (!\core_useragent::is_moodle_app()) {
294             throw new moodle_exception('apprequired', 'tool_mobile');
295         }
296         api::check_autologin_prerequisites($USER->id);
298         if (isset($_GET['privatetoken']) or empty($privatetoken)) {
299             throw new moodle_exception('invalidprivatetoken', 'tool_mobile');
300         }
302         // Check the request counter, we must limit the number of times the privatetoken is sent.
303         // Between each request 6 minutes are required.
304         $last = get_user_preferences('tool_mobile_autologin_request_last', 0, $USER);
305         // Check if we must reset the count.
306         $timenow = time();
307         if ($timenow - $last < 6 * MINSECS) {
308             throw new moodle_exception('autologinkeygenerationlockout', 'tool_mobile');
309         }
310         set_user_preference('tool_mobile_autologin_request_last', $timenow, $USER);
312         // We are expecting a privatetoken linked to the current token being used.
313         // This WS is only valid when using mobile services via REST (this is intended).
314         $currenttoken = required_param('wstoken', PARAM_ALPHANUM);
315         $conditions = array(
316             'userid' => $USER->id,
317             'token' => $currenttoken,
318             'privatetoken' => $privatetoken,
319         );
320         if (!$token = $DB->get_record('external_tokens', $conditions)) {
321             throw new moodle_exception('invalidprivatetoken', 'tool_mobile');
322         }
324         $result = array();
325         $result['key'] = api::get_autologin_key();
326         $autologinurl = new moodle_url("/$CFG->admin/tool/mobile/autologin.php");
327         $result['autologinurl'] = $autologinurl->out(false);
328         $result['warnings'] = array();
329         return $result;
330     }
332     /**
333      * Returns description of get_autologin_key() result value.
334      *
335      * @return external_description
336      * @since  Moodle 3.2
337      */
338     public static function get_autologin_key_returns() {
339         return new external_single_structure(
340             array(
341                 'key' => new external_value(PARAM_ALPHANUMEXT, 'Auto-login key for a single usage with time expiration.'),
342                 'autologinurl' => new external_value(PARAM_URL, 'Auto-login URL.'),
343                 'warnings' => new external_warnings(),
344             )
345         );
346     }
348     /**
349      * Returns description of get_content() parameters
350      *
351      * @return external_function_parameters
352      * @since Moodle 3.5
353      */
354     public static function get_content_parameters() {
355         return new external_function_parameters(
356             array(
357                 'component' => new external_value(PARAM_COMPONENT, 'Component where the class is e.g. mod_assign.'),
358                 'method' => new external_value(PARAM_ALPHANUMEXT, 'Method to execute in class \$component\output\mobile.'),
359                 'args' => new external_multiple_structure(
360                     new external_single_structure(
361                         array(
362                             'name' => new external_value(PARAM_ALPHANUMEXT, 'Param name.'),
363                             'value' => new external_value(PARAM_RAW, 'Param value.')
364                         )
365                     ), 'Args for the method are optional.', VALUE_OPTIONAL
366                 )
367             )
368         );
369     }
371     /**
372      * Returns a piece of content to be displayed in the Mobile app, it usually returns a template, javascript and
373      * other structured data that will be used to render a view in the Mobile app..
374      *
375      * Callbacks (placed in \$component\output\mobile) that are called by this web service are responsible for doing the
376      * appropriate security checks to access the information to be returned.
377      *
378      * @param string $component fame of the component.
379      * @param string $method function method name in class \$component\output\mobile.
380      * @param array $args optional arguments for the method.
381      * @return array HTML, JavaScript and other required data and information to create a view in the app.
382      * @since Moodle 3.5
383      * @throws coding_exception
384      */
385     public static function get_content($component, $method, $args = array()) {
386         global $OUTPUT, $PAGE, $USER;
388         $params = self::validate_parameters(self::get_content_parameters(),
389             array(
390                 'component' => $component,
391                 'method' => $method,
392                 'args' => $args
393             )
394         );
396         // Reformat arguments into something less unwieldy.
397         $arguments = array();
398         foreach ($params['args'] as $paramargument) {
399             $arguments[$paramargument['name']] = $paramargument['value'];
400         }
402         // The component was validated via the PARAM_COMPONENT parameter type.
403         $classname = '\\' . $params['component'] .'\output\mobile';
404         if (!method_exists($classname, $params['method'])) {
405             throw new coding_exception("Missing method in $classname");
406         }
407         $result = call_user_func_array(array($classname, $params['method']), array($arguments));
409         // Populate otherdata.
410         $otherdata = array();
411         if (!empty($result['otherdata'])) {
412             $result['otherdata'] = (array) $result['otherdata'];
413             foreach ($result['otherdata'] as $name => $value) {
414                 $otherdata[] = array(
415                     'name' => $name,
416                     'value' => $value
417                 );
418             }
419         }
421         return array(
422             'templates'  => !empty($result['templates']) ? $result['templates'] : array(),
423             'javascript' => !empty($result['javascript']) ? $result['javascript'] : '',
424             'otherdata'  => $otherdata,
425             'files'      => !empty($result['files']) ? $result['files'] : array(),
426             'restrict'   => !empty($result['restrict']) ? $result['restrict'] : array(),
427         );
428     }
430     /**
431      * Returns description of get_content() result value
432      *
433      * @return array
434      * @since Moodle 3.5
435      */
436     public static function get_content_returns() {
437         return new external_single_structure(
438             array(
439                 'templates' => new external_multiple_structure(
440                     new external_single_structure(
441                         array(
442                             'id' => new external_value(PARAM_TEXT, 'ID of the template.'),
443                             'html' => new external_value(PARAM_RAW, 'HTML code.'),
444                         )
445                     ),
446                     'Templates required by the generated content.'
447                 ),
448                 'javascript' => new external_value(PARAM_RAW, 'JavaScript code.'),
449                 'otherdata' => new external_multiple_structure(
450                     new external_single_structure(
451                         array(
452                             'name' => new external_value(PARAM_RAW, 'Field name.'),
453                             'value' => new external_value(PARAM_RAW, 'Field value.')
454                         )
455                     ),
456                     'Other data that can be used or manipulated by the template via 2-way data-binding.'
457                 ),
458                 'files' => new external_files('Files in the content.'),
459                 'restrict' => new external_single_structure(
460                     array(
461                         'users' => new external_multiple_structure(
462                             new external_value(PARAM_INT, 'user id'), 'List of allowed users.', VALUE_OPTIONAL
463                         ),
464                         'courses' => new external_multiple_structure(
465                             new external_value(PARAM_INT, 'course id'), 'List of allowed courses.', VALUE_OPTIONAL
466                         ),
467                     ),
468                     'Restrict this content to certain users or courses.'
469                 )
470             )
471         );
472     }
474     /**
475      * Returns description of method parameters
476      *
477      * @return external_function_parameters
478      * @since Moodle 3.7
479      */
480     public static function call_external_functions_parameters() {
481         return new external_function_parameters([
482             'requests' => new external_multiple_structure(
483                 new external_single_structure([
484                     'function' => new external_value(PARAM_ALPHANUMEXT, 'Function name'),
485                     'arguments' => new external_value(PARAM_RAW, 'JSON-encoded object with named arguments', VALUE_DEFAULT, '{}'),
486                     'settingraw' => new external_value(PARAM_BOOL, 'Return raw text', VALUE_DEFAULT, false),
487                     'settingfilter' => new external_value(PARAM_BOOL, 'Filter text', VALUE_DEFAULT, false),
488                     'settingfileurl' => new external_value(PARAM_BOOL, 'Rewrite plugin file URLs', VALUE_DEFAULT, true),
489                     'settinglang' => new external_value(PARAM_LANG, 'Session language', VALUE_DEFAULT, ''),
490                 ])
491             )
492         ]);
493     }
495     /**
496      * Call multiple external functions and return all responses.
497      *
498      * @param array $requests List of requests.
499      * @return array Responses.
500      * @since Moodle 3.7
501      */
502     public static function call_external_functions($requests) {
503         global $SESSION;
505         $params = self::validate_parameters(self::call_external_functions_parameters(), ['requests' => $requests]);
507         // We need to check if the functions being called are included in the service of the current token.
508         // This function only works when using mobile services via REST (this is intended).
509         $webservicemanager = new \webservice;
510         $token = $webservicemanager->get_user_ws_token(required_param('wstoken', PARAM_ALPHANUM));
512         $settings = \external_settings::get_instance();
513         $defaultlang = current_language();
514         $responses = [];
516         foreach ($params['requests'] as $request) {
517             // Some external functions modify _GET or $_POST data, we need to restore the original data after each call.
518             $originalget = fullclone($_GET);
519             $originalpost = fullclone($_POST);
521             // Set external settings and language.
522             $settings->set_raw($request['settingraw']);
523             $settings->set_filter($request['settingfilter']);
524             $settings->set_fileurl($request['settingfileurl']);
525             $settings->set_lang($request['settinglang']);
526             $SESSION->lang = $request['settinglang'] ?: $defaultlang;
528             // Parse arguments to an array, validation is done in external_api::call_external_function.
529             $args = @json_decode($request['arguments'], true);
530             if (!is_array($args)) {
531                 $args = [];
532             }
534             if ($webservicemanager->service_function_exists($request['function'], $token->externalserviceid)) {
535                 $response = external_api::call_external_function($request['function'], $args, false);
536             } else {
537                 // Function not included in the service, return an access exception.
538                 $response = [
539                     'error' => true,
540                     'exception' => [
541                         'errorcode' => 'accessexception',
542                         'module' => 'webservice'
543                     ]
544                 ];
545                 if (debugging('', DEBUG_DEVELOPER)) {
546                     $response['exception']['debuginfo'] = 'Access to the function is not allowed.';
547                 }
548             }
550             if (isset($response['data'])) {
551                 $response['data'] = json_encode($response['data']);
552             }
553             if (isset($response['exception'])) {
554                 $response['exception'] = json_encode($response['exception']);
555             }
556             $responses[] = $response;
558             // Restore original $_GET and $_POST.
559             $_GET = $originalget;
560             $_POST = $originalpost;
562             if ($response['error']) {
563                 // Do not process the remaining requests.
564                 break;
565             }
566         }
568         return ['responses' => $responses];
569     }
571     /**
572      * Returns description of method result value
573      *
574      * @return external_single_structure
575      * @since Moodle 3.7
576      */
577     public static function call_external_functions_returns() {
578         return new external_function_parameters([
579             'responses' => new external_multiple_structure(
580                 new external_single_structure([
581                     'error' => new external_value(PARAM_BOOL, 'Whether an exception was thrown.'),
582                     'data' => new external_value(PARAM_RAW, 'JSON-encoded response data', VALUE_OPTIONAL),
583                     'exception' => new external_value(PARAM_RAW, 'JSON-encoed exception info', VALUE_OPTIONAL),
584                 ])
585              )
586         ]);
587     }