Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 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 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]

   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->assertIsString($yes);
2103          $this->assertSame($yesexpected, $yes);
2104  
2105          $yes = get_string('yes', 'moodle');
2106          $this->assertIsString($yes);
2107          $this->assertSame($yesexpected, $yes);
2108  
2109          $yes = get_string('yes', 'core');
2110          $this->assertIsString($yes);
2111          $this->assertSame($yesexpected, $yes);
2112  
2113          $yes = get_string('yes', '');
2114          $this->assertIsString($yes);
2115          $this->assertSame($yesexpected, $yes);
2116  
2117          $yes = get_string('yes', null);
2118          $this->assertIsString($yes);
2119          $this->assertSame($yesexpected, $yes);
2120  
2121          $yes = get_string('yes', null, 1);
2122          $this->assertIsString($yes);
2123          $this->assertSame($yesexpected, $yes);
2124  
2125          $days = 1;
2126          $numdays = get_string('numdays', 'core', '1');
2127          $numdaysexpected = $days.' days';
2128          $this->assertIsString($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->assertIsString($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      public function test_get_string_limitation() {
2234          // This is one of the limitations to the lang_string class. It can't be
2235          // used as a key.
2236          $this->expectException(\PHPUnit\Framework\Error\Warning::class);
2237          $array = array(get_string('yes', null, null, true) => 'yes');
2238      }
2239  
2240      /**
2241       * Test localised float formatting.
2242       */
2243      public function test_format_float() {
2244  
2245          // Special case for null.
2246          $this->assertEquals('', format_float(null));
2247  
2248          // Default 1 decimal place.
2249          $this->assertEquals('5.4', format_float(5.43));
2250          $this->assertEquals('5.0', format_float(5.001));
2251  
2252          // Custom number of decimal places.
2253          $this->assertEquals('5.43000', format_float(5.43, 5));
2254  
2255          // Auto detect the number of decimal places.
2256          $this->assertEquals('5.43', format_float(5.43, -1));
2257          $this->assertEquals('5.43', format_float(5.43000, -1));
2258          $this->assertEquals('5', format_float(5, -1));
2259          $this->assertEquals('5', format_float(5.0, -1));
2260          $this->assertEquals('0.543', format_float('5.43e-1', -1));
2261          $this->assertEquals('0.543', format_float('5.43000e-1', -1));
2262  
2263          // Option to strip ending zeros after rounding.
2264          $this->assertEquals('5.43', format_float(5.43, 5, true, true));
2265          $this->assertEquals('5', format_float(5.0001, 3, true, true));
2266  
2267          // Tests with a localised decimal separator.
2268          $this->define_local_decimal_separator();
2269  
2270          // Localisation on (default).
2271          $this->assertEquals('5X43000', format_float(5.43, 5));
2272          $this->assertEquals('5X43', format_float(5.43, 5, true, true));
2273  
2274          // Localisation off.
2275          $this->assertEquals('5.43000', format_float(5.43, 5, false));
2276          $this->assertEquals('5.43', format_float(5.43, 5, false, true));
2277  
2278          // Tests with tilde as localised decimal separator.
2279          $this->define_local_decimal_separator('~');
2280  
2281          // Must also work for '~' as decimal separator.
2282          $this->assertEquals('5', format_float(5.0001, 3, true, true));
2283          $this->assertEquals('5~43000', format_float(5.43, 5));
2284          $this->assertEquals('5~43', format_float(5.43, 5, true, true));
2285      }
2286  
2287      /**
2288       * Test localised float unformatting.
2289       */
2290      public function test_unformat_float() {
2291  
2292          // Tests without the localised decimal separator.
2293  
2294          // Special case for null, empty or white spaces only strings.
2295          $this->assertEquals(null, unformat_float(null));
2296          $this->assertEquals(null, unformat_float(''));
2297          $this->assertEquals(null, unformat_float('    '));
2298  
2299          // Regular use.
2300          $this->assertEquals(5.4, unformat_float('5.4'));
2301          $this->assertEquals(5.4, unformat_float('5.4', true));
2302  
2303          // No decimal.
2304          $this->assertEquals(5.0, unformat_float('5'));
2305  
2306          // Custom number of decimal.
2307          $this->assertEquals(5.43267, unformat_float('5.43267'));
2308  
2309          // Empty decimal.
2310          $this->assertEquals(100.0, unformat_float('100.00'));
2311  
2312          // With the thousand separator.
2313          $this->assertEquals(1000.0, unformat_float('1 000'));
2314          $this->assertEquals(1000.32, unformat_float('1 000.32'));
2315  
2316          // Negative number.
2317          $this->assertEquals(-100.0, unformat_float('-100'));
2318  
2319          // Wrong value.
2320          $this->assertEquals(0.0, unformat_float('Wrong value'));
2321          // Wrong value in strict mode.
2322          $this->assertFalse(unformat_float('Wrong value', true));
2323  
2324          // Combining options.
2325          $this->assertEquals(-1023.862567, unformat_float('   -1 023.862567     '));
2326  
2327          // Bad decimal separator (should crop the decimal).
2328          $this->assertEquals(50.0, unformat_float('50,57'));
2329          // Bad decimal separator in strict mode (should return false).
2330          $this->assertFalse(unformat_float('50,57', true));
2331  
2332          // Tests with a localised decimal separator.
2333          $this->define_local_decimal_separator();
2334  
2335          // We repeat the tests above but with the current decimal separator.
2336  
2337          // Regular use without and with the localised separator.
2338          $this->assertEquals (5.4, unformat_float('5.4'));
2339          $this->assertEquals (5.4, unformat_float('5X4'));
2340  
2341          // Custom number of decimal.
2342          $this->assertEquals (5.43267, unformat_float('5X43267'));
2343  
2344          // Empty decimal.
2345          $this->assertEquals (100.0, unformat_float('100X00'));
2346  
2347          // With the thousand separator.
2348          $this->assertEquals (1000.32, unformat_float('1 000X32'));
2349  
2350          // Bad different separator (should crop the decimal).
2351          $this->assertEquals (50.0, unformat_float('50Y57'));
2352          // Bad different separator in strict mode (should return false).
2353          $this->assertFalse (unformat_float('50Y57', true));
2354  
2355          // Combining options.
2356          $this->assertEquals (-1023.862567, unformat_float('   -1 023X862567     '));
2357          // Combining options in strict mode.
2358          $this->assertEquals (-1023.862567, unformat_float('   -1 023X862567     ', true));
2359      }
2360  
2361      /**
2362       * Test deleting of users.
2363       */
2364      public function test_delete_user() {
2365          global $DB, $CFG;
2366  
2367          $this->resetAfterTest();
2368  
2369          $guest = $DB->get_record('user', array('id'=>$CFG->siteguest), '*', MUST_EXIST);
2370          $admin = $DB->get_record('user', array('id'=>$CFG->siteadmins), '*', MUST_EXIST);
2371          $this->assertEquals(0, $DB->count_records('user', array('deleted'=>1)));
2372  
2373          $user = $this->getDataGenerator()->create_user(array('idnumber'=>'abc'));
2374          $user2 = $this->getDataGenerator()->create_user(array('idnumber'=>'xyz'));
2375          $usersharedemail1 = $this->getDataGenerator()->create_user(array('email' => 'sharedemail@example.invalid'));
2376          $usersharedemail2 = $this->getDataGenerator()->create_user(array('email' => 'sharedemail@example.invalid'));
2377          $useremptyemail1 = $this->getDataGenerator()->create_user(array('email' => ''));
2378          $useremptyemail2 = $this->getDataGenerator()->create_user(array('email' => ''));
2379  
2380          // Delete user and capture event.
2381          $sink = $this->redirectEvents();
2382          $result = delete_user($user);
2383          $events = $sink->get_events();
2384          $sink->close();
2385          $event = array_pop($events);
2386  
2387          // Test user is deleted in DB.
2388          $this->assertTrue($result);
2389          $deluser = $DB->get_record('user', array('id'=>$user->id), '*', MUST_EXIST);
2390          $this->assertEquals(1, $deluser->deleted);
2391          $this->assertEquals(0, $deluser->picture);
2392          $this->assertSame('', $deluser->idnumber);
2393          $this->assertSame(md5($user->username), $deluser->email);
2394          $this->assertRegExp('/^'.preg_quote($user->email, '/').'\.\d*$/', $deluser->username);
2395  
2396          $this->assertEquals(1, $DB->count_records('user', array('deleted'=>1)));
2397  
2398          // Test Event.
2399          $this->assertInstanceOf('\core\event\user_deleted', $event);
2400          $this->assertSame($user->id, $event->objectid);
2401          $this->assertSame('user_deleted', $event->get_legacy_eventname());
2402          $this->assertEventLegacyData($user, $event);
2403          $expectedlogdata = array(SITEID, 'user', 'delete', "view.php?id=$user->id", $user->firstname.' '.$user->lastname);
2404          $this->assertEventLegacyLogData($expectedlogdata, $event);
2405          $eventdata = $event->get_data();
2406          $this->assertSame($eventdata['other']['username'], $user->username);
2407          $this->assertSame($eventdata['other']['email'], $user->email);
2408          $this->assertSame($eventdata['other']['idnumber'], $user->idnumber);
2409          $this->assertSame($eventdata['other']['picture'], $user->picture);
2410          $this->assertSame($eventdata['other']['mnethostid'], $user->mnethostid);
2411          $this->assertEquals($user, $event->get_record_snapshot('user', $event->objectid));
2412          $this->assertEventContextNotUsed($event);
2413  
2414          // Try invalid params.
2415          $record = new stdClass();
2416          $record->grrr = 1;
2417          try {
2418              delete_user($record);
2419              $this->fail('Expecting exception for invalid delete_user() $user parameter');
2420          } catch (moodle_exception $ex) {
2421              $this->assertInstanceOf('coding_exception', $ex);
2422          }
2423          $record->id = 1;
2424          try {
2425              delete_user($record);
2426              $this->fail('Expecting exception for invalid delete_user() $user parameter');
2427          } catch (moodle_exception $ex) {
2428              $this->assertInstanceOf('coding_exception', $ex);
2429          }
2430  
2431          $record = new stdClass();
2432          $record->id = 666;
2433          $record->username = 'xx';
2434          $this->assertFalse($DB->record_exists('user', array('id'=>666))); // Any non-existent id is ok.
2435          $result = delete_user($record);
2436          $this->assertFalse($result);
2437  
2438          $result = delete_user($guest);
2439          $this->assertFalse($result);
2440  
2441          $result = delete_user($admin);
2442          $this->assertFalse($result);
2443  
2444          // Simultaneously deleting users with identical email addresses.
2445          $result1 = delete_user($usersharedemail1);
2446          $result2 = delete_user($usersharedemail2);
2447  
2448          $usersharedemail1after = $DB->get_record('user', array('id' => $usersharedemail1->id));
2449          $usersharedemail2after = $DB->get_record('user', array('id' => $usersharedemail2->id));
2450          $this->assertTrue($result1);
2451          $this->assertTrue($result2);
2452          $this->assertStringStartsWith($usersharedemail1->email . '.', $usersharedemail1after->username);
2453          $this->assertStringStartsWith($usersharedemail2->email . '.', $usersharedemail2after->username);
2454  
2455          // Simultaneously deleting users without email addresses.
2456          $result1 = delete_user($useremptyemail1);
2457          $result2 = delete_user($useremptyemail2);
2458  
2459          $useremptyemail1after = $DB->get_record('user', array('id' => $useremptyemail1->id));
2460          $useremptyemail2after = $DB->get_record('user', array('id' => $useremptyemail2->id));
2461          $this->assertTrue($result1);
2462          $this->assertTrue($result2);
2463          $this->assertStringStartsWith($useremptyemail1->username . '.' . $useremptyemail1->id . '@unknownemail.invalid.',
2464              $useremptyemail1after->username);
2465          $this->assertStringStartsWith($useremptyemail2->username . '.' . $useremptyemail2->id . '@unknownemail.invalid.',
2466              $useremptyemail2after->username);
2467  
2468          $this->resetDebugging();
2469      }
2470  
2471      /**
2472       * Test deletion of user with long username
2473       */
2474      public function test_delete_user_long_username() {
2475          global $DB;
2476  
2477          $this->resetAfterTest();
2478  
2479          // For users without an e-mail, one will be created during deletion using {$username}.{$id}@unknownemail.invalid format.
2480          $user = $this->getDataGenerator()->create_user([
2481              'username' => str_repeat('a', 75),
2482              'email' => '',
2483          ]);
2484  
2485          delete_user($user);
2486  
2487          // The username for the deleted user shouldn't exceed 100 characters.
2488          $usernamedeleted = $DB->get_field('user', 'username', ['id' => $user->id]);
2489          $this->assertEquals(100, core_text::strlen($usernamedeleted));
2490  
2491          $timestrlength = core_text::strlen((string) time());
2492  
2493          // It should start with the user name, and end with the current time.
2494          $this->assertStringStartsWith("{$user->username}.{$user->id}@", $usernamedeleted);
2495          $this->assertRegExp('/\.\d{' . $timestrlength . '}$/', $usernamedeleted);
2496      }
2497  
2498      /**
2499       * Test deletion of user with long email address
2500       */
2501      public function test_delete_user_long_email() {
2502          global $DB;
2503  
2504          $this->resetAfterTest();
2505  
2506          // Create user with 90 character email address.
2507          $user = $this->getDataGenerator()->create_user([
2508              'email' => str_repeat('a', 78) . '@example.com',
2509          ]);
2510  
2511          delete_user($user);
2512  
2513          // The username for the deleted user shouldn't exceed 100 characters.
2514          $usernamedeleted = $DB->get_field('user', 'username', ['id' => $user->id]);
2515          $this->assertEquals(100, core_text::strlen($usernamedeleted));
2516  
2517          $timestrlength = core_text::strlen((string) time());
2518  
2519          // Max username length is 100 chars. Select up to limit - (length of current time + 1 [period character]) from users email.
2520          $expectedemail = core_text::substr($user->email, 0, 100 - ($timestrlength + 1));
2521          $this->assertRegExp('/^' . preg_quote($expectedemail) . '\.\d{' . $timestrlength . '}$/', $usernamedeleted);
2522      }
2523  
2524      /**
2525       * Test function convert_to_array()
2526       */
2527      public function test_convert_to_array() {
2528          // Check that normal classes are converted to arrays the same way as (array) would do.
2529          $obj = new stdClass();
2530          $obj->prop1 = 'hello';
2531          $obj->prop2 = array('first', 'second', 13);
2532          $obj->prop3 = 15;
2533          $this->assertEquals(convert_to_array($obj), (array)$obj);
2534  
2535          // Check that context object (with iterator) is converted to array properly.
2536          $obj = context_system::instance();
2537          $ar = array(
2538              'id'           => $obj->id,
2539              'contextlevel' => $obj->contextlevel,
2540              'instanceid'   => $obj->instanceid,
2541              'path'         => $obj->path,
2542              'depth'        => $obj->depth,
2543              'locked'       => $obj->locked,
2544          );
2545          $this->assertEquals(convert_to_array($obj), $ar);
2546      }
2547  
2548      /**
2549       * Test the function date_format_string().
2550       */
2551      public function test_date_format_string() {
2552          global $CFG;
2553  
2554          $this->resetAfterTest();
2555          $this->setTimezone(99, 'Australia/Perth');
2556  
2557          $tests = array(
2558              array(
2559                  'tz' => 99,
2560                  'str' => '%A, %d %B %Y, %I:%M %p',
2561                  'expected' => 'Saturday, 01 January 2011, 06:00 PM'
2562              ),
2563              array(
2564                  'tz' => 0,
2565                  'str' => '%A, %d %B %Y, %I:%M %p',
2566                  'expected' => 'Saturday, 01 January 2011, 10:00 AM'
2567              ),
2568              array(
2569                  // Note: this function expected the timestamp in weird format before,
2570                  // since 2.9 it uses UTC.
2571                  'tz' => 'Pacific/Auckland',
2572                  'str' => '%A, %d %B %Y, %I:%M %p',
2573                  'expected' => 'Saturday, 01 January 2011, 11:00 PM'
2574              ),
2575              // Following tests pass on Windows only because en lang pack does
2576              // not contain localewincharset, in real life lang pack maintainers
2577              // may use only characters that are present in localewincharset
2578              // in format strings!
2579              array(
2580                  'tz' => 99,
2581                  'str' => 'Žluťoučký koníček %A',
2582                  'expected' => 'Žluťoučký koníček Saturday'
2583              ),
2584              array(
2585                  'tz' => 99,
2586                  'str' => '言語設定言語 %A',
2587                  'expected' => '言語設定言語 Saturday'
2588              ),
2589              array(
2590                  'tz' => 99,
2591                  'str' => '简体中文简体 %A',
2592                  'expected' => '简体中文简体 Saturday'
2593              ),
2594          );
2595  
2596          // Note: date_format_string() uses the timezone only to differenciate
2597          // the server time from the UTC time. It does not modify the timestamp.
2598          // Hence similar results for timezones <= 13.
2599          // On different systems case of AM PM changes so compare case insensitive.
2600          foreach ($tests as $test) {
2601              $str = date_format_string(1293876000, $test['str'], $test['tz']);
2602              $this->assertSame(core_text::strtolower($test['expected']), core_text::strtolower($str));
2603          }
2604      }
2605  
2606      public function test_get_config() {
2607          global $CFG;
2608  
2609          $this->resetAfterTest();
2610  
2611          // Preparation.
2612          set_config('phpunit_test_get_config_1', 'test 1');
2613          set_config('phpunit_test_get_config_2', 'test 2', 'mod_forum');
2614          if (!is_array($CFG->config_php_settings)) {
2615              $CFG->config_php_settings = array();
2616          }
2617          $CFG->config_php_settings['phpunit_test_get_config_3'] = 'test 3';
2618  
2619          if (!is_array($CFG->forced_plugin_settings)) {
2620              $CFG->forced_plugin_settings = array();
2621          }
2622          if (!array_key_exists('mod_forum', $CFG->forced_plugin_settings)) {
2623              $CFG->forced_plugin_settings['mod_forum'] = array();
2624          }
2625          $CFG->forced_plugin_settings['mod_forum']['phpunit_test_get_config_4'] = 'test 4';
2626          $CFG->phpunit_test_get_config_5 = 'test 5';
2627  
2628          // Testing.
2629          $this->assertSame('test 1', get_config('core', 'phpunit_test_get_config_1'));
2630          $this->assertSame('test 2', get_config('mod_forum', 'phpunit_test_get_config_2'));
2631          $this->assertSame('test 3', get_config('core', 'phpunit_test_get_config_3'));
2632          $this->assertSame('test 4', get_config('mod_forum', 'phpunit_test_get_config_4'));
2633          $this->assertFalse(get_config('core', 'phpunit_test_get_config_5'));
2634          $this->assertFalse(get_config('core', 'phpunit_test_get_config_x'));
2635          $this->assertFalse(get_config('mod_forum', 'phpunit_test_get_config_x'));
2636  
2637          // Test config we know to exist.
2638          $this->assertSame($CFG->dataroot, get_config('core', 'dataroot'));
2639          $this->assertSame($CFG->phpunit_dataroot, get_config('core', 'phpunit_dataroot'));
2640          $this->assertSame($CFG->dataroot, get_config('core', 'phpunit_dataroot'));
2641          $this->assertSame(get_config('core', 'dataroot'), get_config('core', 'phpunit_dataroot'));
2642  
2643          // Test setting a config var that already exists.
2644          set_config('phpunit_test_get_config_1', 'test a');
2645          $this->assertSame('test a', $CFG->phpunit_test_get_config_1);
2646          $this->assertSame('test a', get_config('core', 'phpunit_test_get_config_1'));
2647  
2648          // Test cache invalidation.
2649          $cache = cache::make('core', 'config');
2650          $this->assertIsArray($cache->get('core'));
2651          $this->assertIsArray($cache->get('mod_forum'));
2652          set_config('phpunit_test_get_config_1', 'test b');
2653          $this->assertFalse($cache->get('core'));
2654          set_config('phpunit_test_get_config_4', 'test c', 'mod_forum');
2655          $this->assertFalse($cache->get('mod_forum'));
2656      }
2657  
2658      public function test_get_max_upload_sizes() {
2659          // Test with very low limits so we are not affected by php upload limits.
2660          // Test activity limit smallest.
2661          $sitebytes = 102400;
2662          $coursebytes = 51200;
2663          $modulebytes = 10240;
2664          $result = get_max_upload_sizes($sitebytes, $coursebytes, $modulebytes);
2665  
2666          $this->assertSame('Activity upload limit (10KB)', $result['0']);
2667          $this->assertCount(2, $result);
2668  
2669          // Test course limit smallest.
2670          $sitebytes = 102400;
2671          $coursebytes = 10240;
2672          $modulebytes = 51200;
2673          $result = get_max_upload_sizes($sitebytes, $coursebytes, $modulebytes);
2674  
2675          $this->assertSame('Course upload limit (10KB)', $result['0']);
2676          $this->assertCount(2, $result);
2677  
2678          // Test site limit smallest.
2679          $sitebytes = 10240;
2680          $coursebytes = 102400;
2681          $modulebytes = 51200;
2682          $result = get_max_upload_sizes($sitebytes, $coursebytes, $modulebytes);
2683  
2684          $this->assertSame('Site upload limit (10KB)', $result['0']);
2685          $this->assertCount(2, $result);
2686  
2687          // Test site limit not set.
2688          $sitebytes = 0;
2689          $coursebytes = 102400;
2690          $modulebytes = 51200;
2691          $result = get_max_upload_sizes($sitebytes, $coursebytes, $modulebytes);
2692  
2693          $this->assertSame('Activity upload limit (50KB)', $result['0']);
2694          $this->assertCount(3, $result);
2695  
2696          $sitebytes = 0;
2697          $coursebytes = 51200;
2698          $modulebytes = 102400;
2699          $result = get_max_upload_sizes($sitebytes, $coursebytes, $modulebytes);
2700  
2701          $this->assertSame('Course upload limit (50KB)', $result['0']);
2702          $this->assertCount(3, $result);
2703  
2704          // Test custom bytes in range.
2705          $sitebytes = 102400;
2706          $coursebytes = 51200;
2707          $modulebytes = 51200;
2708          $custombytes = 10240;
2709          $result = get_max_upload_sizes($sitebytes, $coursebytes, $modulebytes, $custombytes);
2710  
2711          $this->assertCount(3, $result);
2712  
2713          // Test custom bytes in range but non-standard.
2714          $sitebytes = 102400;
2715          $coursebytes = 51200;
2716          $modulebytes = 51200;
2717          $custombytes = 25600;
2718          $result = get_max_upload_sizes($sitebytes, $coursebytes, $modulebytes, $custombytes);
2719  
2720          $this->assertCount(4, $result);
2721  
2722          // Test custom bytes out of range.
2723          $sitebytes = 102400;
2724          $coursebytes = 51200;
2725          $modulebytes = 51200;
2726          $custombytes = 102400;
2727          $result = get_max_upload_sizes($sitebytes, $coursebytes, $modulebytes, $custombytes);
2728  
2729          $this->assertCount(3, $result);
2730  
2731          // Test custom bytes out of range and non-standard.
2732          $sitebytes = 102400;
2733          $coursebytes = 51200;
2734          $modulebytes = 51200;
2735          $custombytes = 256000;
2736          $result = get_max_upload_sizes($sitebytes, $coursebytes, $modulebytes, $custombytes);
2737  
2738          $this->assertCount(3, $result);
2739  
2740          // Test site limit only.
2741          $sitebytes = 51200;
2742          $result = get_max_upload_sizes($sitebytes);
2743  
2744          $this->assertSame('Site upload limit (50KB)', $result['0']);
2745          $this->assertSame('50KB', $result['51200']);
2746          $this->assertSame('10KB', $result['10240']);
2747          $this->assertCount(3, $result);
2748  
2749          // Test no limit.
2750          $result = get_max_upload_sizes();
2751          $this->assertArrayHasKey('0', $result);
2752          $this->assertArrayHasKey(get_max_upload_file_size(), $result);
2753      }
2754  
2755      /**
2756       * Test function password_is_legacy_hash().
2757       */
2758      public function test_password_is_legacy_hash() {
2759          // Well formed md5s should be matched.
2760          foreach (array('some', 'strings', 'to_check!') as $string) {
2761              $md5 = md5($string);
2762              $this->assertTrue(password_is_legacy_hash($md5));
2763          }
2764          // Strings that are not md5s should not be matched.
2765          foreach (array('', AUTH_PASSWORD_NOT_CACHED, 'IPW8WTcsWNgAWcUS1FBVHegzJnw5M2jOmYkmfc8z.xdBOyC4Caeum') as $notmd5) {
2766              $this->assertFalse(password_is_legacy_hash($notmd5));
2767          }
2768      }
2769  
2770      /**
2771       * Test function validate_internal_user_password().
2772       */
2773      public function test_validate_internal_user_password() {
2774          // Test bcrypt hashes.
2775          $validhashes = array(
2776              'pw' => '$2y$10$LOSDi5eaQJhutSRun.OVJ.ZSxQZabCMay7TO1KmzMkDMPvU40zGXK',
2777              'abc' => '$2y$10$VWTOhVdsBbWwtdWNDRHSpewjd3aXBQlBQf5rBY/hVhw8hciarFhXa',
2778              'C0mP1eX_&}<?@*&%` |\"' => '$2y$10$3PJf.q.9ywNJlsInPbqc8.IFeSsvXrGvQLKRFBIhVu1h1I3vpIry6',
2779              'ĩńťėŕňăţĩōŋāĹ' => '$2y$10$3A2Y8WpfRAnP3czJiSv6N.6Xp0T8hW3QZz2hUCYhzyWr1kGP1yUve'
2780          );
2781  
2782          foreach ($validhashes as $password => $hash) {
2783              $user = new stdClass();
2784              $user->auth = 'manual';
2785              $user->password = $hash;
2786              // The correct password should be validated.
2787              $this->assertTrue(validate_internal_user_password($user, $password));
2788              // An incorrect password should not be validated.
2789              $this->assertFalse(validate_internal_user_password($user, 'badpw'));
2790          }
2791      }
2792  
2793      /**
2794       * Test function hash_internal_user_password().
2795       */
2796      public function test_hash_internal_user_password() {
2797          $passwords = array('pw', 'abc123', 'C0mP1eX_&}<?@*&%` |\"', 'ĩńťėŕňăţĩōŋāĹ');
2798  
2799          // Check that some passwords that we convert to hashes can
2800          // be validated.
2801          foreach ($passwords as $password) {
2802              $hash = hash_internal_user_password($password);
2803              $fasthash = hash_internal_user_password($password, true);
2804              $user = new stdClass();
2805              $user->auth = 'manual';
2806              $user->password = $hash;
2807              $this->assertTrue(validate_internal_user_password($user, $password));
2808  
2809              // They should not be in md5 format.
2810              $this->assertFalse(password_is_legacy_hash($hash));
2811  
2812              // Check that cost factor in hash is correctly set.
2813              $this->assertRegExp('/\$10\$/', $hash);
2814              $this->assertRegExp('/\$04\$/', $fasthash);
2815          }
2816      }
2817  
2818      /**
2819       * Test function update_internal_user_password().
2820       */
2821      public function test_update_internal_user_password() {
2822          global $DB;
2823          $this->resetAfterTest();
2824          $passwords = array('password', '1234', 'changeme', '****');
2825          foreach ($passwords as $password) {
2826              $user = $this->getDataGenerator()->create_user(array('auth'=>'manual'));
2827              update_internal_user_password($user, $password);
2828              // The user object should have been updated.
2829              $this->assertTrue(validate_internal_user_password($user, $password));
2830              // The database field for the user should also have been updated to the
2831              // same value.
2832              $this->assertSame($user->password, $DB->get_field('user', 'password', array('id' => $user->id)));
2833          }
2834  
2835          $user = $this->getDataGenerator()->create_user(array('auth'=>'manual'));
2836          // Manually set the user's password to the md5 of the string 'password'.
2837          $DB->set_field('user', 'password', '5f4dcc3b5aa765d61d8327deb882cf99', array('id' => $user->id));
2838  
2839          $sink = $this->redirectEvents();
2840          // Update the password.
2841          update_internal_user_password($user, 'password');
2842          $events = $sink->get_events();
2843          $sink->close();
2844          $event = array_pop($events);
2845  
2846          // Password should have been updated to a bcrypt hash.
2847          $this->assertFalse(password_is_legacy_hash($user->password));
2848  
2849          // Verify event information.
2850          $this->assertInstanceOf('\core\event\user_password_updated', $event);
2851          $this->assertSame($user->id, $event->relateduserid);
2852          $this->assertEquals(context_user::instance($user->id), $event->get_context());
2853          $this->assertEventContextNotUsed($event);
2854  
2855          // Verify recovery of property 'auth'.
2856          unset($user->auth);
2857          update_internal_user_password($user, 'newpassword');
2858          $this->assertDebuggingCalled('User record in update_internal_user_password() must include field auth',
2859                  DEBUG_DEVELOPER);
2860          $this->assertEquals('manual', $user->auth);
2861      }
2862  
2863      /**
2864       * Testing that if the password is not cached, that it does not update
2865       * the user table and fire event.
2866       */
2867      public function test_update_internal_user_password_no_cache() {
2868          global $DB;
2869          $this->resetAfterTest();
2870  
2871          $user = $this->getDataGenerator()->create_user(array('auth' => 'cas'));
2872          $DB->update_record('user', ['id' => $user->id, 'password' => AUTH_PASSWORD_NOT_CACHED]);
2873          $user->password = AUTH_PASSWORD_NOT_CACHED;
2874  
2875          $sink = $this->redirectEvents();
2876          update_internal_user_password($user, 'wonkawonka');
2877          $this->assertEquals(0, $sink->count(), 'User updated event should not fire');
2878      }
2879  
2880      /**
2881       * Test if the user has a password hash, but now their auth method
2882       * says not to cache it.  Then it should update.
2883       */
2884      public function test_update_internal_user_password_update_no_cache() {
2885          $this->resetAfterTest();
2886  
2887          $user = $this->getDataGenerator()->create_user(array('password' => 'test'));
2888          $this->assertNotEquals(AUTH_PASSWORD_NOT_CACHED, $user->password);
2889          $user->auth = 'cas'; // Change to a auth that does not store passwords.
2890  
2891          $sink = $this->redirectEvents();
2892          update_internal_user_password($user, 'wonkawonka');
2893          $this->assertGreaterThanOrEqual(1, $sink->count(), 'User updated event should fire');
2894  
2895          $this->assertEquals(AUTH_PASSWORD_NOT_CACHED, $user->password);
2896      }
2897  
2898      public function test_fullname() {
2899          global $CFG;
2900  
2901          $this->resetAfterTest();
2902  
2903          // Create a user to test the name display on.
2904          $record = array();
2905          $record['firstname'] = 'Scott';
2906          $record['lastname'] = 'Fletcher';
2907          $record['firstnamephonetic'] = 'スコット';
2908          $record['lastnamephonetic'] = 'フレチャー';
2909          $record['alternatename'] = 'No friends';
2910          $user = $this->getDataGenerator()->create_user($record);
2911  
2912          // Back up config settings for restore later.
2913          $originalcfg = new stdClass();
2914          $originalcfg->fullnamedisplay = $CFG->fullnamedisplay;
2915          $originalcfg->alternativefullnameformat = $CFG->alternativefullnameformat;
2916  
2917          // Testing existing fullnamedisplay settings.
2918          $CFG->fullnamedisplay = 'firstname';
2919          $testname = fullname($user);
2920          $this->assertSame($user->firstname, $testname);
2921  
2922          $CFG->fullnamedisplay = 'firstname lastname';
2923          $expectedname = "$user->firstname $user->lastname";
2924          $testname = fullname($user);
2925          $this->assertSame($expectedname, $testname);
2926  
2927          $CFG->fullnamedisplay = 'lastname firstname';
2928          $expectedname = "$user->lastname $user->firstname";
2929          $testname = fullname($user);
2930          $this->assertSame($expectedname, $testname);
2931  
2932          $expectedname = get_string('fullnamedisplay', null, $user);
2933          $CFG->fullnamedisplay = 'language';
2934          $testname = fullname($user);
2935          $this->assertSame($expectedname, $testname);
2936  
2937          // Test override parameter.
2938          $CFG->fullnamedisplay = 'firstname';
2939          $expectedname = "$user->firstname $user->lastname";
2940          $testname = fullname($user, true);
2941          $this->assertSame($expectedname, $testname);
2942  
2943          // Test alternativefullnameformat setting.
2944          // Test alternativefullnameformat that has been set to nothing.
2945          $CFG->alternativefullnameformat = '';
2946          $expectedname = "$user->firstname $user->lastname";
2947          $testname = fullname($user, true);
2948          $this->assertSame($expectedname, $testname);
2949  
2950          // Test alternativefullnameformat that has been set to 'language'.
2951          $CFG->alternativefullnameformat = 'language';
2952          $expectedname = "$user->firstname $user->lastname";
2953          $testname = fullname($user, true);
2954          $this->assertSame($expectedname, $testname);
2955  
2956          // Test customising the alternativefullnameformat setting with all additional name fields.
2957          $CFG->alternativefullnameformat = 'firstname lastname firstnamephonetic lastnamephonetic middlename alternatename';
2958          $expectedname = "$user->firstname $user->lastname $user->firstnamephonetic $user->lastnamephonetic $user->middlename $user->alternatename";
2959          $testname = fullname($user, true);
2960          $this->assertSame($expectedname, $testname);
2961  
2962          // Test additional name fields.
2963          $CFG->fullnamedisplay = 'lastname lastnamephonetic firstname firstnamephonetic';
2964          $expectedname = "$user->lastname $user->lastnamephonetic $user->firstname $user->firstnamephonetic";
2965          $testname = fullname($user);
2966          $this->assertSame($expectedname, $testname);
2967  
2968          // Test for handling missing data.
2969          $user->middlename = null;
2970          // Parenthesis with no data.
2971          $CFG->fullnamedisplay = 'firstname (middlename) lastname';
2972          $expectedname = "$user->firstname $user->lastname";
2973          $testname = fullname($user);
2974          $this->assertSame($expectedname, $testname);
2975  
2976          // Extra spaces due to no data.
2977          $CFG->fullnamedisplay = 'firstname middlename lastname';
2978          $expectedname = "$user->firstname $user->lastname";
2979          $testname = fullname($user);
2980          $this->assertSame($expectedname, $testname);
2981  
2982          // Regular expression testing.
2983          // Remove some data from the user fields.
2984          $user->firstnamephonetic = '';
2985          $user->lastnamephonetic = '';
2986  
2987          // Removing empty brackets and excess whitespace.
2988          // All of these configurations should resolve to just firstname lastname.
2989          $configarray = array();
2990          $configarray[] = 'firstname lastname [firstnamephonetic lastnamephonetic]';
2991          $configarray[] = 'firstname lastname \'middlename\'';
2992          $configarray[] = 'firstname "firstnamephonetic" lastname';
2993          $configarray[] = 'firstname 「firstnamephonetic」 lastname 「lastnamephonetic」';
2994  
2995          foreach ($configarray as $config) {
2996              $CFG->fullnamedisplay = $config;
2997              $expectedname = "$user->firstname $user->lastname";
2998              $testname = fullname($user);
2999              $this->assertSame($expectedname, $testname);
3000          }
3001  
3002          // Check to make sure that other characters are left in place.
3003          $configarray = array();
3004          $configarray['0'] = new stdClass();
3005          $configarray['0']->config = 'lastname firstname, middlename';
3006          $configarray['0']->expectedname = "$user->lastname $user->firstname,";
3007          $configarray['1'] = new stdClass();
3008          $configarray['1']->config = 'lastname firstname + alternatename';
3009          $configarray['1']->expectedname = "$user->lastname $user->firstname + $user->alternatename";
3010          $configarray['2'] = new stdClass();
3011          $configarray['2']->config = 'firstname aka: alternatename';
3012          $configarray['2']->expectedname = "$user->firstname aka: $user->alternatename";
3013          $configarray['3'] = new stdClass();
3014          $configarray['3']->config = 'firstname (alternatename)';
3015          $configarray['3']->expectedname = "$user->firstname ($user->alternatename)";
3016          $configarray['4'] = new stdClass();
3017          $configarray['4']->config = 'firstname [alternatename]';
3018          $configarray['4']->expectedname = "$user->firstname [$user->alternatename]";
3019          $configarray['5'] = new stdClass();
3020          $configarray['5']->config = 'firstname "lastname"';
3021          $configarray['5']->expectedname = "$user->firstname \"$user->lastname\"";
3022  
3023          foreach ($configarray as $config) {
3024              $CFG->fullnamedisplay = $config->config;
3025              $expectedname = $config->expectedname;
3026              $testname = fullname($user);
3027              $this->assertSame($expectedname, $testname);
3028          }
3029  
3030          // Test debugging message displays when
3031          // fullnamedisplay setting is "normal".
3032          $CFG->fullnamedisplay = 'firstname lastname';
3033          unset($user);
3034          $user = new stdClass();
3035          $user->firstname = 'Stan';
3036          $user->lastname = 'Lee';
3037          $namedisplay = fullname($user);
3038          $this->assertDebuggingCalled();
3039  
3040          // Tidy up after we finish testing.
3041          $CFG->fullnamedisplay = $originalcfg->fullnamedisplay;
3042          $CFG->alternativefullnameformat = $originalcfg->alternativefullnameformat;
3043      }
3044  
3045      public function test_get_all_user_name_fields() {
3046          $this->resetAfterTest();
3047  
3048          // Additional names in an array.
3049          $testarray = array('firstnamephonetic' => 'firstnamephonetic',
3050                  'lastnamephonetic' => 'lastnamephonetic',
3051                  'middlename' => 'middlename',
3052                  'alternatename' => 'alternatename',
3053                  'firstname' => 'firstname',
3054                  'lastname' => 'lastname');
3055          $this->assertEquals($testarray, get_all_user_name_fields());
3056  
3057          // Additional names as a string.
3058          $teststring = 'firstnamephonetic,lastnamephonetic,middlename,alternatename,firstname,lastname';
3059          $this->assertEquals($teststring, get_all_user_name_fields(true));
3060  
3061          // Additional names as a string with an alias.
3062          $teststring = 't.firstnamephonetic,t.lastnamephonetic,t.middlename,t.alternatename,t.firstname,t.lastname';
3063          $this->assertEquals($teststring, get_all_user_name_fields(true, 't'));
3064  
3065          // Additional name fields with a prefix - object.
3066          $testarray = array('firstnamephonetic' => 'authorfirstnamephonetic',
3067                  'lastnamephonetic' => 'authorlastnamephonetic',
3068                  'middlename' => 'authormiddlename',
3069                  'alternatename' => 'authoralternatename',
3070                  'firstname' => 'authorfirstname',
3071                  'lastname' => 'authorlastname');
3072          $this->assertEquals($testarray, get_all_user_name_fields(false, null, 'author'));
3073  
3074          // Additional name fields with an alias and a title - string.
3075          $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';
3076          $this->assertEquals($teststring, get_all_user_name_fields(true, 'u', null, 'author'));
3077  
3078          // Test the order parameter of the function.
3079          // Returning an array.
3080          $testarray = array('firstname' => 'firstname',
3081                  'lastname' => 'lastname',
3082                  'firstnamephonetic' => 'firstnamephonetic',
3083                  'lastnamephonetic' => 'lastnamephonetic',
3084                  'middlename' => 'middlename',
3085                  'alternatename' => 'alternatename'
3086          );
3087          $this->assertEquals($testarray, get_all_user_name_fields(false, null, null, null, true));
3088  
3089          // Returning a string.
3090          $teststring = 'firstname,lastname,firstnamephonetic,lastnamephonetic,middlename,alternatename';
3091          $this->assertEquals($teststring, get_all_user_name_fields(true, null, null, null, true));
3092      }
3093  
3094      public function test_order_in_string() {
3095          $this->resetAfterTest();
3096  
3097          // Return an array in an order as they are encountered in a string.
3098          $valuearray = array('second', 'firsthalf', 'first');
3099          $formatstring = 'first firsthalf some other text (second)';
3100          $expectedarray = array('0' => 'first', '6' => 'firsthalf', '33' => 'second');
3101          $this->assertEquals($expectedarray, order_in_string($valuearray, $formatstring));
3102  
3103          // Try again with a different order for the format.
3104          $valuearray = array('second', 'firsthalf', 'first');
3105          $formatstring = 'firsthalf first second';
3106          $expectedarray = array('0' => 'firsthalf', '10' => 'first', '16' => 'second');
3107          $this->assertEquals($expectedarray, order_in_string($valuearray, $formatstring));
3108  
3109          // Try again with yet another different order for the format.
3110          $valuearray = array('second', 'firsthalf', 'first');
3111          $formatstring = 'start seconds away second firstquater first firsthalf';
3112          $expectedarray = array('19' => 'second', '38' => 'first', '44' => 'firsthalf');
3113          $this->assertEquals($expectedarray, order_in_string($valuearray, $formatstring));
3114      }
3115  
3116      public function test_complete_user_login() {
3117          global $USER, $DB;
3118  
3119          $this->resetAfterTest();
3120          $user = $this->getDataGenerator()->create_user();
3121          $this->setUser(0);
3122  
3123          $sink = $this->redirectEvents();
3124          $loginuser = clone($user);
3125          $this->setCurrentTimeStart();
3126          @complete_user_login($loginuser); // Hide session header errors.
3127          $this->assertSame($loginuser, $USER);
3128          $this->assertEquals($user->id, $USER->id);
3129          $events = $sink->get_events();
3130          $sink->close();
3131  
3132          $this->assertCount(1, $events);
3133          $event = reset($events);
3134          $this->assertInstanceOf('\core\event\user_loggedin', $event);
3135          $this->assertEquals('user', $event->objecttable);
3136          $this->assertEquals($user->id, $event->objectid);
3137          $this->assertEquals(context_system::instance()->id, $event->contextid);
3138          $this->assertEventContextNotUsed($event);
3139  
3140          $user = $DB->get_record('user', array('id'=>$user->id));
3141  
3142          $this->assertTimeCurrent($user->firstaccess);
3143          $this->assertTimeCurrent($user->lastaccess);
3144  
3145          $this->assertTimeCurrent($USER->firstaccess);
3146          $this->assertTimeCurrent($USER->lastaccess);
3147          $this->assertTimeCurrent($USER->currentlogin);
3148          $this->assertSame(sesskey(), $USER->sesskey);
3149          $this->assertTimeCurrent($USER->preference['_lastloaded']);
3150          $this->assertObjectNotHasAttribute('password', $USER);
3151          $this->assertObjectNotHasAttribute('description', $USER);
3152      }
3153  
3154      /**
3155       * Test require_logout.
3156       */
3157      public function test_require_logout() {
3158          $this->resetAfterTest();
3159          $user = $this->getDataGenerator()->create_user();
3160          $this->setUser($user);
3161  
3162          $this->assertTrue(isloggedin());
3163  
3164          // Logout user and capture event.
3165          $sink = $this->redirectEvents();
3166          require_logout();
3167          $events = $sink->get_events();
3168          $sink->close();
3169          $event = array_pop($events);
3170  
3171          // Check if user is logged out.
3172          $this->assertFalse(isloggedin());
3173  
3174          // Test Event.
3175          $this->assertInstanceOf('\core\event\user_loggedout', $event);
3176          $this->assertSame($user->id, $event->objectid);
3177          $this->assertSame('user_logout', $event->get_legacy_eventname());
3178          $this->assertEventLegacyData($user, $event);
3179          $expectedlogdata = array(SITEID, 'user', 'logout', 'view.php?id='.$event->objectid.'&course='.SITEID, $event->objectid, 0,
3180              $event->objectid);
3181          $this->assertEventLegacyLogData($expectedlogdata, $event);
3182          $this->assertEventContextNotUsed($event);
3183      }
3184  
3185      /**
3186       * A data provider for testing email messageid
3187       */
3188      public function generate_email_messageid_provider() {
3189          return array(
3190              'nopath' => array(
3191                  'wwwroot' => 'http://www.example.com',
3192                  'ids' => array(
3193                      'a-custom-id' => '<a-custom-id@www.example.com>',
3194                      'an-id-with-/-a-slash' => '<an-id-with-%2F-a-slash@www.example.com>',
3195                  ),
3196              ),
3197              'path' => array(
3198                  'wwwroot' => 'http://www.example.com/path/subdir',
3199                  'ids' => array(
3200                      'a-custom-id' => '<a-custom-id/path/subdir@www.example.com>',
3201                      'an-id-with-/-a-slash' => '<an-id-with-%2F-a-slash/path/subdir@www.example.com>',
3202                  ),
3203              ),
3204          );
3205      }
3206  
3207      /**
3208       * Test email message id generation
3209       *
3210       * @dataProvider generate_email_messageid_provider
3211       *
3212       * @param string $wwwroot The wwwroot
3213       * @param array $msgids An array of msgid local parts and the final result
3214       */
3215      public function test_generate_email_messageid($wwwroot, $msgids) {
3216          global $CFG;
3217  
3218          $this->resetAfterTest();
3219          $CFG->wwwroot = $wwwroot;
3220  
3221          foreach ($msgids as $local => $final) {
3222              $this->assertEquals($final, generate_email_messageid($local));
3223          }
3224      }
3225  
3226      /**
3227       * Test email with custom headers
3228       */
3229      public function test_send_email_with_custom_header() {
3230          global $DB, $CFG;
3231          $this->preventResetByRollback();
3232          $this->resetAfterTest();
3233  
3234          $touser = $this->getDataGenerator()->create_user();
3235          $fromuser = $this->getDataGenerator()->create_user();
3236          $fromuser->customheaders = 'X-Custom-Header: foo';
3237  
3238          set_config('allowedemaildomains', 'example.com');
3239          set_config('emailheaders', 'X-Fixed-Header: bar');
3240  
3241          $sink = $this->redirectEmails();
3242          email_to_user($touser, $fromuser, 'subject', 'message');
3243  
3244          $emails = $sink->get_messages();
3245          $this->assertCount(1, $emails);
3246          $email = reset($emails);
3247          $this->assertStringContainsString('X-Custom-Header: foo', $email->header);
3248          $this->assertStringContainsString("X-Fixed-Header: bar", $email->header);
3249          $sink->clear();
3250      }
3251  
3252      /**
3253       * A data provider for testing email diversion
3254       */
3255      public function diverted_emails_provider() {
3256          return array(
3257              'nodiverts' => array(
3258                  'divertallemailsto' => null,
3259                  'divertallemailsexcept' => null,
3260                  array(
3261                      'foo@example.com',
3262                      'test@real.com',
3263                      'fred.jones@example.com',
3264                      'dev1@dev.com',
3265                      'fred@example.com',
3266                      'fred+verp@example.com',
3267                  ),
3268                  false,
3269              ),
3270              'alldiverts' => array(
3271                  'divertallemailsto' => 'somewhere@elsewhere.com',
3272                  'divertallemailsexcept' => null,
3273                  array(
3274                      'foo@example.com',
3275                      'test@real.com',
3276                      'fred.jones@example.com',
3277                      'dev1@dev.com',
3278                      'fred@example.com',
3279                      'fred+verp@example.com',
3280                  ),
3281                  true,
3282              ),
3283              'alsodiverts' => array(
3284                  'divertallemailsto' => 'somewhere@elsewhere.com',
3285                  'divertallemailsexcept' => '@dev.com, fred(\+.*)?@example.com',
3286                  array(
3287                      'foo@example.com',
3288                      'test@real.com',
3289                      'fred.jones@example.com',
3290                  ),
3291                  true,
3292              ),
3293              'divertsexceptions' => array(
3294                  'divertallemailsto' => 'somewhere@elsewhere.com',
3295                  'divertallemailsexcept' => '@dev.com, fred(\+.*)?@example.com',
3296                  array(
3297                      'dev1@dev.com',
3298                      'fred@example.com',
3299                      'fred+verp@example.com',
3300                  ),
3301                  false,
3302              ),
3303              'divertsexceptionsnewline' => array(
3304                  'divertallemailsto' => 'somewhere@elsewhere.com',
3305                  'divertallemailsexcept' => "@dev.com\nfred(\+.*)?@example.com",
3306                  array(
3307                      'dev1@dev.com',
3308                      'fred@example.com',
3309                      'fred+verp@example.com',
3310                  ),
3311                  false,
3312              ),
3313              'alsodivertsnewline' => array(
3314                  'divertallemailsto' => 'somewhere@elsewhere.com',
3315                  'divertallemailsexcept' => "@dev.com\nfred(\+.*)?@example.com",
3316                  array(
3317                      'foo@example.com',
3318                      'test@real.com',
3319                      'fred.jones@example.com',
3320                  ),
3321                  true,
3322              ),
3323          );
3324      }
3325  
3326      /**
3327       * Test email diversion
3328       *
3329       * @dataProvider diverted_emails_provider
3330       *
3331       * @param string $divertallemailsto An optional email address
3332       * @param string $divertallemailsexcept An optional exclusion list
3333       * @param array $addresses An array of test addresses
3334       * @param boolean $expected Expected result
3335       */
3336      public function test_email_should_be_diverted($divertallemailsto, $divertallemailsexcept, $addresses, $expected) {
3337          global $CFG;
3338  
3339          $this->resetAfterTest();
3340          $CFG->divertallemailsto = $divertallemailsto;
3341          $CFG->divertallemailsexcept = $divertallemailsexcept;
3342  
3343          foreach ($addresses as $address) {
3344              $this->assertEquals($expected, email_should_be_diverted($address));
3345          }
3346      }
3347  
3348      public function test_email_to_user() {
3349          global $CFG;
3350  
3351          $this->resetAfterTest();
3352  
3353          $user1 = $this->getDataGenerator()->create_user(array('maildisplay' => 1, 'mailformat' => 0));
3354          $user2 = $this->getDataGenerator()->create_user(array('maildisplay' => 1, 'mailformat' => 1));
3355          $user3 = $this->getDataGenerator()->create_user(array('maildisplay' => 0));
3356          set_config('allowedemaildomains', "example.com\r\nmoodle.org");
3357  
3358          $subject = 'subject';
3359          $messagetext = 'message text';
3360          $subject2 = 'subject 2';
3361          $messagetext2 = '<b>message text 2</b>';
3362  
3363          // Close the default email sink.
3364          $sink = $this->redirectEmails();
3365          $sink->close();
3366  
3367          $CFG->noemailever = true;
3368          $this->assertNotEmpty($CFG->noemailever);
3369          email_to_user($user1, $user2, $subject, $messagetext);
3370          $this->assertDebuggingCalled('Not sending email due to $CFG->noemailever config setting');
3371  
3372          unset_config('noemailever');
3373  
3374          email_to_user($user1, $user2, $subject, $messagetext);
3375          $this->assertDebuggingCalled('Unit tests must not send real emails! Use $this->redirectEmails()');
3376  
3377          $sink = $this->redirectEmails();
3378          email_to_user($user1, $user2, $subject, $messagetext);
3379          email_to_user($user2, $user1, $subject2, $messagetext2);
3380          $this->assertSame(2, $sink->count());
3381          $result = $sink->get_messages();
3382          $this->assertCount(2, $result);
3383          $sink->close();
3384  
3385          $this->assertSame($subject, $result[0]->subject);
3386          $this->assertSame($messagetext, trim($result[0]->body));
3387          $this->assertSame($user1->email, $result[0]->to);
3388          $this->assertSame($user2->email, $result[0]->from);
3389          $this->assertStringContainsString('Content-Type: text/plain', $result[0]->header);
3390  
3391          $this->assertSame($subject2, $result[1]->subject);
3392          $this->assertStringContainsString($messagetext2, quoted_printable_decode($result[1]->body));
3393          $this->assertSame($user2->email, $result[1]->to);
3394          $this->assertSame($user1->email, $result[1]->from);
3395          $this->assertStringNotContainsString('Content-Type: text/plain', $result[1]->header);
3396  
3397          email_to_user($user1, $user2, $subject, $messagetext);
3398          $this->assertDebuggingCalled('Unit tests must not send real emails! Use $this->redirectEmails()');
3399  
3400          // Test that an empty noreplyaddress will default to a no-reply address.
3401          $sink = $this->redirectEmails();
3402          email_to_user($user1, $user3, $subject, $messagetext);
3403          $result = $sink->get_messages();
3404          $this->assertEquals($CFG->noreplyaddress, $result[0]->from);
3405          $sink->close();
3406          set_config('noreplyaddress', '');
3407          $sink = $this->redirectEmails();
3408          email_to_user($user1, $user3, $subject, $messagetext);
3409          $result = $sink->get_messages();
3410          $this->assertEquals('noreply@www.example.com', $result[0]->from);
3411          $sink->close();
3412  
3413          // Test $CFG->allowedemaildomains.
3414          set_config('noreplyaddress', 'noreply@www.example.com');
3415          $this->assertNotEmpty($CFG->allowedemaildomains);
3416          $sink = $this->redirectEmails();
3417          email_to_user($user1, $user2, $subject, $messagetext);
3418          unset_config('allowedemaildomains');
3419          email_to_user($user1, $user2, $subject, $messagetext);
3420          $result = $sink->get_messages();
3421          $this->assertNotEquals($CFG->noreplyaddress, $result[0]->from);
3422          $this->assertEquals($CFG->noreplyaddress, $result[1]->from);
3423          $sink->close();
3424  
3425          // Try to send an unsafe attachment, we should see an error message in the eventual mail body.
3426          $attachment = '../test.txt';
3427          $attachname = 'txt';
3428  
3429          $sink = $this->redirectEmails();
3430          email_to_user($user1, $user2, $subject, $messagetext, '', $attachment, $attachname);
3431          $this->assertSame(1, $sink->count());
3432          $result = $sink->get_messages();
3433          $this->assertCount(1, $result);
3434          $this->assertStringContainsString('error.txt', $result[0]->body);
3435          $this->assertStringContainsString('Error in attachment.  User attempted to attach a filename with a unsafe name.', $result[0]->body);
3436          $sink->close();
3437      }
3438  
3439      /**
3440       * Data provider for {@see test_email_to_user_attachment}
3441       *
3442       * @return array
3443       */
3444      public function email_to_user_attachment_provider(): array {
3445          global $CFG;
3446  
3447          // Return all paths that can be used to send attachments from.
3448          return [
3449              'cachedir' => [$CFG->cachedir],
3450              'dataroot' => [$CFG->dataroot],
3451              'dirroot' => [$CFG->dirroot],
3452              'localcachedir' => [$CFG->localcachedir],
3453              'tempdir' => [$CFG->tempdir],
3454              // Paths within $CFG->localrequestdir.
3455              'localrequestdir_request_directory' => [make_request_directory()],
3456              'localrequestdir_request_storage_directory' => [get_request_storage_directory()],
3457              // Pass null to indicate we want to test a path relative to $CFG->dataroot.
3458              'relative' => [null]
3459          ];
3460      }
3461  
3462      /**
3463       * Test sending attachments with email_to_user
3464       *
3465       * @param string|null $filedir
3466       *
3467       * @dataProvider email_to_user_attachment_provider
3468       */
3469      public function test_email_to_user_attachment(?string $filedir): void {
3470          global $CFG;
3471  
3472          // If $filedir is null, then write our test file to $CFG->dataroot.
3473          $filepath = ($filedir ?: $CFG->dataroot) . '/hello.txt';
3474          file_put_contents($filepath, 'Hello');
3475  
3476          $user = core_user::get_support_user();
3477          $message = 'Test attachment path';
3478  
3479          // Create sink to catch all sent e-mails.
3480          $sink = $this->redirectEmails();
3481  
3482          // Attachment path will be that of the test file if $filedir was passed, otherwise the relative path from $CFG->dataroot.
3483          $filename = basename($filepath);
3484          $attachmentpath = $filedir ? $filepath : $filename;
3485          email_to_user($user, $user, $message, $message, $message, $attachmentpath, $filename);
3486  
3487          $messages = $sink->get_messages();
3488          $sink->close();
3489  
3490          $this->assertCount(1, $messages);
3491  
3492          // Verify attachment in message body (attachment is in MIME format, but we can detect some Content fields).
3493          $messagebody = reset($messages)->body;
3494          $this->assertStringContainsString('Content-Type: text/plain; name=' . $filename, $messagebody);
3495          $this->assertStringContainsString('Content-Disposition: attachment; filename=' . $filename, $messagebody);
3496  
3497          // Cleanup.
3498          unlink($filepath);
3499      }
3500  
3501      /**
3502       * Test sending an attachment that doesn't exist to email_to_user
3503       */
3504      public function test_email_to_user_attachment_missing(): void {
3505          $user = core_user::get_support_user();
3506          $message = 'Test attachment path';
3507  
3508          // Create sink to catch all sent e-mails.
3509          $sink = $this->redirectEmails();
3510  
3511          $attachmentpath = '/hola/hello.txt';
3512          $filename = basename($attachmentpath);
3513          email_to_user($user, $user, $message, $message, $message, $attachmentpath, $filename);
3514  
3515          $messages = $sink->get_messages();
3516          $sink->close();
3517  
3518          $this->assertCount(1, $messages);
3519  
3520          // Verify attachment not in message body (attachment is in MIME format, but we can detect some Content fields).
3521          $messagebody = reset($messages)->body;
3522          $this->assertStringNotContainsString('Content-Type: text/plain; name="' . $filename . '"', $messagebody);
3523          $this->assertStringNotContainsString('Content-Disposition: attachment; filename=' . $filename, $messagebody);
3524      }
3525  
3526      /**
3527       * Test setnew_password_and_mail.
3528       */
3529      public function test_setnew_password_and_mail() {
3530          global $DB, $CFG;
3531  
3532          $this->resetAfterTest();
3533  
3534          $user = $this->getDataGenerator()->create_user();
3535  
3536          // Update user password.
3537          $sink = $this->redirectEvents();
3538          $sink2 = $this->redirectEmails(); // Make sure we are redirecting emails.
3539          setnew_password_and_mail($user);
3540          $events = $sink->get_events();
3541          $sink->close();
3542          $sink2->close();
3543          $event = array_pop($events);
3544  
3545          // Test updated value.
3546          $dbuser = $DB->get_record('user', array('id' => $user->id));
3547          $this->assertSame($user->firstname, $dbuser->firstname);
3548          $this->assertNotEmpty($dbuser->password);
3549  
3550          // Test event.
3551          $this->assertInstanceOf('\core\event\user_password_updated', $event);
3552          $this->assertSame($user->id, $event->relateduserid);
3553          $this->assertEquals(context_user::instance($user->id), $event->get_context());
3554          $this->assertEventContextNotUsed($event);
3555      }
3556  
3557      /**
3558       * Data provider for test_generate_confirmation_link
3559       * @return Array of confirmation urls and expected resultant confirmation links
3560       */
3561      public function generate_confirmation_link_provider() {
3562          global $CFG;
3563          return [
3564              "Simple name" => [
3565                  "username" => "simplename",
3566                  "confirmationurl" => null,
3567                  "expected" => $CFG->wwwroot . "/login/confirm.php?data=/simplename"
3568              ],
3569              "Period in between words in username" => [
3570                  "username" => "period.inbetween",
3571                  "confirmationurl" => null,
3572                  "expected" => $CFG->wwwroot . "/login/confirm.php?data=/period%2Einbetween"
3573              ],
3574              "Trailing periods in username" => [
3575                  "username" => "trailingperiods...",
3576                  "confirmationurl" => null,
3577                  "expected" => $CFG->wwwroot . "/login/confirm.php?data=/trailingperiods%2E%2E%2E"
3578              ],
3579              "At symbol in username" => [
3580                  "username" => "at@symbol",
3581                  "confirmationurl" => null,
3582                  "expected" => $CFG->wwwroot . "/login/confirm.php?data=/at%40symbol"
3583              ],
3584              "Dash symbol in username" => [
3585                  "username" => "has-dash",
3586                  "confirmationurl" => null,
3587                  "expected" => $CFG->wwwroot . "/login/confirm.php?data=/has-dash"
3588              ],
3589              "Underscore in username" => [
3590                  "username" => "under_score",
3591                  "confirmationurl" => null,
3592                  "expected" => $CFG->wwwroot . "/login/confirm.php?data=/under_score"
3593              ],
3594              "Many different characters in username" => [
3595                  "username" => "many_-.@characters@_@-..-..",
3596                  "confirmationurl" => null,
3597                  "expected" => $CFG->wwwroot . "/login/confirm.php?data=/many_-%2E%40characters%40_%40-%2E%2E-%2E%2E"
3598              ],
3599              "Custom relative confirmation url" => [
3600                  "username" => "many_-.@characters@_@-..-..",
3601                  "confirmationurl" => "/custom/local/url.php",
3602                  "expected" => $CFG->wwwroot . "/custom/local/url.php?data=/many_-%2E%40characters%40_%40-%2E%2E-%2E%2E"
3603              ],
3604              "Custom relative confirmation url with parameters" => [
3605                  "username" => "many_-.@characters@_@-..-..",
3606                  "confirmationurl" => "/custom/local/url.php?with=param",
3607                  "expected" => $CFG->wwwroot . "/custom/local/url.php?with=param&data=/many_-%2E%40characters%40_%40-%2E%2E-%2E%2E"
3608              ],
3609              "Custom local confirmation url" => [
3610                  "username" => "many_-.@characters@_@-..-..",
3611                  "confirmationurl" => $CFG->wwwroot . "/custom/local/url.php",
3612                  "expected" => $CFG->wwwroot . "/custom/local/url.php?data=/many_-%2E%40characters%40_%40-%2E%2E-%2E%2E"
3613              ],
3614              "Custom local confirmation url with parameters" => [
3615                  "username" => "many_-.@characters@_@-..-..",
3616                  "confirmationurl" => $CFG->wwwroot . "/custom/local/url.php?with=param",
3617                  "expected" => $CFG->wwwroot . "/custom/local/url.php?with=param&data=/many_-%2E%40characters%40_%40-%2E%2E-%2E%2E"
3618              ],
3619              "Custom external confirmation url" => [
3620                  "username" => "many_-.@characters@_@-..-..",
3621                  "confirmationurl" => "http://moodle.org/custom/external/url.php",
3622                  "expected" => "http://moodle.org/custom/external/url.php?data=/many_-%2E%40characters%40_%40-%2E%2E-%2E%2E"
3623              ],
3624              "Custom external confirmation url with parameters" => [
3625                  "username" => "many_-.@characters@_@-..-..",
3626                  "confirmationurl" => "http://moodle.org/ext.php?with=some&param=eters",
3627                  "expected" => "http://moodle.org/ext.php?with=some&param=eters&data=/many_-%2E%40characters%40_%40-%2E%2E-%2E%2E"
3628              ],
3629              "Custom external confirmation url with parameters" => [
3630                  "username" => "many_-.@characters@_@-..-..",
3631                  "confirmationurl" => "http://moodle.org/ext.php?with=some&data=test",
3632                  "expected" => "http://moodle.org/ext.php?with=some&data=/many_-%2E%40characters%40_%40-%2E%2E-%2E%2E"
3633              ],
3634          ];
3635      }
3636  
3637      /**
3638       * Test generate_confirmation_link
3639       * @dataProvider generate_confirmation_link_provider
3640       * @param string $username The name of the user
3641       * @param string $confirmationurl The url the user should go to to confirm
3642       * @param string $expected The expected url of the confirmation link
3643       */
3644      public function test_generate_confirmation_link($username, $confirmationurl, $expected) {
3645          $this->resetAfterTest();
3646          $sink = $this->redirectEmails();
3647  
3648          $user = $this->getDataGenerator()->create_user(
3649              [
3650                  "username" => $username,
3651                  "confirmed" => 0,
3652                  "email" => 'test@example.com',
3653              ]
3654          );
3655  
3656          send_confirmation_email($user, $confirmationurl);
3657          $sink->close();
3658          $messages = $sink->get_messages();
3659          $message = array_shift($messages);
3660          $messagebody = quoted_printable_decode($message->body);
3661  
3662          $this->assertStringContainsString($expected, $messagebody);
3663      }
3664  
3665      /**
3666       * Test generate_confirmation_link with custom admin link
3667       */
3668      public function test_generate_confirmation_link_with_custom_admin() {
3669          global $CFG;
3670  
3671          $this->resetAfterTest();
3672          $sink = $this->redirectEmails();
3673  
3674          $admin = $CFG->admin;
3675          $CFG->admin = 'custom/admin/path';
3676  
3677          $user = $this->getDataGenerator()->create_user(
3678              [
3679                  "username" => "many_-.@characters@_@-..-..",
3680                  "confirmed" => 0,
3681                  "email" => 'test@example.com',
3682              ]
3683          );
3684          $confirmationurl = "/admin/test.php?with=params";
3685          $expected = $CFG->wwwroot . "/" . $CFG->admin . "/test.php?with=params&data=/many_-%2E%40characters%40_%40-%2E%2E-%2E%2E";
3686  
3687          send_confirmation_email($user, $confirmationurl);
3688          $sink->close();
3689          $messages = $sink->get_messages();
3690          $message = array_shift($messages);
3691          $messagebody = quoted_printable_decode($message->body);
3692  
3693          $sink->close();
3694          $this->assertStringContainsString($expected, $messagebody);
3695  
3696          $CFG->admin = $admin;
3697      }
3698  
3699  
3700      /**
3701       * Test remove_course_content deletes course contents
3702       * TODO Add asserts to verify other data related to course is deleted as well.
3703       */
3704      public function test_remove_course_contents() {
3705  
3706          $this->resetAfterTest();
3707  
3708          $course = $this->getDataGenerator()->create_course();
3709          $user = $this->getDataGenerator()->create_user();
3710          $gen = $this->getDataGenerator()->get_plugin_generator('core_notes');
3711          $note = $gen->create_instance(array('courseid' => $course->id, 'userid' => $user->id));
3712  
3713          $this->assertNotEquals(false, note_load($note->id));
3714          remove_course_contents($course->id, false);
3715          $this->assertFalse(note_load($note->id));
3716      }
3717  
3718      /**
3719       * Test function username_load_fields_from_object().
3720       */
3721      public function test_username_load_fields_from_object() {
3722          $this->resetAfterTest();
3723  
3724          // This object represents the information returned from an sql query.
3725          $userinfo = new stdClass();
3726          $userinfo->userid = 1;
3727          $userinfo->username = 'loosebruce';
3728          $userinfo->firstname = 'Bruce';
3729          $userinfo->lastname = 'Campbell';
3730          $userinfo->firstnamephonetic = 'ブルース';
3731          $userinfo->lastnamephonetic = 'カンベッル';
3732          $userinfo->middlename = '';
3733          $userinfo->alternatename = '';
3734          $userinfo->email = '';
3735          $userinfo->picture = 23;
3736          $userinfo->imagealt = 'Michael Jordan draining another basket.';
3737          $userinfo->idnumber = 3982;
3738  
3739          // Just user name fields.
3740          $user = new stdClass();
3741          $user = username_load_fields_from_object($user, $userinfo);
3742          $expectedarray = new stdClass();
3743          $expectedarray->firstname = 'Bruce';
3744          $expectedarray->lastname = 'Campbell';
3745          $expectedarray->firstnamephonetic = 'ブルース';
3746          $expectedarray->lastnamephonetic = 'カンベッル';
3747          $expectedarray->middlename = '';
3748          $expectedarray->alternatename = '';
3749          $this->assertEquals($user, $expectedarray);
3750  
3751          // User information for showing a picture.
3752          $user = new stdClass();
3753          $additionalfields = explode(',', user_picture::fields());
3754          $user = username_load_fields_from_object($user, $userinfo, null, $additionalfields);
3755          $user->id = $userinfo->userid;
3756          $expectedarray = new stdClass();
3757          $expectedarray->id = 1;
3758          $expectedarray->firstname = 'Bruce';
3759          $expectedarray->lastname = 'Campbell';
3760          $expectedarray->firstnamephonetic = 'ブルース';
3761          $expectedarray->lastnamephonetic = 'カンベッル';
3762          $expectedarray->middlename = '';
3763          $expectedarray->alternatename = '';
3764          $expectedarray->email = '';
3765          $expectedarray->picture = 23;
3766          $expectedarray->imagealt = 'Michael Jordan draining another basket.';
3767          $this->assertEquals($user, $expectedarray);
3768  
3769          // Alter the userinfo object to have a prefix.
3770          $userinfo->authorfirstname = 'Bruce';
3771          $userinfo->authorlastname = 'Campbell';
3772          $userinfo->authorfirstnamephonetic = 'ブルース';
3773          $userinfo->authorlastnamephonetic = 'カンベッル';
3774          $userinfo->authormiddlename = '';
3775          $userinfo->authorpicture = 23;
3776          $userinfo->authorimagealt = 'Michael Jordan draining another basket.';
3777          $userinfo->authoremail = 'test@example.com';
3778  
3779  
3780          // Return an object with user picture information.
3781          $user = new stdClass();
3782          $additionalfields = explode(',', user_picture::fields());
3783          $user = username_load_fields_from_object($user, $userinfo, 'author', $additionalfields);
3784          $user->id = $userinfo->userid;
3785          $expectedarray = new stdClass();
3786          $expectedarray->id = 1;
3787          $expectedarray->firstname = 'Bruce';
3788          $expectedarray->lastname = 'Campbell';
3789          $expectedarray->firstnamephonetic = 'ブルース';
3790          $expectedarray->lastnamephonetic = 'カンベッル';
3791          $expectedarray->middlename = '';
3792          $expectedarray->alternatename = '';
3793          $expectedarray->email = 'test@example.com';
3794          $expectedarray->picture = 23;
3795          $expectedarray->imagealt = 'Michael Jordan draining another basket.';
3796          $this->assertEquals($user, $expectedarray);
3797      }
3798  
3799      /**
3800       * Test function {@see count_words()}.
3801       *
3802       * @dataProvider count_words_testcases
3803       * @param int $expectedcount number of words in $string.
3804       * @param string $string the test string to count the words of.
3805       */
3806      public function test_count_words(int $expectedcount, string $string): void {
3807          $this->assertEquals($expectedcount, count_words($string));
3808      }
3809  
3810      /**
3811       * Data provider for {@see test_count_words}.
3812       *
3813       * @return array of test cases.
3814       */
3815      public function count_words_testcases(): array {
3816          // The counts here should match MS Word and Libre Office.
3817          return [
3818              [0, ''],
3819              [4, 'one two three four'],
3820              [1, "a'b"],
3821              [1, '1+1=2'],
3822              [1, ' one-sided '],
3823              [2, 'one&nbsp;two'],
3824              [1, 'email@example.com'],
3825              [2, 'first\part second/part'],
3826              [4, '<p>one two<br></br>three four</p>'],
3827              [4, '<p>one two<br>three four</p>'],
3828              [4, '<p>one two<br />three four</p>'], // XHTML style.
3829              [3, ' one ... three '],
3830              [1, 'just...one'],
3831              [3, ' one & three '],
3832              [1, 'just&one'],
3833              [2, 'em—dash'],
3834              [2, 'en–dash'],
3835              [4, '1³ £2 €3.45 $6,789'],
3836              [2, 'ブルース カンベッル'], // MS word counts this as 11, but we don't handle that yet.
3837              [4, '<p>one two</p><p>three four</p>'],
3838              [4, '<p>one two</p><p><br/></p><p>three four</p>'],
3839              [4, '<p>one</p><ul><li>two</li><li>three</li></ul><p>four.</p>'],
3840              [1, '<p>em<b>phas</b>is.</p>'],
3841              [1, '<p>em<i>phas</i>is.</p>'],
3842              [1, '<p>em<strong>phas</strong>is.</p>'],
3843              [1, '<p>em<em>phas</em>is.</p>'],
3844              [2, "one\ntwo"],
3845              [2, "one\rtwo"],
3846              [2, "one\ttwo"],
3847              [2, "one\vtwo"],
3848              [2, "one\ftwo"],
3849              [1, "SO<sub>4</sub><sup>2-</sup>"],
3850              [6, '4+4=8 i.e. O(1) a,b,c,d I’m black&blue_really'],
3851          ];
3852      }
3853  
3854      /**
3855       * Test function {@see count_letters()}.
3856       *
3857       * @dataProvider count_letters_testcases
3858       * @param int $expectedcount number of characters in $string.
3859       * @param string $string the test string to count the letters of.
3860       */
3861      public function test_count_letters(int $expectedcount, string $string): void {
3862          $this->assertEquals($expectedcount, count_letters($string));
3863      }
3864  
3865      /**
3866       * Data provider for {@see count_letters_testcases}.
3867       *
3868       * @return array of test cases.
3869       */
3870      public function count_letters_testcases(): array {
3871          return [
3872              [0, ''],
3873              [1, 'x'],
3874              [1, '&amp;'],
3875              [4, '<p>frog</p>'],
3876          ];
3877      }
3878  
3879      /**
3880       * Tests the getremoteaddr() function.
3881       */
3882      public function test_getremoteaddr() {
3883          global $CFG;
3884  
3885          $this->resetAfterTest();
3886  
3887          $CFG->getremoteaddrconf = null; // Use default value, GETREMOTEADDR_SKIP_DEFAULT.
3888          $noip = getremoteaddr('1.1.1.1');
3889          $this->assertEquals('1.1.1.1', $noip);
3890  
3891          $remoteaddr = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null;
3892          $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
3893          $singleip = getremoteaddr();
3894          $this->assertEquals('127.0.0.1', $singleip);
3895  
3896          $_SERVER['REMOTE_ADDR'] = $remoteaddr; // Restore server value.
3897  
3898          $CFG->getremoteaddrconf = 0; // Don't skip any source.
3899          $noip = getremoteaddr('1.1.1.1');
3900          $this->assertEquals('1.1.1.1', $noip);
3901  
3902          // Populate all $_SERVER values to review order.
3903          $ipsources = [
3904              'HTTP_CLIENT_IP' => '2.2.2.2',
3905              'HTTP_X_FORWARDED_FOR' => '3.3.3.3',
3906              'REMOTE_ADDR' => '4.4.4.4',
3907          ];
3908          $originalvalues = [];
3909          foreach ($ipsources as $source => $ip) {
3910              $originalvalues[$source] = isset($_SERVER[$source]) ? $_SERVER[$source] : null; // Saving data to restore later.
3911              $_SERVER[$source] = $ip;
3912          }
3913  
3914          foreach ($ipsources as $source => $expectedip) {
3915              $ip = getremoteaddr();
3916              $this->assertEquals($expectedip, $ip);
3917              unset($_SERVER[$source]); // Removing the value so next time we get the following ip.
3918          }
3919  
3920          // Restore server values.
3921          foreach ($originalvalues as $source => $ip) {
3922              $_SERVER[$source] = $ip;
3923          }
3924  
3925          // All $_SERVER values have been removed, we should get the default again.
3926          $noip = getremoteaddr('1.1.1.1');
3927          $this->assertEquals('1.1.1.1', $noip);
3928  
3929          $CFG->getremoteaddrconf = GETREMOTEADDR_SKIP_HTTP_CLIENT_IP;
3930          $xforwardedfor = isset($_SERVER['HTTP_X_FORWARDED_FOR']) ? $_SERVER['HTTP_X_FORWARDED_FOR'] : null;
3931  
3932          $_SERVER['HTTP_X_FORWARDED_FOR'] = '';
3933          $noip = getremoteaddr('1.1.1.1');
3934          $this->assertEquals('1.1.1.1', $noip);
3935  
3936          $_SERVER['HTTP_X_FORWARDED_FOR'] = '';
3937          $noip = getremoteaddr();
3938          $this->assertEquals('0.0.0.0', $noip);
3939  
3940          $_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1';
3941          $singleip = getremoteaddr();
3942          $this->assertEquals('127.0.0.1', $singleip);
3943  
3944          $_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1,127.0.0.2';
3945          $twoip = getremoteaddr();
3946          $this->assertEquals('127.0.0.2', $twoip);
3947  
3948          $_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1,127.0.0.2,127.0.0.3';
3949          $threeip = getremoteaddr();
3950          $this->assertEquals('127.0.0.3', $threeip);
3951  
3952          $_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1,127.0.0.2:65535';
3953          $portip = getremoteaddr();
3954          $this->assertEquals('127.0.0.2', $portip);
3955  
3956          $_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1,0:0:0:0:0:0:0:2';
3957          $portip = getremoteaddr();
3958          $this->assertEquals('0:0:0:0:0:0:0:2', $portip);
3959  
3960          $_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1,0::2';
3961          $portip = getremoteaddr();
3962          $this->assertEquals('0:0:0:0:0:0:0:2', $portip);
3963  
3964          $_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1,[0:0:0:0:0:0:0:2]:65535';
3965          $portip = getremoteaddr();
3966          $this->assertEquals('0:0:0:0:0:0:0:2', $portip);
3967  
3968          $_SERVER['HTTP_X_FORWARDED_FOR'] = $xforwardedfor;
3969  
3970      }
3971  
3972      /*
3973       * Test emulation of random_bytes() function.
3974       */
3975      public function test_random_bytes_emulate() {
3976          $result = random_bytes_emulate(10);
3977          $this->assertSame(10, strlen($result));
3978          $this->assertnotSame($result, random_bytes_emulate(10));
3979  
3980          $result = random_bytes_emulate(21);
3981          $this->assertSame(21, strlen($result));
3982          $this->assertnotSame($result, random_bytes_emulate(21));
3983  
3984          $result = random_bytes_emulate(666);
3985          $this->assertSame(666, strlen($result));
3986  
3987          $result = random_bytes_emulate(40);
3988          $this->assertSame(40, strlen($result));
3989  
3990          $this->assertDebuggingNotCalled();
3991  
3992          $result = random_bytes_emulate(0);
3993          $this->assertSame('', $result);
3994          $this->assertDebuggingCalled();
3995  
3996          $result = random_bytes_emulate(-1);
3997          $this->assertSame('', $result);
3998          $this->assertDebuggingCalled();
3999      }
4000  
4001      /**
4002       * Test function for creation of random strings.
4003       */
4004      public function test_random_string() {
4005          $pool = 'a-zA-Z0-9';
4006  
4007          $result = random_string(10);
4008          $this->assertSame(10, strlen($result));
4009          $this->assertRegExp('/^[' . $pool . ']+$/', $result);
4010          $this->assertNotSame($result, random_string(10));
4011  
4012          $result = random_string(21);
4013          $this->assertSame(21, strlen($result));
4014          $this->assertRegExp('/^[' . $pool . ']+$/', $result);
4015          $this->assertNotSame($result, random_string(21));
4016  
4017          $result = random_string(666);
4018          $this->assertSame(666, strlen($result));
4019          $this->assertRegExp('/^[' . $pool . ']+$/', $result);
4020  
4021          $result = random_string();
4022          $this->assertSame(15, strlen($result));
4023          $this->assertRegExp('/^[' . $pool . ']+$/', $result);
4024  
4025          $this->assertDebuggingNotCalled();
4026  
4027          $result = random_string(0);
4028          $this->assertSame('', $result);
4029          $this->assertDebuggingCalled();
4030  
4031          $result = random_string(-1);
4032          $this->assertSame('', $result);
4033          $this->assertDebuggingCalled();
4034      }
4035  
4036      /**
4037       * Test function for creation of complex random strings.
4038       */
4039      public function test_complex_random_string() {
4040          $pool = preg_quote('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789`~!@#%^&*()_+-=[];,./<>?:{} ', '/');
4041  
4042          $result = complex_random_string(10);
4043          $this->assertSame(10, strlen($result));
4044          $this->assertRegExp('/^[' . $pool . ']+$/', $result);
4045          $this->assertNotSame($result, complex_random_string(10));
4046  
4047          $result = complex_random_string(21);
4048          $this->assertSame(21, strlen($result));
4049          $this->assertRegExp('/^[' . $pool . ']+$/', $result);
4050          $this->assertNotSame($result, complex_random_string(21));
4051  
4052          $result = complex_random_string(666);
4053          $this->assertSame(666, strlen($result));
4054          $this->assertRegExp('/^[' . $pool . ']+$/', $result);
4055  
4056          $result = complex_random_string();
4057          $this->assertEqualsWithDelta(28, strlen($result), 4); // Expected length is 24 - 32.
4058          $this->assertRegExp('/^[' . $pool . ']+$/', $result);
4059  
4060          $this->assertDebuggingNotCalled();
4061  
4062          $result = complex_random_string(0);
4063          $this->assertSame('', $result);
4064          $this->assertDebuggingCalled();
4065  
4066          $result = complex_random_string(-1);
4067          $this->assertSame('', $result);
4068          $this->assertDebuggingCalled();
4069      }
4070  
4071      /**
4072       * Data provider for private ips.
4073       */
4074      public function data_private_ips() {
4075          return array(
4076              array('10.0.0.0'),
4077              array('172.16.0.0'),
4078              array('192.168.1.0'),
4079              array('fdfe:dcba:9876:ffff:fdc6:c46b:bb8f:7d4c'),
4080              array('fdc6:c46b:bb8f:7d4c:fdc6:c46b:bb8f:7d4c'),
4081              array('fdc6:c46b:bb8f:7d4c:0000:8a2e:0370:7334'),
4082              array('127.0.0.1'), // This has been buggy in past: https://bugs.php.net/bug.php?id=53150.
4083          );
4084      }
4085  
4086      /**
4087       * Checks ip_is_public returns false for private ips.
4088       *
4089       * @param string $ip the ipaddress to test
4090       * @dataProvider data_private_ips
4091       */
4092      public function test_ip_is_public_private_ips($ip) {
4093          $this->assertFalse(ip_is_public($ip));
4094      }
4095  
4096      /**
4097       * Data provider for public ips.
4098       */
4099      public function data_public_ips() {
4100          return array(
4101              array('2400:cb00:2048:1::8d65:71b3'),
4102              array('2400:6180:0:d0::1b:2001'),
4103              array('141.101.113.179'),
4104              array('123.45.67.178'),
4105          );
4106      }
4107  
4108      /**
4109       * Checks ip_is_public returns true for public ips.
4110       *
4111       * @param string $ip the ipaddress to test
4112       * @dataProvider data_public_ips
4113       */
4114      public function test_ip_is_public_public_ips($ip) {
4115          $this->assertTrue(ip_is_public($ip));
4116      }
4117  
4118      /**
4119       * Test the function can_send_from_real_email_address
4120       *
4121       * @param string $email Email address for the from user.
4122       * @param int $display The user's email display preference.
4123       * @param bool $samecourse Are the users in the same course?
4124       * @param string $config The CFG->allowedemaildomains config values
4125       * @param bool $result The expected result.
4126       * @dataProvider data_can_send_from_real_email_address
4127       */
4128      public function test_can_send_from_real_email_address($email, $display, $samecourse, $config, $result) {
4129          $this->resetAfterTest();
4130  
4131          $fromuser = $this->getDataGenerator()->create_user();
4132          $touser = $this->getDataGenerator()->create_user();
4133          $course = $this->getDataGenerator()->create_course();
4134          set_config('allowedemaildomains', $config);
4135  
4136          $fromuser->email = $email;
4137          $fromuser->maildisplay = $display;
4138          if ($samecourse) {
4139              $this->getDataGenerator()->enrol_user($fromuser->id, $course->id, 'student');
4140              $this->getDataGenerator()->enrol_user($touser->id, $course->id, 'student');
4141          } else {
4142              $this->getDataGenerator()->enrol_user($fromuser->id, $course->id, 'student');
4143          }
4144          $this->assertEquals($result, can_send_from_real_email_address($fromuser, $touser));
4145      }
4146  
4147      /**
4148       * Data provider for test_can_send_from_real_email_address.
4149       *
4150       * @return array Returns an array of test data for the above function.
4151       */
4152      public function data_can_send_from_real_email_address() {
4153          return [
4154              // Test from email is in allowed domain.
4155              // Test that from display is set to show no one.
4156              [
4157                  'email' => 'fromuser@example.com',
4158                  'display' => core_user::MAILDISPLAY_HIDE,
4159                  'samecourse' => false,
4160                  'config' => "example.com\r\ntest.com",
4161                  'result' => false
4162              ],
4163              // Test that from display is set to course members only (course member).
4164              [
4165                  'email' => 'fromuser@example.com',
4166                  'display' => core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY,
4167                  'samecourse' => true,
4168                  'config' => "example.com\r\ntest.com",
4169                  'result' => true
4170              ],
4171              // Test that from display is set to course members only (Non course member).
4172              [
4173                  'email' => 'fromuser@example.com',
4174                  'display' => core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY,
4175                  'samecourse' => false,
4176                  'config' => "example.com\r\ntest.com",
4177                  'result' => false
4178              ],
4179              // Test that from display is set to show everyone.
4180              [
4181                  'email' => 'fromuser@example.com',
4182                  'display' => core_user::MAILDISPLAY_EVERYONE,
4183                  'samecourse' => false,
4184                  'config' => "example.com\r\ntest.com",
4185                  'result' => true
4186              ],
4187              // Test a few different config value formats for parsing correctness.
4188              [
4189                  'email' => 'fromuser@example.com',
4190                  'display' => core_user::MAILDISPLAY_EVERYONE,
4191                  'samecourse' => false,
4192                  'config' => "\n test.com\nexample.com \n",
4193                  'result' => true
4194              ],
4195              [
4196                  'email' => 'fromuser@example.com',
4197                  'display' => core_user::MAILDISPLAY_EVERYONE,
4198                  'samecourse' => false,
4199                  'config' => "\r\n example.com \r\n test.com \r\n",
4200                  'result' => true
4201              ],
4202  
4203              // Test from email is not in allowed domain.
4204              // Test that from display is set to show no one.
4205              [   'email' => 'fromuser@moodle.com',
4206                  'display' => core_user::MAILDISPLAY_HIDE,
4207                  'samecourse' => false,
4208                  'config' => "example.com\r\ntest.com",
4209                  'result' => false
4210              ],
4211              // Test that from display is set to course members only (course member).
4212              [   'email' => 'fromuser@moodle.com',
4213                  'display' => core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY,
4214                  'samecourse' => true,
4215                  'config' => "example.com\r\ntest.com",
4216                  'result' => false
4217              ],
4218              // Test that from display is set to course members only (Non course member.
4219              [   'email' => 'fromuser@moodle.com',
4220                  'display' => core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY,
4221                  'samecourse' => false,
4222                  'config' => "example.com\r\ntest.com",
4223                  'result' => false
4224              ],
4225              // Test that from display is set to show everyone.
4226              [   'email' => 'fromuser@moodle.com',
4227                  'display' => core_user::MAILDISPLAY_EVERYONE,
4228                  'samecourse' => false,
4229                  'config' => "example.com\r\ntest.com",
4230                  'result' => false
4231              ],
4232              // Test a few erroneous config value and confirm failure.
4233              [   'email' => 'fromuser@moodle.com',
4234                  'display' => core_user::MAILDISPLAY_EVERYONE,
4235                  'samecourse' => false,
4236                  'config' => "\r\n   \r\n",
4237                  'result' => false
4238              ],
4239              [   'email' => 'fromuser@moodle.com',
4240                  'display' => core_user::MAILDISPLAY_EVERYONE,
4241                  'samecourse' => false,
4242                  'config' => " \n   \n \n ",
4243                  'result' => false
4244              ],
4245          ];
4246      }
4247  
4248      /**
4249       * Test that generate_email_processing_address() returns valid email address.
4250       */
4251      public function test_generate_email_processing_address() {
4252          global $CFG;
4253          $this->resetAfterTest();
4254  
4255          $data = (object)[
4256              'id' => 42,
4257              'email' => 'my.email+from_moodle@example.com',
4258          ];
4259  
4260          $modargs = 'B'.base64_encode(pack('V', $data->id)).substr(md5($data->email), 0, 16);
4261  
4262          $CFG->maildomain = 'example.com';
4263          $CFG->mailprefix = 'mdl+';
4264          $this->assertTrue(validate_email(generate_email_processing_address(0, $modargs)));
4265  
4266          $CFG->maildomain = 'mail.example.com';
4267          $CFG->mailprefix = 'mdl-';
4268          $this->assertTrue(validate_email(generate_email_processing_address(23, $modargs)));
4269      }
4270  
4271      /**
4272       * Test allowemailaddresses setting.
4273       *
4274       * @param string $email Email address for the from user.
4275       * @param string $config The CFG->allowemailaddresses config values
4276       * @param false/string $result The expected result.
4277       *
4278       * @dataProvider data_email_is_not_allowed_for_allowemailaddresses
4279       */
4280      public function test_email_is_not_allowed_for_allowemailaddresses($email, $config, $result) {
4281          $this->resetAfterTest();
4282  
4283          set_config('allowemailaddresses', $config);
4284          $this->assertEquals($result, email_is_not_allowed($email));
4285      }
4286  
4287      /**
4288       * Data provider for data_email_is_not_allowed_for_allowemailaddresses.
4289       *
4290       * @return array Returns an array of test data for the above function.
4291       */
4292      public function data_email_is_not_allowed_for_allowemailaddresses() {
4293          return [
4294              // Test allowed domain empty list.
4295              [
4296                  'email' => 'fromuser@example.com',
4297                  'config' => '',
4298                  'result' => false
4299              ],
4300              // Test from email is in allowed domain.
4301              [
4302                  'email' => 'fromuser@example.com',
4303                  'config' => 'example.com test.com',
4304                  'result' => false
4305              ],
4306              // Test from email is in allowed domain but uppercase config.
4307              [
4308                  'email' => 'fromuser@example.com',
4309                  'config' => 'EXAMPLE.com test.com',
4310                  'result' => false
4311              ],
4312              // Test from email is in allowed domain but uppercase email.
4313              [
4314                  'email' => 'fromuser@EXAMPLE.com',
4315                  'config' => 'example.com test.com',
4316                  'result' => false
4317              ],
4318              // Test from email is in allowed subdomain.
4319              [
4320                  'email' => 'fromuser@something.example.com',
4321                  'config' => '.example.com test.com',
4322                  'result' => false
4323              ],
4324              // Test from email is in allowed subdomain but uppercase config.
4325              [
4326                  'email' => 'fromuser@something.example.com',
4327                  'config' => '.EXAMPLE.com test.com',
4328                  'result' => false
4329              ],
4330              // Test from email is in allowed subdomain but uppercase email.
4331              [
4332                  'email' => 'fromuser@something.EXAMPLE.com',
4333                  'config' => '.example.com test.com',
4334                  'result' => false
4335              ],
4336              // Test from email is not in allowed domain.
4337              [   'email' => 'fromuser@moodle.com',
4338                  'config' => 'example.com test.com',
4339                  'result' => get_string('emailonlyallowed', '', 'example.com test.com')
4340              ],
4341              // Test from email is not in allowed subdomain.
4342              [   'email' => 'fromuser@something.example.com',
4343                  'config' => 'example.com test.com',
4344                  'result' => get_string('emailonlyallowed', '', 'example.com test.com')
4345              ],
4346          ];
4347      }
4348  
4349      /**
4350       * Test denyemailaddresses setting.
4351       *
4352       * @param string $email Email address for the from user.
4353       * @param string $config The CFG->denyemailaddresses config values
4354       * @param false/string $result The expected result.
4355       *
4356       * @dataProvider data_email_is_not_allowed_for_denyemailaddresses
4357       */
4358      public function test_email_is_not_allowed_for_denyemailaddresses($email, $config, $result) {
4359          $this->resetAfterTest();
4360  
4361          set_config('denyemailaddresses', $config);
4362          $this->assertEquals($result, email_is_not_allowed($email));
4363      }
4364  
4365  
4366      /**
4367       * Data provider for test_email_is_not_allowed_for_denyemailaddresses.
4368       *
4369       * @return array Returns an array of test data for the above function.
4370       */
4371      public function data_email_is_not_allowed_for_denyemailaddresses() {
4372          return [
4373              // Test denied domain empty list.
4374              [
4375                  'email' => 'fromuser@example.com',
4376                  'config' => '',
4377                  'result' => false
4378              ],
4379              // Test from email is in denied domain.
4380              [
4381                  'email' => 'fromuser@example.com',
4382                  'config' => 'example.com test.com',
4383                  'result' => get_string('emailnotallowed', '', 'example.com test.com')
4384              ],
4385              // Test from email is in denied domain but uppercase config.
4386              [
4387                  'email' => 'fromuser@example.com',
4388                  'config' => 'EXAMPLE.com test.com',
4389                  'result' => get_string('emailnotallowed', '', 'EXAMPLE.com test.com')
4390              ],
4391              // Test from email is in denied domain but uppercase email.
4392              [
4393                  'email' => 'fromuser@EXAMPLE.com',
4394                  'config' => 'example.com test.com',
4395                  'result' => get_string('emailnotallowed', '', 'example.com test.com')
4396              ],
4397              // Test from email is in denied subdomain.
4398              [
4399                  'email' => 'fromuser@something.example.com',
4400                  'config' => '.example.com test.com',
4401                  'result' => get_string('emailnotallowed', '', '.example.com test.com')
4402              ],
4403              // Test from email is in denied subdomain but uppercase config.
4404              [
4405                  'email' => 'fromuser@something.example.com',
4406                  'config' => '.EXAMPLE.com test.com',
4407                  'result' => get_string('emailnotallowed', '', '.EXAMPLE.com test.com')
4408              ],
4409              // Test from email is in denied subdomain but uppercase email.
4410              [
4411                  'email' => 'fromuser@something.EXAMPLE.com',
4412                  'config' => '.example.com test.com',
4413                  'result' => get_string('emailnotallowed', '', '.example.com test.com')
4414              ],
4415              // Test from email is not in denied domain.
4416              [   'email' => 'fromuser@moodle.com',
4417                  'config' => 'example.com test.com',
4418                  'result' => false
4419              ],
4420              // Test from email is not in denied subdomain.
4421              [   'email' => 'fromuser@something.example.com',
4422                  'config' => 'example.com test.com',
4423                  'result' => false
4424              ],
4425          ];
4426      }
4427  
4428      /**
4429       * Test safe method unserialize_array().
4430       */
4431      public function test_unserialize_array() {
4432          $a = [1, 2, 3];
4433          $this->assertEquals($a, unserialize_array(serialize($a)));
4434          $this->assertEquals($a, unserialize_array(serialize($a)));
4435          $a = ['a' => 1, 2 => 2, 'b' => 'cde'];
4436          $this->assertEquals($a, unserialize_array(serialize($a)));
4437          $this->assertEquals($a, unserialize_array(serialize($a)));
4438          $a = ['a' => 1, 2 => 2, 'b' => 'c"d"e'];
4439          $this->assertEquals($a, unserialize_array(serialize($a)));
4440          $a = ['a' => 1, 2 => ['c' => 'd', 'e' => 'f'], 'b' => 'cde'];
4441          $this->assertEquals($a, unserialize_array(serialize($a)));
4442  
4443          // Can not unserialize if any string contains semicolons.
4444          $a = ['a' => 1, 2 => 2, 'b' => 'c"d";e'];
4445          $this->assertEquals(false, unserialize_array(serialize($a)));
4446  
4447          // Can not unserialize if there are any objects.
4448          $a = (object)['a' => 1, 2 => 2, 'b' => 'cde'];
4449          $this->assertEquals(false, unserialize_array(serialize($a)));
4450          $a = ['a' => 1, 2 => 2, 'b' => (object)['a' => 'cde']];
4451          $this->assertEquals(false, unserialize_array(serialize($a)));
4452  
4453          // Array used in the grader report.
4454          $a = array('aggregatesonly' => [51, 34], 'gradesonly' => [21, 45, 78]);
4455          $this->assertEquals($a, unserialize_array(serialize($a)));
4456      }
4457  
4458      /**
4459       * Test that the component_class_callback returns the correct default value when the class was not found.
4460       *
4461       * @dataProvider component_class_callback_default_provider
4462       * @param $default
4463       */
4464      public function test_component_class_callback_not_found($default) {
4465          $this->assertSame($default, component_class_callback('thisIsNotTheClassYouWereLookingFor', 'anymethod', [], $default));
4466      }
4467  
4468      /**
4469       * Test method for safely unserializing a serialized object of type stdClass
4470       */
4471      public function test_unserialize_object(): void {
4472          $object = (object) [
4473              'foo' => 42,
4474              'bar' => 'Hamster',
4475              'innerobject' => (object) [
4476                  'baz' => 'happy',
4477              ],
4478          ];
4479  
4480          // We should get back the same object we serialized.
4481          $serializedobject = serialize($object);
4482          $this->assertEquals($object, unserialize_object($serializedobject));
4483  
4484          // Try serializing a different class, not allowed.
4485          $langstr = new lang_string('no');
4486          $serializedlangstr = serialize($langstr);
4487          $unserializedlangstr = unserialize_object($serializedlangstr);
4488          $this->assertInstanceOf(stdClass::class, $unserializedlangstr);
4489      }
4490  
4491      /**
4492       * Test that the component_class_callback returns the correct default value when the class was not found.
4493       *
4494       * @dataProvider component_class_callback_default_provider
4495       * @param $default
4496       */
4497      public function test_component_class_callback_method_not_found($default) {
4498          require_once (__DIR__ . '/fixtures/component_class_callback_example.php');
4499  
4500          $this->assertSame($default, component_class_callback(test_component_class_callback_example::class, 'this_is_not_the_method_you_were_looking_for', ['abc'], $default));
4501      }
4502  
4503      /**
4504       * Test that the component_class_callback returns the default when the method returned null.
4505       *
4506       * @dataProvider component_class_callback_default_provider
4507       * @param $default
4508       */
4509      public function test_component_class_callback_found_returns_null($default) {
4510          require_once (__DIR__ . '/fixtures/component_class_callback_example.php');
4511  
4512          $this->assertSame($default, component_class_callback(test_component_class_callback_example::class, 'method_returns_value', [null], $default));
4513          $this->assertSame($default, component_class_callback(test_component_class_callback_child_example::class, 'method_returns_value', [null], $default));
4514      }
4515  
4516      /**
4517       * Test that the component_class_callback returns the expected value and not the default when there was a value.
4518       *
4519       * @dataProvider component_class_callback_data_provider
4520       * @param $default
4521       */
4522      public function test_component_class_callback_found_returns_value($value) {
4523          require_once (__DIR__ . '/fixtures/component_class_callback_example.php');
4524  
4525          $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'));
4526          $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'));
4527      }
4528  
4529      /**
4530       * Test that the component_class_callback handles multiple params correctly.
4531       *
4532       * @dataProvider component_class_callback_multiple_params_provider
4533       * @param $default
4534       */
4535      public function test_component_class_callback_found_accepts_multiple($params, $count) {
4536          require_once (__DIR__ . '/fixtures/component_class_callback_example.php');
4537  
4538          $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'));
4539          $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'));
4540      }
4541  
4542      /**
4543       * Data provider with list of default values for user in component_class_callback tests.
4544       *
4545       * @return array
4546       */
4547      public function component_class_callback_default_provider() {
4548          return [
4549              'null' => [null],
4550              'empty string' => [''],
4551              'string' => ['This is a string'],
4552              'int' => [12345],
4553              'stdClass' => [(object) ['this is my content']],
4554              'array' => [['a' => 'b',]],
4555          ];
4556      }
4557  
4558      /**
4559       * Data provider with list of default values for user in component_class_callback tests.
4560       *
4561       * @return array
4562       */
4563      public function component_class_callback_data_provider() {
4564          return [
4565              'empty string' => [''],
4566              'string' => ['This is a string'],
4567              'int' => [12345],
4568              'stdClass' => [(object) ['this is my content']],
4569              'array' => [['a' => 'b',]],
4570          ];
4571      }
4572  
4573      /**
4574       * Data provider with list of default values for user in component_class_callback tests.
4575       *
4576       * @return array
4577       */
4578      public function component_class_callback_multiple_params_provider() {
4579          return [
4580              'empty array' => [
4581                  [],
4582                  0,
4583              ],
4584              'string value' => [
4585                  ['one'],
4586                  1,
4587              ],
4588              'string values' => [
4589                  ['one', 'two'],
4590                  2,
4591              ],
4592              'arrays' => [
4593                  [[], []],
4594                  2,
4595              ],
4596              'nulls' => [
4597                  [null, null, null, null],
4598                  4,
4599              ],
4600              'mixed' => [
4601                  ['a', 1, null, (object) [], []],
4602                  5,
4603              ],
4604          ];
4605      }
4606  
4607      /**
4608       * Test that {@link get_callable_name()} describes the callable as expected.
4609       *
4610       * @dataProvider callable_names_provider
4611       * @param callable $callable
4612       * @param string $expectedname
4613       */
4614      public function test_get_callable_name($callable, $expectedname) {
4615          $this->assertSame($expectedname, get_callable_name($callable));
4616      }
4617  
4618      /**
4619       * Provides a set of callables and their human readable names.
4620       *
4621       * @return array of (string)case => [(mixed)callable, (string|bool)expected description]
4622       */
4623      public function callable_names_provider() {
4624          return [
4625              'integer' => [
4626                  386,
4627                  false,
4628              ],
4629              'boolean' => [
4630                  true,
4631                  false,
4632              ],
4633              'static_method_as_literal' => [
4634                  'my_foobar_class::my_foobar_method',
4635                  'my_foobar_class::my_foobar_method',
4636              ],
4637              'static_method_of_literal_class' => [
4638                  ['my_foobar_class', 'my_foobar_method'],
4639                  'my_foobar_class::my_foobar_method',
4640              ],
4641              'static_method_of_object' => [
4642                  [$this, 'my_foobar_method'],
4643                  'core_moodlelib_testcase::my_foobar_method',
4644              ],
4645              'method_of_object' => [
4646                  [new lang_string('parentlanguage', 'core_langconfig'), 'my_foobar_method'],
4647                  'lang_string::my_foobar_method',
4648              ],
4649              'function_as_literal' => [
4650                  'my_foobar_callback',
4651                  'my_foobar_callback',
4652              ],
4653              'function_as_closure' => [
4654                  function($a) { return $a; },
4655                  'Closure::__invoke',
4656              ],
4657          ];
4658      }
4659  
4660      /**
4661       * Data provider for \core_moodlelib_testcase::test_get_complete_user_data().
4662       *
4663       * @return array
4664       */
4665      public function user_data_provider() {
4666          return [
4667              'Fetch data using a valid username' => [
4668                  'username', 's1', true
4669              ],
4670              'Fetch data using a valid username, different case' => [
4671                  'username', 'S1', true
4672              ],
4673              'Fetch data using a valid username, different case for fieldname and value' => [
4674                  'USERNAME', 'S1', true
4675              ],
4676              'Fetch data using an invalid username' => [
4677                  'username', 's2', false
4678              ],
4679              'Fetch by email' => [
4680                  'email', 's1@example.com', true
4681              ],
4682              'Fetch data using a non-existent email' => [
4683                  'email', 's2@example.com', false
4684              ],
4685              'Fetch data using a non-existent email, throw exception' => [
4686                  'email', 's2@example.com', false, dml_missing_record_exception::class
4687              ],
4688              'Multiple accounts with the same email' => [
4689                  'email', 's1@example.com', false, 1
4690              ],
4691              'Multiple accounts with the same email, throw exception' => [
4692                  'email', 's1@example.com', false, 1, dml_multiple_records_exception::class
4693              ],
4694              'Fetch data using a valid user ID' => [
4695                  'id', true, true
4696              ],
4697              'Fetch data using a non-existent user ID' => [
4698                  'id', false, false
4699              ],
4700          ];
4701      }
4702  
4703      /**
4704       * Test for get_complete_user_data().
4705       *
4706       * @dataProvider user_data_provider
4707       * @param string $field The field to use for the query.
4708       * @param string|boolean $value The field value. When fetching by ID, set true to fetch valid user ID, false otherwise.
4709       * @param boolean $success Whether we expect for the fetch to succeed or return false.
4710       * @param int $allowaccountssameemail Value for $CFG->allowaccountssameemail.
4711       * @param string $expectedexception The exception to be expected.
4712       */
4713      public function test_get_complete_user_data($field, $value, $success, $allowaccountssameemail = 0, $expectedexception = '') {
4714          $this->resetAfterTest();
4715  
4716          // Set config settings we need for our environment.
4717          set_config('allowaccountssameemail', $allowaccountssameemail);
4718  
4719          // Generate the user data.
4720          $generator = $this->getDataGenerator();
4721          $userdata = [
4722              'username' => 's1',
4723              'email' => 's1@example.com',
4724          ];
4725          $user = $generator->create_user($userdata);
4726  
4727          if ($allowaccountssameemail) {
4728              // Create another user with the same email address.
4729              $generator->create_user(['email' => 's1@example.com']);
4730          }
4731  
4732          // Since the data provider can't know what user ID to use, do a special handling for ID field tests.
4733          if ($field === 'id') {
4734              if ($value) {
4735                  // Test for fetching data using a valid user ID. Use the generated user's ID.
4736                  $value = $user->id;
4737              } else {
4738                  // Test for fetching data using a non-existent user ID.
4739                  $value = $user->id + 1;
4740              }
4741          }
4742  
4743          // When an exception is expected.
4744          $throwexception = false;
4745          if ($expectedexception) {
4746              $this->expectException($expectedexception);
4747              $throwexception = true;
4748          }
4749  
4750          $fetcheduser = get_complete_user_data($field, $value, null, $throwexception);
4751          if ($success) {
4752              $this->assertEquals($user->id, $fetcheduser->id);
4753              $this->assertEquals($user->username, $fetcheduser->username);
4754              $this->assertEquals($user->email, $fetcheduser->email);
4755          } else {
4756              $this->assertFalse($fetcheduser);
4757          }
4758      }
4759  
4760      /**
4761       * Test for send_password_change_().
4762       */
4763      public function test_send_password_change_info() {
4764          $this->resetAfterTest();
4765  
4766          $user = $this->getDataGenerator()->create_user();
4767  
4768          $sink = $this->redirectEmails(); // Make sure we are redirecting emails.
4769          send_password_change_info($user);
4770          $result = $sink->get_messages();
4771          $sink->close();
4772  
4773          $this->assertStringContainsString('passwords cannot be reset on this site', quoted_printable_decode($result[0]->body));
4774      }
4775  
4776      /**
4777       * Test the get_time_interval_string for a range of inputs.
4778       *
4779       * @dataProvider get_time_interval_string_provider
4780       * @param int $time1 the time1 param.
4781       * @param int $time2 the time2 param.
4782       * @param string|null $format the format param.
4783       * @param string $expected the expected string.
4784       */
4785      public function test_get_time_interval_string(int $time1, int $time2, ?string $format, string $expected) {
4786          if (is_null($format)) {
4787              $this->assertEquals($expected, get_time_interval_string($time1, $time2));
4788          } else {
4789              $this->assertEquals($expected, get_time_interval_string($time1, $time2, $format));
4790          }
4791      }
4792  
4793      /**
4794       * Data provider for the test_get_time_interval_string() method.
4795       */
4796      public function get_time_interval_string_provider() {
4797          return [
4798              'Time is after the reference time by 1 minute, omitted format' => [
4799                  'time1' => 12345660,
4800                  'time2' => 12345600,
4801                  'format' => null,
4802                  'expected' => '0d 0h 1m'
4803              ],
4804              'Time is before the reference time by 1 minute, omitted format' => [
4805                  'time1' => 12345540,
4806                  'time2' => 12345600,
4807                  'format' => null,
4808                  'expected' => '0d 0h 1m'
4809              ],
4810              'Time is equal to the reference time, omitted format' => [
4811                  'time1' => 12345600,
4812                  'time2' => 12345600,
4813                  'format' => null,
4814                  'expected' => '0d 0h 0m'
4815              ],
4816              'Time is after the reference time by 1 minute, empty string format' => [
4817                  'time1' => 12345660,
4818                  'time2' => 12345600,
4819                  'format' => '',
4820                  'expected' => '0d 0h 1m'
4821              ],
4822              'Time is before the reference time by 1 minute, empty string format' => [
4823                  'time1' => 12345540,
4824                  'time2' => 12345600,
4825                  'format' => '',
4826                  'expected' => '0d 0h 1m'
4827              ],
4828              'Time is equal to the reference time, empty string format' => [
4829                  'time1' => 12345600,
4830                  'time2' => 12345600,
4831                  'format' => '',
4832                  'expected' => '0d 0h 0m'
4833              ],
4834              'Time is after the reference time by 1 minute, custom format' => [
4835                  'time1' => 12345660,
4836                  'time2' => 12345600,
4837                  'format' => '%R%adays %hhours %imins',
4838                  'expected' => '+0days 0hours 1mins'
4839              ],
4840              'Time is before the reference time by 1 minute, custom format' => [
4841                  'time1' => 12345540,
4842                  'time2' => 12345600,
4843                  'format' => '%R%adays %hhours %imins',
4844                  'expected' => '-0days 0hours 1mins'
4845              ],
4846              'Time is equal to the reference time, custom format' => [
4847                  'time1' => 12345600,
4848                  'time2' => 12345600,
4849                  'format' => '%R%adays %hhours %imins',
4850                  'expected' => '+0days 0hours 0mins'
4851              ],
4852          ];
4853      }
4854  
4855      /**
4856       * Tests the rename_to_unused_name function with a file.
4857       */
4858      public function test_rename_to_unused_name_file() {
4859          global $CFG;
4860  
4861          // Create a new file in dataroot.
4862          $file = $CFG->dataroot . '/argh.txt';
4863          file_put_contents($file, 'Frogs');
4864  
4865          // Rename it.
4866          $newname = rename_to_unused_name($file);
4867  
4868          // Check new name has expected format.
4869          $this->assertRegExp('~/_temp_[a-f0-9]+$~', $newname);
4870  
4871          // Check it's still in the same folder.
4872          $this->assertEquals($CFG->dataroot, dirname($newname));
4873  
4874          // Check file can be loaded.
4875          $this->assertEquals('Frogs', file_get_contents($newname));
4876  
4877          // OK, delete the file.
4878          unlink($newname);
4879      }
4880  
4881      /**
4882       * Tests the rename_to_unused_name function with a directory.
4883       */
4884      public function test_rename_to_unused_name_dir() {
4885          global $CFG;
4886  
4887          // Create a new directory in dataroot.
4888          $file = $CFG->dataroot . '/arghdir';
4889          mkdir($file);
4890  
4891          // Rename it.
4892          $newname = rename_to_unused_name($file);
4893  
4894          // Check new name has expected format.
4895          $this->assertRegExp('~/_temp_[a-f0-9]+$~', $newname);
4896  
4897          // Check it's still in the same folder.
4898          $this->assertEquals($CFG->dataroot, dirname($newname));
4899  
4900          // Check it's still a directory
4901          $this->assertTrue(is_dir($newname));
4902  
4903          // OK, delete the directory.
4904          rmdir($newname);
4905      }
4906  
4907      /**
4908       * Tests the rename_to_unused_name function with error cases.
4909       */
4910      public function test_rename_to_unused_name_failure() {
4911          global $CFG;
4912  
4913          // Rename a file that doesn't exist.
4914          $file = $CFG->dataroot . '/argh.txt';
4915          $this->assertFalse(rename_to_unused_name($file));
4916      }
4917  
4918      /**
4919       * Provider for display_size
4920       *
4921       * @return array of ($size, $expected)
4922       */
4923      public function display_size_provider() {
4924  
4925          return [
4926              [0,     '0 bytes'    ],
4927              [1,     '1 bytes'    ],
4928              [1023,  '1023 bytes' ],
4929              [1024,      '1KB'    ],
4930              [2222,      '2.2KB'  ],
4931              [33333,     '32.6KB' ],
4932              [444444,    '434KB'  ],
4933              [5555555,       '5.3MB'  ],
4934              [66666666,      '63.6MB' ],
4935              [777777777,     '741.7MB'],
4936              [8888888888,        '8.3GB'  ],
4937              [99999999999,       '93.1GB' ],
4938              [111111111111,      '103.5GB'],
4939              [2222222222222,         '2TB'    ],
4940              [33333333333333,        '30.3TB' ],
4941              [444444444444444,       '404.2TB'],
4942              [5555555555555555,          '4.9PB'  ],
4943              [66666666666666666,         '59.2PB' ],
4944              [777777777777777777,        '690.8PB'],
4945          ];
4946      }
4947  
4948      /**
4949       * Test display_size
4950       * @dataProvider display_size_provider
4951       * @param int $size the size in bytes
4952       * @param string $expected the expected string.
4953       */
4954      public function test_display_size($size, $expected) {
4955          $result = display_size($size);
4956          $this->assertEquals($expected, $result);
4957      }
4958  }