Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

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