Search moodle.org's
Developer Documentation

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
  • Differences Between: [Versions 310 and 311] [Versions 311 and 400] [Versions 37 and 311] [Versions 38 and 311] [Versions 39 and 311]

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