weekly on-sync release 5.0dev
[moodle.git] / lib / tests / moodlelib_test.php
blob45b4f7c0b330cdc6d94c6bfb199c9827cc2fc2a2
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.
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/>.
17 namespace core;
19 /**
20 * Unit tests for (some of) ../moodlelib.php.
22 * @package core
23 * @category phpunit
24 * @copyright &copy; 2006 The Open University
25 * @author T.J.Hunt@open.ac.uk
26 * @author nicolas@moodle.com
28 final class moodlelib_test extends \advanced_testcase {
30 /**
31 * Define a local decimal separator.
33 * It is not possible to directly change the result of get_string in
34 * a unit test. Instead, we create a language pack for language 'xx' in
35 * dataroot and make langconfig.php with the string we need to change.
36 * The default example separator used here is 'X'; on PHP 5.3 and before this
37 * must be a single byte character due to PHP bug/limitation in
38 * number_format, so you can't use UTF-8 characters.
40 * @param string $decsep Separator character. Defaults to `'X'`.
42 protected function define_local_decimal_separator(string $decsep = 'X') {
43 global $SESSION, $CFG;
45 $SESSION->lang = 'xx';
46 $langconfig = "<?php\n\$string['decsep'] = '$decsep';";
47 $langfolder = $CFG->dataroot . '/lang/xx';
48 check_dir_exists($langfolder);
49 file_put_contents($langfolder . '/langconfig.php', $langconfig);
51 // Ensure the new value is picked up and not taken from the cache.
52 $stringmanager = get_string_manager();
53 $stringmanager->reset_caches(true);
56 public function test_cleanremoteaddr(): void {
57 // IPv4.
58 $this->assertNull(cleanremoteaddr('1023.121.234.1'));
59 $this->assertSame('123.121.234.1', cleanremoteaddr('123.121.234.01 '));
61 // IPv6.
62 $this->assertNull(cleanremoteaddr('0:0:0:0:0:0:0:0:0'));
63 $this->assertNull(cleanremoteaddr('0:0:0:0:0:0:0:abh'));
64 $this->assertNull(cleanremoteaddr('0:0:0:::0:0:1'));
65 $this->assertSame('::', cleanremoteaddr('0:0:0:0:0:0:0:0', true));
66 $this->assertSame('::1:1', cleanremoteaddr('0:0:0:0:0:0:1:1', true));
67 $this->assertSame('abcd:ef::', cleanremoteaddr('abcd:00ef:0:0:0:0:0:0', true));
68 $this->assertSame('1::1', cleanremoteaddr('1:0:0:0:0:0:0:1', true));
69 $this->assertSame('0:0:0:0:0:0:10:1', cleanremoteaddr('::10:1', false));
70 $this->assertSame('1:1:0:0:0:0:0:0', cleanremoteaddr('01:1::', false));
71 $this->assertSame('10:0:0:0:0:0:0:10', cleanremoteaddr('10::10', false));
72 $this->assertSame('::ffff:c0a8:11', cleanremoteaddr('::ffff:192.168.1.1', true));
75 public function test_address_in_subnet(): void {
76 // 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn (number of bits in net mask).
77 $this->assertTrue(address_in_subnet('123.121.234.1', '123.121.234.1/32'));
78 $this->assertFalse(address_in_subnet('123.121.23.1', '123.121.23.0/32'));
79 $this->assertTrue(address_in_subnet('10.10.10.100', '123.121.23.45/0'));
80 $this->assertTrue(address_in_subnet('123.121.234.1', '123.121.234.0/24'));
81 $this->assertFalse(address_in_subnet('123.121.34.1', '123.121.234.0/24'));
82 $this->assertTrue(address_in_subnet('123.121.234.1', '123.121.234.0/30'));
83 $this->assertFalse(address_in_subnet('123.121.23.8', '123.121.23.0/30'));
84 $this->assertTrue(address_in_subnet('baba:baba::baba', 'baba:baba::baba/128'));
85 $this->assertFalse(address_in_subnet('bab:baba::baba', 'bab:baba::cece/128'));
86 $this->assertTrue(address_in_subnet('baba:baba::baba', 'cece:cece::cece/0'));
87 $this->assertTrue(address_in_subnet('baba:baba::baba', 'baba:baba::baba/128'));
88 $this->assertTrue(address_in_subnet('baba:baba::00ba', 'baba:baba::/120'));
89 $this->assertFalse(address_in_subnet('baba:baba::aba', 'baba:baba::/120'));
90 $this->assertTrue(address_in_subnet('baba::baba:00ba', 'baba::baba:0/112'));
91 $this->assertFalse(address_in_subnet('baba::aba:00ba', 'baba::baba:0/112'));
92 $this->assertFalse(address_in_subnet('aba::baba:0000', 'baba::baba:0/112'));
94 // Fixed input.
95 $this->assertTrue(address_in_subnet('123.121.23.1 ', ' 123.121.23.0 / 24'));
96 $this->assertTrue(address_in_subnet('::ffff:10.1.1.1', ' 0:0:0:000:0:ffff:a1:10 / 126'));
98 // Incorrect input.
99 $this->assertFalse(address_in_subnet('123.121.234.1', '123.121.234.1/-2'));
100 $this->assertFalse(address_in_subnet('123.121.234.1', '123.121.234.1/64'));
101 $this->assertFalse(address_in_subnet('123.121.234.x', '123.121.234.1/24'));
102 $this->assertFalse(address_in_subnet('123.121.234.0', '123.121.234.xx/24'));
103 $this->assertFalse(address_in_subnet('123.121.234.1', '123.121.234.1/xx0'));
104 $this->assertFalse(address_in_subnet('::1', '::aa:0/xx0'));
105 $this->assertFalse(address_in_subnet('::1', '::aa:0/-5'));
106 $this->assertFalse(address_in_subnet('::1', '::aa:0/130'));
107 $this->assertFalse(address_in_subnet('x:1', '::aa:0/130'));
108 $this->assertFalse(address_in_subnet('::1', '::ax:0/130'));
110 // 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).
111 $this->assertTrue(address_in_subnet('123.121.234.12', '123.121.234.12-14'));
112 $this->assertTrue(address_in_subnet('123.121.234.13', '123.121.234.12-14'));
113 $this->assertTrue(address_in_subnet('123.121.234.14', '123.121.234.12-14'));
114 $this->assertFalse(address_in_subnet('123.121.234.1', '123.121.234.12-14'));
115 $this->assertFalse(address_in_subnet('123.121.234.20', '123.121.234.12-14'));
116 $this->assertFalse(address_in_subnet('123.121.23.12', '123.121.234.12-14'));
117 $this->assertFalse(address_in_subnet('123.12.234.12', '123.121.234.12-14'));
118 $this->assertTrue(address_in_subnet('baba:baba::baba', 'baba:baba::baba-babe'));
119 $this->assertTrue(address_in_subnet('baba:baba::babc', 'baba:baba::baba-babe'));
120 $this->assertTrue(address_in_subnet('baba:baba::babe', 'baba:baba::baba-babe'));
121 $this->assertFalse(address_in_subnet('bab:baba::bab0', 'bab:baba::baba-babe'));
122 $this->assertFalse(address_in_subnet('bab:baba::babf', 'bab:baba::baba-babe'));
123 $this->assertFalse(address_in_subnet('bab:baba::bfbe', 'bab:baba::baba-babe'));
124 $this->assertFalse(address_in_subnet('bfb:baba::babe', 'bab:baba::baba-babe'));
126 // Fixed input.
127 $this->assertTrue(address_in_subnet('123.121.234.12', '123.121.234.12 - 14 '));
128 $this->assertTrue(address_in_subnet('bab:baba::babe', 'bab:baba::baba - babe '));
130 // Incorrect input.
131 $this->assertFalse(address_in_subnet('123.121.234.12', '123.121.234.12-234.14'));
132 $this->assertFalse(address_in_subnet('123.121.234.12', '123.121.234.12-256'));
133 $this->assertFalse(address_in_subnet('123.121.234.12', '123.121.234.12--256'));
135 // 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx. (incomplete address, a bit non-technical ;-).
136 $this->assertTrue(address_in_subnet('123.121.234.12', '123.121.234.12'));
137 $this->assertFalse(address_in_subnet('123.121.23.12', '123.121.23.13'));
138 $this->assertTrue(address_in_subnet('123.121.234.12', '123.121.234.'));
139 $this->assertTrue(address_in_subnet('123.121.234.12', '123.121.234'));
140 $this->assertTrue(address_in_subnet('123.121.234.12', '123.121'));
141 $this->assertTrue(address_in_subnet('123.121.234.12', '123'));
142 $this->assertFalse(address_in_subnet('123.121.234.1', '12.121.234.'));
143 $this->assertFalse(address_in_subnet('123.121.234.1', '12.121.234'));
144 $this->assertTrue(address_in_subnet('baba:baba::bab', 'baba:baba::bab'));
145 $this->assertFalse(address_in_subnet('baba:baba::ba', 'baba:baba::bc'));
146 $this->assertTrue(address_in_subnet('baba:baba::bab', 'baba:baba'));
147 $this->assertTrue(address_in_subnet('baba:baba::bab', 'baba:'));
148 $this->assertFalse(address_in_subnet('bab:baba::bab', 'baba:'));
150 // Multiple subnets.
151 $this->assertTrue(address_in_subnet('123.121.234.12', '::1/64, 124., 123.121.234.10-30'));
152 $this->assertTrue(address_in_subnet('124.121.234.12', '::1/64, 124., 123.121.234.10-30'));
153 $this->assertTrue(address_in_subnet('::2', '::1/64, 124., 123.121.234.10-30'));
154 $this->assertFalse(address_in_subnet('12.121.234.12', '::1/64, 124., 123.121.234.10-30'));
156 // Other incorrect input.
157 $this->assertFalse(address_in_subnet('123.123.123.123', ''));
160 public function test_fix_utf8(): void {
161 // Make sure valid data including other types is not changed.
162 $this->assertSame(null, fix_utf8(null));
163 $this->assertSame(1, fix_utf8(1));
164 $this->assertSame(1.1, fix_utf8(1.1));
165 $this->assertSame(true, fix_utf8(true));
166 $this->assertSame('', fix_utf8(''));
167 $this->assertSame('abc', fix_utf8('abc'));
168 $array = array('do', 're', 'mi');
169 $this->assertSame($array, fix_utf8($array));
170 $object = new \stdClass();
171 $object->a = 'aa';
172 $object->b = 'bb';
173 $this->assertEquals($object, fix_utf8($object));
175 // valid utf8 string
176 $this->assertSame("žlutý koníček přeskočil potůček \n\t\r", fix_utf8("žlutý koníček přeskočil potůček \n\t\r\0"));
178 // Invalid utf8 string.
179 $this->assertSame('aš', fix_utf8('a'.chr(130).'š'), 'This fails with buggy iconv() when mbstring extenstion is not available as fallback.');
180 $this->assertSame('Hello ', fix_utf8('Hello ￿'));
183 public function test_optional_param(): void {
184 global $CFG;
186 $_POST['username'] = 'post_user';
187 $_GET['username'] = 'get_user';
188 $this->assertSame($_POST['username'], optional_param('username', 'default_user', PARAM_RAW));
190 unset($_POST['username']);
191 $this->assertSame($_GET['username'], optional_param('username', 'default_user', PARAM_RAW));
193 unset($_GET['username']);
194 $this->assertSame('default_user', optional_param('username', 'default_user', PARAM_RAW));
196 // Make sure warning is displayed if array submitted - TODO: throw exception in Moodle 2.3.
197 $_POST['username'] = array('a'=>'a');
198 try {
199 optional_param('username', 'default_user', PARAM_RAW);
200 $this->fail('coding_exception expected');
201 } catch (\coding_exception $e) {
205 public function test_optional_param_array(): void {
206 global $CFG;
208 $_POST['username'] = array('a'=>'post_user');
209 $_GET['username'] = array('a'=>'get_user');
210 $this->assertSame($_POST['username'], optional_param_array('username', array('a'=>'default_user'), PARAM_RAW));
212 unset($_POST['username']);
213 $this->assertSame($_GET['username'], optional_param_array('username', array('a'=>'default_user'), PARAM_RAW));
215 unset($_GET['username']);
216 $this->assertSame(array('a'=>'default_user'), optional_param_array('username', array('a'=>'default_user'), PARAM_RAW));
218 // Do not allow nested arrays.
219 try {
220 $_POST['username'] = array('a'=>array('b'=>'post_user'));
221 optional_param_array('username', array('a'=>'default_user'), PARAM_RAW);
222 $this->fail('coding_exception expected');
223 } catch (\coding_exception $ex) {
224 $this->assertTrue(true);
227 // Do not allow non-arrays.
228 $_POST['username'] = 'post_user';
229 $this->assertSame(array('a'=>'default_user'), optional_param_array('username', array('a'=>'default_user'), PARAM_RAW));
230 $this->assertDebuggingCalled();
232 // Make sure array keys are sanitised.
233 $_POST['username'] = array('abc123_;-/*-+ '=>'arrggh', 'a1_-'=>'post_user');
234 $this->assertSame(array('a1_-'=>'post_user'), optional_param_array('username', array(), PARAM_RAW));
235 $this->assertDebuggingCalled();
238 public function test_required_param(): void {
239 $_POST['username'] = 'post_user';
240 $_GET['username'] = 'get_user';
241 $this->assertSame('post_user', required_param('username', PARAM_RAW));
243 unset($_POST['username']);
244 $this->assertSame('get_user', required_param('username', PARAM_RAW));
246 unset($_GET['username']);
247 try {
248 $this->assertSame('default_user', required_param('username', PARAM_RAW));
249 $this->fail('moodle_exception expected');
250 } catch (\moodle_exception $ex) {
251 $this->assertInstanceOf('moodle_exception', $ex);
254 try {
255 required_param('', PARAM_RAW);
256 $this->fail('coding_exception expected');
257 } catch (\moodle_exception $ex) {
260 // Make sure warning is displayed if array submitted - TODO: throw exception in Moodle 2.3.
261 $_POST['username'] = array('a'=>'a');
262 try {
263 required_param('username', PARAM_RAW);
264 $this->fail('coding_exception expected');
265 } catch (\coding_exception $e) {
269 public function test_required_param_array(): void {
270 global $CFG;
272 $_POST['username'] = array('a'=>'post_user');
273 $_GET['username'] = array('a'=>'get_user');
274 $this->assertSame($_POST['username'], required_param_array('username', PARAM_RAW));
276 unset($_POST['username']);
277 $this->assertSame($_GET['username'], required_param_array('username', PARAM_RAW));
279 // Do not allow nested arrays.
280 try {
281 $_POST['username'] = array('a'=>array('b'=>'post_user'));
282 required_param_array('username', PARAM_RAW);
283 $this->fail('coding_exception expected');
284 } catch (\moodle_exception $ex) {
285 $this->assertInstanceOf('coding_exception', $ex);
288 // Do not allow non-arrays.
289 try {
290 $_POST['username'] = 'post_user';
291 required_param_array('username', PARAM_RAW);
292 $this->fail('moodle_exception expected');
293 } catch (\moodle_exception $ex) {
294 $this->assertInstanceOf('moodle_exception', $ex);
297 // Make sure array keys are sanitised.
298 $_POST['username'] = array('abc123_;-/*-+ '=>'arrggh', 'a1_-'=>'post_user');
299 $this->assertSame(array('a1_-'=>'post_user'), required_param_array('username', PARAM_RAW));
300 $this->assertDebuggingCalled();
304 * @covers \core\param
305 * @covers \clean_param
307 public function test_clean_param(): void {
308 // Forbid objects and arrays.
309 try {
310 clean_param(array('x', 'y'), PARAM_RAW);
311 $this->fail('coding_exception expected');
312 } catch (\moodle_exception $ex) {
313 $this->assertInstanceOf('coding_exception', $ex);
315 try {
316 $param = new \stdClass();
317 $param->id = 1;
318 clean_param($param, PARAM_RAW);
319 $this->fail('coding_exception expected');
320 } catch (\moodle_exception $ex) {
321 $this->assertInstanceOf('coding_exception', $ex);
324 // Require correct type.
325 try {
326 clean_param('x', 'xxxxxx');
327 $this->fail('moodle_exception expected');
328 } catch (\moodle_exception $ex) {
329 $this->assertInstanceOf('moodle_exception', $ex);
334 * @covers \core\param
335 * @covers \clean_param
337 public function test_clean_param_array(): void {
338 $this->assertSame(array(), clean_param_array(null, PARAM_RAW));
339 $this->assertSame(array('a', 'b'), clean_param_array(array('a', 'b'), PARAM_RAW));
340 $this->assertSame(array('a', array('b')), clean_param_array(array('a', array('b')), PARAM_RAW, true));
342 // Require correct type.
343 try {
344 clean_param_array(array('x'), 'xxxxxx');
345 $this->fail('moodle_exception expected');
346 } catch (\moodle_exception $ex) {
347 $this->assertInstanceOf('moodle_exception', $ex);
350 try {
351 clean_param_array(array('x', array('y')), PARAM_RAW);
352 $this->fail('coding_exception expected');
353 } catch (\moodle_exception $ex) {
354 $this->assertInstanceOf('coding_exception', $ex);
357 // Test recursive.
361 * @covers \core\param
362 * @covers \clean_param
364 public function test_clean_param_raw(): void {
365 $this->assertSame(
366 '#()*#,9789\'".,<42897></?$(*DSFMO#$*)(SDJ)($*)',
367 clean_param('#()*#,9789\'".,<42897></?$(*DSFMO#$*)(SDJ)($*)', PARAM_RAW));
368 $this->assertSame(null, clean_param(null, PARAM_RAW));
372 * @covers \core\param
373 * @covers \clean_param
375 public function test_clean_param_trim(): void {
376 $this->assertSame('Frog toad', clean_param(" Frog toad \r\n ", PARAM_RAW_TRIMMED));
377 $this->assertSame('', clean_param(null, PARAM_RAW_TRIMMED));
381 * @covers \core\param
382 * @covers \clean_param
384 public function test_clean_param_clean(): void {
385 // PARAM_CLEAN is an ugly hack, do not use in new code (skodak),
386 // instead use more specific type, or submit sothing that can be verified properly.
387 $this->assertSame('xx', clean_param('xx<script>', PARAM_CLEAN));
388 $this->assertSame('', clean_param(null, PARAM_CLEAN));
389 $this->assertSame('', clean_param(null, PARAM_CLEANHTML));
393 * @covers \core\param
394 * @covers \clean_param
396 public function test_clean_param_alpha(): void {
397 $this->assertSame('DSFMOSDJ', clean_param('#()*#,9789\'".,<42897></?$(*DSFMO#$*)(SDJ)($*)', PARAM_ALPHA));
398 $this->assertSame('', clean_param(null, PARAM_ALPHA));
402 * @covers \core\param
403 * @covers \clean_param
405 public function test_clean_param_alphanum(): void {
406 $this->assertSame('978942897DSFMOSDJ', clean_param('#()*#,9789\'".,<42897></?$(*DSFMO#$*)(SDJ)($*)', PARAM_ALPHANUM));
407 $this->assertSame('', clean_param(null, PARAM_ALPHANUM));
411 * @covers \core\param
412 * @covers \clean_param
414 public function test_clean_param_alphaext(): void {
415 $this->assertSame('DSFMOSDJ', clean_param('#()*#,9789\'".,<42897></?$(*DSFMO#$*)(SDJ)($*)', PARAM_ALPHAEXT));
416 $this->assertSame('', clean_param(null, PARAM_ALPHAEXT));
420 * @covers \core\param
421 * @covers \clean_param
423 public function test_clean_param_sequence(): void {
424 $this->assertSame(',9789,42897', clean_param('#()*#,9789\'".,<42897></?$(*DSFMO#$*)(SDJ)($*)', PARAM_SEQUENCE));
425 $this->assertSame('', clean_param(null, PARAM_SEQUENCE));
429 * @covers \core\param
430 * @covers \clean_param
432 public function test_clean_param_component(): void {
433 // Please note the cleaning of component names is very strict, no guessing here.
434 $this->assertSame('mod_forum', clean_param('mod_forum', PARAM_COMPONENT));
435 $this->assertSame('block_online_users', clean_param('block_online_users', PARAM_COMPONENT));
436 $this->assertSame('block_blond_online_users', clean_param('block_blond_online_users', PARAM_COMPONENT));
437 $this->assertSame('mod_something2', clean_param('mod_something2', PARAM_COMPONENT));
438 $this->assertSame('forum', clean_param('forum', PARAM_COMPONENT));
439 $this->assertSame('user', clean_param('user', PARAM_COMPONENT));
440 $this->assertSame('rating', clean_param('rating', PARAM_COMPONENT));
441 $this->assertSame('feedback360', clean_param('feedback360', PARAM_COMPONENT));
442 $this->assertSame('mod_feedback360', clean_param('mod_feedback360', PARAM_COMPONENT));
443 $this->assertSame('', clean_param('mod_2something', PARAM_COMPONENT));
444 $this->assertSame('', clean_param('2mod_something', PARAM_COMPONENT));
445 $this->assertSame('', clean_param('mod_something_xx', PARAM_COMPONENT));
446 $this->assertSame('', clean_param('auth_something__xx', PARAM_COMPONENT));
447 $this->assertSame('', clean_param('mod_Something', PARAM_COMPONENT));
448 $this->assertSame('', clean_param('mod_somethíng', PARAM_COMPONENT));
449 $this->assertSame('', clean_param('mod__something', PARAM_COMPONENT));
450 $this->assertSame('', clean_param('auth_xx-yy', PARAM_COMPONENT));
451 $this->assertSame('', clean_param('_auth_xx', PARAM_COMPONENT));
452 $this->assertSame('a2uth_xx', clean_param('a2uth_xx', PARAM_COMPONENT));
453 $this->assertSame('', clean_param('auth_xx_', PARAM_COMPONENT));
454 $this->assertSame('', clean_param('auth_xx.old', PARAM_COMPONENT));
455 $this->assertSame('', clean_param('_user', PARAM_COMPONENT));
456 $this->assertSame('', clean_param('2rating', PARAM_COMPONENT));
457 $this->assertSame('', clean_param('user_', PARAM_COMPONENT));
458 $this->assertSame('', clean_param(null, PARAM_COMPONENT));
462 * @covers \core\param
463 * @covers \clean_param
465 public function test_clean_param_localisedfloat(): void {
467 $this->assertSame(0.5, clean_param('0.5', PARAM_LOCALISEDFLOAT));
468 $this->assertSame(false, clean_param('0X5', PARAM_LOCALISEDFLOAT));
469 $this->assertSame(0.5, clean_param('.5', PARAM_LOCALISEDFLOAT));
470 $this->assertSame(false, clean_param('X5', PARAM_LOCALISEDFLOAT));
471 $this->assertSame(10.5, clean_param('10.5', PARAM_LOCALISEDFLOAT));
472 $this->assertSame(false, clean_param('10X5', PARAM_LOCALISEDFLOAT));
473 $this->assertSame(1000.5, clean_param('1 000.5', PARAM_LOCALISEDFLOAT));
474 $this->assertSame(false, clean_param('1 000X5', PARAM_LOCALISEDFLOAT));
475 $this->assertSame(false, clean_param('1.000.5', PARAM_LOCALISEDFLOAT));
476 $this->assertSame(false, clean_param('1X000X5', PARAM_LOCALISEDFLOAT));
477 $this->assertSame(false, clean_param('nan', PARAM_LOCALISEDFLOAT));
478 $this->assertSame(false, clean_param('10.6blah', PARAM_LOCALISEDFLOAT));
479 $this->assertSame(null, clean_param(null, PARAM_LOCALISEDFLOAT));
481 // Tests with a localised decimal separator.
482 $this->define_local_decimal_separator();
484 $this->assertSame(0.5, clean_param('0.5', PARAM_LOCALISEDFLOAT));
485 $this->assertSame(0.5, clean_param('0X5', PARAM_LOCALISEDFLOAT));
486 $this->assertSame(0.5, clean_param('.5', PARAM_LOCALISEDFLOAT));
487 $this->assertSame(0.5, clean_param('X5', PARAM_LOCALISEDFLOAT));
488 $this->assertSame(10.5, clean_param('10.5', PARAM_LOCALISEDFLOAT));
489 $this->assertSame(10.5, clean_param('10X5', PARAM_LOCALISEDFLOAT));
490 $this->assertSame(1000.5, clean_param('1 000.5', PARAM_LOCALISEDFLOAT));
491 $this->assertSame(1000.5, clean_param('1 000X5', PARAM_LOCALISEDFLOAT));
492 $this->assertSame(false, clean_param('1.000.5', PARAM_LOCALISEDFLOAT));
493 $this->assertSame(false, clean_param('1X000X5', PARAM_LOCALISEDFLOAT));
494 $this->assertSame(false, clean_param('nan', PARAM_LOCALISEDFLOAT));
495 $this->assertSame(false, clean_param('10X6blah', PARAM_LOCALISEDFLOAT));
498 public function test_is_valid_plugin_name(): void {
499 $this->assertTrue(is_valid_plugin_name('forum'));
500 $this->assertTrue(is_valid_plugin_name('forum2'));
501 $this->assertTrue(is_valid_plugin_name('feedback360'));
502 $this->assertTrue(is_valid_plugin_name('online_users'));
503 $this->assertTrue(is_valid_plugin_name('blond_online_users'));
504 $this->assertFalse(is_valid_plugin_name('online__users'));
505 $this->assertFalse(is_valid_plugin_name('forum '));
506 $this->assertFalse(is_valid_plugin_name('forum.old'));
507 $this->assertFalse(is_valid_plugin_name('xx-yy'));
508 $this->assertFalse(is_valid_plugin_name('2xx'));
509 $this->assertFalse(is_valid_plugin_name('Xx'));
510 $this->assertFalse(is_valid_plugin_name('_xx'));
511 $this->assertFalse(is_valid_plugin_name('xx_'));
515 * @covers \core\param
516 * @covers \clean_param
518 public function test_clean_param_plugin(): void {
519 // Please note the cleaning of plugin names is very strict, no guessing here.
520 $this->assertSame('forum', clean_param('forum', PARAM_PLUGIN));
521 $this->assertSame('forum2', clean_param('forum2', PARAM_PLUGIN));
522 $this->assertSame('feedback360', clean_param('feedback360', PARAM_PLUGIN));
523 $this->assertSame('online_users', clean_param('online_users', PARAM_PLUGIN));
524 $this->assertSame('blond_online_users', clean_param('blond_online_users', PARAM_PLUGIN));
525 $this->assertSame('', clean_param('online__users', PARAM_PLUGIN));
526 $this->assertSame('', clean_param('forum ', PARAM_PLUGIN));
527 $this->assertSame('', clean_param('forum.old', PARAM_PLUGIN));
528 $this->assertSame('', clean_param('xx-yy', PARAM_PLUGIN));
529 $this->assertSame('', clean_param('2xx', PARAM_PLUGIN));
530 $this->assertSame('', clean_param('Xx', PARAM_PLUGIN));
531 $this->assertSame('', clean_param('_xx', PARAM_PLUGIN));
532 $this->assertSame('', clean_param('xx_', PARAM_PLUGIN));
533 $this->assertSame('', clean_param(null, PARAM_PLUGIN));
537 * @covers \core\param
538 * @covers \clean_param
540 public function test_clean_param_area(): void {
541 // Please note the cleaning of area names is very strict, no guessing here.
542 $this->assertSame('something', clean_param('something', PARAM_AREA));
543 $this->assertSame('something2', clean_param('something2', PARAM_AREA));
544 $this->assertSame('some_thing', clean_param('some_thing', PARAM_AREA));
545 $this->assertSame('some_thing_xx', clean_param('some_thing_xx', PARAM_AREA));
546 $this->assertSame('feedback360', clean_param('feedback360', PARAM_AREA));
547 $this->assertSame('', clean_param('_something', PARAM_AREA));
548 $this->assertSame('', clean_param('something_', PARAM_AREA));
549 $this->assertSame('', clean_param('2something', PARAM_AREA));
550 $this->assertSame('', clean_param('Something', PARAM_AREA));
551 $this->assertSame('', clean_param('some-thing', PARAM_AREA));
552 $this->assertSame('', clean_param('somethííng', PARAM_AREA));
553 $this->assertSame('', clean_param('something.x', PARAM_AREA));
554 $this->assertSame('', clean_param(null, PARAM_AREA));
558 * @covers \core\param
559 * @covers \clean_param
561 public function test_clean_param_text(): void {
562 // Standard.
563 $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));
564 $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));
565 $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));
566 // Malformed.
567 $this->assertSame('<span lang="en" class="multilang">aa</span>', clean_param('<span lang="en" class="multilang">aa</span>', PARAM_TEXT));
568 $this->assertSame('aa', clean_param('<span lang="en" class="nothing" class="multilang">aa</span>', PARAM_TEXT));
569 $this->assertSame('aa', clean_param('<lang lang="en" class="multilang">aa</lang>', PARAM_TEXT));
570 $this->assertSame('aa', clean_param('<lang lang="en!!">aa</lang>', PARAM_TEXT));
571 $this->assertSame('aa', clean_param('<span lang="en==" class="multilang">aa</span>', PARAM_TEXT));
572 $this->assertSame('abc', clean_param('a<em>b</em>c', PARAM_TEXT));
573 $this->assertSame('a>c>', clean_param('a><xx >c>', PARAM_TEXT)); // Standard strip_tags() behaviour.
574 $this->assertSame('a', clean_param('a<b', PARAM_TEXT));
575 $this->assertSame('a>b', clean_param('a>b', PARAM_TEXT));
576 $this->assertSame('<lang lang="en">a>a</lang>', clean_param('<lang lang="en">a>a</lang>', PARAM_TEXT)); // Standard strip_tags() behaviour.
577 $this->assertSame('a', clean_param('<lang lang="en">a<a</lang>', PARAM_TEXT));
578 $this->assertSame('<lang lang="en">aa</lang>', clean_param('<lang lang="en">a<br>a</lang>', PARAM_TEXT));
579 $this->assertSame('', clean_param(null, PARAM_TEXT));
583 * Data provider for {@see test_clean_param_host}
585 * @return array
587 public static function clean_param_host_provider(): array {
588 return [
589 'Valid (low octets)' => ['0.0.0.0', '0.0.0.0'],
590 'Valid (high octets)' => ['255.255.255.255', '255.255.255.255'],
591 'Invalid first octet' => ['256.1.1.1', ''],
592 'Invalid second octet' => ['1.256.1.1', ''],
593 'Invalid third octet' => ['1.1.256.1', ''],
594 'Invalid fourth octet' => ['1.1.1.256', ''],
595 'Valid host' => ['moodle.org', 'moodle.org'],
596 'Invalid host' => ['.example.com', ''],
601 * Testing cleaning parameters with PARAM_HOST
603 * @param string $param
604 * @param string $expected
606 * @dataProvider clean_param_host_provider
608 * @covers \core\param
609 * @covers \clean_param
611 public function test_clean_param_host(string $param, string $expected): void {
612 $this->assertEquals($expected, clean_param($param, PARAM_HOST));
616 * @covers \core\param
617 * @covers \clean_param
619 public function test_clean_param_url(): void {
620 // Test PARAM_URL and PARAM_LOCALURL a bit.
621 // Valid URLs.
622 $this->assertSame('http://google.com/', clean_param('http://google.com/', PARAM_URL));
623 $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));
624 $this->assertSame('http://localhost/', clean_param('http://localhost/', PARAM_URL));
625 $this->assertSame('http://0.255.1.1/numericip.php', clean_param('http://0.255.1.1/numericip.php', PARAM_URL));
626 $this->assertSame('https://google.com/', clean_param('https://google.com/', PARAM_URL));
627 $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));
628 $this->assertSame('https://localhost/', clean_param('https://localhost/', PARAM_URL));
629 $this->assertSame('https://0.255.1.1/numericip.php', clean_param('https://0.255.1.1/numericip.php', PARAM_URL));
630 $this->assertSame('ftp://ftp.debian.org/debian/', clean_param('ftp://ftp.debian.org/debian/', PARAM_URL));
631 $this->assertSame('/just/a/path', clean_param('/just/a/path', PARAM_URL));
632 // Invalid URLs.
633 $this->assertSame('', clean_param('funny:thing', PARAM_URL));
634 $this->assertSame('', clean_param('http://example.ee/sdsf"f', PARAM_URL));
635 $this->assertSame('', clean_param('javascript://comment%0Aalert(1)', PARAM_URL));
636 $this->assertSame('', clean_param('rtmp://example.com/livestream', PARAM_URL));
637 $this->assertSame('', clean_param('rtmp://example.com/live&foo', PARAM_URL));
638 $this->assertSame('', clean_param('rtmp://example.com/fms&mp4:path/to/file.mp4', PARAM_URL));
639 $this->assertSame('', clean_param('mailto:support@moodle.org', PARAM_URL));
640 $this->assertSame('', clean_param('mailto:support@moodle.org?subject=Hello%20Moodle', PARAM_URL));
641 $this->assertSame('', clean_param('mailto:support@moodle.org?subject=Hello%20Moodle&cc=feedback@moodle.org', PARAM_URL));
642 $this->assertSame('', clean_param(null, PARAM_URL));
646 * @covers \core\param
647 * @covers \clean_param
649 public function test_clean_param_localurl(): void {
650 global $CFG;
652 $this->resetAfterTest();
654 // External, invalid.
655 $this->assertSame('', clean_param('funny:thing', PARAM_LOCALURL));
656 $this->assertSame('', clean_param('http://google.com/', PARAM_LOCALURL));
657 $this->assertSame('', clean_param('https://google.com/?test=true', PARAM_LOCALURL));
658 $this->assertSame('', clean_param('http://some.very.long.and.silly.domain/with/a/path/', PARAM_LOCALURL));
660 // Local absolute.
661 $this->assertSame(clean_param($CFG->wwwroot, PARAM_LOCALURL), $CFG->wwwroot);
662 $this->assertSame($CFG->wwwroot . '/with/something?else=true',
663 clean_param($CFG->wwwroot . '/with/something?else=true', PARAM_LOCALURL));
665 // Local relative.
666 $this->assertSame('/just/a/path', clean_param('/just/a/path', PARAM_LOCALURL));
667 $this->assertSame('course/view.php?id=3', clean_param('course/view.php?id=3', PARAM_LOCALURL));
669 // Local absolute HTTPS in a non HTTPS site.
670 $CFG->wwwroot = str_replace('https:', 'http:', $CFG->wwwroot); // Need to simulate non-https site.
671 $httpsroot = str_replace('http:', 'https:', $CFG->wwwroot);
672 $this->assertSame('', clean_param($httpsroot, PARAM_LOCALURL));
673 $this->assertSame('', clean_param($httpsroot . '/with/something?else=true', PARAM_LOCALURL));
675 // Local absolute HTTPS in a HTTPS site.
676 $CFG->wwwroot = str_replace('http:', 'https:', $CFG->wwwroot);
677 $httpsroot = $CFG->wwwroot;
678 $this->assertSame($httpsroot, clean_param($httpsroot, PARAM_LOCALURL));
679 $this->assertSame($httpsroot . '/with/something?else=true',
680 clean_param($httpsroot . '/with/something?else=true', PARAM_LOCALURL));
682 // Test open redirects are not possible.
683 $CFG->wwwroot = 'http://www.example.com';
684 $this->assertSame('', clean_param('http://www.example.com.evil.net/hack.php', PARAM_LOCALURL));
685 $CFG->wwwroot = 'https://www.example.com';
686 $this->assertSame('', clean_param('https://www.example.com.evil.net/hack.php', PARAM_LOCALURL));
688 $this->assertSame('', clean_param('', PARAM_LOCALURL));
689 $this->assertSame('', clean_param(null, PARAM_LOCALURL));
693 * @covers \core\param
694 * @covers \clean_param
696 public function test_clean_param_file(): void {
697 $this->assertSame('correctfile.txt', clean_param('correctfile.txt', PARAM_FILE));
698 $this->assertSame('badfile.txt', clean_param('b\'a<d`\\/fi:l>e.t"x|t', PARAM_FILE));
699 $this->assertSame('..parentdirfile.txt', clean_param('../parentdirfile.txt', PARAM_FILE));
700 $this->assertSame('....grandparentdirfile.txt', clean_param('../../grandparentdirfile.txt', PARAM_FILE));
701 $this->assertSame('..winparentdirfile.txt', clean_param('..\winparentdirfile.txt', PARAM_FILE));
702 $this->assertSame('....wingrandparentdir.txt', clean_param('..\..\wingrandparentdir.txt', PARAM_FILE));
703 $this->assertSame('myfile.a.b.txt', clean_param('myfile.a.b.txt', PARAM_FILE));
704 $this->assertSame('myfile..a..b.txt', clean_param('myfile..a..b.txt', PARAM_FILE));
705 $this->assertSame('myfile.a..b...txt', clean_param('myfile.a..b...txt', PARAM_FILE));
706 $this->assertSame('myfile.a.txt', clean_param('myfile.a.txt', PARAM_FILE));
707 $this->assertSame('myfile...txt', clean_param('myfile...txt', PARAM_FILE));
708 $this->assertSame('...jpg', clean_param('...jpg', PARAM_FILE));
709 $this->assertSame('.a.b.', clean_param('.a.b.', PARAM_FILE));
710 $this->assertSame('', clean_param('.', PARAM_FILE));
711 $this->assertSame('', clean_param('..', PARAM_FILE));
712 $this->assertSame('...', clean_param('...', PARAM_FILE));
713 $this->assertSame('. . . .', clean_param('. . . .', PARAM_FILE));
714 $this->assertSame('dontrtrim.me. .. .. . ', clean_param('dontrtrim.me. .. .. . ', PARAM_FILE));
715 $this->assertSame(' . .dontltrim.me', clean_param(' . .dontltrim.me', PARAM_FILE));
716 $this->assertSame('here is a tab.txt', clean_param("here is a tab\t.txt", PARAM_FILE));
717 $this->assertSame('here is a linebreak.txt', clean_param("here is a line\r\nbreak.txt", PARAM_FILE));
718 $this->assertSame('', clean_param(null, PARAM_FILE));
720 // The following behaviours have been maintained although they seem a little odd.
721 $this->assertSame('funnything', clean_param('funny:thing', PARAM_FILE));
722 $this->assertSame('.currentdirfile.txt', clean_param('./currentdirfile.txt', PARAM_FILE));
723 $this->assertSame('ctempwindowsfile.txt', clean_param('c:\temp\windowsfile.txt', PARAM_FILE));
724 $this->assertSame('homeuserlinuxfile.txt', clean_param('/home/user/linuxfile.txt', PARAM_FILE));
725 $this->assertSame('~myfile.txt', clean_param('~/myfile.txt', PARAM_FILE));
729 * @covers \core\param
730 * @covers \clean_param
732 public function test_clean_param_path(): void {
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 $this->assertSame('', clean_param(null, PARAM_PATH));
754 * @covers \core\param
755 * @covers \clean_param
757 public function test_clean_param_safepath(): void {
758 $this->assertSame('folder/file', clean_param('folder/file', PARAM_SAFEPATH));
759 $this->assertSame('folder//file', clean_param('folder/../file', PARAM_SAFEPATH));
760 $this->assertSame('', clean_param(null, PARAM_SAFEPATH));
764 * @covers \core\param
765 * @covers \clean_param
767 public function test_clean_param_username(): void {
768 global $CFG;
769 $currentstatus = $CFG->extendedusernamechars;
771 // Run tests with extended character == false;.
772 $CFG->extendedusernamechars = false;
773 $this->assertSame('johndoe123', clean_param('johndoe123', PARAM_USERNAME) );
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('john-doe', clean_param('john- doe', PARAM_USERNAME));
777 $this->assertSame('john_doe', clean_param('john_doe', PARAM_USERNAME));
778 $this->assertSame('john@doe', clean_param('john@doe', PARAM_USERNAME));
779 $this->assertSame('johndoe', clean_param('john~doe', PARAM_USERNAME));
780 $this->assertSame('johndoe', clean_param('john´doe', PARAM_USERNAME));
781 $this->assertSame(clean_param('john# $%&()+_^', PARAM_USERNAME), 'john_');
782 $this->assertSame(clean_param(' john# $%&()+_^ ', PARAM_USERNAME), 'john_');
783 $this->assertSame(clean_param('john#$%&() ', PARAM_USERNAME), 'john');
784 $this->assertSame('johnd', clean_param('JOHNdóé ', PARAM_USERNAME));
785 $this->assertSame(clean_param('john.,:;-_/|\ñÑ[]A_X-,D {} ~!@#$%^&*()_+ ?><[] ščřžžý ?ýáž?žý??šdoe ', PARAM_USERNAME), 'john.-_a_x-d@_doe');
786 $this->assertSame('', clean_param(null, PARAM_USERNAME));
788 // Test success condition, if extendedusernamechars == ENABLE;.
789 $CFG->extendedusernamechars = true;
790 $this->assertSame('john_doe', clean_param('john_doe', PARAM_USERNAME));
791 $this->assertSame('john@doe', clean_param('john@doe', PARAM_USERNAME));
792 $this->assertSame(clean_param('john# $%&()+_^', PARAM_USERNAME), 'john# $%&()+_^');
793 $this->assertSame(clean_param(' john# $%&()+_^ ', PARAM_USERNAME), 'john# $%&()+_^');
794 $this->assertSame('john~doe', clean_param('john~doe', PARAM_USERNAME));
795 $this->assertSame('john´doe', clean_param('joHN´doe', PARAM_USERNAME));
796 $this->assertSame('johndoe', clean_param('johnDOE', PARAM_USERNAME));
797 $this->assertSame('johndóé', clean_param('johndóé ', PARAM_USERNAME));
799 $CFG->extendedusernamechars = $currentstatus;
803 * @covers \core\param
804 * @covers \clean_param
806 public function test_clean_param_stringid(): void {
807 // Test string identifiers validation.
808 // Valid strings.
809 $this->assertSame('validstring', clean_param('validstring', PARAM_STRINGID));
810 $this->assertSame('mod/foobar:valid_capability', clean_param('mod/foobar:valid_capability', PARAM_STRINGID));
811 $this->assertSame('CZ', clean_param('CZ', PARAM_STRINGID));
812 $this->assertSame('application/vnd.ms-powerpoint', clean_param('application/vnd.ms-powerpoint', PARAM_STRINGID));
813 $this->assertSame('grade2', clean_param('grade2', PARAM_STRINGID));
814 // Invalid strings.
815 $this->assertSame('', clean_param('trailing ', PARAM_STRINGID));
816 $this->assertSame('', clean_param('space bar', PARAM_STRINGID));
817 $this->assertSame('', clean_param('0numeric', PARAM_STRINGID));
818 $this->assertSame('', clean_param('*', PARAM_STRINGID));
819 $this->assertSame('', clean_param(' ', PARAM_STRINGID));
820 $this->assertSame('', clean_param(null, PARAM_STRINGID));
824 * @covers \core\param
825 * @covers \clean_param
827 public function test_clean_param_timezone(): void {
828 // Test timezone validation.
829 $testvalues = array (
830 'America/Jamaica' => 'America/Jamaica',
831 'America/Argentina/Cordoba' => 'America/Argentina/Cordoba',
832 'America/Port-au-Prince' => 'America/Port-au-Prince',
833 'America/Argentina/Buenos_Aires' => 'America/Argentina/Buenos_Aires',
834 'PST8PDT' => 'PST8PDT',
835 'Wrong.Value' => '',
836 'Wrong/.Value' => '',
837 'Wrong(Value)' => '',
838 '0' => '0',
839 '0.0' => '0.0',
840 '0.5' => '0.5',
841 '9.0' => '9.0',
842 '-9.0' => '-9.0',
843 '+9.0' => '+9.0',
844 '9.5' => '9.5',
845 '-9.5' => '-9.5',
846 '+9.5' => '+9.5',
847 '12.0' => '12.0',
848 '-12.0' => '-12.0',
849 '+12.0' => '+12.0',
850 '12.5' => '12.5',
851 '-12.5' => '-12.5',
852 '+12.5' => '+12.5',
853 '13.0' => '13.0',
854 '-13.0' => '-13.0',
855 '+13.0' => '+13.0',
856 '13.5' => '',
857 '+13.5' => '',
858 '-13.5' => '',
859 '0.2' => '',
860 '' => '',
863 foreach ($testvalues as $testvalue => $expectedvalue) {
864 $actualvalue = clean_param($testvalue, PARAM_TIMEZONE);
865 $this->assertEquals($expectedvalue, $actualvalue);
868 // Test for null.
869 $this->assertEquals('', clean_param(null, PARAM_TIMEZONE));
873 * @covers \core\param
874 * @covers \clean_param
876 public function test_clean_param_null_argument(): void {
877 $this->assertEquals(0, clean_param(null, PARAM_INT));
878 $this->assertEquals(0, clean_param(null, PARAM_FLOAT));
879 $this->assertEquals(0, clean_param(null, PARAM_LOCALISEDFLOAT));
880 $this->assertEquals(false, clean_param(null, PARAM_BOOL));
881 $this->assertEquals('', clean_param(null, PARAM_NOTAGS));
882 $this->assertEquals('', clean_param(null, PARAM_SAFEDIR));
883 $this->assertEquals('', clean_param(null, PARAM_HOST));
884 $this->assertEquals('', clean_param(null, PARAM_PEM));
885 $this->assertEquals('', clean_param(null, PARAM_BASE64));
886 $this->assertEquals('', clean_param(null, PARAM_TAG));
887 $this->assertEquals('', clean_param(null, PARAM_TAGLIST));
888 $this->assertEquals('', clean_param(null, PARAM_CAPABILITY));
889 $this->assertEquals(0, clean_param(null, PARAM_PERMISSION));
890 $this->assertEquals('', clean_param(null, PARAM_AUTH));
891 $this->assertEquals('', clean_param(null, PARAM_LANG));
892 $this->assertEquals('', clean_param(null, PARAM_THEME));
893 $this->assertEquals('', clean_param(null, PARAM_EMAIL));
896 public function test_validate_param(): void {
897 try {
898 $param = validate_param('11a', PARAM_INT);
899 $this->fail('invalid_parameter_exception expected');
900 } catch (\moodle_exception $ex) {
901 $this->assertInstanceOf('invalid_parameter_exception', $ex);
904 $param = validate_param('11', PARAM_INT);
905 $this->assertSame(11, $param);
907 try {
908 $param = validate_param(null, PARAM_INT, false);
909 $this->fail('invalid_parameter_exception expected');
910 } catch (\moodle_exception $ex) {
911 $this->assertInstanceOf('invalid_parameter_exception', $ex);
914 $param = validate_param(null, PARAM_INT, true);
915 $this->assertSame(null, $param);
917 try {
918 $param = validate_param(array(), PARAM_INT);
919 $this->fail('invalid_parameter_exception expected');
920 } catch (\moodle_exception $ex) {
921 $this->assertInstanceOf('invalid_parameter_exception', $ex);
923 try {
924 $param = validate_param(new \stdClass, PARAM_INT);
925 $this->fail('invalid_parameter_exception expected');
926 } catch (\moodle_exception $ex) {
927 $this->assertInstanceOf('invalid_parameter_exception', $ex);
930 $param = validate_param('1.0', PARAM_FLOAT);
931 $this->assertSame(1.0, $param);
933 // Make sure valid floats do not cause exception.
934 validate_param(1.0, PARAM_FLOAT);
935 validate_param(10, PARAM_FLOAT);
936 validate_param('0', PARAM_FLOAT);
937 validate_param('119813454.545464564564546564545646556564465465456465465465645645465645645645', PARAM_FLOAT);
938 validate_param('011.1', PARAM_FLOAT);
939 validate_param('11', PARAM_FLOAT);
940 validate_param('+.1', PARAM_FLOAT);
941 validate_param('-.1', PARAM_FLOAT);
942 validate_param('1e10', PARAM_FLOAT);
943 validate_param('.1e+10', PARAM_FLOAT);
944 validate_param('1E-1', PARAM_FLOAT);
946 try {
947 $param = validate_param('1,2', PARAM_FLOAT);
948 $this->fail('invalid_parameter_exception expected');
949 } catch (\moodle_exception $ex) {
950 $this->assertInstanceOf('invalid_parameter_exception', $ex);
952 try {
953 $param = validate_param('', PARAM_FLOAT);
954 $this->fail('invalid_parameter_exception expected');
955 } catch (\moodle_exception $ex) {
956 $this->assertInstanceOf('invalid_parameter_exception', $ex);
958 try {
959 $param = validate_param('.', PARAM_FLOAT);
960 $this->fail('invalid_parameter_exception expected');
961 } catch (\moodle_exception $ex) {
962 $this->assertInstanceOf('invalid_parameter_exception', $ex);
964 try {
965 $param = validate_param('e10', PARAM_FLOAT);
966 $this->fail('invalid_parameter_exception expected');
967 } catch (\moodle_exception $ex) {
968 $this->assertInstanceOf('invalid_parameter_exception', $ex);
970 try {
971 $param = validate_param('abc', PARAM_FLOAT);
972 $this->fail('invalid_parameter_exception expected');
973 } catch (\moodle_exception $ex) {
974 $this->assertInstanceOf('invalid_parameter_exception', $ex);
978 public function test_shorten_text_no_tags_already_short_enough(): void {
979 // ......12345678901234567890123456.
980 $text = "short text already no tags";
981 $this->assertSame($text, shorten_text($text));
984 public function test_shorten_text_with_tags_already_short_enough(): void {
985 // .........123456...7890....12345678.......901234567.
986 $text = "<p>short <b>text</b> already</p><p>with tags</p>";
987 $this->assertSame($text, shorten_text($text));
990 public function test_shorten_text_no_tags_needs_shortening(): void {
991 // Default truncation is after 30 chars, but allowing 3 for the final '...'.
992 // ......12345678901234567890123456789023456789012345678901234.
993 $text = "long text without any tags blah de blah blah blah what";
994 $this->assertSame('long text without any tags ...', shorten_text($text));
997 public function test_shorten_text_with_tags_needs_shortening(): void {
998 // .......................................123456789012345678901234567890...
999 $text = "<div class='frog'><p><blockquote>Long text with tags that will ".
1000 "be chopped off but <b>should be added back again</b></blockquote></p></div>";
1001 $this->assertEquals("<div class='frog'><p><blockquote>Long text with " .
1002 "tags that ...</blockquote></p></div>", shorten_text($text));
1005 public function test_shorten_text_with_tags_and_html_comment(): void {
1006 $text = "<div class='frog'><p><blockquote><!--[if !IE]><!-->Long text with ".
1007 "tags that will<!--<![endif]--> ".
1008 "be chopped off but <b>should be added back again</b></blockquote></p></div>";
1009 $this->assertEquals("<div class='frog'><p><blockquote><!--[if !IE]><!-->Long text with " .
1010 "tags that ...<!--<![endif]--></blockquote></p></div>", shorten_text($text));
1013 public function test_shorten_text_with_entities(): void {
1014 // Remember to allow 3 chars for the final '...'.
1015 // ......123456789012345678901234567_____890...
1016 $text = "some text which shouldn't &nbsp; break there";
1017 $this->assertSame("some text which shouldn't &nbsp; ...", shorten_text($text, 31));
1018 $this->assertSame("some text which shouldn't &nbsp;...", shorten_text($text, 30));
1019 $this->assertSame("some text which shouldn't ...", shorten_text($text, 29));
1022 public function test_shorten_text_known_tricky_case(): void {
1023 // This case caused a bug up to 1.9.5
1024 // ..........123456789012345678901234567890123456789.....0_____1___2___...
1025 $text = "<h3>standard 'break-out' sub groups in TGs?</h3>&nbsp;&lt;&lt;There are several";
1026 $this->assertSame("<h3>standard 'break-out' sub groups in ...</h3>",
1027 shorten_text($text, 41));
1028 $this->assertSame("<h3>standard 'break-out' sub groups in TGs?...</h3>",
1029 shorten_text($text, 42));
1030 $this->assertSame("<h3>standard 'break-out' sub groups in TGs?</h3>&nbsp;...",
1031 shorten_text($text, 43));
1034 public function test_shorten_text_no_spaces(): void {
1035 // ..........123456789.
1036 $text = "<h1>123456789</h1>"; // A string with no convenient breaks.
1037 $this->assertSame("<h1>12345...</h1>", shorten_text($text, 8));
1040 public function test_shorten_text_utf8_european(): void {
1041 // Text without tags.
1042 // ......123456789012345678901234567.
1043 $text = "Žluťoučký koníček přeskočil";
1044 $this->assertSame($text, shorten_text($text)); // 30 chars by default.
1045 $this->assertSame("Žluťoučký koníče...", shorten_text($text, 19, true));
1046 $this->assertSame("Žluťoučký ...", shorten_text($text, 19, false));
1047 // And try it with 2-less (that are, in bytes, the middle of a sequence).
1048 $this->assertSame("Žluťoučký koní...", shorten_text($text, 17, true));
1049 $this->assertSame("Žluťoučký ...", shorten_text($text, 17, false));
1051 // .........123456789012345678...901234567....89012345.
1052 $text = "<p>Žluťoučký koníček <b>přeskočil</b> potůček</p>";
1053 $this->assertSame($text, shorten_text($text, 60));
1054 $this->assertSame("<p>Žluťoučký koníček ...</p>", shorten_text($text, 21));
1055 $this->assertSame("<p>Žluťoučký koníče...</p>", shorten_text($text, 19, true));
1056 $this->assertSame("<p>Žluťoučký ...</p>", shorten_text($text, 19, false));
1057 // And try it with 2 fewer (that are, in bytes, the middle of a sequence).
1058 $this->assertSame("<p>Žluťoučký koní...</p>", shorten_text($text, 17, true));
1059 $this->assertSame("<p>Žluťoučký ...</p>", shorten_text($text, 17, false));
1060 // And try over one tag (start/end), it does proper text len.
1061 $this->assertSame("<p>Žluťoučký koníček <b>př...</b></p>", shorten_text($text, 23, true));
1062 $this->assertSame("<p>Žluťoučký koníček <b>přeskočil</b> pot...</p>", shorten_text($text, 34, true));
1063 // And in the middle of one tag.
1064 $this->assertSame("<p>Žluťoučký koníček <b>přeskočil...</b></p>", shorten_text($text, 30, true));
1067 public function test_shorten_text_utf8_oriental(): void {
1068 // Japanese
1069 // text without tags
1070 // ......123456789012345678901234.
1071 $text = '言語設定言語設定abcdefghijkl';
1072 $this->assertSame($text, shorten_text($text)); // 30 chars by default.
1073 $this->assertSame("言語設定言語...", shorten_text($text, 9, true));
1074 $this->assertSame("言語設定言語...", shorten_text($text, 9, false));
1075 $this->assertSame("言語設定言語設定ab...", shorten_text($text, 13, true));
1076 $this->assertSame("言語設定言語設定...", shorten_text($text, 13, false));
1078 // Chinese
1079 // text without tags
1080 // ......123456789012345678901234.
1081 $text = '简体中文简体中文abcdefghijkl';
1082 $this->assertSame($text, shorten_text($text)); // 30 chars by default.
1083 $this->assertSame("简体中文简体...", shorten_text($text, 9, true));
1084 $this->assertSame("简体中文简体...", shorten_text($text, 9, false));
1085 $this->assertSame("简体中文简体中文ab...", shorten_text($text, 13, true));
1086 $this->assertSame("简体中文简体中文...", shorten_text($text, 13, false));
1089 public function test_shorten_text_multilang(): void {
1090 // This is not necessaryily specific to multilang. The issue is really
1091 // tags with attributes, where before we were generating invalid HTML
1092 // output like shorten_text('<span id="x" class="y">A</span> B', 1)
1093 // returning '<span id="x" ...</span>'. It is just that multilang
1094 // requires the sort of HTML that is quite likely to trigger this.
1095 // ........................................1...
1096 $text = '<span lang="en" class="multilang">A</span>' .
1097 '<span lang="fr" class="multilang">B</span>';
1098 $this->assertSame('<span lang="en" class="multilang">...</span>',
1099 shorten_text($text, 1));
1103 * Provider for long filenames and its expected result, with and without hash.
1105 * @return array of ($filename, $length, $expected, $includehash)
1107 public function shorten_filename_provider() {
1108 $filename = 'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium totam rem';
1109 $shortfilename = 'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque';
1111 return [
1112 'More than 100 characters' => [
1113 $filename,
1114 null,
1115 'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium tot',
1116 false,
1118 'More than 100 characters with hash' => [
1119 $filename,
1120 null,
1121 'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque l - 3bec1da8b8',
1122 true,
1124 'More than 100 characters with extension' => [
1125 "{$filename}.zip",
1126 null,
1127 'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium tot.zip',
1128 false,
1130 'More than 100 characters with extension and hash' => [
1131 "{$filename}.zip",
1132 null,
1133 'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque l - 3bec1da8b8.zip',
1134 true,
1136 'Limit filename to 50 chars' => [
1137 $filename,
1139 'sed ut perspiciatis unde omnis iste natus error si',
1140 false,
1142 'Limit filename to 50 chars with hash' => [
1143 $filename,
1145 'sed ut perspiciatis unde omnis iste n - 3bec1da8b8',
1146 true,
1148 'Limit filename to 50 chars with extension' => [
1149 "{$filename}.zip",
1151 'sed ut perspiciatis unde omnis iste natus error si.zip',
1152 false,
1154 'Limit filename to 50 chars with extension and hash' => [
1155 "{$filename}.zip",
1157 'sed ut perspiciatis unde omnis iste n - 3bec1da8b8.zip',
1158 true,
1160 'Test filename that contains less than 100 characters' => [
1161 $shortfilename,
1162 null,
1163 $shortfilename,
1164 false,
1166 'Test filename that contains less than 100 characters and hash' => [
1167 $shortfilename,
1168 null,
1169 $shortfilename,
1170 true,
1172 'Test filename that contains less than 100 characters with extension' => [
1173 "{$shortfilename}.zip",
1174 null,
1175 "{$shortfilename}.zip",
1176 false,
1178 'Test filename that contains less than 100 characters with extension and hash' => [
1179 "{$shortfilename}.zip",
1180 null,
1181 "{$shortfilename}.zip",
1182 true,
1188 * Test the {@link shorten_filename()} method.
1190 * @dataProvider shorten_filename_provider
1192 * @param string $filename
1193 * @param int $length
1194 * @param string $expected
1195 * @param boolean $includehash
1197 public function test_shorten_filename($filename, $length, $expected, $includehash): void {
1198 if (null === $length) {
1199 $length = MAX_FILENAME_SIZE;
1202 $this->assertSame($expected, shorten_filename($filename, $length, $includehash));
1206 * Provider for long filenames and its expected result, with and without hash.
1208 * @return array of ($filename, $length, $expected, $includehash)
1210 public function shorten_filenames_provider() {
1211 $shortfilename = 'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque';
1212 $longfilename = 'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium totam rem';
1213 $extfilename = $longfilename.'.zip';
1214 $expected = 'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium tot';
1215 $expectedwithhash = 'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque l - 3bec1da8b8';
1216 $expectedext = 'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium tot.zip';
1217 $expectedextwithhash = 'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque l - 3bec1da8b8.zip';
1218 $expected50 = 'sed ut perspiciatis unde omnis iste natus error si';
1219 $expected50withhash = 'sed ut perspiciatis unde omnis iste n - 3bec1da8b8';
1220 $expected50ext = 'sed ut perspiciatis unde omnis iste natus error si.zip';
1221 $expected50extwithhash = 'sed ut perspiciatis unde omnis iste n - 3bec1da8b8.zip';
1222 $expected50short = 'sed ut perspiciatis unde omnis iste n - 5fb6543490';
1224 return [
1225 'Empty array without hash' => [
1227 null,
1229 false,
1231 'Empty array with hash' => [
1233 null,
1235 true,
1237 'Array with less than 100 characters' => [
1238 [$shortfilename, $shortfilename, $shortfilename],
1239 null,
1240 [$shortfilename, $shortfilename, $shortfilename],
1241 false,
1243 'Array with more than 100 characters without hash' => [
1244 [$longfilename, $longfilename, $longfilename],
1245 null,
1246 [$expected, $expected, $expected],
1247 false,
1249 'Array with more than 100 characters with hash' => [
1250 [$longfilename, $longfilename, $longfilename],
1251 null,
1252 [$expectedwithhash, $expectedwithhash, $expectedwithhash],
1253 true,
1255 'Array with more than 100 characters with extension' => [
1256 [$extfilename, $extfilename, $extfilename],
1257 null,
1258 [$expectedext, $expectedext, $expectedext],
1259 false,
1261 'Array with more than 100 characters with extension and hash' => [
1262 [$extfilename, $extfilename, $extfilename],
1263 null,
1264 [$expectedextwithhash, $expectedextwithhash, $expectedextwithhash],
1265 true,
1267 'Array with more than 100 characters mix (short, long, with extension) without hash' => [
1268 [$shortfilename, $longfilename, $extfilename],
1269 null,
1270 [$shortfilename, $expected, $expectedext],
1271 false,
1273 'Array with more than 100 characters mix (short, long, with extension) with hash' => [
1274 [$shortfilename, $longfilename, $extfilename],
1275 null,
1276 [$shortfilename, $expectedwithhash, $expectedextwithhash],
1277 true,
1279 'Array with less than 50 characters without hash' => [
1280 [$longfilename, $longfilename, $longfilename],
1282 [$expected50, $expected50, $expected50],
1283 false,
1285 'Array with less than 50 characters with hash' => [
1286 [$longfilename, $longfilename, $longfilename],
1288 [$expected50withhash, $expected50withhash, $expected50withhash],
1289 true,
1291 'Array with less than 50 characters with extension' => [
1292 [$extfilename, $extfilename, $extfilename],
1294 [$expected50ext, $expected50ext, $expected50ext],
1295 false,
1297 'Array with less than 50 characters with extension and hash' => [
1298 [$extfilename, $extfilename, $extfilename],
1300 [$expected50extwithhash, $expected50extwithhash, $expected50extwithhash],
1301 true,
1303 'Array with less than 50 characters mix (short, long, with extension) without hash' => [
1304 [$shortfilename, $longfilename, $extfilename],
1306 [$expected50, $expected50, $expected50ext],
1307 false,
1309 'Array with less than 50 characters mix (short, long, with extension) with hash' => [
1310 [$shortfilename, $longfilename, $extfilename],
1312 [$expected50short, $expected50withhash, $expected50extwithhash],
1313 true,
1319 * Test the {@link shorten_filenames()} method.
1321 * @dataProvider shorten_filenames_provider
1323 * @param array $filenames
1324 * @param int $length
1325 * @param string $expected
1326 * @param boolean $includehash
1328 public function test_shorten_filenames($filenames, $length, $expected, $includehash): void {
1329 if (null === $length) {
1330 $length = MAX_FILENAME_SIZE;
1333 $this->assertSame($expected, shorten_filenames($filenames, $length, $includehash));
1336 public function test_usergetdate(): void {
1337 global $USER, $CFG, $DB;
1338 $this->resetAfterTest();
1340 $this->setAdminUser();
1342 $USER->timezone = 2;// Set the timezone to a known state.
1344 $ts = 1261540267; // The time this function was created.
1346 $arr = usergetdate($ts, 1); // Specify the timezone as an argument.
1347 $arr = array_values($arr);
1349 list($seconds, $minutes, $hours, $mday, $wday, $mon, $year, $yday, $weekday, $month) = $arr;
1350 $this->assertSame(7, $seconds);
1351 $this->assertSame(51, $minutes);
1352 $this->assertSame(4, $hours);
1353 $this->assertSame(23, $mday);
1354 $this->assertSame(3, $wday);
1355 $this->assertSame(12, $mon);
1356 $this->assertSame(2009, $year);
1357 $this->assertSame(356, $yday);
1358 $this->assertSame('Wednesday', $weekday);
1359 $this->assertSame('December', $month);
1360 $arr = usergetdate($ts); // Gets the timezone from the $USER object.
1361 $arr = array_values($arr);
1363 list($seconds, $minutes, $hours, $mday, $wday, $mon, $year, $yday, $weekday, $month) = $arr;
1364 $this->assertSame(7, $seconds);
1365 $this->assertSame(51, $minutes);
1366 $this->assertSame(5, $hours);
1367 $this->assertSame(23, $mday);
1368 $this->assertSame(3, $wday);
1369 $this->assertSame(12, $mon);
1370 $this->assertSame(2009, $year);
1371 $this->assertSame(356, $yday);
1372 $this->assertSame('Wednesday', $weekday);
1373 $this->assertSame('December', $month);
1375 // Edge cases - 0 and null - they all mean 1st Jan 1970. Null shows debugging message.
1376 $this->assertSame(1970, usergetdate(0)['year']);
1377 $this->assertDebuggingNotCalled();
1378 $this->assertSame(1970, usergetdate(null)['year']);
1379 $this->assertDebuggingCalled(null, DEBUG_DEVELOPER);
1382 public function test_mark_user_preferences_changed(): void {
1383 $this->resetAfterTest();
1384 $otheruser = $this->getDataGenerator()->create_user();
1385 $otheruserid = $otheruser->id;
1387 set_cache_flag('userpreferenceschanged', $otheruserid, null);
1388 mark_user_preferences_changed($otheruserid);
1390 $this->assertEquals(get_cache_flag('userpreferenceschanged', $otheruserid, time()-10), 1);
1391 set_cache_flag('userpreferenceschanged', $otheruserid, null);
1394 public function test_check_user_preferences_loaded(): void {
1395 global $DB;
1396 $this->resetAfterTest();
1398 $otheruser = $this->getDataGenerator()->create_user();
1399 $otheruserid = $otheruser->id;
1401 $DB->delete_records('user_preferences', array('userid'=>$otheruserid));
1402 set_cache_flag('userpreferenceschanged', $otheruserid, null);
1404 $user = new \stdClass();
1405 $user->id = $otheruserid;
1407 // Load.
1408 check_user_preferences_loaded($user);
1409 $this->assertTrue(isset($user->preference));
1410 $this->assertTrue(is_array($user->preference));
1411 $this->assertArrayHasKey('_lastloaded', $user->preference);
1412 $this->assertCount(1, $user->preference);
1414 // Add preference via direct call.
1415 $DB->insert_record('user_preferences', array('name'=>'xxx', 'value'=>'yyy', 'userid'=>$user->id));
1417 // No cache reload yet.
1418 check_user_preferences_loaded($user);
1419 $this->assertCount(1, $user->preference);
1421 // Forced reloading of cache.
1422 unset($user->preference);
1423 check_user_preferences_loaded($user);
1424 $this->assertCount(2, $user->preference);
1425 $this->assertSame('yyy', $user->preference['xxx']);
1427 // Add preference via direct call.
1428 $DB->insert_record('user_preferences', array('name'=>'aaa', 'value'=>'bbb', 'userid'=>$user->id));
1430 // Test timeouts and modifications from different session.
1431 set_cache_flag('userpreferenceschanged', $user->id, 1, time() + 1000);
1432 $user->preference['_lastloaded'] = $user->preference['_lastloaded'] - 20;
1433 check_user_preferences_loaded($user);
1434 $this->assertCount(2, $user->preference);
1435 check_user_preferences_loaded($user, 10);
1436 $this->assertCount(3, $user->preference);
1437 $this->assertSame('bbb', $user->preference['aaa']);
1438 set_cache_flag('userpreferenceschanged', $user->id, null);
1441 public function test_set_user_preference(): void {
1442 global $DB, $USER;
1443 $this->resetAfterTest();
1445 $this->setAdminUser();
1447 $otheruser = $this->getDataGenerator()->create_user();
1448 $otheruserid = $otheruser->id;
1450 $DB->delete_records('user_preferences', array('userid'=>$otheruserid));
1451 set_cache_flag('userpreferenceschanged', $otheruserid, null);
1453 $user = new \stdClass();
1454 $user->id = $otheruserid;
1456 set_user_preference('aaa', 'bbb', $otheruserid);
1457 $this->assertSame('bbb', $DB->get_field('user_preferences', 'value', array('userid'=>$otheruserid, 'name'=>'aaa')));
1458 $this->assertSame('bbb', get_user_preferences('aaa', null, $otheruserid));
1460 set_user_preference('xxx', 'yyy', $user);
1461 $this->assertSame('yyy', $DB->get_field('user_preferences', 'value', array('userid'=>$otheruserid, 'name'=>'xxx')));
1462 $this->assertSame('yyy', get_user_preferences('xxx', null, $otheruserid));
1463 $this->assertTrue(is_array($user->preference));
1464 $this->assertSame('bbb', $user->preference['aaa']);
1465 $this->assertSame('yyy', $user->preference['xxx']);
1467 set_user_preference('xxx', null, $user);
1468 $this->assertFalse($DB->get_field('user_preferences', 'value', array('userid'=>$otheruserid, 'name'=>'xxx')));
1469 $this->assertNull(get_user_preferences('xxx', null, $otheruserid));
1471 set_user_preference('ooo', true, $user);
1472 $prefs = get_user_preferences(null, null, $otheruserid);
1473 $this->assertSame($user->preference['aaa'], $prefs['aaa']);
1474 $this->assertSame($user->preference['ooo'], $prefs['ooo']);
1475 $this->assertSame('1', $prefs['ooo']);
1477 set_user_preference('null', 0, $user);
1478 $this->assertSame('0', get_user_preferences('null', null, $otheruserid));
1480 $this->assertSame('lala', get_user_preferences('undefined', 'lala', $otheruserid));
1482 $DB->delete_records('user_preferences', array('userid'=>$otheruserid));
1483 set_cache_flag('userpreferenceschanged', $otheruserid, null);
1485 // Test $USER default.
1486 set_user_preference('_test_user_preferences_pref', 'ok');
1487 $this->assertSame('ok', $USER->preference['_test_user_preferences_pref']);
1488 unset_user_preference('_test_user_preferences_pref');
1489 $this->assertTrue(!isset($USER->preference['_test_user_preferences_pref']));
1491 // Test 1333 char values (no need for unicode, there are already tests for that in DB tests).
1492 $longvalue = str_repeat('a', 1333);
1493 set_user_preference('_test_long_user_preference', $longvalue);
1494 $this->assertEquals($longvalue, get_user_preferences('_test_long_user_preference'));
1495 $this->assertEquals($longvalue,
1496 $DB->get_field('user_preferences', 'value', array('userid' => $USER->id, 'name' => '_test_long_user_preference')));
1498 // Test > 1333 char values, coding_exception expected.
1499 $longvalue = str_repeat('a', 1334);
1500 try {
1501 set_user_preference('_test_long_user_preference', $longvalue);
1502 $this->fail('Exception expected - longer than 1333 chars not allowed as preference value');
1503 } catch (\moodle_exception $ex) {
1504 $this->assertInstanceOf('coding_exception', $ex);
1507 // Test invalid params.
1508 try {
1509 set_user_preference('_test_user_preferences_pref', array());
1510 $this->fail('Exception expected - array not valid preference value');
1511 } catch (\moodle_exception $ex) {
1512 $this->assertInstanceOf('coding_exception', $ex);
1514 try {
1515 set_user_preference('_test_user_preferences_pref', new \stdClass);
1516 $this->fail('Exception expected - class not valid preference value');
1517 } catch (\moodle_exception $ex) {
1518 $this->assertInstanceOf('coding_exception', $ex);
1520 try {
1521 set_user_preference('_test_user_preferences_pref', 1, array('xx' => 1));
1522 $this->fail('Exception expected - user instance expected');
1523 } catch (\moodle_exception $ex) {
1524 $this->assertInstanceOf('coding_exception', $ex);
1526 try {
1527 set_user_preference('_test_user_preferences_pref', 1, 'abc');
1528 $this->fail('Exception expected - user instance expected');
1529 } catch (\moodle_exception $ex) {
1530 $this->assertInstanceOf('coding_exception', $ex);
1532 try {
1533 set_user_preference('', 1);
1534 $this->fail('Exception expected - invalid name accepted');
1535 } catch (\moodle_exception $ex) {
1536 $this->assertInstanceOf('coding_exception', $ex);
1538 try {
1539 set_user_preference('1', 1);
1540 $this->fail('Exception expected - invalid name accepted');
1541 } catch (\moodle_exception $ex) {
1542 $this->assertInstanceOf('coding_exception', $ex);
1546 public function test_set_user_preference_for_current_user(): void {
1547 global $USER;
1548 $this->resetAfterTest();
1549 $this->setAdminUser();
1551 set_user_preference('test_pref', 2);
1552 set_user_preference('test_pref', 1, $USER->id);
1553 $this->assertEquals(1, get_user_preferences('test_pref'));
1556 public function test_unset_user_preference_for_current_user(): void {
1557 global $USER;
1558 $this->resetAfterTest();
1559 $this->setAdminUser();
1561 set_user_preference('test_pref', 1);
1562 unset_user_preference('test_pref', $USER->id);
1563 $this->assertNull(get_user_preferences('test_pref'));
1567 * Test some critical TZ/DST.
1569 * This method tests some special TZ/DST combinations that were fixed
1570 * by MDL-38999. The tests are done by comparing the results of the
1571 * output using Moodle TZ/DST support and PHP native one.
1573 * Note: If you don't trust PHP TZ/DST support, can verify the
1574 * harcoded expectations below with:
1575 * http://www.tools4noobs.com/online_tools/unix_timestamp_to_datetime/
1577 public function test_some_moodle_special_dst(): void {
1578 $stamp = 1365386400; // 2013/04/08 02:00:00 GMT/UTC.
1580 // In Europe/Tallinn it was 2013/04/08 05:00:00.
1581 $expectation = '2013/04/08 05:00:00';
1582 $phpdt = \DateTime::createFromFormat('U', $stamp, new \DateTimeZone('UTC'));
1583 $phpdt->setTimezone(new \DateTimeZone('Europe/Tallinn'));
1584 $phpres = $phpdt->format('Y/m/d H:i:s'); // PHP result.
1585 $moodleres = userdate($stamp, '%Y/%m/%d %H:%M:%S', 'Europe/Tallinn', false); // Moodle result.
1586 $this->assertSame($expectation, $phpres);
1587 $this->assertSame($expectation, $moodleres);
1589 // In St. Johns it was 2013/04/07 23:30:00.
1590 $expectation = '2013/04/07 23:30:00';
1591 $phpdt = \DateTime::createFromFormat('U', $stamp, new \DateTimeZone('UTC'));
1592 $phpdt->setTimezone(new \DateTimeZone('America/St_Johns'));
1593 $phpres = $phpdt->format('Y/m/d H:i:s'); // PHP result.
1594 $moodleres = userdate($stamp, '%Y/%m/%d %H:%M:%S', 'America/St_Johns', false); // Moodle result.
1595 $this->assertSame($expectation, $phpres);
1596 $this->assertSame($expectation, $moodleres);
1598 $stamp = 1383876000; // 2013/11/08 02:00:00 GMT/UTC.
1600 // In Europe/Tallinn it was 2013/11/08 04:00:00.
1601 $expectation = '2013/11/08 04:00:00';
1602 $phpdt = \DateTime::createFromFormat('U', $stamp, new \DateTimeZone('UTC'));
1603 $phpdt->setTimezone(new \DateTimeZone('Europe/Tallinn'));
1604 $phpres = $phpdt->format('Y/m/d H:i:s'); // PHP result.
1605 $moodleres = userdate($stamp, '%Y/%m/%d %H:%M:%S', 'Europe/Tallinn', false); // Moodle result.
1606 $this->assertSame($expectation, $phpres);
1607 $this->assertSame($expectation, $moodleres);
1609 // In St. Johns it was 2013/11/07 22:30:00.
1610 $expectation = '2013/11/07 22:30:00';
1611 $phpdt = \DateTime::createFromFormat('U', $stamp, new \DateTimeZone('UTC'));
1612 $phpdt->setTimezone(new \DateTimeZone('America/St_Johns'));
1613 $phpres = $phpdt->format('Y/m/d H:i:s'); // PHP result.
1614 $moodleres = userdate($stamp, '%Y/%m/%d %H:%M:%S', 'America/St_Johns', false); // Moodle result.
1615 $this->assertSame($expectation, $phpres);
1616 $this->assertSame($expectation, $moodleres);
1619 public function test_userdate(): void {
1620 global $USER, $CFG, $DB;
1621 $this->resetAfterTest();
1623 $this->setAdminUser();
1625 $testvalues = array(
1626 array(
1627 'time' => '1309514400',
1628 'usertimezone' => 'America/Moncton',
1629 'timezone' => '0.0', // No dst offset.
1630 'expectedoutput' => 'Friday, 1 July 2011, 10:00 AM',
1631 'expectedoutputhtml' => '<time datetime="2011-07-01T07:00:00-03:00">Friday, 1 July 2011, 10:00 AM</time>'
1633 array(
1634 'time' => '1309514400',
1635 'usertimezone' => 'America/Moncton',
1636 'timezone' => '99', // Dst offset and timezone offset.
1637 'expectedoutput' => 'Friday, 1 July 2011, 7:00 AM',
1638 'expectedoutputhtml' => '<time datetime="2011-07-01T07:00:00-03:00">Friday, 1 July 2011, 7:00 AM</time>'
1640 array(
1641 'time' => '1309514400',
1642 'usertimezone' => 'America/Moncton',
1643 'timezone' => 'America/Moncton', // Dst offset and timezone offset.
1644 'expectedoutput' => 'Friday, 1 July 2011, 7:00 AM',
1645 'expectedoutputhtml' => '<time datetime="2011-07-01t07:00:00-03:00">Friday, 1 July 2011, 7:00 AM</time>'
1647 array(
1648 'time' => '1293876000 ',
1649 'usertimezone' => 'America/Moncton',
1650 'timezone' => '0.0', // No dst offset.
1651 'expectedoutput' => 'Saturday, 1 January 2011, 10:00 AM',
1652 'expectedoutputhtml' => '<time datetime="2011-01-01T06:00:00-04:00">Saturday, 1 January 2011, 10:00 AM</time>'
1654 array(
1655 'time' => '1293876000 ',
1656 'usertimezone' => 'America/Moncton',
1657 'timezone' => '99', // No dst offset in jan, so just timezone offset.
1658 'expectedoutput' => 'Saturday, 1 January 2011, 6:00 AM',
1659 'expectedoutputhtml' => '<time datetime="2011-01-01T06:00:00-04:00">Saturday, 1 January 2011, 6:00 AM</time>'
1661 array(
1662 'time' => '1293876000 ',
1663 'usertimezone' => 'America/Moncton',
1664 'timezone' => 'America/Moncton', // No dst offset in jan.
1665 'expectedoutput' => 'Saturday, 1 January 2011, 6:00 AM',
1666 'expectedoutputhtml' => '<time datetime="2011-01-01T06:00:00-04:00">Saturday, 1 January 2011, 6:00 AM</time>'
1668 array(
1669 'time' => '1293876000 ',
1670 'usertimezone' => '2',
1671 'timezone' => '99', // Take user timezone.
1672 'expectedoutput' => 'Saturday, 1 January 2011, 12:00 PM',
1673 'expectedoutputhtml' => '<time datetime="2011-01-01T12:00:00+02:00">Saturday, 1 January 2011, 12:00 PM</time>'
1675 array(
1676 'time' => '1293876000 ',
1677 'usertimezone' => '-2',
1678 'timezone' => '99', // Take user timezone.
1679 'expectedoutput' => 'Saturday, 1 January 2011, 8:00 AM',
1680 'expectedoutputhtml' => '<time datetime="2011-01-01T08:00:00-02:00">Saturday, 1 January 2011, 8:00 AM</time>'
1682 array(
1683 'time' => '1293876000 ',
1684 'usertimezone' => '-10',
1685 'timezone' => '2', // Take this timezone.
1686 'expectedoutput' => 'Saturday, 1 January 2011, 12:00 PM',
1687 'expectedoutputhtml' => '<time datetime="2011-01-01T00:00:00-10:00">Saturday, 1 January 2011, 12:00 PM</time>'
1689 array(
1690 'time' => '1293876000 ',
1691 'usertimezone' => '-10',
1692 'timezone' => '-2', // Take this timezone.
1693 'expectedoutput' => 'Saturday, 1 January 2011, 8:00 AM',
1694 'expectedoutputhtml' => '<time datetime="2011-01-01T00:00:00-10:00">Saturday, 1 January 2011, 8:00 AM</time>'
1696 array(
1697 'time' => '1293876000 ',
1698 'usertimezone' => '-10',
1699 'timezone' => 'random/time', // This should show server time.
1700 'expectedoutput' => 'Saturday, 1 January 2011, 6:00 PM',
1701 'expectedoutputhtml' => '<time datetime="2011-01-01T00:00:00-10:00">Saturday, 1 January 2011, 6:00 PM</time>'
1703 array(
1704 'time' => '1293876000 ',
1705 'usertimezone' => '20', // Fallback to server time zone.
1706 'timezone' => '99', // This should show user time.
1707 'expectedoutput' => 'Saturday, 1 January 2011, 6:00 PM',
1708 'expectedoutputhtml' => '<time datetime="2011-01-01T18:00:00+08:00">Saturday, 1 January 2011, 6:00 PM</time>'
1712 // Set default timezone to Australia/Perth, else time calculated
1713 // will not match expected values.
1714 $this->setTimezone(99, 'Australia/Perth');
1716 foreach ($testvalues as $vals) {
1717 $USER->timezone = $vals['usertimezone'];
1718 $actualoutput = userdate($vals['time'], '%A, %d %B %Y, %I:%M %p', $vals['timezone']);
1719 $actualoutputhtml = userdate_htmltime($vals['time'], '%A, %d %B %Y, %I:%M %p', $vals['timezone']);
1721 // On different systems case of AM PM changes so compare case insensitive.
1722 $vals['expectedoutput'] = \core_text::strtolower($vals['expectedoutput']);
1723 $vals['expectedoutputhtml'] = \core_text::strtolower($vals['expectedoutputhtml']);
1724 $actualoutput = \core_text::strtolower($actualoutput);
1725 $actualoutputhtml = \core_text::strtolower($actualoutputhtml);
1727 $this->assertSame($vals['expectedoutput'], $actualoutput,
1728 "Expected: {$vals['expectedoutput']} => Actual: {$actualoutput} \ndata: " . var_export($vals, true));
1729 $this->assertSame($vals['expectedoutputhtml'], $actualoutputhtml,
1730 "Expected: {$vals['expectedoutputhtml']} => Actual: {$actualoutputhtml} \ndata: " . var_export($vals, true));
1735 * Make sure the DST changes happen at the right time in Moodle.
1737 public function test_dst_changes(): void {
1738 // DST switching in Prague.
1739 // From 2AM to 3AM in 1989.
1740 $date = new \DateTime('1989-03-26T01:59:00+01:00');
1741 $this->assertSame('Sunday, 26 March 1989, 01:59', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Europe/Prague'));
1742 $date = new \DateTime('1989-03-26T02:01:00+01:00');
1743 $this->assertSame('Sunday, 26 March 1989, 03:01', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Europe/Prague'));
1744 // From 3AM to 2AM in 1989 - not the same as the west Europe.
1745 $date = new \DateTime('1989-09-24T01:59:00+01:00');
1746 $this->assertSame('Sunday, 24 September 1989, 02:59', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Europe/Prague'));
1747 $date = new \DateTime('1989-09-24T02:01:00+01:00');
1748 $this->assertSame('Sunday, 24 September 1989, 02:01', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Europe/Prague'));
1749 // From 2AM to 3AM in 2014.
1750 $date = new \DateTime('2014-03-30T01:59:00+01:00');
1751 $this->assertSame('Sunday, 30 March 2014, 01:59', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Europe/Prague'));
1752 $date = new \DateTime('2014-03-30T02:01:00+01:00');
1753 $this->assertSame('Sunday, 30 March 2014, 03:01', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Europe/Prague'));
1754 // From 3AM to 2AM in 2014.
1755 $date = new \DateTime('2014-10-26T01:59:00+01:00');
1756 $this->assertSame('Sunday, 26 October 2014, 02:59', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Europe/Prague'));
1757 $date = new \DateTime('2014-10-26T02:01:00+01:00');
1758 $this->assertSame('Sunday, 26 October 2014, 02:01', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Europe/Prague'));
1759 // From 2AM to 3AM in 2020.
1760 $date = new \DateTime('2020-03-29T01:59:00+01:00');
1761 $this->assertSame('Sunday, 29 March 2020, 01:59', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Europe/Prague'));
1762 $date = new \DateTime('2020-03-29T02:01:00+01:00');
1763 $this->assertSame('Sunday, 29 March 2020, 03:01', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Europe/Prague'));
1764 // From 3AM to 2AM in 2020.
1765 $date = new \DateTime('2020-10-25T01:59:00+01:00');
1766 $this->assertSame('Sunday, 25 October 2020, 02:59', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Europe/Prague'));
1767 $date = new \DateTime('2020-10-25T02:01:00+01:00');
1768 $this->assertSame('Sunday, 25 October 2020, 02:01', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Europe/Prague'));
1770 // DST switching in NZ.
1771 // From 3AM to 2AM in 2015.
1772 $date = new \DateTime('2015-04-05T02:59:00+13:00');
1773 $this->assertSame('Sunday, 5 April 2015, 02:59', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Pacific/Auckland'));
1774 $date = new \DateTime('2015-04-05T03:01:00+13:00');
1775 $this->assertSame('Sunday, 5 April 2015, 02:01', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Pacific/Auckland'));
1776 // From 2AM to 3AM in 2009.
1777 $date = new \DateTime('2015-09-27T01:59:00+12:00');
1778 $this->assertSame('Sunday, 27 September 2015, 01:59', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Pacific/Auckland'));
1779 $date = new \DateTime('2015-09-27T02:01:00+12:00');
1780 $this->assertSame('Sunday, 27 September 2015, 03:01', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Pacific/Auckland'));
1782 // DST switching in Perth.
1783 // From 3AM to 2AM in 2009.
1784 $date = new \DateTime('2008-03-30T01:59:00+08:00');
1785 $this->assertSame('Sunday, 30 March 2008, 02:59', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Australia/Perth'));
1786 $date = new \DateTime('2008-03-30T02:01:00+08:00');
1787 $this->assertSame('Sunday, 30 March 2008, 02:01', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Australia/Perth'));
1788 // From 2AM to 3AM in 2009.
1789 $date = new \DateTime('2008-10-26T01:59:00+08:00');
1790 $this->assertSame('Sunday, 26 October 2008, 01:59', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Australia/Perth'));
1791 $date = new \DateTime('2008-10-26T02:01:00+08:00');
1792 $this->assertSame('Sunday, 26 October 2008, 03:01', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'Australia/Perth'));
1794 // DST switching in US.
1795 // From 2AM to 3AM in 2014.
1796 $date = new \DateTime('2014-03-09T01:59:00-05:00');
1797 $this->assertSame('Sunday, 9 March 2014, 01:59', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'America/New_York'));
1798 $date = new \DateTime('2014-03-09T02:01:00-05:00');
1799 $this->assertSame('Sunday, 9 March 2014, 03:01', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'America/New_York'));
1800 // From 3AM to 2AM in 2014.
1801 $date = new \DateTime('2014-11-02T01:59:00-04:00');
1802 $this->assertSame('Sunday, 2 November 2014, 01:59', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'America/New_York'));
1803 $date = new \DateTime('2014-11-02T02:01:00-04:00');
1804 $this->assertSame('Sunday, 2 November 2014, 01:01', userdate($date->getTimestamp(), '%A, %d %B %Y, %H:%M', 'America/New_York'));
1807 public function test_make_timestamp(): void {
1808 global $USER, $CFG, $DB;
1809 $this->resetAfterTest();
1811 $this->setAdminUser();
1813 $testvalues = array(
1814 array(
1815 'usertimezone' => 'America/Moncton',
1816 'year' => '2011',
1817 'month' => '7',
1818 'day' => '1',
1819 'hour' => '10',
1820 'minutes' => '00',
1821 'seconds' => '00',
1822 'timezone' => '0.0',
1823 'applydst' => false, // No dst offset.
1824 'expectedoutput' => '1309514400' // 6pm at UTC+0.
1826 array(
1827 'usertimezone' => 'America/Moncton',
1828 'year' => '2011',
1829 'month' => '7',
1830 'day' => '1',
1831 'hour' => '10',
1832 'minutes' => '00',
1833 'seconds' => '00',
1834 'timezone' => '99', // User default timezone.
1835 'applydst' => false, // Don't apply dst.
1836 'expectedoutput' => '1309528800'
1838 array(
1839 'usertimezone' => 'America/Moncton',
1840 'year' => '2011',
1841 'month' => '7',
1842 'day' => '1',
1843 'hour' => '10',
1844 'minutes' => '00',
1845 'seconds' => '00',
1846 'timezone' => '99', // User default timezone.
1847 'applydst' => true, // Apply dst.
1848 'expectedoutput' => '1309525200'
1850 array(
1851 'usertimezone' => 'America/Moncton',
1852 'year' => '2011',
1853 'month' => '7',
1854 'day' => '1',
1855 'hour' => '10',
1856 'minutes' => '00',
1857 'seconds' => '00',
1858 'timezone' => 'America/Moncton', // String timezone.
1859 'applydst' => true, // Apply dst.
1860 'expectedoutput' => '1309525200'
1862 array(
1863 'usertimezone' => '2', // No dst applyed.
1864 'year' => '2011',
1865 'month' => '7',
1866 'day' => '1',
1867 'hour' => '10',
1868 'minutes' => '00',
1869 'seconds' => '00',
1870 'timezone' => '99', // Take user timezone.
1871 'applydst' => true, // Apply dst.
1872 'expectedoutput' => '1309507200'
1874 array(
1875 'usertimezone' => '-2', // No dst applyed.
1876 'year' => '2011',
1877 'month' => '7',
1878 'day' => '1',
1879 'hour' => '10',
1880 'minutes' => '00',
1881 'seconds' => '00',
1882 'timezone' => '99', // Take usertimezone.
1883 'applydst' => true, // Apply dst.
1884 'expectedoutput' => '1309521600'
1886 array(
1887 'usertimezone' => '-10', // No dst applyed.
1888 'year' => '2011',
1889 'month' => '7',
1890 'day' => '1',
1891 'hour' => '10',
1892 'minutes' => '00',
1893 'seconds' => '00',
1894 'timezone' => '2', // Take this timezone.
1895 'applydst' => true, // Apply dst.
1896 'expectedoutput' => '1309507200'
1898 array(
1899 'usertimezone' => '-10', // No dst applyed.
1900 'year' => '2011',
1901 'month' => '7',
1902 'day' => '1',
1903 'hour' => '10',
1904 'minutes' => '00',
1905 'seconds' => '00',
1906 'timezone' => '-2', // Take this timezone.
1907 'applydst' => true, // Apply dst.
1908 'expectedoutput' => '1309521600'
1910 array(
1911 'usertimezone' => '-10', // No dst applyed.
1912 'year' => '2011',
1913 'month' => '7',
1914 'day' => '1',
1915 'hour' => '10',
1916 'minutes' => '00',
1917 'seconds' => '00',
1918 'timezone' => 'random/time', // This should show server time.
1919 'applydst' => true, // Apply dst.
1920 'expectedoutput' => '1309485600'
1922 array(
1923 'usertimezone' => '-14', // Server time.
1924 'year' => '2011',
1925 'month' => '7',
1926 'day' => '1',
1927 'hour' => '10',
1928 'minutes' => '00',
1929 'seconds' => '00',
1930 'timezone' => '99', // Get user time.
1931 'applydst' => true, // Apply dst.
1932 'expectedoutput' => '1309485600'
1936 // Set default timezone to Australia/Perth, else time calculated
1937 // will not match expected values.
1938 $this->setTimezone(99, 'Australia/Perth');
1940 // Test make_timestamp with all testvals and assert if anything wrong.
1941 foreach ($testvalues as $vals) {
1942 $USER->timezone = $vals['usertimezone'];
1943 $actualoutput = make_timestamp(
1944 $vals['year'],
1945 $vals['month'],
1946 $vals['day'],
1947 $vals['hour'],
1948 $vals['minutes'],
1949 $vals['seconds'],
1950 $vals['timezone'],
1951 $vals['applydst']
1954 // On different systems case of AM PM changes so compare case insensitive.
1955 $vals['expectedoutput'] = \core_text::strtolower($vals['expectedoutput']);
1956 $actualoutput = \core_text::strtolower($actualoutput);
1958 $this->assertSame($vals['expectedoutput'], $actualoutput,
1959 "Expected: {$vals['expectedoutput']} => Actual: {$actualoutput},
1960 Please check if timezones are updated (Site adminstration -> location -> update timezone)");
1965 * Test get_string and most importantly the implementation of the lang_string
1966 * object.
1968 public function test_get_string(): void {
1969 global $COURSE;
1971 // Make sure we are using English.
1972 $originallang = $COURSE->lang;
1973 $COURSE->lang = 'en';
1975 $yes = get_string('yes');
1976 $yesexpected = 'Yes';
1977 $this->assertIsString($yes);
1978 $this->assertSame($yesexpected, $yes);
1980 $yes = get_string('yes', 'moodle');
1981 $this->assertIsString($yes);
1982 $this->assertSame($yesexpected, $yes);
1984 $yes = get_string('yes', 'core');
1985 $this->assertIsString($yes);
1986 $this->assertSame($yesexpected, $yes);
1988 $yes = get_string('yes', '');
1989 $this->assertIsString($yes);
1990 $this->assertSame($yesexpected, $yes);
1992 $yes = get_string('yes', null);
1993 $this->assertIsString($yes);
1994 $this->assertSame($yesexpected, $yes);
1996 $yes = get_string('yes', null, 1);
1997 $this->assertIsString($yes);
1998 $this->assertSame($yesexpected, $yes);
2000 $days = 1;
2001 $numdays = get_string('numdays', 'core', '1');
2002 $numdaysexpected = $days.' days';
2003 $this->assertIsString($numdays);
2004 $this->assertSame($numdaysexpected, $numdays);
2006 $yes = get_string('yes', null, null, true);
2007 $this->assertInstanceOf('lang_string', $yes);
2008 $this->assertSame($yesexpected, (string)$yes);
2010 // Test lazy loading (returning lang_string) correctly interpolates 0 being used as args.
2011 $numdays = get_string('numdays', 'moodle', 0, true);
2012 $this->assertInstanceOf(lang_string::class, $numdays);
2013 $this->assertSame('0 days', (string) $numdays);
2015 // Test using a lang_string object as the $a argument for a normal
2016 // get_string call (returning string).
2017 $test = new lang_string('yes', null, null, true);
2018 $testexpected = get_string('numdays', 'core', get_string('yes'));
2019 $testresult = get_string('numdays', null, $test);
2020 $this->assertIsString($testresult);
2021 $this->assertSame($testexpected, $testresult);
2023 // Test using a lang_string object as the $a argument for an object
2024 // get_string call (returning lang_string).
2025 $test = new lang_string('yes', null, null, true);
2026 $testexpected = get_string('numdays', 'core', get_string('yes'));
2027 $testresult = get_string('numdays', null, $test, true);
2028 $this->assertInstanceOf('lang_string', $testresult);
2029 $this->assertSame($testexpected, "$testresult");
2031 // Make sure that object properties that can't be converted don't cause
2032 // errors.
2033 // Level one: This is as deep as current language processing goes.
2034 $test = new \stdClass;
2035 $test->one = 'here';
2036 $string = get_string('yes', null, $test, true);
2037 $this->assertEquals($yesexpected, $string);
2039 // Make sure that object properties that can't be converted don't cause
2040 // errors.
2041 // Level two: Language processing doesn't currently reach this deep.
2042 // only immediate scalar properties are worked with.
2043 $test = new \stdClass;
2044 $test->one = new \stdClass;
2045 $test->one->two = 'here';
2046 $string = get_string('yes', null, $test, true);
2047 $this->assertEquals($yesexpected, $string);
2049 // Make sure that object properties that can't be converted don't cause
2050 // errors.
2051 // Level three: It should never ever go this deep, but we're making sure
2052 // it doesn't cause any probs anyway.
2053 $test = new \stdClass;
2054 $test->one = new \stdClass;
2055 $test->one->two = new \stdClass;
2056 $test->one->two->three = 'here';
2057 $string = get_string('yes', null, $test, true);
2058 $this->assertEquals($yesexpected, $string);
2060 // Make sure that object properties that can't be converted don't cause
2061 // errors and check lang_string properties.
2062 // Level one: This is as deep as current language processing goes.
2063 $test = new \stdClass;
2064 $test->one = new lang_string('yes');
2065 $string = get_string('yes', null, $test, true);
2066 $this->assertEquals($yesexpected, $string);
2068 // Make sure that object properties that can't be converted don't cause
2069 // errors and check lang_string properties.
2070 // Level two: Language processing doesn't currently reach this deep.
2071 // only immediate scalar properties are worked with.
2072 $test = new \stdClass;
2073 $test->one = new \stdClass;
2074 $test->one->two = new lang_string('yes');
2075 $string = get_string('yes', null, $test, true);
2076 $this->assertEquals($yesexpected, $string);
2078 // Make sure that object properties that can't be converted don't cause
2079 // errors and check lang_string properties.
2080 // Level three: It should never ever go this deep, but we're making sure
2081 // it doesn't cause any probs anyway.
2082 $test = new \stdClass;
2083 $test->one = new \stdClass;
2084 $test->one->two = new \stdClass;
2085 $test->one->two->three = new lang_string('yes');
2086 $string = get_string('yes', null, $test, true);
2087 $this->assertEquals($yesexpected, $string);
2089 // Make sure that array properties that can't be converted don't cause
2090 // errors.
2091 $test = array();
2092 $test['one'] = new \stdClass;
2093 $test['one']->two = 'here';
2094 $string = get_string('yes', null, $test, true);
2095 $this->assertEquals($yesexpected, $string);
2097 // Same thing but as above except using an object... this is allowed :P.
2098 $string = get_string('yes', null, null, true);
2099 $object = new \stdClass;
2100 $object->$string = 'Yes';
2101 $this->assertEquals($yesexpected, $string);
2102 $this->assertEquals($yesexpected, $object->$string);
2104 // Reset the language.
2105 $COURSE->lang = $originallang;
2108 public function test_lang_string_var_export(): void {
2110 // Call var_export() on a newly generated lang_string.
2111 $str = new lang_string('no');
2113 // In PHP 8.2 exported class names are now fully qualified;
2114 // previously, the leading backslash was omitted.
2115 $leadingbackslash = (version_compare(PHP_VERSION, '8.2.0', '>=')) ? '\\' : '';
2117 $expected1 = <<<EOF
2118 {$leadingbackslash}core\lang_string::__set_state(array(
2119 'component' => 'moodle',
2120 'a' => NULL,
2121 'string' => NULL,
2122 'forcedstring' => false,
2123 'identifier' => 'no',
2124 'lang' => NULL,
2126 EOF;
2128 $v = var_export($str, true);
2129 $this->assertEquals($expected1, $v);
2131 // Now execute the code that was returned - it should produce a correct string.
2132 $str = lang_string::__set_state(array(
2133 'identifier' => 'no',
2134 'component' => 'moodle',
2135 'a' => NULL,
2136 'lang' => NULL,
2137 'string' => NULL,
2138 'forcedstring' => false,
2141 $this->assertInstanceOf(lang_string::class, $str);
2142 $this->assertEquals('No', $str);
2145 public function test_get_string_limitation(): void {
2146 // This is one of the limitations to the lang_string class. It can't be
2147 // used as a key.
2148 $this->expectException(\TypeError::class);
2149 $array = array(get_string('yes', null, null, true) => 'yes');
2153 * Test localised float formatting.
2155 public function test_format_float(): void {
2157 // Special case for null.
2158 $this->assertEquals('', format_float(null));
2160 // Default 1 decimal place.
2161 $this->assertEquals('5.4', format_float(5.43));
2162 $this->assertEquals('5.0', format_float(5.001));
2164 // Custom number of decimal places.
2165 $this->assertEquals('5.43000', format_float(5.43, 5));
2167 // Auto detect the number of decimal places.
2168 $this->assertEquals('5.43', format_float(5.43, -1));
2169 $this->assertEquals('5.43', format_float(5.43000, -1));
2170 $this->assertEquals('5', format_float(5, -1));
2171 $this->assertEquals('5', format_float(5.0, -1));
2172 $this->assertEquals('0.543', format_float('5.43e-1', -1));
2173 $this->assertEquals('0.543', format_float('5.43000e-1', -1));
2175 // Option to strip ending zeros after rounding.
2176 $this->assertEquals('5.43', format_float(5.43, 5, true, true));
2177 $this->assertEquals('5', format_float(5.0001, 3, true, true));
2178 $this->assertEquals('100', format_float(100, 2, true, true));
2179 $this->assertEquals('100', format_float(100, 0, true, true));
2181 // Tests with a localised decimal separator.
2182 $this->define_local_decimal_separator();
2184 // Localisation on (default).
2185 $this->assertEquals('5X43000', format_float(5.43, 5));
2186 $this->assertEquals('5X43', format_float(5.43, 5, true, true));
2188 // Localisation off.
2189 $this->assertEquals('5.43000', format_float(5.43, 5, false));
2190 $this->assertEquals('5.43', format_float(5.43, 5, false, true));
2192 // Tests with tilde as localised decimal separator.
2193 $this->define_local_decimal_separator('~');
2195 // Must also work for '~' as decimal separator.
2196 $this->assertEquals('5', format_float(5.0001, 3, true, true));
2197 $this->assertEquals('5~43000', format_float(5.43, 5));
2198 $this->assertEquals('5~43', format_float(5.43, 5, true, true));
2202 * Test localised float unformatting.
2204 public function test_unformat_float(): void {
2206 // Tests without the localised decimal separator.
2208 // Special case for null, empty or white spaces only strings.
2209 $this->assertEquals(null, unformat_float(null));
2210 $this->assertEquals(null, unformat_float(''));
2211 $this->assertEquals(null, unformat_float(' '));
2213 // Regular use.
2214 $this->assertEquals(5.4, unformat_float('5.4'));
2215 $this->assertEquals(5.4, unformat_float('5.4', true));
2217 // No decimal.
2218 $this->assertEquals(5.0, unformat_float('5'));
2220 // Custom number of decimal.
2221 $this->assertEquals(5.43267, unformat_float('5.43267'));
2223 // Empty decimal.
2224 $this->assertEquals(100.0, unformat_float('100.00'));
2226 // With the thousand separator.
2227 $this->assertEquals(1000.0, unformat_float('1 000'));
2228 $this->assertEquals(1000.32, unformat_float('1 000.32'));
2230 // Negative number.
2231 $this->assertEquals(-100.0, unformat_float('-100'));
2233 // Wrong value.
2234 $this->assertEquals(0.0, unformat_float('Wrong value'));
2235 // Wrong value in strict mode.
2236 $this->assertFalse(unformat_float('Wrong value', true));
2238 // Combining options.
2239 $this->assertEquals(-1023.862567, unformat_float(' -1 023.862567 '));
2241 // Bad decimal separator (should crop the decimal).
2242 $this->assertEquals(50.0, unformat_float('50,57'));
2243 // Bad decimal separator in strict mode (should return false).
2244 $this->assertFalse(unformat_float('50,57', true));
2246 // Tests with a localised decimal separator.
2247 $this->define_local_decimal_separator();
2249 // We repeat the tests above but with the current decimal separator.
2251 // Regular use without and with the localised separator.
2252 $this->assertEquals (5.4, unformat_float('5.4'));
2253 $this->assertEquals (5.4, unformat_float('5X4'));
2255 // Custom number of decimal.
2256 $this->assertEquals (5.43267, unformat_float('5X43267'));
2258 // Empty decimal.
2259 $this->assertEquals (100.0, unformat_float('100X00'));
2261 // With the thousand separator.
2262 $this->assertEquals (1000.32, unformat_float('1 000X32'));
2264 // Bad different separator (should crop the decimal).
2265 $this->assertEquals (50.0, unformat_float('50Y57'));
2266 // Bad different separator in strict mode (should return false).
2267 $this->assertFalse (unformat_float('50Y57', true));
2269 // Combining options.
2270 $this->assertEquals (-1023.862567, unformat_float(' -1 023X862567 '));
2271 // Combining options in strict mode.
2272 $this->assertEquals (-1023.862567, unformat_float(' -1 023X862567 ', true));
2276 * Test deleting of users.
2278 public function test_delete_user(): void {
2279 global $DB, $CFG;
2281 $this->resetAfterTest();
2283 $guest = $DB->get_record('user', array('id'=>$CFG->siteguest), '*', MUST_EXIST);
2284 $admin = $DB->get_record('user', array('id'=>$CFG->siteadmins), '*', MUST_EXIST);
2285 $this->assertEquals(0, $DB->count_records('user', array('deleted'=>1)));
2287 $user = $this->getDataGenerator()->create_user(array('idnumber'=>'abc'));
2288 $user2 = $this->getDataGenerator()->create_user(array('idnumber'=>'xyz'));
2289 $usersharedemail1 = $this->getDataGenerator()->create_user(array('email' => 'sharedemail@example.invalid'));
2290 $usersharedemail2 = $this->getDataGenerator()->create_user(array('email' => 'sharedemail@example.invalid'));
2291 $useremptyemail1 = $this->getDataGenerator()->create_user(array('email' => ''));
2292 $useremptyemail2 = $this->getDataGenerator()->create_user(array('email' => ''));
2294 // Delete user and capture event.
2295 $sink = $this->redirectEvents();
2296 $result = delete_user($user);
2297 $events = $sink->get_events();
2298 $sink->close();
2299 $event = array_pop($events);
2301 // Test user is deleted in DB.
2302 $this->assertTrue($result);
2303 $deluser = $DB->get_record('user', array('id'=>$user->id), '*', MUST_EXIST);
2304 $this->assertEquals(1, $deluser->deleted);
2305 $this->assertEquals(0, $deluser->picture);
2306 $this->assertSame('', $deluser->idnumber);
2307 $this->assertSame(md5($user->username), $deluser->email);
2308 $this->assertMatchesRegularExpression('/^'.preg_quote($user->email, '/').'\.\d*$/', $deluser->username);
2310 $this->assertEquals(1, $DB->count_records('user', array('deleted'=>1)));
2312 // Test Event.
2313 $this->assertInstanceOf('\core\event\user_deleted', $event);
2314 $this->assertSame($user->id, $event->objectid);
2315 $eventdata = $event->get_data();
2316 $this->assertSame($eventdata['other']['username'], $user->username);
2317 $this->assertSame($eventdata['other']['email'], $user->email);
2318 $this->assertSame($eventdata['other']['idnumber'], $user->idnumber);
2319 $this->assertSame($eventdata['other']['picture'], $user->picture);
2320 $this->assertSame($eventdata['other']['mnethostid'], $user->mnethostid);
2321 $this->assertEquals($user, $event->get_record_snapshot('user', $event->objectid));
2322 $this->assertEventContextNotUsed($event);
2324 // Try invalid params.
2325 $record = new \stdClass();
2326 $record->grrr = 1;
2327 try {
2328 delete_user($record);
2329 $this->fail('Expecting exception for invalid delete_user() $user parameter');
2330 } catch (\moodle_exception $ex) {
2331 $this->assertInstanceOf('coding_exception', $ex);
2333 $record->id = 1;
2334 try {
2335 delete_user($record);
2336 $this->fail('Expecting exception for invalid delete_user() $user parameter');
2337 } catch (\moodle_exception $ex) {
2338 $this->assertInstanceOf('coding_exception', $ex);
2341 $record = new \stdClass();
2342 $record->id = 666;
2343 $record->username = 'xx';
2344 $this->assertFalse($DB->record_exists('user', array('id'=>666))); // Any non-existent id is ok.
2345 $result = delete_user($record);
2346 $this->assertFalse($result);
2348 $result = delete_user($guest);
2349 $this->assertFalse($result);
2351 $result = delete_user($admin);
2352 $this->assertFalse($result);
2354 // Simultaneously deleting users with identical email addresses.
2355 $result1 = delete_user($usersharedemail1);
2356 $result2 = delete_user($usersharedemail2);
2358 $usersharedemail1after = $DB->get_record('user', array('id' => $usersharedemail1->id));
2359 $usersharedemail2after = $DB->get_record('user', array('id' => $usersharedemail2->id));
2360 $this->assertTrue($result1);
2361 $this->assertTrue($result2);
2362 $this->assertStringStartsWith($usersharedemail1->email . '.', $usersharedemail1after->username);
2363 $this->assertStringStartsWith($usersharedemail2->email . '.', $usersharedemail2after->username);
2365 // Simultaneously deleting users without email addresses.
2366 $result1 = delete_user($useremptyemail1);
2367 $result2 = delete_user($useremptyemail2);
2369 $useremptyemail1after = $DB->get_record('user', array('id' => $useremptyemail1->id));
2370 $useremptyemail2after = $DB->get_record('user', array('id' => $useremptyemail2->id));
2371 $this->assertTrue($result1);
2372 $this->assertTrue($result2);
2373 $this->assertStringStartsWith($useremptyemail1->username . '.' . $useremptyemail1->id . '@unknownemail.invalid.',
2374 $useremptyemail1after->username);
2375 $this->assertStringStartsWith($useremptyemail2->username . '.' . $useremptyemail2->id . '@unknownemail.invalid.',
2376 $useremptyemail2after->username);
2378 $this->resetDebugging();
2382 * Test deletion of user with long username
2384 public function test_delete_user_long_username(): void {
2385 global $DB;
2387 $this->resetAfterTest();
2389 // For users without an e-mail, one will be created during deletion using {$username}.{$id}@unknownemail.invalid format.
2390 $user = $this->getDataGenerator()->create_user([
2391 'username' => str_repeat('a', 75),
2392 'email' => '',
2395 delete_user($user);
2397 // The username for the deleted user shouldn't exceed 100 characters.
2398 $usernamedeleted = $DB->get_field('user', 'username', ['id' => $user->id]);
2399 $this->assertEquals(100, \core_text::strlen($usernamedeleted));
2401 $timestrlength = \core_text::strlen((string) time());
2403 // It should start with the user name, and end with the current time.
2404 $this->assertStringStartsWith("{$user->username}.{$user->id}@", $usernamedeleted);
2405 $this->assertMatchesRegularExpression('/\.\d{' . $timestrlength . '}$/', $usernamedeleted);
2409 * Test deletion of user with long email address
2411 public function test_delete_user_long_email(): void {
2412 global $DB;
2414 $this->resetAfterTest();
2416 // Create user with 90 character email address.
2417 $user = $this->getDataGenerator()->create_user([
2418 'email' => str_repeat('a', 78) . '@example.com',
2421 delete_user($user);
2423 // The username for the deleted user shouldn't exceed 100 characters.
2424 $usernamedeleted = $DB->get_field('user', 'username', ['id' => $user->id]);
2425 $this->assertEquals(100, \core_text::strlen($usernamedeleted));
2427 $timestrlength = \core_text::strlen((string) time());
2429 // Max username length is 100 chars. Select up to limit - (length of current time + 1 [period character]) from users email.
2430 $expectedemail = \core_text::substr($user->email, 0, 100 - ($timestrlength + 1));
2431 $this->assertMatchesRegularExpression('/^' . preg_quote($expectedemail) . '\.\d{' . $timestrlength . '}$/',
2432 $usernamedeleted);
2436 * Test function convert_to_array()
2438 public function test_convert_to_array(): void {
2439 // Check that normal classes are converted to arrays the same way as (array) would do.
2440 $obj = new \stdClass();
2441 $obj->prop1 = 'hello';
2442 $obj->prop2 = array('first', 'second', 13);
2443 $obj->prop3 = 15;
2444 $this->assertEquals(convert_to_array($obj), (array)$obj);
2446 // Check that context object (with iterator) is converted to array properly.
2447 $obj = \context_system::instance();
2448 $ar = array(
2449 'id' => $obj->id,
2450 'contextlevel' => $obj->contextlevel,
2451 'instanceid' => $obj->instanceid,
2452 'path' => $obj->path,
2453 'depth' => $obj->depth,
2454 'locked' => $obj->locked,
2456 $this->assertEquals(convert_to_array($obj), $ar);
2460 * Test the function date_format_string().
2462 public function test_date_format_string(): void {
2463 global $CFG;
2465 $this->resetAfterTest();
2466 $this->setTimezone(99, 'Australia/Perth');
2468 $tests = array(
2469 array(
2470 'tz' => 99,
2471 'str' => '%A, %d %B %Y, %I:%M %p',
2472 'expected' => 'Saturday, 01 January 2011, 06:00 PM'
2474 array(
2475 'tz' => 0,
2476 'str' => '%A, %d %B %Y, %I:%M %p',
2477 'expected' => 'Saturday, 01 January 2011, 10:00 AM'
2479 array(
2480 // Note: this function expected the timestamp in weird format before,
2481 // since 2.9 it uses UTC.
2482 'tz' => 'Pacific/Auckland',
2483 'str' => '%A, %d %B %Y, %I:%M %p',
2484 'expected' => 'Saturday, 01 January 2011, 11:00 PM'
2486 // Following tests pass on Windows only because en lang pack does
2487 // not contain localewincharset, in real life lang pack maintainers
2488 // may use only characters that are present in localewincharset
2489 // in format strings!
2490 array(
2491 'tz' => 99,
2492 'str' => 'Žluťoučký koníček %A',
2493 'expected' => 'Žluťoučký koníček Saturday'
2495 array(
2496 'tz' => 99,
2497 'str' => '言語設定言語 %A',
2498 'expected' => '言語設定言語 Saturday'
2500 array(
2501 'tz' => 99,
2502 'str' => '简体中文简体 %A',
2503 'expected' => '简体中文简体 Saturday'
2507 // Note: date_format_string() uses the timezone only to differenciate
2508 // the server time from the UTC time. It does not modify the timestamp.
2509 // Hence similar results for timezones <= 13.
2510 // On different systems case of AM PM changes so compare case insensitive.
2511 foreach ($tests as $test) {
2512 $str = date_format_string(1293876000, $test['str'], $test['tz']);
2513 $this->assertSame(\core_text::strtolower($test['expected']), \core_text::strtolower($str));
2517 public function test_get_config(): void {
2518 global $CFG;
2520 $this->resetAfterTest();
2522 // Preparation.
2523 set_config('phpunit_test_get_config_1', 'test 1');
2524 set_config('phpunit_test_get_config_2', 'test 2', 'mod_forum');
2525 if (!is_array($CFG->config_php_settings)) {
2526 $CFG->config_php_settings = array();
2528 $CFG->config_php_settings['phpunit_test_get_config_3'] = 'test 3';
2530 if (!is_array($CFG->forced_plugin_settings)) {
2531 $CFG->forced_plugin_settings = array();
2533 if (!array_key_exists('mod_forum', $CFG->forced_plugin_settings)) {
2534 $CFG->forced_plugin_settings['mod_forum'] = array();
2536 $CFG->forced_plugin_settings['mod_forum']['phpunit_test_get_config_4'] = 'test 4';
2537 $CFG->phpunit_test_get_config_5 = 'test 5';
2539 // Testing.
2540 $this->assertSame('test 1', get_config('core', 'phpunit_test_get_config_1'));
2541 $this->assertSame('test 2', get_config('mod_forum', 'phpunit_test_get_config_2'));
2542 $this->assertSame('test 3', get_config('core', 'phpunit_test_get_config_3'));
2543 $this->assertSame('test 4', get_config('mod_forum', 'phpunit_test_get_config_4'));
2544 $this->assertFalse(get_config('core', 'phpunit_test_get_config_5'));
2545 $this->assertFalse(get_config('core', 'phpunit_test_get_config_x'));
2546 $this->assertFalse(get_config('mod_forum', 'phpunit_test_get_config_x'));
2548 // Test config we know to exist.
2549 $this->assertSame($CFG->dataroot, get_config('core', 'dataroot'));
2550 $this->assertSame($CFG->phpunit_dataroot, get_config('core', 'phpunit_dataroot'));
2551 $this->assertSame($CFG->dataroot, get_config('core', 'phpunit_dataroot'));
2552 $this->assertSame(get_config('core', 'dataroot'), get_config('core', 'phpunit_dataroot'));
2554 // Test setting a config var that already exists.
2555 set_config('phpunit_test_get_config_1', 'test a');
2556 $this->assertSame('test a', $CFG->phpunit_test_get_config_1);
2557 $this->assertSame('test a', get_config('core', 'phpunit_test_get_config_1'));
2559 // Test cache invalidation.
2560 $cache = \cache::make('core', 'config');
2561 $this->assertIsArray($cache->get('core'));
2562 $this->assertIsArray($cache->get('mod_forum'));
2563 set_config('phpunit_test_get_config_1', 'test b');
2564 $this->assertFalse($cache->get('core'));
2565 set_config('phpunit_test_get_config_4', 'test c', 'mod_forum');
2566 $this->assertFalse($cache->get('mod_forum'));
2569 public function test_get_max_upload_sizes(): void {
2570 // Test with very low limits so we are not affected by php upload limits.
2571 // Test activity limit smallest.
2572 $sitebytes = 102400;
2573 $coursebytes = 51200;
2574 $modulebytes = 10240;
2575 $result = get_max_upload_sizes($sitebytes, $coursebytes, $modulebytes);
2577 $nbsp = "\xc2\xa0";
2578 $this->assertSame("Activity upload limit (10{$nbsp}KB)", $result['0']);
2579 $this->assertCount(2, $result);
2581 // Test course limit smallest.
2582 $sitebytes = 102400;
2583 $coursebytes = 10240;
2584 $modulebytes = 51200;
2585 $result = get_max_upload_sizes($sitebytes, $coursebytes, $modulebytes);
2587 $this->assertSame("Course upload limit (10{$nbsp}KB)", $result['0']);
2588 $this->assertCount(2, $result);
2590 // Test site limit smallest.
2591 $sitebytes = 10240;
2592 $coursebytes = 102400;
2593 $modulebytes = 51200;
2594 $result = get_max_upload_sizes($sitebytes, $coursebytes, $modulebytes);
2596 $this->assertSame("Site upload limit (10{$nbsp}KB)", $result['0']);
2597 $this->assertCount(2, $result);
2599 // Test site limit not set.
2600 $sitebytes = 0;
2601 $coursebytes = 102400;
2602 $modulebytes = 51200;
2603 $result = get_max_upload_sizes($sitebytes, $coursebytes, $modulebytes);
2605 $this->assertSame("Activity upload limit (50{$nbsp}KB)", $result['0']);
2606 $this->assertCount(3, $result);
2608 $sitebytes = 0;
2609 $coursebytes = 51200;
2610 $modulebytes = 102400;
2611 $result = get_max_upload_sizes($sitebytes, $coursebytes, $modulebytes);
2613 $this->assertSame("Course upload limit (50{$nbsp}KB)", $result['0']);
2614 $this->assertCount(3, $result);
2616 // Test custom bytes in range.
2617 $sitebytes = 102400;
2618 $coursebytes = 51200;
2619 $modulebytes = 51200;
2620 $custombytes = 10240;
2621 $result = get_max_upload_sizes($sitebytes, $coursebytes, $modulebytes, $custombytes);
2623 $this->assertCount(3, $result);
2625 // Test custom bytes in range but non-standard.
2626 $sitebytes = 102400;
2627 $coursebytes = 51200;
2628 $modulebytes = 51200;
2629 $custombytes = 25600;
2630 $result = get_max_upload_sizes($sitebytes, $coursebytes, $modulebytes, $custombytes);
2632 $this->assertCount(4, $result);
2634 // Test custom bytes out of range.
2635 $sitebytes = 102400;
2636 $coursebytes = 51200;
2637 $modulebytes = 51200;
2638 $custombytes = 102400;
2639 $result = get_max_upload_sizes($sitebytes, $coursebytes, $modulebytes, $custombytes);
2641 $this->assertCount(3, $result);
2643 // Test custom bytes out of range and non-standard.
2644 $sitebytes = 102400;
2645 $coursebytes = 51200;
2646 $modulebytes = 51200;
2647 $custombytes = 256000;
2648 $result = get_max_upload_sizes($sitebytes, $coursebytes, $modulebytes, $custombytes);
2650 $this->assertCount(3, $result);
2652 // Test site limit only.
2653 $sitebytes = 51200;
2654 $result = get_max_upload_sizes($sitebytes);
2656 $this->assertSame("Site upload limit (50{$nbsp}KB)", $result['0']);
2657 $this->assertSame("50{$nbsp}KB", $result['51200']);
2658 $this->assertSame("10{$nbsp}KB", $result['10240']);
2659 $this->assertCount(3, $result);
2661 // Test no limit.
2662 $result = get_max_upload_sizes();
2663 $this->assertArrayHasKey('0', $result);
2664 $this->assertArrayHasKey(get_max_upload_file_size(), $result);
2668 * Test function password_is_legacy_hash.
2669 * @covers ::password_is_legacy_hash
2671 public function test_password_is_legacy_hash(): void {
2672 // Well formed bcrypt hashes should be matched.
2673 foreach (array('some', 'strings', 'to_check!') as $password) {
2674 $bcrypt = password_hash($password, '2y');
2675 $this->assertTrue(password_is_legacy_hash($bcrypt));
2677 // Strings that are not bcrypt should not be matched.
2678 $sha512 = '$6$rounds=5000$somesalt$9nEA35u5h4oDrUdcVFUwXDSwIBiZtuKDHiaI/kxnBSslH4wVXeAhVsDn1UFxBxrnRJva/8dZ8IouaijJdd4cF';
2679 foreach (array('', AUTH_PASSWORD_NOT_CACHED, $sha512) as $notbcrypt) {
2680 $this->assertFalse(password_is_legacy_hash($notbcrypt));
2685 * Test function that calculates password pepper entropy.
2686 * @covers ::calculate_entropy
2688 public function test_calculate_entropy(): void {
2689 // Test that the function returns 0 with an empty string.
2690 $this->assertEquals(0, calculate_entropy(''));
2692 // Test that the function returns the correct entropy.
2693 $this->assertEquals(132.8814, number_format(calculate_entropy('#GV]NLie|x$H9[$rW%94bXZvJHa%z'), 4));
2697 * Test function to get password peppers.
2698 * @covers ::get_password_peppers
2700 public function test_get_password_peppers(): void {
2701 global $CFG;
2702 $this->resetAfterTest();
2704 // First assert that the function returns an empty array,
2705 // when no peppers are set.
2706 $this->assertEquals([], get_password_peppers());
2708 // Now set some peppers and check that they are returned.
2709 $CFG->passwordpeppers = [
2710 1 => '#GV]NLie|x$H9[$rW%94bXZvJHa%z',
2711 2 => '#GV]NLie|x$H9[$rW%94bXZvJHa%$'
2713 $peppers = get_password_peppers();
2714 $this->assertCount(2, $peppers);
2715 $this->assertEquals($CFG->passwordpeppers, $peppers);
2717 // Check that the peppers are returned in the correct order.
2718 // Highest numerical key first.
2719 $this->assertEquals('#GV]NLie|x$H9[$rW%94bXZvJHa%$', $peppers[2]);
2720 $this->assertEquals('#GV]NLie|x$H9[$rW%94bXZvJHa%z', $peppers[1]);
2722 // Update the latest pepper to be an empty string,
2723 // to test phasing out peppers.
2724 $CFG->passwordpeppers = [
2725 1 => '#GV]NLie|x$H9[$rW%94bXZvJHa%z',
2726 2 => '#GV]NLie|x$H9[$rW%94bXZvJHa%$',
2727 3 => ''
2729 $peppers = get_password_peppers();
2730 $this->assertCount(3, $peppers);
2731 $this->assertEquals($CFG->passwordpeppers, $peppers);
2733 // Finally, check that low entropy peppers throw an exception.
2734 $CFG->passwordpeppers = [
2735 1 => 'foo',
2736 2 => 'bar'
2738 $this->expectException(\coding_exception::class);
2739 get_password_peppers();
2743 * Test function to validate password length.
2745 * @covers ::exceeds_password_length
2746 * @return void
2748 public function test_exceeds_password_length(): void {
2749 $this->resetAfterTest(true);
2751 // With password less than equals to MAX_PASSWORD_CHARACTERS.
2752 $this->assertFalse(exceeds_password_length('test'));
2754 // With password more than MAX_PASSWORD_CHARACTERS.
2755 $password = 'thisisapasswordthatcontainscharactersthatcan';
2756 $password .= 'exeedthepasswordlengthof128thisispasswordthatcont';
2757 $password .= 'ainscharactersthatcanexeedthelength-----';
2758 $this->assertTrue(exceeds_password_length($password));
2762 * Test function validate_internal_user_password.
2763 * @covers ::validate_internal_user_password
2765 public function test_validate_internal_user_password(): void {
2766 $this->resetAfterTest(true);
2767 // Test bcrypt hashes (these will be updated but will still count as valid).
2768 $bcrypthashes = [
2769 'pw' => '$2y$10$LOSDi5eaQJhutSRun.OVJ.ZSxQZabCMay7TO1KmzMkDMPvU40zGXK',
2770 'abc' => '$2y$10$VWTOhVdsBbWwtdWNDRHSpewjd3aXBQlBQf5rBY/hVhw8hciarFhXa',
2771 'C0mP1eX_&}<?@*&%` |\"' => '$2y$10$3PJf.q.9ywNJlsInPbqc8.IFeSsvXrGvQLKRFBIhVu1h1I3vpIry6',
2772 'ĩńťėŕňăţĩōŋāĹ' => '$2y$10$3A2Y8WpfRAnP3czJiSv6N.6Xp0T8hW3QZz2hUCYhzyWr1kGP1yUve',
2775 // Test sha512 hashes.
2776 $sha512hashes = [
2777 'pw2' => '$6$rounds=10000$0rDIzh/4.MMf9Dm8$Zrj6Ulc1JFj0RFXwMJFsngRSNGlqkPlV1wwRVv7wBLrMeQeMZrwsBO62zy63D//6R5sNGVYQwPB0K8jPCScxB/',
2778 'abc2' => '$6$rounds=10000$t0L6PklgpijV4tMB$1vpCRKCImsVqTPMiZTi6zLGbs.hpAU8I2BhD/IFliBnHJkFZCWEBfTCq6pEzo0Q8nXsryrgeZ.qngcW.eifuW.',
2779 'C0mP1eX_&}<?@*&%` |\"2' => '$6$rounds=10000$3TAyVAXN0zmFZ4il$KF8YzduX6Gu0C2xHsY83zoqQ/rLVsb9mLe417wDObo9tO00qeUC68/y2tMq4FL2ixnMPH3OMwzGYo8VJrm8Eq1',
2780 'ĩńťėŕňăţĩōŋāĹ2' => '$6$rounds=10000$SHR/6ctTkfXOy5NP$YPv42hjDjohVWD3B0boyEYTnLcBXBKO933ijHmkPXNL7BpqAcbYMLfTl9rjsPmCt.1GZvEJZ8ikkCPYBC5Sdp.',
2783 $validhashes = array_merge($bcrypthashes, $sha512hashes);
2785 foreach ($validhashes as $password => $hash) {
2786 $user = $this->getDataGenerator()->create_user(array('auth' => 'manual', 'password' => $password));
2787 $user->password = $hash;
2788 // The correct password should be validated.
2789 $this->assertTrue(validate_internal_user_password($user, $password));
2790 // An incorrect password should not be validated.
2791 $this->assertFalse(validate_internal_user_password($user, 'badpw'));
2796 * Test function validate_internal_user_password() with a peppered password,
2797 * when the pepper no longer exists.
2799 * @covers ::validate_internal_user_password
2801 public function test_validate_internal_user_password_bad_pepper(): void {
2802 global $CFG;
2803 $this->resetAfterTest();
2805 // Set a pepper.
2806 $CFG->passwordpeppers = [
2807 1 => '#GV]NLie|x$H9[$rW%94bXZvJHa%z',
2808 2 => '#GV]NLie|x$H9[$rW%94bXZvJHa%$'
2810 $password = 'test';
2812 $user = $this->getDataGenerator()->create_user(['auth' => 'manual', 'password' => $password]);
2813 $this->assertTrue(validate_internal_user_password($user, $password));
2814 $this->assertFalse(validate_internal_user_password($user, 'badpw'));
2816 // Now remove the peppers.
2817 // Things should not work.
2818 unset($CFG->passwordpeppers);
2819 $this->assertFalse(validate_internal_user_password($user, $password));
2823 * Helper method to test hashing passwords.
2825 * @param array $passwords
2826 * @return void
2827 * @covers ::hash_internal_user_password
2829 public function validate_hashed_passwords(array $passwords): void {
2830 foreach ($passwords as $password) {
2831 $hash = hash_internal_user_password($password);
2832 $fasthash = hash_internal_user_password($password, true);
2833 $user = $this->getDataGenerator()->create_user(['auth' => 'manual']);
2834 $user->password = $hash;
2835 $this->assertTrue(validate_internal_user_password($user, $password));
2837 // They should not be in bycrypt format.
2838 $this->assertFalse(password_is_legacy_hash($hash));
2840 // Check that cost factor in hash is correctly set.
2841 $this->assertMatchesRegularExpression('/\$6\$rounds=10000\$.{103}/', $hash);
2842 $this->assertMatchesRegularExpression('/\$6\$rounds=5000\$.{103}/', $fasthash);
2847 * Test function update_internal_user_password.
2848 * @covers ::update_internal_user_password
2850 public function test_hash_internal_user_password(): void {
2851 global $CFG;
2852 $this->resetAfterTest();
2853 $passwords = ['pw', 'abc123', 'C0mP1eX_&}<?@*&%` |\"', 'ĩńťėŕňăţĩōŋāĹ'];
2855 // Check that some passwords that we convert to hashes can
2856 // be validated.
2857 $this->validate_hashed_passwords($passwords);
2859 // Test again with peppers.
2860 $CFG->passwordpeppers = [
2861 1 => '#GV]NLie|x$H9[$rW%94bXZvJHa%z',
2862 2 => '#GV]NLie|x$H9[$rW%94bXZvJHa%$'
2864 $this->validate_hashed_passwords($passwords);
2866 // Add a new pepper and check that things still pass.
2867 $CFG->passwordpeppers = [
2868 1 => '#GV]NLie|x$H9[$rW%94bXZvJHa%z',
2869 2 => '#GV]NLie|x$H9[$rW%94bXZvJHa%$',
2870 3 => '#GV]NLie|x$H9[$rW%94bXZvJHQ%$'
2872 $this->validate_hashed_passwords($passwords);
2876 * Test function update_internal_user_password().
2878 public function test_update_internal_user_password(): void {
2879 global $DB;
2880 $this->resetAfterTest();
2881 $passwords = array('password', '1234', 'changeme', '****');
2882 foreach ($passwords as $password) {
2883 $user = $this->getDataGenerator()->create_user(array('auth'=>'manual'));
2884 update_internal_user_password($user, $password);
2885 // The user object should have been updated.
2886 $this->assertTrue(validate_internal_user_password($user, $password));
2887 // The database field for the user should also have been updated to the
2888 // same value.
2889 $this->assertSame($user->password, $DB->get_field('user', 'password', array('id' => $user->id)));
2892 $user = $this->getDataGenerator()->create_user(array('auth'=>'manual'));
2893 // Manually set the user's password to the bcrypt of the string 'password'.
2894 $DB->set_field('user', 'password', '$2y$10$HhNAYmQcU1GqU/psOmZjfOWlhPEcxx9aEgSJqBfEtYVyq1jPKqMAi', ['id' => $user->id]);
2896 $sink = $this->redirectEvents();
2897 // Update the password.
2898 update_internal_user_password($user, 'password');
2899 $events = $sink->get_events();
2900 $sink->close();
2901 $event = array_pop($events);
2903 // Password should have been updated to a SHA512 hash.
2904 $this->assertFalse(password_is_legacy_hash($user->password));
2906 // Verify event information.
2907 $this->assertInstanceOf('\core\event\user_password_updated', $event);
2908 $this->assertSame($user->id, $event->relateduserid);
2909 $this->assertEquals(\context_user::instance($user->id), $event->get_context());
2910 $this->assertEventContextNotUsed($event);
2912 // Verify recovery of property 'auth'.
2913 unset($user->auth);
2914 update_internal_user_password($user, 'newpassword');
2915 $this->assertDebuggingCalled('User record in update_internal_user_password() must include field auth',
2916 DEBUG_DEVELOPER);
2917 $this->assertEquals('manual', $user->auth);
2921 * Testing that if the password is not cached, that it does not update
2922 * the user table and fire event.
2924 * @dataProvider update_internal_user_password_no_cache_provider
2925 * @covers ::update_internal_user_password
2927 * @param string $authmethod The authentication method to set for the user.
2928 * @param string|null $password The new password to set for the user.
2930 public function test_update_internal_user_password_no_cache(
2931 string $authmethod,
2932 ?string $password,
2933 ): void {
2934 global $DB;
2935 $this->resetAfterTest();
2937 $user = $this->getDataGenerator()->create_user(['auth' => $authmethod]);
2938 $DB->update_record('user', ['id' => $user->id, 'password' => AUTH_PASSWORD_NOT_CACHED]);
2939 $user->password = AUTH_PASSWORD_NOT_CACHED;
2941 $sink = $this->redirectEvents();
2942 update_internal_user_password($user, $password);
2943 $this->assertEquals(0, $sink->count(), 'User updated event should not fire');
2947 * The data provider will test the {@see test_update_internal_user_password_no_cache}
2948 * for accounts using the authentication method with prevent_local_passwords set to true (no cache).
2950 * @return array
2952 public static function update_internal_user_password_no_cache_provider(): array {
2953 return [
2954 'Password is not empty' => ['cas', 'wonkawonka'],
2955 'Password is an empty string' => ['oauth2', ''],
2956 'Password is null' => ['oauth2', null],
2961 * Test if the user has a password hash, but now their auth method
2962 * says not to cache it. Then it should update.
2964 public function test_update_internal_user_password_update_no_cache(): void {
2965 $this->resetAfterTest();
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.
2971 $sink = $this->redirectEvents();
2972 update_internal_user_password($user, 'wonkawonka');
2973 $this->assertGreaterThanOrEqual(1, $sink->count(), 'User updated event should fire');
2975 $this->assertEquals(AUTH_PASSWORD_NOT_CACHED, $user->password);
2978 public function test_fullname(): void {
2979 global $CFG;
2981 $this->resetAfterTest();
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);
2992 // Back up config settings for restore later.
2993 $originalcfg = new \stdClass();
2994 $originalcfg->fullnamedisplay = $CFG->fullnamedisplay;
2995 $originalcfg->alternativefullnameformat = $CFG->alternativefullnameformat;
2997 // Testing existing fullnamedisplay settings.
2998 $CFG->fullnamedisplay = 'firstname';
2999 $testname = fullname($user);
3000 $this->assertSame($user->firstname, $testname);
3002 $CFG->fullnamedisplay = 'firstname lastname';
3003 $expectedname = "$user->firstname $user->lastname";
3004 $testname = fullname($user);
3005 $this->assertSame($expectedname, $testname);
3007 $CFG->fullnamedisplay = 'lastname firstname';
3008 $expectedname = "$user->lastname $user->firstname";
3009 $testname = fullname($user);
3010 $this->assertSame($expectedname, $testname);
3012 $expectedname = get_string('fullnamedisplay', null, $user);
3013 $CFG->fullnamedisplay = 'language';
3014 $testname = fullname($user);
3015 $this->assertSame($expectedname, $testname);
3017 // Test override parameter.
3018 $CFG->fullnamedisplay = 'firstname';
3019 $expectedname = "$user->firstname $user->lastname";
3020 $testname = fullname($user, true);
3021 $this->assertSame($expectedname, $testname);
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);
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);
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);
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);
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);
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);
3062 // Regular expression testing.
3063 // Remove some data from the user fields.
3064 $user->firstnamephonetic = '';
3065 $user->lastnamephonetic = '';
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」';
3075 foreach ($configarray as $config) {
3076 $CFG->fullnamedisplay = $config;
3077 $expectedname = "$user->firstname $user->lastname";
3078 $testname = fullname($user);
3079 $this->assertSame($expectedname, $testname);
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\"";
3103 foreach ($configarray as $config) {
3104 $CFG->fullnamedisplay = $config->config;
3105 $expectedname = $config->expectedname;
3106 $testname = fullname($user);
3107 $this->assertSame($expectedname, $testname);
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();
3120 // Tidy up after we finish testing.
3121 $CFG->fullnamedisplay = $originalcfg->fullnamedisplay;
3122 $CFG->alternativefullnameformat = $originalcfg->alternativefullnameformat;
3125 public function test_order_in_string(): void {
3126 $this->resetAfterTest();
3128 // Return an array in an order as they are encountered in a string.
3129 $valuearray = array('second', 'firsthalf', 'first');
3130 $formatstring = 'first firsthalf some other text (second)';
3131 $expectedarray = array('0' => 'first', '6' => 'firsthalf', '33' => 'second');
3132 $this->assertEquals($expectedarray, order_in_string($valuearray, $formatstring));
3134 // Try again with a different order for the format.
3135 $valuearray = array('second', 'firsthalf', 'first');
3136 $formatstring = 'firsthalf first second';
3137 $expectedarray = array('0' => 'firsthalf', '10' => 'first', '16' => 'second');
3138 $this->assertEquals($expectedarray, order_in_string($valuearray, $formatstring));
3140 // Try again with yet another different order for the format.
3141 $valuearray = array('second', 'firsthalf', 'first');
3142 $formatstring = 'start seconds away second firstquater first firsthalf';
3143 $expectedarray = array('19' => 'second', '38' => 'first', '44' => 'firsthalf');
3144 $this->assertEquals($expectedarray, order_in_string($valuearray, $formatstring));
3147 public function test_complete_user_login(): void {
3148 global $USER, $DB;
3150 $this->resetAfterTest();
3151 $user = $this->getDataGenerator()->create_user();
3152 $this->setUser(0);
3154 $sink = $this->redirectEvents();
3155 $loginuser = clone($user);
3156 $this->setCurrentTimeStart();
3157 @complete_user_login($loginuser); // Hide session header errors.
3158 $this->assertSame($loginuser, $USER);
3159 $this->assertEquals($user->id, $USER->id);
3160 $events = $sink->get_events();
3161 $sink->close();
3163 $this->assertCount(1, $events);
3164 $event = reset($events);
3165 $this->assertInstanceOf('\core\event\user_loggedin', $event);
3166 $this->assertEquals('user', $event->objecttable);
3167 $this->assertEquals($user->id, $event->objectid);
3168 $this->assertEquals(\context_system::instance()->id, $event->contextid);
3169 $this->assertEventContextNotUsed($event);
3171 $user = $DB->get_record('user', array('id'=>$user->id));
3173 $this->assertTimeCurrent($user->firstaccess);
3174 $this->assertTimeCurrent($user->lastaccess);
3176 $this->assertTimeCurrent($USER->firstaccess);
3177 $this->assertTimeCurrent($USER->lastaccess);
3178 $this->assertTimeCurrent($USER->currentlogin);
3179 $this->assertSame(sesskey(), $USER->sesskey);
3180 $this->assertTimeCurrent($USER->preference['_lastloaded']);
3181 $this->assertObjectNotHasProperty('password', $USER);
3182 $this->assertObjectNotHasProperty('description', $USER);
3186 * Test require_logout.
3188 public function test_require_logout(): void {
3189 $this->resetAfterTest();
3190 $user = $this->getDataGenerator()->create_user();
3191 $this->setUser($user);
3193 $this->assertTrue(isloggedin());
3195 // Logout user and capture event.
3196 $sink = $this->redirectEvents();
3197 require_logout();
3198 $events = $sink->get_events();
3199 $sink->close();
3200 $event = array_pop($events);
3202 // Check if user is logged out.
3203 $this->assertFalse(isloggedin());
3205 // Test Event.
3206 $this->assertInstanceOf('\core\event\user_loggedout', $event);
3207 $this->assertSame($user->id, $event->objectid);
3208 $this->assertEventContextNotUsed($event);
3212 * A data provider for testing email messageid
3214 public function generate_email_messageid_provider() {
3215 return array(
3216 'nopath' => array(
3217 'wwwroot' => 'http://www.example.com',
3218 'ids' => array(
3219 'a-custom-id' => '<a-custom-id@www.example.com>',
3220 'an-id-with-/-a-slash' => '<an-id-with-%2F-a-slash@www.example.com>',
3223 'path' => array(
3224 'wwwroot' => 'http://www.example.com/path/subdir',
3225 'ids' => array(
3226 'a-custom-id' => '<a-custom-id/path/subdir@www.example.com>',
3227 'an-id-with-/-a-slash' => '<an-id-with-%2F-a-slash/path/subdir@www.example.com>',
3234 * Test email message id generation
3236 * @dataProvider generate_email_messageid_provider
3238 * @param string $wwwroot The wwwroot
3239 * @param array $msgids An array of msgid local parts and the final result
3241 public function test_generate_email_messageid($wwwroot, $msgids): void {
3242 global $CFG;
3244 $this->resetAfterTest();
3245 $CFG->wwwroot = $wwwroot;
3247 foreach ($msgids as $local => $final) {
3248 $this->assertEquals($final, generate_email_messageid($local));
3253 * Test email with custom headers
3255 public function test_send_email_with_custom_header(): void {
3256 global $DB, $CFG;
3257 $this->preventResetByRollback();
3258 $this->resetAfterTest();
3260 $touser = $this->getDataGenerator()->create_user();
3261 $fromuser = $this->getDataGenerator()->create_user();
3262 $fromuser->customheaders = 'X-Custom-Header: foo';
3264 set_config('allowedemaildomains', 'example.com');
3265 set_config('emailheaders', 'X-Fixed-Header: bar');
3267 $sink = $this->redirectEmails();
3268 email_to_user($touser, $fromuser, 'subject', 'message');
3270 $emails = $sink->get_messages();
3271 $this->assertCount(1, $emails);
3272 $email = reset($emails);
3273 $this->assertStringContainsString('X-Custom-Header: foo', $email->header);
3274 $this->assertStringContainsString("X-Fixed-Header: bar", $email->header);
3275 $sink->clear();
3279 * A data provider for testing email diversion
3281 public function diverted_emails_provider() {
3282 return array(
3283 'nodiverts' => array(
3284 'divertallemailsto' => null,
3285 'divertallemailsexcept' => null,
3286 array(
3287 'foo@example.com',
3288 'test@real.com',
3289 'fred.jones@example.com',
3290 'dev1@dev.com',
3291 'fred@example.com',
3292 'fred+verp@example.com',
3294 false,
3296 'alldiverts' => array(
3297 'divertallemailsto' => 'somewhere@elsewhere.com',
3298 'divertallemailsexcept' => null,
3299 array(
3300 'foo@example.com',
3301 'test@real.com',
3302 'fred.jones@example.com',
3303 'dev1@dev.com',
3304 'fred@example.com',
3305 'fred+verp@example.com',
3307 true,
3309 'alsodiverts' => array(
3310 'divertallemailsto' => 'somewhere@elsewhere.com',
3311 'divertallemailsexcept' => '@dev.com, fred(\+.*)?@example.com',
3312 array(
3313 'foo@example.com',
3314 'test@real.com',
3315 'fred.jones@example.com',
3316 'Fred.Jones@Example.com',
3318 true,
3320 'divertsexceptions' => array(
3321 'divertallemailsto' => 'somewhere@elsewhere.com',
3322 'divertallemailsexcept' => '@dev.com, fred(\+.*)?@example.com',
3323 array(
3324 'dev1@dev.com',
3325 'fred@example.com',
3326 'Fred@Example.com',
3327 'fred+verp@example.com',
3329 false,
3331 'divertsexceptionsnewline' => array(
3332 'divertallemailsto' => 'somewhere@elsewhere.com',
3333 'divertallemailsexcept' => "@dev.com\nfred(\+.*)?@example.com",
3334 array(
3335 'dev1@dev.com',
3336 'fred@example.com',
3337 'fred+verp@example.com',
3339 false,
3341 'alsodivertsnewline' => array(
3342 'divertallemailsto' => 'somewhere@elsewhere.com',
3343 'divertallemailsexcept' => "@dev.com\nfred(\+.*)?@example.com",
3344 array(
3345 'foo@example.com',
3346 'test@real.com',
3347 'fred.jones@example.com',
3349 true,
3351 'alsodivertsblankline' => array(
3352 'divertallemailsto' => 'somewhere@elsewhere.com',
3353 'divertallemailsexcept' => "@dev.com\n",
3355 'lionel@example.com',
3357 true,
3359 'divertsexceptionblankline' => array(
3360 'divertallemailsto' => 'somewhere@elsewhere.com',
3361 'divertallemailsexcept' => "@example.com\n",
3363 'lionel@example.com',
3365 false,
3371 * Test email diversion
3373 * @dataProvider diverted_emails_provider
3375 * @param string $divertallemailsto An optional email address
3376 * @param string $divertallemailsexcept An optional exclusion list
3377 * @param array $addresses An array of test addresses
3378 * @param boolean $expected Expected result
3380 public function test_email_should_be_diverted($divertallemailsto, $divertallemailsexcept, $addresses, $expected): void {
3381 global $CFG;
3383 $this->resetAfterTest();
3384 $CFG->divertallemailsto = $divertallemailsto;
3385 $CFG->divertallemailsexcept = $divertallemailsexcept;
3387 foreach ($addresses as $address) {
3388 $this->assertEquals($expected, email_should_be_diverted($address));
3392 public function test_email_to_user(): void {
3393 global $CFG;
3395 $this->resetAfterTest();
3397 $user1 = $this->getDataGenerator()->create_user(array('maildisplay' => 1, 'mailformat' => 0));
3398 $user2 = $this->getDataGenerator()->create_user(array('maildisplay' => 1, 'mailformat' => 1));
3399 $user3 = $this->getDataGenerator()->create_user(array('maildisplay' => 0));
3400 set_config('allowedemaildomains', "example.com\r\nmoodle.org");
3402 $subject = 'subject';
3403 $messagetext = 'message text';
3404 $subject2 = 'subject 2';
3405 $messagetext2 = '<b>message text 2</b>';
3407 // Close the default email sink.
3408 $sink = $this->redirectEmails();
3409 $sink->close();
3411 $CFG->noemailever = true;
3412 $this->assertNotEmpty($CFG->noemailever);
3413 email_to_user($user1, $user2, $subject, $messagetext);
3414 $this->assertDebuggingCalled('Not sending email due to $CFG->noemailever config setting');
3416 unset_config('noemailever');
3418 email_to_user($user1, $user2, $subject, $messagetext);
3419 $this->assertDebuggingCalled('Unit tests must not send real emails! Use $this->redirectEmails()');
3421 $sink = $this->redirectEmails();
3422 email_to_user($user1, $user2, $subject, $messagetext);
3423 email_to_user($user2, $user1, $subject2, $messagetext2);
3424 $this->assertSame(2, $sink->count());
3425 $result = $sink->get_messages();
3426 $this->assertCount(2, $result);
3427 $sink->close();
3429 $this->assertSame($subject, $result[0]->subject);
3430 $this->assertSame($messagetext, trim($result[0]->body));
3431 $this->assertSame($user1->email, $result[0]->to);
3432 $this->assertSame($user2->email, $result[0]->from);
3433 $this->assertStringContainsString('Content-Type: text/plain', $result[0]->header);
3435 $this->assertSame($subject2, $result[1]->subject);
3436 $this->assertStringContainsString($messagetext2, quoted_printable_decode($result[1]->body));
3437 $this->assertSame($user2->email, $result[1]->to);
3438 $this->assertSame($user1->email, $result[1]->from);
3439 $this->assertStringNotContainsString('Content-Type: text/plain', $result[1]->header);
3441 email_to_user($user1, $user2, $subject, $messagetext);
3442 $this->assertDebuggingCalled('Unit tests must not send real emails! Use $this->redirectEmails()');
3444 // Test that an empty noreplyaddress will default to a no-reply address.
3445 $sink = $this->redirectEmails();
3446 email_to_user($user1, $user3, $subject, $messagetext);
3447 $result = $sink->get_messages();
3448 $this->assertEquals($CFG->noreplyaddress, $result[0]->from);
3449 $sink->close();
3450 set_config('noreplyaddress', '');
3451 $sink = $this->redirectEmails();
3452 email_to_user($user1, $user3, $subject, $messagetext);
3453 $result = $sink->get_messages();
3454 $this->assertEquals('noreply@www.example.com', $result[0]->from);
3455 $sink->close();
3457 // Test $CFG->allowedemaildomains.
3458 set_config('noreplyaddress', 'noreply@www.example.com');
3459 $this->assertNotEmpty($CFG->allowedemaildomains);
3460 $sink = $this->redirectEmails();
3461 email_to_user($user1, $user2, $subject, $messagetext);
3462 unset_config('allowedemaildomains');
3463 email_to_user($user1, $user2, $subject, $messagetext);
3464 $result = $sink->get_messages();
3465 $this->assertNotEquals($CFG->noreplyaddress, $result[0]->from);
3466 $this->assertEquals($CFG->noreplyaddress, $result[1]->from);
3467 $sink->close();
3469 // Try to send an unsafe attachment, we should see an error message in the eventual mail body.
3470 $attachment = '../test.txt';
3471 $attachname = 'txt';
3473 $sink = $this->redirectEmails();
3474 email_to_user($user1, $user2, $subject, $messagetext, '', $attachment, $attachname);
3475 $this->assertSame(1, $sink->count());
3476 $result = $sink->get_messages();
3477 $this->assertCount(1, $result);
3478 $this->assertStringContainsString('error.txt', $result[0]->body);
3479 $this->assertStringContainsString('Error in attachment. User attempted to attach a filename with a unsafe name.', $result[0]->body);
3480 $sink->close();
3484 * Data provider for {@see test_email_to_user_attachment}
3486 * @return array
3488 public function email_to_user_attachment_provider(): array {
3489 global $CFG;
3491 // Return all paths that can be used to send attachments from.
3492 return [
3493 'cachedir' => [$CFG->cachedir],
3494 'dataroot' => [$CFG->dataroot],
3495 'dirroot' => [$CFG->dirroot],
3496 'localcachedir' => [$CFG->localcachedir],
3497 'tempdir' => [$CFG->tempdir],
3498 // Paths within $CFG->localrequestdir.
3499 'localrequestdir_request_directory' => [make_request_directory()],
3500 'localrequestdir_request_storage_directory' => [get_request_storage_directory()],
3501 // Pass null to indicate we want to test a path relative to $CFG->dataroot.
3502 'relative' => [null]
3507 * Test sending attachments with email_to_user
3509 * @param string|null $filedir
3511 * @dataProvider email_to_user_attachment_provider
3513 public function test_email_to_user_attachment(?string $filedir): void {
3514 global $CFG;
3516 // If $filedir is null, then write our test file to $CFG->dataroot.
3517 $filepath = ($filedir ?: $CFG->dataroot) . '/hello.txt';
3518 file_put_contents($filepath, 'Hello');
3520 $user = \core_user::get_support_user();
3521 $message = 'Test attachment path';
3523 // Create sink to catch all sent e-mails.
3524 $sink = $this->redirectEmails();
3526 // Attachment path will be that of the test file if $filedir was passed, otherwise the relative path from $CFG->dataroot.
3527 $filename = basename($filepath);
3528 $attachmentpath = $filedir ? $filepath : $filename;
3529 email_to_user($user, $user, $message, $message, $message, $attachmentpath, $filename);
3531 $messages = $sink->get_messages();
3532 $sink->close();
3534 $this->assertCount(1, $messages);
3536 // Verify attachment in message body (attachment is in MIME format, but we can detect some Content fields).
3537 $messagebody = reset($messages)->body;
3538 $this->assertStringContainsString('Content-Type: text/plain; name=' . $filename, $messagebody);
3539 $this->assertStringContainsString('Content-Disposition: attachment; filename=' . $filename, $messagebody);
3541 // Cleanup.
3542 unlink($filepath);
3546 * Test sending an attachment that doesn't exist to email_to_user
3548 public function test_email_to_user_attachment_missing(): void {
3549 $user = \core_user::get_support_user();
3550 $message = 'Test attachment path';
3552 // Create sink to catch all sent e-mails.
3553 $sink = $this->redirectEmails();
3555 $attachmentpath = '/hola/hello.txt';
3556 $filename = basename($attachmentpath);
3557 email_to_user($user, $user, $message, $message, $message, $attachmentpath, $filename);
3559 $messages = $sink->get_messages();
3560 $sink->close();
3562 $this->assertCount(1, $messages);
3564 // Verify attachment not in message body (attachment is in MIME format, but we can detect some Content fields).
3565 $messagebody = reset($messages)->body;
3566 $this->assertStringNotContainsString('Content-Type: text/plain; name="' . $filename . '"', $messagebody);
3567 $this->assertStringNotContainsString('Content-Disposition: attachment; filename=' . $filename, $messagebody);
3571 * Test setnew_password_and_mail.
3573 public function test_setnew_password_and_mail(): void {
3574 global $DB, $CFG;
3576 $this->resetAfterTest();
3578 $user = $this->getDataGenerator()->create_user();
3580 // Update user password.
3581 $sink = $this->redirectEvents();
3582 $sink2 = $this->redirectEmails(); // Make sure we are redirecting emails.
3583 setnew_password_and_mail($user);
3584 $events = $sink->get_events();
3585 $sink->close();
3586 $sink2->close();
3587 $event = array_pop($events);
3589 // Test updated value.
3590 $dbuser = $DB->get_record('user', array('id' => $user->id));
3591 $this->assertSame($user->firstname, $dbuser->firstname);
3592 $this->assertNotEmpty($dbuser->password);
3594 // Test event.
3595 $this->assertInstanceOf('\core\event\user_password_updated', $event);
3596 $this->assertSame($user->id, $event->relateduserid);
3597 $this->assertEquals(\context_user::instance($user->id), $event->get_context());
3598 $this->assertEventContextNotUsed($event);
3602 * Data provider for test_generate_confirmation_link
3603 * @return array Confirmation urls and expected resultant confirmation links
3605 public static function generate_confirmation_link_provider(): array {
3606 global $CFG;
3607 return [
3608 "Simple name" => [
3609 "username" => "simplename",
3610 "confirmationurl" => null,
3611 "expected" => $CFG->wwwroot . "/login/confirm.php?data=/simplename"
3613 "Period in between words in username" => [
3614 "username" => "period.inbetween",
3615 "confirmationurl" => null,
3616 "expected" => $CFG->wwwroot . "/login/confirm.php?data=/period%2Einbetween"
3618 "Trailing periods in username" => [
3619 "username" => "trailingperiods...",
3620 "confirmationurl" => null,
3621 "expected" => $CFG->wwwroot . "/login/confirm.php?data=/trailingperiods%2E%2E%2E"
3623 "At symbol in username" => [
3624 "username" => "at@symbol",
3625 "confirmationurl" => null,
3626 "expected" => $CFG->wwwroot . "/login/confirm.php?data=/at%40symbol"
3628 "Dash symbol in username" => [
3629 "username" => "has-dash",
3630 "confirmationurl" => null,
3631 "expected" => $CFG->wwwroot . "/login/confirm.php?data=/has-dash"
3633 "Underscore in username" => [
3634 "username" => "under_score",
3635 "confirmationurl" => null,
3636 "expected" => $CFG->wwwroot . "/login/confirm.php?data=/under_score"
3638 "Many different characters in username" => [
3639 "username" => "many_-.@characters@_@-..-..",
3640 "confirmationurl" => null,
3641 "expected" => $CFG->wwwroot . "/login/confirm.php?data=/many_-%2E%40characters%40_%40-%2E%2E-%2E%2E"
3643 "Custom relative confirmation url" => [
3644 "username" => "many_-.@characters@_@-..-..",
3645 "confirmationurl" => "/custom/local/url.php",
3646 "expected" => $CFG->wwwroot . "/custom/local/url.php?data=/many_-%2E%40characters%40_%40-%2E%2E-%2E%2E"
3648 "Custom relative confirmation url with parameters" => [
3649 "username" => "many_-.@characters@_@-..-..",
3650 "confirmationurl" => "/custom/local/url.php?with=param",
3651 "expected" => $CFG->wwwroot . "/custom/local/url.php?with=param&data=/many_-%2E%40characters%40_%40-%2E%2E-%2E%2E"
3653 "Custom local confirmation url" => [
3654 "username" => "many_-.@characters@_@-..-..",
3655 "confirmationurl" => $CFG->wwwroot . "/custom/local/url.php",
3656 "expected" => $CFG->wwwroot . "/custom/local/url.php?data=/many_-%2E%40characters%40_%40-%2E%2E-%2E%2E"
3658 "Custom local confirmation url with parameters" => [
3659 "username" => "many_-.@characters@_@-..-..",
3660 "confirmationurl" => $CFG->wwwroot . "/custom/local/url.php?with=param",
3661 "expected" => $CFG->wwwroot . "/custom/local/url.php?with=param&data=/many_-%2E%40characters%40_%40-%2E%2E-%2E%2E"
3663 "Custom external confirmation url" => [
3664 "username" => "many_-.@characters@_@-..-..",
3665 "confirmationurl" => "http://moodle.org/custom/external/url.php",
3666 "expected" => "http://moodle.org/custom/external/url.php?data=/many_-%2E%40characters%40_%40-%2E%2E-%2E%2E"
3668 "Custom external confirmation url with parameters" => [
3669 "username" => "many_-.@characters@_@-..-..",
3670 "confirmationurl" => "http://moodle.org/ext.php?with=some&param=eters",
3671 "expected" => "http://moodle.org/ext.php?with=some&param=eters&data=/many_-%2E%40characters%40_%40-%2E%2E-%2E%2E"
3673 "Custom external confirmation url with parameters (again)" => [
3674 "username" => "many_-.@characters@_@-..-..",
3675 "confirmationurl" => "http://moodle.org/ext.php?with=some&data=test",
3676 "expected" => "http://moodle.org/ext.php?with=some&data=/many_-%2E%40characters%40_%40-%2E%2E-%2E%2E"
3682 * Test generate_confirmation_link
3683 * @dataProvider generate_confirmation_link_provider
3684 * @param string $username The name of the user
3685 * @param string $confirmationurl The url the user should go to to confirm
3686 * @param string $expected The expected url of the confirmation link
3688 public function test_generate_confirmation_link($username, $confirmationurl, $expected): void {
3689 $this->resetAfterTest();
3690 $sink = $this->redirectEmails();
3692 $user = $this->getDataGenerator()->create_user(
3694 "username" => $username,
3695 "confirmed" => 0,
3696 "email" => 'test@example.com',
3700 send_confirmation_email($user, $confirmationurl);
3701 $sink->close();
3702 $messages = $sink->get_messages();
3703 $message = array_shift($messages);
3704 $messagebody = quoted_printable_decode($message->body);
3706 $this->assertStringContainsString($expected, $messagebody);
3710 * Test generate_confirmation_link with custom admin link
3712 public function test_generate_confirmation_link_with_custom_admin(): void {
3713 global $CFG;
3715 $this->resetAfterTest();
3716 $sink = $this->redirectEmails();
3718 $admin = $CFG->admin;
3719 $CFG->admin = 'custom/admin/path';
3721 $user = $this->getDataGenerator()->create_user(
3723 "username" => "many_-.@characters@_@-..-..",
3724 "confirmed" => 0,
3725 "email" => 'test@example.com',
3728 $confirmationurl = "/admin/test.php?with=params";
3729 $expected = $CFG->wwwroot . "/" . $CFG->admin . "/test.php?with=params&data=/many_-%2E%40characters%40_%40-%2E%2E-%2E%2E";
3731 send_confirmation_email($user, $confirmationurl);
3732 $sink->close();
3733 $messages = $sink->get_messages();
3734 $message = array_shift($messages);
3735 $messagebody = quoted_printable_decode($message->body);
3737 $sink->close();
3738 $this->assertStringContainsString($expected, $messagebody);
3740 $CFG->admin = $admin;
3745 * Test remove_course_content deletes course contents
3746 * TODO Add asserts to verify other data related to course is deleted as well.
3748 public function test_remove_course_contents(): void {
3750 $this->resetAfterTest();
3752 $course = $this->getDataGenerator()->create_course();
3753 $user = $this->getDataGenerator()->create_user();
3754 $gen = $this->getDataGenerator()->get_plugin_generator('core_notes');
3755 $note = $gen->create_instance(array('courseid' => $course->id, 'userid' => $user->id));
3757 $this->assertNotEquals(false, note_load($note->id));
3758 remove_course_contents($course->id, false);
3759 $this->assertFalse(note_load($note->id));
3763 * Test function username_load_fields_from_object().
3765 public function test_username_load_fields_from_object(): void {
3766 $this->resetAfterTest();
3768 // This object represents the information returned from an sql query.
3769 $userinfo = new \stdClass();
3770 $userinfo->userid = 1;
3771 $userinfo->username = 'loosebruce';
3772 $userinfo->firstname = 'Bruce';
3773 $userinfo->lastname = 'Campbell';
3774 $userinfo->firstnamephonetic = 'ブルース';
3775 $userinfo->lastnamephonetic = 'カンベッル';
3776 $userinfo->middlename = '';
3777 $userinfo->alternatename = '';
3778 $userinfo->email = '';
3779 $userinfo->picture = 23;
3780 $userinfo->imagealt = 'Michael Jordan draining another basket.';
3781 $userinfo->idnumber = 3982;
3783 // Just user name fields.
3784 $user = new \stdClass();
3785 $user = username_load_fields_from_object($user, $userinfo);
3786 $expectedarray = new \stdClass();
3787 $expectedarray->firstname = 'Bruce';
3788 $expectedarray->lastname = 'Campbell';
3789 $expectedarray->firstnamephonetic = 'ブルース';
3790 $expectedarray->lastnamephonetic = 'カンベッル';
3791 $expectedarray->middlename = '';
3792 $expectedarray->alternatename = '';
3793 $this->assertEquals($user, $expectedarray);
3795 // User information for showing a picture.
3796 $user = new \stdClass();
3797 $additionalfields = explode(',', implode(',', \core_user\fields::get_picture_fields()));
3798 $user = username_load_fields_from_object($user, $userinfo, null, $additionalfields);
3799 $user->id = $userinfo->userid;
3800 $expectedarray = new \stdClass();
3801 $expectedarray->id = 1;
3802 $expectedarray->firstname = 'Bruce';
3803 $expectedarray->lastname = 'Campbell';
3804 $expectedarray->firstnamephonetic = 'ブルース';
3805 $expectedarray->lastnamephonetic = 'カンベッル';
3806 $expectedarray->middlename = '';
3807 $expectedarray->alternatename = '';
3808 $expectedarray->email = '';
3809 $expectedarray->picture = 23;
3810 $expectedarray->imagealt = 'Michael Jordan draining another basket.';
3811 $this->assertEquals($user, $expectedarray);
3813 // Alter the userinfo object to have a prefix.
3814 $userinfo->authorfirstname = 'Bruce';
3815 $userinfo->authorlastname = 'Campbell';
3816 $userinfo->authorfirstnamephonetic = 'ブルース';
3817 $userinfo->authorlastnamephonetic = 'カンベッル';
3818 $userinfo->authormiddlename = '';
3819 $userinfo->authorpicture = 23;
3820 $userinfo->authorimagealt = 'Michael Jordan draining another basket.';
3821 $userinfo->authoremail = 'test@example.com';
3824 // Return an object with user picture information.
3825 $user = new \stdClass();
3826 $additionalfields = explode(',', implode(',', \core_user\fields::get_picture_fields()));
3827 $user = username_load_fields_from_object($user, $userinfo, 'author', $additionalfields);
3828 $user->id = $userinfo->userid;
3829 $expectedarray = new \stdClass();
3830 $expectedarray->id = 1;
3831 $expectedarray->firstname = 'Bruce';
3832 $expectedarray->lastname = 'Campbell';
3833 $expectedarray->firstnamephonetic = 'ブルース';
3834 $expectedarray->lastnamephonetic = 'カンベッル';
3835 $expectedarray->middlename = '';
3836 $expectedarray->alternatename = '';
3837 $expectedarray->email = 'test@example.com';
3838 $expectedarray->picture = 23;
3839 $expectedarray->imagealt = 'Michael Jordan draining another basket.';
3840 $this->assertEquals($user, $expectedarray);
3844 * Test function {@see count_words()}.
3846 * @dataProvider count_words_testcases
3847 * @param int $expectedcount number of words in $string.
3848 * @param string $string the test string to count the words of.
3849 * @param int|null $format
3851 public function test_count_words(int $expectedcount, string $string, $format = null): void {
3852 $this->assertEquals($expectedcount, count_words($string, $format),
3853 "'$string' with format '$format' does not match count $expectedcount");
3857 * Data provider for {@see test_count_words}.
3859 * @return array of test cases.
3861 public function count_words_testcases(): array {
3862 // Copy-pasting example from MDL-64240.
3863 $copypasted = <<<EOT
3864 <p onclick="alert('boop');">Snoot is booped</p>
3865 <script>alert('Boop the snoot');</script>
3866 <img alt="Boop the Snoot." src="https://proxy.duckduckgo.com/iu/?u=http%3A%2F%2Fwww.geekfill.com%2Fwp-content%2Fuploads%2F2015%2F08%2FBoop-the-Snoot.jpg&f=1">
3867 EOT;
3869 // The counts here should match MS Word and Libre Office.
3870 return [
3871 [0, ''],
3872 [4, 'one two three four'],
3873 [1, "a'b"],
3874 [1, '1+1=2'],
3875 [1, ' one-sided '],
3876 [2, 'one&nbsp;two'],
3877 [1, 'email@example.com'],
3878 [2, 'first\part second/part'],
3879 [4, '<p>one two<br></br>three four</p>'],
3880 [4, '<p>one two<br>three four</p>'],
3881 [4, '<p>one two<br />three four</p>'], // XHTML style.
3882 [3, ' one ... three '],
3883 [1, 'just...one'],
3884 [3, ' one & three '],
3885 [1, 'just&one'],
3886 [2, 'em—dash'],
3887 [2, 'en–dash'],
3888 [4, '1³ £2 €3.45 $6,789'],
3889 [2, 'ブルース カンベッル'], // MS word counts this as 11, but we don't handle that yet.
3890 [4, '<p>one two</p><p>three four</p>'],
3891 [4, '<p>one two</p><p><br/></p><p>three four</p>'],
3892 [4, '<p>one</p><ul><li>two</li><li>three</li></ul><p>four.</p>'],
3893 [1, '<p>em<b>phas</b>is.</p>'],
3894 [1, '<p>em<i>phas</i>is.</p>'],
3895 [1, '<p>em<strong>phas</strong>is.</p>'],
3896 [1, '<p>em<em>phas</em>is.</p>'],
3897 [2, "one\ntwo"],
3898 [2, "one\rtwo"],
3899 [2, "one\ttwo"],
3900 [2, "one\vtwo"],
3901 [2, "one\ftwo"],
3902 [1, "SO<sub>4</sub><sup>2-</sup>"],
3903 [6, '4+4=8 i.e. O(1) a,b,c,d I’m black&blue_really'],
3904 [1, '<span>a</span><span>b</span>'],
3905 [1, '<span>a</span><span>b</span>', FORMAT_PLAIN],
3906 [1, '<span>a</span><span>b</span>', FORMAT_HTML],
3907 [1, '<span>a</span><span>b</span>', FORMAT_MOODLE],
3908 [1, '<span>a</span><span>b</span>', FORMAT_MARKDOWN],
3909 [1, 'aa <argh <bleh>pokus</bleh>'],
3910 [2, 'aa <argh <bleh>pokus</bleh>', FORMAT_HTML],
3911 [6, $copypasted],
3912 [6, $copypasted, FORMAT_PLAIN],
3913 [3, $copypasted, FORMAT_HTML],
3914 [3, $copypasted, FORMAT_MOODLE],
3919 * Test function {@see count_letters()}.
3921 * @dataProvider count_letters_testcases
3922 * @param int $expectedcount number of characters in $string.
3923 * @param string $string the test string to count the letters of.
3924 * @param int|null $format
3926 public function test_count_letters(int $expectedcount, string $string, $format = null): void {
3927 $this->assertEquals($expectedcount, count_letters($string, $format),
3928 "'$string' with format '$format' does not match count $expectedcount");
3932 * Data provider for {@see count_letters_testcases}.
3934 * @return array of test cases.
3936 public function count_letters_testcases(): array {
3937 return [
3938 [0, ''],
3939 [1, 'x'],
3940 [1, '&amp;'],
3941 [4, '<p>frog</p>'],
3942 [4, '<p>frog</p>', FORMAT_PLAIN],
3943 [4, '<p>frog</p>', FORMAT_MOODLE],
3944 [4, '<p>frog</p>', FORMAT_HTML],
3945 [4, '<p>frog</p>', FORMAT_MARKDOWN],
3946 [2, 'aa <argh <bleh>pokus</bleh>'],
3947 [7, 'aa <argh <bleh>pokus</bleh>', FORMAT_HTML],
3952 * Tests the getremoteaddr() function.
3954 public function test_getremoteaddr(): void {
3955 global $CFG;
3957 $this->resetAfterTest();
3959 $CFG->getremoteaddrconf = null; // Use default value, GETREMOTEADDR_SKIP_DEFAULT.
3960 $noip = getremoteaddr('1.1.1.1');
3961 $this->assertEquals('1.1.1.1', $noip);
3963 $remoteaddr = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null;
3964 $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
3965 $singleip = getremoteaddr();
3966 $this->assertEquals('127.0.0.1', $singleip);
3968 $_SERVER['REMOTE_ADDR'] = $remoteaddr; // Restore server value.
3970 $CFG->getremoteaddrconf = 0; // Don't skip any source.
3971 $noip = getremoteaddr('1.1.1.1');
3972 $this->assertEquals('1.1.1.1', $noip);
3974 // Populate all $_SERVER values to review order.
3975 $ipsources = [
3976 'HTTP_CLIENT_IP' => '2.2.2.2',
3977 'HTTP_X_FORWARDED_FOR' => '3.3.3.3',
3978 'REMOTE_ADDR' => '4.4.4.4',
3980 $originalvalues = [];
3981 foreach ($ipsources as $source => $ip) {
3982 $originalvalues[$source] = isset($_SERVER[$source]) ? $_SERVER[$source] : null; // Saving data to restore later.
3983 $_SERVER[$source] = $ip;
3986 foreach ($ipsources as $source => $expectedip) {
3987 $ip = getremoteaddr();
3988 $this->assertEquals($expectedip, $ip);
3989 unset($_SERVER[$source]); // Removing the value so next time we get the following ip.
3992 // Restore server values.
3993 foreach ($originalvalues as $source => $ip) {
3994 $_SERVER[$source] = $ip;
3997 // All $_SERVER values have been removed, we should get the default again.
3998 $noip = getremoteaddr('1.1.1.1');
3999 $this->assertEquals('1.1.1.1', $noip);
4001 $CFG->getremoteaddrconf = GETREMOTEADDR_SKIP_HTTP_CLIENT_IP;
4002 $xforwardedfor = isset($_SERVER['HTTP_X_FORWARDED_FOR']) ? $_SERVER['HTTP_X_FORWARDED_FOR'] : null;
4004 $_SERVER['HTTP_X_FORWARDED_FOR'] = '';
4005 $noip = getremoteaddr('1.1.1.1');
4006 $this->assertEquals('1.1.1.1', $noip);
4008 $_SERVER['HTTP_X_FORWARDED_FOR'] = '';
4009 $noip = getremoteaddr();
4010 $this->assertEquals('0.0.0.0', $noip);
4012 $_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1';
4013 $singleip = getremoteaddr();
4014 $this->assertEquals('127.0.0.1', $singleip);
4016 $_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1,127.0.0.2';
4017 $twoip = getremoteaddr();
4018 $this->assertEquals('127.0.0.2', $twoip);
4020 $_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1,127.0.0.2,127.0.0.3';
4021 $threeip = getremoteaddr();
4022 $this->assertEquals('127.0.0.3', $threeip);
4024 $_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1,127.0.0.2:65535';
4025 $portip = getremoteaddr();
4026 $this->assertEquals('127.0.0.2', $portip);
4028 $_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1,0:0:0:0:0:0:0:2';
4029 $portip = getremoteaddr();
4030 $this->assertEquals('0:0:0:0:0:0:0:2', $portip);
4032 $_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1,0::2';
4033 $portip = getremoteaddr();
4034 $this->assertEquals('0:0:0:0:0:0:0:2', $portip);
4036 $_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1,[0:0:0:0:0:0:0:2]:65535';
4037 $portip = getremoteaddr();
4038 $this->assertEquals('0:0:0:0:0:0:0:2', $portip);
4040 $_SERVER['HTTP_X_FORWARDED_FOR'] = $xforwardedfor;
4045 * Test function for creation of random strings.
4047 public function test_random_string(): void {
4048 $pool = 'a-zA-Z0-9';
4050 $result = random_string(10);
4051 $this->assertSame(10, strlen($result));
4052 $this->assertMatchesRegularExpression('/^[' . $pool . ']+$/', $result);
4053 $this->assertNotSame($result, random_string(10));
4055 $result = random_string(21);
4056 $this->assertSame(21, strlen($result));
4057 $this->assertMatchesRegularExpression('/^[' . $pool . ']+$/', $result);
4058 $this->assertNotSame($result, random_string(21));
4060 $result = random_string(666);
4061 $this->assertSame(666, strlen($result));
4062 $this->assertMatchesRegularExpression('/^[' . $pool . ']+$/', $result);
4064 $result = random_string();
4065 $this->assertSame(15, strlen($result));
4066 $this->assertMatchesRegularExpression('/^[' . $pool . ']+$/', $result);
4068 $this->assertDebuggingNotCalled();
4072 * Test function for creation of complex random strings.
4074 public function test_complex_random_string(): void {
4075 $pool = preg_quote('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789`~!@#%^&*()_+-=[];,./<>?:{} ', '/');
4077 $result = complex_random_string(10);
4078 $this->assertSame(10, strlen($result));
4079 $this->assertMatchesRegularExpression('/^[' . $pool . ']+$/', $result);
4080 $this->assertNotSame($result, complex_random_string(10));
4082 $result = complex_random_string(21);
4083 $this->assertSame(21, strlen($result));
4084 $this->assertMatchesRegularExpression('/^[' . $pool . ']+$/', $result);
4085 $this->assertNotSame($result, complex_random_string(21));
4087 $result = complex_random_string(666);
4088 $this->assertSame(666, strlen($result));
4089 $this->assertMatchesRegularExpression('/^[' . $pool . ']+$/', $result);
4091 $result = complex_random_string();
4092 $this->assertEqualsWithDelta(28, strlen($result), 4); // Expected length is 24 - 32.
4093 $this->assertMatchesRegularExpression('/^[' . $pool . ']+$/', $result);
4095 $this->assertDebuggingNotCalled();
4099 * Data provider for private ips.
4101 public function data_private_ips() {
4102 return array(
4103 array('10.0.0.0'),
4104 array('172.16.0.0'),
4105 array('192.168.1.0'),
4106 array('fdfe:dcba:9876:ffff:fdc6:c46b:bb8f:7d4c'),
4107 array('fdc6:c46b:bb8f:7d4c:fdc6:c46b:bb8f:7d4c'),
4108 array('fdc6:c46b:bb8f:7d4c:0000:8a2e:0370:7334'),
4109 array('127.0.0.1'), // This has been buggy in past: https://bugs.php.net/bug.php?id=53150.
4114 * Checks ip_is_public returns false for private ips.
4116 * @param string $ip the ipaddress to test
4117 * @dataProvider data_private_ips
4119 public function test_ip_is_public_private_ips($ip): void {
4120 $this->assertFalse(ip_is_public($ip));
4124 * Data provider for public ips.
4126 public function data_public_ips() {
4127 return array(
4128 array('2400:cb00:2048:1::8d65:71b3'),
4129 array('2400:6180:0:d0::1b:2001'),
4130 array('141.101.113.179'),
4131 array('123.45.67.178'),
4136 * Checks ip_is_public returns true for public ips.
4138 * @param string $ip the ipaddress to test
4139 * @dataProvider data_public_ips
4141 public function test_ip_is_public_public_ips($ip): void {
4142 $this->assertTrue(ip_is_public($ip));
4146 * Test the function can_send_from_real_email_address
4148 * @param string $email Email address for the from user.
4149 * @param int $display The user's email display preference.
4150 * @param bool $samecourse Are the users in the same course?
4151 * @param string $config The CFG->allowedemaildomains config values
4152 * @param bool $result The expected result.
4153 * @dataProvider data_can_send_from_real_email_address
4155 public function test_can_send_from_real_email_address($email, $display, $samecourse, $config, $result): void {
4156 $this->resetAfterTest();
4158 $fromuser = $this->getDataGenerator()->create_user();
4159 $touser = $this->getDataGenerator()->create_user();
4160 $course = $this->getDataGenerator()->create_course();
4161 set_config('allowedemaildomains', $config);
4163 $fromuser->email = $email;
4164 $fromuser->maildisplay = $display;
4165 if ($samecourse) {
4166 $this->getDataGenerator()->enrol_user($fromuser->id, $course->id, 'student');
4167 $this->getDataGenerator()->enrol_user($touser->id, $course->id, 'student');
4168 } else {
4169 $this->getDataGenerator()->enrol_user($fromuser->id, $course->id, 'student');
4171 $this->assertEquals($result, can_send_from_real_email_address($fromuser, $touser));
4175 * Data provider for test_can_send_from_real_email_address.
4177 * @return array Returns an array of test data for the above function.
4179 public function data_can_send_from_real_email_address() {
4180 return [
4181 // Test from email is in allowed domain.
4182 // Test that from display is set to show no one.
4184 'email' => 'fromuser@example.com',
4185 'display' => \core_user::MAILDISPLAY_HIDE,
4186 'samecourse' => false,
4187 'config' => "example.com\r\ntest.com",
4188 'result' => false
4190 // Test that from display is set to course members only (course member).
4192 'email' => 'fromuser@example.com',
4193 'display' => \core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY,
4194 'samecourse' => true,
4195 'config' => "example.com\r\ntest.com",
4196 'result' => true
4198 // Test that from display is set to course members only (Non course member).
4200 'email' => 'fromuser@example.com',
4201 'display' => \core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY,
4202 'samecourse' => false,
4203 'config' => "example.com\r\ntest.com",
4204 'result' => false
4206 // Test that from display is set to show everyone.
4208 'email' => 'fromuser@example.com',
4209 'display' => \core_user::MAILDISPLAY_EVERYONE,
4210 'samecourse' => false,
4211 'config' => "example.com\r\ntest.com",
4212 'result' => true
4214 // Test a few different config value formats for parsing correctness.
4216 'email' => 'fromuser@example.com',
4217 'display' => \core_user::MAILDISPLAY_EVERYONE,
4218 'samecourse' => false,
4219 'config' => "\n test.com\nexample.com \n",
4220 'result' => true
4223 'email' => 'fromuser@example.com',
4224 'display' => \core_user::MAILDISPLAY_EVERYONE,
4225 'samecourse' => false,
4226 'config' => "\r\n example.com \r\n test.com \r\n",
4227 'result' => true
4230 'email' => 'fromuser@EXAMPLE.com',
4231 'display' => \core_user::MAILDISPLAY_EVERYONE,
4232 'samecourse' => false,
4233 'config' => "example.com\r\ntest.com",
4234 'result' => true,
4236 // Test from email is not in allowed domain.
4237 // Test that from display is set to show no one.
4238 [ 'email' => 'fromuser@moodle.com',
4239 'display' => \core_user::MAILDISPLAY_HIDE,
4240 'samecourse' => false,
4241 'config' => "example.com\r\ntest.com",
4242 'result' => false
4244 // Test that from display is set to course members only (course member).
4245 [ 'email' => 'fromuser@moodle.com',
4246 'display' => \core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY,
4247 'samecourse' => true,
4248 'config' => "example.com\r\ntest.com",
4249 'result' => false
4251 // Test that from display is set to course members only (Non course member.
4252 [ 'email' => 'fromuser@moodle.com',
4253 'display' => \core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY,
4254 'samecourse' => false,
4255 'config' => "example.com\r\ntest.com",
4256 'result' => false
4258 // Test that from display is set to show everyone.
4259 [ 'email' => 'fromuser@moodle.com',
4260 'display' => \core_user::MAILDISPLAY_EVERYONE,
4261 'samecourse' => false,
4262 'config' => "example.com\r\ntest.com",
4263 'result' => false
4265 // Test a few erroneous config value and confirm failure.
4266 [ 'email' => 'fromuser@moodle.com',
4267 'display' => \core_user::MAILDISPLAY_EVERYONE,
4268 'samecourse' => false,
4269 'config' => "\r\n \r\n",
4270 'result' => false
4272 [ 'email' => 'fromuser@moodle.com',
4273 'display' => \core_user::MAILDISPLAY_EVERYONE,
4274 'samecourse' => false,
4275 'config' => " \n \n \n ",
4276 'result' => false
4282 * Test that generate_email_processing_address() returns valid email address.
4284 public function test_generate_email_processing_address(): void {
4285 global $CFG;
4286 $this->resetAfterTest();
4288 $data = (object)[
4289 'id' => 42,
4290 'email' => 'my.email+from_moodle@example.com',
4293 $modargs = 'B'.base64_encode(pack('V', $data->id)).substr(md5($data->email), 0, 16);
4295 $CFG->maildomain = 'example.com';
4296 $CFG->mailprefix = 'mdl+';
4297 $this->assertTrue(validate_email(generate_email_processing_address(0, $modargs)));
4299 $CFG->maildomain = 'mail.example.com';
4300 $CFG->mailprefix = 'mdl-';
4301 $this->assertTrue(validate_email(generate_email_processing_address(23, $modargs)));
4305 * Test allowemailaddresses setting.
4307 * @param string $email Email address for the from user.
4308 * @param string $config The CFG->allowemailaddresses config values
4309 * @param false/string $result The expected result.
4311 * @dataProvider data_email_is_not_allowed_for_allowemailaddresses
4313 public function test_email_is_not_allowed_for_allowemailaddresses($email, $config, $result): void {
4314 $this->resetAfterTest();
4316 set_config('allowemailaddresses', $config);
4317 $this->assertEquals($result, email_is_not_allowed($email));
4321 * Data provider for data_email_is_not_allowed_for_allowemailaddresses.
4323 * @return array Returns an array of test data for the above function.
4325 public function data_email_is_not_allowed_for_allowemailaddresses() {
4326 return [
4327 // Test allowed domain empty list.
4329 'email' => 'fromuser@example.com',
4330 'config' => '',
4331 'result' => false
4333 // Test from email is in allowed domain.
4335 'email' => 'fromuser@example.com',
4336 'config' => 'example.com test.com',
4337 'result' => false
4339 // Test from email is in allowed domain but uppercase config.
4341 'email' => 'fromuser@example.com',
4342 'config' => 'EXAMPLE.com test.com',
4343 'result' => false
4345 // Test from email is in allowed domain but uppercase email.
4347 'email' => 'fromuser@EXAMPLE.com',
4348 'config' => 'example.com test.com',
4349 'result' => false
4351 // Test from email is in allowed subdomain.
4353 'email' => 'fromuser@something.example.com',
4354 'config' => '.example.com test.com',
4355 'result' => false
4357 // Test from email is in allowed subdomain but uppercase config.
4359 'email' => 'fromuser@something.example.com',
4360 'config' => '.EXAMPLE.com test.com',
4361 'result' => false
4363 // Test from email is in allowed subdomain but uppercase email.
4365 'email' => 'fromuser@something.EXAMPLE.com',
4366 'config' => '.example.com test.com',
4367 'result' => false
4369 // Test from email is not in allowed domain.
4370 [ 'email' => 'fromuser@moodle.com',
4371 'config' => 'example.com test.com',
4372 'result' => get_string('emailonlyallowed', '', 'example.com test.com')
4374 // Test from email is not in allowed subdomain.
4375 [ 'email' => 'fromuser@something.example.com',
4376 'config' => 'example.com test.com',
4377 'result' => get_string('emailonlyallowed', '', 'example.com test.com')
4383 * Test denyemailaddresses setting.
4385 * @param string $email Email address for the from user.
4386 * @param string $config The CFG->denyemailaddresses config values
4387 * @param false/string $result The expected result.
4389 * @dataProvider data_email_is_not_allowed_for_denyemailaddresses
4391 public function test_email_is_not_allowed_for_denyemailaddresses($email, $config, $result): void {
4392 $this->resetAfterTest();
4394 set_config('denyemailaddresses', $config);
4395 $this->assertEquals($result, email_is_not_allowed($email));
4400 * Data provider for test_email_is_not_allowed_for_denyemailaddresses.
4402 * @return array Returns an array of test data for the above function.
4404 public function data_email_is_not_allowed_for_denyemailaddresses() {
4405 return [
4406 // Test denied domain empty list.
4408 'email' => 'fromuser@example.com',
4409 'config' => '',
4410 'result' => false
4412 // Test from email is in denied domain.
4414 'email' => 'fromuser@example.com',
4415 'config' => 'example.com test.com',
4416 'result' => get_string('emailnotallowed', '', 'example.com test.com')
4418 // Test from email is in denied domain but uppercase config.
4420 'email' => 'fromuser@example.com',
4421 'config' => 'EXAMPLE.com test.com',
4422 'result' => get_string('emailnotallowed', '', 'EXAMPLE.com test.com')
4424 // Test from email is in denied domain but uppercase email.
4426 'email' => 'fromuser@EXAMPLE.com',
4427 'config' => 'example.com test.com',
4428 'result' => get_string('emailnotallowed', '', 'example.com test.com')
4430 // Test from email is in denied subdomain.
4432 'email' => 'fromuser@something.example.com',
4433 'config' => '.example.com test.com',
4434 'result' => get_string('emailnotallowed', '', '.example.com test.com')
4436 // Test from email is in denied subdomain but uppercase config.
4438 'email' => 'fromuser@something.example.com',
4439 'config' => '.EXAMPLE.com test.com',
4440 'result' => get_string('emailnotallowed', '', '.EXAMPLE.com test.com')
4442 // Test from email is in denied subdomain but uppercase email.
4444 'email' => 'fromuser@something.EXAMPLE.com',
4445 'config' => '.example.com test.com',
4446 'result' => get_string('emailnotallowed', '', '.example.com test.com')
4448 // Test from email is not in denied domain.
4449 [ 'email' => 'fromuser@moodle.com',
4450 'config' => 'example.com test.com',
4451 'result' => false
4453 // Test from email is not in denied subdomain.
4454 [ 'email' => 'fromuser@something.example.com',
4455 'config' => 'example.com test.com',
4456 'result' => false
4462 * Test safe method unserialize_array().
4464 public function test_unserialize_array(): void {
4465 $a = [1, 2, 3];
4466 $this->assertEquals($a, unserialize_array(serialize($a)));
4467 $a = ['a' => 1, 2 => 2, 'b' => 'cde'];
4468 $this->assertEquals($a, unserialize_array(serialize($a)));
4469 $a = ['a' => 1, 2 => 2, 'b' => 'c"d"e'];
4470 $this->assertEquals($a, unserialize_array(serialize($a)));
4471 $a = ['a' => 1, 2 => ['c' => 'd', 'e' => 'f'], 'b' => 'cde'];
4472 $this->assertEquals($a, unserialize_array(serialize($a)));
4473 $a = ['a' => 1, 2 => ['c' => 'd', 'e' => ['f' => 'g']], 'b' => 'cde'];
4474 $this->assertEquals($a, unserialize_array(serialize($a)));
4475 $a = ['a' => 1, 2 => 2, 'b' => 'c"d";e'];
4476 $this->assertEquals($a, unserialize_array(serialize($a)));
4478 // Can not unserialize if there are any objects.
4479 $a = (object)['a' => 1, 2 => 2, 'b' => 'cde'];
4480 $this->assertFalse(unserialize_array(serialize($a)));
4481 $a = ['a' => 1, 2 => 2, 'b' => (object)['a' => 'cde']];
4482 $this->assertFalse(unserialize_array(serialize($a)));
4483 $a = ['a' => 1, 2 => 2, 'b' => ['c' => (object)['a' => 'cde']]];
4484 $this->assertFalse(unserialize_array(serialize($a)));
4485 $a = ['a' => 1, 2 => 2, 'b' => ['c' => new lang_string('no')]];
4486 $this->assertFalse(unserialize_array(serialize($a)));
4488 // Array used in the grader report.
4489 $a = array('aggregatesonly' => [51, 34], 'gradesonly' => [21, 45, 78]);
4490 $this->assertEquals($a, unserialize_array(serialize($a)));
4494 * Test method for safely unserializing a serialized object of type stdClass
4496 public function test_unserialize_object(): void {
4497 $object = (object) [
4498 'foo' => 42,
4499 'bar' => 'Hamster',
4500 'innerobject' => (object) [
4501 'baz' => 'happy',
4505 // We should get back the same object we serialized.
4506 $serializedobject = serialize($object);
4507 $this->assertEquals($object, unserialize_object($serializedobject));
4509 // Try serializing a different class, not allowed.
4510 $langstr = new lang_string('no');
4511 $serializedlangstr = serialize($langstr);
4512 $unserializedlangstr = unserialize_object($serializedlangstr);
4513 $this->assertInstanceOf(\stdClass::class, $unserializedlangstr);
4517 * Test that the component_class_callback returns the correct default value when the class was not found.
4519 * @dataProvider component_class_callback_default_provider
4520 * @param $default
4522 public function test_component_class_callback_not_found($default): void {
4523 $this->assertSame($default, component_class_callback('thisIsNotTheClassYouWereLookingFor', 'anymethod', [], $default));
4527 * Test that the component_class_callback returns the correct default value when the class was not found.
4529 * @dataProvider component_class_callback_default_provider
4530 * @param $default
4532 public function test_component_class_callback_method_not_found($default): void {
4533 require_once(__DIR__ . '/fixtures/component_class_callback_example.php');
4535 $this->assertSame($default, component_class_callback(test_component_class_callback_example::class, 'this_is_not_the_method_you_were_looking_for', ['abc'], $default));
4539 * Test that the component_class_callback returns the default when the method returned null.
4541 * @dataProvider component_class_callback_default_provider
4542 * @param $default
4544 public function test_component_class_callback_found_returns_null($default): void {
4545 require_once(__DIR__ . '/fixtures/component_class_callback_example.php');
4547 $this->assertSame($default, component_class_callback(\test_component_class_callback_example::class, 'method_returns_value', [null], $default));
4548 $this->assertSame($default, component_class_callback(\test_component_class_callback_child_example::class, 'method_returns_value', [null], $default));
4552 * Test that the component_class_callback returns the expected value and not the default when there was a value.
4554 * @dataProvider component_class_callback_data_provider
4555 * @param $default
4557 public function test_component_class_callback_found_returns_value($value): void {
4558 require_once(__DIR__ . '/fixtures/component_class_callback_example.php');
4560 $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'));
4561 $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'));
4565 * Test that the component_class_callback handles multiple params correctly.
4567 * @dataProvider component_class_callback_multiple_params_provider
4568 * @param $default
4570 public function test_component_class_callback_found_accepts_multiple($params, $count): void {
4571 require_once(__DIR__ . '/fixtures/component_class_callback_example.php');
4573 $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'));
4574 $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'));
4578 * Data provider with list of default values for user in component_class_callback tests.
4580 * @return array
4582 public function component_class_callback_default_provider() {
4583 return [
4584 'null' => [null],
4585 'empty string' => [''],
4586 'string' => ['This is a string'],
4587 'int' => [12345],
4588 'stdClass' => [(object) ['this is my content']],
4589 'array' => [['a' => 'b',]],
4594 * Data provider with list of default values for user in component_class_callback tests.
4596 * @return array
4598 public function component_class_callback_data_provider() {
4599 return [
4600 'empty string' => [''],
4601 'string' => ['This is a string'],
4602 'int' => [12345],
4603 'stdClass' => [(object) ['this is my content']],
4604 'array' => [['a' => 'b',]],
4609 * Data provider with list of default values for user in component_class_callback tests.
4611 * @return array
4613 public function component_class_callback_multiple_params_provider() {
4614 return [
4615 'empty array' => [
4619 'string value' => [
4620 ['one'],
4623 'string values' => [
4624 ['one', 'two'],
4627 'arrays' => [
4628 [[], []],
4631 'nulls' => [
4632 [null, null, null, null],
4635 'mixed' => [
4636 ['a', 1, null, (object) [], []],
4643 * Test that {@link get_callable_name()} describes the callable as expected.
4645 * @dataProvider callable_names_provider
4646 * @param callable $callable
4647 * @param string $expectedname
4649 public function test_get_callable_name($callable, $expectedname): void {
4650 $this->assertSame($expectedname, get_callable_name($callable));
4654 * Provides a set of callables and their human readable names.
4656 * @return array of (string)case => [(mixed)callable, (string|bool)expected description]
4658 public function callable_names_provider() {
4659 return [
4660 'integer' => [
4661 386,
4662 false,
4664 'boolean' => [
4665 true,
4666 false,
4668 'static_method_as_literal' => [
4669 'my_foobar_class::my_foobar_method',
4670 'my_foobar_class::my_foobar_method',
4672 'static_method_of_literal_class' => [
4673 ['my_foobar_class', 'my_foobar_method'],
4674 'my_foobar_class::my_foobar_method',
4676 'static_method_of_object' => [
4677 [$this, 'my_foobar_method'],
4678 'core\moodlelib_test::my_foobar_method',
4680 'method_of_object' => [
4681 [new lang_string('parentlanguage', 'core_langconfig'), 'my_foobar_method'],
4682 'core\lang_string::my_foobar_method',
4684 'function_as_literal' => [
4685 'my_foobar_callback',
4686 'my_foobar_callback',
4688 'function_as_closure' => [
4689 function($a) { return $a; },
4690 'Closure::__invoke',
4696 * Data provider for \core_moodlelib_testcase::test_get_complete_user_data().
4698 * @return array
4700 public function user_data_provider() {
4701 return [
4702 'Fetch data using a valid username' => [
4703 'username', 's1', true
4705 'Fetch data using a valid username, different case' => [
4706 'username', 'S1', true
4708 'Fetch data using a valid username, different case for fieldname and value' => [
4709 'USERNAME', 'S1', true
4711 'Fetch data using an invalid username' => [
4712 'username', 's2', false
4714 'Fetch by email' => [
4715 'email', 's1@example.com', true
4717 'Fetch data using a non-existent email' => [
4718 'email', 's2@example.com', false
4720 'Fetch data using a non-existent email, throw exception' => [
4721 'email', 's2@example.com', false, \dml_missing_record_exception::class
4723 'Multiple accounts with the same email' => [
4724 'email', 's1@example.com', false, 1
4726 'Multiple accounts with the same email, throw exception' => [
4727 'email', 's1@example.com', false, 1, \dml_multiple_records_exception::class
4729 'Fetch data using a valid user ID' => [
4730 'id', true, true
4732 'Fetch data using a non-existent user ID' => [
4733 'id', false, false
4739 * Test for get_complete_user_data().
4741 * @dataProvider user_data_provider
4742 * @param string $field The field to use for the query.
4743 * @param string|boolean $value The field value. When fetching by ID, set true to fetch valid user ID, false otherwise.
4744 * @param boolean $success Whether we expect for the fetch to succeed or return false.
4745 * @param int $allowaccountssameemail Value for $CFG->allowaccountssameemail.
4746 * @param string $expectedexception The exception to be expected.
4748 public function test_get_complete_user_data($field, $value, $success, $allowaccountssameemail = 0, $expectedexception = ''): void {
4749 $this->resetAfterTest();
4751 // Set config settings we need for our environment.
4752 set_config('allowaccountssameemail', $allowaccountssameemail);
4754 // Generate the user data.
4755 $generator = $this->getDataGenerator();
4756 $userdata = [
4757 'username' => 's1',
4758 'email' => 's1@example.com',
4760 $user = $generator->create_user($userdata);
4762 if ($allowaccountssameemail) {
4763 // Create another user with the same email address.
4764 $generator->create_user(['email' => 's1@example.com']);
4767 // Since the data provider can't know what user ID to use, do a special handling for ID field tests.
4768 if ($field === 'id') {
4769 if ($value) {
4770 // Test for fetching data using a valid user ID. Use the generated user's ID.
4771 $value = $user->id;
4772 } else {
4773 // Test for fetching data using a non-existent user ID.
4774 $value = $user->id + 1;
4778 // When an exception is expected.
4779 $throwexception = false;
4780 if ($expectedexception) {
4781 $this->expectException($expectedexception);
4782 $throwexception = true;
4785 $fetcheduser = get_complete_user_data($field, $value, null, $throwexception);
4786 if ($success) {
4787 $this->assertEquals($user->id, $fetcheduser->id);
4788 $this->assertEquals($user->username, $fetcheduser->username);
4789 $this->assertEquals($user->email, $fetcheduser->email);
4790 } else {
4791 $this->assertFalse($fetcheduser);
4796 * Test for send_password_change_().
4798 public function test_send_password_change_info(): void {
4799 $this->resetAfterTest();
4801 $user = $this->getDataGenerator()->create_user();
4803 $sink = $this->redirectEmails(); // Make sure we are redirecting emails.
4804 send_password_change_info($user);
4805 $result = $sink->get_messages();
4806 $sink->close();
4808 $this->assertStringContainsString('passwords cannot be reset on this site', quoted_printable_decode($result[0]->body));
4812 * Test the get_time_interval_string for a range of inputs.
4814 * @dataProvider get_time_interval_string_provider
4815 * @param int $time1 the time1 param.
4816 * @param int $time2 the time2 param.
4817 * @param string|null $format the format param.
4818 * @param string $expected the expected string.
4819 * @param bool $dropzeroes the value passed for the `$dropzeros` param.
4820 * @param bool $fullformat the value passed for the `$fullformat` param.
4821 * @covers \get_time_interval_string
4823 public function test_get_time_interval_string(int $time1, int $time2, ?string $format, string $expected,
4824 bool $dropzeroes = false, bool $fullformat = false): void {
4825 if (is_null($format)) {
4826 $this->assertEquals($expected, get_time_interval_string($time1, $time2));
4827 } else {
4828 $this->assertEquals($expected, get_time_interval_string($time1, $time2, $format, $dropzeroes, $fullformat));
4833 * Data provider for the test_get_time_interval_string() method.
4835 public function get_time_interval_string_provider() {
4836 return [
4837 'Time is after the reference time by 1 minute, omitted format' => [
4838 'time1' => 12345660,
4839 'time2' => 12345600,
4840 'format' => null,
4841 'expected' => '0d 0h 1m'
4843 'Time is before the reference time by 1 minute, omitted format' => [
4844 'time1' => 12345540,
4845 'time2' => 12345600,
4846 'format' => null,
4847 'expected' => '0d 0h 1m'
4849 'Time is equal to the reference time, omitted format' => [
4850 'time1' => 12345600,
4851 'time2' => 12345600,
4852 'format' => null,
4853 'expected' => '0d 0h 0m'
4855 'Time is after the reference time by 1 minute, empty string format' => [
4856 'time1' => 12345660,
4857 'time2' => 12345600,
4858 'format' => '',
4859 'expected' => '0d 0h 1m'
4861 'Time is before the reference time by 1 minute, empty string format' => [
4862 'time1' => 12345540,
4863 'time2' => 12345600,
4864 'format' => '',
4865 'expected' => '0d 0h 1m'
4867 'Time is equal to the reference time, empty string format' => [
4868 'time1' => 12345600,
4869 'time2' => 12345600,
4870 'format' => '',
4871 'expected' => '0d 0h 0m'
4873 'Time is after the reference time by 1 minute, custom format' => [
4874 'time1' => 12345660,
4875 'time2' => 12345600,
4876 'format' => '%R%adays %hhours %imins',
4877 'expected' => '+0days 0hours 1mins'
4879 'Time is before the reference time by 1 minute, custom format' => [
4880 'time1' => 12345540,
4881 'time2' => 12345600,
4882 'format' => '%R%adays %hhours %imins',
4883 'expected' => '-0days 0hours 1mins'
4885 'Time is equal to the reference time, custom format' => [
4886 'time1' => 12345600,
4887 'time2' => 12345600,
4888 'format' => '%R%adays %hhours %imins',
4889 'expected' => '+0days 0hours 0mins'
4891 'Default format, time is after the reference time by 1 minute, drop zeroes, short form' => [
4892 'time1' => 12345660,
4893 'time2' => 12345600,
4894 'format' => '',
4895 'expected' => '1m',
4896 'dropzeroes' => true,
4898 'Default format, time is after the reference time by 1 minute, drop zeroes, full form' => [
4899 'time1' => 12345660,
4900 'time2' => 12345600,
4901 'format' => '',
4902 'expected' => '1 minutes',
4903 'dropzeroes' => true,
4904 'fullformat' => true,
4906 'Default format, time is after the reference time by 1 minute, retain zeroes, full form' => [
4907 'time1' => 12345660,
4908 'time2' => 12345600,
4909 'format' => '',
4910 'expected' => '0 days 0 hours 1 minutes',
4911 'dropzeroes' => false,
4912 'fullformat' => true,
4914 'Empty string format, time is after the reference time by 1 minute, retain zeroes, full form' => [
4915 'time1' => 12345660,
4916 'time2' => 12345600,
4917 'format' => ' ',
4918 'expected' => '0 days 0 hours 1 minutes',
4919 'dropzeroes' => false,
4920 'fullformat' => true,
4926 * Tests the rename_to_unused_name function with a file.
4928 public function test_rename_to_unused_name_file(): void {
4929 global $CFG;
4931 // Create a new file in dataroot.
4932 $file = $CFG->dataroot . '/argh.txt';
4933 file_put_contents($file, 'Frogs');
4935 // Rename it.
4936 $newname = rename_to_unused_name($file);
4938 // Check new name has expected format.
4939 $this->assertMatchesRegularExpression('~/_temp_[a-f0-9]+$~', $newname);
4941 // Check it's still in the same folder.
4942 $this->assertEquals($CFG->dataroot, dirname($newname));
4944 // Check file can be loaded.
4945 $this->assertEquals('Frogs', file_get_contents($newname));
4947 // OK, delete the file.
4948 unlink($newname);
4952 * Tests the rename_to_unused_name function with a directory.
4954 public function test_rename_to_unused_name_dir(): void {
4955 global $CFG;
4957 // Create a new directory in dataroot.
4958 $file = $CFG->dataroot . '/arghdir';
4959 mkdir($file);
4961 // Rename it.
4962 $newname = rename_to_unused_name($file);
4964 // Check new name has expected format.
4965 $this->assertMatchesRegularExpression('~/_temp_[a-f0-9]+$~', $newname);
4967 // Check it's still in the same folder.
4968 $this->assertEquals($CFG->dataroot, dirname($newname));
4970 // Check it's still a directory
4971 $this->assertTrue(is_dir($newname));
4973 // OK, delete the directory.
4974 rmdir($newname);
4978 * Tests the rename_to_unused_name function with error cases.
4980 public function test_rename_to_unused_name_failure(): void {
4981 global $CFG;
4983 // Rename a file that doesn't exist.
4984 $file = $CFG->dataroot . '/argh.txt';
4985 $this->assertFalse(rename_to_unused_name($file));
4989 * Provider for display_size
4991 * @return array of ($size, $expected)
4993 public function display_size_provider() {
4995 return [
4996 [0, '0 bytes'],
4997 [1, '1 bytes'],
4998 [1023, '1023 bytes'],
4999 [1024, '1.0 KB'],
5000 [2222, '2.2 KB'],
5001 [33333, '32.6 KB'],
5002 [444444, '434.0 KB'],
5003 [5555555, '5.3 MB'],
5004 [66666666, '63.6 MB'],
5005 [777777777, '741.7 MB'],
5006 [8888888888, '8.3 GB'],
5007 [99999999999, '93.1 GB'],
5008 [111111111111, '103.5 GB'],
5009 [2222222222222, '2.0 TB'],
5010 [33333333333333, '30.3 TB'],
5011 [444444444444444, '404.2 TB'],
5012 [5555555555555555, '4.9 PB'],
5013 [66666666666666666, '59.2 PB'],
5014 [777777777777777777, '690.8 PB'],
5019 * Test display_size
5020 * @dataProvider display_size_provider
5021 * @param int $size the size in bytes
5022 * @param string $expected the expected string.
5024 public function test_display_size($size, $expected): void {
5025 $result = display_size($size);
5026 $expected = str_replace(' ', "\xc2\xa0", $expected); // Should be non-breaking space.
5027 $this->assertEquals($expected, $result);
5031 * Provider for display_size using fixed units.
5033 * @return array of ($size, $units, $expected)
5035 public function display_size_fixed_provider(): array {
5036 return [
5037 [0, 'KB', '0.0 KB'],
5038 [1, 'MB', '0.0 MB'],
5039 [777777777, 'GB', '0.7 GB'],
5040 [8888888888, 'PB', '0.0 PB'],
5041 [99999999999, 'TB', '0.1 TB'],
5042 [99999999999, 'B', '99999999999 bytes'],
5047 * Test display_size using fixed units.
5049 * @dataProvider display_size_fixed_provider
5050 * @param int $size Size in bytes
5051 * @param string $units Fixed units
5052 * @param string $expected Expected string.
5054 public function test_display_size_fixed(int $size, string $units, string $expected): void {
5055 $result = display_size($size, 1, $units);
5056 $expected = str_replace(' ', "\xc2\xa0", $expected); // Should be non-breaking space.
5057 $this->assertEquals($expected, $result);
5061 * Provider for display_size using specified decimal places.
5063 * @return array of ($size, $decimalplaces, $units, $expected)
5065 public function display_size_dp_provider(): array {
5066 return [
5067 [0, 1, 'KB', '0.0 KB'],
5068 [1, 6, 'MB', '0.000001 MB'],
5069 [777777777, 0, 'GB', '1 GB'],
5070 [777777777, 0, '', '742 MB'],
5071 [42, 6, '', '42 bytes'],
5076 * Test display_size using specified decimal places.
5078 * @dataProvider display_size_dp_provider
5079 * @param int $size Size in bytes
5080 * @param int $places Number of decimal places
5081 * @param string $units Fixed units
5082 * @param string $expected Expected string.
5084 public function test_display_size_dp(int $size, int $places, string $units, string $expected): void {
5085 $result = display_size($size, $places, $units);
5086 $expected = str_replace(' ', "\xc2\xa0", $expected); // Should be non-breaking space.
5087 $this->assertEquals($expected, $result);
5091 * Test that the get_list_of_plugins function includes/excludes directories as appropriate.
5093 * @dataProvider get_list_of_plugins_provider
5094 * @param array $expectedlist The expected list of folders
5095 * @param array $content The list of file content to set up in the virtual file root
5096 * @param string $dir The base dir to look at in the virtual file root
5097 * @param string $exclude Any additional folder to exclude
5099 public function test_get_list_of_plugins(array $expectedlist, array $content, string $dir, string $exclude): void {
5100 $vfileroot = \org\bovigo\vfs\vfsStream::setup('root', null, $content);
5101 $base = \org\bovigo\vfs\vfsStream::url('root');
5103 $this->assertEquals($expectedlist, get_list_of_plugins($dir, $exclude, $base));
5107 * Data provider for get_list_of_plugins checks.
5109 * @return array
5111 public function get_list_of_plugins_provider(): array {
5112 return [
5113 'Standard excludes' => [
5114 ['amdd', 'class', 'local', 'test'],
5116 '.' => [],
5117 '..' => [],
5118 'amd' => [],
5119 'amdd' => [],
5120 'class' => [],
5121 'classes' => [],
5122 'local' => [],
5123 'test' => [],
5124 'tests' => [],
5125 'yui' => [],
5130 'Standard excludes with addition' => [
5131 ['amdd', 'local', 'test'],
5133 '.' => [],
5134 '..' => [],
5135 'amd' => [],
5136 'amdd' => [],
5137 'class' => [],
5138 'classes' => [],
5139 'local' => [],
5140 'test' => [],
5141 'tests' => [],
5142 'yui' => [],
5145 'class',
5147 'Files excluded' => [
5148 ['def'],
5150 '.' => [],
5151 '..' => [],
5152 'abc' => 'File with filename abc',
5153 'def' => [
5154 '.' => [],
5155 '..' => [],
5156 'example.txt' => 'In a directory called "def"',
5162 'Subdirectories only' => [
5163 ['abc'],
5165 '.' => [],
5166 '..' => [],
5167 'foo' => [
5168 '.' => [],
5169 '..' => [],
5170 'abc' => [],
5172 'bar' => [
5173 '.' => [],
5174 '..' => [],
5175 'def' => [],
5178 'foo',
5185 * Test get_home_page() method.
5187 * @dataProvider get_home_page_provider
5188 * @param string $user Whether the user is logged, guest or not logged.
5189 * @param int $expected Expected value after calling the get_home_page method.
5190 * @param int|string|null $defaulthomepage The $CFG->defaulthomepage setting value.
5191 * @param int|null $enabledashboard Whether the dashboard should be enabled or not.
5192 * @param int|string|null $userpreference User preference for the home page setting.
5193 * $param int|null $allowguestmymoodle The $CFG->allowguestmymoodle setting value.
5194 * @covers ::get_home_page
5196 public function test_get_home_page(
5197 string $user,
5198 int $expected,
5199 int|string|null $defaulthomepage = null,
5200 ?int $enabledashboard = null,
5201 int|string|null $userpreference = null,
5202 ?int $allowguestmymoodle = null,
5203 ): void {
5204 global $CFG, $USER;
5206 $this->resetAfterTest();
5208 if ($user == 'guest') {
5209 $this->setGuestUser();
5210 } else if ($user == 'logged') {
5211 $this->setUser($this->getDataGenerator()->create_user());
5214 if (isset($defaulthomepage)) {
5215 $CFG->defaulthomepage = $defaulthomepage;
5217 if (isset($enabledashboard)) {
5218 $CFG->enabledashboard = $enabledashboard;
5220 if (isset($allowguestmymoodle)) {
5221 $CFG->allowguestmymoodle = $allowguestmymoodle;
5224 if ($USER) {
5225 set_user_preferences(['user_home_page_preference' => $userpreference], $USER->id);
5228 $homepage = get_home_page();
5229 $this->assertEquals($expected, $homepage);
5233 * Data provider for get_home_page checks.
5235 * @return array
5237 public static function get_home_page_provider(): array {
5238 global $CFG;
5240 return [
5241 'No logged user' => [
5242 'user' => 'nologged',
5243 'expected' => HOMEPAGE_SITE,
5245 'Guest user. Dashboard set as default home page and enabled for guests' => [
5246 'user' => 'guest',
5247 'expected' => HOMEPAGE_MY,
5249 'Guest user. Dashboard set as default home page but disabled for guests' => [
5250 'user' => 'guest',
5251 'expected' => HOMEPAGE_SITE,
5252 'defaulthomepage' => HOMEPAGE_MY,
5253 'enabledashboard' => 1,
5254 'userpreference' => null,
5255 'allowguestmymoodle' => 0,
5257 'Guest user. My courses set as default home page' => [
5258 'user' => 'guest',
5259 'expected' => HOMEPAGE_SITE,
5260 'defaulthomepage' => HOMEPAGE_MYCOURSES,
5262 'Guest user. User preference set as default page' => [
5263 'user' => 'guest',
5264 'expected' => HOMEPAGE_SITE,
5265 'defaulthomepage' => HOMEPAGE_USER,
5267 'Logged user. Dashboard set as default home page and enabled' => [
5268 'user' => 'logged',
5269 'expected' => HOMEPAGE_MY,
5271 'Logged user. Dashboard set as default home page but disabled' => [
5272 'user' => 'logged',
5273 'expected' => HOMEPAGE_MYCOURSES,
5274 'defaulthomepage' => HOMEPAGE_MY,
5275 'enabledashboard' => 0,
5277 'Logged user. My courses set as default home page with dashboard enabled' => [
5278 'user' => 'logged',
5279 'expected' => HOMEPAGE_MYCOURSES,
5280 'defaulthomepage' => HOMEPAGE_MYCOURSES,
5281 'enabledashboard' => 1,
5283 'Logged user. My courses set as default home page with dashboard disabled' => [
5284 'user' => 'logged',
5285 'expected' => HOMEPAGE_MYCOURSES,
5286 'defaulthomepage' => HOMEPAGE_MYCOURSES,
5287 'enabledashboard' => 0,
5289 'Logged user. Site set as default home page with dashboard enabled' => [
5290 'user' => 'logged',
5291 'expected' => HOMEPAGE_SITE,
5292 'defaulthomepage' => HOMEPAGE_SITE,
5293 'enabledashboard' => 1,
5295 'Logged user. Site set as default home page with dashboard disabled' => [
5296 'user' => 'logged',
5297 'expected' => HOMEPAGE_SITE,
5298 'defaulthomepage' => HOMEPAGE_SITE,
5299 'enabledashboard' => 0,
5301 'Logged user. URL set as default home page.' => [
5302 'user' => 'logged',
5303 'expected' => HOMEPAGE_URL,
5304 'defaulthomepage' => "/home",
5306 'Logged user. User preference set as default page with dashboard enabled and user preference set to dashboard' => [
5307 'user' => 'logged',
5308 'expected' => HOMEPAGE_MY,
5309 'defaulthomepage' => HOMEPAGE_USER,
5310 'enabledashboard' => 1,
5311 'userpreference' => HOMEPAGE_MY,
5313 'Logged user. User preference set as default page with dashboard disabled and user preference set to dashboard' => [
5314 'user' => 'logged',
5315 'expected' => HOMEPAGE_MYCOURSES,
5316 'defaulthomepage' => HOMEPAGE_USER,
5317 'enabledashboard' => 0,
5318 'userpreference' => HOMEPAGE_MY,
5320 'Logged user. User preference set as default page with dashboard enabled and user preference set to my courses' => [
5321 'user' => 'logged',
5322 'expected' => HOMEPAGE_MYCOURSES,
5323 'defaulthomepage' => HOMEPAGE_USER,
5324 'enabledashboard' => 1,
5325 'userpreference' => HOMEPAGE_MYCOURSES,
5327 'Logged user. User preference set as default page with dashboard disabled and user preference set to my courses' => [
5328 'user' => 'logged',
5329 'expected' => HOMEPAGE_MYCOURSES,
5330 'defaulthomepage' => HOMEPAGE_USER,
5331 'enabledashboard' => 0,
5332 'userpreference' => HOMEPAGE_MYCOURSES,
5334 'Logged user. User preference set as default page with user preference set to URL.' => [
5335 'user' => 'logged',
5336 'expected' => HOMEPAGE_URL,
5337 'defaulthomepage' => HOMEPAGE_USER,
5338 'enabledashboard' => null,
5339 'userpreference' => "/home",
5345 * Test get_default_home_page() method.
5347 * @covers ::get_default_home_page
5349 public function test_get_default_home_page(): void {
5350 global $CFG;
5352 $this->resetAfterTest();
5354 $CFG->enabledashboard = 1;
5355 $default = get_default_home_page();
5356 $this->assertEquals(HOMEPAGE_MY, $default);
5358 $CFG->enabledashboard = 0;
5359 $default = get_default_home_page();
5360 $this->assertEquals(HOMEPAGE_MYCOURSES, $default);
5364 * Test getting default home page for {@see HOMEPAGE_URL}
5366 * @covers ::get_default_home_page_url
5368 public function test_get_default_home_page_url(): void {
5369 global $CFG;
5371 $this->resetAfterTest();
5372 $this->setAdminUser();
5374 $this->assertNull(get_default_home_page_url());
5376 // Site configuration.
5377 $CFG->defaulthomepage = "/home";
5378 $this->assertEquals("{$CFG->wwwroot}/home", get_default_home_page_url());
5380 // Site configuration with invalid value.
5381 $CFG->defaulthomepage = "home";
5382 $this->assertNull(get_default_home_page_url());
5384 // User preference.
5385 $CFG->defaulthomepage = HOMEPAGE_USER;
5387 $userpreference = "/about";
5388 set_user_preference('user_home_page_preference', $userpreference);
5389 $this->assertEquals("{$CFG->wwwroot}/about", get_default_home_page_url());
5391 // User preference with invalid value.
5392 set_user_preference('user_home_page_preference', "about");
5393 $this->assertNull(get_default_home_page_url());
5397 * Tests the get_performance_info function with regard to locks.
5399 * @covers ::get_performance_info
5401 public function test_get_performance_info_locks(): void {
5402 global $PERF;
5404 // Unset lock data just in case previous tests have set it.
5405 unset($PERF->locks);
5407 // With no lock data, there should be no information about locks in the results.
5408 $result = get_performance_info();
5409 $this->assertStringNotContainsString('Lock', $result['html']);
5410 $this->assertStringNotContainsString('Lock', $result['txt']);
5412 // Rather than really do locks, just fill the array with fake data in the right format.
5413 $PERF->locks = [
5414 (object) [
5415 'type' => 'phpunit',
5416 'resource' => 'lock1',
5417 'wait' => 0.59,
5418 'success' => true,
5419 'held' => '6.04'
5420 ], (object) [
5421 'type' => 'phpunit',
5422 'resource' => 'lock2',
5423 'wait' => 0.91,
5424 'success' => false
5427 $result = get_performance_info();
5429 // Extract HTML table rows.
5430 $this->assertEquals(1, preg_match('~<table class="locktimings.*?</table>~s',
5431 $result['html'], $matches));
5432 $this->assertEquals(3, preg_match_all('~<tr[> ].*?</tr>~s', $matches[0], $matches2));
5433 $rows = $matches2[0];
5435 // Check header.
5436 $this->assertMatchesRegularExpression('~Lock.*Waited.*Obtained.*Held~s', $rows[0]);
5437 // Check both locks.
5438 $this->assertMatchesRegularExpression('~phpunit/lock1.*0\.6.*&#x2713;.*6\.0~s', $rows[1]);
5439 $this->assertMatchesRegularExpression('~phpunit/lock2.*0\.9.*&#x274c;.*-~s', $rows[2]);
5441 $this->assertStringContainsString('Locks (waited/obtained/held): ' .
5442 'phpunit/lock1 (0.6/y/6.0) phpunit/lock2 (0.9/n/-).', $result['txt']);
5446 * Tests the get_performance_info function with regard to session wait time.
5448 * @covers ::get_performance_info
5450 public function test_get_performance_info_session_wait(): void {
5451 global $PERF;
5453 // With no session lock data, there should be no session wait information in the results.
5454 unset($PERF->sessionlock);
5455 $result = get_performance_info();
5456 $this->assertStringNotContainsString('Session wait', $result['html']);
5457 $this->assertStringNotContainsString('sessionwait', $result['txt']);
5459 // With suitable data, it should be included in the result.
5460 $PERF->sessionlock = ['wait' => 4.2];
5461 $result = get_performance_info();
5462 $this->assertStringContainsString('Session wait: 4.200 secs', $result['html']);
5463 $this->assertStringContainsString('sessionwait: 4.200 secs', $result['txt']);
5467 * Test the html_is_blank() function.
5469 * @covers ::html_is_blank
5471 public function test_html_is_blank(): void {
5472 $this->assertEquals(true, html_is_blank(null));
5473 $this->assertEquals(true, html_is_blank(''));
5474 $this->assertEquals(true, html_is_blank('<p> </p>'));
5475 $this->assertEquals(false, html_is_blank('<p>.</p>'));
5476 $this->assertEquals(false, html_is_blank('<img src="#">'));
5480 * Provider for is_proxybypass
5482 * @return array of test cases.
5484 public function is_proxybypass_provider(): array {
5486 return [
5487 'Proxybypass contains the same IP as the beginning of the URL' => [
5488 'http://192.168.5.5-fake-app-7f000101.nip.io',
5489 '192.168.5.5, 127.0.0.1',
5490 false
5492 'Proxybypass contains the last part of the URL' => [
5493 'http://192.168.5.5-fake-app-7f000101.nip.io',
5494 'app-7f000101.nip.io',
5495 false
5497 'Proxybypass contains the last part of the URL 2' => [
5498 'http://store.mydomain.com',
5499 'mydomain.com',
5500 false
5502 'Proxybypass contains part of the url' => [
5503 'http://myweb.com',
5504 'store.myweb.com',
5505 false
5507 'Different IPs used in proxybypass' => [
5508 'http://192.168.5.5',
5509 '192.168.5.3',
5510 false
5512 'Proxybypass and URL matchs' => [
5513 'http://store.mydomain.com',
5514 'store.mydomain.com',
5515 true
5517 'IP used in proxybypass' => [
5518 'http://192.168.5.5',
5519 '192.168.5.5',
5520 true
5526 * Check if $url matches anything in proxybypass list
5528 * Test function {@see is_proxybypass()}.
5529 * @dataProvider is_proxybypass_provider
5530 * @param string $url url to check
5531 * @param string $proxybypass
5532 * @param bool $expected Expected value.
5534 public function test_is_proxybypass(string $url, string $proxybypass, bool $expected): void {
5535 $this->resetAfterTest();
5537 global $CFG;
5538 $CFG->proxyhost = '192.168.5.5'; // Test with a fake proxy.
5539 $CFG->proxybypass = $proxybypass;
5541 $this->assertEquals($expected, is_proxybypass($url));
5545 * Test that the moodle_array_keys_filter method behaves in the same way
5546 * that array_keys behaved before Moodle 8.3.
5548 * @dataProvider moodle_array_keys_filter_provider
5549 * @param array $array
5550 * @param mixed $filter
5551 * @param bool $strict
5552 * @param array $expected
5553 * @covers ::moodle_array_keys_filter
5555 public function test_moodle_array_keys_filter(
5556 array $array,
5557 mixed $filter,
5558 bool $strict,
5559 array $expected,
5560 ): void {
5561 $this->assertSame(
5562 $expected,
5563 moodle_array_keys_filter($array, $filter, $strict),
5568 * Data provider for moodle_array_keys_filter tests.
5570 * @return array
5572 public static function moodle_array_keys_filter_provider(): array {
5573 return [
5574 [['a', 'b', 'c'], 'b', false, [1]],
5577 'alpha' => 'a',
5578 'bravo' => 'b',
5579 'charlie' => 'c',
5581 'b',
5582 false,
5583 ['bravo'],
5587 'zero' => 0,
5588 'one' => 1,
5589 'true' => true,
5591 '1',
5592 false,
5593 ['one', 'true'],
5597 'zero' => 0,
5598 'one' => 1,
5599 'true' => true,
5601 true,
5602 false,
5603 ['one', 'true'],
5607 'zero' => 0,
5608 'one' => 1,
5609 'true' => true,
5611 true,
5612 true,
5613 ['true'],
5617 'zero' => 0,
5618 'one' => 1,
5619 'true' => true,
5622 true,
5623 ['one'],
5629 * Test case for checking the email greetings in various user notification emails.
5631 * @dataProvider email_greetings_provider
5632 * @param string $funcname The name of the function to call for sending the email.
5633 * @param mixed $extra Any extra parameter required by the function.
5634 * @covers ::send_password_change_info()
5635 * @covers ::send_confirmation_email()
5636 * @covers ::setnew_password_and_mail()
5637 * @covers ::send_password_change_confirmation_email()
5639 public function test_email_greetings($funcname, $extra): void {
5640 $this->resetAfterTest();
5642 $user = $this->getDataGenerator()->create_user();
5644 $sink = $this->redirectEmails(); // Make sure we are redirecting emails.
5645 $funcname($user, $extra);
5646 $result = $sink->get_messages();
5647 $sink->close();
5648 // Test greetings.
5649 $this->assertStringContainsString('Hi ' . $user->firstname, quoted_printable_decode($result[0]->body));
5653 * Data provider for test_email_greetings tests.
5655 * @return array
5657 public static function email_greetings_provider(): array {
5658 $extrasendpasswordchangeconfirmationemail = new \stdClass();
5659 $extrasendpasswordchangeconfirmationemail->token = '123';
5661 return [
5662 ['send_password_change_info', null],
5663 ['send_confirmation_email', null],
5664 ['setnew_password_and_mail', false],
5665 ['send_password_change_confirmation_email', $extrasendpasswordchangeconfirmationemail],