Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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

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