Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 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 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]

   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->assertStringContainsString($CFG->wwwroot, $identityproviders[0]['url']);
 162  
 163          $this->assertEquals('CAS', $identityproviders[1]['name']);
 164          $this->assertEmpty($identityproviders[1]['iconurl']);
 165          $this->assertStringContainsString($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->assertStringContainsString($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_filetypeexclusionlist', 'value' => ''),
 217              array('name' => 'tool_mobile_custommenuitems', 'value' => ''),
 218              array('name' => 'tool_mobile_apppolicy', 'value' => ''),
 219              array('name' => 'calendartype', 'value' => $CFG->calendartype),
 220              array('name' => 'calendar_site_timeformat', 'value' => $CFG->calendar_site_timeformat),
 221              array('name' => 'calendar_startwday', 'value' => $CFG->calendar_startwday),
 222              array('name' => 'calendar_adminseesall', 'value' => $CFG->calendar_adminseesall),
 223              array('name' => 'calendar_lookahead', 'value' => $CFG->calendar_lookahead),
 224              array('name' => 'calendar_maxevents', 'value' => $CFG->calendar_maxevents),
 225          );
 226          $colornumbers = range(1, 10);
 227          foreach ($colornumbers as $number) {
 228              $expected[] = [
 229                  'name' => 'core_admin_coursecolor' . $number,
 230                  'value' => get_config('core_admin', 'coursecolor' . $number)
 231              ];
 232          }
 233          $expected[] = ['name' => 'supportname', 'value' => $CFG->supportname];
 234          $expected[] = ['name' => 'supportemail', 'value' => $CFG->supportemail];
 235          $expected[] = ['name' => 'supportpage', 'value' => $CFG->supportpage];
 236  
 237          $expected[] = ['name' => 'coursegraceperiodafter', 'value' => $CFG->coursegraceperiodafter];
 238          $expected[] = ['name' => 'coursegraceperiodbefore', 'value' => $CFG->coursegraceperiodbefore];
 239  
 240          $this->assertCount(0, $result['warnings']);
 241          $this->assertEquals($expected, $result['settings']);
 242  
 243          // Change a value and retrieve filtering by section.
 244          set_config('commentsperpage', 1);
 245          $expected[10]['value'] = 1;
 246          // Remove not expected elements.
 247          array_splice($expected, 11);
 248  
 249          $result = external::get_config('frontpagesettings');
 250          $result = external_api::clean_returnvalue(external::get_config_returns(), $result);
 251          $this->assertCount(0, $result['warnings']);
 252          $this->assertEquals($expected, $result['settings']);
 253      }
 254  
 255      /*
 256       * Test get_autologin_key.
 257       */
 258      public function test_get_autologin_key() {
 259          global $DB, $CFG, $USER;
 260  
 261          $this->resetAfterTest(true);
 262  
 263          $user = $this->getDataGenerator()->create_user();
 264          $this->setUser($user);
 265          $service = $DB->get_record('external_services', array('shortname' => MOODLE_OFFICIAL_MOBILE_SERVICE));
 266  
 267          $token = external_generate_token_for_current_user($service);
 268  
 269          // Check we got the private token.
 270          $this->assertTrue(isset($token->privatetoken));
 271  
 272          // Enable requeriments.
 273          $_GET['wstoken'] = $token->token;   // Mock parameters.
 274  
 275          // Fake the app.
 276          core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
 277                  'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');
 278  
 279          // Even if we force the password change for the current user we should be able to retrieve the key.
 280          set_user_preference('auth_forcepasswordchange', 1, $user->id);
 281  
 282          $this->setCurrentTimeStart();
 283          $result = external::get_autologin_key($token->privatetoken);
 284          $result = external_api::clean_returnvalue(external::get_autologin_key_returns(), $result);
 285          // Validate the key.
 286          $this->assertEquals(32, core_text::strlen($result['key']));
 287          $key = $DB->get_record('user_private_key', array('value' => $result['key']));
 288          $this->assertEquals($USER->id, $key->userid);
 289          $this->assertTimeCurrent($key->validuntil - api::LOGIN_KEY_TTL);
 290  
 291          // Now, try with an invalid private token.
 292          set_user_preference('tool_mobile_autologin_request_last', time() - HOURSECS, $USER);
 293  
 294          $this->expectException('moodle_exception');
 295          $this->expectExceptionMessage(get_string('invalidprivatetoken', 'tool_mobile'));
 296          $result = external::get_autologin_key(random_string('64'));
 297      }
 298  
 299      /**
 300       * Test get_autologin_key missing ws.
 301       */
 302      public function test_get_autologin_key_missing_ws() {
 303          global $CFG;
 304          $this->resetAfterTest(true);
 305  
 306          // Fake the app.
 307          core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
 308              'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');
 309  
 310          // Need to disable webservices to verify that's checked.
 311          $CFG->enablewebservices = 0;
 312          $CFG->enablemobilewebservice = 0;
 313  
 314          $this->setAdminUser();
 315          $this->expectException('moodle_exception');
 316          $this->expectExceptionMessage(get_string('enablewsdescription', 'webservice'));
 317          $result = external::get_autologin_key('');
 318      }
 319  
 320      /**
 321       * Test get_autologin_key missing https.
 322       */
 323      public function test_get_autologin_key_missing_https() {
 324          global $CFG;
 325  
 326          // Fake the app.
 327          core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
 328              'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');
 329  
 330          // Need to simulate a non HTTPS site here.
 331          $CFG->wwwroot = str_replace('https:', 'http:', $CFG->wwwroot);
 332  
 333          $this->resetAfterTest(true);
 334          $this->setAdminUser();
 335  
 336          $this->expectException('moodle_exception');
 337          $this->expectExceptionMessage(get_string('httpsrequired', 'tool_mobile'));
 338          $result = external::get_autologin_key('');
 339      }
 340  
 341      /**
 342       * Test get_autologin_key missing admin.
 343       */
 344      public function test_get_autologin_key_missing_admin() {
 345          global $CFG;
 346  
 347          $this->resetAfterTest(true);
 348          $this->setAdminUser();
 349  
 350          // Fake the app.
 351          core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
 352              'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');
 353  
 354          $this->expectException('moodle_exception');
 355          $this->expectExceptionMessage(get_string('autologinnotallowedtoadmins', 'tool_mobile'));
 356          $result = external::get_autologin_key('');
 357      }
 358  
 359      /**
 360       * Test get_autologin_key locked.
 361       */
 362      public function test_get_autologin_key_missing_locked() {
 363          global $CFG, $DB, $USER;
 364  
 365          $this->resetAfterTest(true);
 366          $user = $this->getDataGenerator()->create_user();
 367          $this->setUser($user);
 368  
 369          $service = $DB->get_record('external_services', array('shortname' => MOODLE_OFFICIAL_MOBILE_SERVICE));
 370  
 371          $token = external_generate_token_for_current_user($service);
 372          $_GET['wstoken'] = $token->token;   // Mock parameters.
 373  
 374          // Fake the app.
 375          core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
 376              'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');
 377  
 378          $result = external::get_autologin_key($token->privatetoken);
 379          $result = external_api::clean_returnvalue(external::get_autologin_key_returns(), $result);
 380  
 381          // Mock last time request.
 382          $mocktime = time() - 7 * MINSECS;
 383          set_user_preference('tool_mobile_autologin_request_last', $mocktime, $USER);
 384          $result = external::get_autologin_key($token->privatetoken);
 385          $result = external_api::clean_returnvalue(external::get_autologin_key_returns(), $result);
 386  
 387          // We just requested one token, we must wait.
 388          $this->expectException('moodle_exception');
 389          $this->expectExceptionMessage(get_string('autologinkeygenerationlockout', 'tool_mobile'));
 390          $result = external::get_autologin_key($token->privatetoken);
 391      }
 392  
 393      /**
 394       * Test get_autologin_key missing app_request.
 395       */
 396      public function test_get_autologin_key_missing_app_request() {
 397          global $CFG;
 398  
 399          $this->resetAfterTest(true);
 400          $this->setAdminUser();
 401  
 402          $this->expectException('moodle_exception');
 403          $this->expectExceptionMessage(get_string('apprequired', 'tool_mobile'));
 404          $result = external::get_autologin_key('');
 405      }
 406  
 407      /**
 408       * Test get_content.
 409       */
 410      public function test_get_content() {
 411  
 412          $paramval = 16;
 413          $result = external::get_content('tool_mobile', 'test_view', array(array('name' => 'param1', 'value' => $paramval)));
 414          $result = external_api::clean_returnvalue(external::get_content_returns(), $result);
 415          $this->assertCount(1, $result['templates']);
 416          $this->assertCount(1, $result['otherdata']);
 417          $this->assertCount(2, $result['restrict']['users']);
 418          $this->assertCount(2, $result['restrict']['courses']);
 419          $this->assertEquals('alert();', $result['javascript']);
 420          $this->assertEquals('main', $result['templates'][0]['id']);
 421          $this->assertEquals('The HTML code', $result['templates'][0]['html']);
 422          $this->assertEquals('otherdata1', $result['otherdata'][0]['name']);
 423          $this->assertEquals($paramval, $result['otherdata'][0]['value']);
 424          $this->assertEquals(array(1, 2), $result['restrict']['users']);
 425          $this->assertEquals(array(3, 4), $result['restrict']['courses']);
 426          $this->assertEmpty($result['files']);
 427          $this->assertFalse($result['disabled']);
 428      }
 429  
 430      /**
 431       * Test get_content disabled.
 432       */
 433      public function test_get_content_disabled() {
 434  
 435          $paramval = 16;
 436          $result = external::get_content('tool_mobile', 'test_view_disabled',
 437              array(array('name' => 'param1', 'value' => $paramval)));
 438          $result = external_api::clean_returnvalue(external::get_content_returns(), $result);
 439          $this->assertTrue($result['disabled']);
 440      }
 441  
 442      /**
 443       * Test get_content non existent function in valid component.
 444       */
 445      public function test_get_content_non_existent_function() {
 446  
 447          $this->expectException('coding_exception');
 448          $result = external::get_content('tool_mobile', 'test_blahblah');
 449      }
 450  
 451      /**
 452       * Test get_content incorrect component.
 453       */
 454      public function test_get_content_invalid_component() {
 455  
 456          $this->expectException('moodle_exception');
 457          $result = external::get_content('tool_mobile\hack', 'test_view');
 458      }
 459  
 460      /**
 461       * Test get_content non existent component.
 462       */
 463      public function test_get_content_non_existent_component() {
 464  
 465          $this->expectException('moodle_exception');
 466          $result = external::get_content('tool_blahblahblah', 'test_view');
 467      }
 468  
 469      public function test_call_external_functions() {
 470          global $SESSION;
 471  
 472          $this->resetAfterTest(true);
 473  
 474          $category = self::getDataGenerator()->create_category(array('name' => 'Category 1'));
 475          $course = self::getDataGenerator()->create_course([
 476              'category' => $category->id,
 477              'shortname' => 'c1',
 478              'summary' => '<span lang="en" class="multilang">Course summary</span>'
 479                  . '<span lang="eo" class="multilang">Kurso resumo</span>'
 480                  . '@@PLUGINFILE@@/filename.txt'
 481                  . '<!-- Comment stripped when formatting text -->',
 482              'summaryformat' => FORMAT_MOODLE
 483          ]);
 484          $user1 = self::getDataGenerator()->create_user(['username' => 'user1', 'lastaccess' => time()]);
 485          $user2 = self::getDataGenerator()->create_user(['username' => 'user2', 'lastaccess' => time()]);
 486  
 487          self::setUser($user1);
 488  
 489          // Setup WS token.
 490          $webservicemanager = new \webservice;
 491          $service = $webservicemanager->get_external_service_by_shortname(MOODLE_OFFICIAL_MOBILE_SERVICE);
 492          $token = external_generate_token_for_current_user($service);
 493          $_POST['wstoken'] = $token->token;
 494  
 495          // Workaround for external_api::call_external_function requiring sesskey.
 496          $_POST['sesskey'] = sesskey();
 497  
 498          // Call some functions.
 499  
 500          $requests = [
 501              [
 502                  'function' => 'core_course_get_courses_by_field',
 503                  'arguments' => json_encode(['field' => 'id', 'value' => $course->id])
 504              ],
 505              [
 506                  'function' => 'core_user_get_users_by_field',
 507                  'arguments' => json_encode(['field' => 'id', 'values' => [$user1->id]])
 508              ],
 509              [
 510                  'function' => 'core_user_get_user_preferences',
 511                  'arguments' => json_encode(['name' => 'some_setting', 'userid' => $user2->id])
 512              ],
 513              [
 514                  'function' => 'core_course_get_courses_by_field',
 515                  'arguments' => json_encode(['field' => 'shortname', 'value' => $course->shortname])
 516              ],
 517          ];
 518          $result = external::call_external_functions($requests);
 519  
 520          // We need to execute the return values cleaning process to simulate the web service server.
 521          $result = external_api::clean_returnvalue(external::call_external_functions_returns(), $result);
 522  
 523          // Only 3 responses, the 4th request is not executed because the 3rd throws an exception.
 524          $this->assertCount(3, $result['responses']);
 525  
 526          $this->assertFalse($result['responses'][0]['error']);
 527          $coursedata = external_api::clean_returnvalue(
 528              core_course_external::get_courses_by_field_returns(),
 529              core_course_external::get_courses_by_field('id', $course->id));
 530           $this->assertEquals(json_encode($coursedata), $result['responses'][0]['data']);
 531  
 532          $this->assertFalse($result['responses'][1]['error']);
 533          $userdata = external_api::clean_returnvalue(
 534              core_user_external::get_users_by_field_returns(),
 535              core_user_external::get_users_by_field('id', [$user1->id]));
 536          $this->assertEquals(json_encode($userdata), $result['responses'][1]['data']);
 537  
 538          $this->assertTrue($result['responses'][2]['error']);
 539          $exception = json_decode($result['responses'][2]['exception'], true);
 540          $this->assertEquals('nopermissions', $exception['errorcode']);
 541  
 542          // Call a function not included in the external service.
 543  
 544          $_POST['wstoken'] = $token->token;
 545          $functions = $webservicemanager->get_not_associated_external_functions($service->id);
 546          $requests = [['function' => current($functions)->name]];
 547          $result = external::call_external_functions($requests);
 548  
 549          $this->assertTrue($result['responses'][0]['error']);
 550          $exception = json_decode($result['responses'][0]['exception'], true);
 551          $this->assertEquals('accessexception', $exception['errorcode']);
 552          $this->assertEquals('webservice', $exception['module']);
 553  
 554          // Call a function with different external settings.
 555  
 556          filter_set_global_state('multilang', TEXTFILTER_ON);
 557          $_POST['wstoken'] = $token->token;
 558          $SESSION->lang = 'eo'; // Change default language, so we can test changing it to "en".
 559          $requests = [
 560              [
 561                  'function' => 'core_course_get_courses_by_field',
 562                  'arguments' => json_encode(['field' => 'id', 'value' => $course->id]),
 563              ],
 564              [
 565                  'function' => 'core_course_get_courses_by_field',
 566                  'arguments' => json_encode(['field' => 'id', 'value' => $course->id]),
 567                  'settingraw' => '1'
 568              ],
 569              [
 570                  'function' => 'core_course_get_courses_by_field',
 571                  'arguments' => json_encode(['field' => 'id', 'value' => $course->id]),
 572                  'settingraw' => '1',
 573                  'settingfileurl' => '0'
 574              ],
 575              [
 576                  'function' => 'core_course_get_courses_by_field',
 577                  'arguments' => json_encode(['field' => 'id', 'value' => $course->id]),
 578                  'settingfilter' => '1',
 579                  'settinglang' => 'en'
 580              ],
 581          ];
 582          $result = external::call_external_functions($requests);
 583  
 584          $this->assertCount(4, $result['responses']);
 585  
 586          $context = \context_course::instance($course->id);
 587          $pluginfile = 'webservice/pluginfile.php';
 588  
 589          $this->assertFalse($result['responses'][0]['error']);
 590          $data = json_decode($result['responses'][0]['data']);
 591          $expected = file_rewrite_pluginfile_urls($course->summary, $pluginfile, $context->id, 'course', 'summary', null);
 592          $expected = format_text($expected, $course->summaryformat, ['para' => false, 'filter' => false]);
 593          $this->assertEquals($expected, $data->courses[0]->summary);
 594  
 595          $this->assertFalse($result['responses'][1]['error']);
 596          $data = json_decode($result['responses'][1]['data']);
 597          $expected = file_rewrite_pluginfile_urls($course->summary, $pluginfile, $context->id, 'course', 'summary', null);
 598          $this->assertEquals($expected, $data->courses[0]->summary);
 599  
 600          $this->assertFalse($result['responses'][2]['error']);
 601          $data = json_decode($result['responses'][2]['data']);
 602          $this->assertEquals($course->summary, $data->courses[0]->summary);
 603  
 604          $this->assertFalse($result['responses'][3]['error']);
 605          $data = json_decode($result['responses'][3]['data']);
 606          $expected = file_rewrite_pluginfile_urls($course->summary, $pluginfile, $context->id, 'course', 'summary', null);
 607          $SESSION->lang = 'en'; // We expect filtered text in english.
 608          $expected = format_text($expected, $course->summaryformat, ['para' => false, 'filter' => true]);
 609          $this->assertEquals($expected, $data->courses[0]->summary);
 610      }
 611  
 612      /*
 613       * Test get_tokens_for_qr_login.
 614       */
 615      public function test_get_tokens_for_qr_login() {
 616          global $DB, $CFG, $USER;
 617  
 618          $this->resetAfterTest(true);
 619  
 620          $user = $this->getDataGenerator()->create_user();
 621          $this->setUser($user);
 622  
 623          $qrloginkey = api::get_qrlogin_key();
 624  
 625          // Generate new tokens, the ones we expect to receive.
 626          $service = $DB->get_record('external_services', array('shortname' => MOODLE_OFFICIAL_MOBILE_SERVICE));
 627          $token = external_generate_token_for_current_user($service);
 628  
 629          // Fake the app.
 630          core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
 631                  'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');
 632  
 633          $result = external::get_tokens_for_qr_login($qrloginkey, $USER->id);
 634          $result = external_api::clean_returnvalue(external::get_tokens_for_qr_login_returns(), $result);
 635  
 636          $this->assertEmpty($result['warnings']);
 637          $this->assertEquals($token->token, $result['token']);
 638          $this->assertEquals($token->privatetoken, $result['privatetoken']);
 639  
 640          // Now, try with an invalid key.
 641          $this->expectException('moodle_exception');
 642          $this->expectExceptionMessage(get_string('invalidkey', 'error'));
 643          $result = external::get_tokens_for_qr_login(random_string('64'), $user->id);
 644      }
 645  
 646      /**
 647       * Test get_tokens_for_qr_login missing QR code enabled.
 648       */
 649      public function test_get_tokens_for_qr_login_missing_enableqr() {
 650          global $CFG, $USER;
 651          $this->resetAfterTest(true);
 652          $this->setAdminUser();
 653  
 654          set_config('qrcodetype', tool_mobile\api::QR_CODE_DISABLED, 'tool_mobile');
 655  
 656          $this->expectExceptionMessage(get_string('qrcodedisabled', 'tool_mobile'));
 657          $result = external::get_tokens_for_qr_login('', $USER->id);
 658      }
 659  
 660      /**
 661       * Test get_tokens_for_qr_login missing ws.
 662       */
 663      public function test_get_tokens_for_qr_login_missing_ws() {
 664          global $CFG;
 665          $this->resetAfterTest(true);
 666  
 667          $user = $this->getDataGenerator()->create_user();
 668          $this->setUser($user);
 669  
 670          // Fake the app.
 671          core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
 672              'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');
 673  
 674          // Need to disable webservices to verify that's checked.
 675          $CFG->enablewebservices = 0;
 676          $CFG->enablemobilewebservice = 0;
 677  
 678          $this->setAdminUser();
 679          $this->expectException('moodle_exception');
 680          $this->expectExceptionMessage(get_string('enablewsdescription', 'webservice'));
 681          $result = external::get_tokens_for_qr_login('', $user->id);
 682      }
 683  
 684      /**
 685       * Test get_tokens_for_qr_login missing https.
 686       */
 687      public function test_get_tokens_for_qr_login_missing_https() {
 688          global $CFG, $USER;
 689  
 690          // Fake the app.
 691          core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
 692              'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');
 693  
 694          // Need to simulate a non HTTPS site here.
 695          $CFG->wwwroot = str_replace('https:', 'http:', $CFG->wwwroot);
 696  
 697          $this->resetAfterTest(true);
 698          $this->setAdminUser();
 699  
 700          $this->expectException('moodle_exception');
 701          $this->expectExceptionMessage(get_string('httpsrequired', 'tool_mobile'));
 702          $result = external::get_tokens_for_qr_login('', $USER->id);
 703      }
 704  
 705      /**
 706       * Test get_tokens_for_qr_login missing admin.
 707       */
 708      public function test_get_tokens_for_qr_login_missing_admin() {
 709          global $CFG, $USER;
 710  
 711          $this->resetAfterTest(true);
 712          $this->setAdminUser();
 713  
 714          // Fake the app.
 715          core_useragent::instance(true, 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) ' .
 716              'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile');
 717  
 718          $this->expectException('moodle_exception');
 719          $this->expectExceptionMessage(get_string('autologinnotallowedtoadmins', 'tool_mobile'));
 720          $result = external::get_tokens_for_qr_login('', $USER->id);
 721      }
 722  
 723      /**
 724       * Test get_tokens_for_qr_login missing app_request.
 725       */
 726      public function test_get_tokens_for_qr_login_missing_app_request() {
 727          global $CFG, $USER;
 728  
 729          $this->resetAfterTest(true);
 730          $this->setAdminUser();
 731  
 732          $this->expectException('moodle_exception');
 733          $this->expectExceptionMessage(get_string('apprequired', 'tool_mobile'));
 734          $result = external::get_tokens_for_qr_login('', $USER->id);
 735      }
 736  
 737      /**
 738       * Test validate subscription key.
 739       */
 740      public function test_validate_subscription_key_valid() {
 741          $this->resetAfterTest(true);
 742  
 743          $sitesubscriptionkey = ['validuntil' => time() + MINSECS, 'key' => complex_random_string(32)];
 744          set_config('sitesubscriptionkey', json_encode($sitesubscriptionkey), 'tool_mobile');
 745  
 746          $result = external::validate_subscription_key($sitesubscriptionkey['key']);
 747          $result = external_api::clean_returnvalue(external::validate_subscription_key_returns(), $result);
 748          $this->assertEmpty($result['warnings']);
 749          $this->assertTrue($result['validated']);
 750      }
 751  
 752      /**
 753       * Test validate subscription key invalid first and then a valid one.
 754       */
 755      public function test_validate_subscription_key_invalid_key_first() {
 756          $this->resetAfterTest(true);
 757  
 758          $sitesubscriptionkey = ['validuntil' => time() + MINSECS, 'key' => complex_random_string(32)];
 759          set_config('sitesubscriptionkey', json_encode($sitesubscriptionkey), 'tool_mobile');
 760  
 761          $result = external::validate_subscription_key('fakekey');
 762          $result = external_api::clean_returnvalue(external::validate_subscription_key_returns(), $result);
 763          $this->assertEmpty($result['warnings']);
 764          $this->assertFalse($result['validated']);
 765  
 766          // The valid one has been invalidated because the previous attempt.
 767          $result = external::validate_subscription_key($sitesubscriptionkey['key']);
 768          $result = external_api::clean_returnvalue(external::validate_subscription_key_returns(), $result);
 769          $this->assertEmpty($result['warnings']);
 770          $this->assertFalse($result['validated']);
 771      }
 772  
 773      /**
 774       * Test validate subscription key invalid.
 775       */
 776      public function test_validate_subscription_key_invalid_key() {
 777          $this->resetAfterTest(true);
 778  
 779          $result = external::validate_subscription_key('fakekey');
 780          $result = external_api::clean_returnvalue(external::validate_subscription_key_returns(), $result);
 781          $this->assertEmpty($result['warnings']);
 782          $this->assertFalse($result['validated']);
 783      }
 784  
 785      /**
 786       * Test validate subscription key invalid.
 787       */
 788      public function test_validate_subscription_key_outdated() {
 789          $this->resetAfterTest(true);
 790  
 791          $sitesubscriptionkey = ['validuntil' => time() - MINSECS, 'key' => complex_random_string(32)];
 792          set_config('sitesubscriptionkey', json_encode($sitesubscriptionkey), 'tool_mobile');
 793  
 794          $result = external::validate_subscription_key($sitesubscriptionkey['key']);
 795          $result = external_api::clean_returnvalue(external::validate_subscription_key_returns(), $result);
 796          $this->assertEmpty($result['warnings']);
 797          $this->assertFalse($result['validated']);
 798      }
 799  }