Search moodle.org's
Developer Documentation

See Release Notes

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

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