Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 310 and 311] [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]

   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/>.
  16  
  17  namespace tool_mobile;
  18  
  19  use externallib_advanced_testcase;
  20  
  21  defined('MOODLE_INTERNAL') || die();
  22  
  23  global $CFG;
  24  
  25  require_once($CFG->dirroot . '/webservice/tests/helpers.php');
  26  require_once($CFG->dirroot . '/admin/tool/mobile/tests/fixtures/output/mobile.php');
  27  require_once($CFG->dirroot . '/webservice/lib.php');
  28  
  29  /**
  30   * Moodle Mobile admin tool external functions tests.
  31   *
  32   * @package     tool_mobile
  33   * @copyright   2016 Juan Leyva
  34   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  35   * @since       Moodle 3.1
  36   */
  37  class externallib_test extends externallib_advanced_testcase {
  38  
  39      /**
  40       * Test get_plugins_supporting_mobile.
  41       * This is a very basic test because currently there aren't plugins supporting Mobile in core.
  42       */
  43      public function test_get_plugins_supporting_mobile() {
  44          $result = external::get_plugins_supporting_mobile();
  45          $result = \external_api::clean_returnvalue(external::get_plugins_supporting_mobile_returns(), $result);
  46          $this->assertCount(0, $result['warnings']);
  47          $this->assertArrayHasKey('plugins', $result);
  48          $this->assertTrue(is_array($result['plugins']));
  49      }
  50  
  51      public function test_get_public_config() {
  52          global $CFG, $SITE, $OUTPUT;
  53  
  54          $this->resetAfterTest(true);
  55          $result = external::get_public_config();
  56          $result = \external_api::clean_returnvalue(external::get_public_config_returns(), $result);
  57  
  58          // Test default values.
  59          $context = \context_system::instance();
  60          list($authinstructions, $notusedformat) = external_format_text($CFG->auth_instructions, FORMAT_MOODLE, $context->id);
  61          list($maintenancemessage, $notusedformat) = external_format_text($CFG->maintenance_message, FORMAT_MOODLE, $context->id);
  62  
  63          $expected = array(
  64              'wwwroot' => $CFG->wwwroot,
  65              'httpswwwroot' => $CFG->wwwroot,
  66              'sitename' => external_format_string($SITE->fullname, $context->id, true),
  67              'guestlogin' => $CFG->guestloginbutton,
  68              'rememberusername' => $CFG->rememberusername,
  69              'authloginviaemail' => $CFG->authloginviaemail,
  70              'registerauth' => $CFG->registerauth,
  71              'forgottenpasswordurl' => $CFG->forgottenpasswordurl,
  72              'authinstructions' => $authinstructions,
  73              'authnoneenabled' => (int) is_enabled_auth('none'),
  74              'enablewebservices' => $CFG->enablewebservices,
  75              'enablemobilewebservice' => $CFG->enablemobilewebservice,
  76              'maintenanceenabled' => $CFG->maintenance_enabled,
  77              'maintenancemessage' => $maintenancemessage,
  78              'typeoflogin' => api::LOGIN_VIA_APP,
  79              'mobilecssurl' => '',
  80              'tool_mobile_disabledfeatures' => '',
  81              'launchurl' => "$CFG->wwwroot/$CFG->admin/tool/mobile/launch.php",
  82              'country' => $CFG->country,
  83              'agedigitalconsentverification' => \core_auth\digital_consent::is_age_digital_consent_verification_enabled(),
  84              'autolang' => $CFG->autolang,
  85              'lang' => $CFG->lang,
  86              'langmenu' => $CFG->langmenu,
  87              'langlist' => $CFG->langlist,
  88              'locale' => $CFG->locale,
  89              'tool_mobile_minimumversion' => '',
  90              'tool_mobile_iosappid' => get_config('tool_mobile', 'iosappid'),
  91              'tool_mobile_androidappid' => get_config('tool_mobile', 'androidappid'),
  92              'tool_mobile_setuplink' => get_config('tool_mobile', 'setuplink'),
  93              'warnings' => array()
  94          );
  95          $this->assertEquals($expected, $result);
  96  
  97          $this->setAdminUser();
  98          // Change some values.
  99          set_config('registerauth', 'email');
 100          $authinstructions = 'Something with <b>html tags</b>';
 101          set_config('auth_instructions', $authinstructions);
 102          set_config('typeoflogin', api::LOGIN_VIA_BROWSER, 'tool_mobile');
 103          set_config('logo', 'mock.png', 'core_admin');
 104          set_config('logocompact', 'mock.png', 'core_admin');
 105          set_config('forgottenpasswordurl', 'mailto:fake@email.zy'); // Test old hack.
 106          set_config('agedigitalconsentverification', 1);
 107          set_config('autolang', 1);
 108          set_config('lang', 'a_b');  // Set invalid lang.
 109          set_config('disabledfeatures', 'myoverview', 'tool_mobile');
 110          set_config('minimumversion', '3.8.0', 'tool_mobile');
 111  
 112          // Enable couple of issuers.
 113          $issuer = \core\oauth2\api::create_standard_issuer('google');
 114          $irecord = $issuer->to_record();
 115          $irecord->clientid = 'mock';
 116          $irecord->clientsecret = 'mock';
 117          \core\oauth2\api::update_issuer($irecord);
 118  
 119          set_config('hostname', 'localhost', 'auth_cas');
 120          set_config('auth_logo', 'http://invalidurl.com//invalid/', 'auth_cas');
 121          set_config('auth_name', 'CAS', 'auth_cas');
 122          set_config('auth', 'oauth2,cas');
 123  
 124          list($authinstructions, $notusedformat) = external_format_text($authinstructions, FORMAT_MOODLE, $context->id);
 125          $expected['registerauth'] = 'email';
 126          $expected['authinstructions'] = $authinstructions;
 127          $expected['typeoflogin'] = api::LOGIN_VIA_BROWSER;
 128          $expected['forgottenpasswordurl'] = ''; // Expect empty when it's not an URL.
 129          $expected['agedigitalconsentverification'] = true;
 130          $expected['supportname'] = $CFG->supportname;
 131          $expected['supportemail'] = $CFG->supportemail;
 132          $expected['autolang'] = '1';
 133          $expected['lang'] = ''; // Expect empty because it was set to an invalid lang.
 134          $expected['tool_mobile_disabledfeatures'] = 'myoverview';
 135          $expected['tool_mobile_minimumversion'] = '3.8.0';
 136  
 137          if ($logourl = $OUTPUT->get_logo_url()) {
 138              $expected['logourl'] = $logourl->out(false);
 139          }
 140          if ($compactlogourl = $OUTPUT->get_compact_logo_url()) {
 141              $expected['compactlogourl'] = $compactlogourl->out(false);
 142          }
 143  
 144          $result = external::get_public_config();
 145          $result = \external_api::clean_returnvalue(external::get_public_config_returns(), $result);
 146          // First check providers.
 147          $identityproviders = $result['identityproviders'];
 148          unset($result['identityproviders']);
 149  
 150          $this->assertEquals('Google', $identityproviders[0]['name']);
 151          $this->assertEquals($irecord->image, $identityproviders[0]['iconurl']);
 152          $this->assertStringContainsString($CFG->wwwroot, $identityproviders[0]['url']);
 153  
 154          $this->assertEquals('CAS', $identityproviders[1]['name']);
 155          $this->assertEmpty($identityproviders[1]['iconurl']);
 156          $this->assertStringContainsString($CFG->wwwroot, $identityproviders[1]['url']);
 157  
 158          $this->assertEquals($expected, $result);
 159  
 160          // Change providers img.
 161          $newurl = 'validimage.png';
 162          set_config('auth_logo', $newurl, 'auth_cas');
 163          $result = external::get_public_config();
 164          $result = \external_api::clean_returnvalue(external::get_public_config_returns(), $result);
 165          $this->assertStringContainsString($newurl, $result['identityproviders'][1]['iconurl']);
 166      }
 167  
 168      /**
 169       * Test get_config
 170       */
 171      public function test_get_config() {
 172          global $CFG, $SITE;
 173          require_once($CFG->dirroot . '/course/format/lib.php');
 174  
 175          $this->resetAfterTest(true);
 176  
 177          $mysitepolicy = 'http://mysite.is/policy/';
 178          set_config('sitepolicy', $mysitepolicy);
 179  
 180          $result = external::get_config();
 181          $result = \external_api::clean_returnvalue(external::get_config_returns(), $result);
 182  
 183          // SITE summary is null in phpunit which gets transformed to an empty string by format_text.
 184          list($sitesummary, $unused) = external_format_text($SITE->summary, $SITE->summaryformat, \context_system::instance()->id);
 185  
 186          // Test default values.
 187          $context = \context_system::instance();
 188          $expected = array(
 189              array('name' => 'fullname', 'value' => $SITE->fullname),
 190              array('name' => 'shortname', 'value' => $SITE->shortname),
 191              array('name' => 'summary', 'value' => $sitesummary),
 192              array('name' => 'summaryformat', 'value' => FORMAT_HTML),
 193              array('name' => 'frontpage', 'value' => $CFG->frontpage),
 194              array('name' => 'frontpageloggedin', 'value' => $CFG->frontpageloggedin),
 195              array('name' => 'maxcategorydepth', 'value' => $CFG->maxcategorydepth),
 196              array('name' => 'frontpagecourselimit', 'value' => $CFG->frontpagecourselimit),
 197              array('name' => 'numsections', 'value' => course_get_format($SITE)->get_last_section_number()),
 198              array('name' => 'newsitems', 'value' => $SITE->newsitems),
 199              array('name' => 'commentsperpage', 'value' => $CFG->commentsperpage),
 200              array('name' => 'sitepolicy', 'value' => $mysitepolicy),
 201              array('name' => 'sitepolicyhandler', 'value' => ''),
 202              array('name' => 'disableuserimages', 'value' => $CFG->disableuserimages),
 203              array('name' => 'mygradesurl', 'value' => user_mygrades_url()->out(false)),
 204              array('name' => 'tool_mobile_forcelogout', 'value' => 0),
 205              array('name' => 'tool_mobile_customlangstrings', 'value' => ''),
 206              array('name' => 'tool_mobile_disabledfeatures', 'value' => ''),
 207              array('name' => 'tool_mobile_filetypeexclusionlist', 'value' => ''),
 208              array('name' => 'tool_mobile_custommenuitems', 'value' => ''),
 209              array('name' => 'tool_mobile_apppolicy', 'value' => ''),
 210              array('name' => 'calendartype', 'value' => $CFG->calendartype),
 211              array('name' => 'calendar_site_timeformat', 'value' => $CFG->calendar_site_timeformat),
 212              array('name' => 'calendar_startwday', 'value' => $CFG->calendar_startwday),
 213              array('name' => 'calendar_adminseesall', 'value' => $CFG->calendar_adminseesall),
 214              array('name' => 'calendar_lookahead', 'value' => $CFG->calendar_lookahead),
 215              array('name' => 'calendar_maxevents', 'value' => $CFG->calendar_maxevents),
 216          );
 217          $colornumbers = range(1, 10);
 218          foreach ($colornumbers as $number) {
 219              $expected[] = [
 220                  'name' => 'core_admin_coursecolor' . $number,
 221                  'value' => get_config('core_admin', 'coursecolor' . $number)
 222              ];
 223          }
 224          $expected[] = ['name' => 'supportname', 'value' => $CFG->supportname];
 225          $expected[] = ['name' => 'supportemail', 'value' => $CFG->supportemail];
 226          $expected[] = ['name' => 'supportpage', 'value' => $CFG->supportpage];
 227  
 228          $expected[] = ['name' => 'coursegraceperiodafter', 'value' => $CFG->coursegraceperiodafter];
 229          $expected[] = ['name' => 'coursegraceperiodbefore', 'value' => $CFG->coursegraceperiodbefore];
 230  
 231          $this->assertCount(0, $result['warnings']);
 232          $this->assertEquals($expected, $result['settings']);
 233  
 234          // Change a value and retrieve filtering by section.
 235          set_config('commentsperpage', 1);
 236          $expected[10]['value'] = 1;
 237          // Remove not expected elements.
 238          array_splice($expected, 11);
 239  
 240          $result = external::get_config('frontpagesettings');
 241          $result = \external_api::clean_returnvalue(external::get_config_returns(), $result);
 242          $this->assertCount(0, $result['warnings']);
 243          $this->assertEquals($expected, $result['settings']);
 244      }
 245  
 246      /*
 247       * Test get_autologin_key.
 248       */
 249      public function test_get_autologin_key() {
 250          global $DB, $CFG, $USER;
 251  
 252          $this->resetAfterTest(true);
 253  
 254          $user = $this->getDataGenerator()->create_user();
 255          $this->setUser($user);
 256          $service = $DB->get_record('external_services', array('shortname' => MOODLE_OFFICIAL_MOBILE_SERVICE));
 257  
 258          $token = external_generate_token_for_current_user($service);
 259  
 260          // Check we got the private token.
 261          $this->assertTrue(isset($token->privatetoken));
 262  
 263          // Enable requeriments.
 264          $_GET['wstoken'] = $token->token;   // Mock parameters.
 265  
 266          // Fake the app.
 267          \core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
 268                  'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');
 269  
 270          // Even if we force the password change for the current user we should be able to retrieve the key.
 271          set_user_preference('auth_forcepasswordchange', 1, $user->id);
 272  
 273          $this->setCurrentTimeStart();
 274          $result = external::get_autologin_key($token->privatetoken);
 275          $result = \external_api::clean_returnvalue(external::get_autologin_key_returns(), $result);
 276          // Validate the key.
 277          $this->assertEquals(32, \core_text::strlen($result['key']));
 278          $key = $DB->get_record('user_private_key', array('value' => $result['key']));
 279          $this->assertEquals($USER->id, $key->userid);
 280          $this->assertTimeCurrent($key->validuntil - api::LOGIN_KEY_TTL);
 281  
 282          // Now, try with an invalid private token.
 283          set_user_preference('tool_mobile_autologin_request_last', time() - HOURSECS, $USER);
 284  
 285          $this->expectException('moodle_exception');
 286          $this->expectExceptionMessage(get_string('invalidprivatetoken', 'tool_mobile'));
 287          $result = external::get_autologin_key(random_string('64'));
 288      }
 289  
 290      /**
 291       * Test get_autologin_key missing ws.
 292       */
 293      public function test_get_autologin_key_missing_ws() {
 294          global $CFG;
 295          $this->resetAfterTest(true);
 296  
 297          // Fake the app.
 298          \core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
 299              'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');
 300  
 301          // Need to disable webservices to verify that's checked.
 302          $CFG->enablewebservices = 0;
 303          $CFG->enablemobilewebservice = 0;
 304  
 305          $this->setAdminUser();
 306          $this->expectException('moodle_exception');
 307          $this->expectExceptionMessage(get_string('enablewsdescription', 'webservice'));
 308          $result = external::get_autologin_key('');
 309      }
 310  
 311      /**
 312       * Test get_autologin_key missing https.
 313       */
 314      public function test_get_autologin_key_missing_https() {
 315          global $CFG;
 316  
 317          // Fake the app.
 318          \core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
 319              'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');
 320  
 321          // Need to simulate a non HTTPS site here.
 322          $CFG->wwwroot = str_replace('https:', 'http:', $CFG->wwwroot);
 323  
 324          $this->resetAfterTest(true);
 325          $this->setAdminUser();
 326  
 327          $this->expectException('moodle_exception');
 328          $this->expectExceptionMessage(get_string('httpsrequired', 'tool_mobile'));
 329          $result = external::get_autologin_key('');
 330      }
 331  
 332      /**
 333       * Test get_autologin_key missing admin.
 334       */
 335      public function test_get_autologin_key_missing_admin() {
 336          global $CFG;
 337  
 338          $this->resetAfterTest(true);
 339          $this->setAdminUser();
 340  
 341          // Fake the app.
 342          \core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
 343              'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');
 344  
 345          $this->expectException('moodle_exception');
 346          $this->expectExceptionMessage(get_string('autologinnotallowedtoadmins', 'tool_mobile'));
 347          $result = external::get_autologin_key('');
 348      }
 349  
 350      /**
 351       * Test get_autologin_key locked.
 352       */
 353      public function test_get_autologin_key_missing_locked() {
 354          global $CFG, $DB, $USER;
 355  
 356          $this->resetAfterTest(true);
 357          $user = $this->getDataGenerator()->create_user();
 358          $this->setUser($user);
 359  
 360          $service = $DB->get_record('external_services', array('shortname' => MOODLE_OFFICIAL_MOBILE_SERVICE));
 361  
 362          $token = external_generate_token_for_current_user($service);
 363          $_GET['wstoken'] = $token->token;   // Mock parameters.
 364  
 365          // Fake the app.
 366          \core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
 367              'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');
 368  
 369          $result = external::get_autologin_key($token->privatetoken);
 370          $result = \external_api::clean_returnvalue(external::get_autologin_key_returns(), $result);
 371  
 372          // Mock last time request.
 373          $mocktime = time() - 7 * MINSECS;
 374          set_user_preference('tool_mobile_autologin_request_last', $mocktime, $USER);
 375          $result = external::get_autologin_key($token->privatetoken);
 376          $result = \external_api::clean_returnvalue(external::get_autologin_key_returns(), $result);
 377  
 378          // We just requested one token, we must wait.
 379          $this->expectException('moodle_exception');
 380          $this->expectExceptionMessage(get_string('autologinkeygenerationlockout', 'tool_mobile'));
 381          $result = external::get_autologin_key($token->privatetoken);
 382      }
 383  
 384      /**
 385       * Test get_autologin_key missing app_request.
 386       */
 387      public function test_get_autologin_key_missing_app_request() {
 388          global $CFG;
 389  
 390          $this->resetAfterTest(true);
 391          $this->setAdminUser();
 392  
 393          $this->expectException('moodle_exception');
 394          $this->expectExceptionMessage(get_string('apprequired', 'tool_mobile'));
 395          $result = external::get_autologin_key('');
 396      }
 397  
 398      /**
 399       * Test get_content.
 400       */
 401      public function test_get_content() {
 402  
 403          $paramval = 16;
 404          $result = external::get_content('tool_mobile', 'test_view', array(array('name' => 'param1', 'value' => $paramval)));
 405          $result = \external_api::clean_returnvalue(external::get_content_returns(), $result);
 406          $this->assertCount(1, $result['templates']);
 407          $this->assertCount(1, $result['otherdata']);
 408          $this->assertCount(2, $result['restrict']['users']);
 409          $this->assertCount(2, $result['restrict']['courses']);
 410          $this->assertEquals('alert();', $result['javascript']);
 411          $this->assertEquals('main', $result['templates'][0]['id']);
 412          $this->assertEquals('The HTML code', $result['templates'][0]['html']);
 413          $this->assertEquals('otherdata1', $result['otherdata'][0]['name']);
 414          $this->assertEquals($paramval, $result['otherdata'][0]['value']);
 415          $this->assertEquals(array(1, 2), $result['restrict']['users']);
 416          $this->assertEquals(array(3, 4), $result['restrict']['courses']);
 417          $this->assertEmpty($result['files']);
 418          $this->assertFalse($result['disabled']);
 419      }
 420  
 421      /**
 422       * Test get_content disabled.
 423       */
 424      public function test_get_content_disabled() {
 425  
 426          $paramval = 16;
 427          $result = external::get_content('tool_mobile', 'test_view_disabled',
 428              array(array('name' => 'param1', 'value' => $paramval)));
 429          $result = \external_api::clean_returnvalue(external::get_content_returns(), $result);
 430          $this->assertTrue($result['disabled']);
 431      }
 432  
 433      /**
 434       * Test get_content non existent function in valid component.
 435       */
 436      public function test_get_content_non_existent_function() {
 437  
 438          $this->expectException('coding_exception');
 439          $result = external::get_content('tool_mobile', 'test_blahblah');
 440      }
 441  
 442      /**
 443       * Test get_content incorrect component.
 444       */
 445      public function test_get_content_invalid_component() {
 446  
 447          $this->expectException('moodle_exception');
 448          $result = external::get_content('tool_mobile\hack', 'test_view');
 449      }
 450  
 451      /**
 452       * Test get_content non existent component.
 453       */
 454      public function test_get_content_non_existent_component() {
 455  
 456          $this->expectException('moodle_exception');
 457          $result = external::get_content('tool_blahblahblah', 'test_view');
 458      }
 459  
 460      public function test_call_external_functions() {
 461          global $SESSION;
 462  
 463          $this->resetAfterTest(true);
 464  
 465          $category = self::getDataGenerator()->create_category(array('name' => 'Category 1'));
 466          $course = self::getDataGenerator()->create_course([
 467              'category' => $category->id,
 468              'shortname' => 'c1',
 469              'summary' => '<span lang="en" class="multilang">Course summary</span>'
 470                  . '<span lang="eo" class="multilang">Kurso resumo</span>'
 471                  . '@@PLUGINFILE@@/filename.txt'
 472                  . '<!-- Comment stripped when formatting text -->',
 473              'summaryformat' => FORMAT_MOODLE
 474          ]);
 475          $user1 = self::getDataGenerator()->create_user(['username' => 'user1', 'lastaccess' => time()]);
 476          $user2 = self::getDataGenerator()->create_user(['username' => 'user2', 'lastaccess' => time()]);
 477  
 478          self::setUser($user1);
 479  
 480          // Setup WS token.
 481          $webservicemanager = new \webservice;
 482          $service = $webservicemanager->get_external_service_by_shortname(MOODLE_OFFICIAL_MOBILE_SERVICE);
 483          $token = external_generate_token_for_current_user($service);
 484          $_POST['wstoken'] = $token->token;
 485  
 486          // Workaround for \external_api::call_external_function requiring sesskey.
 487          $_POST['sesskey'] = sesskey();
 488  
 489          // Call some functions.
 490  
 491          $requests = [
 492              [
 493                  'function' => 'core_course_get_courses_by_field',
 494                  'arguments' => json_encode(['field' => 'id', 'value' => $course->id])
 495              ],
 496              [
 497                  'function' => 'core_user_get_users_by_field',
 498                  'arguments' => json_encode(['field' => 'id', 'values' => [$user1->id]])
 499              ],
 500              [
 501                  'function' => 'core_user_get_user_preferences',
 502                  'arguments' => json_encode(['name' => 'some_setting', 'userid' => $user2->id])
 503              ],
 504              [
 505                  'function' => 'core_course_get_courses_by_field',
 506                  'arguments' => json_encode(['field' => 'shortname', 'value' => $course->shortname])
 507              ],
 508          ];
 509          $result = external::call_external_functions($requests);
 510  
 511          // We need to execute the return values cleaning process to simulate the web service server.
 512          $result = \external_api::clean_returnvalue(external::call_external_functions_returns(), $result);
 513  
 514          // Only 3 responses, the 4th request is not executed because the 3rd throws an exception.
 515          $this->assertCount(3, $result['responses']);
 516  
 517          $this->assertFalse($result['responses'][0]['error']);
 518          $coursedata = \external_api::clean_returnvalue(
 519              \core_course_external::get_courses_by_field_returns(),
 520              \core_course_external::get_courses_by_field('id', $course->id));
 521           $this->assertEquals(json_encode($coursedata), $result['responses'][0]['data']);
 522  
 523          $this->assertFalse($result['responses'][1]['error']);
 524          $userdata = \external_api::clean_returnvalue(
 525              \core_user_external::get_users_by_field_returns(),
 526              \core_user_external::get_users_by_field('id', [$user1->id]));
 527          $this->assertEquals(json_encode($userdata), $result['responses'][1]['data']);
 528  
 529          $this->assertTrue($result['responses'][2]['error']);
 530          $exception = json_decode($result['responses'][2]['exception'], true);
 531          $this->assertEquals('nopermissions', $exception['errorcode']);
 532  
 533          // Call a function not included in the external service.
 534  
 535          $_POST['wstoken'] = $token->token;
 536          $functions = $webservicemanager->get_not_associated_external_functions($service->id);
 537          $requests = [['function' => current($functions)->name]];
 538          $result = external::call_external_functions($requests);
 539  
 540          $this->assertTrue($result['responses'][0]['error']);
 541          $exception = json_decode($result['responses'][0]['exception'], true);
 542          $this->assertEquals('accessexception', $exception['errorcode']);
 543          $this->assertEquals('webservice', $exception['module']);
 544  
 545          // Call a function with different external settings.
 546  
 547          filter_set_global_state('multilang', TEXTFILTER_ON);
 548          $_POST['wstoken'] = $token->token;
 549          $SESSION->lang = 'eo'; // Change default language, so we can test changing it to "en".
 550          $requests = [
 551              [
 552                  'function' => 'core_course_get_courses_by_field',
 553                  'arguments' => json_encode(['field' => 'id', 'value' => $course->id]),
 554              ],
 555              [
 556                  'function' => 'core_course_get_courses_by_field',
 557                  'arguments' => json_encode(['field' => 'id', 'value' => $course->id]),
 558                  'settingraw' => '1'
 559              ],
 560              [
 561                  'function' => 'core_course_get_courses_by_field',
 562                  'arguments' => json_encode(['field' => 'id', 'value' => $course->id]),
 563                  'settingraw' => '1',
 564                  'settingfileurl' => '0'
 565              ],
 566              [
 567                  'function' => 'core_course_get_courses_by_field',
 568                  'arguments' => json_encode(['field' => 'id', 'value' => $course->id]),
 569                  'settingfilter' => '1',
 570                  'settinglang' => 'en'
 571              ],
 572          ];
 573          $result = external::call_external_functions($requests);
 574  
 575          $this->assertCount(4, $result['responses']);
 576  
 577          $context = \context_course::instance($course->id);
 578          $pluginfile = 'webservice/pluginfile.php';
 579  
 580          $this->assertFalse($result['responses'][0]['error']);
 581          $data = json_decode($result['responses'][0]['data']);
 582          $expected = file_rewrite_pluginfile_urls($course->summary, $pluginfile, $context->id, 'course', 'summary', null);
 583          $expected = format_text($expected, $course->summaryformat, ['para' => false, 'filter' => false]);
 584          $this->assertEquals($expected, $data->courses[0]->summary);
 585  
 586          $this->assertFalse($result['responses'][1]['error']);
 587          $data = json_decode($result['responses'][1]['data']);
 588          $expected = file_rewrite_pluginfile_urls($course->summary, $pluginfile, $context->id, 'course', 'summary', null);
 589          $this->assertEquals($expected, $data->courses[0]->summary);
 590  
 591          $this->assertFalse($result['responses'][2]['error']);
 592          $data = json_decode($result['responses'][2]['data']);
 593          $this->assertEquals($course->summary, $data->courses[0]->summary);
 594  
 595          $this->assertFalse($result['responses'][3]['error']);
 596          $data = json_decode($result['responses'][3]['data']);
 597          $expected = file_rewrite_pluginfile_urls($course->summary, $pluginfile, $context->id, 'course', 'summary', null);
 598          $SESSION->lang = 'en'; // We expect filtered text in english.
 599          $expected = format_text($expected, $course->summaryformat, ['para' => false, 'filter' => true]);
 600          $this->assertEquals($expected, $data->courses[0]->summary);
 601      }
 602  
 603      /*
 604       * Test get_tokens_for_qr_login.
 605       */
 606      public function test_get_tokens_for_qr_login() {
 607          global $DB, $CFG, $USER;
 608  
 609          $this->resetAfterTest(true);
 610  
 611          $user = $this->getDataGenerator()->create_user();
 612          $this->setUser($user);
 613  
 614          $qrloginkey = api::get_qrlogin_key();
 615  
 616          // Generate new tokens, the ones we expect to receive.
 617          $service = $DB->get_record('external_services', array('shortname' => MOODLE_OFFICIAL_MOBILE_SERVICE));
 618          $token = external_generate_token_for_current_user($service);
 619  
 620          // Fake the app.
 621          \core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
 622                  'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');
 623  
 624          $result = external::get_tokens_for_qr_login($qrloginkey, $USER->id);
 625          $result = \external_api::clean_returnvalue(external::get_tokens_for_qr_login_returns(), $result);
 626  
 627          $this->assertEmpty($result['warnings']);
 628          $this->assertEquals($token->token, $result['token']);
 629          $this->assertEquals($token->privatetoken, $result['privatetoken']);
 630  
 631          // Now, try with an invalid key.
 632          $this->expectException('moodle_exception');
 633          $this->expectExceptionMessage(get_string('invalidkey', 'error'));
 634          $result = external::get_tokens_for_qr_login(random_string('64'), $user->id);
 635      }
 636  
 637      /**
 638       * Test get_tokens_for_qr_login missing QR code enabled.
 639       */
 640      public function test_get_tokens_for_qr_login_missing_enableqr() {
 641          global $CFG, $USER;
 642          $this->resetAfterTest(true);
 643          $this->setAdminUser();
 644  
 645          set_config('qrcodetype', api::QR_CODE_DISABLED, 'tool_mobile');
 646  
 647          $this->expectExceptionMessage(get_string('qrcodedisabled', 'tool_mobile'));
 648          $result = external::get_tokens_for_qr_login('', $USER->id);
 649      }
 650  
 651      /**
 652       * Test get_tokens_for_qr_login missing ws.
 653       */
 654      public function test_get_tokens_for_qr_login_missing_ws() {
 655          global $CFG;
 656          $this->resetAfterTest(true);
 657  
 658          $user = $this->getDataGenerator()->create_user();
 659          $this->setUser($user);
 660  
 661          // Fake the app.
 662          \core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
 663              'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');
 664  
 665          // Need to disable webservices to verify that's checked.
 666          $CFG->enablewebservices = 0;
 667          $CFG->enablemobilewebservice = 0;
 668  
 669          $this->setAdminUser();
 670          $this->expectException('moodle_exception');
 671          $this->expectExceptionMessage(get_string('enablewsdescription', 'webservice'));
 672          $result = external::get_tokens_for_qr_login('', $user->id);
 673      }
 674  
 675      /**
 676       * Test get_tokens_for_qr_login missing https.
 677       */
 678      public function test_get_tokens_for_qr_login_missing_https() {
 679          global $CFG, $USER;
 680  
 681          // Fake the app.
 682          \core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
 683              'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');
 684  
 685          // Need to simulate a non HTTPS site here.
 686          $CFG->wwwroot = str_replace('https:', 'http:', $CFG->wwwroot);
 687  
 688          $this->resetAfterTest(true);
 689          $this->setAdminUser();
 690  
 691          $this->expectException('moodle_exception');
 692          $this->expectExceptionMessage(get_string('httpsrequired', 'tool_mobile'));
 693          $result = external::get_tokens_for_qr_login('', $USER->id);
 694      }
 695  
 696      /**
 697       * Test get_tokens_for_qr_login missing admin.
 698       */
 699      public function test_get_tokens_for_qr_login_missing_admin() {
 700          global $CFG, $USER;
 701  
 702          $this->resetAfterTest(true);
 703          $this->setAdminUser();
 704  
 705          // Fake the app.
 706          \core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
 707              'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');
 708  
 709          $this->expectException('moodle_exception');
 710          $this->expectExceptionMessage(get_string('autologinnotallowedtoadmins', 'tool_mobile'));
 711          $result = external::get_tokens_for_qr_login('', $USER->id);
 712      }
 713  
 714      /**
 715       * Test get_tokens_for_qr_login missing app_request.
 716       */
 717      public function test_get_tokens_for_qr_login_missing_app_request() {
 718          global $CFG, $USER;
 719  
 720          $this->resetAfterTest(true);
 721          $this->setAdminUser();
 722  
 723          $this->expectException('moodle_exception');
 724          $this->expectExceptionMessage(get_string('apprequired', 'tool_mobile'));
 725          $result = external::get_tokens_for_qr_login('', $USER->id);
 726      }
 727  
 728      /**
 729       * Test validate subscription key.
 730       */
 731      public function test_validate_subscription_key_valid() {
 732          $this->resetAfterTest(true);
 733  
 734          $sitesubscriptionkey = ['validuntil' => time() + MINSECS, 'key' => complex_random_string(32)];
 735          set_config('sitesubscriptionkey', json_encode($sitesubscriptionkey), 'tool_mobile');
 736  
 737          $result = external::validate_subscription_key($sitesubscriptionkey['key']);
 738          $result = \external_api::clean_returnvalue(external::validate_subscription_key_returns(), $result);
 739          $this->assertEmpty($result['warnings']);
 740          $this->assertTrue($result['validated']);
 741      }
 742  
 743      /**
 744       * Test validate subscription key invalid first and then a valid one.
 745       */
 746      public function test_validate_subscription_key_invalid_key_first() {
 747          $this->resetAfterTest(true);
 748  
 749          $sitesubscriptionkey = ['validuntil' => time() + MINSECS, 'key' => complex_random_string(32)];
 750          set_config('sitesubscriptionkey', json_encode($sitesubscriptionkey), 'tool_mobile');
 751  
 752          $result = external::validate_subscription_key('fakekey');
 753          $result = \external_api::clean_returnvalue(external::validate_subscription_key_returns(), $result);
 754          $this->assertEmpty($result['warnings']);
 755          $this->assertFalse($result['validated']);
 756  
 757          // The valid one has been invalidated because the previous attempt.
 758          $result = external::validate_subscription_key($sitesubscriptionkey['key']);
 759          $result = \external_api::clean_returnvalue(external::validate_subscription_key_returns(), $result);
 760          $this->assertEmpty($result['warnings']);
 761          $this->assertFalse($result['validated']);
 762      }
 763  
 764      /**
 765       * Test validate subscription key invalid.
 766       */
 767      public function test_validate_subscription_key_invalid_key() {
 768          $this->resetAfterTest(true);
 769  
 770          $result = external::validate_subscription_key('fakekey');
 771          $result = \external_api::clean_returnvalue(external::validate_subscription_key_returns(), $result);
 772          $this->assertEmpty($result['warnings']);
 773          $this->assertFalse($result['validated']);
 774      }
 775  
 776      /**
 777       * Test validate subscription key invalid.
 778       */
 779      public function test_validate_subscription_key_outdated() {
 780          $this->resetAfterTest(true);
 781  
 782          $sitesubscriptionkey = ['validuntil' => time() - MINSECS, 'key' => complex_random_string(32)];
 783          set_config('sitesubscriptionkey', json_encode($sitesubscriptionkey), 'tool_mobile');
 784  
 785          $result = external::validate_subscription_key($sitesubscriptionkey['key']);
 786          $result = \external_api::clean_returnvalue(external::validate_subscription_key_returns(), $result);
 787          $this->assertEmpty($result['warnings']);
 788          $this->assertFalse($result['validated']);
 789      }
 790  }