Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

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