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
27defined('MOODLE_INTERNAL') || die();
28
29class core_moodlelib_testcase extends advanced_testcase {
30
31    public static $includecoverage = array('lib/moodlelib.php');
32
33    /**
34     * Define a local decimal separator.
35     *
36     * It is not possible to directly change the result of get_string in
37     * a unit test. Instead, we create a language pack for language 'xx' in
38     * dataroot and make langconfig.php with the string we need to change.
39     * The default example separator used here is 'X'; on PHP 5.3 and before this
40     * must be a single byte character due to PHP bug/limitation in
41     * number_format, so you can't use UTF-8 characters.
42     *
43     * @param string $decsep Separator character. Defaults to `'X'`.
44     */
45    protected function define_local_decimal_separator(string $decsep = 'X') {
46        global $SESSION, $CFG;
47
48        $SESSION->lang = 'xx';
49        $langconfig = "<?php\n\$string['decsep'] = '$decsep';";
50        $langfolder = $CFG->dataroot . '/lang/xx';
51        check_dir_exists($langfolder);
52        file_put_contents($langfolder . '/langconfig.php', $langconfig);
53
54        // Ensure the new value is picked up and not taken from the cache.
55        $stringmanager = get_string_manager();
56        $stringmanager->reset_caches(true);
57    }
58
59    public function test_cleanremoteaddr() {
60        // IPv4.
61        $this->assertNull(cleanremoteaddr('1023.121.234.1'));
62        $this->assertSame('123.121.234.1', cleanremoteaddr('123.121.234.01 '));
63
64        // IPv6.
65        $this->assertNull(cleanremoteaddr('0:0:0:0:0:0:0:0:0'));
66        $this->assertNull(cleanremoteaddr('0:0:0:0:0:0:0:abh'));
67        $this->assertNull(cleanremoteaddr('0:0:0:::0:0:1'));
68        $this->assertSame('::', cleanremoteaddr('0:0:0:0:0:0:0:0', true));
69        $this->assertSame('::1:1', cleanremoteaddr('0:0:0:0:0:0:1:1', true));
70        $this->assertSame('abcd:ef::', cleanremoteaddr('abcd:00ef:0:0:0:0:0:0', true));
71        $this->assertSame('1::1', cleanremoteaddr('1:0:0:0:0:0:0:1', true));
72        $this->assertSame('0:0:0:0:0:0:10:1', cleanremoteaddr('::10:1', false));
73        $this->assertSame('1:1:0:0:0:0:0:0', cleanremoteaddr('01:1::', false));
74        $this->assertSame('10:0:0:0:0:0:0:10', cleanremoteaddr('10::10', false));
75        $this->assertSame('::ffff:c0a8:11', cleanremoteaddr('::ffff:192.168.1.1', true));
76    }
77
78    public function test_address_in_subnet() {
79        // 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn (number of bits in net mask).
80        $this->assertTrue(address_in_subnet('123.121.234.1', '123.121.234.1/32'));
81        $this->assertFalse(address_in_subnet('123.121.23.1', '123.121.23.0/32'));
82        $this->assertTrue(address_in_subnet('10.10.10.100',  '123.121.23.45/0'));
83        $this->assertTrue(address_in_subnet('123.121.234.1', '123.121.234.0/24'));
84        $this->assertFalse(address_in_subnet('123.121.34.1', '123.121.234.0/24'));
85        $this->assertTrue(address_in_subnet('123.121.234.1', '123.121.234.0/30'));
86        $this->assertFalse(address_in_subnet('123.121.23.8', '123.121.23.0/30'));
87        $this->assertTrue(address_in_subnet('baba:baba::baba', 'baba:baba::baba/128'));
88        $this->assertFalse(address_in_subnet('bab:baba::baba', 'bab:baba::cece/128'));
89        $this->assertTrue(address_in_subnet('baba:baba::baba', 'cece:cece::cece/0'));
90        $this->assertTrue(address_in_subnet('baba:baba::baba', 'baba:baba::baba/128'));
91        $this->assertTrue(address_in_subnet('baba:baba::00ba', 'baba:baba::/120'));
92        $this->assertFalse(address_in_subnet('baba:baba::aba', 'baba:baba::/120'));
93        $this->assertTrue(address_in_subnet('baba::baba:00ba', 'baba::baba:0/112'));
94        $this->assertFalse(address_in_subnet('baba::aba:00ba', 'baba::baba:0/112'));
95        $this->assertFalse(address_in_subnet('aba::baba:0000', 'baba::baba:0/112'));
96
97        // Fixed input.
98        $this->assertTrue(address_in_subnet('123.121.23.1   ', ' 123.121.23.0 / 24'));
99        $this->assertTrue(address_in_subnet('::ffff:10.1.1.1', ' 0:0:0:000:0:ffff:a1:10 / 126'));
100
101        // Incorrect input.
102        $this->assertFalse(address_in_subnet('123.121.234.1', '123.121.234.1/-2'));
103        $this->assertFalse(address_in_subnet('123.121.234.1', '123.121.234.1/64'));
104        $this->assertFalse(address_in_subnet('123.121.234.x', '123.121.234.1/24'));
105        $this->assertFalse(address_in_subnet('123.121.234.0', '123.121.234.xx/24'));
106        $this->assertFalse(address_in_subnet('123.121.234.1', '123.121.234.1/xx0'));
107        $this->assertFalse(address_in_subnet('::1', '::aa:0/xx0'));
108        $this->assertFalse(address_in_subnet('::1', '::aa:0/-5'));
109        $this->assertFalse(address_in_subnet('::1', '::aa:0/130'));
110        $this->assertFalse(address_in_subnet('x:1', '::aa:0/130'));
111        $this->assertFalse(address_in_subnet('::1', '::ax:0/130'));
112
113        // 2: xxx.xxx.xxx.xxx-yyy or  xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx::xxxx-yyyy (a range of IP addresses in the last group).
114        $this->assertTrue(address_in_subnet('123.121.234.12', '123.121.234.12-14'));
115        $this->assertTrue(address_in_subnet('123.121.234.13', '123.121.234.12-14'));
116        $this->assertTrue(address_in_subnet('123.121.234.14', '123.121.234.12-14'));
117        $this->assertFalse(address_in_subnet('123.121.234.1', '123.121.234.12-14'));
118        $this->assertFalse(address_in_subnet('123.121.234.20', '123.121.234.12-14'));
119        $this->assertFalse(address_in_subnet('123.121.23.12', '123.121.234.12-14'));
120        $this->assertFalse(address_in_subnet('123.12.234.12', '123.121.234.12-14'));
121        $this->assertTrue(address_in_subnet('baba:baba::baba', 'baba:baba::baba-babe'));
122        $this->assertTrue(address_in_subnet('baba:baba::babc', 'baba:baba::baba-babe'));
123        $this->assertTrue(address_in_subnet('baba:baba::babe', 'baba:baba::baba-babe'));
124        $this->assertFalse(address_in_subnet('bab:baba::bab0', 'bab:baba::baba-babe'));
125        $this->assertFalse(address_in_subnet('bab:baba::babf', 'bab:baba::baba-babe'));
126        $this->assertFalse(address_in_subnet('bab:baba::bfbe', 'bab:baba::baba-babe'));
127        $this->assertFalse(address_in_subnet('bfb:baba::babe', 'bab:baba::baba-babe'));
128
129        // Fixed input.
130        $this->assertTrue(address_in_subnet('123.121.234.12', '123.121.234.12 - 14 '));
131        $this->assertTrue(address_in_subnet('bab:baba::babe', 'bab:baba::baba - babe  '));
132
133        // Incorrect input.
134        $this->assertFalse(address_in_subnet('123.121.234.12', '123.121.234.12-234.14'));
135        $this->assertFalse(address_in_subnet('123.121.234.12', '123.121.234.12-256'));
136        $this->assertFalse(address_in_subnet('123.121.234.12', '123.121.234.12--256'));
137
138        // 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx. (incomplete address, a bit non-technical ;-).
139        $this->assertTrue(address_in_subnet('123.121.234.12', '123.121.234.12'));
140        $this->assertFalse(address_in_subnet('123.121.23.12', '123.121.23.13'));
141        $this->assertTrue(address_in_subnet('123.121.234.12', '123.121.234.'));
142        $this->assertTrue(address_in_subnet('123.121.234.12', '123.121.234'));
143        $this->assertTrue(address_in_subnet('123.121.234.12', '123.121'));
144        $this->assertTrue(address_in_subnet('123.121.234.12', '123'));
145        $this->assertFalse(address_in_subnet('123.121.234.1', '12.121.234.'));
146        $this->assertFalse(address_in_subnet('123.121.234.1', '12.121.234'));
147        $this->assertTrue(address_in_subnet('baba:baba::bab', 'baba:baba::bab'));
148        $this->assertFalse(address_in_subnet('baba:baba::ba', 'baba:baba::bc'));
149        $this->assertTrue(address_in_subnet('baba:baba::bab', 'baba:baba'));
150        $this->assertTrue(address_in_subnet('baba:baba::bab', 'baba:'));
151        $this->assertFalse(address_in_subnet('bab:baba::bab', 'baba:'));
152
153        // Multiple subnets.
154        $this->assertTrue(address_in_subnet('123.121.234.12', '::1/64, 124., 123.121.234.10-30'));
155        $this->assertTrue(address_in_subnet('124.121.234.12', '::1/64, 124., 123.121.234.10-30'));
156        $this->assertTrue(address_in_subnet('::2',            '::1/64, 124., 123.121.234.10-30'));
157        $this->assertFalse(address_in_subnet('12.121.234.12', '::1/64, 124., 123.121.234.10-30'));
158
159        // Other incorrect input.
160        $this->assertFalse(address_in_subnet('123.123.123.123', ''));
161    }
162
163    public function test_fix_utf8() {
164        // Make sure valid data including other types is not changed.
165        $this->assertSame(null, fix_utf8(null));
166        $this->assertSame(1, fix_utf8(1));
167        $this->assertSame(1.1, fix_utf8(1.1));
168        $this->assertSame(true, fix_utf8(true));
169        $this->assertSame('', fix_utf8(''));
170        $this->assertSame('abc', fix_utf8('abc'));
171        $array = array('do', 're', 'mi');
172        $this->assertSame($array, fix_utf8($array));
173        $object = new stdClass();
174        $object->a = 'aa';
175        $object->b = 'bb';
176        $this->assertEquals($object, fix_utf8($object));
177
178        // valid utf8 string
179        $this->assertSame("žlutý koníček přeskočil potůček \n\t\r", fix_utf8("žlutý koníček přeskočil potůček \n\t\r\0"));
180
181        // Invalid utf8 string.
182        $this->assertSame('aš', fix_utf8('a'.chr(130).'š'), 'This fails with buggy iconv() when mbstring extenstion is not available as fallback.');
183    }
184
185    public function test_optional_param() {
186        global $CFG;
187
188        $_POST['username'] = 'post_user';
189        $_GET['username'] = 'get_user';
190        $this->assertSame($_POST['username'], optional_param('username', 'default_user', PARAM_RAW));
191
192        unset($_POST['username']);
193        $this->assertSame($_GET['username'], optional_param('username', 'default_user', PARAM_RAW));
194
195        unset($_GET['username']);
196        $this->assertSame('default_user', optional_param('username', 'default_user', PARAM_RAW));
197
198        // Make sure exception is triggered when some params are missing, hide error notices here - new in 2.2.
199        $_POST['username'] = 'post_user';
200        try {
201            optional_param('username', 'default_user', null);
202            $this->fail('coding_exception expected');
203        } catch (moodle_exception $ex) {
204            $this->assertInstanceOf('coding_exception', $ex);
205        }
206        try {
207            @optional_param('username', 'default_user');
208            $this->fail('coding_exception expected');
209        } catch (moodle_exception $ex) {
210            $this->assertInstanceOf('coding_exception', $ex);
211        } catch (Error $error) {
212            // PHP 7.1 throws Error even earlier.
213            $this->assertMatchesRegularExpression('/Too few arguments to function/', $error->getMessage());
214        }
215        try {
216            @optional_param('username');
217            $this->fail('coding_exception expected');
218        } catch (moodle_exception $ex) {
219            $this->assertInstanceOf('coding_exception', $ex);
220        } catch (Error $error) {
221            // PHP 7.1 throws Error even earlier.
222            $this->assertMatchesRegularExpression('/Too few arguments to function/', $error->getMessage());
223        }
224        try {
225            optional_param('', 'default_user', PARAM_RAW);
226            $this->fail('coding_exception expected');
227        } catch (moodle_exception $ex) {
228            $this->assertInstanceOf('coding_exception', $ex);
229        }
230
231        // Make sure warning is displayed if array submitted - TODO: throw exception in Moodle 2.3.
232        $_POST['username'] = array('a'=>'a');
233        $this->assertSame($_POST['username'], optional_param('username', 'default_user', PARAM_RAW));
234        $this->assertDebuggingCalled();
235    }
236
237    public function test_optional_param_array() {
238        global $CFG;
239
240        $_POST['username'] = array('a'=>'post_user');
241        $_GET['username'] = array('a'=>'get_user');
242        $this->assertSame($_POST['username'], optional_param_array('username', array('a'=>'default_user'), PARAM_RAW));
243
244        unset($_POST['username']);
245        $this->assertSame($_GET['username'], optional_param_array('username', array('a'=>'default_user'), PARAM_RAW));
246
247        unset($_GET['username']);
248        $this->assertSame(array('a'=>'default_user'), optional_param_array('username', array('a'=>'default_user'), PARAM_RAW));
249
250        // Make sure exception is triggered when some params are missing, hide error notices here - new in 2.2.
251        $_POST['username'] = array('a'=>'post_user');
252        try {
253            optional_param_array('username', array('a'=>'default_user'), null);
254            $this->fail('coding_exception expected');
255        } catch (moodle_exception $ex) {
256            $this->assertInstanceOf('coding_exception', $ex);
257        }
258        try {
259            @optional_param_array('username', array('a'=>'default_user'));
260            $this->fail('coding_exception expected');
261        } catch (moodle_exception $ex) {
262            $this->assertInstanceOf('coding_exception', $ex);
263        } catch (Error $error) {
264            // PHP 7.1 throws Error even earlier.
265            $this->assertMatchesRegularExpression('/Too few arguments to function/', $error->getMessage());
266        }
267        try {
268            @optional_param_array('username');
269            $this->fail('coding_exception expected');
270        } catch (moodle_exception $ex) {
271            $this->assertInstanceOf('coding_exception', $ex);
272        } catch (Error $error) {
273            // PHP 7.1 throws Error even earlier.
274            $this->assertMatchesRegularExpression('/Too few arguments to function/', $error->getMessage());
275        }
276        try {
277            optional_param_array('', array('a'=>'default_user'), PARAM_RAW);
278            $this->fail('coding_exception expected');
279        } catch (moodle_exception $ex) {
280            $this->assertInstanceOf('coding_exception', $ex);
281        }
282
283        // Do not allow nested arrays.
284        try {
285            $_POST['username'] = array('a'=>array('b'=>'post_user'));
286            optional_param_array('username', array('a'=>'default_user'), PARAM_RAW);
287            $this->fail('coding_exception expected');
288        } catch (coding_exception $ex) {
289            $this->assertTrue(true);
290        }
291
292        // Do not allow non-arrays.
293        $_POST['username'] = 'post_user';
294        $this->assertSame(array('a'=>'default_user'), optional_param_array('username', array('a'=>'default_user'), PARAM_RAW));
295        $this->assertDebuggingCalled();
296
297        // Make sure array keys are sanitised.
298        $_POST['username'] = array('abc123_;-/*-+ '=>'arrggh', 'a1_-'=>'post_user');
299        $this->assertSame(array('a1_-'=>'post_user'), optional_param_array('username', array(), PARAM_RAW));
300        $this->assertDebuggingCalled();
301    }
302
303    public function test_required_param() {
304        $_POST['username'] = 'post_user';
305        $_GET['username'] = 'get_user';
306        $this->assertSame('post_user', required_param('username', PARAM_RAW));
307
308        unset($_POST['username']);
309        $this->assertSame('get_user', required_param('username', PARAM_RAW));
310
311        unset($_GET['username']);
312        try {
313            $this->assertSame('default_user', required_param('username', PARAM_RAW));
314            $this->fail('moodle_exception expected');
315        } catch (moodle_exception $ex) {
316            $this->assertInstanceOf('moodle_exception', $ex);
317        }
318
319        // Make sure exception is triggered when some params are missing, hide error notices here - new in 2.2.
320        $_POST['username'] = 'post_user';
321        try {
322            @required_param('username');
323            $this->fail('coding_exception expected');
324        } catch (moodle_exception $ex) {
325            $this->assertInstanceOf('coding_exception', $ex);
326        } catch (Error $error) {
327            // PHP 7.1 throws Error even earlier.
328            $this->assertMatchesRegularExpression('/Too few arguments to function/', $error->getMessage());
329        }
330        try {
331            required_param('username', '');
332            $this->fail('coding_exception expected');
333        } catch (moodle_exception $ex) {
334            $this->assertInstanceOf('coding_exception', $ex);
335        }
336        try {
337            required_param('', PARAM_RAW);
338            $this->fail('coding_exception expected');
339        } catch (moodle_exception $ex) {
340            $this->assertInstanceOf('coding_exception', $ex);
341        }
342
343        // Make sure warning is displayed if array submitted - TODO: throw exception in Moodle 2.3.
344        $_POST['username'] = array('a'=>'a');
345        $this->assertSame($_POST['username'], required_param('username', PARAM_RAW));
346        $this->assertDebuggingCalled();
347    }
348
349    public function test_required_param_array() {
350        global $CFG;
351
352        $_POST['username'] = array('a'=>'post_user');
353        $_GET['username'] = array('a'=>'get_user');
354        $this->assertSame($_POST['username'], required_param_array('username', PARAM_RAW));
355
356        unset($_POST['username']);
357        $this->assertSame($_GET['username'], required_param_array('username', PARAM_RAW));
358
359        // Make sure exception is triggered when some params are missing, hide error notices here - new in 2.2.
360        $_POST['username'] = array('a'=>'post_user');
361        try {
362            required_param_array('username', null);
363            $this->fail('coding_exception expected');
364        } catch (moodle_exception $ex) {
365            $this->assertInstanceOf('coding_exception', $ex);
366        }
367        try {
368            @required_param_array('username');
369            $this->fail('coding_exception expected');
370        } catch (moodle_exception $ex) {
371            $this->assertInstanceOf('coding_exception', $ex);
372        } catch (Error $error) {
373            // PHP 7.1 throws Error.
374            $this->assertMatchesRegularExpression('/Too few arguments to function/', $error->getMessage());
375        }
376        try {
377            required_param_array('', PARAM_RAW);
378            $this->fail('coding_exception expected');
379        } catch (moodle_exception $ex) {
380            $this->assertInstanceOf('coding_exception', $ex);
381        }
382
383        // Do not allow nested arrays.
384        try {
385            $_POST['username'] = array('a'=>array('b'=>'post_user'));
386            required_param_array('username', PARAM_RAW);
387            $this->fail('coding_exception expected');
388        } catch (moodle_exception $ex) {
389            $this->assertInstanceOf('coding_exception', $ex);
390        }
391
392        // Do not allow non-arrays.
393        try {
394            $_POST['username'] = 'post_user';
395            required_param_array('username', PARAM_RAW);
396            $this->fail('moodle_exception expected');
397        } catch (moodle_exception $ex) {
398            $this->assertInstanceOf('moodle_exception', $ex);
399        }
400
401        // Make sure array keys are sanitised.
402        $_POST['username'] = array('abc123_;-/*-+ '=>'arrggh', 'a1_-'=>'post_user');
403        $this->assertSame(array('a1_-'=>'post_user'), required_param_array('username', PARAM_RAW));
404        $this->assertDebuggingCalled();
405    }
406
407    public function test_clean_param() {
408        // Forbid objects and arrays.
409        try {
410            clean_param(array('x', 'y'), PARAM_RAW);
411            $this->fail('coding_exception expected');
412        } catch (moodle_exception $ex) {
413            $this->assertInstanceOf('coding_exception', $ex);
414        }
415        try {
416            $param = new stdClass();
417            $param->id = 1;
418            clean_param($param, PARAM_RAW);
419            $this->fail('coding_exception expected');
420        } catch (moodle_exception $ex) {
421            $this->assertInstanceOf('coding_exception', $ex);
422        }
423
424        // Require correct type.
425        try {
426            clean_param('x', 'xxxxxx');
427            $this->fail('moodle_exception expected');
428        } catch (moodle_exception $ex) {
429            $this->assertInstanceOf('moodle_exception', $ex);
430        }
431        try {
432            @clean_param('x');
433            $this->fail('moodle_exception expected');
434        } catch (moodle_exception $ex) {
435            $this->assertInstanceOf('moodle_exception', $ex);
436        } catch (Error $error) {
437            // PHP 7.1 throws Error even earlier.
438            $this->assertMatchesRegularExpression('/Too few arguments to function/', $error->getMessage());
439        }
440    }
441
442    public function test_clean_param_array() {
443        $this->assertSame(array(), clean_param_array(null, PARAM_RAW));
444        $this->assertSame(array('a', 'b'), clean_param_array(array('a', 'b'), PARAM_RAW));
445        $this->assertSame(array('a', array('b')), clean_param_array(array('a', array('b')), PARAM_RAW, true));
446
447        // Require correct type.
448        try {
449            clean_param_array(array('x'), 'xxxxxx');
450            $this->fail('moodle_exception expected');
451        } catch (moodle_exception $ex) {
452            $this->assertInstanceOf('moodle_exception', $ex);
453        }
454        try {
455            @clean_param_array(array('x'));
456            $this->fail('moodle_exception expected');
457        } catch (moodle_exception $ex) {
458            $this->assertInstanceOf('moodle_exception', $ex);
459        } catch (Error $error) {
460            // PHP 7.1 throws Error even earlier.
461            $this->assertMatchesRegularExpression('/Too few arguments to function/', $error->getMessage());
462        }
463
464        try {
465            clean_param_array(array('x', array('y')), PARAM_RAW);
466            $this->fail('coding_exception expected');
467        } catch (moodle_exception $ex) {
468            $this->assertInstanceOf('coding_exception', $ex);
469        }
470
471        // Test recursive.
472    }
473
474    public function test_clean_param_raw() {
475        $this->assertSame(
476            '#()*#,9789\'".,<42897></?$(*DSFMO#$*)(SDJ)($*)',
477            clean_param('#()*#,9789\'".,<42897></?$(*DSFMO#$*)(SDJ)($*)', PARAM_RAW));
478    }
479
480    public function test_clean_param_trim() {
481        $this->assertSame('Frog toad', clean_param("   Frog toad   \r\n  ", PARAM_RAW_TRIMMED));
482    }
483
484    public function test_clean_param_clean() {
485        // PARAM_CLEAN is an ugly hack, do not use in new code (skodak),
486        // instead use more specific type, or submit sothing that can be verified properly.
487        $this->assertSame('xx', clean_param('xx<script>', PARAM_CLEAN));
488    }
489
490    public function test_clean_param_alpha() {
491        $this->assertSame('DSFMOSDJ', clean_param('#()*#,9789\'".,<42897></?$(*DSFMO#$*)(SDJ)($*)', PARAM_ALPHA));
492    }
493
494    public function test_clean_param_alphanum() {
495        $this->assertSame('978942897DSFMOSDJ', clean_param('#()*#,9789\'".,<42897></?$(*DSFMO#$*)(SDJ)($*)', PARAM_ALPHANUM));
496    }
497
498    public function test_clean_param_alphaext() {
499        $this->assertSame('DSFMOSDJ', clean_param('#()*#,9789\'".,<42897></?$(*DSFMO#$*)(SDJ)($*)', PARAM_ALPHAEXT));
500    }
501
502    public function test_clean_param_sequence() {
503        $this->assertSame(',9789,42897', clean_param('#()*#,9789\'".,<42897></?$(*DSFMO#$*)(SDJ)($*)', PARAM_SEQUENCE));
504    }
505
506    public function test_clean_param_component() {
507        // Please note the cleaning of component names is very strict, no guessing here.
508        $this->assertSame('mod_forum', clean_param('mod_forum', PARAM_COMPONENT));
509        $this->assertSame('block_online_users', clean_param('block_online_users', PARAM_COMPONENT));
510        $this->assertSame('block_blond_online_users', clean_param('block_blond_online_users', PARAM_COMPONENT));
511        $this->assertSame('mod_something2', clean_param('mod_something2', PARAM_COMPONENT));
512        $this->assertSame('forum', clean_param('forum', PARAM_COMPONENT));
513        $this->assertSame('user', clean_param('user', PARAM_COMPONENT));
514        $this->assertSame('rating', clean_param('rating', PARAM_COMPONENT));
515        $this->assertSame('feedback360', clean_param('feedback360', PARAM_COMPONENT));
516        $this->assertSame('mod_feedback360', clean_param('mod_feedback360', PARAM_COMPONENT));
517        $this->assertSame('', clean_param('mod_2something', PARAM_COMPONENT));
518        $this->assertSame('', clean_param('2mod_something', PARAM_COMPONENT));
519        $this->assertSame('', clean_param('mod_something_xx', PARAM_COMPONENT));
520        $this->assertSame('', clean_param('auth_something__xx', PARAM_COMPONENT));
521        $this->assertSame('', clean_param('mod_Something', PARAM_COMPONENT));
522        $this->assertSame('', clean_param('mod_somethíng', PARAM_COMPONENT));
523        $this->assertSame('', clean_param('mod__something', PARAM_COMPONENT));
524        $this->assertSame('', clean_param('auth_xx-yy', PARAM_COMPONENT));
525        $this->assertSame('', clean_param('_auth_xx', PARAM_COMPONENT));
526        $this->assertSame('a2uth_xx', clean_param('a2uth_xx', PARAM_COMPONENT));
527        $this->assertSame('', clean_param('auth_xx_', PARAM_COMPONENT));
528        $this->assertSame('', clean_param('auth_xx.old', PARAM_COMPONENT));
529        $this->assertSame('', clean_param('_user', PARAM_COMPONENT));
530        $this->assertSame('', clean_param('2rating', PARAM_COMPONENT));
531        $this->assertSame('', clean_param('user_', PARAM_COMPONENT));
532    }
533
534    public function test_clean_param_localisedfloat() {
535
536        $this->assertSame(0.5, clean_param('0.5', PARAM_LOCALISEDFLOAT));
537        $this->assertSame(false, clean_param('0X5', PARAM_LOCALISEDFLOAT));
538        $this->assertSame(0.5, clean_param('.5', PARAM_LOCALISEDFLOAT));
539        $this->assertSame(false, clean_param('X5', PARAM_LOCALISEDFLOAT));
540        $this->assertSame(10.5, clean_param('10.5', PARAM_LOCALISEDFLOAT));
541        $this->assertSame(false, clean_param('10X5', PARAM_LOCALISEDFLOAT));
542        $this->assertSame(1000.5, clean_param('1 000.5', PARAM_LOCALISEDFLOAT));
543        $this->assertSame(false, clean_param('1 000X5', PARAM_LOCALISEDFLOAT));
544        $this->assertSame(false, clean_param('1.000.5', PARAM_LOCALISEDFLOAT));
545        $this->assertSame(false, clean_param('1X000X5', PARAM_LOCALISEDFLOAT));
546        $this->assertSame(false, clean_param('nan', PARAM_LOCALISEDFLOAT));
547        $this->assertSame(false, clean_param('10.6blah', PARAM_LOCALISEDFLOAT));
548
549        // Tests with a localised decimal separator.
550        $this->define_local_decimal_separator();
551
552        $this->assertSame(0.5, clean_param('0.5', PARAM_LOCALISEDFLOAT));
553        $this->assertSame(0.5, clean_param('0X5', PARAM_LOCALISEDFLOAT));
554        $this->assertSame(0.5, clean_param('.5', PARAM_LOCALISEDFLOAT));
555        $this->assertSame(0.5, clean_param('X5', PARAM_LOCALISEDFLOAT));
556        $this->assertSame(10.5, clean_param('10.5', PARAM_LOCALISEDFLOAT));
557        $this->assertSame(10.5, clean_param('10X5', PARAM_LOCALISEDFLOAT));
558        $this->assertSame(1000.5, clean_param('1 000.5', PARAM_LOCALISEDFLOAT));
559        $this->assertSame(1000.5, clean_param('1 000X5', PARAM_LOCALISEDFLOAT));
560        $this->assertSame(false, clean_param('1.000.5', PARAM_LOCALISEDFLOAT));
561        $this->assertSame(false, clean_param('1X000X5', PARAM_LOCALISEDFLOAT));
562        $this->assertSame(false, clean_param('nan', PARAM_LOCALISEDFLOAT));
563        $this->assertSame(false, clean_param('10X6blah', PARAM_LOCALISEDFLOAT));
564    }
565
566    public function test_is_valid_plugin_name() {
567        $this->assertTrue(is_valid_plugin_name('forum'));
568        $this->assertTrue(is_valid_plugin_name('forum2'));
569        $this->assertTrue(is_valid_plugin_name('feedback360'));
570        $this->assertTrue(is_valid_plugin_name('online_users'));
571        $this->assertTrue(is_valid_plugin_name('blond_online_users'));
572        $this->assertFalse(is_valid_plugin_name('online__users'));
573        $this->assertFalse(is_valid_plugin_name('forum '));
574        $this->assertFalse(is_valid_plugin_name('forum.old'));
575        $this->assertFalse(is_valid_plugin_name('xx-yy'));
576        $this->assertFalse(is_valid_plugin_name('2xx'));
577        $this->assertFalse(is_valid_plugin_name('Xx'));
578        $this->assertFalse(is_valid_plugin_name('_xx'));
579        $this->assertFalse(is_valid_plugin_name('xx_'));
580    }
581
582    public function test_clean_param_plugin() {
583        // Please note the cleaning of plugin names is very strict, no guessing here.
584        $this->assertSame('forum', clean_param('forum', PARAM_PLUGIN));
585        $this->assertSame('forum2', clean_param('forum2', PARAM_PLUGIN));
586        $this->assertSame('feedback360', clean_param('feedback360', PARAM_PLUGIN));
587        $this->assertSame('online_users', clean_param('online_users', PARAM_PLUGIN));
588        $this->assertSame('blond_online_users', clean_param('blond_online_users', PARAM_PLUGIN));
589        $this->assertSame('', clean_param('online__users', PARAM_PLUGIN));
590        $this->assertSame('', clean_param('forum ', PARAM_PLUGIN));
591        $this->assertSame('', clean_param('forum.old', PARAM_PLUGIN));
592        $this->assertSame('', clean_param('xx-yy', PARAM_PLUGIN));
593        $this->assertSame('', clean_param('2xx', PARAM_PLUGIN));
594        $this->assertSame('', clean_param('Xx', PARAM_PLUGIN));
595        $this->assertSame('', clean_param('_xx', PARAM_PLUGIN));
596        $this->assertSame('', clean_param('xx_', PARAM_PLUGIN));
597    }
598
599    public function test_clean_param_area() {
600        // Please note the cleaning of area names is very strict, no guessing here.
601        $this->assertSame('something', clean_param('something', PARAM_AREA));
602        $this->assertSame('something2', clean_param('something2', PARAM_AREA));
603        $this->assertSame('some_thing', clean_param('some_thing', PARAM_AREA));
604        $this->assertSame('some_thing_xx', clean_param('some_thing_xx', PARAM_AREA));
605        $this->assertSame('feedback360', clean_param('feedback360', PARAM_AREA));
606        $this->assertSame('', clean_param('_something', PARAM_AREA));
607        $this->assertSame('', clean_param('something_', PARAM_AREA));
608        $this->assertSame('', clean_param('2something', PARAM_AREA));
609        $this->assertSame('', clean_param('Something', PARAM_AREA));
610        $this->assertSame('', clean_param('some-thing', PARAM_AREA));
611        $this->assertSame('', clean_param('somethííng', PARAM_AREA));
612        $this->assertSame('', clean_param('something.x', PARAM_AREA));
613    }
614
615    public function test_clean_param_text() {
616        $this->assertSame(PARAM_TEXT, PARAM_MULTILANG);
617        // Standard.
618        $this->assertSame('xx<lang lang="en">aa</lang><lang lang="yy">pp</lang>', clean_param('xx<lang lang="en">aa</lang><lang lang="yy">pp</lang>', PARAM_TEXT));
619        $this->assertSame('<span lang="en" class="multilang">aa</span><span lang="xy" class="multilang">bb</span>', clean_param('<span lang="en" class="multilang">aa</span><span lang="xy" class="multilang">bb</span>', PARAM_TEXT));
620        $this->assertSame('xx<lang lang="en">aa'."\n".'</lang><lang lang="yy">pp</lang>', clean_param('xx<lang lang="en">aa'."\n".'</lang><lang lang="yy">pp</lang>', PARAM_TEXT));
621        // Malformed.
622        $this->assertSame('<span lang="en" class="multilang">aa</span>', clean_param('<span lang="en" class="multilang">aa</span>', PARAM_TEXT));
623        $this->assertSame('aa', clean_param('<span lang="en" class="nothing" class="multilang">aa</span>', PARAM_TEXT));
624        $this->assertSame('aa', clean_param('<lang lang="en" class="multilang">aa</lang>', PARAM_TEXT));
625        $this->assertSame('aa', clean_param('<lang lang="en!!">aa</lang>', PARAM_TEXT));
626        $this->assertSame('aa', clean_param('<span lang="en==" class="multilang">aa</span>', PARAM_TEXT));
627        $this->assertSame('abc', clean_param('a<em>b</em>c', PARAM_TEXT));
628        $this->assertSame('a>c>', clean_param('a><xx >c>', PARAM_TEXT)); // Standard strip_tags() behaviour.
629        $this->assertSame('a', clean_param('a<b', PARAM_TEXT));
630        $this->assertSame('a>b', clean_param('a>b', PARAM_TEXT));
631        $this->assertSame('<lang lang="en">a>a</lang>', clean_param('<lang lang="en">a>a</lang>', PARAM_TEXT)); // Standard strip_tags() behaviour.
632        $this->assertSame('a', clean_param('<lang lang="en">a<a</lang>', PARAM_TEXT));
633        $this->assertSame('<lang lang="en">aa</lang>', clean_param('<lang lang="en">a<br>a</lang>', PARAM_TEXT));
634    }
635
636    public function test_clean_param_url() {
637        // Test PARAM_URL and PARAM_LOCALURL a bit.
638        // Valid URLs.
639        $this->assertSame('http://google.com/', clean_param('http://google.com/', PARAM_URL));
640        $this->assertSame('http://some.very.long.and.silly.domain/with/a/path/', clean_param('http://some.very.long.and.silly.domain/with/a/path/', PARAM_URL));
641        $this->assertSame('http://localhost/', clean_param('http://localhost/', PARAM_URL));
642        $this->assertSame('http://0.255.1.1/numericip.php', clean_param('http://0.255.1.1/numericip.php', PARAM_URL));
643        $this->assertSame('https://google.com/', clean_param('https://google.com/', PARAM_URL));
644        $this->assertSame('https://some.very.long.and.silly.domain/with/a/path/', clean_param('https://some.very.long.and.silly.domain/with/a/path/', PARAM_URL));
645        $this->assertSame('https://localhost/', clean_param('https://localhost/', PARAM_URL));
646        $this->assertSame('https://0.255.1.1/numericip.php', clean_param('https://0.255.1.1/numericip.php', PARAM_URL));
647        $this->assertSame('ftp://ftp.debian.org/debian/', clean_param('ftp://ftp.debian.org/debian/', PARAM_URL));
648        $this->assertSame('/just/a/path', clean_param('/just/a/path', PARAM_URL));
649        // Invalid URLs.
650        $this->assertSame('', clean_param('funny:thing', PARAM_URL));
651        $this->assertSame('', clean_param('http://example.ee/sdsf"f', PARAM_URL));
652        $this->assertSame('', clean_param('javascript://comment%0Aalert(1)', PARAM_URL));
653        $this->assertSame('', clean_param('rtmp://example.com/livestream', PARAM_URL));
654        $this->assertSame('', clean_param('rtmp://example.com/live&foo', PARAM_URL));
655        $this->assertSame('', clean_param('rtmp://example.com/fms&mp4:path/to/file.mp4', PARAM_URL));
656        $this->assertSame('', clean_param('mailto:support@moodle.org', PARAM_URL));
657        $this->assertSame('', clean_param('mailto:support@moodle.org?subject=Hello%20Moodle', PARAM_URL));
658        $this->assertSame('', clean_param('mailto:support@moodle.org?subject=Hello%20Moodle&cc=feedback@moodle.org', PARAM_URL));
659    }
660
661    public function test_clean_param_localurl() {
662        global $CFG;
663
664        $this->resetAfterTest();
665
666        // External, invalid.
667        $this->assertSame('', clean_param('funny:thing', PARAM_LOCALURL));
668        $this->assertSame('', clean_param('http://google.com/', PARAM_LOCALURL));
669        $this->assertSame('', clean_param('https://google.com/?test=true', PARAM_LOCALURL));
670        $this->assertSame('', clean_param('http://some.very.long.and.silly.domain/with/a/path/', PARAM_LOCALURL));
671
672        // Local absolute.
673        $this->assertSame(clean_param($CFG->wwwroot, PARAM_LOCALURL), $CFG->wwwroot);
674        $this->assertSame($CFG->wwwroot . '/with/something?else=true',
675            clean_param($CFG->wwwroot . '/with/something?else=true', PARAM_LOCALURL));
676
677        // Local relative.
678        $this->assertSame('/just/a/path', clean_param('/just/a/path', PARAM_LOCALURL));
679        $this->assertSame('course/view.php?id=3', clean_param('course/view.php?id=3', PARAM_LOCALURL));
680
681        // Local absolute HTTPS in a non HTTPS site.
682        $CFG->wwwroot = str_replace('https:', 'http:', $CFG->wwwroot); // Need to simulate non-https site.
683        $httpsroot = str_replace('http:', 'https:', $CFG->wwwroot);
684        $this->assertSame('', clean_param($httpsroot, PARAM_LOCALURL));
685        $this->assertSame('', clean_param($httpsroot . '/with/something?else=true', PARAM_LOCALURL));
686
687        // Local absolute HTTPS in a HTTPS site.
688        $CFG->wwwroot = str_replace('http:', 'https:', $CFG->wwwroot);
689        $httpsroot = $CFG->wwwroot;
690        $this->assertSame($httpsroot, clean_param($httpsroot, PARAM_LOCALURL));
691        $this->assertSame($httpsroot . '/with/something?else=true',
692            clean_param($httpsroot . '/with/something?else=true', PARAM_LOCALURL));
693
694        // Test open redirects are not possible.
695        $CFG->wwwroot = 'http://www.example.com';
696        $this->assertSame('', clean_param('http://www.example.com.evil.net/hack.php', PARAM_LOCALURL));
697        $CFG->wwwroot = 'https://www.example.com';
698        $this->assertSame('', clean_param('https://www.example.com.evil.net/hack.php', PARAM_LOCALURL));
699    }
700
701    public function test_clean_param_file() {
702        $this->assertSame('correctfile.txt', clean_param('correctfile.txt', PARAM_FILE));
703        $this->assertSame('badfile.txt', clean_param('b\'a<d`\\/fi:l>e.t"x|t', PARAM_FILE));
704        $this->assertSame('..parentdirfile.txt', clean_param('../parentdirfile.txt', PARAM_FILE));
705        $this->assertSame('....grandparentdirfile.txt', clean_param('../../grandparentdirfile.txt', PARAM_FILE));
706        $this->assertSame('..winparentdirfile.txt', clean_param('..\winparentdirfile.txt', PARAM_FILE));
707        $this->assertSame('....wingrandparentdir.txt', clean_param('..\..\wingrandparentdir.txt', PARAM_FILE));
708        $this->assertSame('myfile.a.b.txt', clean_param('myfile.a.b.txt', PARAM_FILE));
709        $this->assertSame('myfile..a..b.txt', clean_param('myfile..a..b.txt', PARAM_FILE));
710        $this->assertSame('myfile.a..b...txt', clean_param('myfile.a..b...txt', PARAM_FILE));
711        $this->assertSame('myfile.a.txt', clean_param('myfile.a.txt', PARAM_FILE));
712        $this->assertSame('myfile...txt', clean_param('myfile...txt', PARAM_FILE));
713        $this->assertSame('...jpg', clean_param('...jpg', PARAM_FILE));
714        $this->assertSame('.a.b.', clean_param('.a.b.', PARAM_FILE));
715        $this->assertSame('', clean_param('.', PARAM_FILE));
716        $this->assertSame('', clean_param('..', PARAM_FILE));
717        $this->assertSame('...', clean_param('...', PARAM_FILE));
718        $this->assertSame('. . . .', clean_param('. . . .', PARAM_FILE));
719        $this->assertSame('dontrtrim.me. .. .. . ', clean_param('dontrtrim.me. .. .. . ', PARAM_FILE));
720        $this->assertSame(' . .dontltrim.me', clean_param(' . .dontltrim.me', PARAM_FILE));
721        $this->assertSame('here is a tab.txt', clean_param("here is a tab\t.txt", PARAM_FILE));
722        $this->assertSame('here is a linebreak.txt', clean_param("here is a line\r\nbreak.txt", PARAM_FILE));
723
724        // The following behaviours have been maintained although they seem a little odd.
725        $this->assertSame('funnything', clean_param('funny:thing', PARAM_FILE));
726        $this->assertSame('.currentdirfile.txt', clean_param('./currentdirfile.txt', PARAM_FILE));
727        $this->assertSame('ctempwindowsfile.txt', clean_param('c:\temp\windowsfile.txt', PARAM_FILE));
728        $this->assertSame('homeuserlinuxfile.txt', clean_param('/home/user/linuxfile.txt', PARAM_FILE));
729        $this->assertSame('~myfile.txt', clean_param('~/myfile.txt', PARAM_FILE));
730    }
731
732    public function test_clean_param_path() {
733        $this->assertSame('correctfile.txt', clean_param('correctfile.txt', PARAM_PATH));
734        $this->assertSame('bad/file.txt', clean_param('b\'a<d`\\/fi:l>e.t"x|t', PARAM_PATH));
735        $this->assertSame('/parentdirfile.txt', clean_param('../parentdirfile.txt', PARAM_PATH));
736        $this->assertSame('/grandparentdirfile.txt', clean_param('../../grandparentdirfile.txt', PARAM_PATH));
737        $this->assertSame('/winparentdirfile.txt', clean_param('..\winparentdirfile.txt', PARAM_PATH));
738        $this->assertSame('/wingrandparentdir.txt', clean_param('..\..\wingrandparentdir.txt', PARAM_PATH));
739        $this->assertSame('funnything', clean_param('funny:thing', PARAM_PATH));
740        $this->assertSame('./here', clean_param('./././here', PARAM_PATH));
741        $this->assertSame('./currentdirfile.txt', clean_param('./currentdirfile.txt', PARAM_PATH));
742        $this->assertSame('c/temp/windowsfile.txt', clean_param('c:\temp\windowsfile.txt', PARAM_PATH));
743        $this->assertSame('/home/user/linuxfile.txt', clean_param('/home/user/linuxfile.txt', PARAM_PATH));
744        $this->assertSame('/home../user ./.linuxfile.txt', clean_param('/home../user ./.linuxfile.txt', PARAM_PATH));
745        $this->assertSame('~/myfile.txt', clean_param('~/myfile.txt', PARAM_PATH));
746        $this->assertSame('~/myfile.txt', clean_param('~/../myfile.txt', PARAM_PATH));
747        $this->assertSame('/..b../.../myfile.txt', clean_param('/..b../.../myfile.txt', PARAM_PATH));
748        $this->assertSame('..b../.../myfile.txt', clean_param('..b../.../myfile.txt', PARAM_PATH));
749        $this->assertSame('/super/slashes/', clean_param('/super//slashes///', PARAM_PATH));
750    }
751
752    public function test_clean_param_username() {
753        global $CFG;
754        $currentstatus =  $CFG->extendedusernamechars;
755
756        // Run tests with extended character == false;.
757        $CFG->extendedusernamechars = false;
758        $this->assertSame('johndoe123', clean_param('johndoe123', PARAM_USERNAME) );
759        $this->assertSame('john.doe', clean_param('john.doe', PARAM_USERNAME));
760        $this->assertSame('john-doe', clean_param('john-doe', PARAM_USERNAME));
761        $this->assertSame('john-doe', clean_param('john- doe', PARAM_USERNAME));
762        $this->assertSame('john_doe', clean_param('john_doe', PARAM_USERNAME));
763        $this->assertSame('john@doe', clean_param('john@doe', PARAM_USERNAME));
764        $this->assertSame('johndoe', clean_param('john~doe', PARAM_USERNAME));
765        $this->assertSame('johndoe', clean_param('john´doe', PARAM_USERNAME));
766        $this->assertSame(clean_param('john# $%&()+_^', PARAM_USERNAME), 'john_');
767        $this->assertSame(clean_param(' john# $%&()+_^ ', PARAM_USERNAME), 'john_');
768        $this->assertSame(clean_param('john#$%&() ', PARAM_USERNAME), 'john');
769        $this->assertSame('johnd', clean_param('JOHNdóé ', PARAM_USERNAME));
770        $this->assertSame(clean_param('john.,:;-_/|\ñÑ[]A_X-,D {} ~!@#$%^&*()_+ ?><[] ščřžžý ?ýáž?žý??šdoe ', PARAM_USERNAME), 'john.-_a_x-d@_doe');
771
772        // Test success condition, if extendedusernamechars == ENABLE;.
773        $CFG->extendedusernamechars = true;
774        $this->assertSame('john_doe', clean_param('john_doe', PARAM_USERNAME));
775        $this->assertSame('john@doe', clean_param('john@doe', PARAM_USERNAME));
776        $this->assertSame(clean_param('john# $%&()+_^', PARAM_USERNAME), 'john# $%&()+_^');
777        $this->assertSame(clean_param(' john# $%&()+_^ ', PARAM_USERNAME), 'john# $%&()+_^');
778        $this->assertSame('john~doe', clean_param('john~doe', PARAM_USERNAME));
779        $this->assertSame('john´doe', clean_param('joHN´doe', PARAM_USERNAME));
780        $this->assertSame('johndoe', clean_param('johnDOE', PARAM_USERNAME));
781        $this->assertSame('johndóé', clean_param('johndóé ', PARAM_USERNAME));
782
783        $CFG->extendedusernamechars = $currentstatus;
784    }
785
786    public function test_clean_param_stringid() {
787        // Test string identifiers validation.
788        // Valid strings.
789        $this->assertSame('validstring', clean_param('validstring', PARAM_STRINGID));
790        $this->assertSame('mod/foobar:valid_capability', clean_param('mod/foobar:valid_capability', PARAM_STRINGID));
791        $this->assertSame('CZ', clean_param('CZ', PARAM_STRINGID));
792        $this->assertSame('application/vnd.ms-powerpoint', clean_param('application/vnd.ms-powerpoint', PARAM_STRINGID));
793        $this->assertSame('grade2', clean_param('grade2', PARAM_STRINGID));
794        // Invalid strings.
795        $this->assertSame('', clean_param('trailing ', PARAM_STRINGID));
796        $this->assertSame('', clean_param('space bar', PARAM_STRINGID));
797        $this->assertSame('', clean_param('0numeric', PARAM_STRINGID));
798        $this->assertSame('', clean_param('*', PARAM_STRINGID));
799        $this->assertSame('', clean_param(' ', PARAM_STRINGID));
800    }
801
802    public function test_clean_param_timezone() {
803        // Test timezone validation.
804        $testvalues = array (
805            'America/Jamaica'                => 'America/Jamaica',
806            'America/Argentina/Cordoba'      => 'America/Argentina/Cordoba',
807            'America/Port-au-Prince'         => 'America/Port-au-Prince',
808            'America/Argentina/Buenos_Aires' => 'America/Argentina/Buenos_Aires',
809            'PST8PDT'                        => 'PST8PDT',
810            'Wrong.Value'                    => '',
811            'Wrong/.Value'                   => '',
812            'Wrong(Value)'                   => '',
813            '0'                              => '0',
814            '0.0'                            => '0.0',
815            '0.5'                            => '0.5',
816            '9.0'                            => '9.0',
817            '-9.0'                           => '-9.0',
818            '+9.0'                           => '+9.0',
819            '9.5'                            => '9.5',
820            '-9.5'                           => '-9.5',
821            '+9.5'                           => '+9.5',
822            '12.0'                           => '12.0',
823            '-12.0'                          => '-12.0',
824            '+12.0'                          => '+12.0',
825            '12.5'                           => '12.5',
826            '-12.5'                          => '-12.5',
827            '+12.5'                          => '+12.5',
828            '13.0'                           => '13.0',
829            '-13.0'                          => '-13.0',
830            '+13.0'                          => '+13.0',
831            '13.5'                           => '',
832            '+13.5'                          => '',
833            '-13.5'                          => '',
834            '0.2'                            => '');
835
836        foreach ($testvalues as $testvalue => $expectedvalue) {
837            $actualvalue = clean_param($testvalue, PARAM_TIMEZONE);
838            $this->assertEquals($expectedvalue, $actualvalue);
839        }
840    }
841
842    public function test_validate_param() {
843        try {
844            $param = validate_param('11a', PARAM_INT);
845            $this->fail('invalid_parameter_exception expected');
846        } catch (moodle_exception $ex) {
847            $this->assertInstanceOf('invalid_parameter_exception', $ex);
848        }
849
850        $param = validate_param('11', PARAM_INT);
851        $this->assertSame(11, $param);
852
853        try {
854            $param = validate_param(null, PARAM_INT, false);
855            $this->fail('invalid_parameter_exception expected');
856        } catch (moodle_exception $ex) {
857            $this->assertInstanceOf('invalid_parameter_exception', $ex);
858        }
859
860        $param = validate_param(null, PARAM_INT, true);
861        $this->assertSame(null, $param);
862
863        try {
864            $param = validate_param(array(), PARAM_INT);
865            $this->fail('invalid_parameter_exception expected');
866        } catch (moodle_exception $ex) {
867            $this->assertInstanceOf('invalid_parameter_exception', $ex);
868        }
869        try {
870            $param = validate_param(new stdClass, PARAM_INT);
871            $this->fail('invalid_parameter_exception expected');
872        } catch (moodle_exception $ex) {
873            $this->assertInstanceOf('invalid_parameter_exception', $ex);
874        }
875
876        $param = validate_param('1.0', PARAM_FLOAT);
877        $this->assertSame(1.0, $param);
878
879        // Make sure valid floats do not cause exception.
880        validate_param(1.0, PARAM_FLOAT);
881        validate_param(10, PARAM_FLOAT);
882        validate_param('0', PARAM_FLOAT);
883        validate_param('119813454.545464564564546564545646556564465465456465465465645645465645645645', PARAM_FLOAT);
884        validate_param('011.1', PARAM_FLOAT);
885        validate_param('11', PARAM_FLOAT);
886        validate_param('+.1', PARAM_FLOAT);
887        validate_param('-.1', PARAM_FLOAT);
888        validate_param('1e10', PARAM_FLOAT);
889        validate_param('.1e+10', PARAM_FLOAT);
890        validate_param('1E-1', PARAM_FLOAT);
891
892        try {
893            $param = validate_param('1,2', PARAM_FLOAT);
894            $this->fail('invalid_parameter_exception expected');
895        } catch (moodle_exception $ex) {
896            $this->assertInstanceOf('invalid_parameter_exception', $ex);
897        }
898        try {
899            $param = validate_param('', PARAM_FLOAT);
900            $this->fail('invalid_parameter_exception expected');
901        } catch (moodle_exception $ex) {
902            $this->assertInstanceOf('invalid_parameter_exception', $ex);
903        }
904        try {
905            $param = validate_param('.', PARAM_FLOAT);
906            $this->fail('invalid_parameter_exception expected');
907        } catch (moodle_exception $ex) {
908            $this->assertInstanceOf('invalid_parameter_exception', $ex);
909        }
910        try {
911            $param = validate_param('e10', PARAM_FLOAT);
912            $this->fail('invalid_parameter_exception expected');
913        } catch (moodle_exception $ex) {
914            $this->assertInstanceOf('invalid_parameter_exception', $ex);
915        }
916        try {
917            $param = validate_param('abc', PARAM_FLOAT);
918            $this->fail('invalid_parameter_exception expected');
919        } catch (moodle_exception $ex) {
920            $this->assertInstanceOf('invalid_parameter_exception', $ex);
921        }
922    }
923
924    public function test_shorten_text_no_tags_already_short_enough() {
925        // ......12345678901234567890123456.
926        $text = "short text already no tags";
927        $this->assertSame($text, shorten_text($text));
928    }
929
930    public function test_shorten_text_with_tags_already_short_enough() {
931        // .........123456...7890....12345678.......901234567.
932        $text = "<p>short <b>text</b> already</p><p>with tags</p>";
933        $this->assertSame($text, shorten_text($text));
934    }
935
936    public function test_shorten_text_no_tags_needs_shortening() {
937        // Default truncation is after 30 chars, but allowing 3 for the final '...'.
938        // ......12345678901234567890123456789023456789012345678901234.
939        $text = "long text without any tags blah de blah blah blah what";
940        $this->assertSame('long text without any tags ...', shorten_text($text));
941    }
942
943    public function test_shorten_text_with_tags_needs_shortening() {
944        // .......................................123456789012345678901234567890...
945        $text = "<div class='frog'><p><blockquote>Long text with tags that will ".
946            "be chopped off but <b>should be added back again</b></blockquote></p></div>";
947        $this->assertEquals("<div class='frog'><p><blockquote>Long text with " .
948            "tags that ...</blockquote></p></div>", shorten_text($text));
949    }
950
951    public function test_shorten_text_with_tags_and_html_comment() {
952        $text = "<div class='frog'><p><blockquote><!--[if !IE]><!-->Long text with ".
953            "tags that will<!--<![endif]--> ".
954            "be chopped off but <b>should be added back again</b></blockquote></p></div>";
955        $this->assertEquals("<div class='frog'><p><blockquote><!--[if !IE]><!-->Long text with " .
956            "tags that ...<!--<![endif]--></blockquote></p></div>", shorten_text($text));
957    }
958
959    public function test_shorten_text_with_entities() {
960        // Remember to allow 3 chars for the final '...'.
961        // ......123456789012345678901234567_____890...
962        $text = "some text which shouldn't &nbsp; break there";
963        $this->assertSame("some text which shouldn't &nbsp; ...", shorten_text($text, 31));
964        $this->assertSame("some text which shouldn't &nbsp;...", shorten_text($text, 30));
965        $this->assertSame("some text which shouldn't ...", shorten_text($text, 29));
966    }
967
968    public function test_shorten_text_known_tricky_case() {
969        // This case caused a bug up to 1.9.5
970        // ..........123456789012345678901234567890123456789.....0_____1___2___...
971        $text = "<h3>standard 'break-out' sub groups in TGs?</h3>&nbsp;&lt;&lt;There are several";
972        $this->assertSame("<h3>standard 'break-out' sub groups in ...</h3>",
973            shorten_text($text, 41));
974        $this->assertSame("<h3>standard 'break-out' sub groups in TGs?...</h3>",
975            shorten_text($text, 42));
976        $this->assertSame("<h3>standard 'break-out' sub groups in TGs?</h3>&nbsp;...",
977            shorten_text($text, 43));
978    }
979
980    public function test_shorten_text_no_spaces() {
981        // ..........123456789.
982        $text = "<h1>123456789</h1>"; // A string with no convenient breaks.
983        $this->assertSame("<h1>12345...</h1>", shorten_text($text, 8));
984    }
985
986    public function test_shorten_text_utf8_european() {
987        // Text without tags.
988        // ......123456789012345678901234567.
989        $text = "Žluťoučký koníček přeskočil";
990        $this->assertSame($text, shorten_text($text)); // 30 chars by default.
991        $this->assertSame("Žluťoučký koníče...", shorten_text($text, 19, true));
992        $this->assertSame("Žluťoučký ...", shorten_text($text, 19, false));
993        // And try it with 2-less (that are, in bytes, the middle of a sequence).
994        $this->assertSame("Žluťoučký koní...", shorten_text($text, 17, true));
995        $this->assertSame("Žluťoučký ...", shorten_text($text, 17, false));
996
997        // .........123456789012345678...901234567....89012345.
998        $text = "<p>Žluťoučký koníček <b>přeskočil</b> potůček</p>";
999        $this->assertSame($text, shorten_text($text, 60));
1000        $this->assertSame("<p>Žluťoučký koníček ...</p>", shorten_text($text, 21));
1001        $this->assertSame("<p>Žluťoučký koníče...</p>", shorten_text($text, 19, true));
1002        $this->assertSame("<p>Žluťoučký ...</p>", shorten_text($text, 19, false));
1003        // And try it with 2 fewer (that are, in bytes, the middle of a sequence).
1004        $this->assertSame("<p>Žluťoučký koní...</p>", shorten_text($text, 17, true));
1005        $this->assertSame("<p>Žluťoučký ...</p>", shorten_text($text, 17, false));
1006        // And try over one tag (start/end), it does proper text len.
1007        $this->assertSame("<p>Žluťoučký koníček <b>př...</b></p>", shorten_text($text, 23, true));
1008        $this->assertSame("<p>Žluťoučký koníček <b>přeskočil</b> pot...</p>", shorten_text($text, 34, true));
1009        // And in the middle of one tag.
1010        $this->assertSame("<p>Žluťoučký koníček <b>přeskočil...</b></p>", shorten_text($text, 30, true));
1011    }
1012
1013    public function test_shorten_text_utf8_oriental() {
1014        // Japanese
1015        // text without tags
1016        // ......123456789012345678901234.
1017        $text = '言語設定言語設定abcdefghijkl';
1018        $this->assertSame($text, shorten_text($text)); // 30 chars by default.
1019        $this->assertSame("言語設定言語...", shorten_text($text, 9, true));
1020        $this->assertSame("言語設定言語...", shorten_text($text, 9, false));
1021        $this->assertSame("言語設定言語設定ab...", shorten_text($text, 13, true));
1022        $this->assertSame("言語設定言語設定...", shorten_text($text, 13, false));
1023
1024        // Chinese
1025        // text without tags
1026        // ......123456789012345678901234.
1027        $text = '简体中文简体中文abcdefghijkl';
1028        $this->assertSame($text, shorten_text($text)); // 30 chars by default.
1029        $this->assertSame("简体中文简体...", shorten_text($text, 9, true));
1030        $this->assertSame("简体中文简体...", shorten_text($text, 9, false));
1031        $this->assertSame("简体中文简体中文ab...", shorten_text($text, 13, true));
1032        $this->assertSame("简体中文简体中文...", shorten_text($text, 13, false));
1033    }
1034
1035    public function test_shorten_text_multilang() {
1036        // This is not necessaryily specific to multilang. The issue is really
1037        // tags with attributes, where before we were generating invalid HTML
1038        // output like shorten_text('<span id="x" class="y">A</span> B', 1)
1039        // returning '<span id="x" ...</span>'. It is just that multilang
1040        // requires the sort of HTML that is quite likely to trigger this.
1041        // ........................................1...
1042        $text = '<span lang="en" class="multilang">A</span>' .
1043                '<span lang="fr" class="multilang">B</span>';
1044        $this->assertSame('<span lang="en" class="multilang">...</span>',
1045                shorten_text($text, 1));
1046    }
1047
1048    /**
1049     * Provider for long filenames and its expected result, with and without hash.
1050     *
1051     * @return array of ($filename, $length, $expected, $includehash)
1052     */
1053    public function shorten_filename_provider() {
1054        $filename = 'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium totam rem';
1055        $shortfilename = 'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque';
1056
1057        return [
1058            'More than 100 characters' => [
1059                $filename,
1060                null,
1061                'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium tot',
1062                false,
1063            ],
1064            'More than 100 characters with hash' => [
1065                $filename,
1066                null,
1067                'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque l - 3bec1da8b8',
1068                true,
1069            ],
1070            'More than 100 characters with extension' => [
1071                "{$filename}.zip",
1072                null,
1073                'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium tot.zip',
1074                false,
1075            ],
1076            'More than 100 characters with extension and hash' => [
1077                "{$filename}.zip",
1078                null,
1079                'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque l - 3bec1da8b8.zip',
1080                true,
1081            ],
1082            'Limit filename to 50 chars' => [
1083                $filename,
1084                50,
1085                'sed ut perspiciatis unde omnis iste natus error si',
1086                false,
1087            ],
1088            'Limit filename to 50 chars with hash' => [
1089                $filename,
1090                50,
1091                'sed ut perspiciatis unde omnis iste n - 3bec1da8b8',
1092                true,
1093            ],
1094            'Limit filename to 50 chars with extension' => [
1095                "{$filename}.zip",
1096                50,
1097                'sed ut perspiciatis unde omnis iste natus error si.zip',
1098                false,
1099            ],
1100            'Limit filename to 50 chars with extension and hash' => [
1101                "{$filename}.zip",
1102                50,
1103                'sed ut perspiciatis unde omnis iste n - 3bec1da8b8.zip',
1104                true,
1105            ],
1106            'Test filename that contains less than 100 characters' => [
1107                $shortfilename,
1108                null,
1109                $shortfilename,
1110                false,
1111            ],
1112            'Test filename that contains less than 100 characters and hash' => [
1113                $shortfilename,
1114                null,
1115                $shortfilename,
1116                true,
1117            ],
1118            'Test filename that contains less than 100 characters with extension' => [
1119                "{$shortfilename}.zip",
1120                null,
1121                "{$shortfilename}.zip",
1122                false,
1123            ],
1124            'Test filename that contains less than 100 characters with extension and hash' => [
1125                "{$shortfilename}.zip",
1126                null,
1127                "{$shortfilename}.zip",
1128                true,
1129            ],
1130        ];
1131    }
1132
1133    /**
1134     * Test the {@link shorten_filename()} method.
1135     *
1136     * @dataProvider shorten_filename_provider
1137     *
1138     * @param string $filename
1139     * @param int $length
1140     * @param string $expected
1141     * @param boolean $includehash
1142     */
1143    public function test_shorten_filename($filename, $length, $expected, $includehash) {
1144        if (null === $length) {
1145            $length = MAX_FILENAME_SIZE;
1146        }
1147
1148        $this->assertSame($expected, shorten_filename($filename, $length, $includehash));
1149    }
1150
1151    /**
1152     * Provider for long filenames and its expected result, with and without hash.
1153     *
1154     * @return array of ($filename, $length, $expected, $includehash)
1155     */
1156    public function shorten_filenames_provider() {
1157        $shortfilename = 'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque';
1158        $longfilename = 'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium totam rem';
1159        $extfilename = $longfilename.'.zip';
1160        $expected = 'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium tot';
1161        $expectedwithhash = 'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque l - 3bec1da8b8';
1162        $expectedext = 'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium tot.zip';
1163        $expectedextwithhash = 'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque l - 3bec1da8b8.zip';
1164        $expected50 = 'sed ut perspiciatis unde omnis iste natus error si';
1165        $expected50withhash = 'sed ut perspiciatis unde omnis iste n - 3bec1da8b8';
1166        $expected50ext = 'sed ut perspiciatis unde omnis iste natus error si.zip';
1167        $expected50extwithhash = 'sed ut perspiciatis unde omnis iste n - 3bec1da8b8.zip';
1168        $expected50short = 'sed ut perspiciatis unde omnis iste n - 5fb6543490';
1169
1170        return [
1171            'Empty array without hash' => [
1172                [],
1173                null,
1174                [],
1175                false,
1176            ],
1177            'Empty array with hash' => [
1178                [],
1179                null,
1180                [],
1181                true,
1182            ],
1183            'Array with less than 100 characters' => [
1184                [$shortfilename, $shortfilename, $shortfilename],
1185                null,
1186                [$shortfilename, $shortfilename, $shortfilename],
1187                false,
1188            ],
1189            'Array with more than 100 characters without hash' => [
1190                [$longfilename, $longfilename, $longfilename],
1191                null,
1192                [$expected, $expected, $expected],
1193                false,
1194            ],
1195            'Array with more than 100 characters with hash' => [
1196                [$longfilename, $longfilename, $longfilename],
1197                null,
1198                [$expectedwithhash, $expectedwithhash, $expectedwithhash],
1199                true,
1200            ],
1201            'Array with more than 100 characters with extension' => [
1202                [$extfilename, $extfilename, $extfilename],
1203                null,
1204                [$expectedext, $expectedext, $expectedext],
1205                false,
1206            ],
1207            'Array with more than 100 characters with extension and hash' => [
1208                [$extfilename, $extfilename, $extfilename],
1209                null,
1210                [$expectedextwithhash, $expectedextwithhash, $expectedextwithhash],
1211                true,
1212            ],
1213            'Array with more than 100 characters mix (short, long, with extension) without hash' => [
1214                [$shortfilename, $longfilename, $extfilename],
1215                null,
1216                [$shortfilename, $expected, $expectedext],
1217                false,
1218            ],
1219            'Array with more than 100 characters mix (short, long, with extension) with hash' => [
1220                [$shortfilename, $longfilename, $extfilename],
1221                null,
1222                [$shortfilename, $expectedwithhash, $expectedextwithhash],
1223                true,
1224            ],
1225            'Array with less than 50 characters without hash' => [
1226                [$longfilename, $longfilename, $longfilename],
1227                50,
1228                [$expected50, $expected50, $expected50],
1229                false,
1230            ],
1231            'Array with less than 50 characters with hash' => [
1232                [$longfilename, $longfilename, $longfilename],
1233                50,
1234                [$expected50withhash, $expected50withhash, $expected50withhash],
1235                true,
1236            ],
1237            'Array with less than 50 characters with extension' => [
1238                [$extfilename, $extfilename, $extfilename],
1239                50,
1240                [$expected50ext, $expected50ext, $expected50ext],
1241                false,
1242            ],
1243            'Array with less than 50 characters with extension and hash' => [
1244                [$extfilename, $extfilename, $extfilename],
1245                50,
1246                [$expected50extwithhash, $expected50extwithhash, $expected50extwithhash],
1247                true,
1248            ],
1249            'Array with less than 50 characters mix (short, long, with extension) without hash' => [
1250                [$shortfilename, $longfilename, $extfilename],
1251                50,
1252                [$expected50, $expected50, $expected50ext],
1253                false,
1254            ],
1255            'Array with less than 50 characters mix (short, long, with extension) with hash' => [
1256                [$shortfilename, $longfilename, $extfilename],
1257                50,
1258                [$expected50short, $expected50withhash, $expected50extwithhash],
1259                true,
1260            ],
1261        ];
1262    }
1263
1264    /**
1265     * Test the {@link shorten_filenames()} method.
1266     *
1267     * @dataProvider shorten_filenames_provider
1268     *
1269     * @param string $filenames
1270     * @param int $length
1271     * @param string $expected
1272     * @param boolean $includehash
1273     */
1274    public function test_shorten_filenames($filenames, $length, $expected, $includehash) {
1275        if (null === $length) {
1276            $length = MAX_FILENAME_SIZE;
1277        }
1278
1279        $this->assertSame($expected, shorten_filenames($filenames, $length, $includehash));
1280    }
1281
1282    public function test_usergetdate() {
1283        global $USER, $CFG, $DB;
1284        $this->resetAfterTest();
1285
1286        $this->setAdminUser();
1287
1288        $USER->timezone = 2;// Set the timezone to a known state.
1289
1290        $ts = 1261540267; // The time this function was created.
1291
1292        $arr = usergetdate($ts, 1); // Specify the timezone as an argument.
1293        $arr = array_values($arr);
1294
1295        list($seconds, $minutes, $hours, $mday, $wday, $mon, $year, $yday, $weekday, $month) = $arr;
1296        $this->assertSame(7, $seconds);
1297        $this->assertSame(51, $minutes);
1298        $this->assertSame(4, $hours);
1299        $this->assertSame(23, $mday);
1300        $this->assertSame(3, $wday);
1301        $this->assertSame(12, $mon);
1302        $this->assertSame(2009, $year);
1303        $this->assertSame(356, $yday);
1304        $this->assertSame('Wednesday', $weekday);
1305        $this->assertSame('December', $month);
1306        $arr = usergetdate($ts); // Gets the timezone from the $USER object.
1307        $arr = array_values($arr);
1308
1309        list($seconds, $minutes, $hours, $mday, $wday, $mon, $year, $yday, $weekday, $month) = $arr;
1310        $this->assertSame(7, $seconds);
1311        $this->assertSame(51, $minutes);
1312        $this->assertSame(5, $hours);
1313        $this->assertSame(23, $mday);
1314        $this->assertSame(3, $wday);
1315        $this->assertSame(12, $mon);
1316        $this->assertSame(2009, $year);
1317        $this->assertSame(356, $yday);
1318        $this->assertSame('Wednesday', $weekday);
1319        $this->assertSame('December', $month);
1320
1321        // Edge cases - 0 and null - they all mean 1st Jan 1970. Null shows debugging message.
1322        $this->assertSame(1970, usergetdate(0)['year']);
1323        $this->assertDebuggingNotCalled();
1324        $this->assertSame(1970, usergetdate(null)['year']);
1325        $this->assertDebuggingCalled(null, DEBUG_DEVELOPER);
1326    }
1327
1328    public function test_mark_user_preferences_changed() {
1329        $this->resetAfterTest();
1330        $otheruser = $this->getDataGenerator()->create_user();
1331        $otheruserid = $otheruser->id;
1332
1333        set_cache_flag('userpreferenceschanged', $otheruserid, null);
1334        mark_user_preferences_changed($otheruserid);
1335
1336        $this->assertEquals(get_cache_flag('userpreferenceschanged', $otheruserid, time()-10), 1);
1337        set_cache_flag('userpreferenceschanged', $otheruserid, null);
1338    }
1339
1340    public function test_check_user_preferences_loaded() {
1341        global $DB;
1342        $this->resetAfterTest();
1343
1344        $otheruser = $this->getDataGenerator()->create_user();
1345        $otheruserid = $otheruser->id;
1346
1347        $DB->delete_records('user_preferences', array('userid'=>$otheruserid));
1348        set_cache_flag('userpreferenceschanged', $otheruserid, null);
1349
1350        $user = new stdClass();
1351        $user->id = $otheruserid;
1352
1353        // Load.
1354        check_user_preferences_loaded($user);
1355        $this->assertTrue(isset($user->preference));
1356        $this->assertTrue(is_array($user->preference));
1357        $this->assertArrayHasKey('_lastloaded', $user->preference);
1358        $this->assertCount(1, $user->preference);
1359
1360        // Add preference via direct call.
1361        $DB->insert_record('user_preferences', array('name'=>'xxx', 'value'=>'yyy', 'userid'=>$user->id));
1362
1363        // No cache reload yet.
1364        check_user_preferences_loaded($user);
1365        $this->assertCount(1, $user->preference);
1366
1367        // Forced reloading of cache.
1368        unset($user->preference);
1369        check_user_preferences_loaded($user);
1370        $this->assertCount(2, $user->preference);
1371        $this->assertSame('yyy', $user->preference['xxx']);
1372
1373        // Add preference via direct call.
1374        $DB->insert_record('user_preferences', array('name'=>'aaa', 'value'=>'bbb', 'userid'=>$user->id));
1375
1376        // Test timeouts and modifications from different session.
1377        set_cache_flag('userpreferenceschanged', $user->id, 1, time() + 1000);
1378        $user->preference['_lastloaded'] = $user->preference['_lastloaded'] - 20;
1379        check_user_preferences_loaded($user);
1380        $this->assertCount(2, $user->preference);
1381        check_user_preferences_loaded($user, 10);
1382        $this->assertCount(3, $user->preference);
1383        $this->assertSame('bbb', $user->preference['aaa']);
1384        set_cache_flag('userpreferenceschanged', $user->id, null);
1385    }
1386
1387    public function test_set_user_preference() {
1388        global $DB, $USER;
1389        $this->resetAfterTest();
1390
1391        $this->setAdminUser();
1392
1393        $otheruser = $this->getDataGenerator()->create_user();
1394        $otheruserid = $otheruser->id;
1395
1396        $DB->delete_records('user_preferences', array('userid'=>$otheruserid));
1397        set_cache_flag('userpreferenceschanged', $otheruserid, null);
1398
1399        $user = new stdClass();
1400        $user->id = $otheruserid;
1401
1402        set_user_preference('aaa', 'bbb', $otheruserid);
1403        $this->assertSame('bbb', $DB->get_field('user_preferences', 'value', array('userid'=>$otheruserid, 'name'=>'aaa')));
1404        $this->assertSame('bbb', get_user_preferences('aaa', null, $otheruserid));
1405
1406        set_user_preference('xxx', 'yyy', $user);
1407        $this->assertSame('yyy', $DB->get_field('user_preferences', 'value', array('userid'=>$otheruserid, 'name'=>'xxx')));
1408        $this->assertSame('yyy', get_user_preferences('xxx', null, $otheruserid));
1409        $this->assertTrue(is_array($user->preference));
1410        $this->assertSame('bbb', $user->preference['aaa']);
1411        $this->assertSame('yyy', $user->preference['xxx']);
1412
1413        set_user_preference('xxx', null, $user);
1414        $this->assertFalse($DB->get_field('user_preferences', 'value', array('userid'=>$otheruserid, 'name'=>'xxx')));
1415        $this->assertNull(get_user_preferences('xxx', null, $otheruserid));
1416
1417        set_user_preference('ooo', true, $user);
1418        $prefs = get_user_preferences(null, null, $otheruserid);
1419        $this->assertSame($user->preference['aaa'], $prefs['aaa']);
1420        $this->assertSame($user->preference['ooo'], $prefs['ooo']);
1421        $this->assertSame('1', $prefs['ooo']);
1422
1423        set_user_preference('null', 0, $user);
1424        $this->assertSame('0', get_user_preferences('null', null, $otheruserid));
1425
1426        $this->assertSame('lala', get_user_preferences('undefined', 'lala', $otheruserid));
1427
1428        $DB->delete_records('user_preferences', array('userid'=>$otheruserid));
1429        set_cache_flag('userpreferenceschanged', $otheruserid, null);
1430
1431        // Test $USER default.
1432        set_user_preference('_test_user_preferences_pref', 'ok');
1433        $this->assertSame('ok', $USER->preference['_test_user_preferences_pref']);
1434        unset_user_preference('_test_user_preferences_pref');
1435        $this->assertTrue(!isset($USER->preference['_test_user_preferences_pref']));
1436
1437        // Test 1333 char values (no need for unicode, there are already tests for that in DB tests).
1438        $longvalue = str_repeat('a', 1333);
1439        set_user_preference('_test_long_user_preference', $longvalue);
1440        $this->assertEquals($longvalue, get_user_preferences('_test_long_user_preference'));
1441        $this->assertEquals($longvalue,
1442            $DB->get_field('user_preferences', 'value', array('userid' => $USER->id, 'name' => '_test_long_user_preference')));
1443
1444        // Test > 1333 char values, coding_exception expected.
1445        $longvalue = str_repeat('a', 1334);
1446        try {
1447            set_user_preference('_test_long_user_preference', $longvalue);
1448            $this->fail('Exception expected - longer than 1333 chars not allowed as preference value');
1449        } catch (moodle_exception $ex) {
1450            $this->assertInstanceOf('coding_exception', $ex);
1451        }
1452
1453        // Test invalid params.
1454        try {
1455            set_user_preference('_test_user_preferences_pref', array());
1456            $this->fail('Exception expected - array not valid preference value');
1457        } catch (moodle_exception $ex) {
1458            $this->assertInstanceOf('coding_exception', $ex);
1459        }
1460        try {
1461            set_user_preference('_test_user_preferences_pref', new stdClass);
1462            $this->fail('Exception expected - class not valid preference value');
1463        } catch (moodle_exception $ex) {
1464            $this->assertInstanceOf('coding_exception', $ex);
1465        }
1466        try {
1467            set_user_preference('_test_user_preferences_pref', 1, array('xx' => 1));
1468            $this->fail('Exception expected - user instance expected');
1469        } catch (moodle_exception $ex) {
1470            $this->assertInstanceOf('coding_exception', $ex);
1471        }
1472        try {
1473            set_user_preference('_test_user_preferences_pref', 1, 'abc');
1474            $this->fail('Exception expected - user instance expected');
1475        } catch (moodle_exception $ex) {
1476            $this->assertInstanceOf('coding_exception', $ex);
1477        }
1478        try {
1479            set_user_preference('', 1);
1480            $this->fail('Exception expected - invalid name accepted');
1481        } catch (moodle_exception $ex) {
1482            $this->assertInstanceOf('coding_exception', $ex);
1483        }
1484        try {
1485            set_user_preference('1', 1);
1486            $this->fail('Exception expected - invalid name accepted');
1487        } catch (moodle_exception $ex) {
1488            $this->assertInstanceOf('coding_exception', $ex);
1489        }
1490    }
1491
1492    public function test_set_user_preference_for_current_user() {
1493        global $USER;
1494        $this->resetAfterTest();
1495        $this->setAdminUser();
1496
1497        set_user_preference('test_pref', 2);
1498        set_user_preference('test_pref', 1, $USER->id);
1499        $this->assertEquals(1, get_user_preferences('test_pref'));
1500    }
1501
1502    public function test_unset_user_preference_for_current_user() {
1503        global $USER;
1504        $this->resetAfterTest();
1505        $this->setAdminUser();
1506
1507        set_user_preference('test_pref', 1);
1508        unset_user_preference('test_pref', $USER->id);
1509        $this->assertNull(get_user_preferences('test_pref'));
1510    }
1511
1512    /**
1513     * Test essential features implementation of {@link get_extra_user_fields()} as the admin user with all capabilities.
1514     *
1515     * @deprecated since Moodle 3.11 MDL-45242
1516     */
1517    public function test_get_extra_user_fields_essentials() {
1518        global $CFG, $USER, $DB;
1519        $this->resetAfterTest();
1520
1521        $this->setAdminUser();
1522        $context = context_system::instance();
1523
1524        // No fields.
1525        $CFG->showuseridentity = '';
1526        $this->assertEquals(array(), get_extra_user_fields($context));
1527
1528        // One field.
1529        $CFG->showuseridentity = 'frog';
1530        $this->assertEquals(array('frog'), get_extra_user_fields($context));
1531
1532        // Two fields.
1533        $CFG->showuseridentity = 'frog,zombie';
1534        $this->assertEquals(array('frog', 'zombie'), get_extra_user_fields($context));
1535
1536        // No fields, except.
1537        $CFG->showuseridentity = '';
1538        $this->assertEquals(array(), get_extra_user_fields($context, array('frog')));
1539
1540        // One field.
1541        $CFG->showuseridentity = 'frog';
1542        $this->assertEquals(array(), get_extra_user_fields($context, array('frog')));
1543
1544        // Two fields.
1545        $CFG->showuseridentity = 'frog,zombie';
1546        $this->assertEquals(array('zombie'), get_extra_user_fields($context, array('frog')));
1547
1548        $this->assertDebuggingCalledCount(6);
1549    }
1550
1551    /**
1552     * Prepare environment for couple of tests related to permission checks in {@link get_extra_user_fields()}.
1553     *
1554     * @return stdClass
1555     * @deprecated since Moodle 3.11 MDL-45242
1556     */
1557    protected function environment_for_get_extra_user_fields_tests() {
1558        global $CFG, $DB;
1559
1560        $CFG->showuseridentity = 'idnumber,country,city';
1561        $CFG->hiddenuserfields = 'country,city';
1562
1563        $env = new stdClass();
1564
1565        $env->course = $this->getDataGenerator()->create_course();
1566        $env->coursecontext = context_course::instance($env->course->id);
1567
1568        $env->teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));
1569        $env->studentrole = $DB->get_record('role', array('shortname' => 'student'));
1570        $env->managerrole = $DB->get_record('role', array('shortname' => 'manager'));
1571
1572        $env->student = $this->getDataGenerator()->create_user();
1573        $env->teacher = $this->getDataGenerator()->create_user();
1574        $env->manager = $this->getDataGenerator()->create_user();
1575
1576        role_assign($env->studentrole->id, $env->student->id, $env->coursecontext->id);
1577        role_assign($env->teacherrole->id, $env->teacher->id, $env->coursecontext->id);
1578        role_assign($env->managerrole->id, $env->manager->id, SYSCONTEXTID);
1579
1580        return $env;
1581    }
1582
1583    /**
1584     * No identity fields shown to student user (no permission to view identity fields).
1585     *
1586     * @deprecated since Moodle 3.11 MDL-45242
1587     */
1588    public function test_get_extra_user_fields_no_access() {
1589
1590        $this->resetAfterTest();
1591        $env = $this->environment_for_get_extra_user_fields_tests();
1592        $this->setUser($env->student);
1593
1594        $this->assertEquals(array(), get_extra_user_fields($env->coursecontext));
1595        $this->assertEquals(array(), get_extra_user_fields(context_system::instance()));
1596
1597        $this->assertDebuggingCalledCount(2);
1598    }
1599
1600    /**
1601     * Teacher can see students' identity fields only within the course.
1602     *
1603     * @deprecated since Moodle 3.11 MDL-45242
1604     */
1605    public function test_get_extra_user_fields_course_only_access() {
1606
1607        $this->resetAfterTest();
1608        $env = $this->environment_for_get_extra_user_fields_tests();
1609        $this->setUser($env->teacher);
1610
1611        $this->assertEquals(array('idnumber', 'country', 'city'), get_extra_user_fields($env->coursecontext));
1612        $this->assertEquals(array(), get_extra_user_fields(context_system::instance()));
1613
1614        $this->assertDebuggingCalledCount(2);
1615    }
1616
1617    /**
1618     * Teacher can be prevented from seeing students' identity fields even within the course.
1619     *
1620     * @deprecated since Moodle 3.11 MDL-45242
1621     */
1622    public function test_get_extra_user_fields_course_prevented_access() {
1623
1624        $this->resetAfterTest();
1625        $env = $this->environment_for_get_extra_user_fields_tests();
1626        $this->setUser($env->teacher);
1627
1628        assign_capability('moodle/course:viewhiddenuserfields', CAP_PREVENT, $env->teacherrole->id, $env->coursecontext->id);
1629        $this->assertEquals(array('idnumber'), get_extra_user_fields($env->coursecontext));
1630
1631        $this->assertDebuggingCalledCount(1);
1632    }
1633
1634    /**
1635     * Manager can see students' identity fields anywhere.
1636     *
1637     * @deprecated since Moodle 3.11 MDL-45242
1638     */
1639    public function test_get_extra_user_fields_anywhere_access() {
1640
1641        $this->resetAfterTest();
1642        $env = $this->environment_for_get_extra_user_fields_tests();
1643        $this->setUser($env->manager);
1644
1645        $this->assertEquals(array('idnumber', 'country', 'city'), get_extra_user_fields($env->coursecontext));
1646        $this->assertEquals(array('idnumber', 'country', 'city'), get_extra_user_fields(context_system::instance()));
1647
1648        $this->assertDebuggingCalledCount(2);
1649    }
1650
1651    /**
1652     * Manager can be prevented from seeing hidden fields outside the course.
1653     *
1654     * @deprecated since Moodle 3.11 MDL-45242
1655     */
1656    public function test_get_extra_user_fields_schismatic_access() {
1657
1658        $this->resetAfterTest();
1659        $env = $this->environment_for_get_extra_user_fields_tests();
1660        $this->setUser($env->manager);
1661
1662        assign_capability('moodle/user:viewhiddendetails', CAP_PREVENT, $env->managerrole->id, SYSCONTEXTID, true);
1663        $this->assertEquals(array('idnumber'), get_extra_user_fields(context_system::instance()));
1664        // Note that inside the course, the manager can still see the hidden identifiers as this is currently
1665        // controlled by a separate capability for legacy reasons.
1666        $this->assertEquals(array('idnumber', 'country', 'city'), get_extra_user_fields($env->coursecontext));
1667
1668        $this->assertDebuggingCalledCount(2);
1669    }
1670
1671    /**
1672     * Two capabilities must be currently set to prevent manager from seeing hidden fields.
1673     *
1674     * @deprecated since Moodle 3.11 MDL-45242
1675     */
1676    public function test_get_extra_user_fields_hard_to_prevent_access() {
1677
1678        $this->resetAfterTest();
1679        $env = $this->environment_for_get_extra_user_fields_tests();
1680        $this->setUser($env->manager);
1681
1682        assign_capability('moodle/user:viewhiddendetails', CAP_PREVENT, $env->managerrole->id, SYSCONTEXTID, true);
1683        assign_capability('moodle/course:viewhiddenuserfields', CAP_PREVENT, $env->managerrole->id, SYSCONTEXTID, true);
1684
1685        $this->assertEquals(array('idnumber'), get_extra_user_fields(context_system::instance()));
1686        $this->assertEquals(array('idnumber'), get_extra_user_fields($env->coursecontext));
1687
1688        $this->assertDebuggingCalledCount(2);
1689    }
1690
1691    /**
1692     * Tests get_extra_user_fields_sql.
1693     *
1694     * @deprecated since Moodle 3.11 MDL-45242
1695     */
1696    public function test_get_extra_user_fields_sql() {
1697        global $CFG, $USER, $DB;
1698        $this->resetAfterTest();
1699
1700        $this->setAdminUser();
1701
1702        $context = context_system::instance();
1703
1704        // No fields.
1705        $CFG->showuseridentity = '';
1706        $this->assertSame('', get_extra_user_fields_sql($context));
1707
1708        // One field.
1709        $CFG->showuseridentity = 'frog';
1710        $this->assertSame(', frog', get_extra_user_fields_sql($context));
1711
1712        // Two fields with table prefix.
1713        $CFG->showuseridentity = 'frog,zombie';
1714        $this->assertSame(', u1.frog, u1.zombie', get_extra_user_fields_sql($context, 'u1'));
1715
1716        // Two fields with field prefix.
1717        $CFG->showuseridentity = 'frog,zombie';
1718        $this->assertSame(', frog AS u_frog, zombie AS u_zombie',
1719            get_extra_user_fields_sql($context, '', 'u_'));
1720
1721        // One field excluded.
1722        $CFG->showuseridentity = 'frog';
1723        $this->assertSame('', get_extra_user_fields_sql($context, '', '', array('frog')));
1724
1725        // Two fields, one excluded, table+field prefix.
1726        $CFG->showuseridentity = 'frog,zombie';
1727        $this->assertEquals(', u1.zombie AS u_zombie',
1728            get_extra_user_fields_sql($context, 'u1', 'u_', array('frog')));
1729
1730        $this->assertDebuggingCalledCount(6);
1731    }
1732
1733    /**
1734     * Test some critical TZ/DST.
1735     *
1736     * This method tests some special TZ/DST combinations that were fixed
1737     * by MDL-38999. The tests are done by comparing the results of the
1738     * output using Moodle TZ/DST support and PHP native one.
1739     *
1740     * Note: If you don't trust PHP TZ/DST support, can verify the
1741     * harcoded expectations below with:
1742     * http://www.tools4noobs.com/online_tools/unix_timestamp_to_datetime/
1743     */
1744    public function test_some_moodle_special_dst() {
1745        $stamp = 1365386400; // 2013/04/08 02:00:00 GMT/UTC.
1746
1747        // In Europe/Tallinn it was 2013/04/08 05:00:00.
1748        $expectation = '2013/04/08 05:00:00';
1749        $phpdt = DateTime::createFromFormat('U', $stamp, new DateTimeZone('UTC'));
1750        $phpdt->setTimezone(new DateTimeZone('Europe/Tallinn'));
1751        $phpres = $phpdt->format('Y/m/d H:i:s'); // PHP result.
1752        $moodleres = userdate($stamp, '%Y/%m/%d %H:%M:%S', 'Europe/Tallinn', false); // Moodle result.
1753        $this->assertSame($expectation, $phpres);
1754        $this->assertSame($expectation, $moodleres);
1755
1756        // In St. Johns it was 2013/04/07 23:30:00.
1757        $expectation = '2013/04/07 23:30:00';
1758        $phpdt = DateTime::createFromFormat('U', $stamp, new DateTimeZone('UTC'));
1759        $phpdt->setTimezone(new DateTimeZone('America/St_Johns'));
1760        $phpres = $phpdt->format('Y/m/d H:i:s'); // PHP result.
1761        $moodleres = userdate($stamp, '%Y/%m/%d %H:%M:%S', 'America/St_Johns', false); // Moodle result.
1762        $this->assertSame($expectation, $phpres);
1763        $this->assertSame($expectation, $moodleres);
1764
1765        $stamp = 1383876000; // 2013/11/08 02:00:00 GMT/UTC.
1766
1767        // In Europe/Tallinn it was 2013/11/08 04:00:00.
1768        $expectation = '2013/11/08 04:00:00';
1769        $phpdt = DateTime::createFromFormat('U', $stamp, new DateTimeZone('UTC'));
1770        $phpdt->setTimezone(new DateTimeZone('Europe/Tallinn'));
1771        $phpres = $phpdt->format('Y/m/d H:i:s'); // PHP result.
1772        $moodleres = userdate($stamp, '%Y/%m/%d %H:%M:%S', 'Europe/Tallinn', false); // Moodle result.
1773        $this->assertSame($expectation, $phpres);
1774        $this->assertSame($expectation, $moodleres);
1775
1776        // In St. Johns it was 2013/11/07 22:30:00.
1777        $expectation = '2013/11/07 22:30:00';
1778        $phpdt = DateTime::createFromFormat('U', $stamp, new DateTimeZone('UTC'));
1779        $phpdt->setTimezone(new DateTimeZone('America/St_Johns'));
1780        $phpres = $phpdt->format('Y/m/d H:i:s'); // PHP result.
1781        $moodleres = userdate($stamp, '%Y/%m/%d %H:%M:%S', 'America/St_Johns', false); // Moodle result.
1782        $this->assertSame($expectation, $phpres);
1783        $this->assertSame($expectation, $moodleres);
1784    }
1785
1786    public function test_userdate() {
1787        global $USER, $CFG, $DB;
1788        $this->resetAfterTest();
1789
1790        $this->setAdminUser();
1791
1792        $testvalues = array(
1793            array(
1794                'time' => '1309514400',
1795                'usertimezone' => 'America/Moncton',
1796                'timezone' => '0.0', // No dst offset.
1797                'expectedoutput' => 'Friday, 1 July 2011, 10:00 AM',
1798                'expectedoutputhtml' => '<time datetime="2011-07-01T07:00:00-03:00">Friday, 1 July 2011, 10:00 AM</time>'
1799            ),
1800            array(
1801                'time' => '1309514400',
1802                'usertimezone' => 'America/Moncton',
1803                'timezone' => '99', // Dst offset and timezone offset.
1804                'expectedoutput' => 'Friday, 1 July 2011, 7:00 AM',
1805                'expectedoutputhtml' => '<time datetime="2011-07-01T07:00:00-03:00">Friday, 1 July 2011, 7:00 AM</time>'
1806            ),
1807            array(
1808                'time' => '1309514400',
1809                'usertimezone' => 'America/Moncton',
1810                'timezone' => 'America/Moncton', // Dst offset and timezone offset.
1811                'expectedoutput' => 'Friday, 1 July 2011, 7:00 AM',
1812                'expectedoutputhtml' => '<time datetime="2011-07-01t07:00:00-03:00">Friday, 1 July 2011, 7:00 AM</time>'
1813            ),
1814            array(
1815                'time' => '1293876000 ',
1816                'usertimezone' => 'America/Moncton',
1817                'timezone' => '0.0', // No dst offset.
1818                'expectedoutput' => 'Saturday, 1 January 2011, 10:00 AM',
1819                'expectedoutputhtml' => '<time datetime="2011-01-01T06:00:00-04:00">Saturday, 1 January 2011, 10:00 AM</time>'
1820            ),
1821            array(
1822                'time' => '1293876000 ',
1823                'usertimezone' => 'America/Moncton',
1824                'timezone' => '99', // No dst offset in jan, so just timezone offset.
1825                'expectedoutput' => 'Saturday, 1 January 2011, 6:00 AM',
1826                'expectedoutputhtml' => '<time datetime="2011-01-01T06:00:00-04:00">Saturday, 1 January 2011, 6:00 AM</time>'
1827            ),
1828            array(
1829                'time' => '1293876000 ',
1830                'usertimezone' => 'America/Moncton',
1831                'timezone' => 'America/Moncton', // No dst offset in jan.
1832                'expectedoutput' => 'Saturday, 1 January 2011, 6:00 AM',
1833                'expectedoutputhtml' => '<time datetime="2011-01-01T06:00:00-04:00">Saturday, 1 January 2011, 6:00 AM</time>'
1834            ),
1835            array(
1836                'time' => '1293876000 ',
1837                'usertimezone' => '2',
1838                'timezone' => '99', // Take user timezone.
1839                'expectedoutput' => 'Saturday, 1 January 2011, 12:00 PM',
1840                'expectedoutputhtml' => '<time datetime="2011-01-01T12:00:00+02:00">Saturday, 1 January 2011, 12:00 PM</time>'
1841            ),
1842            array(
1843                'time' => '1293876000 ',
1844                'usertimezone' => '-2',
1845                'timezone' => '99', // Take user timezone.
1846                'expectedoutput' => 'Saturday, 1 January 2011, 8:00 AM',
1847                'expectedoutputhtml' => '<time datetime="2011-01-01T08:00:00-02:00">Saturday, 1 January 2011, 8:00 AM</time>'
1848            ),
1849            array(
1850                'time' => '1293876000 ',
1851                'usertimezone' => '-10',
1852                'timezone' => '2', // Take this timezone.
1853                'expectedoutput' => 'Saturday, 1 January 2011, 12:00 PM',
1854                'expectedoutputhtml' => '<time datetime="2011-01-01T00:00:00-10:00">Saturday, 1 January 2011, 12:00 PM</time>'
1855            ),
1856            array(
1857                'time' => '1293876000 ',
1858                'usertimezone' => '-10',
1859                'timezone' => '-2', // Take this timezone.
1860                'expectedoutput' => 'Saturday, 1 January 2011, 8:00 AM',
1861                'expectedoutputhtml' => '<time datetime="2011-01-01T00:00:00-10:00">Saturday, 1 January 2011, 8:00 AM</time>'
1862            ),
1863            array(
1864                'time' => '1293876000 ',
1865                'usertimezone' => '-10',
1866                'timezone' => 'random/time', // This should show server time.
1867                'expectedoutput' => 'Saturday, 1 January 2011, 6:00 PM',
1868                'expectedoutputhtml' => '<time datetime="2011-01-01T00:00:00-10:00">Saturday, 1 January 2011, 6:00 PM</time>'
1869            ),
1870            array(
1871                'time' => '1293876000 ',
1872                'usertimezone' => '20', // Fallback to server time zone.
1873                'timezone' => '99',     // This should show user time.
1874                'expectedoutput' => 'Saturday, 1 January 2011, 6:00 PM',
1875                'expectedoutputhtml' => '<time datetime="2011-01-01T18:00:00+08:00">Saturday, 1 January 2011, 6:00 PM</time>'
1876            ),
1877        );
1878
1879        // Set default timezone to Australia/Perth, else time calculated
1880        // will not match expected values.
1881        $this->setTimezone(99, 'Australia/Perth');
1882
1883        foreach ($testvalues as $vals) {
1884            $USER->timezone = $vals['usertimezone'];
1885            $actualoutput = userdate($vals['time'], '%A, %d %B %Y, %I:%M %p', $vals['timezone']);
1886            $actualoutputhtml = userdate_htmltime($vals['time'], '%A, %d %B %Y, %I:%M %p', $vals['timezone']);
1887
1888            // On different systems case of AM PM changes so compare case insensitive.
1889            $vals['expectedoutput'] = core_text::strtolower($vals['expectedoutput']);
1890            $vals['expectedoutputhtml'] = core_text::strtolower($vals['expectedoutputhtml']);
1891            $actualoutput = core_text::strtolower($actualoutput);
1892            $actualoutputhtml = core_text::strtolower($actualoutputhtml);
1893
1894            $this->assertSame($vals['expectedoutput'], $actualoutput,
1895                "Expected: {$vals['expectedoutput']} => Actual: {$actualoutput} \ndata: " . var_export($vals, true));
1896            $this->assertSame($vals['expectedoutputhtml'], $actualoutputhtml,
1897                "Expected: {$vals['expectedoutputhtml']} => Actual: {$actualoutputhtml} \ndata: " . var_export($vals, true));
1898        }
1899    }
1900
1901    /**
1902     * Make sure the DST changes happen at the right time in Moodle.
1903     */
1904    public function test_dst_changes() {
1905        // DST switching in Prague.
1906        // From 2AM to 3AM in 1989.
1907        $date = new DateTime('1989-03-26T01:59:00+01:00');
1908        $this->assertSame('Sunday, 26 March 1989, 01:59', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Europe/Prague'));
1909        $date = new DateTime('1989-03-26T02:01:00+01:00');
1910        $this->assertSame('Sunday, 26 March 1989, 03:01', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Europe/Prague'));
1911        // From 3AM to 2AM in 1989 - not the same as the west Europe.
1912        $date = new DateTime('1989-09-24T01:59:00+01:00');
1913        $this->assertSame('Sunday, 24 September 1989, 02:59', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Europe/Prague'));
1914        $date = new DateTime('1989-09-24T02:01:00+01:00');
1915        $this->assertSame('Sunday, 24 September 1989, 02:01', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Europe/Prague'));
1916        // From 2AM to 3AM in 2014.
1917        $date = new DateTime('2014-03-30T01:59:00+01:00');
1918        $this->assertSame('Sunday, 30 March 2014, 01:59', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Europe/Prague'));
1919        $date = new DateTime('2014-03-30T02:01:00+01:00');
1920        $this->assertSame('Sunday, 30 March 2014, 03:01', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Europe/Prague'));
1921        // From 3AM to 2AM in 2014.
1922        $date = new DateTime('2014-10-26T01:59:00+01:00');
1923        $this->assertSame('Sunday, 26 October 2014, 02:59', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Europe/Prague'));
1924        $date = new DateTime('2014-10-26T02:01:00+01:00');
1925        $this->assertSame('Sunday, 26 October 2014, 02:01', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Europe/Prague'));
1926        // From 2AM to 3AM in 2020.
1927        $date = new DateTime('2020-03-29T01:59:00+01:00');
1928        $this->assertSame('Sunday, 29 March 2020, 01:59', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Europe/Prague'));
1929        $date = new DateTime('2020-03-29T02:01:00+01:00');
1930        $this->assertSame('Sunday, 29 March 2020, 03:01', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Europe/Prague'));
1931        // From 3AM to 2AM in 2020.
1932        $date = new DateTime('2020-10-25T01:59:00+01:00');
1933        $this->assertSame('Sunday, 25 October 2020, 02:59', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Europe/Prague'));
1934        $date = new DateTime('2020-10-25T02:01:00+01:00');
1935        $this->assertSame('Sunday, 25 October 2020, 02:01', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Europe/Prague'));
1936
1937        // DST switching in NZ.
1938        // From 3AM to 2AM in 2015.
1939        $date = new DateTime('2015-04-05T02:59:00+13:00');
1940        $this->assertSame('Sunday, 5 April 2015, 02:59', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Pacific/Auckland'));
1941        $date = new DateTime('2015-04-05T03:01:00+13:00');
1942        $this->assertSame('Sunday, 5 April 2015, 02:01', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Pacific/Auckland'));
1943        // From 2AM to 3AM in 2009.
1944        $date = new DateTime('2015-09-27T01:59:00+12:00');
1945        $this->assertSame('Sunday, 27 September 2015, 01:59', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Pacific/Auckland'));
1946        $date = new DateTime('2015-09-27T02:01:00+12:00');
1947        $this->assertSame('Sunday, 27 September 2015, 03:01', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Pacific/Auckland'));
1948
1949        // DST switching in Perth.
1950        // From 3AM to 2AM in 2009.
1951        $date = new DateTime('2008-03-30T01:59:00+08:00');
1952        $this->assertSame('Sunday, 30 March 2008, 02:59', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Australia/Perth'));
1953        $date = new DateTime('2008-03-30T02:01:00+08:00');
1954        $this->assertSame('Sunday, 30 March 2008, 02:01', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Australia/Perth'));
1955        // From 2AM to 3AM in 2009.
1956        $date = new DateTime('2008-10-26T01:59:00+08:00');
1957        $this->assertSame('Sunday, 26 October 2008, 01:59', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Australia/Perth'));
1958        $date = new DateTime('2008-10-26T02:01:00+08:00');
1959        $this->assertSame('Sunday, 26 October 2008, 03:01', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Australia/Perth'));
1960
1961        // DST switching in US.
1962        // From 2AM to 3AM in 2014.
1963        $date = new DateTime('2014-03-09T01:59:00-05:00');
1964        $this->assertSame('Sunday, 9 March 2014, 01:59', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'America/New_York'));
1965        $date = new DateTime('2014-03-09T02:01:00-05:00');
1966        $this->assertSame('Sunday, 9 March 2014, 03:01', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'America/New_York'));
1967        // From 3AM to 2AM in 2014.
1968        $date = new DateTime('2014-11-02T01:59:00-04:00');
1969        $this->assertSame('Sunday, 2 November 2014, 01:59', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'America/New_York'));
1970        $date = new DateTime('2014-11-02T02:01:00-04:00');
1971        $this->assertSame('Sunday, 2 November 2014, 01:01', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'America/New_York'));
1972    }
1973
1974    public function test_make_timestamp() {
1975        global $USER, $CFG, $DB;
1976        $this->resetAfterTest();
1977
1978        $this->setAdminUser();
1979
1980        $testvalues = array(
1981            array(
1982                'usertimezone' => 'America/Moncton',
1983                'year' => '2011',
1984                'month' => '7',
1985                'day' => '1',
1986                'hour' => '10',
1987                'minutes' => '00',
1988                'seconds' => '00',
1989                'timezone' => '0.0',
1990                'applydst' => false, // No dst offset.
1991                'expectedoutput' => '1309514400' // 6pm at UTC+0.
1992            ),
1993            array(
1994                'usertimezone' => 'America/Moncton',
1995                'year' => '2011',
1996                'month' => '7',
1997                'day' => '1',
1998                'hour' => '10',
1999                'minutes' => '00',
2000                'seconds' => '00',
2001                'timezone' => '99',  // User default timezone.
2002                'applydst' => false, // Don't apply dst.
2003                'expectedoutput' => '1309528800'
2004            ),
2005            array(
2006                'usertimezone' => 'America/Moncton',
2007                'year' => '2011',
2008                'month' => '7',
2009                'day' => '1',
2010                'hour' => '10',
2011                'minutes' => '00',
2012                'seconds' => '00',
2013                'timezone' => '99', // User default timezone.
2014                'applydst' => true, // Apply dst.
2015                'expectedoutput' => '1309525200'
2016            ),
2017            array(
2018                'usertimezone' => 'America/Moncton',
2019                'year' => '2011',
2020                'month' => '7',
2021                'day' => '1',
2022                'hour' => '10',
2023                'minutes' => '00',
2024                'seconds' => '00',
2025                'timezone' => 'America/Moncton', // String timezone.
2026                'applydst' => true, // Apply dst.
2027                'expectedoutput' => '1309525200'
2028            ),
2029            array(
2030                'usertimezone' => '2', // No dst applyed.
2031                'year' => '2011',
2032                'month' => '7',
2033                'day' => '1',
2034                'hour' => '10',
2035                'minutes' => '00',
2036                'seconds' => '00',
2037                'timezone' => '99', // Take user timezone.
2038                'applydst' => true, // Apply dst.
2039                'expectedoutput' => '1309507200'
2040            ),
2041            array(
2042                'usertimezone' => '-2', // No dst applyed.
2043                'year' => '2011',
2044                'month' => '7',
2045                'day' => '1',
2046                'hour' => '10',
2047                'minutes' => '00',
2048                'seconds' => '00',
2049                'timezone' => '99', // Take usertimezone.
2050                'applydst' => true, // Apply dst.
2051                'expectedoutput' => '1309521600'
2052            ),
2053            array(
2054                'usertimezone' => '-10', // No dst applyed.
2055                'year' => '2011',
2056                'month' => '7',
2057                'day' => '1',
2058                'hour' => '10',
2059                'minutes' => '00',
2060                'seconds' => '00',
2061                'timezone' => '2',  // Take this timezone.
2062                'applydst' => true, // Apply dst.
2063                'expectedoutput' => '1309507200'
2064            ),
2065            array(
2066                'usertimezone' => '-10', // No dst applyed.
2067                'year' => '2011',
2068                'month' => '7',
2069                'day' => '1',
2070                'hour' => '10',
2071                'minutes' => '00',
2072                'seconds' => '00',
2073                'timezone' => '-2', // Take this timezone.
2074                'applydst' => true, // Apply dst.
2075                'expectedoutput' => '1309521600'
2076            ),
2077            array(
2078                'usertimezone' => '-10', // No dst applyed.
2079                'year' => '2011',
2080                'month' => '7',
2081                'day' => '1',
2082                'hour' => '10',
2083                'minutes' => '00',
2084                'seconds' => '00',
2085                'timezone' => 'random/time', // This should show server time.
2086                'applydst' => true,          // Apply dst.
2087                'expectedoutput' => '1309485600'
2088            ),
2089            array(
2090                'usertimezone' => '-14', // Server time.
2091                'year' => '2011',
2092                'month' => '7',
2093                'day' => '1',
2094                'hour' => '10',
2095                'minutes' => '00',
2096                'seconds' => '00',
2097                'timezone' => '99', // Get user time.
2098                'applydst' => true, // Apply dst.
2099                'expectedoutput' => '1309485600'
2100            )
2101        );
2102
2103        // Set default timezone to Australia/Perth, else time calculated
2104        // will not match expected values.
2105        $this->setTimezone(99, 'Australia/Perth');
2106
2107        // Test make_timestamp with all testvals and assert if anything wrong.
2108        foreach ($testvalues as $vals) {
2109            $USER->timezone = $vals['usertimezone'];
2110            $actualoutput = make_timestamp(
2111                $vals['year'],
2112                $vals['month'],
2113                $vals['day'],
2114                $vals['hour'],
2115                $vals['minutes'],
2116                $vals['seconds'],
2117                $vals['timezone'],
2118                $vals['applydst']
2119            );
2120
2121            // On different systems case of AM PM changes so compare case insensitive.
2122            $vals['expectedoutput'] = core_text::strtolower($vals['expectedoutput']);
2123            $actualoutput = core_text::strtolower($actualoutput);
2124
2125            $this->assertSame($vals['expectedoutput'], $actualoutput,
2126                "Expected: {$vals['expectedoutput']} => Actual: {$actualoutput},
2127                Please check if timezones are updated (Site adminstration -> location -> update timezone)");
2128        }
2129    }
2130
2131    /**
2132     * Test get_string and most importantly the implementation of the lang_string
2133     * object.
2134     */
2135    public function test_get_string() {
2136        global $COURSE;
2137
2138        // Make sure we are using English.
2139        $originallang = $COURSE->lang;
2140        $COURSE->lang = 'en';
2141
2142        $yes = get_string('yes');
2143        $yesexpected = 'Yes';
2144        $this->assertIsString($yes);
2145        $this->assertSame($yesexpected, $yes);
2146
2147        $yes = get_string('yes', 'moodle');
2148        $this->assertIsString($yes);
2149        $this->assertSame($yesexpected, $yes);
2150
2151        $yes = get_string('yes', 'core');
2152        $this->assertIsString($yes);
2153        $this->assertSame($yesexpected, $yes);
2154
2155        $yes = get_string('yes', '');
2156        $this->assertIsString($yes);
2157        $this->assertSame($yesexpected, $yes);
2158
2159        $yes = get_string('yes', null);
2160        $this->assertIsString($yes);
2161        $this->assertSame($yesexpected, $yes);
2162
2163        $yes = get_string('yes', null, 1);
2164        $this->assertIsString($yes);
2165        $this->assertSame($yesexpected, $yes);
2166
2167        $days = 1;
2168        $numdays = get_string('numdays', 'core', '1');
2169        $numdaysexpected = $days.' days';
2170        $this->assertIsString($numdays);
2171        $this->assertSame($numdaysexpected, $numdays);
2172
2173        $yes = get_string('yes', null, null, true);
2174        $this->assertInstanceOf('lang_string', $yes);
2175        $this->assertSame($yesexpected, (string)$yes);
2176
2177        // Test lazy loading (returning lang_string) correctly interpolates 0 being used as args.
2178        $numdays = get_string('numdays', 'moodle', 0, true);
2179        $this->assertInstanceOf(lang_string::class, $numdays);
2180        $this->assertSame('0 days', (string) $numdays);
2181
2182        // Test using a lang_string object as the $a argument for a normal
2183        // get_string call (returning string).
2184        $test = new lang_string('yes', null, null, true);
2185        $testexpected = get_string('numdays', 'core', get_string('yes'));
2186        $testresult = get_string('numdays', null, $test);
2187        $this->assertIsString($testresult);
2188        $this->assertSame($testexpected, $testresult);
2189
2190        // Test using a lang_string object as the $a argument for an object
2191        // get_string call (returning lang_string).
2192        $test = new lang_string('yes', null, null, true);
2193        $testexpected = get_string('numdays', 'core', get_string('yes'));
2194        $testresult = get_string('numdays', null, $test, true);
2195        $this->assertInstanceOf('lang_string', $testresult);
2196        $this->assertSame($testexpected, "$testresult");
2197
2198        // Make sure that object properties that can't be converted don't cause
2199        // errors.
2200        // Level one: This is as deep as current language processing goes.
2201        $test = new stdClass;
2202        $test->one = 'here';
2203        $string = get_string('yes', null, $test, true);
2204        $this->assertEquals($yesexpected, $string);
2205
2206        // Make sure that object properties that can't be converted don't cause
2207        // errors.
2208        // Level two: Language processing doesn't currently reach this deep.
2209        // only immediate scalar properties are worked with.
2210        $test = new stdClass;
2211        $test->one = new stdClass;
2212        $test->one->two = 'here';
2213        $string = get_string('yes', null, $test, true);
2214        $this->assertEquals($yesexpected, $string);
2215
2216        // Make sure that object properties that can't be converted don't cause
2217        // errors.
2218        // Level three: It should never ever go this deep, but we're making sure
2219        // it doesn't cause any probs anyway.
2220        $test = new stdClass;
2221        $test->one = new stdClass;
2222        $test->one->two = new stdClass;
2223        $test->one->two->three = 'here';
2224        $string = get_string('yes', null, $test, true);
2225        $this->assertEquals($yesexpected, $string);
2226
2227        // Make sure that object properties that can't be converted don't cause
2228        // errors and check lang_string properties.
2229        // Level one: This is as deep as current language processing goes.
2230        $test = new stdClass;
2231        $test->one = new lang_string('yes');
2232        $string = get_string('yes', null, $test, true);
2233        $this->assertEquals($yesexpected, $string);
2234
2235        // Make sure that object properties that can't be converted don't cause
2236        // errors and check lang_string properties.
2237        // Level two: Language processing doesn't currently reach this deep.
2238        // only immediate scalar properties are worked with.
2239        $test = new stdClass;
2240        $test->one = new stdClass;
2241        $test->one->two = new lang_string('yes');
2242        $string = get_string('yes', null, $test, true);
2243        $this->assertEquals($yesexpected, $string);
2244
2245        // Make sure that object properties that can't be converted don't cause
2246        // errors and check lang_string properties.
2247        // Level three: It should never ever go this deep, but we're making sure
2248        // it doesn't cause any probs anyway.
2249        $test = new stdClass;
2250        $test->one = new stdClass;
2251        $test->one->two = new stdClass;
2252        $test->one->two->three = new lang_string('yes');
2253        $string = get_string('yes', null, $test, true);
2254        $this->assertEquals($yesexpected, $string);
2255
2256        // Make sure that array properties that can't be converted don't cause
2257        // errors.
2258        $test = array();
2259        $test['one'] = new stdClass;
2260        $test['one']->two = 'here';
2261        $string = get_string('yes', null, $test, true);
2262        $this->assertEquals($yesexpected, $string);
2263
2264        // Same thing but as above except using an object... this is allowed :P.
2265        $string = get_string('yes', null, null, true);
2266        $object = new stdClass;
2267        $object->$string = 'Yes';
2268        $this->assertEquals($yesexpected, $string);
2269        $this->assertEquals($yesexpected, $object->$string);
2270
2271        // Reset the language.
2272        $COURSE->lang = $originallang;
2273    }
2274
2275    public function test_lang_string_var_export() {
2276
2277        // Call var_export() on a newly generated lang_string.
2278        $str = new lang_string('no');
2279
2280        $expected1 = <<<EOF
2281lang_string::__set_state(array(
2282   'identifier' => 'no',
2283   'component' => 'moodle',
2284   'a' => NULL,
2285   'lang' => NULL,
2286   'string' => NULL,
2287   'forcedstring' => false,
2288))
2289EOF;
2290
2291        $v = var_export($str, true);
2292        $this->assertEquals($expected1, $v);
2293
2294        // Now execute the code that was returned - it should produce a correct string.
2295        $str = lang_string::__set_state(array(
2296            'identifier' => 'no',
2297            'component' => 'moodle',
2298            'a' => NULL,
2299            'lang' => NULL,
2300            'string' => NULL,
2301            'forcedstring' => false,
2302        ));
2303
2304        $this->assertInstanceOf(lang_string::class, $str);
2305        $this->assertEquals('No', $str);
2306    }
2307
2308    public function test_get_string_limitation() {
2309        // This is one of the limitations to the lang_string class. It can't be
2310        // used as a key.
2311        if (PHP_VERSION_ID >= 80000) {
2312            $this->expectException(TypeError::class);
2313        } else {
2314            $this->expectWarning();
2315        }
2316        $array = array(get_string('yes', null, null, true) => 'yes');
2317    }
2318
2319    /**
2320     * Test localised float formatting.
2321     */
2322    public function test_format_float() {
2323
2324        // Special case for null.
2325        $this->assertEquals('', format_float(null));
2326
2327        // Default 1 decimal place.
2328        $this->assertEquals('5.4', format_float(5.43));
2329        $this->assertEquals('5.0', format_float(5.001));
2330
2331        // Custom number of decimal places.
2332        $this->assertEquals('5.43000', format_float(5.43, 5));
2333
2334        // Auto detect the number of decimal places.
2335        $this->assertEquals('5.43', format_float(5.43, -1));
2336        $this->assertEquals('5.43', format_float(5.43000, -1));
2337        $this->assertEquals('5', format_float(5, -1));
2338        $this->assertEquals('5', format_float(5.0, -1));
2339        $this->assertEquals('0.543', format_float('5.43e-1', -1));
2340        $this->assertEquals('0.543', format_float('5.43000e-1', -1));
2341
2342        // Option to strip ending zeros after rounding.
2343        $this->assertEquals('5.43', format_float(5.43, 5, true, true));
2344        $this->assertEquals('5', format_float(5.0001, 3, true, true));
2345
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->assertEquals('0.0.0.0', $noip);
4026
4027        $_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1';
4028        $singleip = getremoteaddr();
4029        $this->assertEquals('127.0.0.1', $singleip);
4030
4031        $_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1,127.0.0.2';
4032        $twoip = getremoteaddr();
4033        $this->assertEquals('127.0.0.2', $twoip);
4034
4035        $_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1,127.0.0.2,127.0.0.3';
4036        $threeip = getremoteaddr();
4037        $this->assertEquals('127.0.0.3', $threeip);
4038
4039        $_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1,127.0.0.2:65535';
4040        $portip = getremoteaddr();
4041        $this->assertEquals('127.0.0.2', $portip);
4042
4043        $_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1,0:0:0:0:0:0:0:2';
4044        $portip = getremoteaddr();
4045        $this->assertEquals('0:0:0:0:0:0:0:2', $portip);
4046
4047        $_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1,0::2';
4048        $portip = getremoteaddr();
4049        $this->assertEquals('0:0:0:0:0:0:0:2', $portip);
4050
4051        $_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1,[0:0:0:0:0:0:0:2]:65535';
4052        $portip = getremoteaddr();
4053        $this->assertEquals('0:0:0:0:0:0:0:2', $portip);
4054
4055        $_SERVER['HTTP_X_FORWARDED_FOR'] = $xforwardedfor;
4056
4057    }
4058
4059    /*
4060     * Test emulation of random_bytes() function.
4061     */
4062    public function test_random_bytes_emulate() {
4063        $result = random_bytes_emulate(10);
4064        $this->assertSame(10, strlen($result));
4065        $this->assertnotSame($result, random_bytes_emulate(10));
4066
4067        $result = random_bytes_emulate(21);
4068        $this->assertSame(21, strlen($result));
4069        $this->assertnotSame($result, random_bytes_emulate(21));
4070
4071        $result = random_bytes_emulate(666);
4072        $this->assertSame(666, strlen($result));
4073
4074        $result = random_bytes_emulate(40);
4075        $this->assertSame(40, strlen($result));
4076
4077        $this->assertDebuggingNotCalled();
4078
4079        $result = random_bytes_emulate(0);
4080        $this->assertSame('', $result);
4081        $this->assertDebuggingCalled();
4082
4083        $result = random_bytes_emulate(-1);
4084        $this->assertSame('', $result);
4085        $this->assertDebuggingCalled();
4086    }
4087
4088    /**
4089     * Test function for creation of random strings.
4090     */
4091    public function test_random_string() {
4092        $pool = 'a-zA-Z0-9';
4093
4094        $result = random_string(10);
4095        $this->assertSame(10, strlen($result));
4096        $this->assertMatchesRegularExpression('/^[' . $pool . ']+$/', $result);
4097        $this->assertNotSame($result, random_string(10));
4098
4099        $result = random_string(21);
4100        $this->assertSame(21, strlen($result));
4101        $this->assertMatchesRegularExpression('/^[' . $pool . ']+$/', $result);
4102        $this->assertNotSame($result, random_string(21));
4103
4104        $result = random_string(666);
4105        $this->assertSame(666, strlen($result));
4106        $this->assertMatchesRegularExpression('/^[' . $pool . ']+$/', $result);
4107
4108        $result = random_string();
4109        $this->assertSame(15, strlen($result));
4110        $this->assertMatchesRegularExpression('/^[' . $pool . ']+$/', $result);
4111
4112        $this->assertDebuggingNotCalled();
4113
4114        $result = random_string(0);
4115        $this->assertSame('', $result);
4116        $this->assertDebuggingCalled();
4117
4118        $result = random_string(-1);
4119        $this->assertSame('', $result);
4120        $this->assertDebuggingCalled();
4121    }
4122
4123    /**
4124     * Test function for creation of complex random strings.
4125     */
4126    public function test_complex_random_string() {
4127        $pool = preg_quote('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789`~!@#%^&*()_+-=[];,./<>?:{} ', '/');
4128
4129        $result = complex_random_string(10);
4130        $this->assertSame(10, strlen($result));
4131        $this->assertMatchesRegularExpression('/^[' . $pool . ']+$/', $result);
4132        $this->assertNotSame($result, complex_random_string(10));
4133
4134        $result = complex_random_string(21);
4135        $this->assertSame(21, strlen($result));
4136        $this->assertMatchesRegularExpression('/^[' . $pool . ']+$/', $result);
4137        $this->assertNotSame($result, complex_random_string(21));
4138
4139        $result = complex_random_string(666);
4140        $this->assertSame(666, strlen($result));
4141        $this->assertMatchesRegularExpression('/^[' . $pool . ']+$/', $result);
4142
4143        $result = complex_random_string();
4144        $this->assertEqualsWithDelta(28, strlen($result), 4); // Expected length is 24 - 32.
4145        $this->assertMatchesRegularExpression('/^[' . $pool . ']+$/', $result);
4146
4147        $this->assertDebuggingNotCalled();
4148
4149        $result = complex_random_string(0);
4150        $this->assertSame('', $result);
4151        $this->assertDebuggingCalled();
4152
4153        $result = complex_random_string(-1);
4154        $this->assertSame('', $result);
4155        $this->assertDebuggingCalled();
4156    }
4157
4158    /**
4159     * Data provider for private ips.
4160     */
4161    public function data_private_ips() {
4162        return array(
4163            array('10.0.0.0'),
4164            array('172.16.0.0'),
4165            array('192.168.1.0'),
4166            array('fdfe:dcba:9876:ffff:fdc6:c46b:bb8f:7d4c'),
4167            array('fdc6:c46b:bb8f:7d4c:fdc6:c46b:bb8f:7d4c'),
4168            array('fdc6:c46b:bb8f:7d4c:0000:8a2e:0370:7334'),
4169            array('127.0.0.1'), // This has been buggy in past: https://bugs.php.net/bug.php?id=53150.
4170        );
4171    }
4172
4173    /**
4174     * Checks ip_is_public returns false for private ips.
4175     *
4176     * @param string $ip the ipaddress to test
4177     * @dataProvider data_private_ips
4178     */
4179    public function test_ip_is_public_private_ips($ip) {
4180        $this->assertFalse(ip_is_public($ip));
4181    }
4182
4183    /**
4184     * Data provider for public ips.
4185     */
4186    public function data_public_ips() {
4187        return array(
4188            array('2400:cb00:2048:1::8d65:71b3'),
4189            array('2400:6180:0:d0::1b:2001'),
4190            array('141.101.113.179'),
4191            array('123.45.67.178'),
4192        );
4193    }
4194
4195    /**
4196     * Checks ip_is_public returns true for public ips.
4197     *
4198     * @param string $ip the ipaddress to test
4199     * @dataProvider data_public_ips
4200     */
4201    public function test_ip_is_public_public_ips($ip) {
4202        $this->assertTrue(ip_is_public($ip));
4203    }
4204
4205    /**
4206     * Test the function can_send_from_real_email_address
4207     *
4208     * @param string $email Email address for the from user.
4209     * @param int $display The user's email display preference.
4210     * @param bool $samecourse Are the users in the same course?
4211     * @param string $config The CFG->allowedemaildomains config values
4212     * @param bool $result The expected result.
4213     * @dataProvider data_can_send_from_real_email_address
4214     */
4215    public function test_can_send_from_real_email_address($email, $display, $samecourse, $config, $result) {
4216        $this->resetAfterTest();
4217
4218        $fromuser = $this->getDataGenerator()->create_user();
4219        $touser = $this->getDataGenerator()->create_user();
4220        $course = $this->getDataGenerator()->create_course();
4221        set_config('allowedemaildomains', $config);
4222
4223        $fromuser->email = $email;
4224        $fromuser->maildisplay = $display;
4225        if ($samecourse) {
4226            $this->getDataGenerator()->enrol_user($fromuser->id, $course->id, 'student');
4227            $this->getDataGenerator()->enrol_user($touser->id, $course->id, 'student');
4228        } else {
4229            $this->getDataGenerator()->enrol_user($fromuser->id, $course->id, 'student');
4230        }
4231        $this->assertEquals($result, can_send_from_real_email_address($fromuser, $touser));
4232    }
4233
4234    /**
4235     * Data provider for test_can_send_from_real_email_address.
4236     *
4237     * @return array Returns an array of test data for the above function.
4238     */
4239    public function data_can_send_from_real_email_address() {
4240        return [
4241            // Test from email is in allowed domain.
4242            // Test that from display is set to show no one.
4243            [
4244                'email' => 'fromuser@example.com',
4245                'display' => core_user::MAILDISPLAY_HIDE,
4246                'samecourse' => false,
4247                'config' => "example.com\r\ntest.com",
4248                'result' => false
4249            ],
4250            // Test that from display is set to course members only (course member).
4251            [
4252                'email' => 'fromuser@example.com',
4253                'display' => core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY,
4254                'samecourse' => true,
4255                'config' => "example.com\r\ntest.com",
4256                'result' => true
4257            ],
4258            // Test that from display is set to course members only (Non course member).
4259            [
4260                'email' => 'fromuser@example.com',
4261                'display' => core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY,
4262                'samecourse' => false,
4263                'config' => "example.com\r\ntest.com",
4264                'result' => false
4265            ],
4266            // Test that from display is set to show everyone.
4267            [
4268                'email' => 'fromuser@example.com',
4269                'display' => core_user::MAILDISPLAY_EVERYONE,
4270                'samecourse' => false,
4271                'config' => "example.com\r\ntest.com",
4272                'result' => true
4273            ],
4274            // Test a few different config value formats for parsing correctness.
4275            [
4276                'email' => 'fromuser@example.com',
4277                'display' => core_user::MAILDISPLAY_EVERYONE,
4278                'samecourse' => false,
4279                'config' => "\n test.com\nexample.com \n",
4280                'result' => true
4281            ],
4282            [
4283                'email' => 'fromuser@example.com',
4284                'display' => core_user::MAILDISPLAY_EVERYONE,
4285                'samecourse' => false,
4286                'config' => "\r\n example.com \r\n test.com \r\n",
4287                'result' => true
4288            ],
4289
4290            // Test from email is not in allowed domain.
4291            // Test that from display is set to show no one.
4292            [   'email' => 'fromuser@moodle.com',
4293                'display' => core_user::MAILDISPLAY_HIDE,
4294                'samecourse' => false,
4295                'config' => "example.com\r\ntest.com",
4296                'result' => false
4297            ],
4298            // Test that from display is set to course members only (course member).
4299            [   'email' => 'fromuser@moodle.com',
4300                'display' => core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY,
4301                'samecourse' => true,
4302                'config' => "example.com\r\ntest.com",
4303                'result' => false
4304            ],
4305            // Test that from display is set to course members only (Non course member.
4306            [   'email' => 'fromuser@moodle.com',
4307                'display' => core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY,
4308                'samecourse' => false,
4309                'config' => "example.com\r\ntest.com",
4310                'result' => false
4311            ],
4312            // Test that from display is set to show everyone.
4313            [   'email' => 'fromuser@moodle.com',
4314                'display' => core_user::MAILDISPLAY_EVERYONE,
4315                'samecourse' => false,
4316                'config' => "example.com\r\ntest.com",
4317                'result' => false
4318            ],
4319            // Test a few erroneous config value and confirm failure.
4320            [   'email' => 'fromuser@moodle.com',
4321                'display' => core_user::MAILDISPLAY_EVERYONE,
4322                'samecourse' => false,
4323                'config' => "\r\n   \r\n",
4324                'result' => false
4325            ],
4326            [   'email' => 'fromuser@moodle.com',
4327                'display' => core_user::MAILDISPLAY_EVERYONE,
4328                'samecourse' => false,
4329                'config' => " \n   \n \n ",
4330                'result' => false
4331            ],
4332        ];
4333    }
4334
4335    /**
4336     * Test that generate_email_processing_address() returns valid email address.
4337     */
4338    public function test_generate_email_processing_address() {
4339        global $CFG;
4340        $this->resetAfterTest();
4341
4342        $data = (object)[
4343            'id' => 42,
4344            'email' => 'my.email+from_moodle@example.com',
4345        ];
4346
4347        $modargs = 'B'.base64_encode(pack('V', $data->id)).substr(md5($data->email), 0, 16);
4348
4349        $CFG->maildomain = 'example.com';
4350        $CFG->mailprefix = 'mdl+';
4351        $this->assertTrue(validate_email(generate_email_processing_address(0, $modargs)));
4352
4353        $CFG->maildomain = 'mail.example.com';
4354        $CFG->mailprefix = 'mdl-';
4355        $this->assertTrue(validate_email(generate_email_processing_address(23, $modargs)));
4356    }
4357
4358    /**
4359     * Test allowemailaddresses setting.
4360     *
4361     * @param string $email Email address for the from user.
4362     * @param string $config The CFG->allowemailaddresses config values
4363     * @param false/string $result The expected result.
4364     *
4365     * @dataProvider data_email_is_not_allowed_for_allowemailaddresses
4366     */
4367    public function test_email_is_not_allowed_for_allowemailaddresses($email, $config, $result) {
4368        $this->resetAfterTest();
4369
4370        set_config('allowemailaddresses', $config);
4371        $this->assertEquals($result, email_is_not_allowed($email));
4372    }
4373
4374    /**
4375     * Data provider for data_email_is_not_allowed_for_allowemailaddresses.
4376     *
4377     * @return array Returns an array of test data for the above function.
4378     */
4379    public function data_email_is_not_allowed_for_allowemailaddresses() {
4380        return [
4381            // Test allowed domain empty list.
4382            [
4383                'email' => 'fromuser@example.com',
4384                'config' => '',
4385                'result' => false
4386            ],
4387            // Test from email is in allowed domain.
4388            [
4389                'email' => 'fromuser@example.com',
4390                'config' => 'example.com test.com',
4391                'result' => false
4392            ],
4393            // Test from email is in allowed domain but uppercase config.
4394            [
4395                'email' => 'fromuser@example.com',
4396                'config' => 'EXAMPLE.com test.com',
4397                'result' => false
4398            ],
4399            // Test from email is in allowed domain but uppercase email.
4400            [
4401                'email' => 'fromuser@EXAMPLE.com',
4402                'config' => 'example.com test.com',
4403                'result' => false
4404            ],
4405            // Test from email is in allowed subdomain.
4406            [
4407                'email' => 'fromuser@something.example.com',
4408                'config' => '.example.com test.com',
4409                'result' => false
4410            ],
4411            // Test from email is in allowed subdomain but uppercase config.
4412            [
4413                'email' => 'fromuser@something.example.com',
4414                'config' => '.EXAMPLE.com test.com',
4415                'result' => false
4416            ],
4417            // Test from email is in allowed subdomain but uppercase email.
4418            [
4419                'email' => 'fromuser@something.EXAMPLE.com',
4420                'config' => '.example.com test.com',
4421                'result' => false
4422            ],
4423            // Test from email is not in allowed domain.
4424            [   'email' => 'fromuser@moodle.com',
4425                'config' => 'example.com test.com',
4426                'result' => get_string('emailonlyallowed', '', 'example.com test.com')
4427            ],
4428            // Test from email is not in allowed subdomain.
4429            [   'email' => 'fromuser@something.example.com',
4430                'config' => 'example.com test.com',
4431                'result' => get_string('emailonlyallowed', '', 'example.com test.com')
4432            ],
4433        ];
4434    }
4435
4436    /**
4437     * Test denyemailaddresses setting.
4438     *
4439     * @param string $email Email address for the from user.
4440     * @param string $config The CFG->denyemailaddresses config values
4441     * @param false/string $result The expected result.
4442     *
4443     * @dataProvider data_email_is_not_allowed_for_denyemailaddresses
4444     */
4445    public function test_email_is_not_allowed_for_denyemailaddresses($email, $config, $result) {
4446        $this->resetAfterTest();
4447
4448        set_config('denyemailaddresses', $config);
4449        $this->assertEquals($result, email_is_not_allowed($email));
4450    }
4451
4452
4453    /**
4454     * Data provider for test_email_is_not_allowed_for_denyemailaddresses.
4455     *
4456     * @return array Returns an array of test data for the above function.
4457     */
4458    public function data_email_is_not_allowed_for_denyemailaddresses() {
4459        return [
4460            // Test denied domain empty list.
4461            [
4462                'email' => 'fromuser@example.com',
4463                'config' => '',
4464                'result' => false
4465            ],
4466            // Test from email is in denied domain.
4467            [
4468                'email' => 'fromuser@example.com',
4469                'config' => 'example.com test.com',
4470                'result' => get_string('emailnotallowed', '', 'example.com test.com')
4471            ],
4472            // Test from email is in denied domain but uppercase config.
4473            [
4474                'email' => 'fromuser@example.com',
4475                'config' => 'EXAMPLE.com test.com',
4476                'result' => get_string('emailnotallowed', '', 'EXAMPLE.com test.com')
4477            ],
4478            // Test from email is in denied domain but uppercase email.
4479            [
4480                'email' => 'fromuser@EXAMPLE.com',
4481                'config' => 'example.com test.com',
4482                'result' => get_string('emailnotallowed', '', 'example.com test.com')
4483            ],
4484            // Test from email is in denied subdomain.
4485            [
4486                'email' => 'fromuser@something.example.com',
4487                'config' => '.example.com test.com',
4488                'result' => get_string('emailnotallowed', '', '.example.com test.com')
4489            ],
4490            // Test from email is in denied subdomain but uppercase config.
4491            [
4492                'email' => 'fromuser@something.example.com',
4493                'config' => '.EXAMPLE.com test.com',
4494                'result' => get_string('emailnotallowed', '', '.EXAMPLE.com test.com')
4495            ],
4496            // Test from email is in denied subdomain but uppercase email.
4497            [
4498                'email' => 'fromuser@something.EXAMPLE.com',
4499                'config' => '.example.com test.com',
4500                'result' => get_string('emailnotallowed', '', '.example.com test.com')
4501            ],
4502            // Test from email is not in denied domain.
4503            [   'email' => 'fromuser@moodle.com',
4504                'config' => 'example.com test.com',
4505                'result' => false
4506            ],
4507            // Test from email is not in denied subdomain.
4508            [   'email' => 'fromuser@something.example.com',
4509                'config' => 'example.com test.com',
4510                'result' => false
4511            ],
4512        ];
4513    }
4514
4515    /**
4516     * Test safe method unserialize_array().
4517     */
4518    public function test_unserialize_array() {
4519        $a = [1, 2, 3];
4520        $this->assertEquals($a, unserialize_array(serialize($a)));
4521        $this->assertEquals($a, unserialize_array(serialize($a)));
4522        $a = ['a' => 1, 2 => 2, 'b' => 'cde'];
4523        $this->assertEquals($a, unserialize_array(serialize($a)));
4524        $this->assertEquals($a, unserialize_array(serialize($a)));
4525        $a = ['a' => 1, 2 => 2, 'b' => 'c"d"e'];
4526        $this->assertEquals($a, unserialize_array(serialize($a)));
4527        $a = ['a' => 1, 2 => ['c' => 'd', 'e' => 'f'], 'b' => 'cde'];
4528        $this->assertEquals($a, unserialize_array(serialize($a)));
4529
4530        // Can not unserialize if any string contains semicolons.
4531        $a = ['a' => 1, 2 => 2, 'b' => 'c"d";e'];
4532        $this->assertEquals(false, unserialize_array(serialize($a)));
4533
4534        // Can not unserialize if there are any objects.
4535        $a = (object)['a' => 1, 2 => 2, 'b' => 'cde'];
4536        $this->assertEquals(false, unserialize_array(serialize($a)));
4537        $a = ['a' => 1, 2 => 2, 'b' => (object)['a' => 'cde']];
4538        $this->assertEquals(false, unserialize_array(serialize($a)));
4539
4540        // Array used in the grader report.
4541        $a = array('aggregatesonly' => [51, 34], 'gradesonly' => [21, 45, 78]);
4542        $this->assertEquals($a, unserialize_array(serialize($a)));
4543    }
4544
4545    /**
4546     * Test method for safely unserializing a serialized object of type stdClass
4547     */
4548    public function test_unserialize_object(): void {
4549        $object = (object) [
4550            'foo' => 42,
4551            'bar' => 'Hamster',
4552            'innerobject' => (object) [
4553                'baz' => 'happy',
4554            ],
4555        ];
4556
4557        // We should get back the same object we serialized.
4558        $serializedobject = serialize($object);
4559        $this->assertEquals($object, unserialize_object($serializedobject));
4560
4561        // Try serializing a different class, not allowed.
4562        $langstr = new lang_string('no');
4563        $serializedlangstr = serialize($langstr);
4564        $unserializedlangstr = unserialize_object($serializedlangstr);
4565        $this->assertInstanceOf(stdClass::class, $unserializedlangstr);
4566    }
4567
4568    /**
4569     * Test that the component_class_callback returns the correct default value when the class was not found.
4570     *
4571     * @dataProvider component_class_callback_default_provider
4572     * @param $default
4573     */
4574    public function test_component_class_callback_not_found($default) {
4575        $this->assertSame($default, component_class_callback('thisIsNotTheClassYouWereLookingFor', 'anymethod', [], $default));
4576    }
4577
4578    /**
4579     * Test that the component_class_callback returns the correct default value when the class was not found.
4580     *
4581     * @dataProvider component_class_callback_default_provider
4582     * @param $default
4583     */
4584    public function test_component_class_callback_method_not_found($default) {
4585        require_once(__DIR__ . '/fixtures/component_class_callback_example.php');
4586
4587        $this->assertSame($default, component_class_callback(test_component_class_callback_example::class, 'this_is_not_the_method_you_were_looking_for', ['abc'], $default));
4588    }
4589
4590    /**
4591     * Test that the component_class_callback returns the default when the method returned null.
4592     *
4593     * @dataProvider component_class_callback_default_provider
4594     * @param $default
4595     */
4596    public function test_component_class_callback_found_returns_null($default) {
4597        require_once(__DIR__ . '/fixtures/component_class_callback_example.php');
4598
4599        $this->assertSame($default, component_class_callback(test_component_class_callback_example::class, 'method_returns_value', [null], $default));
4600        $this->assertSame($default, component_class_callback(test_component_class_callback_child_example::class, 'method_returns_value', [null], $default));
4601    }
4602
4603    /**
4604     * Test that the component_class_callback returns the expected value and not the default when there was a value.
4605     *
4606     * @dataProvider component_class_callback_data_provider
4607     * @param $default
4608     */
4609    public function test_component_class_callback_found_returns_value($value) {
4610        require_once(__DIR__ . '/fixtures/component_class_callback_example.php');
4611
4612        $this->assertSame($value, component_class_callback(test_component_class_callback_example::class, 'method_returns_value', [$value], 'This is not the value you were looking for'));
4613        $this->assertSame($value, component_class_callback(test_component_class_callback_child_example::class, 'method_returns_value', [$value], 'This is not the value you were looking for'));
4614    }
4615
4616    /**
4617     * Test that the component_class_callback handles multiple params correctly.
4618     *
4619     * @dataProvider component_class_callback_multiple_params_provider
4620     * @param $default
4621     */
4622    public function test_component_class_callback_found_accepts_multiple($params, $count) {
4623        require_once(__DIR__ . '/fixtures/component_class_callback_example.php');
4624
4625        $this->assertSame($count, component_class_callback(test_component_class_callback_example::class, 'method_returns_all_params', $params, 'This is not the value you were looking for'));
4626        $this->assertSame($count, component_class_callback(test_component_class_callback_child_example::class, 'method_returns_all_params', $params, 'This is not the value you were looking for'));
4627    }
4628
4629    /**
4630     * Data provider with list of default values for user in component_class_callback tests.
4631     *
4632     * @return array
4633     */
4634    public function component_class_callback_default_provider() {
4635        return [
4636            'null' => [null],
4637            'empty string' => [''],
4638            'string' => ['This is a string'],
4639            'int' => [12345],
4640            'stdClass' => [(object) ['this is my content']],
4641            'array' => [['a' => 'b',]],
4642        ];
4643    }
4644
4645    /**
4646     * Data provider with list of default values for user in component_class_callback tests.
4647     *
4648     * @return array
4649     */
4650    public function component_class_callback_data_provider() {
4651        return [
4652            'empty string' => [''],
4653            'string' => ['This is a string'],
4654            'int' => [12345],
4655            'stdClass' => [(object) ['this is my content']],
4656            'array' => [['a' => 'b',]],
4657        ];
4658    }
4659
4660    /**
4661     * Data provider with list of default values for user in component_class_callback tests.
4662     *
4663     * @return array
4664     */
4665    public function component_class_callback_multiple_params_provider() {
4666        return [
4667            'empty array' => [
4668                [],
4669                0,
4670            ],
4671            'string value' => [
4672                ['one'],
4673                1,
4674            ],
4675            'string values' => [
4676                ['one', 'two'],
4677                2,
4678            ],
4679            'arrays' => [
4680                [[], []],
4681                2,
4682            ],
4683            'nulls' => [
4684                [null, null, null, null],
4685                4,
4686            ],
4687            'mixed' => [
4688                ['a', 1, null, (object) [], []],
4689                5,
4690            ],
4691        ];
4692    }
4693
4694    /**
4695     * Test that {@link get_callable_name()} describes the callable as expected.
4696     *
4697     * @dataProvider callable_names_provider
4698     * @param callable $callable
4699     * @param string $expectedname
4700     */
4701    public function test_get_callable_name($callable, $expectedname) {
4702        $this->assertSame($expectedname, get_callable_name($callable));
4703    }
4704
4705    /**
4706     * Provides a set of callables and their human readable names.
4707     *
4708     * @return array of (string)case => [(mixed)callable, (string|bool)expected description]
4709     */
4710    public function callable_names_provider() {
4711        return [
4712            'integer' => [
4713                386,
4714                false,
4715            ],
4716            'boolean' => [
4717                true,
4718                false,
4719            ],
4720            'static_method_as_literal' => [
4721                'my_foobar_class::my_foobar_method',
4722                'my_foobar_class::my_foobar_method',
4723            ],
4724            'static_method_of_literal_class' => [
4725                ['my_foobar_class', 'my_foobar_method'],
4726                'my_foobar_class::my_foobar_method',
4727            ],
4728            'static_method_of_object' => [
4729                [$this, 'my_foobar_method'],
4730                'core_moodlelib_testcase::my_foobar_method',
4731            ],
4732            'method_of_object' => [
4733                [new lang_string('parentlanguage', 'core_langconfig'), 'my_foobar_method'],
4734                'lang_string::my_foobar_method',
4735            ],
4736            'function_as_literal' => [
4737                'my_foobar_callback',
4738                'my_foobar_callback',
4739            ],
4740            'function_as_closure' => [
4741                function($a) { return $a; },
4742                'Closure::__invoke',
4743            ],
4744        ];
4745    }
4746
4747    /**
4748     * Data provider for \core_moodlelib_testcase::test_get_complete_user_data().
4749     *
4750     * @return array
4751     */
4752    public function user_data_provider() {
4753        return [
4754            'Fetch data using a valid username' => [
4755                'username', 's1', true
4756            ],
4757            'Fetch data using a valid username, different case' => [
4758                'username', 'S1', true
4759            ],
4760            'Fetch data using a valid username, different case for fieldname and value' => [
4761                'USERNAME', 'S1', true
4762            ],
4763            'Fetch data using an invalid username' => [
4764                'username', 's2', false
4765            ],
4766            'Fetch by email' => [
4767                'email', 's1@example.com', true
4768            ],
4769            'Fetch data using a non-existent email' => [
4770                'email', 's2@example.com', false
4771            ],
4772            'Fetch data using a non-existent email, throw exception' => [
4773                'email', 's2@example.com', false, dml_missing_record_exception::class
4774            ],
4775            'Multiple accounts with the same email' => [
4776                'email', 's1@example.com', false, 1
4777            ],
4778            'Multiple accounts with the same email, throw exception' => [
4779                'email', 's1@example.com', false, 1, dml_multiple_records_exception::class
4780            ],
4781            'Fetch data using a valid user ID' => [
4782                'id', true, true
4783            ],
4784            'Fetch data using a non-existent user ID' => [
4785                'id', false, false
4786            ],
4787        ];
4788    }
4789
4790    /**
4791     * Test for get_complete_user_data().
4792     *
4793     * @dataProvider user_data_provider
4794     * @param string $field The field to use for the query.
4795     * @param string|boolean $value The field value. When fetching by ID, set true to fetch valid user ID, false otherwise.
4796     * @param boolean $success Whether we expect for the fetch to succeed or return false.
4797     * @param int $allowaccountssameemail Value for $CFG->allowaccountssameemail.
4798     * @param string $expectedexception The exception to be expected.
4799     */
4800    public function test_get_complete_user_data($field, $value, $success, $allowaccountssameemail = 0, $expectedexception = '') {
4801        $this->resetAfterTest();
4802
4803        // Set config settings we need for our environment.
4804        set_config('allowaccountssameemail', $allowaccountssameemail);
4805
4806        // Generate the user data.
4807        $generator = $this->getDataGenerator();
4808        $userdata = [
4809            'username' => 's1',
4810            'email' => 's1@example.com',
4811        ];
4812        $user = $generator->create_user($userdata);
4813
4814        if ($allowaccountssameemail) {
4815            // Create another user with the same email address.
4816            $generator->create_user(['email' => 's1@example.com']);
4817        }
4818
4819        // Since the data provider can't know what user ID to use, do a special handling for ID field tests.
4820        if ($field === 'id') {
4821            if ($value) {
4822                // Test for fetching data using a valid user ID. Use the generated user's ID.
4823                $value = $user->id;
4824            } else {
4825                // Test for fetching data using a non-existent user ID.
4826                $value = $user->id + 1;
4827            }
4828        }
4829
4830        // When an exception is expected.
4831        $throwexception = false;
4832        if ($expectedexception) {
4833            $this->expectException($expectedexception);
4834            $throwexception = true;
4835        }
4836
4837        $fetcheduser = get_complete_user_data($field, $value, null, $throwexception);
4838        if ($success) {
4839            $this->assertEquals($user->id, $fetcheduser->id);
4840            $this->assertEquals($user->username, $fetcheduser->username);
4841            $this->assertEquals($user->email, $fetcheduser->email);
4842        } else {
4843            $this->assertFalse($fetcheduser);
4844        }
4845    }
4846
4847    /**
4848     * Test for send_password_change_().
4849     */
4850    public function test_send_password_change_info() {
4851        $this->resetAfterTest();
4852
4853        $user = $this->getDataGenerator()->create_user();
4854
4855        $sink = $this->redirectEmails(); // Make sure we are redirecting emails.
4856        send_password_change_info($user);
4857        $result = $sink->get_messages();
4858        $sink->close();
4859
4860        $this->assertStringContainsString('passwords cannot be reset on this site', quoted_printable_decode($result[0]->body));
4861    }
4862
4863    /**
4864     * Test the get_time_interval_string for a range of inputs.
4865     *
4866     * @dataProvider get_time_interval_string_provider
4867     * @param int $time1 the time1 param.
4868     * @param int $time2 the time2 param.
4869     * @param string|null $format the format param.
4870     * @param string $expected the expected string.
4871     */
4872    public function test_get_time_interval_string(int $time1, int $time2, ?string $format, string $expected) {
4873        if (is_null($format)) {
4874            $this->assertEquals($expected, get_time_interval_string($time1, $time2));
4875        } else {
4876            $this->assertEquals($expected, get_time_interval_string($time1, $time2, $format));
4877        }
4878    }
4879
4880    /**
4881     * Data provider for the test_get_time_interval_string() method.
4882     */
4883    public function get_time_interval_string_provider() {
4884        return [
4885            'Time is after the reference time by 1 minute, omitted format' => [
4886                'time1' => 12345660,
4887                'time2' => 12345600,
4888                'format' => null,
4889                'expected' => '0d 0h 1m'
4890            ],
4891            'Time is before the reference time by 1 minute, omitted format' => [
4892                'time1' => 12345540,
4893                'time2' => 12345600,
4894                'format' => null,
4895                'expected' => '0d 0h 1m'
4896            ],
4897            'Time is equal to the reference time, omitted format' => [
4898                'time1' => 12345600,
4899                'time2' => 12345600,
4900                'format' => null,
4901                'expected' => '0d 0h 0m'
4902            ],
4903            'Time is after the reference time by 1 minute, empty string format' => [
4904                'time1' => 12345660,
4905                'time2' => 12345600,
4906                'format' => '',
4907                'expected' => '0d 0h 1m'
4908            ],
4909            'Time is before the reference time by 1 minute, empty string format' => [
4910                'time1' => 12345540,
4911                'time2' => 12345600,
4912                'format' => '',
4913                'expected' => '0d 0h 1m'
4914            ],
4915            'Time is equal to the reference time, empty string format' => [
4916                'time1' => 12345600,
4917                'time2' => 12345600,
4918                'format' => '',
4919                'expected' => '0d 0h 0m'
4920            ],
4921            'Time is after the reference time by 1 minute, custom format' => [
4922                'time1' => 12345660,
4923                'time2' => 12345600,
4924                'format' => '%R%adays %hhours %imins',
4925                'expected' => '+0days 0hours 1mins'
4926            ],
4927            'Time is before the reference time by 1 minute, custom format' => [
4928                'time1' => 12345540,
4929                'time2' => 12345600,
4930                'format' => '%R%adays %hhours %imins',
4931                'expected' => '-0days 0hours 1mins'
4932            ],
4933            'Time is equal to the reference time, custom format' => [
4934                'time1' => 12345600,
4935                'time2' => 12345600,
4936                'format' => '%R%adays %hhours %imins',
4937                'expected' => '+0days 0hours 0mins'
4938            ],
4939        ];
4940    }
4941
4942    /**
4943     * Tests the rename_to_unused_name function with a file.
4944     */
4945    public function test_rename_to_unused_name_file() {
4946        global $CFG;
4947
4948        // Create a new file in dataroot.
4949        $file = $CFG->dataroot . '/argh.txt';
4950        file_put_contents($file, 'Frogs');
4951
4952        // Rename it.
4953        $newname = rename_to_unused_name($file);
4954
4955        // Check new name has expected format.
4956        $this->assertMatchesRegularExpression('~/_temp_[a-f0-9]+$~', $newname);
4957
4958        // Check it's still in the same folder.
4959        $this->assertEquals($CFG->dataroot, dirname($newname));
4960
4961        // Check file can be loaded.
4962        $this->assertEquals('Frogs', file_get_contents($newname));
4963
4964        // OK, delete the file.
4965        unlink($newname);
4966    }
4967
4968    /**
4969     * Tests the rename_to_unused_name function with a directory.
4970     */
4971    public function test_rename_to_unused_name_dir() {
4972        global $CFG;
4973
4974        // Create a new directory in dataroot.
4975        $file = $CFG->dataroot . '/arghdir';
4976        mkdir($file);
4977
4978        // Rename it.
4979        $newname = rename_to_unused_name($file);
4980
4981        // Check new name has expected format.
4982        $this->assertMatchesRegularExpression('~/_temp_[a-f0-9]+$~', $newname);
4983
4984        // Check it's still in the same folder.
4985        $this->assertEquals($CFG->dataroot, dirname($newname));
4986
4987        // Check it's still a directory
4988        $this->assertTrue(is_dir($newname));
4989
4990        // OK, delete the directory.
4991        rmdir($newname);
4992    }
4993
4994    /**
4995     * Tests the rename_to_unused_name function with error cases.
4996     */
4997    public function test_rename_to_unused_name_failure() {
4998        global $CFG;
4999
5000        // Rename a file that doesn't exist.
5001        $file = $CFG->dataroot . '/argh.txt';
5002        $this->assertFalse(rename_to_unused_name($file));
5003    }
5004
5005    /**
5006     * Provider for display_size
5007     *
5008     * @return array of ($size, $expected)
5009     */
5010    public function display_size_provider() {
5011
5012        return [
5013            [0,     '0 bytes'    ],
5014            [1,     '1 bytes'    ],
5015            [1023,  '1023 bytes' ],
5016            [1024,      '1KB'    ],
5017            [2222,      '2.2KB'  ],
5018            [33333,     '32.6KB' ],
5019            [444444,    '434KB'  ],
5020            [5555555,       '5.3MB'  ],
5021            [66666666,      '63.6MB' ],
5022            [777777777,     '741.7MB'],
5023            [8888888888,        '8.3GB'  ],
5024            [99999999999,       '93.1GB' ],
5025            [111111111111,      '103.5GB'],
5026            [2222222222222,         '2TB'    ],
5027            [33333333333333,        '30.3TB' ],
5028            [444444444444444,       '404.2TB'],
5029            [5555555555555555,          '4.9PB'  ],
5030            [66666666666666666,         '59.2PB' ],
5031            [777777777777777777,        '690.8PB'],
5032        ];
5033    }
5034
5035    /**
5036     * Test display_size
5037     * @dataProvider display_size_provider
5038     * @param int $size the size in bytes
5039     * @param string $expected the expected string.
5040     */
5041    public function test_display_size($size, $expected) {
5042        $result = display_size($size);
5043        $this->assertEquals($expected, $result);
5044    }
5045}
5046