Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

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