Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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

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