Search moodle.org's
Developer Documentation

See Release Notes

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

Differences Between: [Versions 310 and 400] [Versions 311 and 400] [Versions 39 and 400] [Versions 400 and 401] [Versions 400 and 402] [Versions 400 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  namespace core;
  18  
  19  use lang_string;
  20  
  21  /**
  22   * Unit tests for (some of) ../moodlelib.php.
  23   *
  24   * @package    core
  25   * @category   phpunit
  26   * @copyright  &copy; 2006 The Open University
  27   * @author     T.J.Hunt@open.ac.uk
  28   * @author     nicolas@moodle.com
  29   */
  30  class moodlelib_test extends \advanced_testcase {
  31  
  32      /**
  33       * Define a local decimal separator.
  34       *
  35       * It is not possible to directly change the result of get_string in
  36       * a unit test. Instead, we create a language pack for language 'xx' in
  37       * dataroot and make langconfig.php with the string we need to change.
  38       * The default example separator used here is 'X'; on PHP 5.3 and before this
  39       * must be a single byte character due to PHP bug/limitation in
  40       * number_format, so you can't use UTF-8 characters.
  41       *
  42       * @param string $decsep Separator character. Defaults to `'X'`.
  43       */
  44      protected function define_local_decimal_separator(string $decsep = 'X') {
  45          global $SESSION, $CFG;
  46  
  47          $SESSION->lang = 'xx';
  48          $langconfig = "<?php\n\$string['decsep'] = '$decsep';";
  49          $langfolder = $CFG->dataroot . '/lang/xx';
  50          check_dir_exists($langfolder);
  51          file_put_contents($langfolder . '/langconfig.php', $langconfig);
  52  
  53          // Ensure the new value is picked up and not taken from the cache.
  54          $stringmanager = get_string_manager();
  55          $stringmanager->reset_caches(true);
  56      }
  57  
  58      public function test_cleanremoteaddr() {
  59          // IPv4.
  60          $this->assertNull(cleanremoteaddr('1023.121.234.1'));
  61          $this->assertSame('123.121.234.1', cleanremoteaddr('123.121.234.01 '));
  62  
  63          // IPv6.
  64          $this->assertNull(cleanremoteaddr('0:0:0:0:0:0:0:0:0'));
  65          $this->assertNull(cleanremoteaddr('0:0:0:0:0:0:0:abh'));
  66          $this->assertNull(cleanremoteaddr('0:0:0:::0:0:1'));
  67          $this->assertSame('::', cleanremoteaddr('0:0:0:0:0:0:0:0', true));
  68          $this->assertSame('::1:1', cleanremoteaddr('0:0:0:0:0:0:1:1', true));
  69          $this->assertSame('abcd:ef::', cleanremoteaddr('abcd:00ef:0:0:0:0:0:0', true));
  70          $this->assertSame('1::1', cleanremoteaddr('1:0:0:0:0:0:0:1', true));
  71          $this->assertSame('0:0:0:0:0:0:10:1', cleanremoteaddr('::10:1', false));
  72          $this->assertSame('1:1:0:0:0:0:0:0', cleanremoteaddr('01:1::', false));
  73          $this->assertSame('10:0:0:0:0:0:0:10', cleanremoteaddr('10::10', false));
  74          $this->assertSame('::ffff:c0a8:11', cleanremoteaddr('::ffff:192.168.1.1', true));
  75      }
  76  
  77      public function test_address_in_subnet() {
  78          // 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn (number of bits in net mask).
  79          $this->assertTrue(address_in_subnet('123.121.234.1', '123.121.234.1/32'));
  80          $this->assertFalse(address_in_subnet('123.121.23.1', '123.121.23.0/32'));
  81          $this->assertTrue(address_in_subnet('10.10.10.100',  '123.121.23.45/0'));
  82          $this->assertTrue(address_in_subnet('123.121.234.1', '123.121.234.0/24'));
  83          $this->assertFalse(address_in_subnet('123.121.34.1', '123.121.234.0/24'));
  84          $this->assertTrue(address_in_subnet('123.121.234.1', '123.121.234.0/30'));
  85          $this->assertFalse(address_in_subnet('123.121.23.8', '123.121.23.0/30'));
  86          $this->assertTrue(address_in_subnet('baba:baba::baba', 'baba:baba::baba/128'));
  87          $this->assertFalse(address_in_subnet('bab:baba::baba', 'bab:baba::cece/128'));
  88          $this->assertTrue(address_in_subnet('baba:baba::baba', 'cece:cece::cece/0'));
  89          $this->assertTrue(address_in_subnet('baba:baba::baba', 'baba:baba::baba/128'));
  90          $this->assertTrue(address_in_subnet('baba:baba::00ba', 'baba:baba::/120'));
  91          $this->assertFalse(address_in_subnet('baba:baba::aba', 'baba:baba::/120'));
  92          $this->assertTrue(address_in_subnet('baba::baba:00ba', 'baba::baba:0/112'));
  93          $this->assertFalse(address_in_subnet('baba::aba:00ba', 'baba::baba:0/112'));
  94          $this->assertFalse(address_in_subnet('aba::baba:0000', 'baba::baba:0/112'));
  95  
  96          // Fixed input.
  97          $this->assertTrue(address_in_subnet('123.121.23.1   ', ' 123.121.23.0 / 24'));
  98          $this->assertTrue(address_in_subnet('::ffff:10.1.1.1', ' 0:0:0:000:0:ffff:a1:10 / 126'));
  99  
 100          // Incorrect input.
 101          $this->assertFalse(address_in_subnet('123.121.234.1', '123.121.234.1/-2'));
 102          $this->assertFalse(address_in_subnet('123.121.234.1', '123.121.234.1/64'));
 103          $this->assertFalse(address_in_subnet('123.121.234.x', '123.121.234.1/24'));
 104          $this->assertFalse(address_in_subnet('123.121.234.0', '123.121.234.xx/24'));
 105          $this->assertFalse(address_in_subnet('123.121.234.1', '123.121.234.1/xx0'));
 106          $this->assertFalse(address_in_subnet('::1', '::aa:0/xx0'));
 107          $this->assertFalse(address_in_subnet('::1', '::aa:0/-5'));
 108          $this->assertFalse(address_in_subnet('::1', '::aa:0/130'));
 109          $this->assertFalse(address_in_subnet('x:1', '::aa:0/130'));
 110          $this->assertFalse(address_in_subnet('::1', '::ax:0/130'));
 111  
 112          // 2: xxx.xxx.xxx.xxx-yyy or  xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx::xxxx-yyyy (a range of IP addresses in the last group).
 113          $this->assertTrue(address_in_subnet('123.121.234.12', '123.121.234.12-14'));
 114          $this->assertTrue(address_in_subnet('123.121.234.13', '123.121.234.12-14'));
 115          $this->assertTrue(address_in_subnet('123.121.234.14', '123.121.234.12-14'));
 116          $this->assertFalse(address_in_subnet('123.121.234.1', '123.121.234.12-14'));
 117          $this->assertFalse(address_in_subnet('123.121.234.20', '123.121.234.12-14'));
 118          $this->assertFalse(address_in_subnet('123.121.23.12', '123.121.234.12-14'));
 119          $this->assertFalse(address_in_subnet('123.12.234.12', '123.121.234.12-14'));
 120          $this->assertTrue(address_in_subnet('baba:baba::baba', 'baba:baba::baba-babe'));
 121          $this->assertTrue(address_in_subnet('baba:baba::babc', 'baba:baba::baba-babe'));
 122          $this->assertTrue(address_in_subnet('baba:baba::babe', 'baba:baba::baba-babe'));
 123          $this->assertFalse(address_in_subnet('bab:baba::bab0', 'bab:baba::baba-babe'));
 124          $this->assertFalse(address_in_subnet('bab:baba::babf', 'bab:baba::baba-babe'));
 125          $this->assertFalse(address_in_subnet('bab:baba::bfbe', 'bab:baba::baba-babe'));
 126          $this->assertFalse(address_in_subnet('bfb:baba::babe', 'bab:baba::baba-babe'));
 127  
 128          // Fixed input.
 129          $this->assertTrue(address_in_subnet('123.121.234.12', '123.121.234.12 - 14 '));
 130          $this->assertTrue(address_in_subnet('bab:baba::babe', 'bab:baba::baba - babe  '));
 131  
 132          // Incorrect input.
 133          $this->assertFalse(address_in_subnet('123.121.234.12', '123.121.234.12-234.14'));
 134          $this->assertFalse(address_in_subnet('123.121.234.12', '123.121.234.12-256'));
 135          $this->assertFalse(address_in_subnet('123.121.234.12', '123.121.234.12--256'));
 136  
 137          // 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx. (incomplete address, a bit non-technical ;-).
 138          $this->assertTrue(address_in_subnet('123.121.234.12', '123.121.234.12'));
 139          $this->assertFalse(address_in_subnet('123.121.23.12', '123.121.23.13'));
 140          $this->assertTrue(address_in_subnet('123.121.234.12', '123.121.234.'));
 141          $this->assertTrue(address_in_subnet('123.121.234.12', '123.121.234'));
 142          $this->assertTrue(address_in_subnet('123.121.234.12', '123.121'));
 143          $this->assertTrue(address_in_subnet('123.121.234.12', '123'));
 144          $this->assertFalse(address_in_subnet('123.121.234.1', '12.121.234.'));
 145          $this->assertFalse(address_in_subnet('123.121.234.1', '12.121.234'));
 146          $this->assertTrue(address_in_subnet('baba:baba::bab', 'baba:baba::bab'));
 147          $this->assertFalse(address_in_subnet('baba:baba::ba', 'baba:baba::bc'));
 148          $this->assertTrue(address_in_subnet('baba:baba::bab', 'baba:baba'));
 149          $this->assertTrue(address_in_subnet('baba:baba::bab', 'baba:'));
 150          $this->assertFalse(address_in_subnet('bab:baba::bab', 'baba:'));
 151  
 152          // Multiple subnets.
 153          $this->assertTrue(address_in_subnet('123.121.234.12', '::1/64, 124., 123.121.234.10-30'));
 154          $this->assertTrue(address_in_subnet('124.121.234.12', '::1/64, 124., 123.121.234.10-30'));
 155          $this->assertTrue(address_in_subnet('::2',            '::1/64, 124., 123.121.234.10-30'));
 156          $this->assertFalse(address_in_subnet('12.121.234.12', '::1/64, 124., 123.121.234.10-30'));
 157  
 158          // Other incorrect input.
 159          $this->assertFalse(address_in_subnet('123.123.123.123', ''));
 160      }
 161  
 162      public function test_fix_utf8() {
 163          // Make sure valid data including other types is not changed.
 164          $this->assertSame(null, fix_utf8(null));
 165          $this->assertSame(1, fix_utf8(1));
 166          $this->assertSame(1.1, fix_utf8(1.1));
 167          $this->assertSame(true, fix_utf8(true));
 168          $this->assertSame('', fix_utf8(''));
 169          $this->assertSame('abc', fix_utf8('abc'));
 170          $array = array('do', 're', 'mi');
 171          $this->assertSame($array, fix_utf8($array));
 172          $object = new \stdClass();
 173          $object->a = 'aa';
 174          $object->b = 'bb';
 175          $this->assertEquals($object, fix_utf8($object));
 176  
 177          // valid utf8 string
 178          $this->assertSame("žlutý koníček přeskočil potůček \n\t\r", fix_utf8("žlutý koníček přeskočil potůček \n\t\r\0"));
 179  
 180          // Invalid utf8 string.
 181          $this->assertSame('aš', fix_utf8('a'.chr(130).'š'), 'This fails with buggy iconv() when mbstring extenstion is not available as fallback.');
 182          $this->assertSame('Hello ', fix_utf8('Hello ￿'));
 183      }
 184  
 185      public function test_optional_param() {
 186          global $CFG;
 187  
 188          $_POST['username'] = 'post_user';
 189          $_GET['username'] = 'get_user';
 190          $this->assertSame($_POST['username'], optional_param('username', 'default_user', PARAM_RAW));
 191  
 192          unset($_POST['username']);
 193          $this->assertSame($_GET['username'], optional_param('username', 'default_user', PARAM_RAW));
 194  
 195          unset($_GET['username']);
 196          $this->assertSame('default_user', optional_param('username', 'default_user', PARAM_RAW));
 197  
 198          // Make sure exception is triggered when some params are missing, hide error notices here - new in 2.2.
 199          $_POST['username'] = 'post_user';
 200          try {
 201              optional_param('username', 'default_user', null);
 202              $this->fail('coding_exception expected');
 203          } catch (\moodle_exception $ex) {
 204              $this->assertInstanceOf('coding_exception', $ex);
 205          }
 206          try {
 207              @optional_param('username', 'default_user');
 208              $this->fail('coding_exception expected');
 209          } catch (\moodle_exception $ex) {
 210              $this->assertInstanceOf('coding_exception', $ex);
 211          } catch (\Error $error) {
 212              // PHP 7.1 throws \Error even earlier.
 213              $this->assertMatchesRegularExpression('/Too few arguments to function/', $error->getMessage());
 214          }
 215          try {
 216              @optional_param('username');
 217              $this->fail('coding_exception expected');
 218          } catch (\moodle_exception $ex) {
 219              $this->assertInstanceOf('coding_exception', $ex);
 220          } catch (\Error $error) {
 221              // PHP 7.1 throws \Error even earlier.
 222              $this->assertMatchesRegularExpression('/Too few arguments to function/', $error->getMessage());
 223          }
 224          try {
 225              optional_param('', 'default_user', PARAM_RAW);
 226              $this->fail('coding_exception expected');
 227          } catch (\moodle_exception $ex) {
 228              $this->assertInstanceOf('coding_exception', $ex);
 229          }
 230  
 231          // Make sure warning is displayed if array submitted - TODO: throw exception in Moodle 2.3.
 232          $_POST['username'] = array('a'=>'a');
 233          $this->assertSame($_POST['username'], optional_param('username', 'default_user', PARAM_RAW));
 234          $this->assertDebuggingCalled();
 235      }
 236  
 237      public function test_optional_param_array() {
 238          global $CFG;
 239  
 240          $_POST['username'] = array('a'=>'post_user');
 241          $_GET['username'] = array('a'=>'get_user');
 242          $this->assertSame($_POST['username'], optional_param_array('username', array('a'=>'default_user'), PARAM_RAW));
 243  
 244          unset($_POST['username']);
 245          $this->assertSame($_GET['username'], optional_param_array('username', array('a'=>'default_user'), PARAM_RAW));
 246  
 247          unset($_GET['username']);
 248          $this->assertSame(array('a'=>'default_user'), optional_param_array('username', array('a'=>'default_user'), PARAM_RAW));
 249  
 250          // Make sure exception is triggered when some params are missing, hide error notices here - new in 2.2.
 251          $_POST['username'] = array('a'=>'post_user');
 252          try {
 253              optional_param_array('username', array('a'=>'default_user'), null);
 254              $this->fail('coding_exception expected');
 255          } catch (\moodle_exception $ex) {
 256              $this->assertInstanceOf('coding_exception', $ex);
 257          }
 258          try {
 259              @optional_param_array('username', array('a'=>'default_user'));
 260              $this->fail('coding_exception expected');
 261          } catch (\moodle_exception $ex) {
 262              $this->assertInstanceOf('coding_exception', $ex);
 263          } catch (\Error $error) {
 264              // PHP 7.1 throws \Error even earlier.
 265              $this->assertMatchesRegularExpression('/Too few arguments to function/', $error->getMessage());
 266          }
 267          try {
 268              @optional_param_array('username');
 269              $this->fail('coding_exception expected');
 270          } catch (\moodle_exception $ex) {
 271              $this->assertInstanceOf('coding_exception', $ex);
 272          } catch (\Error $error) {
 273              // PHP 7.1 throws \Error even earlier.
 274              $this->assertMatchesRegularExpression('/Too few arguments to function/', $error->getMessage());
 275          }
 276          try {
 277              optional_param_array('', array('a'=>'default_user'), PARAM_RAW);
 278              $this->fail('coding_exception expected');
 279          } catch (\moodle_exception $ex) {
 280              $this->assertInstanceOf('coding_exception', $ex);
 281          }
 282  
 283          // Do not allow nested arrays.
 284          try {
 285              $_POST['username'] = array('a'=>array('b'=>'post_user'));
 286              optional_param_array('username', array('a'=>'default_user'), PARAM_RAW);
 287              $this->fail('coding_exception expected');
 288          } catch (\coding_exception $ex) {
 289              $this->assertTrue(true);
 290          }
 291  
 292          // Do not allow non-arrays.
 293          $_POST['username'] = 'post_user';
 294          $this->assertSame(array('a'=>'default_user'), optional_param_array('username', array('a'=>'default_user'), PARAM_RAW));
 295          $this->assertDebuggingCalled();
 296  
 297          // Make sure array keys are sanitised.
 298          $_POST['username'] = array('abc123_;-/*-+ '=>'arrggh', 'a1_-'=>'post_user');
 299          $this->assertSame(array('a1_-'=>'post_user'), optional_param_array('username', array(), PARAM_RAW));
 300          $this->assertDebuggingCalled();
 301      }
 302  
 303      public function test_required_param() {
 304          $_POST['username'] = 'post_user';
 305          $_GET['username'] = 'get_user';
 306          $this->assertSame('post_user', required_param('username', PARAM_RAW));
 307  
 308          unset($_POST['username']);
 309          $this->assertSame('get_user', required_param('username', PARAM_RAW));
 310  
 311          unset($_GET['username']);
 312          try {
 313              $this->assertSame('default_user', required_param('username', PARAM_RAW));
 314              $this->fail('moodle_exception expected');
 315          } catch (\moodle_exception $ex) {
 316              $this->assertInstanceOf('moodle_exception', $ex);
 317          }
 318  
 319          // Make sure exception is triggered when some params are missing, hide error notices here - new in 2.2.
 320          $_POST['username'] = 'post_user';
 321          try {
 322              @required_param('username');
 323              $this->fail('coding_exception expected');
 324          } catch (\moodle_exception $ex) {
 325              $this->assertInstanceOf('coding_exception', $ex);
 326          } catch (\Error $error) {
 327              // PHP 7.1 throws \Error even earlier.
 328              $this->assertMatchesRegularExpression('/Too few arguments to function/', $error->getMessage());
 329          }
 330          try {
 331              required_param('username', '');
 332              $this->fail('coding_exception expected');
 333          } catch (\moodle_exception $ex) {
 334              $this->assertInstanceOf('coding_exception', $ex);
 335          }
 336          try {
 337              required_param('', PARAM_RAW);
 338              $this->fail('coding_exception expected');
 339          } catch (\moodle_exception $ex) {
 340              $this->assertInstanceOf('coding_exception', $ex);
 341          }
 342  
 343          // Make sure warning is displayed if array submitted - TODO: throw exception in Moodle 2.3.
 344          $_POST['username'] = array('a'=>'a');
 345          $this->assertSame($_POST['username'], required_param('username', PARAM_RAW));
 346          $this->assertDebuggingCalled();
 347      }
 348  
 349      public function test_required_param_array() {
 350          global $CFG;
 351  
 352          $_POST['username'] = array('a'=>'post_user');
 353          $_GET['username'] = array('a'=>'get_user');
 354          $this->assertSame($_POST['username'], required_param_array('username', PARAM_RAW));
 355  
 356          unset($_POST['username']);
 357          $this->assertSame($_GET['username'], required_param_array('username', PARAM_RAW));
 358  
 359          // Make sure exception is triggered when some params are missing, hide error notices here - new in 2.2.
 360          $_POST['username'] = array('a'=>'post_user');
 361          try {
 362              required_param_array('username', null);
 363              $this->fail('coding_exception expected');
 364          } catch (\moodle_exception $ex) {
 365              $this->assertInstanceOf('coding_exception', $ex);
 366          }
 367          try {
 368              @required_param_array('username');
 369              $this->fail('coding_exception expected');
 370          } catch (\moodle_exception $ex) {
 371              $this->assertInstanceOf('coding_exception', $ex);
 372          } catch (\Error $error) {
 373              // PHP 7.1 throws \Error.
 374              $this->assertMatchesRegularExpression('/Too few arguments to function/', $error->getMessage());
 375          }
 376          try {
 377              required_param_array('', PARAM_RAW);
 378              $this->fail('coding_exception expected');
 379          } catch (\moodle_exception $ex) {
 380              $this->assertInstanceOf('coding_exception', $ex);
 381          }
 382  
 383          // Do not allow nested arrays.
 384          try {
 385              $_POST['username'] = array('a'=>array('b'=>'post_user'));
 386              required_param_array('username', PARAM_RAW);
 387              $this->fail('coding_exception expected');
 388          } catch (\moodle_exception $ex) {
 389              $this->assertInstanceOf('coding_exception', $ex);
 390          }
 391  
 392          // Do not allow non-arrays.
 393          try {
 394              $_POST['username'] = 'post_user';
 395              required_param_array('username', PARAM_RAW);
 396              $this->fail('moodle_exception expected');
 397          } catch (\moodle_exception $ex) {
 398              $this->assertInstanceOf('moodle_exception', $ex);
 399          }
 400  
 401          // Make sure array keys are sanitised.
 402          $_POST['username'] = array('abc123_;-/*-+ '=>'arrggh', 'a1_-'=>'post_user');
 403          $this->assertSame(array('a1_-'=>'post_user'), required_param_array('username', PARAM_RAW));
 404          $this->assertDebuggingCalled();
 405      }
 406  
 407      public function test_clean_param() {
 408          // Forbid objects and arrays.
 409          try {
 410              clean_param(array('x', 'y'), PARAM_RAW);
 411              $this->fail('coding_exception expected');
 412          } catch (\moodle_exception $ex) {
 413              $this->assertInstanceOf('coding_exception', $ex);
 414          }
 415          try {
 416              $param = new \stdClass();
 417              $param->id = 1;
 418              clean_param($param, PARAM_RAW);
 419              $this->fail('coding_exception expected');
 420          } catch (\moodle_exception $ex) {
 421              $this->assertInstanceOf('coding_exception', $ex);
 422          }
 423  
 424          // Require correct type.
 425          try {
 426              clean_param('x', 'xxxxxx');
 427              $this->fail('moodle_exception expected');
 428          } catch (\moodle_exception $ex) {
 429              $this->assertInstanceOf('moodle_exception', $ex);
 430          }
 431          try {
 432              @clean_param('x');
 433              $this->fail('moodle_exception expected');
 434          } catch (\moodle_exception $ex) {
 435              $this->assertInstanceOf('moodle_exception', $ex);
 436          } catch (\Error $error) {
 437              // PHP 7.1 throws \Error even earlier.
 438              $this->assertMatchesRegularExpression('/Too few arguments to function/', $error->getMessage());
 439          }
 440      }
 441  
 442      public function test_clean_param_array() {
 443          $this->assertSame(array(), clean_param_array(null, PARAM_RAW));
 444          $this->assertSame(array('a', 'b'), clean_param_array(array('a', 'b'), PARAM_RAW));
 445          $this->assertSame(array('a', array('b')), clean_param_array(array('a', array('b')), PARAM_RAW, true));
 446  
 447          // Require correct type.
 448          try {
 449              clean_param_array(array('x'), 'xxxxxx');
 450              $this->fail('moodle_exception expected');
 451          } catch (\moodle_exception $ex) {
 452              $this->assertInstanceOf('moodle_exception', $ex);
 453          }
 454          try {
 455              @clean_param_array(array('x'));
 456              $this->fail('moodle_exception expected');
 457          } catch (\moodle_exception $ex) {
 458              $this->assertInstanceOf('moodle_exception', $ex);
 459          } catch (\Error $error) {
 460              // PHP 7.1 throws \Error even earlier.
 461              $this->assertMatchesRegularExpression('/Too few arguments to function/', $error->getMessage());
 462          }
 463  
 464          try {
 465              clean_param_array(array('x', array('y')), PARAM_RAW);
 466              $this->fail('coding_exception expected');
 467          } catch (\moodle_exception $ex) {
 468              $this->assertInstanceOf('coding_exception', $ex);
 469          }
 470  
 471          // Test recursive.
 472      }
 473  
 474      public function test_clean_param_raw() {
 475          $this->assertSame(
 476              '#()*#,9789\'".,<42897></?$(*DSFMO#$*)(SDJ)($*)',
 477              clean_param('#()*#,9789\'".,<42897></?$(*DSFMO#$*)(SDJ)($*)', PARAM_RAW));
 478      }
 479  
 480      public function test_clean_param_trim() {
 481          $this->assertSame('Frog toad', clean_param("   Frog toad   \r\n  ", PARAM_RAW_TRIMMED));
 482      }
 483  
 484      public function test_clean_param_clean() {
 485          // PARAM_CLEAN is an ugly hack, do not use in new code (skodak),
 486          // instead use more specific type, or submit sothing that can be verified properly.
 487          $this->assertSame('xx', clean_param('xx<script>', PARAM_CLEAN));
 488      }
 489  
 490      public function test_clean_param_alpha() {
 491          $this->assertSame('DSFMOSDJ', clean_param('#()*#,9789\'".,<42897></?$(*DSFMO#$*)(SDJ)($*)', PARAM_ALPHA));
 492      }
 493  
 494      public function test_clean_param_alphanum() {
 495          $this->assertSame('978942897DSFMOSDJ', clean_param('#()*#,9789\'".,<42897></?$(*DSFMO#$*)(SDJ)($*)', PARAM_ALPHANUM));
 496      }
 497  
 498      public function test_clean_param_alphaext() {
 499          $this->assertSame('DSFMOSDJ', clean_param('#()*#,9789\'".,<42897></?$(*DSFMO#$*)(SDJ)($*)', PARAM_ALPHAEXT));
 500      }
 501  
 502      public function test_clean_param_sequence() {
 503          $this->assertSame(',9789,42897', clean_param('#()*#,9789\'".,<42897></?$(*DSFMO#$*)(SDJ)($*)', PARAM_SEQUENCE));
 504      }
 505  
 506      public function test_clean_param_component() {
 507          // Please note the cleaning of component names is very strict, no guessing here.
 508          $this->assertSame('mod_forum', clean_param('mod_forum', PARAM_COMPONENT));
 509          $this->assertSame('block_online_users', clean_param('block_online_users', PARAM_COMPONENT));
 510          $this->assertSame('block_blond_online_users', clean_param('block_blond_online_users', PARAM_COMPONENT));
 511          $this->assertSame('mod_something2', clean_param('mod_something2', PARAM_COMPONENT));
 512          $this->assertSame('forum', clean_param('forum', PARAM_COMPONENT));
 513          $this->assertSame('user', clean_param('user', PARAM_COMPONENT));
 514          $this->assertSame('rating', clean_param('rating', PARAM_COMPONENT));
 515          $this->assertSame('feedback360', clean_param('feedback360', PARAM_COMPONENT));
 516          $this->assertSame('mod_feedback360', clean_param('mod_feedback360', PARAM_COMPONENT));
 517          $this->assertSame('', clean_param('mod_2something', PARAM_COMPONENT));
 518          $this->assertSame('', clean_param('2mod_something', PARAM_COMPONENT));
 519          $this->assertSame('', clean_param('mod_something_xx', PARAM_COMPONENT));
 520          $this->assertSame('', clean_param('auth_something__xx', PARAM_COMPONENT));
 521          $this->assertSame('', clean_param('mod_Something', PARAM_COMPONENT));
 522          $this->assertSame('', clean_param('mod_somethíng', PARAM_COMPONENT));
 523          $this->assertSame('', clean_param('mod__something', PARAM_COMPONENT));
 524          $this->assertSame('', clean_param('auth_xx-yy', PARAM_COMPONENT));
 525          $this->assertSame('', clean_param('_auth_xx', PARAM_COMPONENT));
 526          $this->assertSame('a2uth_xx', clean_param('a2uth_xx', PARAM_COMPONENT));
 527          $this->assertSame('', clean_param('auth_xx_', PARAM_COMPONENT));
 528          $this->assertSame('', clean_param('auth_xx.old', PARAM_COMPONENT));
 529          $this->assertSame('', clean_param('_user', PARAM_COMPONENT));
 530          $this->assertSame('', clean_param('2rating', PARAM_COMPONENT));
 531          $this->assertSame('', clean_param('user_', PARAM_COMPONENT));
 532      }
 533  
 534      public function test_clean_param_localisedfloat() {
 535  
 536          $this->assertSame(0.5, clean_param('0.5', PARAM_LOCALISEDFLOAT));
 537          $this->assertSame(false, clean_param('0X5', PARAM_LOCALISEDFLOAT));
 538          $this->assertSame(0.5, clean_param('.5', PARAM_LOCALISEDFLOAT));
 539          $this->assertSame(false, clean_param('X5', PARAM_LOCALISEDFLOAT));
 540          $this->assertSame(10.5, clean_param('10.5', PARAM_LOCALISEDFLOAT));
 541          $this->assertSame(false, clean_param('10X5', PARAM_LOCALISEDFLOAT));
 542          $this->assertSame(1000.5, clean_param('1 000.5', PARAM_LOCALISEDFLOAT));
 543          $this->assertSame(false, clean_param('1 000X5', PARAM_LOCALISEDFLOAT));
 544          $this->assertSame(false, clean_param('1.000.5', PARAM_LOCALISEDFLOAT));
 545          $this->assertSame(false, clean_param('1X000X5', PARAM_LOCALISEDFLOAT));
 546          $this->assertSame(false, clean_param('nan', PARAM_LOCALISEDFLOAT));
 547          $this->assertSame(false, clean_param('10.6blah', PARAM_LOCALISEDFLOAT));
 548  
 549          // Tests with a localised decimal separator.
 550          $this->define_local_decimal_separator();
 551  
 552          $this->assertSame(0.5, clean_param('0.5', PARAM_LOCALISEDFLOAT));
 553          $this->assertSame(0.5, clean_param('0X5', PARAM_LOCALISEDFLOAT));
 554          $this->assertSame(0.5, clean_param('.5', PARAM_LOCALISEDFLOAT));
 555          $this->assertSame(0.5, clean_param('X5', PARAM_LOCALISEDFLOAT));
 556          $this->assertSame(10.5, clean_param('10.5', PARAM_LOCALISEDFLOAT));
 557          $this->assertSame(10.5, clean_param('10X5', PARAM_LOCALISEDFLOAT));
 558          $this->assertSame(1000.5, clean_param('1 000.5', PARAM_LOCALISEDFLOAT));
 559          $this->assertSame(1000.5, clean_param('1 000X5', PARAM_LOCALISEDFLOAT));
 560          $this->assertSame(false, clean_param('1.000.5', PARAM_LOCALISEDFLOAT));
 561          $this->assertSame(false, clean_param('1X000X5', PARAM_LOCALISEDFLOAT));
 562          $this->assertSame(false, clean_param('nan', PARAM_LOCALISEDFLOAT));
 563          $this->assertSame(false, clean_param('10X6blah', PARAM_LOCALISEDFLOAT));
 564      }
 565  
 566      public function test_is_valid_plugin_name() {
 567          $this->assertTrue(is_valid_plugin_name('forum'));
 568          $this->assertTrue(is_valid_plugin_name('forum2'));
 569          $this->assertTrue(is_valid_plugin_name('feedback360'));
 570          $this->assertTrue(is_valid_plugin_name('online_users'));
 571          $this->assertTrue(is_valid_plugin_name('blond_online_users'));
 572          $this->assertFalse(is_valid_plugin_name('online__users'));
 573          $this->assertFalse(is_valid_plugin_name('forum '));
 574          $this->assertFalse(is_valid_plugin_name('forum.old'));
 575          $this->assertFalse(is_valid_plugin_name('xx-yy'));
 576          $this->assertFalse(is_valid_plugin_name('2xx'));
 577          $this->assertFalse(is_valid_plugin_name('Xx'));
 578          $this->assertFalse(is_valid_plugin_name('_xx'));
 579          $this->assertFalse(is_valid_plugin_name('xx_'));
 580      }
 581  
 582      public function test_clean_param_plugin() {
 583          // Please note the cleaning of plugin names is very strict, no guessing here.
 584          $this->assertSame('forum', clean_param('forum', PARAM_PLUGIN));
 585          $this->assertSame('forum2', clean_param('forum2', PARAM_PLUGIN));
 586          $this->assertSame('feedback360', clean_param('feedback360', PARAM_PLUGIN));
 587          $this->assertSame('online_users', clean_param('online_users', PARAM_PLUGIN));
 588          $this->assertSame('blond_online_users', clean_param('blond_online_users', PARAM_PLUGIN));
 589          $this->assertSame('', clean_param('online__users', PARAM_PLUGIN));
 590          $this->assertSame('', clean_param('forum ', PARAM_PLUGIN));
 591          $this->assertSame('', clean_param('forum.old', PARAM_PLUGIN));
 592          $this->assertSame('', clean_param('xx-yy', PARAM_PLUGIN));
 593          $this->assertSame('', clean_param('2xx', PARAM_PLUGIN));
 594          $this->assertSame('', clean_param('Xx', PARAM_PLUGIN));
 595          $this->assertSame('', clean_param('_xx', PARAM_PLUGIN));
 596          $this->assertSame('', clean_param('xx_', PARAM_PLUGIN));
 597      }
 598  
 599      public function test_clean_param_area() {
 600          // Please note the cleaning of area names is very strict, no guessing here.
 601          $this->assertSame('something', clean_param('something', PARAM_AREA));
 602          $this->assertSame('something2', clean_param('something2', PARAM_AREA));
 603          $this->assertSame('some_thing', clean_param('some_thing', PARAM_AREA));
 604          $this->assertSame('some_thing_xx', clean_param('some_thing_xx', PARAM_AREA));
 605          $this->assertSame('feedback360', clean_param('feedback360', PARAM_AREA));
 606          $this->assertSame('', clean_param('_something', PARAM_AREA));
 607          $this->assertSame('', clean_param('something_', PARAM_AREA));
 608          $this->assertSame('', clean_param('2something', PARAM_AREA));
 609          $this->assertSame('', clean_param('Something', PARAM_AREA));
 610          $this->assertSame('', clean_param('some-thing', PARAM_AREA));
 611          $this->assertSame('', clean_param('somethííng', PARAM_AREA));
 612          $this->assertSame('', clean_param('something.x', PARAM_AREA));
 613      }
 614  
 615      public function test_clean_param_text() {
 616          $this->assertSame(PARAM_TEXT, PARAM_MULTILANG);
 617          // Standard.
 618          $this->assertSame('xx<lang lang="en">aa</lang><lang lang="yy">pp</lang>', clean_param('xx<lang lang="en">aa</lang><lang lang="yy">pp</lang>', PARAM_TEXT));
 619          $this->assertSame('<span lang="en" class="multilang">aa</span><span lang="xy" class="multilang">bb</span>', clean_param('<span lang="en" class="multilang">aa</span><span lang="xy" class="multilang">bb</span>', PARAM_TEXT));
 620          $this->assertSame('xx<lang lang="en">aa'."\n".'</lang><lang lang="yy">pp</lang>', clean_param('xx<lang lang="en">aa'."\n".'</lang><lang lang="yy">pp</lang>', PARAM_TEXT));
 621          // Malformed.
 622          $this->assertSame('<span lang="en" class="multilang">aa</span>', clean_param('<span lang="en" class="multilang">aa</span>', PARAM_TEXT));
 623          $this->assertSame('aa', clean_param('<span lang="en" class="nothing" class="multilang">aa</span>', PARAM_TEXT));
 624          $this->assertSame('aa', clean_param('<lang lang="en" class="multilang">aa</lang>', PARAM_TEXT));
 625          $this->assertSame('aa', clean_param('<lang lang="en!!">aa</lang>', PARAM_TEXT));
 626          $this->assertSame('aa', clean_param('<span lang="en==" class="multilang">aa</span>', PARAM_TEXT));
 627          $this->assertSame('abc', clean_param('a<em>b</em>c', PARAM_TEXT));
 628          $this->assertSame('a>c>', clean_param('a><xx >c>', PARAM_TEXT)); // Standard strip_tags() behaviour.
 629          $this->assertSame('a', clean_param('a<b', PARAM_TEXT));
 630          $this->assertSame('a>b', clean_param('a>b', PARAM_TEXT));
 631          $this->assertSame('<lang lang="en">a>a</lang>', clean_param('<lang lang="en">a>a</lang>', PARAM_TEXT)); // Standard strip_tags() behaviour.
 632          $this->assertSame('a', clean_param('<lang lang="en">a<a</lang>', PARAM_TEXT));
 633          $this->assertSame('<lang lang="en">aa</lang>', clean_param('<lang lang="en">a<br>a</lang>', PARAM_TEXT));
 634      }
 635  
 636      public function test_clean_param_url() {
 637          // Test PARAM_URL and PARAM_LOCALURL a bit.
 638          // Valid URLs.
 639          $this->assertSame('http://google.com/', clean_param('http://google.com/', PARAM_URL));
 640          $this->assertSame('http://some.very.long.and.silly.domain/with/a/path/', clean_param('http://some.very.long.and.silly.domain/with/a/path/', PARAM_URL));
 641          $this->assertSame('http://localhost/', clean_param('http://localhost/', PARAM_URL));
 642          $this->assertSame('http://0.255.1.1/numericip.php', clean_param('http://0.255.1.1/numericip.php', PARAM_URL));
 643          $this->assertSame('https://google.com/', clean_param('https://google.com/', PARAM_URL));
 644          $this->assertSame('https://some.very.long.and.silly.domain/with/a/path/', clean_param('https://some.very.long.and.silly.domain/with/a/path/', PARAM_URL));
 645          $this->assertSame('https://localhost/', clean_param('https://localhost/', PARAM_URL));
 646          $this->assertSame('https://0.255.1.1/numericip.php', clean_param('https://0.255.1.1/numericip.php', PARAM_URL));
 647          $this->assertSame('ftp://ftp.debian.org/debian/', clean_param('ftp://ftp.debian.org/debian/', PARAM_URL));
 648          $this->assertSame('/just/a/path', clean_param('/just/a/path', PARAM_URL));
 649          // Invalid URLs.
 650          $this->assertSame('', clean_param('funny:thing', PARAM_URL));
 651          $this->assertSame('', clean_param('http://example.ee/sdsf"f', PARAM_URL));
 652          $this->assertSame('', clean_param('javascript://comment%0Aalert(1)', PARAM_URL));
 653          $this->assertSame('', clean_param('rtmp://example.com/livestream', PARAM_URL));
 654          $this->assertSame('', clean_param('rtmp://example.com/live&foo', PARAM_URL));
 655          $this->assertSame('', clean_param('rtmp://example.com/fms&mp4:path/to/file.mp4', PARAM_URL));
 656          $this->assertSame('', clean_param('mailto:support@moodle.org', PARAM_URL));
 657          $this->assertSame('', clean_param('mailto:support@moodle.org?subject=Hello%20Moodle', PARAM_URL));
 658          $this->assertSame('', clean_param('mailto:support@moodle.org?subject=Hello%20Moodle&cc=feedback@moodle.org', PARAM_URL));
 659      }
 660  
 661      public function test_clean_param_localurl() {
 662          global $CFG;
 663  
 664          $this->resetAfterTest();
 665  
 666          // External, invalid.
 667          $this->assertSame('', clean_param('funny:thing', PARAM_LOCALURL));
 668          $this->assertSame('', clean_param('http://google.com/', PARAM_LOCALURL));
 669          $this->assertSame('', clean_param('https://google.com/?test=true', PARAM_LOCALURL));
 670          $this->assertSame('', clean_param('http://some.very.long.and.silly.domain/with/a/path/', PARAM_LOCALURL));
 671  
 672          // Local absolute.
 673          $this->assertSame(clean_param($CFG->wwwroot, PARAM_LOCALURL), $CFG->wwwroot);
 674          $this->assertSame($CFG->wwwroot . '/with/something?else=true',
 675              clean_param($CFG->wwwroot . '/with/something?else=true', PARAM_LOCALURL));
 676  
 677          // Local relative.
 678          $this->assertSame('/just/a/path', clean_param('/just/a/path', PARAM_LOCALURL));
 679          $this->assertSame('course/view.php?id=3', clean_param('course/view.php?id=3', PARAM_LOCALURL));
 680  
 681          // Local absolute HTTPS in a non HTTPS site.
 682          $CFG->wwwroot = str_replace('https:', 'http:', $CFG->wwwroot); // Need to simulate non-https site.
 683          $httpsroot = str_replace('http:', 'https:', $CFG->wwwroot);
 684          $this->assertSame('', clean_param($httpsroot, PARAM_LOCALURL));
 685          $this->assertSame('', clean_param($httpsroot . '/with/something?else=true', PARAM_LOCALURL));
 686  
 687          // Local absolute HTTPS in a HTTPS site.
 688          $CFG->wwwroot = str_replace('http:', 'https:', $CFG->wwwroot);
 689          $httpsroot = $CFG->wwwroot;
 690          $this->assertSame($httpsroot, clean_param($httpsroot, PARAM_LOCALURL));
 691          $this->assertSame($httpsroot . '/with/something?else=true',
 692              clean_param($httpsroot . '/with/something?else=true', PARAM_LOCALURL));
 693  
 694          // Test open redirects are not possible.
 695          $CFG->wwwroot = 'http://www.example.com';
 696          $this->assertSame('', clean_param('http://www.example.com.evil.net/hack.php', PARAM_LOCALURL));
 697          $CFG->wwwroot = 'https://www.example.com';
 698          $this->assertSame('', clean_param('https://www.example.com.evil.net/hack.php', PARAM_LOCALURL));
 699      }
 700  
 701      public function test_clean_param_file() {
 702          $this->assertSame('correctfile.txt', clean_param('correctfile.txt', PARAM_FILE));
 703          $this->assertSame('badfile.txt', clean_param('b\'a<d`\\/fi:l>e.t"x|t', PARAM_FILE));
 704          $this->assertSame('..parentdirfile.txt', clean_param('../parentdirfile.txt', PARAM_FILE));
 705          $this->assertSame('....grandparentdirfile.txt', clean_param('../../grandparentdirfile.txt', PARAM_FILE));
 706          $this->assertSame('..winparentdirfile.txt', clean_param('..\winparentdirfile.txt', PARAM_FILE));
 707          $this->assertSame('....wingrandparentdir.txt', clean_param('..\..\wingrandparentdir.txt', PARAM_FILE));
 708          $this->assertSame('myfile.a.b.txt', clean_param('myfile.a.b.txt', PARAM_FILE));
 709          $this->assertSame('myfile..a..b.txt', clean_param('myfile..a..b.txt', PARAM_FILE));
 710          $this->assertSame('myfile.a..b...txt', clean_param('myfile.a..b...txt', PARAM_FILE));
 711          $this->assertSame('myfile.a.txt', clean_param('myfile.a.txt', PARAM_FILE));
 712          $this->assertSame('myfile...txt', clean_param('myfile...txt', PARAM_FILE));
 713          $this->assertSame('...jpg', clean_param('...jpg', PARAM_FILE));
 714          $this->assertSame('.a.b.', clean_param('.a.b.', PARAM_FILE));
 715          $this->assertSame('', clean_param('.', PARAM_FILE));
 716          $this->assertSame('', clean_param('..', PARAM_FILE));
 717          $this->assertSame('...', clean_param('...', PARAM_FILE));
 718          $this->assertSame('. . . .', clean_param('. . . .', PARAM_FILE));
 719          $this->assertSame('dontrtrim.me. .. .. . ', clean_param('dontrtrim.me. .. .. . ', PARAM_FILE));
 720          $this->assertSame(' . .dontltrim.me', clean_param(' . .dontltrim.me', PARAM_FILE));
 721          $this->assertSame('here is a tab.txt', clean_param("here is a tab\t.txt", PARAM_FILE));
 722          $this->assertSame('here is a linebreak.txt', clean_param("here is a line\r\nbreak.txt", PARAM_FILE));
 723  
 724          // The following behaviours have been maintained although they seem a little odd.
 725          $this->assertSame('funnything', clean_param('funny:thing', PARAM_FILE));
 726          $this->assertSame('.currentdirfile.txt', clean_param('./currentdirfile.txt', PARAM_FILE));
 727          $this->assertSame('ctempwindowsfile.txt', clean_param('c:\temp\windowsfile.txt', PARAM_FILE));
 728          $this->assertSame('homeuserlinuxfile.txt', clean_param('/home/user/linuxfile.txt', PARAM_FILE));
 729          $this->assertSame('~myfile.txt', clean_param('~/myfile.txt', PARAM_FILE));
 730      }
 731  
 732      public function test_clean_param_path() {
 733          $this->assertSame('correctfile.txt', clean_param('correctfile.txt', PARAM_PATH));
 734          $this->assertSame('bad/file.txt', clean_param('b\'a<d`\\/fi:l>e.t"x|t', PARAM_PATH));
 735          $this->assertSame('/parentdirfile.txt', clean_param('../parentdirfile.txt', PARAM_PATH));
 736          $this->assertSame('/grandparentdirfile.txt', clean_param('../../grandparentdirfile.txt', PARAM_PATH));
 737          $this->assertSame('/winparentdirfile.txt', clean_param('..\winparentdirfile.txt', PARAM_PATH));
 738          $this->assertSame('/wingrandparentdir.txt', clean_param('..\..\wingrandparentdir.txt', PARAM_PATH));
 739          $this->assertSame('funnything', clean_param('funny:thing', PARAM_PATH));
 740          $this->assertSame('./here', clean_param('./././here', PARAM_PATH));
 741          $this->assertSame('./currentdirfile.txt', clean_param('./currentdirfile.txt', PARAM_PATH));
 742          $this->assertSame('c/temp/windowsfile.txt', clean_param('c:\temp\windowsfile.txt', PARAM_PATH));
 743          $this->assertSame('/home/user/linuxfile.txt', clean_param('/home/user/linuxfile.txt', PARAM_PATH));
 744          $this->assertSame('/home../user ./.linuxfile.txt', clean_param('/home../user ./.linuxfile.txt', PARAM_PATH));
 745          $this->assertSame('~/myfile.txt', clean_param('~/myfile.txt', PARAM_PATH));
 746          $this->assertSame('~/myfile.txt', clean_param('~/../myfile.txt', PARAM_PATH));
 747          $this->assertSame('/..b../.../myfile.txt', clean_param('/..b../.../myfile.txt', PARAM_PATH));
 748          $this->assertSame('..b../.../myfile.txt', clean_param('..b../.../myfile.txt', PARAM_PATH));
 749          $this->assertSame('/super/slashes/', clean_param('/super//slashes///', PARAM_PATH));
 750      }
 751  
 752      public function test_clean_param_username() {
 753          global $CFG;
 754          $currentstatus =  $CFG->extendedusernamechars;
 755  
 756          // Run tests with extended character == false;.
 757          $CFG->extendedusernamechars = false;
 758          $this->assertSame('johndoe123', clean_param('johndoe123', PARAM_USERNAME) );
 759          $this->assertSame('john.doe', clean_param('john.doe', PARAM_USERNAME));
 760          $this->assertSame('john-doe', clean_param('john-doe', PARAM_USERNAME));
 761          $this->assertSame('john-doe', clean_param('john- doe', PARAM_USERNAME));
 762          $this->assertSame('john_doe', clean_param('john_doe', PARAM_USERNAME));
 763          $this->assertSame('john@doe', clean_param('john@doe', PARAM_USERNAME));
 764          $this->assertSame('johndoe', clean_param('john~doe', PARAM_USERNAME));
 765          $this->assertSame('johndoe', clean_param('john´doe', PARAM_USERNAME));
 766          $this->assertSame(clean_param('john# $%&()+_^', PARAM_USERNAME), 'john_');
 767          $this->assertSame(clean_param(' john# $%&()+_^ ', PARAM_USERNAME), 'john_');
 768          $this->assertSame(clean_param('john#$%&() ', PARAM_USERNAME), 'john');
 769          $this->assertSame('johnd', clean_param('JOHNdóé ', PARAM_USERNAME));
 770          $this->assertSame(clean_param('john.,:;-_/|\ñÑ[]A_X-,D {} ~!@#$%^&*()_+ ?><[] ščřžžý ?ýáž?žý??šdoe ', PARAM_USERNAME), 'john.-_a_x-d@_doe');
 771  
 772          // Test success condition, if extendedusernamechars == ENABLE;.
 773          $CFG->extendedusernamechars = true;
 774          $this->assertSame('john_doe', clean_param('john_doe', PARAM_USERNAME));
 775          $this->assertSame('john@doe', clean_param('john@doe', PARAM_USERNAME));
 776          $this->assertSame(clean_param('john# $%&()+_^', PARAM_USERNAME), 'john# $%&()+_^');
 777          $this->assertSame(clean_param(' john# $%&()+_^ ', PARAM_USERNAME), 'john# $%&()+_^');
 778          $this->assertSame('john~doe', clean_param('john~doe', PARAM_USERNAME));
 779          $this->assertSame('john´doe', clean_param('joHN´doe', PARAM_USERNAME));
 780          $this->assertSame('johndoe', clean_param('johnDOE', PARAM_USERNAME));
 781          $this->assertSame('johndóé', clean_param('johndóé ', PARAM_USERNAME));
 782  
 783          $CFG->extendedusernamechars = $currentstatus;
 784      }
 785  
 786      public function test_clean_param_stringid() {
 787          // Test string identifiers validation.
 788          // Valid strings.
 789          $this->assertSame('validstring', clean_param('validstring', PARAM_STRINGID));
 790          $this->assertSame('mod/foobar:valid_capability', clean_param('mod/foobar:valid_capability', PARAM_STRINGID));
 791          $this->assertSame('CZ', clean_param('CZ', PARAM_STRINGID));
 792          $this->assertSame('application/vnd.ms-powerpoint', clean_param('application/vnd.ms-powerpoint', PARAM_STRINGID));
 793          $this->assertSame('grade2', clean_param('grade2', PARAM_STRINGID));
 794          // Invalid strings.
 795          $this->assertSame('', clean_param('trailing ', PARAM_STRINGID));
 796          $this->assertSame('', clean_param('space bar', PARAM_STRINGID));
 797          $this->assertSame('', clean_param('0numeric', PARAM_STRINGID));
 798          $this->assertSame('', clean_param('*', PARAM_STRINGID));
 799          $this->assertSame('', clean_param(' ', PARAM_STRINGID));
 800      }
 801  
 802      public function test_clean_param_timezone() {
 803          // Test timezone validation.
 804          $testvalues = array (
 805              'America/Jamaica'                => 'America/Jamaica',
 806              'America/Argentina/Cordoba'      => 'America/Argentina/Cordoba',
 807              'America/Port-au-Prince'         => 'America/Port-au-Prince',
 808              'America/Argentina/Buenos_Aires' => 'America/Argentina/Buenos_Aires',
 809              'PST8PDT'                        => 'PST8PDT',
 810              'Wrong.Value'                    => '',
 811              'Wrong/.Value'                   => '',
 812              'Wrong(Value)'                   => '',
 813              '0'                              => '0',
 814              '0.0'                            => '0.0',
 815              '0.5'                            => '0.5',
 816              '9.0'                            => '9.0',
 817              '-9.0'                           => '-9.0',
 818              '+9.0'                           => '+9.0',
 819              '9.5'                            => '9.5',
 820              '-9.5'                           => '-9.5',
 821              '+9.5'                           => '+9.5',
 822              '12.0'                           => '12.0',
 823              '-12.0'                          => '-12.0',
 824              '+12.0'                          => '+12.0',
 825              '12.5'                           => '12.5',
 826              '-12.5'                          => '-12.5',
 827              '+12.5'                          => '+12.5',
 828              '13.0'                           => '13.0',
 829              '-13.0'                          => '-13.0',
 830              '+13.0'                          => '+13.0',
 831              '13.5'                           => '',
 832              '+13.5'                          => '',
 833              '-13.5'                          => '',
 834              '0.2'                            => '');
 835  
 836          foreach ($testvalues as $testvalue => $expectedvalue) {
 837              $actualvalue = clean_param($testvalue, PARAM_TIMEZONE);
 838              $this->assertEquals($expectedvalue, $actualvalue);
 839          }
 840      }
 841  
 842      public function test_validate_param() {
 843          try {
 844              $param = validate_param('11a', PARAM_INT);
 845              $this->fail('invalid_parameter_exception expected');
 846          } catch (\moodle_exception $ex) {
 847              $this->assertInstanceOf('invalid_parameter_exception', $ex);
 848          }
 849  
 850          $param = validate_param('11', PARAM_INT);
 851          $this->assertSame(11, $param);
 852  
 853          try {
 854              $param = validate_param(null, PARAM_INT, false);
 855              $this->fail('invalid_parameter_exception expected');
 856          } catch (\moodle_exception $ex) {
 857              $this->assertInstanceOf('invalid_parameter_exception', $ex);
 858          }
 859  
 860          $param = validate_param(null, PARAM_INT, true);
 861          $this->assertSame(null, $param);
 862  
 863          try {
 864              $param = validate_param(array(), PARAM_INT);
 865              $this->fail('invalid_parameter_exception expected');
 866          } catch (\moodle_exception $ex) {
 867              $this->assertInstanceOf('invalid_parameter_exception', $ex);
 868          }
 869          try {
 870              $param = validate_param(new \stdClass, PARAM_INT);
 871              $this->fail('invalid_parameter_exception expected');
 872          } catch (\moodle_exception $ex) {
 873              $this->assertInstanceOf('invalid_parameter_exception', $ex);
 874          }
 875  
 876          $param = validate_param('1.0', PARAM_FLOAT);
 877          $this->assertSame(1.0, $param);
 878  
 879          // Make sure valid floats do not cause exception.
 880          validate_param(1.0, PARAM_FLOAT);
 881          validate_param(10, PARAM_FLOAT);
 882          validate_param('0', PARAM_FLOAT);
 883          validate_param('119813454.545464564564546564545646556564465465456465465465645645465645645645', PARAM_FLOAT);
 884          validate_param('011.1', PARAM_FLOAT);
 885          validate_param('11', PARAM_FLOAT);
 886          validate_param('+.1', PARAM_FLOAT);
 887          validate_param('-.1', PARAM_FLOAT);
 888          validate_param('1e10', PARAM_FLOAT);
 889          validate_param('.1e+10', PARAM_FLOAT);
 890          validate_param('1E-1', PARAM_FLOAT);
 891  
 892          try {
 893              $param = validate_param('1,2', PARAM_FLOAT);
 894              $this->fail('invalid_parameter_exception expected');
 895          } catch (\moodle_exception $ex) {
 896              $this->assertInstanceOf('invalid_parameter_exception', $ex);
 897          }
 898          try {
 899              $param = validate_param('', PARAM_FLOAT);
 900              $this->fail('invalid_parameter_exception expected');
 901          } catch (\moodle_exception $ex) {
 902              $this->assertInstanceOf('invalid_parameter_exception', $ex);
 903          }
 904          try {
 905              $param = validate_param('.', PARAM_FLOAT);
 906              $this->fail('invalid_parameter_exception expected');
 907          } catch (\moodle_exception $ex) {
 908              $this->assertInstanceOf('invalid_parameter_exception', $ex);
 909          }
 910          try {
 911              $param = validate_param('e10', PARAM_FLOAT);
 912              $this->fail('invalid_parameter_exception expected');
 913          } catch (\moodle_exception $ex) {
 914              $this->assertInstanceOf('invalid_parameter_exception', $ex);
 915          }
 916          try {
 917              $param = validate_param('abc', PARAM_FLOAT);
 918              $this->fail('invalid_parameter_exception expected');
 919          } catch (\moodle_exception $ex) {
 920              $this->assertInstanceOf('invalid_parameter_exception', $ex);
 921          }
 922      }
 923  
 924      public function test_shorten_text_no_tags_already_short_enough() {
 925          // ......12345678901234567890123456.
 926          $text = "short text already no tags";
 927          $this->assertSame($text, shorten_text($text));
 928      }
 929  
 930      public function test_shorten_text_with_tags_already_short_enough() {
 931          // .........123456...7890....12345678.......901234567.
 932          $text = "<p>short <b>text</b> already</p><p>with tags</p>";
 933          $this->assertSame($text, shorten_text($text));
 934      }
 935  
 936      public function test_shorten_text_no_tags_needs_shortening() {
 937          // Default truncation is after 30 chars, but allowing 3 for the final '...'.
 938          // ......12345678901234567890123456789023456789012345678901234.
 939          $text = "long text without any tags blah de blah blah blah what";
 940          $this->assertSame('long text without any tags ...', shorten_text($text));
 941      }
 942  
 943      public function test_shorten_text_with_tags_needs_shortening() {
 944          // .......................................123456789012345678901234567890...
 945          $text = "<div class='frog'><p><blockquote>Long text with tags that will ".
 946              "be chopped off but <b>should be added back again</b></blockquote></p></div>";
 947          $this->assertEquals("<div class='frog'><p><blockquote>Long text with " .
 948              "tags that ...</blockquote></p></div>", shorten_text($text));
 949      }
 950  
 951      public function test_shorten_text_with_tags_and_html_comment() {
 952          $text = "<div class='frog'><p><blockquote><!--[if !IE]><!-->Long text with ".
 953              "tags that will<!--<![endif]--> ".
 954              "be chopped off but <b>should be added back again</b></blockquote></p></div>";
 955          $this->assertEquals("<div class='frog'><p><blockquote><!--[if !IE]><!-->Long text with " .
 956              "tags that ...<!--<![endif]--></blockquote></p></div>", shorten_text($text));
 957      }
 958  
 959      public function test_shorten_text_with_entities() {
 960          // Remember to allow 3 chars for the final '...'.
 961          // ......123456789012345678901234567_____890...
 962          $text = "some text which shouldn't &nbsp; break there";
 963          $this->assertSame("some text which shouldn't &nbsp; ...", shorten_text($text, 31));
 964          $this->assertSame("some text which shouldn't &nbsp;...", shorten_text($text, 30));
 965          $this->assertSame("some text which shouldn't ...", shorten_text($text, 29));
 966      }
 967  
 968      public function test_shorten_text_known_tricky_case() {
 969          // This case caused a bug up to 1.9.5
 970          // ..........123456789012345678901234567890123456789.....0_____1___2___...
 971          $text = "<h3>standard 'break-out' sub groups in TGs?</h3>&nbsp;&lt;&lt;There are several";
 972          $this->assertSame("<h3>standard 'break-out' sub groups in ...</h3>",
 973              shorten_text($text, 41));
 974          $this->assertSame("<h3>standard 'break-out' sub groups in TGs?...</h3>",
 975              shorten_text($text, 42));
 976          $this->assertSame("<h3>standard 'break-out' sub groups in TGs?</h3>&nbsp;...",
 977              shorten_text($text, 43));
 978      }
 979  
 980      public function test_shorten_text_no_spaces() {
 981          // ..........123456789.
 982          $text = "<h1>123456789</h1>"; // A string with no convenient breaks.
 983          $this->assertSame("<h1>12345...</h1>", shorten_text($text, 8));
 984      }
 985  
 986      public function test_shorten_text_utf8_european() {
 987          // Text without tags.
 988          // ......123456789012345678901234567.
 989          $text = "Žluťoučký koníček přeskočil";
 990          $this->assertSame($text, shorten_text($text)); // 30 chars by default.
 991          $this->assertSame("Žluťoučký koníče...", shorten_text($text, 19, true));
 992          $this->assertSame("Žluťoučký ...", shorten_text($text, 19, false));
 993          // And try it with 2-less (that are, in bytes, the middle of a sequence).
 994          $this->assertSame("Žluťoučký koní...", shorten_text($text, 17, true));
 995          $this->assertSame("Žluťoučký ...", shorten_text($text, 17, false));
 996  
 997          // .........123456789012345678...901234567....89012345.
 998          $text = "<p>Žluťoučký koníček <b>přeskočil</b> potůček</p>";
 999          $this->assertSame($text, shorten_text($text, 60));
1000          $this->assertSame("<p>Žluťoučký koníček ...</p>", shorten_text($text, 21));
1001          $this->assertSame("<p>Žluťoučký koníče...</p>", shorten_text($text, 19, true));
1002          $this->assertSame("<p>Žluťoučký ...</p>", shorten_text($text, 19, false));
1003          // And try it with 2 fewer (that are, in bytes, the middle of a sequence).
1004          $this->assertSame("<p>Žluťoučký koní...</p>", shorten_text($text, 17, true));
1005          $this->assertSame("<p>Žluťoučký ...</p>", shorten_text($text, 17, false));
1006          // And try over one tag (start/end), it does proper text len.
1007          $this->assertSame("<p>Žluťoučký koníček <b>př...</b></p>", shorten_text($text, 23, true));
1008          $this->assertSame("<p>Žluťoučký koníček <b>přeskočil</b> pot...</p>", shorten_text($text, 34, true));
1009          // And in the middle of one tag.
1010          $this->assertSame("<p>Žluťoučký koníček <b>přeskočil...</b></p>", shorten_text($text, 30, true));
1011      }
1012  
1013      public function test_shorten_text_utf8_oriental() {
1014          // Japanese
1015          // text without tags
1016          // ......123456789012345678901234.
1017          $text = '言語設定言語設定abcdefghijkl';
1018          $this->assertSame($text, shorten_text($text)); // 30 chars by default.
1019          $this->assertSame("言語設定言語...", shorten_text($text, 9, true));
1020          $this->assertSame("言語設定言語...", shorten_text($text, 9, false));
1021          $this->assertSame("言語設定言語設定ab...", shorten_text($text, 13, true));
1022          $this->assertSame("言語設定言語設定...", shorten_text($text, 13, false));
1023  
1024          // Chinese
1025          // text without tags
1026          // ......123456789012345678901234.
1027          $text = '简体中文简体中文abcdefghijkl';
1028          $this->assertSame($text, shorten_text($text)); // 30 chars by default.
1029          $this->assertSame("简体中文简体...", shorten_text($text, 9, true));
1030          $this->assertSame("简体中文简体...", shorten_text($text, 9, false));
1031          $this->assertSame("简体中文简体中文ab...", shorten_text($text, 13, true));
1032          $this->assertSame("简体中文简体中文...", shorten_text($text, 13, false));
1033      }
1034  
1035      public function test_shorten_text_multilang() {
1036          // This is not necessaryily specific to multilang. The issue is really
1037          // tags with attributes, where before we were generating invalid HTML
1038          // output like shorten_text('<span id="x" class="y">A</span> B', 1)
1039          // returning '<span id="x" ...</span>'. It is just that multilang
1040          // requires the sort of HTML that is quite likely to trigger this.
1041          // ........................................1...
1042          $text = '<span lang="en" class="multilang">A</span>' .
1043                  '<span lang="fr" class="multilang">B</span>';
1044          $this->assertSame('<span lang="en" class="multilang">...</span>',
1045                  shorten_text($text, 1));
1046      }
1047  
1048      /**
1049       * Provider for long filenames and its expected result, with and without hash.
1050       *
1051       * @return array of ($filename, $length, $expected, $includehash)
1052       */
1053      public function shorten_filename_provider() {
1054          $filename = 'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium totam rem';
1055          $shortfilename = 'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque';
1056  
1057          return [
1058              'More than 100 characters' => [
1059                  $filename,
1060                  null,
1061                  'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium tot',
1062                  false,
1063              ],
1064              'More than 100 characters with hash' => [
1065                  $filename,
1066                  null,
1067                  'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque l - 3bec1da8b8',
1068                  true,
1069              ],
1070              'More than 100 characters with extension' => [
1071                  "{$filename}.zip",
1072                  null,
1073                  'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium tot.zip',
1074                  false,
1075              ],
1076              'More than 100 characters with extension and hash' => [
1077                  "{$filename}.zip",
1078                  null,
1079                  'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque l - 3bec1da8b8.zip',
1080                  true,
1081              ],
1082              'Limit filename to 50 chars' => [
1083                  $filename,
1084                  50,
1085                  'sed ut perspiciatis unde omnis iste natus error si',
1086                  false,
1087              ],
1088              'Limit filename to 50 chars with hash' => [
1089                  $filename,
1090                  50,
1091                  'sed ut perspiciatis unde omnis iste n - 3bec1da8b8',
1092                  true,
1093              ],
1094              'Limit filename to 50 chars with extension' => [
1095                  "{$filename}.zip",
1096                  50,
1097                  'sed ut perspiciatis unde omnis iste natus error si.zip',
1098                  false,
1099              ],
1100              'Limit filename to 50 chars with extension and hash' => [
1101                  "{$filename}.zip",
1102                  50,
1103                  'sed ut perspiciatis unde omnis iste n - 3bec1da8b8.zip',
1104                  true,
1105              ],
1106              'Test filename that contains less than 100 characters' => [
1107                  $shortfilename,
1108                  null,
1109                  $shortfilename,
1110                  false,
1111              ],
1112              'Test filename that contains less than 100 characters and hash' => [
1113                  $shortfilename,
1114                  null,
1115                  $shortfilename,
1116                  true,
1117              ],
1118              'Test filename that contains less than 100 characters with extension' => [
1119                  "{$shortfilename}.zip",
1120                  null,
1121                  "{$shortfilename}.zip",
1122                  false,
1123              ],
1124              'Test filename that contains less than 100 characters with extension and hash' => [
1125                  "{$shortfilename}.zip",
1126                  null,
1127                  "{$shortfilename}.zip",
1128                  true,
1129              ],
1130          ];
1131      }
1132  
1133      /**
1134       * Test the {@link shorten_filename()} method.
1135       *
1136       * @dataProvider shorten_filename_provider
1137       *
1138       * @param string $filename
1139       * @param int $length
1140       * @param string $expected
1141       * @param boolean $includehash
1142       */
1143      public function test_shorten_filename($filename, $length, $expected, $includehash) {
1144          if (null === $length) {
1145              $length = MAX_FILENAME_SIZE;
1146          }
1147  
1148          $this->assertSame($expected, shorten_filename($filename, $length, $includehash));
1149      }
1150  
1151      /**
1152       * Provider for long filenames and its expected result, with and without hash.
1153       *
1154       * @return array of ($filename, $length, $expected, $includehash)
1155       */
1156      public function shorten_filenames_provider() {
1157          $shortfilename = 'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque';
1158          $longfilename = 'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium totam rem';
1159          $extfilename = $longfilename.'.zip';
1160          $expected = 'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium tot';
1161          $expectedwithhash = 'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque l - 3bec1da8b8';
1162          $expectedext = 'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium tot.zip';
1163          $expectedextwithhash = 'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque l - 3bec1da8b8.zip';
1164          $expected50 = 'sed ut perspiciatis unde omnis iste natus error si';
1165          $expected50withhash = 'sed ut perspiciatis unde omnis iste n - 3bec1da8b8';
1166          $expected50ext = 'sed ut perspiciatis unde omnis iste natus error si.zip';
1167          $expected50extwithhash = 'sed ut perspiciatis unde omnis iste n - 3bec1da8b8.zip';
1168          $expected50short = 'sed ut perspiciatis unde omnis iste n - 5fb6543490';
1169  
1170          return [
1171              'Empty array without hash' => [
1172                  [],
1173                  null,
1174                  [],
1175                  false,
1176              ],
1177              'Empty array with hash' => [
1178                  [],
1179                  null,
1180                  [],
1181                  true,
1182              ],
1183              'Array with less than 100 characters' => [
1184                  [$shortfilename, $shortfilename, $shortfilename],
1185                  null,
1186                  [$shortfilename, $shortfilename, $shortfilename],
1187                  false,
1188              ],
1189              'Array with more than 100 characters without hash' => [
1190                  [$longfilename, $longfilename, $longfilename],
1191                  null,
1192                  [$expected, $expected, $expected],
1193                  false,
1194              ],
1195              'Array with more than 100 characters with hash' => [
1196                  [$longfilename, $longfilename, $longfilename],
1197                  null,
1198                  [$expectedwithhash, $expectedwithhash, $expectedwithhash],
1199                  true,
1200              ],
1201              'Array with more than 100 characters with extension' => [
1202                  [$extfilename, $extfilename, $extfilename],
1203                  null,
1204                  [$expectedext, $expectedext, $expectedext],
1205                  false,
1206              ],
1207              'Array with more than 100 characters with extension and hash' => [
1208                  [$extfilename, $extfilename, $extfilename],
1209                  null,
1210                  [$expectedextwithhash, $expectedextwithhash, $expectedextwithhash],
1211                  true,
1212              ],
1213              'Array with more than 100 characters mix (short, long, with extension) without hash' => [
1214                  [$shortfilename, $longfilename, $extfilename],
1215                  null,
1216                  [$shortfilename, $expected, $expectedext],
1217                  false,
1218              ],
1219              'Array with more than 100 characters mix (short, long, with extension) with hash' => [
1220                  [$shortfilename, $longfilename, $extfilename],
1221                  null,
1222                  [$shortfilename, $expectedwithhash, $expectedextwithhash],
1223                  true,
1224              ],
1225              'Array with less than 50 characters without hash' => [
1226                  [$longfilename, $longfilename, $longfilename],
1227                  50,
1228                  [$expected50, $expected50, $expected50],
1229                  false,
1230              ],
1231              'Array with less than 50 characters with hash' => [
1232                  [$longfilename, $longfilename, $longfilename],
1233                  50,
1234                  [$expected50withhash, $expected50withhash, $expected50withhash],
1235                  true,
1236              ],
1237              'Array with less than 50 characters with extension' => [
1238                  [$extfilename, $extfilename, $extfilename],
1239                  50,
1240                  [$expected50ext, $expected50ext, $expected50ext],
1241                  false,
1242              ],
1243              'Array with less than 50 characters with extension and hash' => [
1244                  [$extfilename, $extfilename, $extfilename],
1245                  50,
1246                  [$expected50extwithhash, $expected50extwithhash, $expected50extwithhash],
1247                  true,
1248              ],
1249              'Array with less than 50 characters mix (short, long, with extension) without hash' => [
1250                  [$shortfilename, $longfilename, $extfilename],
1251                  50,
1252                  [$expected50, $expected50, $expected50ext],
1253                  false,
1254              ],
1255              'Array with less than 50 characters mix (short, long, with extension) with hash' => [
1256                  [$shortfilename, $longfilename, $extfilename],
1257                  50,
1258                  [$expected50short, $expected50withhash, $expected50extwithhash],
1259                  true,
1260              ],
1261          ];
1262      }
1263  
1264      /**
1265       * Test the {@link shorten_filenames()} method.
1266       *
1267       * @dataProvider shorten_filenames_provider
1268       *
1269       * @param string $filenames
1270       * @param int $length
1271       * @param string $expected
1272       * @param boolean $includehash
1273       */
1274      public function test_shorten_filenames($filenames, $length, $expected, $includehash) {
1275          if (null === $length) {
1276              $length = MAX_FILENAME_SIZE;
1277          }
1278  
1279          $this->assertSame($expected, shorten_filenames($filenames, $length, $includehash));
1280      }
1281  
1282      public function test_usergetdate() {
1283          global $USER, $CFG, $DB;
1284          $this->resetAfterTest();
1285  
1286          $this->setAdminUser();
1287  
1288          $USER->timezone = 2;// Set the timezone to a known state.
1289  
1290          $ts = 1261540267; // The time this function was created.
1291  
1292          $arr = usergetdate($ts, 1); // Specify the timezone as an argument.
1293          $arr = array_values($arr);
1294  
1295          list($seconds, $minutes, $hours, $mday, $wday, $mon, $year, $yday, $weekday, $month) = $arr;
1296          $this->assertSame(7, $seconds);
1297          $this->assertSame(51, $minutes);
1298          $this->assertSame(4, $hours);
1299          $this->assertSame(23, $mday);
1300          $this->assertSame(3, $wday);
1301          $this->assertSame(12, $mon);
1302          $this->assertSame(2009, $year);
1303          $this->assertSame(356, $yday);
1304          $this->assertSame('Wednesday', $weekday);
1305          $this->assertSame('December', $month);
1306          $arr = usergetdate($ts); // Gets the timezone from the $USER object.
1307          $arr = array_values($arr);
1308  
1309          list($seconds, $minutes, $hours, $mday, $wday, $mon, $year, $yday, $weekday, $month) = $arr;
1310          $this->assertSame(7, $seconds);
1311          $this->assertSame(51, $minutes);
1312          $this->assertSame(5, $hours);
1313          $this->assertSame(23, $mday);
1314          $this->assertSame(3, $wday);
1315          $this->assertSame(12, $mon);
1316          $this->assertSame(2009, $year);
1317          $this->assertSame(356, $yday);
1318          $this->assertSame('Wednesday', $weekday);
1319          $this->assertSame('December', $month);
1320  
1321          // Edge cases - 0 and null - they all mean 1st Jan 1970. Null shows debugging message.
1322          $this->assertSame(1970, usergetdate(0)['year']);
1323          $this->assertDebuggingNotCalled();
1324          $this->assertSame(1970, usergetdate(null)['year']);
1325          $this->assertDebuggingCalled(null, DEBUG_DEVELOPER);
1326      }
1327  
1328      public function test_mark_user_preferences_changed() {
1329          $this->resetAfterTest();
1330          $otheruser = $this->getDataGenerator()->create_user();
1331          $otheruserid = $otheruser->id;
1332  
1333          set_cache_flag('userpreferenceschanged', $otheruserid, null);
1334          mark_user_preferences_changed($otheruserid);
1335  
1336          $this->assertEquals(get_cache_flag('userpreferenceschanged', $otheruserid, time()-10), 1);
1337          set_cache_flag('userpreferenceschanged', $otheruserid, null);
1338      }
1339  
1340      public function test_check_user_preferences_loaded() {
1341          global $DB;
1342          $this->resetAfterTest();
1343  
1344          $otheruser = $this->getDataGenerator()->create_user();
1345          $otheruserid = $otheruser->id;
1346  
1347          $DB->delete_records('user_preferences', array('userid'=>$otheruserid));
1348          set_cache_flag('userpreferenceschanged', $otheruserid, null);
1349  
1350          $user = new \stdClass();
1351          $user->id = $otheruserid;
1352  
1353          // Load.
1354          check_user_preferences_loaded($user);
1355          $this->assertTrue(isset($user->preference));
1356          $this->assertTrue(is_array($user->preference));
1357          $this->assertArrayHasKey('_lastloaded', $user->preference);
1358          $this->assertCount(1, $user->preference);
1359  
1360          // Add preference via direct call.
1361          $DB->insert_record('user_preferences', array('name'=>'xxx', 'value'=>'yyy', 'userid'=>$user->id));
1362  
1363          // No cache reload yet.
1364          check_user_preferences_loaded($user);
1365          $this->assertCount(1, $user->preference);
1366  
1367          // Forced reloading of cache.
1368          unset($user->preference);
1369          check_user_preferences_loaded($user);
1370          $this->assertCount(2, $user->preference);
1371          $this->assertSame('yyy', $user->preference['xxx']);
1372  
1373          // Add preference via direct call.
1374          $DB->insert_record('user_preferences', array('name'=>'aaa', 'value'=>'bbb', 'userid'=>$user->id));
1375  
1376          // Test timeouts and modifications from different session.
1377          set_cache_flag('userpreferenceschanged', $user->id, 1, time() + 1000);
1378          $user->preference['_lastloaded'] = $user->preference['_lastloaded'] - 20;
1379          check_user_preferences_loaded($user);
1380          $this->assertCount(2, $user->preference);
1381          check_user_preferences_loaded($user, 10);
1382          $this->assertCount(3, $user->preference);
1383          $this->assertSame('bbb', $user->preference['aaa']);
1384          set_cache_flag('userpreferenceschanged', $user->id, null);
1385      }
1386  
1387      public function test_set_user_preference() {
1388          global $DB, $USER;
1389          $this->resetAfterTest();
1390  
1391          $this->setAdminUser();
1392  
1393          $otheruser = $this->getDataGenerator()->create_user();
1394          $otheruserid = $otheruser->id;
1395  
1396          $DB->delete_records('user_preferences', array('userid'=>$otheruserid));
1397          set_cache_flag('userpreferenceschanged', $otheruserid, null);
1398  
1399          $user = new \stdClass();
1400          $user->id = $otheruserid;
1401  
1402          set_user_preference('aaa', 'bbb', $otheruserid);
1403          $this->assertSame('bbb', $DB->get_field('user_preferences', 'value', array('userid'=>$otheruserid, 'name'=>'aaa')));
1404          $this->assertSame('bbb', get_user_preferences('aaa', null, $otheruserid));
1405  
1406          set_user_preference('xxx', 'yyy', $user);
1407          $this->assertSame('yyy', $DB->get_field('user_preferences', 'value', array('userid'=>$otheruserid, 'name'=>'xxx')));
1408          $this->assertSame('yyy', get_user_preferences('xxx', null, $otheruserid));
1409          $this->assertTrue(is_array($user->preference));
1410          $this->assertSame('bbb', $user->preference['aaa']);
1411          $this->assertSame('yyy', $user->preference['xxx']);
1412  
1413          set_user_preference('xxx', null, $user);
1414          $this->assertFalse($DB->get_field('user_preferences', 'value', array('userid'=>$otheruserid, 'name'=>'xxx')));
1415          $this->assertNull(get_user_preferences('xxx', null, $otheruserid));
1416  
1417          set_user_preference('ooo', true, $user);
1418          $prefs = get_user_preferences(null, null, $otheruserid);
1419          $this->assertSame($user->preference['aaa'], $prefs['aaa']);
1420          $this->assertSame($user->preference['ooo'], $prefs['ooo']);
1421          $this->assertSame('1', $prefs['ooo']);
1422  
1423          set_user_preference('null', 0, $user);
1424          $this->assertSame('0', get_user_preferences('null', null, $otheruserid));
1425  
1426          $this->assertSame('lala', get_user_preferences('undefined', 'lala', $otheruserid));
1427  
1428          $DB->delete_records('user_preferences', array('userid'=>$otheruserid));
1429          set_cache_flag('userpreferenceschanged', $otheruserid, null);
1430  
1431          // Test $USER default.
1432          set_user_preference('_test_user_preferences_pref', 'ok');
1433          $this->assertSame('ok', $USER->preference['_test_user_preferences_pref']);
1434          unset_user_preference('_test_user_preferences_pref');
1435          $this->assertTrue(!isset($USER->preference['_test_user_preferences_pref']));
1436  
1437          // Test 1333 char values (no need for unicode, there are already tests for that in DB tests).
1438          $longvalue = str_repeat('a', 1333);
1439          set_user_preference('_test_long_user_preference', $longvalue);
1440          $this->assertEquals($longvalue, get_user_preferences('_test_long_user_preference'));
1441          $this->assertEquals($longvalue,
1442              $DB->get_field('user_preferences', 'value', array('userid' => $USER->id, 'name' => '_test_long_user_preference')));
1443  
1444          // Test > 1333 char values, coding_exception expected.
1445          $longvalue = str_repeat('a', 1334);
1446          try {
1447              set_user_preference('_test_long_user_preference', $longvalue);
1448              $this->fail('Exception expected - longer than 1333 chars not allowed as preference value');
1449          } catch (\moodle_exception $ex) {
1450              $this->assertInstanceOf('coding_exception', $ex);
1451          }
1452  
1453          // Test invalid params.
1454          try {
1455              set_user_preference('_test_user_preferences_pref', array());
1456              $this->fail('Exception expected - array not valid preference value');
1457          } catch (\moodle_exception $ex) {
1458              $this->assertInstanceOf('coding_exception', $ex);
1459          }
1460          try {
1461              set_user_preference('_test_user_preferences_pref', new \stdClass);
1462              $this->fail('Exception expected - class not valid preference value');
1463          } catch (\moodle_exception $ex) {
1464              $this->assertInstanceOf('coding_exception', $ex);
1465          }
1466          try {
1467              set_user_preference('_test_user_preferences_pref', 1, array('xx' => 1));
1468              $this->fail('Exception expected - user instance expected');
1469          } catch (\moodle_exception $ex) {
1470              $this->assertInstanceOf('coding_exception', $ex);
1471          }
1472          try {
1473              set_user_preference('_test_user_preferences_pref', 1, 'abc');
1474              $this->fail('Exception expected - user instance expected');
1475          } catch (\moodle_exception $ex) {
1476              $this->assertInstanceOf('coding_exception', $ex);
1477          }
1478          try {
1479              set_user_preference('', 1);
1480              $this->fail('Exception expected - invalid name accepted');
1481          } catch (\moodle_exception $ex) {
1482              $this->assertInstanceOf('coding_exception', $ex);
1483          }
1484          try {
1485              set_user_preference('1', 1);
1486              $this->fail('Exception expected - invalid name accepted');
1487          } catch (\moodle_exception $ex) {
1488              $this->assertInstanceOf('coding_exception', $ex);
1489          }
1490      }
1491  
1492      public function test_set_user_preference_for_current_user() {
1493          global $USER;
1494          $this->resetAfterTest();
1495          $this->setAdminUser();
1496  
1497          set_user_preference('test_pref', 2);
1498          set_user_preference('test_pref', 1, $USER->id);
1499          $this->assertEquals(1, get_user_preferences('test_pref'));
1500      }
1501  
1502      public function test_unset_user_preference_for_current_user() {
1503          global $USER;
1504          $this->resetAfterTest();
1505          $this->setAdminUser();
1506  
1507          set_user_preference('test_pref', 1);
1508          unset_user_preference('test_pref', $USER->id);
1509          $this->assertNull(get_user_preferences('test_pref'));
1510      }
1511  
1512      /**
1513       * Test essential features implementation of {@link get_extra_user_fields()} as the admin user with all capabilities.
1514       *
1515       * @deprecated since Moodle 3.11 MDL-45242
1516       */
1517      public function test_get_extra_user_fields_essentials() {
1518          global $CFG, $USER, $DB;
1519          $this->resetAfterTest();
1520  
1521          $this->setAdminUser();
1522          $context = \context_system::instance();
1523  
1524          // No fields.
1525          $CFG->showuseridentity = '';
1526          $this->assertEquals(array(), get_extra_user_fields($context));
1527  
1528          // One field.
1529          $CFG->showuseridentity = 'frog';
1530          $this->assertEquals(array('frog'), get_extra_user_fields($context));
1531  
1532          // Two fields.
1533          $CFG->showuseridentity = 'frog,zombie';
1534          $this->assertEquals(array('frog', 'zombie'), get_extra_user_fields($context));
1535  
1536          // No fields, except.
1537          $CFG->showuseridentity = '';
1538          $this->assertEquals(array(), get_extra_user_fields($context, array('frog')));
1539  
1540          // One field.
1541          $CFG->showuseridentity = 'frog';
1542          $this->assertEquals(array(), get_extra_user_fields($context, array('frog')));
1543  
1544          // Two fields.
1545          $CFG->showuseridentity = 'frog,zombie';
1546          $this->assertEquals(array('zombie'), get_extra_user_fields($context, array('frog')));
1547  
1548          $this->assertDebuggingCalledCount(6);
1549      }
1550  
1551      /**
1552       * Prepare environment for couple of tests related to permission checks in {@link get_extra_user_fields()}.
1553       *
1554       * @return stdClass
1555       * @deprecated since Moodle 3.11 MDL-45242
1556       */
1557      protected function environment_for_get_extra_user_fields_tests() {
1558          global $CFG, $DB;
1559  
1560          $CFG->showuseridentity = 'idnumber,country,city';
1561          $CFG->hiddenuserfields = 'country,city';
1562  
1563          $env = new \stdClass();
1564  
1565          $env->course = $this->getDataGenerator()->create_course();
1566          $env->coursecontext = \context_course::instance($env->course->id);
1567  
1568          $env->teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));
1569          $env->studentrole = $DB->get_record('role', array('shortname' => 'student'));
1570          $env->managerrole = $DB->get_record('role', array('shortname' => 'manager'));
1571  
1572          $env->student = $this->getDataGenerator()->create_user();
1573          $env->teacher = $this->getDataGenerator()->create_user();
1574          $env->manager = $this->getDataGenerator()->create_user();
1575  
1576          role_assign($env->studentrole->id, $env->student->id, $env->coursecontext->id);
1577          role_assign($env->teacherrole->id, $env->teacher->id, $env->coursecontext->id);
1578          role_assign($env->managerrole->id, $env->manager->id, SYSCONTEXTID);
1579  
1580          return $env;
1581      }
1582  
1583      /**
1584       * No identity fields shown to student user (no permission to view identity fields).
1585       *
1586       * @deprecated since Moodle 3.11 MDL-45242
1587       */
1588      public function test_get_extra_user_fields_no_access() {
1589  
1590          $this->resetAfterTest();
1591          $env = $this->environment_for_get_extra_user_fields_tests();
1592          $this->setUser($env->student);
1593  
1594          $this->assertEquals(array(), get_extra_user_fields($env->coursecontext));
1595          $this->assertEquals(array(), get_extra_user_fields(\context_system::instance()));
1596  
1597          $this->assertDebuggingCalledCount(2);
1598      }
1599  
1600      /**
1601       * Teacher can see students' identity fields only within the course.
1602       *
1603       * @deprecated since Moodle 3.11 MDL-45242
1604       */
1605      public function test_get_extra_user_fields_course_only_access() {
1606  
1607          $this->resetAfterTest();
1608          $env = $this->environment_for_get_extra_user_fields_tests();
1609          $this->setUser($env->teacher);
1610  
1611          $this->assertEquals(array('idnumber', 'country', 'city'), get_extra_user_fields($env->coursecontext));
1612          $this->assertEquals(array(), get_extra_user_fields(\context_system::instance()));
1613  
1614          $this->assertDebuggingCalledCount(2);
1615      }
1616  
1617      /**
1618       * Teacher can be prevented from seeing students' identity fields even within the course.
1619       *
1620       * @deprecated since Moodle 3.11 MDL-45242
1621       */
1622      public function test_get_extra_user_fields_course_prevented_access() {
1623  
1624          $this->resetAfterTest();
1625          $env = $this->environment_for_get_extra_user_fields_tests();
1626          $this->setUser($env->teacher);
1627  
1628          assign_capability('moodle/course:viewhiddenuserfields', CAP_PREVENT, $env->teacherrole->id, $env->coursecontext->id);
1629          $this->assertEquals(array('idnumber'), get_extra_user_fields($env->coursecontext));
1630  
1631          $this->assertDebuggingCalledCount(1);
1632      }
1633  
1634      /**
1635       * Manager can see students' identity fields anywhere.
1636       *
1637       * @deprecated since Moodle 3.11 MDL-45242
1638       */
1639      public function test_get_extra_user_fields_anywhere_access() {
1640  
1641          $this->resetAfterTest();
1642          $env = $this->environment_for_get_extra_user_fields_tests();
1643          $this->setUser($env->manager);
1644  
1645          $this->assertEquals(array('idnumber', 'country', 'city'), get_extra_user_fields($env->coursecontext));
1646          $this->assertEquals(array('idnumber', 'country', 'city'), get_extra_user_fields(\context_system::instance()));
1647  
1648          $this->assertDebuggingCalledCount(2);
1649      }
1650  
1651      /**
1652       * Manager can be prevented from seeing hidden fields outside the course.
1653       *
1654       * @deprecated since Moodle 3.11 MDL-45242
1655       */
1656      public function test_get_extra_user_fields_schismatic_access() {
1657  
1658          $this->resetAfterTest();
1659          $env = $this->environment_for_get_extra_user_fields_tests();
1660          $this->setUser($env->manager);
1661  
1662          assign_capability('moodle/user:viewhiddendetails', CAP_PREVENT, $env->managerrole->id, SYSCONTEXTID, true);
1663          $this->assertEquals(array('idnumber'), get_extra_user_fields(\context_system::instance()));
1664          // Note that inside the course, the manager can still see the hidden identifiers as this is currently
1665          // controlled by a separate capability for legacy reasons.
1666          $this->assertEquals(array('idnumber', 'country', 'city'), get_extra_user_fields($env->coursecontext));
1667  
1668          $this->assertDebuggingCalledCount(2);
1669      }
1670  
1671      /**
1672       * Two capabilities must be currently set to prevent manager from seeing hidden fields.
1673       *
1674       * @deprecated since Moodle 3.11 MDL-45242
1675       */
1676      public function test_get_extra_user_fields_hard_to_prevent_access() {
1677  
1678          $this->resetAfterTest();
1679          $env = $this->environment_for_get_extra_user_fields_tests();
1680          $this->setUser($env->manager);
1681  
1682          assign_capability('moodle/user:viewhiddendetails', CAP_PREVENT, $env->managerrole->id, SYSCONTEXTID, true);
1683          assign_capability('moodle/course:viewhiddenuserfields', CAP_PREVENT, $env->managerrole->id, SYSCONTEXTID, true);
1684  
1685          $this->assertEquals(array('idnumber'), get_extra_user_fields(\context_system::instance()));
1686          $this->assertEquals(array('idnumber'), get_extra_user_fields($env->coursecontext));
1687  
1688          $this->assertDebuggingCalledCount(2);
1689      }
1690  
1691      /**
1692       * Tests get_extra_user_fields_sql.
1693       *
1694       * @deprecated since Moodle 3.11 MDL-45242
1695       */
1696      public function test_get_extra_user_fields_sql() {
1697          global $CFG, $USER, $DB;
1698          $this->resetAfterTest();
1699  
1700          $this->setAdminUser();
1701  
1702          $context = \context_system::instance();
1703  
1704          // No fields.
1705          $CFG->showuseridentity = '';
1706          $this->assertSame('', get_extra_user_fields_sql($context));
1707  
1708          // One field.
1709          $CFG->showuseridentity = 'frog';
1710          $this->assertSame(', frog', get_extra_user_fields_sql($context));
1711  
1712          // Two fields with table prefix.
1713          $CFG->showuseridentity = 'frog,zombie';
1714          $this->assertSame(', u1.frog, u1.zombie', get_extra_user_fields_sql($context, 'u1'));
1715  
1716          // Two fields with field prefix.
1717          $CFG->showuseridentity = 'frog,zombie';
1718          $this->assertSame(', frog AS u_frog, zombie AS u_zombie',
1719              get_extra_user_fields_sql($context, '', 'u_'));
1720  
1721          // One field excluded.
1722          $CFG->showuseridentity = 'frog';
1723          $this->assertSame('', get_extra_user_fields_sql($context, '', '', array('frog')));
1724  
1725          // Two fields, one excluded, table+field prefix.
1726          $CFG->showuseridentity = 'frog,zombie';
1727          $this->assertEquals(', u1.zombie AS u_zombie',
1728              get_extra_user_fields_sql($context, 'u1', 'u_', array('frog')));
1729  
1730          $this->assertDebuggingCalledCount(6);
1731      }
1732  
1733      /**
1734       * Test some critical TZ/DST.
1735       *
1736       * This method tests some special TZ/DST combinations that were fixed
1737       * by MDL-38999. The tests are done by comparing the results of the
1738       * output using Moodle TZ/DST support and PHP native one.
1739       *
1740       * Note: If you don't trust PHP TZ/DST support, can verify the
1741       * harcoded expectations below with:
1742       * http://www.tools4noobs.com/online_tools/unix_timestamp_to_datetime/
1743       */
1744      public function test_some_moodle_special_dst() {
1745          $stamp = 1365386400; // 2013/04/08 02:00:00 GMT/UTC.
1746  
1747          // In Europe/Tallinn it was 2013/04/08 05:00:00.
1748          $expectation = '2013/04/08 05:00:00';
1749          $phpdt = \DateTime::createFromFormat('U', $stamp, new \DateTimeZone('UTC'));
1750          $phpdt->setTimezone(new \DateTimeZone('Europe/Tallinn'));
1751          $phpres = $phpdt->format('Y/m/d H:i:s'); // PHP result.
1752          $moodleres = userdate($stamp, '%Y/%m/%d %H:%M:%S', 'Europe/Tallinn', false); // Moodle result.
1753          $this->assertSame($expectation, $phpres);
1754          $this->assertSame($expectation, $moodleres);
1755  
1756          // In St. Johns it was 2013/04/07 23:30:00.
1757          $expectation = '2013/04/07 23:30:00';
1758          $phpdt = \DateTime::createFromFormat('U', $stamp, new \DateTimeZone('UTC'));
1759          $phpdt->setTimezone(new \DateTimeZone('America/St_Johns'));
1760          $phpres = $phpdt->format('Y/m/d H:i:s'); // PHP result.
1761          $moodleres = userdate($stamp, '%Y/%m/%d %H:%M:%S', 'America/St_Johns', false); // Moodle result.
1762          $this->assertSame($expectation, $phpres);
1763          $this->assertSame($expectation, $moodleres);
1764  
1765          $stamp = 1383876000; // 2013/11/08 02:00:00 GMT/UTC.
1766  
1767          // In Europe/Tallinn it was 2013/11/08 04:00:00.
1768          $expectation = '2013/11/08 04:00:00';
1769          $phpdt = \DateTime::createFromFormat('U', $stamp, new \DateTimeZone('UTC'));
1770          $phpdt->setTimezone(new \DateTimeZone('Europe/Tallinn'));
1771          $phpres = $phpdt->format('Y/m/d H:i:s'); // PHP result.
1772          $moodleres = userdate($stamp, '%Y/%m/%d %H:%M:%S', 'Europe/Tallinn', false); // Moodle result.
1773          $this->assertSame($expectation, $phpres);
1774          $this->assertSame($expectation, $moodleres);
1775  
1776          // In St. Johns it was 2013/11/07 22:30:00.
1777          $expectation = '2013/11/07 22:30:00';
1778          $phpdt = \DateTime::createFromFormat('U', $stamp, new \DateTimeZone('UTC'));
1779          $phpdt->setTimezone(new \DateTimeZone('America/St_Johns'));
1780          $phpres = $phpdt->format('Y/m/d H:i:s'); // PHP result.
1781          $moodleres = userdate($stamp, '%Y/%m/%d %H:%M:%S', 'America/St_Johns', false); // Moodle result.
1782          $this->assertSame($expectation, $phpres);
1783          $this->assertSame($expectation, $moodleres);
1784      }
1785  
1786      public function test_userdate() {
1787          global $USER, $CFG, $DB;
1788          $this->resetAfterTest();
1789  
1790          $this->setAdminUser();
1791  
1792          $testvalues = array(
1793              array(
1794                  'time' => '1309514400',
1795                  'usertimezone' => 'America/Moncton',
1796                  'timezone' => '0.0', // No dst offset.
1797                  'expectedoutput' => 'Friday, 1 July 2011, 10:00 AM',
1798                  'expectedoutputhtml' => '<time datetime="2011-07-01T07:00:00-03:00">Friday, 1 July 2011, 10:00 AM</time>'
1799              ),
1800              array(
1801                  'time' => '1309514400',
1802                  'usertimezone' => 'America/Moncton',
1803                  'timezone' => '99', // Dst offset and timezone offset.
1804                  'expectedoutput' => 'Friday, 1 July 2011, 7:00 AM',
1805                  'expectedoutputhtml' => '<time datetime="2011-07-01T07:00:00-03:00">Friday, 1 July 2011, 7:00 AM</time>'
1806              ),
1807              array(
1808                  'time' => '1309514400',
1809                  'usertimezone' => 'America/Moncton',
1810                  'timezone' => 'America/Moncton', // Dst offset and timezone offset.
1811                  'expectedoutput' => 'Friday, 1 July 2011, 7:00 AM',
1812                  'expectedoutputhtml' => '<time datetime="2011-07-01t07:00:00-03:00">Friday, 1 July 2011, 7:00 AM</time>'
1813              ),
1814              array(
1815                  'time' => '1293876000 ',
1816                  'usertimezone' => 'America/Moncton',
1817                  'timezone' => '0.0', // No dst offset.
1818                  'expectedoutput' => 'Saturday, 1 January 2011, 10:00 AM',
1819                  'expectedoutputhtml' => '<time datetime="2011-01-01T06:00:00-04:00">Saturday, 1 January 2011, 10:00 AM</time>'
1820              ),
1821              array(
1822                  'time' => '1293876000 ',
1823                  'usertimezone' => 'America/Moncton',
1824                  'timezone' => '99', // No dst offset in jan, so just timezone offset.
1825                  'expectedoutput' => 'Saturday, 1 January 2011, 6:00 AM',
1826                  'expectedoutputhtml' => '<time datetime="2011-01-01T06:00:00-04:00">Saturday, 1 January 2011, 6:00 AM</time>'
1827              ),
1828              array(
1829                  'time' => '1293876000 ',
1830                  'usertimezone' => 'America/Moncton',
1831                  'timezone' => 'America/Moncton', // No dst offset in jan.
1832                  'expectedoutput' => 'Saturday, 1 January 2011, 6:00 AM',
1833                  'expectedoutputhtml' => '<time datetime="2011-01-01T06:00:00-04:00">Saturday, 1 January 2011, 6:00 AM</time>'
1834              ),
1835              array(
1836                  'time' => '1293876000 ',
1837                  'usertimezone' => '2',
1838                  'timezone' => '99', // Take user timezone.
1839                  'expectedoutput' => 'Saturday, 1 January 2011, 12:00 PM',
1840                  'expectedoutputhtml' => '<time datetime="2011-01-01T12:00:00+02:00">Saturday, 1 January 2011, 12:00 PM</time>'
1841              ),
1842              array(
1843                  'time' => '1293876000 ',
1844                  'usertimezone' => '-2',
1845                  'timezone' => '99', // Take user timezone.
1846                  'expectedoutput' => 'Saturday, 1 January 2011, 8:00 AM',
1847                  'expectedoutputhtml' => '<time datetime="2011-01-01T08:00:00-02:00">Saturday, 1 January 2011, 8:00 AM</time>'
1848              ),
1849              array(
1850                  'time' => '1293876000 ',
1851                  'usertimezone' => '-10',
1852                  'timezone' => '2', // Take this timezone.
1853                  'expectedoutput' => 'Saturday, 1 January 2011, 12:00 PM',
1854                  'expectedoutputhtml' => '<time datetime="2011-01-01T00:00:00-10:00">Saturday, 1 January 2011, 12:00 PM</time>'
1855              ),
1856              array(
1857                  'time' => '1293876000 ',
1858                  'usertimezone' => '-10',
1859                  'timezone' => '-2', // Take this timezone.
1860                  'expectedoutput' => 'Saturday, 1 January 2011, 8:00 AM',
1861                  'expectedoutputhtml' => '<time datetime="2011-01-01T00:00:00-10:00">Saturday, 1 January 2011, 8:00 AM</time>'
1862              ),
1863              array(
1864                  'time' => '1293876000 ',
1865                  'usertimezone' => '-10',
1866                  'timezone' => 'random/time', // This should show server time.
1867                  'expectedoutput' => 'Saturday, 1 January 2011, 6:00 PM',
1868                  'expectedoutputhtml' => '<time datetime="2011-01-01T00:00:00-10:00">Saturday, 1 January 2011, 6:00 PM</time>'
1869              ),
1870              array(
1871                  'time' => '1293876000 ',
1872                  'usertimezone' => '20', // Fallback to server time zone.
1873                  'timezone' => '99',     // This should show user time.
1874                  'expectedoutput' => 'Saturday, 1 January 2011, 6:00 PM',
1875                  'expectedoutputhtml' => '<time datetime="2011-01-01T18:00:00+08:00">Saturday, 1 January 2011, 6:00 PM</time>'
1876              ),
1877          );
1878  
1879          // Set default timezone to Australia/Perth, else time calculated
1880          // will not match expected values.
1881          $this->setTimezone(99, 'Australia/Perth');
1882  
1883          foreach ($testvalues as $vals) {
1884              $USER->timezone = $vals['usertimezone'];
1885              $actualoutput = userdate($vals['time'], '%A, %d %B %Y, %I:%M %p', $vals['timezone']);
1886              $actualoutputhtml = userdate_htmltime($vals['time'], '%A, %d %B %Y, %I:%M %p', $vals['timezone']);
1887  
1888              // On different systems case of AM PM changes so compare case insensitive.
1889              $vals['expectedoutput'] = \core_text::strtolower($vals['expectedoutput']);
1890              $vals['expectedoutputhtml'] = \core_text::strtolower($vals['expectedoutputhtml']);
1891              $actualoutput = \core_text::strtolower($actualoutput);
1892              $actualoutputhtml = \core_text::strtolower($actualoutputhtml);
1893  
1894              $this->assertSame($vals['expectedoutput'], $actualoutput,
1895                  "Expected: {$vals['expectedoutput']} => Actual: {$actualoutput} \ndata: " . var_export($vals, true));
1896              $this->assertSame($vals['expectedoutputhtml'], $actualoutputhtml,
1897                  "Expected: {$vals['expectedoutputhtml']} => Actual: {$actualoutputhtml} \ndata: " . var_export($vals, true));
1898          }
1899      }
1900  
1901      /**
1902       * Make sure the DST changes happen at the right time in Moodle.
1903       */
1904      public function test_dst_changes() {
1905          // DST switching in Prague.
1906          // From 2AM to 3AM in 1989.
1907          $date = new \DateTime('1989-03-26T01:59:00+01:00');
1908          $this->assertSame('Sunday, 26 March 1989, 01:59', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Europe/Prague'));
1909          $date = new \DateTime('1989-03-26T02:01:00+01:00');
1910          $this->assertSame('Sunday, 26 March 1989, 03:01', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Europe/Prague'));
1911          // From 3AM to 2AM in 1989 - not the same as the west Europe.
1912          $date = new \DateTime('1989-09-24T01:59:00+01:00');
1913          $this->assertSame('Sunday, 24 September 1989, 02:59', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Europe/Prague'));
1914          $date = new \DateTime('1989-09-24T02:01:00+01:00');
1915          $this->assertSame('Sunday, 24 September 1989, 02:01', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Europe/Prague'));
1916          // From 2AM to 3AM in 2014.
1917          $date = new \DateTime('2014-03-30T01:59:00+01:00');
1918          $this->assertSame('Sunday, 30 March 2014, 01:59', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Europe/Prague'));
1919          $date = new \DateTime('2014-03-30T02:01:00+01:00');
1920          $this->assertSame('Sunday, 30 March 2014, 03:01', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Europe/Prague'));
1921          // From 3AM to 2AM in 2014.
1922          $date = new \DateTime('2014-10-26T01:59:00+01:00');
1923          $this->assertSame('Sunday, 26 October 2014, 02:59', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Europe/Prague'));
1924          $date = new \DateTime('2014-10-26T02:01:00+01:00');
1925          $this->assertSame('Sunday, 26 October 2014, 02:01', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Europe/Prague'));
1926          // From 2AM to 3AM in 2020.
1927          $date = new \DateTime('2020-03-29T01:59:00+01:00');
1928          $this->assertSame('Sunday, 29 March 2020, 01:59', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Europe/Prague'));
1929          $date = new \DateTime('2020-03-29T02:01:00+01:00');
1930          $this->assertSame('Sunday, 29 March 2020, 03:01', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Europe/Prague'));
1931          // From 3AM to 2AM in 2020.
1932          $date = new \DateTime('2020-10-25T01:59:00+01:00');
1933          $this->assertSame('Sunday, 25 October 2020, 02:59', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Europe/Prague'));
1934          $date = new \DateTime('2020-10-25T02:01:00+01:00');
1935          $this->assertSame('Sunday, 25 October 2020, 02:01', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Europe/Prague'));
1936  
1937          // DST switching in NZ.
1938          // From 3AM to 2AM in 2015.
1939          $date = new \DateTime('2015-04-05T02:59:00+13:00');
1940          $this->assertSame('Sunday, 5 April 2015, 02:59', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Pacific/Auckland'));
1941          $date = new \DateTime('2015-04-05T03:01:00+13:00');
1942          $this->assertSame('Sunday, 5 April 2015, 02:01', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Pacific/Auckland'));
1943          // From 2AM to 3AM in 2009.
1944          $date = new \DateTime('2015-09-27T01:59:00+12:00');
1945          $this->assertSame('Sunday, 27 September 2015, 01:59', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Pacific/Auckland'));
1946          $date = new \DateTime('2015-09-27T02:01:00+12:00');
1947          $this->assertSame('Sunday, 27 September 2015, 03:01', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Pacific/Auckland'));
1948  
1949          // DST switching in Perth.
1950          // From 3AM to 2AM in 2009.
1951          $date = new \DateTime('2008-03-30T01:59:00+08:00');
1952          $this->assertSame('Sunday, 30 March 2008, 02:59', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Australia/Perth'));
1953          $date = new \DateTime('2008-03-30T02:01:00+08:00');
1954          $this->assertSame('Sunday, 30 March 2008, 02:01', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Australia/Perth'));
1955          // From 2AM to 3AM in 2009.
1956          $date = new \DateTime('2008-10-26T01:59:00+08:00');
1957          $this->assertSame('Sunday, 26 October 2008, 01:59', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Australia/Perth'));
1958          $date = new \DateTime('2008-10-26T02:01:00+08:00');
1959          $this->assertSame('Sunday, 26 October 2008, 03:01', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Australia/Perth'));
1960  
1961          // DST switching in US.
1962          // From 2AM to 3AM in 2014.
1963          $date = new \DateTime('2014-03-09T01:59:00-05:00');
1964          $this->assertSame('Sunday, 9 March 2014, 01:59', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'America/New_York'));
1965          $date = new \DateTime('2014-03-09T02:01:00-05:00');
1966          $this->assertSame('Sunday, 9 March 2014, 03:01', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'America/New_York'));
1967          // From 3AM to 2AM in 2014.
1968          $date = new \DateTime('2014-11-02T01:59:00-04:00');
1969          $this->assertSame('Sunday, 2 November 2014, 01:59', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'America/New_York'));
1970          $date = new \DateTime('2014-11-02T02:01:00-04:00');
1971          $this->assertSame('Sunday, 2 November 2014, 01:01', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'America/New_York'));
1972      }
1973  
1974      public function test_make_timestamp() {
1975          global $USER, $CFG, $DB;
1976          $this->resetAfterTest();
1977  
1978          $this->setAdminUser();
1979  
1980          $testvalues = array(
1981              array(
1982                  'usertimezone' => 'America/Moncton',
1983                  'year' => '2011',
1984                  'month' => '7',
1985                  'day' => '1',
1986                  'hour' => '10',
1987                  'minutes' => '00',
1988                  'seconds' => '00',
1989                  'timezone' => '0.0',
1990                  'applydst' => false, // No dst offset.
1991                  'expectedoutput' => '1309514400' // 6pm at UTC+0.
1992              ),
1993              array(
1994                  'usertimezone' => 'America/Moncton',
1995                  'year' => '2011',
1996                  'month' => '7',
1997                  'day' => '1',
1998                  'hour' => '10',
1999                  'minutes' => '00',
2000                  'seconds' => '00',
2001                  'timezone' => '99',  // User default timezone.
2002                  'applydst' => false, // Don't apply dst.
2003                  'expectedoutput' => '1309528800'
2004              ),
2005              array(
2006                  'usertimezone' => 'America/Moncton',
2007                  'year' => '2011',
2008                  'month' => '7',
2009                  'day' => '1',
2010                  'hour' => '10',
2011                  'minutes' => '00',
2012                  'seconds' => '00',
2013                  'timezone' => '99', // User default timezone.
2014                  'applydst' => true, // Apply dst.
2015                  'expectedoutput' => '1309525200'
2016              ),
2017              array(
2018                  'usertimezone' => 'America/Moncton',
2019                  'year' => '2011',
2020                  'month' => '7',
2021                  'day' => '1',
2022                  'hour' => '10',
2023                  'minutes' => '00',
2024                  'seconds' => '00',
2025                  'timezone' => 'America/Moncton', // String timezone.
2026                  'applydst' => true, // Apply dst.
2027                  'expectedoutput' => '1309525200'
2028              ),
2029              array(
2030                  'usertimezone' => '2', // No dst applyed.
2031                  'year' => '2011',
2032                  'month' => '7',
2033                  'day' => '1',
2034                  'hour' => '10',
2035                  'minutes' => '00',
2036                  'seconds' => '00',
2037                  'timezone' => '99', // Take user timezone.
2038                  'applydst' => true, // Apply dst.
2039                  'expectedoutput' => '1309507200'
2040              ),
2041              array(
2042                  'usertimezone' => '-2', // No dst applyed.
2043                  'year' => '2011',
2044                  'month' => '7',
2045                  'day' => '1',
2046                  'hour' => '10',
2047                  'minutes' => '00',
2048                  'seconds' => '00',
2049                  'timezone' => '99', // Take usertimezone.
2050                  'applydst' => true, // Apply dst.
2051                  'expectedoutput' => '1309521600'
2052              ),
2053              array(
2054                  'usertimezone' => '-10', // No dst applyed.
2055                  'year' => '2011',
2056                  'month' => '7',
2057                  'day' => '1',
2058                  'hour' => '10',
2059                  'minutes' => '00',
2060                  'seconds' => '00',
2061                  'timezone' => '2',  // Take this timezone.
2062                  'applydst' => true, // Apply dst.
2063                  'expectedoutput' => '1309507200'
2064              ),
2065              array(
2066                  'usertimezone' => '-10', // No dst applyed.
2067                  'year' => '2011',
2068                  'month' => '7',
2069                  'day' => '1',
2070                  'hour' => '10',
2071                  'minutes' => '00',
2072                  'seconds' => '00',
2073                  'timezone' => '-2', // Take this timezone.
2074                  'applydst' => true, // Apply dst.
2075                  'expectedoutput' => '1309521600'
2076              ),
2077              array(
2078                  'usertimezone' => '-10', // No dst applyed.
2079                  'year' => '2011',
2080                  'month' => '7',
2081                  'day' => '1',
2082                  'hour' => '10',
2083                  'minutes' => '00',
2084                  'seconds' => '00',
2085                  'timezone' => 'random/time', // This should show server time.
2086                  'applydst' => true,          // Apply dst.
2087                  'expectedoutput' => '1309485600'
2088              ),
2089              array(
2090                  'usertimezone' => '-14', // Server time.
2091                  'year' => '2011',
2092                  'month' => '7',
2093                  'day' => '1',
2094                  'hour' => '10',
2095                  'minutes' => '00',
2096                  'seconds' => '00',
2097                  'timezone' => '99', // Get user time.
2098                  'applydst' => true, // Apply dst.
2099                  'expectedoutput' => '1309485600'
2100              )
2101          );
2102  
2103          // Set default timezone to Australia/Perth, else time calculated
2104          // will not match expected values.
2105          $this->setTimezone(99, 'Australia/Perth');
2106  
2107          // Test make_timestamp with all testvals and assert if anything wrong.
2108          foreach ($testvalues as $vals) {
2109              $USER->timezone = $vals['usertimezone'];
2110              $actualoutput = make_timestamp(
2111                  $vals['year'],
2112                  $vals['month'],
2113                  $vals['day'],
2114                  $vals['hour'],
2115                  $vals['minutes'],
2116                  $vals['seconds'],
2117                  $vals['timezone'],
2118                  $vals['applydst']
2119              );
2120  
2121              // On different systems case of AM PM changes so compare case insensitive.
2122              $vals['expectedoutput'] = \core_text::strtolower($vals['expectedoutput']);
2123              $actualoutput = \core_text::strtolower($actualoutput);
2124  
2125              $this->assertSame($vals['expectedoutput'], $actualoutput,
2126                  "Expected: {$vals['expectedoutput']} => Actual: {$actualoutput},
2127                  Please check if timezones are updated (Site adminstration -> location -> update timezone)");
2128          }
2129      }
2130  
2131      /**
2132       * Test get_string and most importantly the implementation of the lang_string
2133       * object.
2134       */
2135      public function test_get_string() {
2136          global $COURSE;
2137  
2138          // Make sure we are using English.
2139          $originallang = $COURSE->lang;
2140          $COURSE->lang = 'en';
2141  
2142          $yes = get_string('yes');
2143          $yesexpected = 'Yes';
2144          $this->assertIsString($yes);
2145          $this->assertSame($yesexpected, $yes);
2146  
2147          $yes = get_string('yes', 'moodle');
2148          $this->assertIsString($yes);
2149          $this->assertSame($yesexpected, $yes);
2150  
2151          $yes = get_string('yes', 'core');
2152          $this->assertIsString($yes);
2153          $this->assertSame($yesexpected, $yes);
2154  
2155          $yes = get_string('yes', '');
2156          $this->assertIsString($yes);
2157          $this->assertSame($yesexpected, $yes);
2158  
2159          $yes = get_string('yes', null);
2160          $this->assertIsString($yes);
2161          $this->assertSame($yesexpected, $yes);
2162  
2163          $yes = get_string('yes', null, 1);
2164          $this->assertIsString($yes);
2165          $this->assertSame($yesexpected, $yes);
2166  
2167          $days = 1;
2168          $numdays = get_string('numdays', 'core', '1');
2169          $numdaysexpected = $days.' days';
2170          $this->assertIsString($numdays);
2171          $this->assertSame($numdaysexpected, $numdays);
2172  
2173          $yes = get_string('yes', null, null, true);
2174          $this->assertInstanceOf('lang_string', $yes);
2175          $this->assertSame($yesexpected, (string)$yes);
2176  
2177          // Test lazy loading (returning lang_string) correctly interpolates 0 being used as args.
2178          $numdays = get_string('numdays', 'moodle', 0, true);
2179          $this->assertInstanceOf(lang_string::class, $numdays);
2180          $this->assertSame('0 days', (string) $numdays);
2181  
2182          // Test using a lang_string object as the $a argument for a normal
2183          // get_string call (returning string).
2184          $test = new lang_string('yes', null, null, true);
2185          $testexpected = get_string('numdays', 'core', get_string('yes'));
2186          $testresult = get_string('numdays', null, $test);
2187          $this->assertIsString($testresult);
2188          $this->assertSame($testexpected, $testresult);
2189  
2190          // Test using a lang_string object as the $a argument for an object
2191          // get_string call (returning lang_string).
2192          $test = new lang_string('yes', null, null, true);
2193          $testexpected = get_string('numdays', 'core', get_string('yes'));
2194          $testresult = get_string('numdays', null, $test, true);
2195          $this->assertInstanceOf('lang_string', $testresult);
2196          $this->assertSame($testexpected, "$testresult");
2197  
2198          // Make sure that object properties that can't be converted don't cause
2199          // errors.
2200          // Level one: This is as deep as current language processing goes.
2201          $test = new \stdClass;
2202          $test->one = 'here';
2203          $string = get_string('yes', null, $test, true);
2204          $this->assertEquals($yesexpected, $string);
2205  
2206          // Make sure that object properties that can't be converted don't cause
2207          // errors.
2208          // Level two: Language processing doesn't currently reach this deep.
2209          // only immediate scalar properties are worked with.
2210          $test = new \stdClass;
2211          $test->one = new \stdClass;
2212          $test->one->two = 'here';
2213          $string = get_string('yes', null, $test, true);
2214          $this->assertEquals($yesexpected, $string);
2215  
2216          // Make sure that object properties that can't be converted don't cause
2217          // errors.
2218          // Level three: It should never ever go this deep, but we're making sure
2219          // it doesn't cause any probs anyway.
2220          $test = new \stdClass;
2221          $test->one = new \stdClass;
2222          $test->one->two = new \stdClass;
2223          $test->one->two->three = 'here';
2224          $string = get_string('yes', null, $test, true);
2225          $this->assertEquals($yesexpected, $string);
2226  
2227          // Make sure that object properties that can't be converted don't cause
2228          // errors and check lang_string properties.
2229          // Level one: This is as deep as current language processing goes.
2230          $test = new \stdClass;
2231          $test->one = new lang_string('yes');
2232          $string = get_string('yes', null, $test, true);
2233          $this->assertEquals($yesexpected, $string);
2234  
2235          // Make sure that object properties that can't be converted don't cause
2236          // errors and check lang_string properties.
2237          // Level two: Language processing doesn't currently reach this deep.
2238          // only immediate scalar properties are worked with.
2239          $test = new \stdClass;
2240          $test->one = new \stdClass;
2241          $test->one->two = new lang_string('yes');
2242          $string = get_string('yes', null, $test, true);
2243          $this->assertEquals($yesexpected, $string);
2244  
2245          // Make sure that object properties that can't be converted don't cause
2246          // errors and check lang_string properties.
2247          // Level three: It should never ever go this deep, but we're making sure
2248          // it doesn't cause any probs anyway.
2249          $test = new \stdClass;
2250          $test->one = new \stdClass;
2251          $test->one->two = new \stdClass;
2252          $test->one->two->three = new lang_string('yes');
2253          $string = get_string('yes', null, $test, true);
2254          $this->assertEquals($yesexpected, $string);
2255  
2256          // Make sure that array properties that can't be converted don't cause
2257          // errors.
2258          $test = array();
2259          $test['one'] = new \stdClass;
2260          $test['one']->two = 'here';
2261          $string = get_string('yes', null, $test, true);
2262          $this->assertEquals($yesexpected, $string);
2263  
2264          // Same thing but as above except using an object... this is allowed :P.
2265          $string = get_string('yes', null, null, true);
2266          $object = new \stdClass;
2267          $object->$string = 'Yes';
2268          $this->assertEquals($yesexpected, $string);
2269          $this->assertEquals($yesexpected, $object->$string);
2270  
2271          // Reset the language.
2272          $COURSE->lang = $originallang;
2273      }
2274  
2275      public function test_lang_string_var_export() {
2276  
2277          // Call var_export() on a newly generated lang_string.
2278          $str = new lang_string('no');
2279  
2280          $expected1 = <<<EOF
2281  lang_string::__set_state(array(
2282     'identifier' => 'no',
2283     'component' => 'moodle',
2284     'a' => NULL,
2285     'lang' => NULL,
2286     'string' => NULL,
2287     'forcedstring' => false,
2288  ))
2289  EOF;
2290  
2291          $v = var_export($str, true);
2292          $this->assertEquals($expected1, $v);
2293  
2294          // Now execute the code that was returned - it should produce a correct string.
2295          $str = lang_string::__set_state(array(
2296              'identifier' => 'no',
2297              'component' => 'moodle',
2298              'a' => NULL,
2299              'lang' => NULL,
2300              'string' => NULL,
2301              'forcedstring' => false,
2302          ));
2303  
2304          $this->assertInstanceOf(lang_string::class, $str);
2305          $this->assertEquals('No', $str);
2306      }
2307  
2308      public function test_get_string_limitation() {
2309          // This is one of the limitations to the lang_string class. It can't be
2310          // used as a key.
2311          if (PHP_VERSION_ID >= 80000) {
2312              $this->expectException(\TypeError::class);
2313          } else {
2314              $this->expectWarning();
2315          }
2316          $array = array(get_string('yes', null, null, true) => 'yes');
2317      }
2318  
2319      /**
2320       * Test localised float formatting.
2321       */
2322      public function test_format_float() {
2323  
2324          // Special case for null.
2325          $this->assertEquals('', format_float(null));
2326  
2327          // Default 1 decimal place.
2328          $this->assertEquals('5.4', format_float(5.43));
2329          $this->assertEquals('5.0', format_float(5.001));
2330  
2331          // Custom number of decimal places.
2332          $this->assertEquals('5.43000', format_float(5.43, 5));
2333  
2334          // Auto detect the number of decimal places.
2335          $this->assertEquals('5.43', format_float(5.43, -1));
2336          $this->assertEquals('5.43', format_float(5.43000, -1));
2337          $this->assertEquals('5', format_float(5, -1));
2338          $this->assertEquals('5', format_float(5.0, -1));
2339          $this->assertEquals('0.543', format_float('5.43e-1', -1));
2340          $this->assertEquals('0.543', format_float('5.43000e-1', -1));
2341  
2342          // Option to strip ending zeros after rounding.
2343          $this->assertEquals('5.43', format_float(5.43, 5, true, true));
2344          $this->assertEquals('5', format_float(5.0001, 3, true, true));
2345          $this->assertEquals('100', format_float(100, 2, true, true));
2346          $this->assertEquals('100', format_float(100, 0, true, true));
2347  
2348          // Tests with a localised decimal separator.
2349          $this->define_local_decimal_separator();
2350  
2351          // Localisation on (default).
2352          $this->assertEquals('5X43000', format_float(5.43, 5));
2353          $this->assertEquals('5X43', format_float(5.43, 5, true, true));
2354  
2355          // Localisation off.
2356          $this->assertEquals('5.43000', format_float(5.43, 5, false));
2357          $this->assertEquals('5.43', format_float(5.43, 5, false, true));
2358  
2359          // Tests with tilde as localised decimal separator.
2360          $this->define_local_decimal_separator('~');
2361  
2362          // Must also work for '~' as decimal separator.
2363          $this->assertEquals('5', format_float(5.0001, 3, true, true));
2364          $this->assertEquals('5~43000', format_float(5.43, 5));
2365          $this->assertEquals('5~43', format_float(5.43, 5, true, true));
2366      }
2367  
2368      /**
2369       * Test localised float unformatting.
2370       */
2371      public function test_unformat_float() {
2372  
2373          // Tests without the localised decimal separator.
2374  
2375          // Special case for null, empty or white spaces only strings.
2376          $this->assertEquals(null, unformat_float(null));
2377          $this->assertEquals(null, unformat_float(''));
2378          $this->assertEquals(null, unformat_float('    '));
2379  
2380          // Regular use.
2381          $this->assertEquals(5.4, unformat_float('5.4'));
2382          $this->assertEquals(5.4, unformat_float('5.4', true));
2383  
2384          // No decimal.
2385          $this->assertEquals(5.0, unformat_float('5'));
2386  
2387          // Custom number of decimal.
2388          $this->assertEquals(5.43267, unformat_float('5.43267'));
2389  
2390          // Empty decimal.
2391          $this->assertEquals(100.0, unformat_float('100.00'));
2392  
2393          // With the thousand separator.
2394          $this->assertEquals(1000.0, unformat_float('1 000'));
2395          $this->assertEquals(1000.32, unformat_float('1 000.32'));
2396  
2397          // Negative number.
2398          $this->assertEquals(-100.0, unformat_float('-100'));
2399  
2400          // Wrong value.
2401          $this->assertEquals(0.0, unformat_float('Wrong value'));
2402          // Wrong value in strict mode.
2403          $this->assertFalse(unformat_float('Wrong value', true));
2404  
2405          // Combining options.
2406          $this->assertEquals(-1023.862567, unformat_float('   -1 023.862567     '));
2407  
2408          // Bad decimal separator (should crop the decimal).
2409          $this->assertEquals(50.0, unformat_float('50,57'));
2410          // Bad decimal separator in strict mode (should return false).
2411          $this->assertFalse(unformat_float('50,57', true));
2412  
2413          // Tests with a localised decimal separator.
2414          $this->define_local_decimal_separator();
2415  
2416          // We repeat the tests above but with the current decimal separator.
2417  
2418          // Regular use without and with the localised separator.
2419          $this->assertEquals (5.4, unformat_float('5.4'));
2420          $this->assertEquals (5.4, unformat_float('5X4'));
2421  
2422          // Custom number of decimal.
2423          $this->assertEquals (5.43267, unformat_float('5X43267'));
2424  
2425          // Empty decimal.
2426          $this->assertEquals (100.0, unformat_float('100X00'));
2427  
2428          // With the thousand separator.
2429          $this->assertEquals (1000.32, unformat_float('1 000X32'));
2430  
2431          // Bad different separator (should crop the decimal).
2432          $this->assertEquals (50.0, unformat_float('50Y57'));
2433          // Bad different separator in strict mode (should return false).
2434          $this->assertFalse (unformat_float('50Y57', true));
2435  
2436          // Combining options.
2437          $this->assertEquals (-1023.862567, unformat_float('   -1 023X862567     '));
2438          // Combining options in strict mode.
2439          $this->assertEquals (-1023.862567, unformat_float('   -1 023X862567     ', true));
2440      }
2441  
2442      /**
2443       * Test deleting of users.
2444       */
2445      public function test_delete_user() {
2446          global $DB, $CFG;
2447  
2448          $this->resetAfterTest();
2449  
2450          $guest = $DB->get_record('user', array('id'=>$CFG->siteguest), '*', MUST_EXIST);
2451          $admin = $DB->get_record('user', array('id'=>$CFG->siteadmins), '*', MUST_EXIST);
2452          $this->assertEquals(0, $DB->count_records('user', array('deleted'=>1)));
2453  
2454          $user = $this->getDataGenerator()->create_user(array('idnumber'=>'abc'));
2455          $user2 = $this->getDataGenerator()->create_user(array('idnumber'=>'xyz'));
2456          $usersharedemail1 = $this->getDataGenerator()->create_user(array('email' => 'sharedemail@example.invalid'));
2457          $usersharedemail2 = $this->getDataGenerator()->create_user(array('email' => 'sharedemail@example.invalid'));
2458          $useremptyemail1 = $this->getDataGenerator()->create_user(array('email' => ''));
2459          $useremptyemail2 = $this->getDataGenerator()->create_user(array('email' => ''));
2460  
2461          // Delete user and capture event.
2462          $sink = $this->redirectEvents();
2463          $result = delete_user($user);
2464          $events = $sink->get_events();
2465          $sink->close();
2466          $event = array_pop($events);
2467  
2468          // Test user is deleted in DB.
2469          $this->assertTrue($result);
2470          $deluser = $DB->get_record('user', array('id'=>$user->id), '*', MUST_EXIST);
2471          $this->assertEquals(1, $deluser->deleted);
2472          $this->assertEquals(0, $deluser->picture);
2473          $this->assertSame('', $deluser->idnumber);
2474          $this->assertSame(md5($user->username), $deluser->email);
2475          $this->assertMatchesRegularExpression('/^'.preg_quote($user->email, '/').'\.\d*$/', $deluser->username);
2476  
2477          $this->assertEquals(1, $DB->count_records('user', array('deleted'=>1)));
2478  
2479          // Test Event.
2480          $this->assertInstanceOf('\core\event\user_deleted', $event);
2481          $this->assertSame($user->id, $event->objectid);
2482          $this->assertSame('user_deleted', $event->get_legacy_eventname());
2483          $this->assertEventLegacyData($user, $event);
2484          $expectedlogdata = array(SITEID, 'user', 'delete', "view.php?id=$user->id", $user->firstname.' '.$user->lastname);
2485          $this->assertEventLegacyLogData($expectedlogdata, $event);
2486          $eventdata = $event->get_data();
2487          $this->assertSame($eventdata['other']['username'], $user->username);
2488          $this->assertSame($eventdata['other']['email'], $user->email);
2489          $this->assertSame($eventdata['other']['idnumber'], $user->idnumber);
2490          $this->assertSame($eventdata['other']['picture'], $user->picture);
2491          $this->assertSame($eventdata['other']['mnethostid'], $user->mnethostid);
2492          $this->assertEquals($user, $event->get_record_snapshot('user', $event->objectid));
2493          $this->assertEventContextNotUsed($event);
2494  
2495          // Try invalid params.
2496          $record = new \stdClass();
2497          $record->grrr = 1;
2498          try {
2499              delete_user($record);
2500              $this->fail('Expecting exception for invalid delete_user() $user parameter');
2501          } catch (\moodle_exception $ex) {
2502              $this->assertInstanceOf('coding_exception', $ex);
2503          }
2504          $record->id = 1;
2505          try {
2506              delete_user($record);
2507              $this->fail('Expecting exception for invalid delete_user() $user parameter');
2508          } catch (\moodle_exception $ex) {
2509              $this->assertInstanceOf('coding_exception', $ex);
2510          }
2511  
2512          $record = new \stdClass();
2513          $record->id = 666;
2514          $record->username = 'xx';
2515          $this->assertFalse($DB->record_exists('user', array('id'=>666))); // Any non-existent id is ok.
2516          $result = delete_user($record);
2517          $this->assertFalse($result);
2518  
2519          $result = delete_user($guest);
2520          $this->assertFalse($result);
2521  
2522          $result = delete_user($admin);
2523          $this->assertFalse($result);
2524  
2525          // Simultaneously deleting users with identical email addresses.
2526          $result1 = delete_user($usersharedemail1);
2527          $result2 = delete_user($usersharedemail2);
2528  
2529          $usersharedemail1after = $DB->get_record('user', array('id' => $usersharedemail1->id));
2530          $usersharedemail2after = $DB->get_record('user', array('id' => $usersharedemail2->id));
2531          $this->assertTrue($result1);
2532          $this->assertTrue($result2);
2533          $this->assertStringStartsWith($usersharedemail1->email . '.', $usersharedemail1after->username);
2534          $this->assertStringStartsWith($usersharedemail2->email . '.', $usersharedemail2after->username);
2535  
2536          // Simultaneously deleting users without email addresses.
2537          $result1 = delete_user($useremptyemail1);
2538          $result2 = delete_user($useremptyemail2);
2539  
2540          $useremptyemail1after = $DB->get_record('user', array('id' => $useremptyemail1->id));
2541          $useremptyemail2after = $DB->get_record('user', array('id' => $useremptyemail2->id));
2542          $this->assertTrue($result1);
2543          $this->assertTrue($result2);
2544          $this->assertStringStartsWith($useremptyemail1->username . '.' . $useremptyemail1->id . '@unknownemail.invalid.',
2545              $useremptyemail1after->username);
2546          $this->assertStringStartsWith($useremptyemail2->username . '.' . $useremptyemail2->id . '@unknownemail.invalid.',
2547              $useremptyemail2after->username);
2548  
2549          $this->resetDebugging();
2550      }
2551  
2552      /**
2553       * Test deletion of user with long username
2554       */
2555      public function test_delete_user_long_username() {
2556          global $DB;
2557  
2558          $this->resetAfterTest();
2559  
2560          // For users without an e-mail, one will be created during deletion using {$username}.{$id}@unknownemail.invalid format.
2561          $user = $this->getDataGenerator()->create_user([
2562              'username' => str_repeat('a', 75),
2563              'email' => '',
2564          ]);
2565  
2566          delete_user($user);
2567  
2568          // The username for the deleted user shouldn't exceed 100 characters.
2569          $usernamedeleted = $DB->get_field('user', 'username', ['id' => $user->id]);
2570          $this->assertEquals(100, \core_text::strlen($usernamedeleted));
2571  
2572          $timestrlength = \core_text::strlen((string) time());
2573  
2574          // It should start with the user name, and end with the current time.
2575          $this->assertStringStartsWith("{$user->username}.{$user->id}@", $usernamedeleted);
2576          $this->assertMatchesRegularExpression('/\.\d{' . $timestrlength . '}$/', $usernamedeleted);
2577      }
2578  
2579      /**
2580       * Test deletion of user with long email address
2581       */
2582      public function test_delete_user_long_email() {
2583          global $DB;
2584  
2585          $this->resetAfterTest();
2586  
2587          // Create user with 90 character email address.
2588          $user = $this->getDataGenerator()->create_user([
2589              'email' => str_repeat('a', 78) . '@example.com',
2590          ]);
2591  
2592          delete_user($user);
2593  
2594          // The username for the deleted user shouldn't exceed 100 characters.
2595          $usernamedeleted = $DB->get_field('user', 'username', ['id' => $user->id]);
2596          $this->assertEquals(100, \core_text::strlen($usernamedeleted));
2597  
2598          $timestrlength = \core_text::strlen((string) time());
2599  
2600          // Max username length is 100 chars. Select up to limit - (length of current time + 1 [period character]) from users email.
2601          $expectedemail = \core_text::substr($user->email, 0, 100 - ($timestrlength + 1));
2602          $this->assertMatchesRegularExpression('/^' . preg_quote($expectedemail) . '\.\d{' . $timestrlength . '}$/',
2603              $usernamedeleted);
2604      }
2605  
2606      /**
2607       * Test function convert_to_array()
2608       */
2609      public function test_convert_to_array() {
2610          // Check that normal classes are converted to arrays the same way as (array) would do.
2611          $obj = new \stdClass();
2612          $obj->prop1 = 'hello';
2613          $obj->prop2 = array('first', 'second', 13);
2614          $obj->prop3 = 15;
2615          $this->assertEquals(convert_to_array($obj), (array)$obj);
2616  
2617          // Check that context object (with iterator) is converted to array properly.
2618          $obj = \context_system::instance();
2619          $ar = array(
2620              'id'           => $obj->id,
2621              'contextlevel' => $obj->contextlevel,
2622              'instanceid'   => $obj->instanceid,
2623              'path'         => $obj->path,
2624              'depth'        => $obj->depth,
2625              'locked'       => $obj->locked,
2626          );
2627          $this->assertEquals(convert_to_array($obj), $ar);
2628      }
2629  
2630      /**
2631       * Test the function date_format_string().
2632       */
2633      public function test_date_format_string() {
2634          global $CFG;
2635  
2636          $this->resetAfterTest();
2637          $this->setTimezone(99, 'Australia/Perth');
2638  
2639          $tests = array(
2640              array(
2641                  'tz' => 99,
2642                  'str' => '%A, %d %B %Y, %I:%M %p',
2643                  'expected' => 'Saturday, 01 January 2011, 06:00 PM'
2644              ),
2645              array(
2646                  'tz' => 0,
2647                  'str' => '%A, %d %B %Y, %I:%M %p',
2648                  'expected' => 'Saturday, 01 January 2011, 10:00 AM'
2649              ),
2650              array(
2651                  // Note: this function expected the timestamp in weird format before,
2652                  // since 2.9 it uses UTC.
2653                  'tz' => 'Pacific/Auckland',
2654                  'str' => '%A, %d %B %Y, %I:%M %p',
2655                  'expected' => 'Saturday, 01 January 2011, 11:00 PM'
2656              ),
2657              // Following tests pass on Windows only because en lang pack does
2658              // not contain localewincharset, in real life lang pack maintainers
2659              // may use only characters that are present in localewincharset
2660              // in format strings!
2661              array(
2662                  'tz' => 99,
2663                  'str' => 'Žluťoučký koníček %A',
2664                  'expected' => 'Žluťoučký koníček Saturday'
2665              ),
2666              array(
2667                  'tz' => 99,
2668                  'str' => '言語設定言語 %A',
2669                  'expected' => '言語設定言語 Saturday'
2670              ),
2671              array(
2672                  'tz' => 99,
2673                  'str' => '简体中文简体 %A',
2674                  'expected' => '简体中文简体 Saturday'
2675              ),
2676          );
2677  
2678          // Note: date_format_string() uses the timezone only to differenciate
2679          // the server time from the UTC time. It does not modify the timestamp.
2680          // Hence similar results for timezones <= 13.
2681          // On different systems case of AM PM changes so compare case insensitive.
2682          foreach ($tests as $test) {
2683              $str = date_format_string(1293876000, $test['str'], $test['tz']);
2684              $this->assertSame(\core_text::strtolower($test['expected']), \core_text::strtolower($str));
2685          }
2686      }
2687  
2688      public function test_get_config() {
2689          global $CFG;
2690  
2691          $this->resetAfterTest();
2692  
2693          // Preparation.
2694          set_config('phpunit_test_get_config_1', 'test 1');
2695          set_config('phpunit_test_get_config_2', 'test 2', 'mod_forum');
2696          if (!is_array($CFG->config_php_settings)) {
2697              $CFG->config_php_settings = array();
2698          }
2699          $CFG->config_php_settings['phpunit_test_get_config_3'] = 'test 3';
2700  
2701          if (!is_array($CFG->forced_plugin_settings)) {
2702              $CFG->forced_plugin_settings = array();
2703          }
2704          if (!array_key_exists('mod_forum', $CFG->forced_plugin_settings)) {
2705              $CFG->forced_plugin_settings['mod_forum'] = array();
2706          }
2707          $CFG->forced_plugin_settings['mod_forum']['phpunit_test_get_config_4'] = 'test 4';
2708          $CFG->phpunit_test_get_config_5 = 'test 5';
2709  
2710          // Testing.
2711          $this->assertSame('test 1', get_config('core', 'phpunit_test_get_config_1'));
2712          $this->assertSame('test 2', get_config('mod_forum', 'phpunit_test_get_config_2'));
2713          $this->assertSame('test 3', get_config('core', 'phpunit_test_get_config_3'));
2714          $this->assertSame('test 4', get_config('mod_forum', 'phpunit_test_get_config_4'));
2715          $this->assertFalse(get_config('core', 'phpunit_test_get_config_5'));
2716          $this->assertFalse(get_config('core', 'phpunit_test_get_config_x'));
2717          $this->assertFalse(get_config('mod_forum', 'phpunit_test_get_config_x'));
2718  
2719          // Test config we know to exist.
2720          $this->assertSame($CFG->dataroot, get_config('core', 'dataroot'));
2721          $this->assertSame($CFG->phpunit_dataroot, get_config('core', 'phpunit_dataroot'));
2722          $this->assertSame($CFG->dataroot, get_config('core', 'phpunit_dataroot'));
2723          $this->assertSame(get_config('core', 'dataroot'), get_config('core', 'phpunit_dataroot'));
2724  
2725          // Test setting a config var that already exists.
2726          set_config('phpunit_test_get_config_1', 'test a');
2727          $this->assertSame('test a', $CFG->phpunit_test_get_config_1);
2728          $this->assertSame('test a', get_config('core', 'phpunit_test_get_config_1'));
2729  
2730          // Test cache invalidation.
2731          $cache = \cache::make('core', 'config');
2732          $this->assertIsArray($cache->get('core'));
2733          $this->assertIsArray($cache->get('mod_forum'));
2734          set_config('phpunit_test_get_config_1', 'test b');
2735          $this->assertFalse($cache->get('core'));
2736          set_config('phpunit_test_get_config_4', 'test c', 'mod_forum');
2737          $this->assertFalse($cache->get('mod_forum'));
2738      }
2739  
2740      public function test_get_max_upload_sizes() {
2741          // Test with very low limits so we are not affected by php upload limits.
2742          // Test activity limit smallest.
2743          $sitebytes = 102400;
2744          $coursebytes = 51200;
2745          $modulebytes = 10240;
2746          $result = get_max_upload_sizes($sitebytes, $coursebytes, $modulebytes);
2747  
2748          $nbsp = "\xc2\xa0";
2749          $this->assertSame("Activity upload limit (10{$nbsp}KB)", $result['0']);
2750          $this->assertCount(2, $result);
2751  
2752          // Test course limit smallest.
2753          $sitebytes = 102400;
2754          $coursebytes = 10240;
2755          $modulebytes = 51200;
2756          $result = get_max_upload_sizes($sitebytes, $coursebytes, $modulebytes);
2757  
2758          $this->assertSame("Course upload limit (10{$nbsp}KB)", $result['0']);
2759          $this->assertCount(2, $result);
2760  
2761          // Test site limit smallest.
2762          $sitebytes = 10240;
2763          $coursebytes = 102400;
2764          $modulebytes = 51200;
2765          $result = get_max_upload_sizes($sitebytes, $coursebytes, $modulebytes);
2766  
2767          $this->assertSame("Site upload limit (10{$nbsp}KB)", $result['0']);
2768          $this->assertCount(2, $result);
2769  
2770          // Test site limit not set.
2771          $sitebytes = 0;
2772          $coursebytes = 102400;
2773          $modulebytes = 51200;
2774          $result = get_max_upload_sizes($sitebytes, $coursebytes, $modulebytes);
2775  
2776          $this->assertSame("Activity upload limit (50{$nbsp}KB)", $result['0']);
2777          $this->assertCount(3, $result);
2778  
2779          $sitebytes = 0;
2780          $coursebytes = 51200;
2781          $modulebytes = 102400;
2782          $result = get_max_upload_sizes($sitebytes, $coursebytes, $modulebytes);
2783  
2784          $this->assertSame("Course upload limit (50{$nbsp}KB)", $result['0']);
2785          $this->assertCount(3, $result);
2786  
2787          // Test custom bytes in range.
2788          $sitebytes = 102400;
2789          $coursebytes = 51200;
2790          $modulebytes = 51200;
2791          $custombytes = 10240;
2792          $result = get_max_upload_sizes($sitebytes, $coursebytes, $modulebytes, $custombytes);
2793  
2794          $this->assertCount(3, $result);
2795  
2796          // Test custom bytes in range but non-standard.
2797          $sitebytes = 102400;
2798          $coursebytes = 51200;
2799          $modulebytes = 51200;
2800          $custombytes = 25600;
2801          $result = get_max_upload_sizes($sitebytes, $coursebytes, $modulebytes, $custombytes);
2802  
2803          $this->assertCount(4, $result);
2804  
2805          // Test custom bytes out of range.
2806          $sitebytes = 102400;
2807          $coursebytes = 51200;
2808          $modulebytes = 51200;
2809          $custombytes = 102400;
2810          $result = get_max_upload_sizes($sitebytes, $coursebytes, $modulebytes, $custombytes);
2811  
2812          $this->assertCount(3, $result);
2813  
2814          // Test custom bytes out of range and non-standard.
2815          $sitebytes = 102400;
2816          $coursebytes = 51200;
2817          $modulebytes = 51200;
2818          $custombytes = 256000;
2819          $result = get_max_upload_sizes($sitebytes, $coursebytes, $modulebytes, $custombytes);
2820  
2821          $this->assertCount(3, $result);
2822  
2823          // Test site limit only.
2824          $sitebytes = 51200;
2825          $result = get_max_upload_sizes($sitebytes);
2826  
2827          $this->assertSame("Site upload limit (50{$nbsp}KB)", $result['0']);
2828          $this->assertSame("50{$nbsp}KB", $result['51200']);
2829          $this->assertSame("10{$nbsp}KB", $result['10240']);
2830          $this->assertCount(3, $result);
2831  
2832          // Test no limit.
2833          $result = get_max_upload_sizes();
2834          $this->assertArrayHasKey('0', $result);
2835          $this->assertArrayHasKey(get_max_upload_file_size(), $result);
2836      }
2837  
2838      /**
2839       * Test function password_is_legacy_hash().
2840       */
2841      public function test_password_is_legacy_hash() {
2842          // Well formed md5s should be matched.
2843          foreach (array('some', 'strings', 'to_check!') as $string) {
2844              $md5 = md5($string);
2845              $this->assertTrue(password_is_legacy_hash($md5));
2846          }
2847          // Strings that are not md5s should not be matched.
2848          foreach (array('', AUTH_PASSWORD_NOT_CACHED, 'IPW8WTcsWNgAWcUS1FBVHegzJnw5M2jOmYkmfc8z.xdBOyC4Caeum') as $notmd5) {
2849              $this->assertFalse(password_is_legacy_hash($notmd5));
2850          }
2851      }
2852  
2853      /**
2854       * Test function validate_internal_user_password().
2855       */
2856      public function test_validate_internal_user_password() {
2857          // Test bcrypt hashes.
2858          $validhashes = array(
2859              'pw' => '$2y$10$LOSDi5eaQJhutSRun.OVJ.ZSxQZabCMay7TO1KmzMkDMPvU40zGXK',
2860              'abc' => '$2y$10$VWTOhVdsBbWwtdWNDRHSpewjd3aXBQlBQf5rBY/hVhw8hciarFhXa',
2861              'C0mP1eX_&}<?@*&%` |\"' => '$2y$10$3PJf.q.9ywNJlsInPbqc8.IFeSsvXrGvQLKRFBIhVu1h1I3vpIry6',
2862              'ĩńťėŕňăţĩōŋāĹ' => '$2y$10$3A2Y8WpfRAnP3czJiSv6N.6Xp0T8hW3QZz2hUCYhzyWr1kGP1yUve'
2863          );
2864  
2865          foreach ($validhashes as $password => $hash) {
2866              $user = new \stdClass();
2867              $user->auth = 'manual';
2868              $user->password = $hash;
2869              // The correct password should be validated.
2870              $this->assertTrue(validate_internal_user_password($user, $password));
2871              // An incorrect password should not be validated.
2872              $this->assertFalse(validate_internal_user_password($user, 'badpw'));
2873          }
2874      }
2875  
2876      /**
2877       * Test function hash_internal_user_password().
2878       */
2879      public function test_hash_internal_user_password() {
2880          $passwords = array('pw', 'abc123', 'C0mP1eX_&}<?@*&%` |\"', 'ĩńťėŕňăţĩōŋāĹ');
2881  
2882          // Check that some passwords that we convert to hashes can
2883          // be validated.
2884          foreach ($passwords as $password) {
2885              $hash = hash_internal_user_password($password);
2886              $fasthash = hash_internal_user_password($password, true);
2887              $user = new \stdClass();
2888              $user->auth = 'manual';
2889              $user->password = $hash;
2890              $this->assertTrue(validate_internal_user_password($user, $password));
2891  
2892              // They should not be in md5 format.
2893              $this->assertFalse(password_is_legacy_hash($hash));
2894  
2895              // Check that cost factor in hash is correctly set.
2896              $this->assertMatchesRegularExpression('/\$10\$/', $hash);
2897              $this->assertMatchesRegularExpression('/\$04\$/', $fasthash);
2898          }
2899      }
2900  
2901      /**
2902       * Test function update_internal_user_password().
2903       */
2904      public function test_update_internal_user_password() {
2905          global $DB;
2906          $this->resetAfterTest();
2907          $passwords = array('password', '1234', 'changeme', '****');
2908          foreach ($passwords as $password) {
2909              $user = $this->getDataGenerator()->create_user(array('auth'=>'manual'));
2910              update_internal_user_password($user, $password);
2911              // The user object should have been updated.
2912              $this->assertTrue(validate_internal_user_password($user, $password));
2913              // The database field for the user should also have been updated to the
2914              // same value.
2915              $this->assertSame($user->password, $DB->get_field('user', 'password', array('id' => $user->id)));
2916          }
2917  
2918          $user = $this->getDataGenerator()->create_user(array('auth'=>'manual'));
2919          // Manually set the user's password to the md5 of the string 'password'.
2920          $DB->set_field('user', 'password', '5f4dcc3b5aa765d61d8327deb882cf99', array('id' => $user->id));
2921  
2922          $sink = $this->redirectEvents();
2923          // Update the password.
2924          update_internal_user_password($user, 'password');
2925          $events = $sink->get_events();
2926          $sink->close();
2927          $event = array_pop($events);
2928  
2929          // Password should have been updated to a bcrypt hash.
2930          $this->assertFalse(password_is_legacy_hash($user->password));
2931  
2932          // Verify event information.
2933          $this->assertInstanceOf('\core\event\user_password_updated', $event);
2934          $this->assertSame($user->id, $event->relateduserid);
2935          $this->assertEquals(\context_user::instance($user->id), $event->get_context());
2936          $this->assertEventContextNotUsed($event);
2937  
2938          // Verify recovery of property 'auth'.
2939          unset($user->auth);
2940          update_internal_user_password($user, 'newpassword');
2941          $this->assertDebuggingCalled('User record in update_internal_user_password() must include field auth',
2942                  DEBUG_DEVELOPER);
2943          $this->assertEquals('manual', $user->auth);
2944      }
2945  
2946      /**
2947       * Testing that if the password is not cached, that it does not update
2948       * the user table and fire event.
2949       */
2950      public function test_update_internal_user_password_no_cache() {
2951          global $DB;
2952          $this->resetAfterTest();
2953  
2954          $user = $this->getDataGenerator()->create_user(array('auth' => 'cas'));
2955          $DB->update_record('user', ['id' => $user->id, 'password' => AUTH_PASSWORD_NOT_CACHED]);
2956          $user->password = AUTH_PASSWORD_NOT_CACHED;
2957  
2958          $sink = $this->redirectEvents();
2959          update_internal_user_password($user, 'wonkawonka');
2960          $this->assertEquals(0, $sink->count(), 'User updated event should not fire');
2961      }
2962  
2963      /**
2964       * Test if the user has a password hash, but now their auth method
2965       * says not to cache it.  Then it should update.
2966       */
2967      public function test_update_internal_user_password_update_no_cache() {
2968          $this->resetAfterTest();
2969  
2970          $user = $this->getDataGenerator()->create_user(array('password' => 'test'));
2971          $this->assertNotEquals(AUTH_PASSWORD_NOT_CACHED, $user->password);
2972          $user->auth = 'cas'; // Change to a auth that does not store passwords.
2973  
2974          $sink = $this->redirectEvents();
2975          update_internal_user_password($user, 'wonkawonka');
2976          $this->assertGreaterThanOrEqual(1, $sink->count(), 'User updated event should fire');
2977  
2978          $this->assertEquals(AUTH_PASSWORD_NOT_CACHED, $user->password);
2979      }
2980  
2981      public function test_fullname() {
2982          global $CFG;
2983  
2984          $this->resetAfterTest();
2985  
2986          // Create a user to test the name display on.
2987          $record = array();
2988          $record['firstname'] = 'Scott';
2989          $record['lastname'] = 'Fletcher';
2990          $record['firstnamephonetic'] = 'スコット';
2991          $record['lastnamephonetic'] = 'フレチャー';
2992          $record['alternatename'] = 'No friends';
2993          $user = $this->getDataGenerator()->create_user($record);
2994  
2995          // Back up config settings for restore later.
2996          $originalcfg = new \stdClass();
2997          $originalcfg->fullnamedisplay = $CFG->fullnamedisplay;
2998          $originalcfg->alternativefullnameformat = $CFG->alternativefullnameformat;
2999  
3000          // Testing existing fullnamedisplay settings.
3001          $CFG->fullnamedisplay = 'firstname';
3002          $testname = fullname($user);
3003          $this->assertSame($user->firstname, $testname);
3004  
3005          $CFG->fullnamedisplay = 'firstname lastname';
3006          $expectedname = "$user->firstname $user->lastname";
3007          $testname = fullname($user);
3008          $this->assertSame($expectedname, $testname);
3009  
3010          $CFG->fullnamedisplay = 'lastname firstname';
3011          $expectedname = "$user->lastname $user->firstname";
3012          $testname = fullname($user);
3013          $this->assertSame($expectedname, $testname);
3014  
3015          $expectedname = get_string('fullnamedisplay', null, $user);
3016          $CFG->fullnamedisplay = 'language';
3017          $testname = fullname($user);
3018          $this->assertSame($expectedname, $testname);
3019  
3020          // Test override parameter.
3021          $CFG->fullnamedisplay = 'firstname';
3022          $expectedname = "$user->firstname $user->lastname";
3023          $testname = fullname($user, true);
3024          $this->assertSame($expectedname, $testname);
3025  
3026          // Test alternativefullnameformat setting.
3027          // Test alternativefullnameformat that has been set to nothing.
3028          $CFG->alternativefullnameformat = '';
3029          $expectedname = "$user->firstname $user->lastname";
3030          $testname = fullname($user, true);
3031          $this->assertSame($expectedname, $testname);
3032  
3033          // Test alternativefullnameformat that has been set to 'language'.
3034          $CFG->alternativefullnameformat = 'language';
3035          $expectedname = "$user->firstname $user->lastname";
3036          $testname = fullname($user, true);
3037          $this->assertSame($expectedname, $testname);
3038  
3039          // Test customising the alternativefullnameformat setting with all additional name fields.
3040          $CFG->alternativefullnameformat = 'firstname lastname firstnamephonetic lastnamephonetic middlename alternatename';
3041          $expectedname = "$user->firstname $user->lastname $user->firstnamephonetic $user->lastnamephonetic $user->middlename $user->alternatename";
3042          $testname = fullname($user, true);
3043          $this->assertSame($expectedname, $testname);
3044  
3045          // Test additional name fields.
3046          $CFG->fullnamedisplay = 'lastname lastnamephonetic firstname firstnamephonetic';
3047          $expectedname = "$user->lastname $user->lastnamephonetic $user->firstname $user->firstnamephonetic";
3048          $testname = fullname($user);
3049          $this->assertSame($expectedname, $testname);
3050  
3051          // Test for handling missing data.
3052          $user->middlename = null;
3053          // Parenthesis with no data.
3054          $CFG->fullnamedisplay = 'firstname (middlename) lastname';
3055          $expectedname = "$user->firstname $user->lastname";
3056          $testname = fullname($user);
3057          $this->assertSame($expectedname, $testname);
3058  
3059          // Extra spaces due to no data.
3060          $CFG->fullnamedisplay = 'firstname middlename lastname';
3061          $expectedname = "$user->firstname $user->lastname";
3062          $testname = fullname($user);
3063          $this->assertSame($expectedname, $testname);
3064  
3065          // Regular expression testing.
3066          // Remove some data from the user fields.
3067          $user->firstnamephonetic = '';
3068          $user->lastnamephonetic = '';
3069  
3070          // Removing empty brackets and excess whitespace.
3071          // All of these configurations should resolve to just firstname lastname.
3072          $configarray = array();
3073          $configarray[] = 'firstname lastname [firstnamephonetic lastnamephonetic]';
3074          $configarray[] = 'firstname lastname \'middlename\'';
3075          $configarray[] = 'firstname "firstnamephonetic" lastname';
3076          $configarray[] = 'firstname 「firstnamephonetic」 lastname 「lastnamephonetic」';
3077  
3078          foreach ($configarray as $config) {
3079              $CFG->fullnamedisplay = $config;
3080              $expectedname = "$user->firstname $user->lastname";
3081              $testname = fullname($user);
3082              $this->assertSame($expectedname, $testname);
3083          }
3084  
3085          // Check to make sure that other characters are left in place.
3086          $configarray = array();
3087          $configarray['0'] = new \stdClass();
3088          $configarray['0']->config = 'lastname firstname, middlename';
3089          $configarray['0']->expectedname = "$user->lastname $user->firstname,";
3090          $configarray['1'] = new \stdClass();
3091          $configarray['1']->config = 'lastname firstname + alternatename';
3092          $configarray['1']->expectedname = "$user->lastname $user->firstname + $user->alternatename";
3093          $configarray['2'] = new \stdClass();
3094          $configarray['2']->config = 'firstname aka: alternatename';
3095          $configarray['2']->expectedname = "$user->firstname aka: $user->alternatename";
3096          $configarray['3'] = new \stdClass();
3097          $configarray['3']->config = 'firstname (alternatename)';
3098          $configarray['3']->expectedname = "$user->firstname ($user->alternatename)";
3099          $configarray['4'] = new \stdClass();
3100          $configarray['4']->config = 'firstname [alternatename]';
3101          $configarray['4']->expectedname = "$user->firstname [$user->alternatename]";
3102          $configarray['5'] = new \stdClass();
3103          $configarray['5']->config = 'firstname "lastname"';
3104          $configarray['5']->expectedname = "$user->firstname \"$user->lastname\"";
3105  
3106          foreach ($configarray as $config) {
3107              $CFG->fullnamedisplay = $config->config;
3108              $expectedname = $config->expectedname;
3109              $testname = fullname($user);
3110              $this->assertSame($expectedname, $testname);
3111          }
3112  
3113          // Test debugging message displays when
3114          // fullnamedisplay setting is "normal".
3115          $CFG->fullnamedisplay = 'firstname lastname';
3116          unset($user);
3117          $user = new \stdClass();
3118          $user->firstname = 'Stan';
3119          $user->lastname = 'Lee';
3120          $namedisplay = fullname($user);
3121          $this->assertDebuggingCalled();
3122  
3123          // Tidy up after we finish testing.
3124          $CFG->fullnamedisplay = $originalcfg->fullnamedisplay;
3125          $CFG->alternativefullnameformat = $originalcfg->alternativefullnameformat;
3126      }
3127  
3128      /**
3129       * Tests the get_all_user_name_fields() deprecated function.
3130       *
3131       * @deprecated since Moodle 3.11 MDL-45242
3132       */
3133      public function test_get_all_user_name_fields() {
3134          $this->resetAfterTest();
3135  
3136          // Additional names in an array.
3137          $testarray = array('firstnamephonetic' => 'firstnamephonetic',
3138                  'lastnamephonetic' => 'lastnamephonetic',
3139                  'middlename' => 'middlename',
3140                  'alternatename' => 'alternatename',
3141                  'firstname' => 'firstname',
3142                  'lastname' => 'lastname');
3143          $this->assertEquals($testarray, get_all_user_name_fields());
3144  
3145          // Additional names as a string.
3146          $teststring = 'firstnamephonetic,lastnamephonetic,middlename,alternatename,firstname,lastname';
3147          $this->assertEquals($teststring, get_all_user_name_fields(true));
3148  
3149          // Additional names as a string with an alias.
3150          $teststring = 't.firstnamephonetic,t.lastnamephonetic,t.middlename,t.alternatename,t.firstname,t.lastname';
3151          $this->assertEquals($teststring, get_all_user_name_fields(true, 't'));
3152  
3153          // Additional name fields with a prefix - object.
3154          $testarray = array('firstnamephonetic' => 'authorfirstnamephonetic',
3155                  'lastnamephonetic' => 'authorlastnamephonetic',
3156                  'middlename' => 'authormiddlename',
3157                  'alternatename' => 'authoralternatename',
3158                  'firstname' => 'authorfirstname',
3159                  'lastname' => 'authorlastname');
3160          $this->assertEquals($testarray, get_all_user_name_fields(false, null, 'author'));
3161  
3162          // Additional name fields with an alias and a title - string.
3163          $teststring = 'u.firstnamephonetic AS authorfirstnamephonetic,u.lastnamephonetic AS authorlastnamephonetic,u.middlename AS authormiddlename,u.alternatename AS authoralternatename,u.firstname AS authorfirstname,u.lastname AS authorlastname';
3164          $this->assertEquals($teststring, get_all_user_name_fields(true, 'u', null, 'author'));
3165  
3166          // Test the order parameter of the function.
3167          // Returning an array.
3168          $testarray = array('firstname' => 'firstname',
3169                  'lastname' => 'lastname',
3170                  'firstnamephonetic' => 'firstnamephonetic',
3171                  'lastnamephonetic' => 'lastnamephonetic',
3172                  'middlename' => 'middlename',
3173                  'alternatename' => 'alternatename'
3174          );
3175          $this->assertEquals($testarray, get_all_user_name_fields(false, null, null, null, true));
3176  
3177          // Returning a string.
3178          $teststring = 'firstname,lastname,firstnamephonetic,lastnamephonetic,middlename,alternatename';
3179          $this->assertEquals($teststring, get_all_user_name_fields(true, null, null, null, true));
3180  
3181          $this->assertDebuggingCalledCount(7);
3182      }
3183  
3184      public function test_order_in_string() {
3185          $this->resetAfterTest();
3186  
3187          // Return an array in an order as they are encountered in a string.
3188          $valuearray = array('second', 'firsthalf', 'first');
3189          $formatstring = 'first firsthalf some other text (second)';
3190          $expectedarray = array('0' => 'first', '6' => 'firsthalf', '33' => 'second');
3191          $this->assertEquals($expectedarray, order_in_string($valuearray, $formatstring));
3192  
3193          // Try again with a different order for the format.
3194          $valuearray = array('second', 'firsthalf', 'first');
3195          $formatstring = 'firsthalf first second';
3196          $expectedarray = array('0' => 'firsthalf', '10' => 'first', '16' => 'second');
3197          $this->assertEquals($expectedarray, order_in_string($valuearray, $formatstring));
3198  
3199          // Try again with yet another different order for the format.
3200          $valuearray = array('second', 'firsthalf', 'first');
3201          $formatstring = 'start seconds away second firstquater first firsthalf';
3202          $expectedarray = array('19' => 'second', '38' => 'first', '44' => 'firsthalf');
3203          $this->assertEquals($expectedarray, order_in_string($valuearray, $formatstring));
3204      }
3205  
3206      public function test_complete_user_login() {
3207          global $USER, $DB;
3208  
3209          $this->resetAfterTest();
3210          $user = $this->getDataGenerator()->create_user();
3211          $this->setUser(0);
3212  
3213          $sink = $this->redirectEvents();
3214          $loginuser = clone($user);
3215          $this->setCurrentTimeStart();
3216          @complete_user_login($loginuser); // Hide session header errors.
3217          $this->assertSame($loginuser, $USER);
3218          $this->assertEquals($user->id, $USER->id);
3219          $events = $sink->get_events();
3220          $sink->close();
3221  
3222          $this->assertCount(1, $events);
3223          $event = reset($events);
3224          $this->assertInstanceOf('\core\event\user_loggedin', $event);
3225          $this->assertEquals('user', $event->objecttable);
3226          $this->assertEquals($user->id, $event->objectid);
3227          $this->assertEquals(\context_system::instance()->id, $event->contextid);
3228          $this->assertEventContextNotUsed($event);
3229  
3230          $user = $DB->get_record('user', array('id'=>$user->id));
3231  
3232          $this->assertTimeCurrent($user->firstaccess);
3233          $this->assertTimeCurrent($user->lastaccess);
3234  
3235          $this->assertTimeCurrent($USER->firstaccess);
3236          $this->assertTimeCurrent($USER->lastaccess);
3237          $this->assertTimeCurrent($USER->currentlogin);
3238          $this->assertSame(sesskey(), $USER->sesskey);
3239          $this->assertTimeCurrent($USER->preference['_lastloaded']);
3240          $this->assertObjectNotHasAttribute('password', $USER);
3241          $this->assertObjectNotHasAttribute('description', $USER);
3242      }
3243  
3244      /**
3245       * Test require_logout.
3246       */
3247      public function test_require_logout() {
3248          $this->resetAfterTest();
3249          $user = $this->getDataGenerator()->create_user();
3250          $this->setUser($user);
3251  
3252          $this->assertTrue(isloggedin());
3253  
3254          // Logout user and capture event.
3255          $sink = $this->redirectEvents();
3256          require_logout();
3257          $events = $sink->get_events();
3258          $sink->close();
3259          $event = array_pop($events);
3260  
3261          // Check if user is logged out.
3262          $this->assertFalse(isloggedin());
3263  
3264          // Test Event.
3265          $this->assertInstanceOf('\core\event\user_loggedout', $event);
3266          $this->assertSame($user->id, $event->objectid);
3267          $this->assertSame('user_logout', $event->get_legacy_eventname());
3268          $this->assertEventLegacyData($user, $event);
3269          $expectedlogdata = array(SITEID, 'user', 'logout', 'view.php?id='.$event->objectid.'&course='.SITEID, $event->objectid, 0,
3270              $event->objectid);
3271          $this->assertEventLegacyLogData($expectedlogdata, $event);
3272          $this->assertEventContextNotUsed($event);
3273      }
3274  
3275      /**
3276       * A data provider for testing email messageid
3277       */
3278      public function generate_email_messageid_provider() {
3279          return array(
3280              'nopath' => array(
3281                  'wwwroot' => 'http://www.example.com',
3282                  'ids' => array(
3283                      'a-custom-id' => '<a-custom-id@www.example.com>',
3284                      'an-id-with-/-a-slash' => '<an-id-with-%2F-a-slash@www.example.com>',
3285                  ),
3286              ),
3287              'path' => array(
3288                  'wwwroot' => 'http://www.example.com/path/subdir',
3289                  'ids' => array(
3290                      'a-custom-id' => '<a-custom-id/path/subdir@www.example.com>',
3291                      'an-id-with-/-a-slash' => '<an-id-with-%2F-a-slash/path/subdir@www.example.com>',
3292                  ),
3293              ),
3294          );
3295      }
3296  
3297      /**
3298       * Test email message id generation
3299       *
3300       * @dataProvider generate_email_messageid_provider
3301       *
3302       * @param string $wwwroot The wwwroot
3303       * @param array $msgids An array of msgid local parts and the final result
3304       */
3305      public function test_generate_email_messageid($wwwroot, $msgids) {
3306          global $CFG;
3307  
3308          $this->resetAfterTest();
3309          $CFG->wwwroot = $wwwroot;
3310  
3311          foreach ($msgids as $local => $final) {
3312              $this->assertEquals($final, generate_email_messageid($local));
3313          }
3314      }
3315  
3316      /**
3317       * Test email with custom headers
3318       */
3319      public function test_send_email_with_custom_header() {
3320          global $DB, $CFG;
3321          $this->preventResetByRollback();
3322          $this->resetAfterTest();
3323  
3324          $touser = $this->getDataGenerator()->create_user();
3325          $fromuser = $this->getDataGenerator()->create_user();
3326          $fromuser->customheaders = 'X-Custom-Header: foo';
3327  
3328          set_config('allowedemaildomains', 'example.com');
3329          set_config('emailheaders', 'X-Fixed-Header: bar');
3330  
3331          $sink = $this->redirectEmails();
3332          email_to_user($touser, $fromuser, 'subject', 'message');
3333  
3334          $emails = $sink->get_messages();
3335          $this->assertCount(1, $emails);
3336          $email = reset($emails);
3337          $this->assertStringContainsString('X-Custom-Header: foo', $email->header);
3338          $this->assertStringContainsString("X-Fixed-Header: bar", $email->header);
3339          $sink->clear();
3340      }
3341  
3342      /**
3343       * A data provider for testing email diversion
3344       */
3345      public function diverted_emails_provider() {
3346          return array(
3347              'nodiverts' => array(
3348                  'divertallemailsto' => null,
3349                  'divertallemailsexcept' => null,
3350                  array(
3351                      'foo@example.com',
3352                      'test@real.com',
3353                      'fred.jones@example.com',
3354                      'dev1@dev.com',
3355                      'fred@example.com',
3356                      'fred+verp@example.com',
3357                  ),
3358                  false,
3359              ),
3360              'alldiverts' => array(
3361                  'divertallemailsto' => 'somewhere@elsewhere.com',
3362                  'divertallemailsexcept' => null,
3363                  array(
3364                      'foo@example.com',
3365                      'test@real.com',
3366                      'fred.jones@example.com',
3367                      'dev1@dev.com',
3368                      'fred@example.com',
3369                      'fred+verp@example.com',
3370                  ),
3371                  true,
3372              ),
3373              'alsodiverts' => array(
3374                  'divertallemailsto' => 'somewhere@elsewhere.com',
3375                  'divertallemailsexcept' => '@dev.com, fred(\+.*)?@example.com',
3376                  array(
3377                      'foo@example.com',
3378                      'test@real.com',
3379                      'fred.jones@example.com',
3380                  ),
3381                  true,
3382              ),
3383              'divertsexceptions' => array(
3384                  'divertallemailsto' => 'somewhere@elsewhere.com',
3385                  'divertallemailsexcept' => '@dev.com, fred(\+.*)?@example.com',
3386                  array(
3387                      'dev1@dev.com',
3388                      'fred@example.com',
3389                      'fred+verp@example.com',
3390                  ),
3391                  false,
3392              ),
3393              'divertsexceptionsnewline' => array(
3394                  'divertallemailsto' => 'somewhere@elsewhere.com',
3395                  'divertallemailsexcept' => "@dev.com\nfred(\+.*)?@example.com",
3396                  array(
3397                      'dev1@dev.com',
3398                      'fred@example.com',
3399                      'fred+verp@example.com',
3400                  ),
3401                  false,
3402              ),
3403              'alsodivertsnewline' => array(
3404                  'divertallemailsto' => 'somewhere@elsewhere.com',
3405                  'divertallemailsexcept' => "@dev.com\nfred(\+.*)?@example.com",
3406                  array(
3407                      'foo@example.com',
3408                      'test@real.com',
3409                      'fred.jones@example.com',
3410                  ),
3411                  true,
3412              ),
3413              'alsodivertsblankline' => array(
3414                  'divertallemailsto' => 'somewhere@elsewhere.com',
3415                  'divertallemailsexcept' => "@dev.com\n",
3416                  [
3417                      'lionel@example.com',
3418                  ],
3419                  true,
3420              ),
3421              'divertsexceptionblankline' => array(
3422                  'divertallemailsto' => 'somewhere@elsewhere.com',
3423                  'divertallemailsexcept' => "@example.com\n",
3424                  [
3425                      'lionel@example.com',
3426                  ],
3427                  false,
3428              ),
3429          );
3430      }
3431  
3432      /**
3433       * Test email diversion
3434       *
3435       * @dataProvider diverted_emails_provider
3436       *
3437       * @param string $divertallemailsto An optional email address
3438       * @param string $divertallemailsexcept An optional exclusion list
3439       * @param array $addresses An array of test addresses
3440       * @param boolean $expected Expected result
3441       */
3442      public function test_email_should_be_diverted($divertallemailsto, $divertallemailsexcept, $addresses, $expected) {
3443          global $CFG;
3444  
3445          $this->resetAfterTest();
3446          $CFG->divertallemailsto = $divertallemailsto;
3447          $CFG->divertallemailsexcept = $divertallemailsexcept;
3448  
3449          foreach ($addresses as $address) {
3450              $this->assertEquals($expected, email_should_be_diverted($address));
3451          }
3452      }
3453  
3454      public function test_email_to_user() {
3455          global $CFG;
3456  
3457          $this->resetAfterTest();
3458  
3459          $user1 = $this->getDataGenerator()->create_user(array('maildisplay' => 1, 'mailformat' => 0));
3460          $user2 = $this->getDataGenerator()->create_user(array('maildisplay' => 1, 'mailformat' => 1));
3461          $user3 = $this->getDataGenerator()->create_user(array('maildisplay' => 0));
3462          set_config('allowedemaildomains', "example.com\r\nmoodle.org");
3463  
3464          $subject = 'subject';
3465          $messagetext = 'message text';
3466          $subject2 = 'subject 2';
3467          $messagetext2 = '<b>message text 2</b>';
3468  
3469          // Close the default email sink.
3470          $sink = $this->redirectEmails();
3471          $sink->close();
3472  
3473          $CFG->noemailever = true;
3474          $this->assertNotEmpty($CFG->noemailever);
3475          email_to_user($user1, $user2, $subject, $messagetext);
3476          $this->assertDebuggingCalled('Not sending email due to $CFG->noemailever config setting');
3477  
3478          unset_config('noemailever');
3479  
3480          email_to_user($user1, $user2, $subject, $messagetext);
3481          $this->assertDebuggingCalled('Unit tests must not send real emails! Use $this->redirectEmails()');
3482  
3483          $sink = $this->redirectEmails();
3484          email_to_user($user1, $user2, $subject, $messagetext);
3485          email_to_user($user2, $user1, $subject2, $messagetext2);
3486          $this->assertSame(2, $sink->count());
3487          $result = $sink->get_messages();
3488          $this->assertCount(2, $result);
3489          $sink->close();
3490  
3491          $this->assertSame($subject, $result[0]->subject);
3492          $this->assertSame($messagetext, trim($result[0]->body));
3493          $this->assertSame($user1->email, $result[0]->to);
3494          $this->assertSame($user2->email, $result[0]->from);
3495          $this->assertStringContainsString('Content-Type: text/plain', $result[0]->header);
3496  
3497          $this->assertSame($subject2, $result[1]->subject);
3498          $this->assertStringContainsString($messagetext2, quoted_printable_decode($result[1]->body));
3499          $this->assertSame($user2->email, $result[1]->to);
3500          $this->assertSame($user1->email, $result[1]->from);
3501          $this->assertStringNotContainsString('Content-Type: text/plain', $result[1]->header);
3502  
3503          email_to_user($user1, $user2, $subject, $messagetext);
3504          $this->assertDebuggingCalled('Unit tests must not send real emails! Use $this->redirectEmails()');
3505  
3506          // Test that an empty noreplyaddress will default to a no-reply address.
3507          $sink = $this->redirectEmails();
3508          email_to_user($user1, $user3, $subject, $messagetext);
3509          $result = $sink->get_messages();
3510          $this->assertEquals($CFG->noreplyaddress, $result[0]->from);
3511          $sink->close();
3512          set_config('noreplyaddress', '');
3513          $sink = $this->redirectEmails();
3514          email_to_user($user1, $user3, $subject, $messagetext);
3515          $result = $sink->get_messages();
3516          $this->assertEquals('noreply@www.example.com', $result[0]->from);
3517          $sink->close();
3518  
3519          // Test $CFG->allowedemaildomains.
3520          set_config('noreplyaddress', 'noreply@www.example.com');
3521          $this->assertNotEmpty($CFG->allowedemaildomains);
3522          $sink = $this->redirectEmails();
3523          email_to_user($user1, $user2, $subject, $messagetext);
3524          unset_config('allowedemaildomains');
3525          email_to_user($user1, $user2, $subject, $messagetext);
3526          $result = $sink->get_messages();
3527          $this->assertNotEquals($CFG->noreplyaddress, $result[0]->from);
3528          $this->assertEquals($CFG->noreplyaddress, $result[1]->from);
3529          $sink->close();
3530  
3531          // Try to send an unsafe attachment, we should see an error message in the eventual mail body.
3532          $attachment = '../test.txt';
3533          $attachname = 'txt';
3534  
3535          $sink = $this->redirectEmails();
3536          email_to_user($user1, $user2, $subject, $messagetext, '', $attachment, $attachname);
3537          $this->assertSame(1, $sink->count());
3538          $result = $sink->get_messages();
3539          $this->assertCount(1, $result);
3540          $this->assertStringContainsString('error.txt', $result[0]->body);
3541          $this->assertStringContainsString('Error in attachment.  User attempted to attach a filename with a unsafe name.', $result[0]->body);
3542          $sink->close();
3543      }
3544  
3545      /**
3546       * Data provider for {@see test_email_to_user_attachment}
3547       *
3548       * @return array
3549       */
3550      public function email_to_user_attachment_provider(): array {
3551          global $CFG;
3552  
3553          // Return all paths that can be used to send attachments from.
3554          return [
3555              'cachedir' => [$CFG->cachedir],
3556              'dataroot' => [$CFG->dataroot],
3557              'dirroot' => [$CFG->dirroot],
3558              'localcachedir' => [$CFG->localcachedir],
3559              'tempdir' => [$CFG->tempdir],
3560              // Paths within $CFG->localrequestdir.
3561              'localrequestdir_request_directory' => [make_request_directory()],
3562              'localrequestdir_request_storage_directory' => [get_request_storage_directory()],
3563              // Pass null to indicate we want to test a path relative to $CFG->dataroot.
3564              'relative' => [null]
3565          ];
3566      }
3567  
3568      /**
3569       * Test sending attachments with email_to_user
3570       *
3571       * @param string|null $filedir
3572       *
3573       * @dataProvider email_to_user_attachment_provider
3574       */
3575      public function test_email_to_user_attachment(?string $filedir): void {
3576          global $CFG;
3577  
3578          // If $filedir is null, then write our test file to $CFG->dataroot.
3579          $filepath = ($filedir ?: $CFG->dataroot) . '/hello.txt';
3580          file_put_contents($filepath, 'Hello');
3581  
3582          $user = \core_user::get_support_user();
3583          $message = 'Test attachment path';
3584  
3585          // Create sink to catch all sent e-mails.
3586          $sink = $this->redirectEmails();
3587  
3588          // Attachment path will be that of the test file if $filedir was passed, otherwise the relative path from $CFG->dataroot.
3589          $filename = basename($filepath);
3590          $attachmentpath = $filedir ? $filepath : $filename;
3591          email_to_user($user, $user, $message, $message, $message, $attachmentpath, $filename);
3592  
3593          $messages = $sink->get_messages();
3594          $sink->close();
3595  
3596          $this->assertCount(1, $messages);
3597  
3598          // Verify attachment in message body (attachment is in MIME format, but we can detect some Content fields).
3599          $messagebody = reset($messages)->body;
3600          $this->assertStringContainsString('Content-Type: text/plain; name=' . $filename, $messagebody);
3601          $this->assertStringContainsString('Content-Disposition: attachment; filename=' . $filename, $messagebody);
3602  
3603          // Cleanup.
3604          unlink($filepath);
3605      }
3606  
3607      /**
3608       * Test sending an attachment that doesn't exist to email_to_user
3609       */
3610      public function test_email_to_user_attachment_missing(): void {
3611          $user = \core_user::get_support_user();
3612          $message = 'Test attachment path';
3613  
3614          // Create sink to catch all sent e-mails.
3615          $sink = $this->redirectEmails();
3616  
3617          $attachmentpath = '/hola/hello.txt';
3618          $filename = basename($attachmentpath);
3619          email_to_user($user, $user, $message, $message, $message, $attachmentpath, $filename);
3620  
3621          $messages = $sink->get_messages();
3622          $sink->close();
3623  
3624          $this->assertCount(1, $messages);
3625  
3626          // Verify attachment not in message body (attachment is in MIME format, but we can detect some Content fields).
3627          $messagebody = reset($messages)->body;
3628          $this->assertStringNotContainsString('Content-Type: text/plain; name="' . $filename . '"', $messagebody);
3629          $this->assertStringNotContainsString('Content-Disposition: attachment; filename=' . $filename, $messagebody);
3630      }
3631  
3632      /**
3633       * Test setnew_password_and_mail.
3634       */
3635      public function test_setnew_password_and_mail() {
3636          global $DB, $CFG;
3637  
3638          $this->resetAfterTest();
3639  
3640          $user = $this->getDataGenerator()->create_user();
3641  
3642          // Update user password.
3643          $sink = $this->redirectEvents();
3644          $sink2 = $this->redirectEmails(); // Make sure we are redirecting emails.
3645          setnew_password_and_mail($user);
3646          $events = $sink->get_events();
3647          $sink->close();
3648          $sink2->close();
3649          $event = array_pop($events);
3650  
3651          // Test updated value.
3652          $dbuser = $DB->get_record('user', array('id' => $user->id));
3653          $this->assertSame($user->firstname, $dbuser->firstname);
3654          $this->assertNotEmpty($dbuser->password);
3655  
3656          // Test event.
3657          $this->assertInstanceOf('\core\event\user_password_updated', $event);
3658          $this->assertSame($user->id, $event->relateduserid);
3659          $this->assertEquals(\context_user::instance($user->id), $event->get_context());
3660          $this->assertEventContextNotUsed($event);
3661      }
3662  
3663      /**
3664       * Data provider for test_generate_confirmation_link
3665       * @return Array of confirmation urls and expected resultant confirmation links
3666       */
3667      public function generate_confirmation_link_provider() {
3668          global $CFG;
3669          return [
3670              "Simple name" => [
3671                  "username" => "simplename",
3672                  "confirmationurl" => null,
3673                  "expected" => $CFG->wwwroot . "/login/confirm.php?data=/simplename"
3674              ],
3675              "Period in between words in username" => [
3676                  "username" => "period.inbetween",
3677                  "confirmationurl" => null,
3678                  "expected" => $CFG->wwwroot . "/login/confirm.php?data=/period%2Einbetween"
3679              ],
3680              "Trailing periods in username" => [
3681                  "username" => "trailingperiods...",
3682                  "confirmationurl" => null,
3683                  "expected" => $CFG->wwwroot . "/login/confirm.php?data=/trailingperiods%2E%2E%2E"
3684              ],
3685              "At symbol in username" => [
3686                  "username" => "at@symbol",
3687                  "confirmationurl" => null,
3688                  "expected" => $CFG->wwwroot . "/login/confirm.php?data=/at%40symbol"
3689              ],
3690              "Dash symbol in username" => [
3691                  "username" => "has-dash",
3692                  "confirmationurl" => null,
3693                  "expected" => $CFG->wwwroot . "/login/confirm.php?data=/has-dash"
3694              ],
3695              "Underscore in username" => [
3696                  "username" => "under_score",
3697                  "confirmationurl" => null,
3698                  "expected" => $CFG->wwwroot . "/login/confirm.php?data=/under_score"
3699              ],
3700              "Many different characters in username" => [
3701                  "username" => "many_-.@characters@_@-..-..",
3702                  "confirmationurl" => null,
3703                  "expected" => $CFG->wwwroot . "/login/confirm.php?data=/many_-%2E%40characters%40_%40-%2E%2E-%2E%2E"
3704              ],
3705              "Custom relative confirmation url" => [
3706                  "username" => "many_-.@characters@_@-..-..",
3707                  "confirmationurl" => "/custom/local/url.php",
3708                  "expected" => $CFG->wwwroot . "/custom/local/url.php?data=/many_-%2E%40characters%40_%40-%2E%2E-%2E%2E"
3709              ],
3710              "Custom relative confirmation url with parameters" => [
3711                  "username" => "many_-.@characters@_@-..-..",
3712                  "confirmationurl" => "/custom/local/url.php?with=param",
3713                  "expected" => $CFG->wwwroot . "/custom/local/url.php?with=param&data=/many_-%2E%40characters%40_%40-%2E%2E-%2E%2E"
3714              ],
3715              "Custom local confirmation url" => [
3716                  "username" => "many_-.@characters@_@-..-..",
3717                  "confirmationurl" => $CFG->wwwroot . "/custom/local/url.php",
3718                  "expected" => $CFG->wwwroot . "/custom/local/url.php?data=/many_-%2E%40characters%40_%40-%2E%2E-%2E%2E"
3719              ],
3720              "Custom local confirmation url with parameters" => [
3721                  "username" => "many_-.@characters@_@-..-..",
3722                  "confirmationurl" => $CFG->wwwroot . "/custom/local/url.php?with=param",
3723                  "expected" => $CFG->wwwroot . "/custom/local/url.php?with=param&data=/many_-%2E%40characters%40_%40-%2E%2E-%2E%2E"
3724              ],
3725              "Custom external confirmation url" => [
3726                  "username" => "many_-.@characters@_@-..-..",
3727                  "confirmationurl" => "http://moodle.org/custom/external/url.php",
3728                  "expected" => "http://moodle.org/custom/external/url.php?data=/many_-%2E%40characters%40_%40-%2E%2E-%2E%2E"
3729              ],
3730              "Custom external confirmation url with parameters" => [
3731                  "username" => "many_-.@characters@_@-..-..",
3732                  "confirmationurl" => "http://moodle.org/ext.php?with=some&param=eters",
3733                  "expected" => "http://moodle.org/ext.php?with=some&param=eters&data=/many_-%2E%40characters%40_%40-%2E%2E-%2E%2E"
3734              ],
3735              "Custom external confirmation url with parameters" => [
3736                  "username" => "many_-.@characters@_@-..-..",
3737                  "confirmationurl" => "http://moodle.org/ext.php?with=some&data=test",
3738                  "expected" => "http://moodle.org/ext.php?with=some&data=/many_-%2E%40characters%40_%40-%2E%2E-%2E%2E"
3739              ],
3740          ];
3741      }
3742  
3743      /**
3744       * Test generate_confirmation_link
3745       * @dataProvider generate_confirmation_link_provider
3746       * @param string $username The name of the user
3747       * @param string $confirmationurl The url the user should go to to confirm
3748       * @param string $expected The expected url of the confirmation link
3749       */
3750      public function test_generate_confirmation_link($username, $confirmationurl, $expected) {
3751          $this->resetAfterTest();
3752          $sink = $this->redirectEmails();
3753  
3754          $user = $this->getDataGenerator()->create_user(
3755              [
3756                  "username" => $username,
3757                  "confirmed" => 0,
3758                  "email" => 'test@example.com',
3759              ]
3760          );
3761  
3762          send_confirmation_email($user, $confirmationurl);
3763          $sink->close();
3764          $messages = $sink->get_messages();
3765          $message = array_shift($messages);
3766          $messagebody = quoted_printable_decode($message->body);
3767  
3768          $this->assertStringContainsString($expected, $messagebody);
3769      }
3770  
3771      /**
3772       * Test generate_confirmation_link with custom admin link
3773       */
3774      public function test_generate_confirmation_link_with_custom_admin() {
3775          global $CFG;
3776  
3777          $this->resetAfterTest();
3778          $sink = $this->redirectEmails();
3779  
3780          $admin = $CFG->admin;
3781          $CFG->admin = 'custom/admin/path';
3782  
3783          $user = $this->getDataGenerator()->create_user(
3784              [
3785                  "username" => "many_-.@characters@_@-..-..",
3786                  "confirmed" => 0,
3787                  "email" => 'test@example.com',
3788              ]
3789          );
3790          $confirmationurl = "/admin/test.php?with=params";
3791          $expected = $CFG->wwwroot . "/" . $CFG->admin . "/test.php?with=params&data=/many_-%2E%40characters%40_%40-%2E%2E-%2E%2E";
3792  
3793          send_confirmation_email($user, $confirmationurl);
3794          $sink->close();
3795          $messages = $sink->get_messages();
3796          $message = array_shift($messages);
3797          $messagebody = quoted_printable_decode($message->body);
3798  
3799          $sink->close();
3800          $this->assertStringContainsString($expected, $messagebody);
3801  
3802          $CFG->admin = $admin;
3803      }
3804  
3805  
3806      /**
3807       * Test remove_course_content deletes course contents
3808       * TODO Add asserts to verify other data related to course is deleted as well.
3809       */
3810      public function test_remove_course_contents() {
3811  
3812          $this->resetAfterTest();
3813  
3814          $course = $this->getDataGenerator()->create_course();
3815          $user = $this->getDataGenerator()->create_user();
3816          $gen = $this->getDataGenerator()->get_plugin_generator('core_notes');
3817          $note = $gen->create_instance(array('courseid' => $course->id, 'userid' => $user->id));
3818  
3819          $this->assertNotEquals(false, note_load($note->id));
3820          remove_course_contents($course->id, false);
3821          $this->assertFalse(note_load($note->id));
3822      }
3823  
3824      /**
3825       * Test function username_load_fields_from_object().
3826       */
3827      public function test_username_load_fields_from_object() {
3828          $this->resetAfterTest();
3829  
3830          // This object represents the information returned from an sql query.
3831          $userinfo = new \stdClass();
3832          $userinfo->userid = 1;
3833          $userinfo->username = 'loosebruce';
3834          $userinfo->firstname = 'Bruce';
3835          $userinfo->lastname = 'Campbell';
3836          $userinfo->firstnamephonetic = 'ブルース';
3837          $userinfo->lastnamephonetic = 'カンベッル';
3838          $userinfo->middlename = '';
3839          $userinfo->alternatename = '';
3840          $userinfo->email = '';
3841          $userinfo->picture = 23;
3842          $userinfo->imagealt = 'Michael Jordan draining another basket.';
3843          $userinfo->idnumber = 3982;
3844  
3845          // Just user name fields.
3846          $user = new \stdClass();
3847          $user = username_load_fields_from_object($user, $userinfo);
3848          $expectedarray = new \stdClass();
3849          $expectedarray->firstname = 'Bruce';
3850          $expectedarray->lastname = 'Campbell';
3851          $expectedarray->firstnamephonetic = 'ブルース';
3852          $expectedarray->lastnamephonetic = 'カンベッル';
3853          $expectedarray->middlename = '';
3854          $expectedarray->alternatename = '';
3855          $this->assertEquals($user, $expectedarray);
3856  
3857          // User information for showing a picture.
3858          $user = new \stdClass();
3859          $additionalfields = explode(',', implode(',', \core_user\fields::get_picture_fields()));
3860          $user = username_load_fields_from_object($user, $userinfo, null, $additionalfields);
3861          $user->id = $userinfo->userid;
3862          $expectedarray = new \stdClass();
3863          $expectedarray->id = 1;
3864          $expectedarray->firstname = 'Bruce';
3865          $expectedarray->lastname = 'Campbell';
3866          $expectedarray->firstnamephonetic = 'ブルース';
3867          $expectedarray->lastnamephonetic = 'カンベッル';
3868          $expectedarray->middlename = '';
3869          $expectedarray->alternatename = '';
3870          $expectedarray->email = '';
3871          $expectedarray->picture = 23;
3872          $expectedarray->imagealt = 'Michael Jordan draining another basket.';
3873          $this->assertEquals($user, $expectedarray);
3874  
3875          // Alter the userinfo object to have a prefix.
3876          $userinfo->authorfirstname = 'Bruce';
3877          $userinfo->authorlastname = 'Campbell';
3878          $userinfo->authorfirstnamephonetic = 'ブルース';
3879          $userinfo->authorlastnamephonetic = 'カンベッル';
3880          $userinfo->authormiddlename = '';
3881          $userinfo->authorpicture = 23;
3882          $userinfo->authorimagealt = 'Michael Jordan draining another basket.';
3883          $userinfo->authoremail = 'test@example.com';
3884  
3885  
3886          // Return an object with user picture information.
3887          $user = new \stdClass();
3888          $additionalfields = explode(',', implode(',', \core_user\fields::get_picture_fields()));
3889          $user = username_load_fields_from_object($user, $userinfo, 'author', $additionalfields);
3890          $user->id = $userinfo->userid;
3891          $expectedarray = new \stdClass();
3892          $expectedarray->id = 1;
3893          $expectedarray->firstname = 'Bruce';
3894          $expectedarray->lastname = 'Campbell';
3895          $expectedarray->firstnamephonetic = 'ブルース';
3896          $expectedarray->lastnamephonetic = 'カンベッル';
3897          $expectedarray->middlename = '';
3898          $expectedarray->alternatename = '';
3899          $expectedarray->email = 'test@example.com';
3900          $expectedarray->picture = 23;
3901          $expectedarray->imagealt = 'Michael Jordan draining another basket.';
3902          $this->assertEquals($user, $expectedarray);
3903      }
3904  
3905      /**
3906       * Test function {@see count_words()}.
3907       *
3908       * @dataProvider count_words_testcases
3909       * @param int $expectedcount number of words in $string.
3910       * @param string $string the test string to count the words of.
3911       */
3912      public function test_count_words(int $expectedcount, string $string): void {
3913          $this->assertEquals($expectedcount, count_words($string));
3914      }
3915  
3916      /**
3917       * Data provider for {@see test_count_words}.
3918       *
3919       * @return array of test cases.
3920       */
3921      public function count_words_testcases(): array {
3922          // The counts here should match MS Word and Libre Office.
3923          return [
3924              [0, ''],
3925              [4, 'one two three four'],
3926              [1, "a'b"],
3927              [1, '1+1=2'],
3928              [1, ' one-sided '],
3929              [2, 'one&nbsp;two'],
3930              [1, 'email@example.com'],
3931              [2, 'first\part second/part'],
3932              [4, '<p>one two<br></br>three four</p>'],
3933              [4, '<p>one two<br>three four</p>'],
3934              [4, '<p>one two<br />three four</p>'], // XHTML style.
3935              [3, ' one ... three '],
3936              [1, 'just...one'],
3937              [3, ' one & three '],
3938              [1, 'just&one'],
3939              [2, 'em—dash'],
3940              [2, 'en–dash'],
3941              [4, '1³ £2 €3.45 $6,789'],
3942              [2, 'ブルース カンベッル'], // MS word counts this as 11, but we don't handle that yet.
3943              [4, '<p>one two</p><p>three four</p>'],
3944              [4, '<p>one two</p><p><br/></p><p>three four</p>'],
3945              [4, '<p>one</p><ul><li>two</li><li>three</li></ul><p>four.</p>'],
3946              [1, '<p>em<b>phas</b>is.</p>'],
3947              [1, '<p>em<i>phas</i>is.</p>'],
3948              [1, '<p>em<strong>phas</strong>is.</p>'],
3949              [1, '<p>em<em>phas</em>is.</p>'],
3950              [2, "one\ntwo"],
3951              [2, "one\rtwo"],
3952              [2, "one\ttwo"],
3953              [2, "one\vtwo"],
3954              [2, "one\ftwo"],
3955              [1, "SO<sub>4</sub><sup>2-</sup>"],
3956              [6, '4+4=8 i.e. O(1) a,b,c,d I’m black&blue_really'],
3957              [1, '<span>a</span><span>b</span>'],
3958          ];
3959      }
3960  
3961      /**
3962       * Test function {@see count_letters()}.
3963       *
3964       * @dataProvider count_letters_testcases
3965       * @param int $expectedcount number of characters in $string.
3966       * @param string $string the test string to count the letters of.
3967       */
3968      public function test_count_letters(int $expectedcount, string $string): void {
3969          $this->assertEquals($expectedcount, count_letters($string));
3970      }
3971  
3972      /**
3973       * Data provider for {@see count_letters_testcases}.
3974       *
3975       * @return array of test cases.
3976       */
3977      public function count_letters_testcases(): array {
3978          return [
3979              [0, ''],
3980              [1, 'x'],
3981              [1, '&amp;'],
3982              [4, '<p>frog</p>'],
3983          ];
3984      }
3985  
3986      /**
3987       * Tests the getremoteaddr() function.
3988       */
3989      public function test_getremoteaddr() {
3990          global $CFG;
3991  
3992          $this->resetAfterTest();
3993  
3994          $CFG->getremoteaddrconf = null; // Use default value, GETREMOTEADDR_SKIP_DEFAULT.
3995          $noip = getremoteaddr('1.1.1.1');
3996          $this->assertEquals('1.1.1.1', $noip);
3997  
3998          $remoteaddr = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null;
3999          $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
4000          $singleip = getremoteaddr();
4001          $this->assertEquals('127.0.0.1', $singleip);
4002  
4003          $_SERVER['REMOTE_ADDR'] = $remoteaddr; // Restore server value.
4004  
4005          $CFG->getremoteaddrconf = 0; // Don't skip any source.
4006          $noip = getremoteaddr('1.1.1.1');
4007          $this->assertEquals('1.1.1.1', $noip);
4008  
4009          // Populate all $_SERVER values to review order.
4010          $ipsources = [
4011              'HTTP_CLIENT_IP' => '2.2.2.2',
4012              'HTTP_X_FORWARDED_FOR' => '3.3.3.3',
4013              'REMOTE_ADDR' => '4.4.4.4',
4014          ];
4015          $originalvalues = [];
4016          foreach ($ipsources as $source => $ip) {
4017              $originalvalues[$source] = isset($_SERVER[$source]) ? $_SERVER[$source] : null; // Saving data to restore later.
4018              $_SERVER[$source] = $ip;
4019          }
4020  
4021          foreach ($ipsources as $source => $expectedip) {
4022              $ip = getremoteaddr();
4023              $this->assertEquals($expectedip, $ip);
4024              unset($_SERVER[$source]); // Removing the value so next time we get the following ip.
4025          }
4026  
4027          // Restore server values.
4028          foreach ($originalvalues as $source => $ip) {
4029              $_SERVER[$source] = $ip;
4030          }
4031  
4032          // All $_SERVER values have been removed, we should get the default again.
4033          $noip = getremoteaddr('1.1.1.1');
4034          $this->assertEquals('1.1.1.1', $noip);
4035  
4036          $CFG->getremoteaddrconf = GETREMOTEADDR_SKIP_HTTP_CLIENT_IP;
4037          $xforwardedfor = isset($_SERVER['HTTP_X_FORWARDED_FOR']) ? $_SERVER['HTTP_X_FORWARDED_FOR'] : null;
4038  
4039          $_SERVER['HTTP_X_FORWARDED_FOR'] = '';
4040          $noip = getremoteaddr('1.1.1.1');
4041          $this->assertEquals('1.1.1.1', $noip);
4042  
4043          $_SERVER['HTTP_X_FORWARDED_FOR'] = '';
4044          $noip = getremoteaddr();
4045          $this->assertEquals('0.0.0.0', $noip);
4046  
4047          $_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1';
4048          $singleip = getremoteaddr();
4049          $this->assertEquals('127.0.0.1', $singleip);
4050  
4051          $_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1,127.0.0.2';
4052          $twoip = getremoteaddr();
4053          $this->assertEquals('127.0.0.2', $twoip);
4054  
4055          $_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1,127.0.0.2,127.0.0.3';
4056          $threeip = getremoteaddr();
4057          $this->assertEquals('127.0.0.3', $threeip);
4058  
4059          $_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1,127.0.0.2:65535';
4060          $portip = getremoteaddr();
4061          $this->assertEquals('127.0.0.2', $portip);
4062  
4063          $_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1,0:0:0:0:0:0:0:2';
4064          $portip = getremoteaddr();
4065          $this->assertEquals('0:0:0:0:0:0:0:2', $portip);
4066  
4067          $_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1,0::2';
4068          $portip = getremoteaddr();
4069          $this->assertEquals('0:0:0:0:0:0:0:2', $portip);
4070  
4071          $_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1,[0:0:0:0:0:0:0:2]:65535';
4072          $portip = getremoteaddr();
4073          $this->assertEquals('0:0:0:0:0:0:0:2', $portip);
4074  
4075          $_SERVER['HTTP_X_FORWARDED_FOR'] = $xforwardedfor;
4076  
4077      }
4078  
4079      /*
4080       * Test emulation of random_bytes() function.
4081       */
4082      public function test_random_bytes_emulate() {
4083          $result = random_bytes_emulate(10);
4084          $this->assertSame(10, strlen($result));
4085          $this->assertnotSame($result, random_bytes_emulate(10));
4086  
4087          $result = random_bytes_emulate(21);
4088          $this->assertSame(21, strlen($result));
4089          $this->assertnotSame($result, random_bytes_emulate(21));
4090  
4091          $result = random_bytes_emulate(666);
4092          $this->assertSame(666, strlen($result));
4093  
4094          $result = random_bytes_emulate(40);
4095          $this->assertSame(40, strlen($result));
4096  
4097          $this->assertDebuggingNotCalled();
4098  
4099          $result = random_bytes_emulate(0);
4100          $this->assertSame('', $result);
4101          $this->assertDebuggingCalled();
4102  
4103          $result = random_bytes_emulate(-1);
4104          $this->assertSame('', $result);
4105          $this->assertDebuggingCalled();
4106      }
4107  
4108      /**
4109       * Test function for creation of random strings.
4110       */
4111      public function test_random_string() {
4112          $pool = 'a-zA-Z0-9';
4113  
4114          $result = random_string(10);
4115          $this->assertSame(10, strlen($result));
4116          $this->assertMatchesRegularExpression('/^[' . $pool . ']+$/', $result);
4117          $this->assertNotSame($result, random_string(10));
4118  
4119          $result = random_string(21);
4120          $this->assertSame(21, strlen($result));
4121          $this->assertMatchesRegularExpression('/^[' . $pool . ']+$/', $result);
4122          $this->assertNotSame($result, random_string(21));
4123  
4124          $result = random_string(666);
4125          $this->assertSame(666, strlen($result));
4126          $this->assertMatchesRegularExpression('/^[' . $pool . ']+$/', $result);
4127  
4128          $result = random_string();
4129          $this->assertSame(15, strlen($result));
4130          $this->assertMatchesRegularExpression('/^[' . $pool . ']+$/', $result);
4131  
4132          $this->assertDebuggingNotCalled();
4133  
4134          $result = random_string(0);
4135          $this->assertSame('', $result);
4136          $this->assertDebuggingCalled();
4137  
4138          $result = random_string(-1);
4139          $this->assertSame('', $result);
4140          $this->assertDebuggingCalled();
4141      }
4142  
4143      /**
4144       * Test function for creation of complex random strings.
4145       */
4146      public function test_complex_random_string() {
4147          $pool = preg_quote('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789`~!@#%^&*()_+-=[];,./<>?:{} ', '/');
4148  
4149          $result = complex_random_string(10);
4150          $this->assertSame(10, strlen($result));
4151          $this->assertMatchesRegularExpression('/^[' . $pool . ']+$/', $result);
4152          $this->assertNotSame($result, complex_random_string(10));
4153  
4154          $result = complex_random_string(21);
4155          $this->assertSame(21, strlen($result));
4156          $this->assertMatchesRegularExpression('/^[' . $pool . ']+$/', $result);
4157          $this->assertNotSame($result, complex_random_string(21));
4158  
4159          $result = complex_random_string(666);
4160          $this->assertSame(666, strlen($result));
4161          $this->assertMatchesRegularExpression('/^[' . $pool . ']+$/', $result);
4162  
4163          $result = complex_random_string();
4164          $this->assertEqualsWithDelta(28, strlen($result), 4); // Expected length is 24 - 32.
4165          $this->assertMatchesRegularExpression('/^[' . $pool . ']+$/', $result);
4166  
4167          $this->assertDebuggingNotCalled();
4168  
4169          $result = complex_random_string(0);
4170          $this->assertSame('', $result);
4171          $this->assertDebuggingCalled();
4172  
4173          $result = complex_random_string(-1);
4174          $this->assertSame('', $result);
4175          $this->assertDebuggingCalled();
4176      }
4177  
4178      /**
4179       * Data provider for private ips.
4180       */
4181      public function data_private_ips() {
4182          return array(
4183              array('10.0.0.0'),
4184              array('172.16.0.0'),
4185              array('192.168.1.0'),
4186              array('fdfe:dcba:9876:ffff:fdc6:c46b:bb8f:7d4c'),
4187              array('fdc6:c46b:bb8f:7d4c:fdc6:c46b:bb8f:7d4c'),
4188              array('fdc6:c46b:bb8f:7d4c:0000:8a2e:0370:7334'),
4189              array('127.0.0.1'), // This has been buggy in past: https://bugs.php.net/bug.php?id=53150.
4190          );
4191      }
4192  
4193      /**
4194       * Checks ip_is_public returns false for private ips.
4195       *
4196       * @param string $ip the ipaddress to test
4197       * @dataProvider data_private_ips
4198       */
4199      public function test_ip_is_public_private_ips($ip) {
4200          $this->assertFalse(ip_is_public($ip));
4201      }
4202  
4203      /**
4204       * Data provider for public ips.
4205       */
4206      public function data_public_ips() {
4207          return array(
4208              array('2400:cb00:2048:1::8d65:71b3'),
4209              array('2400:6180:0:d0::1b:2001'),
4210              array('141.101.113.179'),
4211              array('123.45.67.178'),
4212          );
4213      }
4214  
4215      /**
4216       * Checks ip_is_public returns true for public ips.
4217       *
4218       * @param string $ip the ipaddress to test
4219       * @dataProvider data_public_ips
4220       */
4221      public function test_ip_is_public_public_ips($ip) {
4222          $this->assertTrue(ip_is_public($ip));
4223      }
4224  
4225      /**
4226       * Test the function can_send_from_real_email_address
4227       *
4228       * @param string $email Email address for the from user.
4229       * @param int $display The user's email display preference.
4230       * @param bool $samecourse Are the users in the same course?
4231       * @param string $config The CFG->allowedemaildomains config values
4232       * @param bool $result The expected result.
4233       * @dataProvider data_can_send_from_real_email_address
4234       */
4235      public function test_can_send_from_real_email_address($email, $display, $samecourse, $config, $result) {
4236          $this->resetAfterTest();
4237  
4238          $fromuser = $this->getDataGenerator()->create_user();
4239          $touser = $this->getDataGenerator()->create_user();
4240          $course = $this->getDataGenerator()->create_course();
4241          set_config('allowedemaildomains', $config);
4242  
4243          $fromuser->email = $email;
4244          $fromuser->maildisplay = $display;
4245          if ($samecourse) {
4246              $this->getDataGenerator()->enrol_user($fromuser->id, $course->id, 'student');
4247              $this->getDataGenerator()->enrol_user($touser->id, $course->id, 'student');
4248          } else {
4249              $this->getDataGenerator()->enrol_user($fromuser->id, $course->id, 'student');
4250          }
4251          $this->assertEquals($result, can_send_from_real_email_address($fromuser, $touser));
4252      }
4253  
4254      /**
4255       * Data provider for test_can_send_from_real_email_address.
4256       *
4257       * @return array Returns an array of test data for the above function.
4258       */
4259      public function data_can_send_from_real_email_address() {
4260          return [
4261              // Test from email is in allowed domain.
4262              // Test that from display is set to show no one.
4263              [
4264                  'email' => 'fromuser@example.com',
4265                  'display' => \core_user::MAILDISPLAY_HIDE,
4266                  'samecourse' => false,
4267                  'config' => "example.com\r\ntest.com",
4268                  'result' => false
4269              ],
4270              // Test that from display is set to course members only (course member).
4271              [
4272                  'email' => 'fromuser@example.com',
4273                  'display' => \core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY,
4274                  'samecourse' => true,
4275                  'config' => "example.com\r\ntest.com",
4276                  'result' => true
4277              ],
4278              // Test that from display is set to course members only (Non course member).
4279              [
4280                  'email' => 'fromuser@example.com',
4281                  'display' => \core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY,
4282                  'samecourse' => false,
4283                  'config' => "example.com\r\ntest.com",
4284                  'result' => false
4285              ],
4286              // Test that from display is set to show everyone.
4287              [
4288                  'email' => 'fromuser@example.com',
4289                  'display' => \core_user::MAILDISPLAY_EVERYONE,
4290                  'samecourse' => false,
4291                  'config' => "example.com\r\ntest.com",
4292                  'result' => true
4293              ],
4294              // Test a few different config value formats for parsing correctness.
4295              [
4296                  'email' => 'fromuser@example.com',
4297                  'display' => \core_user::MAILDISPLAY_EVERYONE,
4298                  'samecourse' => false,
4299                  'config' => "\n test.com\nexample.com \n",
4300                  'result' => true
4301              ],
4302              [
4303                  'email' => 'fromuser@example.com',
4304                  'display' => \core_user::MAILDISPLAY_EVERYONE,
4305                  'samecourse' => false,
4306                  'config' => "\r\n example.com \r\n test.com \r\n",
4307                  'result' => true
4308              ],
4309              [
4310                  'email' => 'fromuser@EXAMPLE.com',
4311                  'display' => \core_user::MAILDISPLAY_EVERYONE,
4312                  'samecourse' => false,
4313                  'config' => "example.com\r\ntest.com",
4314                  'result' => true,
4315              ],
4316              // Test from email is not in allowed domain.
4317              // Test that from display is set to show no one.
4318              [   'email' => 'fromuser@moodle.com',
4319                  'display' => \core_user::MAILDISPLAY_HIDE,
4320                  'samecourse' => false,
4321                  'config' => "example.com\r\ntest.com",
4322                  'result' => false
4323              ],
4324              // Test that from display is set to course members only (course member).
4325              [   'email' => 'fromuser@moodle.com',
4326                  'display' => \core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY,
4327                  'samecourse' => true,
4328                  'config' => "example.com\r\ntest.com",
4329                  'result' => false
4330              ],
4331              // Test that from display is set to course members only (Non course member.
4332              [   'email' => 'fromuser@moodle.com',
4333                  'display' => \core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY,
4334                  'samecourse' => false,
4335                  'config' => "example.com\r\ntest.com",
4336                  'result' => false
4337              ],
4338              // Test that from display is set to show everyone.
4339              [   'email' => 'fromuser@moodle.com',
4340                  'display' => \core_user::MAILDISPLAY_EVERYONE,
4341                  'samecourse' => false,
4342                  'config' => "example.com\r\ntest.com",
4343                  'result' => false
4344              ],
4345              // Test a few erroneous config value and confirm failure.
4346              [   'email' => 'fromuser@moodle.com',
4347                  'display' => \core_user::MAILDISPLAY_EVERYONE,
4348                  'samecourse' => false,
4349                  'config' => "\r\n   \r\n",
4350                  'result' => false
4351              ],
4352              [   'email' => 'fromuser@moodle.com',
4353                  'display' => \core_user::MAILDISPLAY_EVERYONE,
4354                  'samecourse' => false,
4355                  'config' => " \n   \n \n ",
4356                  'result' => false
4357              ],
4358          ];
4359      }
4360  
4361      /**
4362       * Test that generate_email_processing_address() returns valid email address.
4363       */
4364      public function test_generate_email_processing_address() {
4365          global $CFG;
4366          $this->resetAfterTest();
4367  
4368          $data = (object)[
4369              'id' => 42,
4370              'email' => 'my.email+from_moodle@example.com',
4371          ];
4372  
4373          $modargs = 'B'.base64_encode(pack('V', $data->id)).substr(md5($data->email), 0, 16);
4374  
4375          $CFG->maildomain = 'example.com';
4376          $CFG->mailprefix = 'mdl+';
4377          $this->assertTrue(validate_email(generate_email_processing_address(0, $modargs)));
4378  
4379          $CFG->maildomain = 'mail.example.com';
4380          $CFG->mailprefix = 'mdl-';
4381          $this->assertTrue(validate_email(generate_email_processing_address(23, $modargs)));
4382      }
4383  
4384      /**
4385       * Test allowemailaddresses setting.
4386       *
4387       * @param string $email Email address for the from user.
4388       * @param string $config The CFG->allowemailaddresses config values
4389       * @param false/string $result The expected result.
4390       *
4391       * @dataProvider data_email_is_not_allowed_for_allowemailaddresses
4392       */
4393      public function test_email_is_not_allowed_for_allowemailaddresses($email, $config, $result) {
4394          $this->resetAfterTest();
4395  
4396          set_config('allowemailaddresses', $config);
4397          $this->assertEquals($result, email_is_not_allowed($email));
4398      }
4399  
4400      /**
4401       * Data provider for data_email_is_not_allowed_for_allowemailaddresses.
4402       *
4403       * @return array Returns an array of test data for the above function.
4404       */
4405      public function data_email_is_not_allowed_for_allowemailaddresses() {
4406          return [
4407              // Test allowed domain empty list.
4408              [
4409                  'email' => 'fromuser@example.com',
4410                  'config' => '',
4411                  'result' => false
4412              ],
4413              // Test from email is in allowed domain.
4414              [
4415                  'email' => 'fromuser@example.com',
4416                  'config' => 'example.com test.com',
4417                  'result' => false
4418              ],
4419              // Test from email is in allowed domain but uppercase config.
4420              [
4421                  'email' => 'fromuser@example.com',
4422                  'config' => 'EXAMPLE.com test.com',
4423                  'result' => false
4424              ],
4425              // Test from email is in allowed domain but uppercase email.
4426              [
4427                  'email' => 'fromuser@EXAMPLE.com',
4428                  'config' => 'example.com test.com',
4429                  'result' => false
4430              ],
4431              // Test from email is in allowed subdomain.
4432              [
4433                  'email' => 'fromuser@something.example.com',
4434                  'config' => '.example.com test.com',
4435                  'result' => false
4436              ],
4437              // Test from email is in allowed subdomain but uppercase config.
4438              [
4439                  'email' => 'fromuser@something.example.com',
4440                  'config' => '.EXAMPLE.com test.com',
4441                  'result' => false
4442              ],
4443              // Test from email is in allowed subdomain but uppercase email.
4444              [
4445                  'email' => 'fromuser@something.EXAMPLE.com',
4446                  'config' => '.example.com test.com',
4447                  'result' => false
4448              ],
4449              // Test from email is not in allowed domain.
4450              [   'email' => 'fromuser@moodle.com',
4451                  'config' => 'example.com test.com',
4452                  'result' => get_string('emailonlyallowed', '', 'example.com test.com')
4453              ],
4454              // Test from email is not in allowed subdomain.
4455              [   'email' => 'fromuser@something.example.com',
4456                  'config' => 'example.com test.com',
4457                  'result' => get_string('emailonlyallowed', '', 'example.com test.com')
4458              ],
4459          ];
4460      }
4461  
4462      /**
4463       * Test denyemailaddresses setting.
4464       *
4465       * @param string $email Email address for the from user.
4466       * @param string $config The CFG->denyemailaddresses config values
4467       * @param false/string $result The expected result.
4468       *
4469       * @dataProvider data_email_is_not_allowed_for_denyemailaddresses
4470       */
4471      public function test_email_is_not_allowed_for_denyemailaddresses($email, $config, $result) {
4472          $this->resetAfterTest();
4473  
4474          set_config('denyemailaddresses', $config);
4475          $this->assertEquals($result, email_is_not_allowed($email));
4476      }
4477  
4478  
4479      /**
4480       * Data provider for test_email_is_not_allowed_for_denyemailaddresses.
4481       *
4482       * @return array Returns an array of test data for the above function.
4483       */
4484      public function data_email_is_not_allowed_for_denyemailaddresses() {
4485          return [
4486              // Test denied domain empty list.
4487              [
4488                  'email' => 'fromuser@example.com',
4489                  'config' => '',
4490                  'result' => false
4491              ],
4492              // Test from email is in denied domain.
4493              [
4494                  'email' => 'fromuser@example.com',
4495                  'config' => 'example.com test.com',
4496                  'result' => get_string('emailnotallowed', '', 'example.com test.com')
4497              ],
4498              // Test from email is in denied domain but uppercase config.
4499              [
4500                  'email' => 'fromuser@example.com',
4501                  'config' => 'EXAMPLE.com test.com',
4502                  'result' => get_string('emailnotallowed', '', 'EXAMPLE.com test.com')
4503              ],
4504              // Test from email is in denied domain but uppercase email.
4505              [
4506                  'email' => 'fromuser@EXAMPLE.com',
4507                  'config' => 'example.com test.com',
4508                  'result' => get_string('emailnotallowed', '', 'example.com test.com')
4509              ],
4510              // Test from email is in denied subdomain.
4511              [
4512                  'email' => 'fromuser@something.example.com',
4513                  'config' => '.example.com test.com',
4514                  'result' => get_string('emailnotallowed', '', '.example.com test.com')
4515              ],
4516              // Test from email is in denied subdomain but uppercase config.
4517              [
4518                  'email' => 'fromuser@something.example.com',
4519                  'config' => '.EXAMPLE.com test.com',
4520                  'result' => get_string('emailnotallowed', '', '.EXAMPLE.com test.com')
4521              ],
4522              // Test from email is in denied subdomain but uppercase email.
4523              [
4524                  'email' => 'fromuser@something.EXAMPLE.com',
4525                  'config' => '.example.com test.com',
4526                  'result' => get_string('emailnotallowed', '', '.example.com test.com')
4527              ],
4528              // Test from email is not in denied domain.
4529              [   'email' => 'fromuser@moodle.com',
4530                  'config' => 'example.com test.com',
4531                  'result' => false
4532              ],
4533              // Test from email is not in denied subdomain.
4534              [   'email' => 'fromuser@something.example.com',
4535                  'config' => 'example.com test.com',
4536                  'result' => false
4537              ],
4538          ];
4539      }
4540  
4541      /**
4542       * Test safe method unserialize_array().
4543       */
4544      public function test_unserialize_array() {
4545          $a = [1, 2, 3];
4546          $this->assertEquals($a, unserialize_array(serialize($a)));
4547          $a = ['a' => 1, 2 => 2, 'b' => 'cde'];
4548          $this->assertEquals($a, unserialize_array(serialize($a)));
4549          $a = ['a' => 1, 2 => 2, 'b' => 'c"d"e'];
4550          $this->assertEquals($a, unserialize_array(serialize($a)));
4551          $a = ['a' => 1, 2 => ['c' => 'd', 'e' => 'f'], 'b' => 'cde'];
4552          $this->assertEquals($a, unserialize_array(serialize($a)));
4553          $a = ['a' => 1, 2 => ['c' => 'd', 'e' => ['f' => 'g']], 'b' => 'cde'];
4554          $this->assertEquals($a, unserialize_array(serialize($a)));
4555          $a = ['a' => 1, 2 => 2, 'b' => 'c"d";e'];
4556          $this->assertEquals($a, unserialize_array(serialize($a)));
4557  
4558          // Can not unserialize if there are any objects.
4559          $a = (object)['a' => 1, 2 => 2, 'b' => 'cde'];
4560          $this->assertFalse(unserialize_array(serialize($a)));
4561          $a = ['a' => 1, 2 => 2, 'b' => (object)['a' => 'cde']];
4562          $this->assertFalse(unserialize_array(serialize($a)));
4563          $a = ['a' => 1, 2 => 2, 'b' => ['c' => (object)['a' => 'cde']]];
4564          $this->assertFalse(unserialize_array(serialize($a)));
4565          $a = ['a' => 1, 2 => 2, 'b' => ['c' => new lang_string('no')]];
4566          $this->assertFalse(unserialize_array(serialize($a)));
4567  
4568          // Array used in the grader report.
4569          $a = array('aggregatesonly' => [51, 34], 'gradesonly' => [21, 45, 78]);
4570          $this->assertEquals($a, unserialize_array(serialize($a)));
4571      }
4572  
4573      /**
4574       * Test method for safely unserializing a serialized object of type stdClass
4575       */
4576      public function test_unserialize_object(): void {
4577          $object = (object) [
4578              'foo' => 42,
4579              'bar' => 'Hamster',
4580              'innerobject' => (object) [
4581                  'baz' => 'happy',
4582              ],
4583          ];
4584  
4585          // We should get back the same object we serialized.
4586          $serializedobject = serialize($object);
4587          $this->assertEquals($object, unserialize_object($serializedobject));
4588  
4589          // Try serializing a different class, not allowed.
4590          $langstr = new lang_string('no');
4591          $serializedlangstr = serialize($langstr);
4592          $unserializedlangstr = unserialize_object($serializedlangstr);
4593          $this->assertInstanceOf(\stdClass::class, $unserializedlangstr);
4594      }
4595  
4596      /**
4597       * Test that the component_class_callback returns the correct default value when the class was not found.
4598       *
4599       * @dataProvider component_class_callback_default_provider
4600       * @param $default
4601       */
4602      public function test_component_class_callback_not_found($default) {
4603          $this->assertSame($default, component_class_callback('thisIsNotTheClassYouWereLookingFor', 'anymethod', [], $default));
4604      }
4605  
4606      /**
4607       * Test that the component_class_callback returns the correct default value when the class was not found.
4608       *
4609       * @dataProvider component_class_callback_default_provider
4610       * @param $default
4611       */
4612      public function test_component_class_callback_method_not_found($default) {
4613          require_once (__DIR__ . '/fixtures/component_class_callback_example.php');
4614  
4615          $this->assertSame($default, component_class_callback(test_component_class_callback_example::class, 'this_is_not_the_method_you_were_looking_for', ['abc'], $default));
4616      }
4617  
4618      /**
4619       * Test that the component_class_callback returns the default when the method returned null.
4620       *
4621       * @dataProvider component_class_callback_default_provider
4622       * @param $default
4623       */
4624      public function test_component_class_callback_found_returns_null($default) {
4625          require_once (__DIR__ . '/fixtures/component_class_callback_example.php');
4626  
4627          $this->assertSame($default, component_class_callback(\test_component_class_callback_example::class, 'method_returns_value', [null], $default));
4628          $this->assertSame($default, component_class_callback(\test_component_class_callback_child_example::class, 'method_returns_value', [null], $default));
4629      }
4630  
4631      /**
4632       * Test that the component_class_callback returns the expected value and not the default when there was a value.
4633       *
4634       * @dataProvider component_class_callback_data_provider
4635       * @param $default
4636       */
4637      public function test_component_class_callback_found_returns_value($value) {
4638          require_once (__DIR__ . '/fixtures/component_class_callback_example.php');
4639  
4640          $this->assertSame($value, component_class_callback(\test_component_class_callback_example::class, 'method_returns_value', [$value], 'This is not the value you were looking for'));
4641          $this->assertSame($value, component_class_callback(\test_component_class_callback_child_example::class, 'method_returns_value', [$value], 'This is not the value you were looking for'));
4642      }
4643  
4644      /**
4645       * Test that the component_class_callback handles multiple params correctly.
4646       *
4647       * @dataProvider component_class_callback_multiple_params_provider
4648       * @param $default
4649       */
4650      public function test_component_class_callback_found_accepts_multiple($params, $count) {
4651          require_once (__DIR__ . '/fixtures/component_class_callback_example.php');
4652  
4653          $this->assertSame($count, component_class_callback(\test_component_class_callback_example::class, 'method_returns_all_params', $params, 'This is not the value you were looking for'));
4654          $this->assertSame($count, component_class_callback(\test_component_class_callback_child_example::class, 'method_returns_all_params', $params, 'This is not the value you were looking for'));
4655      }
4656  
4657      /**
4658       * Data provider with list of default values for user in component_class_callback tests.
4659       *
4660       * @return array
4661       */
4662      public function component_class_callback_default_provider() {
4663          return [
4664              'null' => [null],
4665              'empty string' => [''],
4666              'string' => ['This is a string'],
4667              'int' => [12345],
4668              'stdClass' => [(object) ['this is my content']],
4669              'array' => [['a' => 'b',]],
4670          ];
4671      }
4672  
4673      /**
4674       * Data provider with list of default values for user in component_class_callback tests.
4675       *
4676       * @return array
4677       */
4678      public function component_class_callback_data_provider() {
4679          return [
4680              'empty string' => [''],
4681              'string' => ['This is a string'],
4682              'int' => [12345],
4683              'stdClass' => [(object) ['this is my content']],
4684              'array' => [['a' => 'b',]],
4685          ];
4686      }
4687  
4688      /**
4689       * Data provider with list of default values for user in component_class_callback tests.
4690       *
4691       * @return array
4692       */
4693      public function component_class_callback_multiple_params_provider() {
4694          return [
4695              'empty array' => [
4696                  [],
4697                  0,
4698              ],
4699              'string value' => [
4700                  ['one'],
4701                  1,
4702              ],
4703              'string values' => [
4704                  ['one', 'two'],
4705                  2,
4706              ],
4707              'arrays' => [
4708                  [[], []],
4709                  2,
4710              ],
4711              'nulls' => [
4712                  [null, null, null, null],
4713                  4,
4714              ],
4715              'mixed' => [
4716                  ['a', 1, null, (object) [], []],
4717                  5,
4718              ],
4719          ];
4720      }
4721  
4722      /**
4723       * Test that {@link get_callable_name()} describes the callable as expected.
4724       *
4725       * @dataProvider callable_names_provider
4726       * @param callable $callable
4727       * @param string $expectedname
4728       */
4729      public function test_get_callable_name($callable, $expectedname) {
4730          $this->assertSame($expectedname, get_callable_name($callable));
4731      }
4732  
4733      /**
4734       * Provides a set of callables and their human readable names.
4735       *
4736       * @return array of (string)case => [(mixed)callable, (string|bool)expected description]
4737       */
4738      public function callable_names_provider() {
4739          return [
4740              'integer' => [
4741                  386,
4742                  false,
4743              ],
4744              'boolean' => [
4745                  true,
4746                  false,
4747              ],
4748              'static_method_as_literal' => [
4749                  'my_foobar_class::my_foobar_method',
4750                  'my_foobar_class::my_foobar_method',
4751              ],
4752              'static_method_of_literal_class' => [
4753                  ['my_foobar_class', 'my_foobar_method'],
4754                  'my_foobar_class::my_foobar_method',
4755              ],
4756              'static_method_of_object' => [
4757                  [$this, 'my_foobar_method'],
4758                  'core\moodlelib_test::my_foobar_method',
4759              ],
4760              'method_of_object' => [
4761                  [new lang_string('parentlanguage', 'core_langconfig'), 'my_foobar_method'],
4762                  'lang_string::my_foobar_method',
4763              ],
4764              'function_as_literal' => [
4765                  'my_foobar_callback',
4766                  'my_foobar_callback',
4767              ],
4768              'function_as_closure' => [
4769                  function($a) { return $a; },
4770                  'Closure::__invoke',
4771              ],
4772          ];
4773      }
4774  
4775      /**
4776       * Data provider for \core_moodlelib_testcase::test_get_complete_user_data().
4777       *
4778       * @return array
4779       */
4780      public function user_data_provider() {
4781          return [
4782              'Fetch data using a valid username' => [
4783                  'username', 's1', true
4784              ],
4785              'Fetch data using a valid username, different case' => [
4786                  'username', 'S1', true
4787              ],
4788              'Fetch data using a valid username, different case for fieldname and value' => [
4789                  'USERNAME', 'S1', true
4790              ],
4791              'Fetch data using an invalid username' => [
4792                  'username', 's2', false
4793              ],
4794              'Fetch by email' => [
4795                  'email', 's1@example.com', true
4796              ],
4797              'Fetch data using a non-existent email' => [
4798                  'email', 's2@example.com', false
4799              ],
4800              'Fetch data using a non-existent email, throw exception' => [
4801                  'email', 's2@example.com', false, \dml_missing_record_exception::class
4802              ],
4803              'Multiple accounts with the same email' => [
4804                  'email', 's1@example.com', false, 1
4805              ],
4806              'Multiple accounts with the same email, throw exception' => [
4807                  'email', 's1@example.com', false, 1, \dml_multiple_records_exception::class
4808              ],
4809              'Fetch data using a valid user ID' => [
4810                  'id', true, true
4811              ],
4812              'Fetch data using a non-existent user ID' => [
4813                  'id', false, false
4814              ],
4815          ];
4816      }
4817  
4818      /**
4819       * Test for get_complete_user_data().
4820       *
4821       * @dataProvider user_data_provider
4822       * @param string $field The field to use for the query.
4823       * @param string|boolean $value The field value. When fetching by ID, set true to fetch valid user ID, false otherwise.
4824       * @param boolean $success Whether we expect for the fetch to succeed or return false.
4825       * @param int $allowaccountssameemail Value for $CFG->allowaccountssameemail.
4826       * @param string $expectedexception The exception to be expected.
4827       */
4828      public function test_get_complete_user_data($field, $value, $success, $allowaccountssameemail = 0, $expectedexception = '') {
4829          $this->resetAfterTest();
4830  
4831          // Set config settings we need for our environment.
4832          set_config('allowaccountssameemail', $allowaccountssameemail);
4833  
4834          // Generate the user data.
4835          $generator = $this->getDataGenerator();
4836          $userdata = [
4837              'username' => 's1',
4838              'email' => 's1@example.com',
4839          ];
4840          $user = $generator->create_user($userdata);
4841  
4842          if ($allowaccountssameemail) {
4843              // Create another user with the same email address.
4844              $generator->create_user(['email' => 's1@example.com']);
4845          }
4846  
4847          // Since the data provider can't know what user ID to use, do a special handling for ID field tests.
4848          if ($field === 'id') {
4849              if ($value) {
4850                  // Test for fetching data using a valid user ID. Use the generated user's ID.
4851                  $value = $user->id;
4852              } else {
4853                  // Test for fetching data using a non-existent user ID.
4854                  $value = $user->id + 1;
4855              }
4856          }
4857  
4858          // When an exception is expected.
4859          $throwexception = false;
4860          if ($expectedexception) {
4861              $this->expectException($expectedexception);
4862              $throwexception = true;
4863          }
4864  
4865          $fetcheduser = get_complete_user_data($field, $value, null, $throwexception);
4866          if ($success) {
4867              $this->assertEquals($user->id, $fetcheduser->id);
4868              $this->assertEquals($user->username, $fetcheduser->username);
4869              $this->assertEquals($user->email, $fetcheduser->email);
4870          } else {
4871              $this->assertFalse($fetcheduser);
4872          }
4873      }
4874  
4875      /**
4876       * Test for send_password_change_().
4877       */
4878      public function test_send_password_change_info() {
4879          $this->resetAfterTest();
4880  
4881          $user = $this->getDataGenerator()->create_user();
4882  
4883          $sink = $this->redirectEmails(); // Make sure we are redirecting emails.
4884          send_password_change_info($user);
4885          $result = $sink->get_messages();
4886          $sink->close();
4887  
4888          $this->assertStringContainsString('passwords cannot be reset on this site', quoted_printable_decode($result[0]->body));
4889      }
4890  
4891      /**
4892       * Test the get_time_interval_string for a range of inputs.
4893       *
4894       * @dataProvider get_time_interval_string_provider
4895       * @param int $time1 the time1 param.
4896       * @param int $time2 the time2 param.
4897       * @param string|null $format the format param.
4898       * @param string $expected the expected string.
4899       */
4900      public function test_get_time_interval_string(int $time1, int $time2, ?string $format, string $expected) {
4901          if (is_null($format)) {
4902              $this->assertEquals($expected, get_time_interval_string($time1, $time2));
4903          } else {
4904              $this->assertEquals($expected, get_time_interval_string($time1, $time2, $format));
4905          }
4906      }
4907  
4908      /**
4909       * Data provider for the test_get_time_interval_string() method.
4910       */
4911      public function get_time_interval_string_provider() {
4912          return [
4913              'Time is after the reference time by 1 minute, omitted format' => [
4914                  'time1' => 12345660,
4915                  'time2' => 12345600,
4916                  'format' => null,
4917                  'expected' => '0d 0h 1m'
4918              ],
4919              'Time is before the reference time by 1 minute, omitted format' => [
4920                  'time1' => 12345540,
4921                  'time2' => 12345600,
4922                  'format' => null,
4923                  'expected' => '0d 0h 1m'
4924              ],
4925              'Time is equal to the reference time, omitted format' => [
4926                  'time1' => 12345600,
4927                  'time2' => 12345600,
4928                  'format' => null,
4929                  'expected' => '0d 0h 0m'
4930              ],
4931              'Time is after the reference time by 1 minute, empty string format' => [
4932                  'time1' => 12345660,
4933                  'time2' => 12345600,
4934                  'format' => '',
4935                  'expected' => '0d 0h 1m'
4936              ],
4937              'Time is before the reference time by 1 minute, empty string format' => [
4938                  'time1' => 12345540,
4939                  'time2' => 12345600,
4940                  'format' => '',
4941                  'expected' => '0d 0h 1m'
4942              ],
4943              'Time is equal to the reference time, empty string format' => [
4944                  'time1' => 12345600,
4945                  'time2' => 12345600,
4946                  'format' => '',
4947                  'expected' => '0d 0h 0m'
4948              ],
4949              'Time is after the reference time by 1 minute, custom format' => [
4950                  'time1' => 12345660,
4951                  'time2' => 12345600,
4952                  'format' => '%R%adays %hhours %imins',
4953                  'expected' => '+0days 0hours 1mins'
4954              ],
4955              'Time is before the reference time by 1 minute, custom format' => [
4956                  'time1' => 12345540,
4957                  'time2' => 12345600,
4958                  'format' => '%R%adays %hhours %imins',
4959                  'expected' => '-0days 0hours 1mins'
4960              ],
4961              'Time is equal to the reference time, custom format' => [
4962                  'time1' => 12345600,
4963                  'time2' => 12345600,
4964                  'format' => '%R%adays %hhours %imins',
4965                  'expected' => '+0days 0hours 0mins'
4966              ],
4967          ];
4968      }
4969  
4970      /**
4971       * Tests the rename_to_unused_name function with a file.
4972       */
4973      public function test_rename_to_unused_name_file() {
4974          global $CFG;
4975  
4976          // Create a new file in dataroot.
4977          $file = $CFG->dataroot . '/argh.txt';
4978          file_put_contents($file, 'Frogs');
4979  
4980          // Rename it.
4981          $newname = rename_to_unused_name($file);
4982  
4983          // Check new name has expected format.
4984          $this->assertMatchesRegularExpression('~/_temp_[a-f0-9]+$~', $newname);
4985  
4986          // Check it's still in the same folder.
4987          $this->assertEquals($CFG->dataroot, dirname($newname));
4988  
4989          // Check file can be loaded.
4990          $this->assertEquals('Frogs', file_get_contents($newname));
4991  
4992          // OK, delete the file.
4993          unlink($newname);
4994      }
4995  
4996      /**
4997       * Tests the rename_to_unused_name function with a directory.
4998       */
4999      public function test_rename_to_unused_name_dir() {
5000          global $CFG;
5001  
5002          // Create a new directory in dataroot.
5003          $file = $CFG->dataroot . '/arghdir';
5004          mkdir($file);
5005  
5006          // Rename it.
5007          $newname = rename_to_unused_name($file);
5008  
5009          // Check new name has expected format.
5010          $this->assertMatchesRegularExpression('~/_temp_[a-f0-9]+$~', $newname);
5011  
5012          // Check it's still in the same folder.
5013          $this->assertEquals($CFG->dataroot, dirname($newname));
5014  
5015          // Check it's still a directory
5016          $this->assertTrue(is_dir($newname));
5017  
5018          // OK, delete the directory.
5019          rmdir($newname);
5020      }
5021  
5022      /**
5023       * Tests the rename_to_unused_name function with error cases.
5024       */
5025      public function test_rename_to_unused_name_failure() {
5026          global $CFG;
5027  
5028          // Rename a file that doesn't exist.
5029          $file = $CFG->dataroot . '/argh.txt';
5030          $this->assertFalse(rename_to_unused_name($file));
5031      }
5032  
5033      /**
5034       * Provider for display_size
5035       *
5036       * @return array of ($size, $expected)
5037       */
5038      public function display_size_provider() {
5039  
5040          return [
5041              [0, '0 bytes'],
5042              [1, '1 bytes'],
5043              [1023, '1023 bytes'],
5044              [1024, '1.0 KB'],
5045              [2222, '2.2 KB'],
5046              [33333, '32.6 KB'],
5047              [444444, '434.0 KB'],
5048              [5555555, '5.3 MB'],
5049              [66666666, '63.6 MB'],
5050              [777777777, '741.7 MB'],
5051              [8888888888, '8.3 GB'],
5052              [99999999999, '93.1 GB'],
5053              [111111111111, '103.5 GB'],
5054              [2222222222222, '2.0 TB'],
5055              [33333333333333, '30.3 TB'],
5056              [444444444444444, '404.2 TB'],
5057              [5555555555555555, '4.9 PB'],
5058              [66666666666666666, '59.2 PB'],
5059              [777777777777777777, '690.8 PB'],
5060          ];
5061      }
5062  
5063      /**
5064       * Test display_size
5065       * @dataProvider display_size_provider
5066       * @param int $size the size in bytes
5067       * @param string $expected the expected string.
5068       */
5069      public function test_display_size($size, $expected) {
5070          $result = display_size($size);
5071          $expected = str_replace(' ', "\xc2\xa0", $expected); // Should be non-breaking space.
5072          $this->assertEquals($expected, $result);
5073      }
5074  
5075      /**
5076       * Provider for display_size using fixed units.
5077       *
5078       * @return array of ($size, $units, $expected)
5079       */
5080      public function display_size_fixed_provider(): array {
5081          return [
5082              [0, 'KB', '0.0 KB'],
5083              [1, 'MB', '0.0 MB'],
5084              [777777777, 'GB', '0.7 GB'],
5085              [8888888888, 'PB', '0.0 PB'],
5086              [99999999999, 'TB', '0.1 TB'],
5087              [99999999999, 'B', '99999999999 bytes'],
5088          ];
5089      }
5090  
5091      /**
5092       * Test display_size using fixed units.
5093       *
5094       * @dataProvider display_size_fixed_provider
5095       * @param int $size Size in bytes
5096       * @param string $units Fixed units
5097       * @param string $expected Expected string.
5098       */
5099      public function test_display_size_fixed(int $size, string $units, string $expected): void {
5100          $result = display_size($size, 1, $units);
5101          $expected = str_replace(' ', "\xc2\xa0", $expected); // Should be non-breaking space.
5102          $this->assertEquals($expected, $result);
5103      }
5104  
5105      /**
5106       * Provider for display_size using specified decimal places.
5107       *
5108       * @return array of ($size, $decimalplaces, $units, $expected)
5109       */
5110      public function display_size_dp_provider(): array {
5111          return [
5112              [0, 1, 'KB', '0.0 KB'],
5113              [1, 6, 'MB', '0.000001 MB'],
5114              [777777777, 0, 'GB', '1 GB'],
5115              [777777777, 0, '', '742 MB'],
5116              [42, 6, '', '42 bytes'],
5117          ];
5118      }
5119  
5120      /**
5121       * Test display_size using specified decimal places.
5122       *
5123       * @dataProvider display_size_dp_provider
5124       * @param int $size Size in bytes
5125       * @param int $places Number of decimal places
5126       * @param string $units Fixed units
5127       * @param string $expected Expected string.
5128       */
5129      public function test_display_size_dp(int $size, int $places, string $units, string $expected): void {
5130          $result = display_size($size, $places, $units);
5131          $expected = str_replace(' ', "\xc2\xa0", $expected); // Should be non-breaking space.
5132          $this->assertEquals($expected, $result);
5133      }
5134  
5135      /**
5136       * Test that the get_list_of_plugins function includes/excludes directories as appropriate.
5137       *
5138       * @dataProvider get_list_of_plugins_provider
5139       * @param   array $expectedlist The expected list of folders
5140       * @param   array $content The list of file content to set up in the virtual file root
5141       * @param   string $dir The base dir to look at in the virtual file root
5142       * @param   string $exclude Any additional folder to exclude
5143       */
5144      public function test_get_list_of_plugins(array $expectedlist, array $content, string $dir, string $exclude): void {
5145          $vfileroot = \org\bovigo\vfs\vfsStream::setup('root', null, $content);
5146          $base = \org\bovigo\vfs\vfsStream::url('root');
5147  
5148          $this->assertEquals($expectedlist, get_list_of_plugins($dir, $exclude, $base));
5149      }
5150  
5151      /**
5152       * Data provider for get_list_of_plugins checks.
5153       *
5154       * @return  array
5155       */
5156      public function get_list_of_plugins_provider(): array {
5157          return [
5158              'Standard excludes' => [
5159                  ['amdd', 'class', 'local', 'test'],
5160                  [
5161                      '.' => [],
5162                      '..' => [],
5163                      'amd' => [],
5164                      'amdd' => [],
5165                      'class' => [],
5166                      'classes' => [],
5167                      'local' => [],
5168                      'test' => [],
5169                      'tests' => [],
5170                      'yui' => [],
5171                  ],
5172                  '',
5173                  '',
5174              ],
5175              'Standard excludes with addition' => [
5176                  ['amdd', 'local', 'test'],
5177                  [
5178                      '.' => [],
5179                      '..' => [],
5180                      'amd' => [],
5181                      'amdd' => [],
5182                      'class' => [],
5183                      'classes' => [],
5184                      'local' => [],
5185                      'test' => [],
5186                      'tests' => [],
5187                      'yui' => [],
5188                  ],
5189                  '',
5190                  'class',
5191              ],
5192              'Files excluded' => [
5193                  ['def'],
5194                  [
5195                      '.' => [],
5196                      '..' => [],
5197                      'abc' => 'File with filename abc',
5198                      'def' => [
5199                          '.' => [],
5200                          '..' => [],
5201                          'example.txt' => 'In a directory called "def"',
5202                      ],
5203                  ],
5204                  '',
5205                  '',
5206              ],
5207              'Subdirectories only' => [
5208                  ['abc'],
5209                  [
5210                      '.' => [],
5211                      '..' => [],
5212                      'foo' => [
5213                          '.' => [],
5214                          '..' => [],
5215                          'abc' => [],
5216                      ],
5217                      'bar' => [
5218                          '.' => [],
5219                          '..' => [],
5220                          'def' => [],
5221                      ],
5222                  ],
5223                  'foo',
5224                  '',
5225              ],
5226          ];
5227      }
5228  
5229      /**
5230       * Test get_home_page() method.
5231       *
5232       * @dataProvider get_home_page_provider
5233       * @param string $user Whether the user is logged, guest or not logged.
5234       * @param int $expected Expected value after calling the get_home_page method.
5235       * @param int $defaulthomepage The $CFG->defaulthomepage setting value.
5236       * @param int $enabledashboard Whether the dashboard should be enabled or not.
5237       * @param int $userpreference User preference for the home page setting.
5238       * @covers ::get_home_page
5239       */
5240      public function test_get_home_page(string $user, int $expected, ?int $defaulthomepage = null, ?int $enabledashboard = null,
5241              ?int $userpreference = null) {
5242          global $CFG, $USER;
5243  
5244          $this->resetAfterTest();
5245  
5246          if ($user == 'guest') {
5247              $this->setGuestUser();
5248          } else if ($user == 'logged') {
5249              $this->setUser($this->getDataGenerator()->create_user());
5250          }
5251  
5252          if (isset($defaulthomepage)) {
5253              $CFG->defaulthomepage = $defaulthomepage;
5254          }
5255          if (isset($enabledashboard)) {
5256              $CFG->enabledashboard = $enabledashboard;
5257          }
5258  
5259          if ($USER) {
5260              set_user_preferences(['user_home_page_preference' => $userpreference], $USER->id);
5261          }
5262  
5263          $homepage = get_home_page();
5264          $this->assertEquals($expected, $homepage);
5265      }
5266  
5267      /**
5268       * Data provider for get_home_page checks.
5269       *
5270       * @return array
5271       */
5272      public function get_home_page_provider(): array {
5273          return [
5274              'No logged user' => [
5275                  'user' => 'nologged',
5276                  'expected' => HOMEPAGE_SITE,
5277              ],
5278              'Guest user' => [
5279                  'user' => 'guest',
5280                  'expected' => HOMEPAGE_SITE,
5281              ],
5282              'Logged user. Dashboard set as default home page and enabled' => [
5283                  'user' => 'logged',
5284                  'expected' => HOMEPAGE_MY,
5285                  'defaulthomepage' => HOMEPAGE_MY,
5286                  'enabledashboard' => 1,
5287              ],
5288              'Logged user. Dashboard set as default home page but disabled' => [
5289                  'user' => 'logged',
5290                  'expected' => HOMEPAGE_MYCOURSES,
5291                  'defaulthomepage' => HOMEPAGE_MY,
5292                  'enabledashboard' => 0,
5293              ],
5294              'Logged user. My courses set as default home page with dashboard enabled' => [
5295                  'user' => 'logged',
5296                  'expected' => HOMEPAGE_MYCOURSES,
5297                  'defaulthomepage' => HOMEPAGE_MYCOURSES,
5298                  'enabledashboard' => 1,
5299              ],
5300              'Logged user. My courses set as default home page with dashboard disabled' => [
5301                  'user' => 'logged',
5302                  'expected' => HOMEPAGE_MYCOURSES,
5303                  'defaulthomepage' => HOMEPAGE_MYCOURSES,
5304                  'enabledashboard' => 0,
5305              ],
5306              'Logged user. Site set as default home page with dashboard enabled' => [
5307                  'user' => 'logged',
5308                  'expected' => HOMEPAGE_SITE,
5309                  'defaulthomepage' => HOMEPAGE_SITE,
5310                  'enabledashboard' => 1,
5311              ],
5312              'Logged user. Site set as default home page with dashboard disabled' => [
5313                  'user' => 'logged',
5314                  'expected' => HOMEPAGE_SITE,
5315                  'defaulthomepage' => HOMEPAGE_SITE,
5316                  'enabledashboard' => 0,
5317              ],
5318              'Logged user. User preference set as default page with dashboard enabled and user preference set to dashboard' => [
5319                  'user' => 'logged',
5320                  'expected' => HOMEPAGE_MY,
5321                  'defaulthomepage' => HOMEPAGE_USER,
5322                  'enabledashboard' => 1,
5323                  'userpreference' => HOMEPAGE_MY,
5324              ],
5325              'Logged user. User preference set as default page with dashboard disabled and user preference set to dashboard' => [
5326                  'user' => 'logged',
5327                  'expected' => HOMEPAGE_MYCOURSES,
5328                  'defaulthomepage' => HOMEPAGE_USER,
5329                  'enabledashboard' => 0,
5330                  'userpreference' => HOMEPAGE_MY,
5331              ],
5332              'Logged user. User preference set as default page with dashboard enabled and user preference set to my courses' => [
5333                  'user' => 'logged',
5334                  'expected' => HOMEPAGE_MYCOURSES,
5335                  'defaulthomepage' => HOMEPAGE_USER,
5336                  'enabledashboard' => 1,
5337                  'userpreference' => HOMEPAGE_MYCOURSES,
5338              ],
5339              'Logged user. User preference set as default page with dashboard disabled and user preference set to my courses' => [
5340                  'user' => 'logged',
5341                  'expected' => HOMEPAGE_MYCOURSES,
5342                  'defaulthomepage' => HOMEPAGE_USER,
5343                  'enabledashboard' => 0,
5344                  'userpreference' => HOMEPAGE_MYCOURSES,
5345              ],
5346          ];
5347      }
5348  
5349      /**
5350       * Test get_default_home_page() method.
5351       *
5352       * @covers ::get_default_home_page
5353       */
5354      public function test_get_default_home_page() {
5355          global $CFG;
5356  
5357          $this->resetAfterTest();
5358  
5359          $CFG->enabledashboard = 1;
5360          $default = get_default_home_page();
5361          $this->assertEquals(HOMEPAGE_MY, $default);
5362  
5363          $CFG->enabledashboard = 0;
5364          $default = get_default_home_page();
5365          $this->assertEquals(HOMEPAGE_MYCOURSES, $default);
5366      }
5367  
5368      /**
5369       * Provider for is_proxybypass
5370       *
5371       * @return array of test cases.
5372       */
5373      public function is_proxybypass_provider(): array {
5374  
5375          return [
5376              'Proxybypass contains the same IP as the beginning of the URL' => [
5377                  'http://192.168.5.5-fake-app-7f000101.nip.io',
5378                  '192.168.5.5, 127.0.0.1',
5379                  false
5380              ],
5381              'Proxybypass contains the last part of the URL' => [
5382                  'http://192.168.5.5-fake-app-7f000101.nip.io',
5383                  'app-7f000101.nip.io',
5384                  false
5385              ],
5386              'Proxybypass contains the last part of the URL 2' => [
5387                  'http://store.mydomain.com',
5388                  'mydomain.com',
5389                  false
5390              ],
5391              'Proxybypass contains part of the url' => [
5392                  'http://myweb.com',
5393                  'store.myweb.com',
5394                  false
5395              ],
5396              'Different IPs used in proxybypass' => [
5397                  'http://192.168.5.5',
5398                  '192.168.5.3',
5399                  false
5400              ],
5401              'Proxybypass and URL matchs' => [
5402                  'http://store.mydomain.com',
5403                  'store.mydomain.com',
5404                  true
5405              ],
5406              'IP used in proxybypass' => [
5407                  'http://192.168.5.5',
5408                  '192.168.5.5',
5409                  true
5410              ],
5411          ];
5412      }
5413  
5414      /**
5415       * Check if $url matches anything in proxybypass list
5416       *
5417       * Test function {@see is_proxybypass()}.
5418       * @dataProvider is_proxybypass_provider
5419       * @param string $url url to check
5420       * @param string $proxybypass
5421       * @param bool $expected Expected value.
5422       */
5423      public function test_is_proxybypass(string $url, string $proxybypass, bool $expected): void {
5424          $this->resetAfterTest();
5425  
5426          global $CFG;
5427          $CFG->proxyhost = '192.168.5.5'; // Test with a fake proxy.
5428          $CFG->proxybypass = $proxybypass;
5429  
5430          $this->assertEquals($expected, is_proxybypass($url));
5431      }
5432  }