Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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

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