2 // This file is part of Moodle - http://moodle.org/
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.
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/>.
20 * Unit tests for (some of) ../moodlelib.php.
24 * @copyright © 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
{
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
{
58 $this->assertNull(cleanremoteaddr('1023.121.234.1'));
59 $this->assertSame('123.121.234.1', cleanremoteaddr('123.121.234.01 '));
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'));
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'));
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'));
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 '));
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:'));
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();
173 $this->assertEquals($object, fix_utf8($object));
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
{
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');
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
{
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.
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']);
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);
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');
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
{
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.
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.
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.
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);
316 $param = new \
stdClass();
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.
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.
344 clean_param_array(array('x'), 'xxxxxx');
345 $this->fail('moodle_exception expected');
346 } catch (\moodle_exception
$ex) {
347 $this->assertInstanceOf('moodle_exception', $ex);
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);
361 * @covers \core\param
362 * @covers \clean_param
364 public function test_clean_param_raw(): void
{
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
{
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
));
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}
587 public static function clean_param_host_provider(): array {
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.
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
));
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
{
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
));
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
));
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
{
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.
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
));
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',
836 'Wrong/.Value' => '',
837 'Wrong(Value)' => '',
863 foreach ($testvalues as $testvalue => $expectedvalue) {
864 $actualvalue = clean_param($testvalue, PARAM_TIMEZONE
);
865 $this->assertEquals($expectedvalue, $actualvalue);
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
{
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);
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);
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);
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
);
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);
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);
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);
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);
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 break there";
1017 $this->assertSame("some text which shouldn't ...", shorten_text($text, 31));
1018 $this->assertSame("some text which shouldn't ...", 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> <<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> ...",
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
{
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));
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';
1112 'More than 100 characters' => [
1115 'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium tot',
1118 'More than 100 characters with hash' => [
1121 'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque l - 3bec1da8b8',
1124 'More than 100 characters with extension' => [
1127 'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium tot.zip',
1130 'More than 100 characters with extension and hash' => [
1133 'sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque l - 3bec1da8b8.zip',
1136 'Limit filename to 50 chars' => [
1139 'sed ut perspiciatis unde omnis iste natus error si',
1142 'Limit filename to 50 chars with hash' => [
1145 'sed ut perspiciatis unde omnis iste n - 3bec1da8b8',
1148 'Limit filename to 50 chars with extension' => [
1151 'sed ut perspiciatis unde omnis iste natus error si.zip',
1154 'Limit filename to 50 chars with extension and hash' => [
1157 'sed ut perspiciatis unde omnis iste n - 3bec1da8b8.zip',
1160 'Test filename that contains less than 100 characters' => [
1166 'Test filename that contains less than 100 characters and hash' => [
1172 'Test filename that contains less than 100 characters with extension' => [
1173 "{$shortfilename}.zip",
1175 "{$shortfilename}.zip",
1178 'Test filename that contains less than 100 characters with extension and hash' => [
1179 "{$shortfilename}.zip",
1181 "{$shortfilename}.zip",
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';
1225 'Empty array without hash' => [
1231 'Empty array with hash' => [
1237 'Array with less than 100 characters' => [
1238 [$shortfilename, $shortfilename, $shortfilename],
1240 [$shortfilename, $shortfilename, $shortfilename],
1243 'Array with more than 100 characters without hash' => [
1244 [$longfilename, $longfilename, $longfilename],
1246 [$expected, $expected, $expected],
1249 'Array with more than 100 characters with hash' => [
1250 [$longfilename, $longfilename, $longfilename],
1252 [$expectedwithhash, $expectedwithhash, $expectedwithhash],
1255 'Array with more than 100 characters with extension' => [
1256 [$extfilename, $extfilename, $extfilename],
1258 [$expectedext, $expectedext, $expectedext],
1261 'Array with more than 100 characters with extension and hash' => [
1262 [$extfilename, $extfilename, $extfilename],
1264 [$expectedextwithhash, $expectedextwithhash, $expectedextwithhash],
1267 'Array with more than 100 characters mix (short, long, with extension) without hash' => [
1268 [$shortfilename, $longfilename, $extfilename],
1270 [$shortfilename, $expected, $expectedext],
1273 'Array with more than 100 characters mix (short, long, with extension) with hash' => [
1274 [$shortfilename, $longfilename, $extfilename],
1276 [$shortfilename, $expectedwithhash, $expectedextwithhash],
1279 'Array with less than 50 characters without hash' => [
1280 [$longfilename, $longfilename, $longfilename],
1282 [$expected50, $expected50, $expected50],
1285 'Array with less than 50 characters with hash' => [
1286 [$longfilename, $longfilename, $longfilename],
1288 [$expected50withhash, $expected50withhash, $expected50withhash],
1291 'Array with less than 50 characters with extension' => [
1292 [$extfilename, $extfilename, $extfilename],
1294 [$expected50ext, $expected50ext, $expected50ext],
1297 'Array with less than 50 characters with extension and hash' => [
1298 [$extfilename, $extfilename, $extfilename],
1300 [$expected50extwithhash, $expected50extwithhash, $expected50extwithhash],
1303 'Array with less than 50 characters mix (short, long, with extension) without hash' => [
1304 [$shortfilename, $longfilename, $extfilename],
1306 [$expected50, $expected50, $expected50ext],
1309 'Array with less than 50 characters mix (short, long, with extension) with hash' => [
1310 [$shortfilename, $longfilename, $extfilename],
1312 [$expected50short, $expected50withhash, $expected50extwithhash],
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
{
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;
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
{
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);
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.
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);
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);
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);
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);
1533 set_user_preference('', 1);
1534 $this->fail('Exception expected - invalid name accepted');
1535 } catch (\moodle_exception
$ex) {
1536 $this->assertInstanceOf('coding_exception', $ex);
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
{
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
{
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(
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>'
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>'
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>'
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>'
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>'
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>'
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>'
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>'
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>'
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>'
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>'
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(
1815 'usertimezone' => 'America/Moncton',
1822 'timezone' => '0.0',
1823 'applydst' => false, // No dst offset.
1824 'expectedoutput' => '1309514400' // 6pm at UTC+0.
1827 'usertimezone' => 'America/Moncton',
1834 'timezone' => '99', // User default timezone.
1835 'applydst' => false, // Don't apply dst.
1836 'expectedoutput' => '1309528800'
1839 'usertimezone' => 'America/Moncton',
1846 'timezone' => '99', // User default timezone.
1847 'applydst' => true, // Apply dst.
1848 'expectedoutput' => '1309525200'
1851 'usertimezone' => 'America/Moncton',
1858 'timezone' => 'America/Moncton', // String timezone.
1859 'applydst' => true, // Apply dst.
1860 'expectedoutput' => '1309525200'
1863 'usertimezone' => '2', // No dst applyed.
1870 'timezone' => '99', // Take user timezone.
1871 'applydst' => true, // Apply dst.
1872 'expectedoutput' => '1309507200'
1875 'usertimezone' => '-2', // No dst applyed.
1882 'timezone' => '99', // Take usertimezone.
1883 'applydst' => true, // Apply dst.
1884 'expectedoutput' => '1309521600'
1887 'usertimezone' => '-10', // No dst applyed.
1894 'timezone' => '2', // Take this timezone.
1895 'applydst' => true, // Apply dst.
1896 'expectedoutput' => '1309507200'
1899 'usertimezone' => '-10', // No dst applyed.
1906 'timezone' => '-2', // Take this timezone.
1907 'applydst' => true, // Apply dst.
1908 'expectedoutput' => '1309521600'
1911 'usertimezone' => '-10', // No dst applyed.
1918 'timezone' => 'random/time', // This should show server time.
1919 'applydst' => true, // Apply dst.
1920 'expectedoutput' => '1309485600'
1923 'usertimezone' => '-14', // Server time.
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(
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
1968 public function test_get_string(): void
{
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);
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
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
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
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
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', '>=')) ?
'\\' : '';
2118 {$leadingbackslash}core\lang_string::__set_state(array(
2119 'component' => 'moodle',
2122 'forcedstring' => false,
2123 'identifier' => 'no',
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',
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
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(' '));
2214 $this->assertEquals(5.4, unformat_float('5.4'));
2215 $this->assertEquals(5.4, unformat_float('5.4', true));
2218 $this->assertEquals(5.0, unformat_float('5'));
2220 // Custom number of decimal.
2221 $this->assertEquals(5.43267, unformat_float('5.43267'));
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'));
2231 $this->assertEquals(-100.0, unformat_float('-100'));
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'));
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
{
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();
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)));
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();
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);
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();
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
{
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),
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
{
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',
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 . '}$/',
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);
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();
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
{
2465 $this->resetAfterTest();
2466 $this->setTimezone(99, 'Australia/Perth');
2471 'str' => '%A, %d %B %Y, %I:%M %p',
2472 'expected' => 'Saturday, 01 January 2011, 06:00 PM'
2476 'str' => '%A, %d %B %Y, %I:%M %p',
2477 'expected' => 'Saturday, 01 January 2011, 10:00 AM'
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!
2492 'str' => 'Žluťoučký koníček %A',
2493 'expected' => 'Žluťoučký koníček Saturday'
2497 'str' => '言語設定言語 %A',
2498 'expected' => '言語設定言語 Saturday'
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
{
2520 $this->resetAfterTest();
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';
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);
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.
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.
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);
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.
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);
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
{
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%$',
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
= [
2738 $this->expectException(\coding_exception
::class);
2739 get_password_peppers();
2743 * Test function to validate password length.
2745 * @covers ::exceeds_password_length
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).
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.
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
{
2803 $this->resetAfterTest();
2806 $CFG->passwordpeppers
= [
2807 1 => '#GV]NLie|x$H9[$rW%94bXZvJHa%z',
2808 2 => '#GV]NLie|x$H9[$rW%94bXZvJHa%$'
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
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
{
2852 $this->resetAfterTest();
2853 $passwords = ['pw', 'abc123', 'C0mP1eX_&}<?@*&%` |\"', 'ĩńťėŕňăţĩōŋāĹ'];
2855 // Check that some passwords that we convert to hashes can
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
{
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
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();
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'.
2914 update_internal_user_password($user, 'newpassword');
2915 $this->assertDebuggingCalled('User record in update_internal_user_password() must include field auth',
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(
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).
2952 public static function update_internal_user_password_no_cache_provider(): array {
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
{
2981 $this->resetAfterTest();
2983 // Create a user to test the name display on.
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';
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
{
3150 $this->resetAfterTest();
3151 $user = $this->getDataGenerator()->create_user();
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();
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();
3198 $events = $sink->get_events();
3200 $event = array_pop($events);
3202 // Check if user is logged out.
3203 $this->assertFalse(isloggedin());
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() {
3217 'wwwroot' => 'http://www.example.com',
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>',
3224 'wwwroot' => 'http://www.example.com/path/subdir',
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
{
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
{
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
);
3279 * A data provider for testing email diversion
3281 public function diverted_emails_provider() {
3283 'nodiverts' => array(
3284 'divertallemailsto' => null,
3285 'divertallemailsexcept' => null,
3289 'fred.jones@example.com',
3292 'fred+verp@example.com',
3296 'alldiverts' => array(
3297 'divertallemailsto' => 'somewhere@elsewhere.com',
3298 'divertallemailsexcept' => null,
3302 'fred.jones@example.com',
3305 'fred+verp@example.com',
3309 'alsodiverts' => array(
3310 'divertallemailsto' => 'somewhere@elsewhere.com',
3311 'divertallemailsexcept' => '@dev.com, fred(\+.*)?@example.com',
3315 'fred.jones@example.com',
3316 'Fred.Jones@Example.com',
3320 'divertsexceptions' => array(
3321 'divertallemailsto' => 'somewhere@elsewhere.com',
3322 'divertallemailsexcept' => '@dev.com, fred(\+.*)?@example.com',
3327 'fred+verp@example.com',
3331 'divertsexceptionsnewline' => array(
3332 'divertallemailsto' => 'somewhere@elsewhere.com',
3333 'divertallemailsexcept' => "@dev.com\nfred(\+.*)?@example.com",
3337 'fred+verp@example.com',
3341 'alsodivertsnewline' => array(
3342 'divertallemailsto' => 'somewhere@elsewhere.com',
3343 'divertallemailsexcept' => "@dev.com\nfred(\+.*)?@example.com",
3347 'fred.jones@example.com',
3351 'alsodivertsblankline' => array(
3352 'divertallemailsto' => 'somewhere@elsewhere.com',
3353 'divertallemailsexcept' => "@dev.com\n",
3355 'lionel@example.com',
3359 'divertsexceptionblankline' => array(
3360 'divertallemailsto' => 'somewhere@elsewhere.com',
3361 'divertallemailsexcept' => "@example.com\n",
3363 'lionel@example.com',
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
{
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
{
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();
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);
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
);
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
);
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
);
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
);
3484 * Data provider for {@see test_email_to_user_attachment}
3488 public function email_to_user_attachment_provider(): array {
3491 // Return all paths that can be used to send attachments from.
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
{
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();
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);
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();
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
{
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();
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
);
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 {
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¶m=eters",
3671 "expected" => "http://moodle.org/ext.php?with=some¶m=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,
3696 "email" => 'test@example.com',
3700 send_confirmation_email($user, $confirmationurl);
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
{
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@_@-..-..",
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);
3733 $messages = $sink->get_messages();
3734 $message = array_shift($messages);
3735 $messagebody = quoted_printable_decode($message->body
);
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">
3869 // The counts here should match MS Word and Libre Office.
3872 [4, 'one two three four'],
3876 [2, 'one 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 '],
3884 [3, ' one & three '],
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>'],
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
],
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 {
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
{
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.
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() {
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() {
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;
4166 $this->getDataGenerator()->enrol_user($fromuser->id
, $course->id
, 'student');
4167 $this->getDataGenerator()->enrol_user($touser->id
, $course->id
, 'student');
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() {
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",
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",
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",
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",
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",
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",
4230 'email' => 'fromuser@EXAMPLE.com',
4231 'display' => \core_user
::MAILDISPLAY_EVERYONE
,
4232 'samecourse' => false,
4233 'config' => "example.com\r\ntest.com",
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",
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",
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",
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",
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",
4272 [ 'email' => 'fromuser@moodle.com',
4273 'display' => \core_user
::MAILDISPLAY_EVERYONE
,
4274 'samecourse' => false,
4275 'config' => " \n \n \n ",
4282 * Test that generate_email_processing_address() returns valid email address.
4284 public function test_generate_email_processing_address(): void
{
4286 $this->resetAfterTest();
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() {
4327 // Test allowed domain empty list.
4329 'email' => 'fromuser@example.com',
4333 // Test from email is in allowed domain.
4335 'email' => 'fromuser@example.com',
4336 'config' => 'example.com test.com',
4339 // Test from email is in allowed domain but uppercase config.
4341 'email' => 'fromuser@example.com',
4342 'config' => 'EXAMPLE.com test.com',
4345 // Test from email is in allowed domain but uppercase email.
4347 'email' => 'fromuser@EXAMPLE.com',
4348 'config' => 'example.com test.com',
4351 // Test from email is in allowed subdomain.
4353 'email' => 'fromuser@something.example.com',
4354 'config' => '.example.com test.com',
4357 // Test from email is in allowed subdomain but uppercase config.
4359 'email' => 'fromuser@something.example.com',
4360 'config' => '.EXAMPLE.com test.com',
4363 // Test from email is in allowed subdomain but uppercase email.
4365 'email' => 'fromuser@something.EXAMPLE.com',
4366 'config' => '.example.com test.com',
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() {
4406 // Test denied domain empty list.
4408 'email' => 'fromuser@example.com',
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',
4453 // Test from email is not in denied subdomain.
4454 [ 'email' => 'fromuser@something.example.com',
4455 'config' => 'example.com test.com',
4462 * Test safe method unserialize_array().
4464 public function test_unserialize_array(): void
{
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) [
4500 'innerobject' => (object) [
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
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
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
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
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
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.
4582 public function component_class_callback_default_provider() {
4585 'empty string' => [''],
4586 'string' => ['This is a string'],
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.
4598 public function component_class_callback_data_provider() {
4600 'empty string' => [''],
4601 'string' => ['This is a string'],
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.
4613 public function component_class_callback_multiple_params_provider() {
4623 'string values' => [
4632 [null, null, null, null],
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() {
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().
4700 public function user_data_provider() {
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' => [
4732 'Fetch data using a non-existent user ID' => [
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();
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') {
4770 // Test for fetching data using a valid user ID. Use the generated user's ID.
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);
4787 $this->assertEquals($user->id
, $fetcheduser->id
);
4788 $this->assertEquals($user->username
, $fetcheduser->username
);
4789 $this->assertEquals($user->email
, $fetcheduser->email
);
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();
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));
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() {
4837 'Time is after the reference time by 1 minute, omitted format' => [
4838 'time1' => 12345660,
4839 'time2' => 12345600,
4841 'expected' => '0d 0h 1m'
4843 'Time is before the reference time by 1 minute, omitted format' => [
4844 'time1' => 12345540,
4845 'time2' => 12345600,
4847 'expected' => '0d 0h 1m'
4849 'Time is equal to the reference time, omitted format' => [
4850 'time1' => 12345600,
4851 'time2' => 12345600,
4853 'expected' => '0d 0h 0m'
4855 'Time is after the reference time by 1 minute, empty string format' => [
4856 'time1' => 12345660,
4857 'time2' => 12345600,
4859 'expected' => '0d 0h 1m'
4861 'Time is before the reference time by 1 minute, empty string format' => [
4862 'time1' => 12345540,
4863 'time2' => 12345600,
4865 'expected' => '0d 0h 1m'
4867 'Time is equal to the reference time, empty string format' => [
4868 'time1' => 12345600,
4869 'time2' => 12345600,
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,
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,
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,
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,
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
{
4931 // Create a new file in dataroot.
4932 $file = $CFG->dataroot
. '/argh.txt';
4933 file_put_contents($file, 'Frogs');
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.
4952 * Tests the rename_to_unused_name function with a directory.
4954 public function test_rename_to_unused_name_dir(): void
{
4957 // Create a new directory in dataroot.
4958 $file = $CFG->dataroot
. '/arghdir';
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.
4978 * Tests the rename_to_unused_name function with error cases.
4980 public function test_rename_to_unused_name_failure(): void
{
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() {
4998 [1023, '1023 bytes'],
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'],
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 {
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 {
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.
5111 public function get_list_of_plugins_provider(): array {
5113 'Standard excludes' => [
5114 ['amdd', 'class', 'local', 'test'],
5130 'Standard excludes with addition' => [
5131 ['amdd', 'local', 'test'],
5147 'Files excluded' => [
5152 'abc' => 'File with filename abc',
5156 'example.txt' => 'In a directory called "def"',
5162 'Subdirectories only' => [
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(
5199 int|
string|
null $defaulthomepage = null,
5200 ?
int $enabledashboard = null,
5201 int|
string|
null $userpreference = null,
5202 ?
int $allowguestmymoodle = null,
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;
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.
5237 public static function get_home_page_provider(): array {
5241 'No logged user' => [
5242 'user' => 'nologged',
5243 'expected' => HOMEPAGE_SITE
,
5245 'Guest user. Dashboard set as default home page and enabled for guests' => [
5247 'expected' => HOMEPAGE_MY
,
5249 'Guest user. Dashboard set as default home page but disabled for guests' => [
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' => [
5259 'expected' => HOMEPAGE_SITE
,
5260 'defaulthomepage' => HOMEPAGE_MYCOURSES
,
5262 'Guest user. User preference set as default page' => [
5264 'expected' => HOMEPAGE_SITE
,
5265 'defaulthomepage' => HOMEPAGE_USER
,
5267 'Logged user. Dashboard set as default home page and enabled' => [
5269 'expected' => HOMEPAGE_MY
,
5271 'Logged user. Dashboard set as default home page but disabled' => [
5273 'expected' => HOMEPAGE_MYCOURSES
,
5274 'defaulthomepage' => HOMEPAGE_MY
,
5275 'enabledashboard' => 0,
5277 'Logged user. My courses set as default home page with dashboard enabled' => [
5279 'expected' => HOMEPAGE_MYCOURSES
,
5280 'defaulthomepage' => HOMEPAGE_MYCOURSES
,
5281 'enabledashboard' => 1,
5283 'Logged user. My courses set as default home page with dashboard disabled' => [
5285 'expected' => HOMEPAGE_MYCOURSES
,
5286 'defaulthomepage' => HOMEPAGE_MYCOURSES
,
5287 'enabledashboard' => 0,
5289 'Logged user. Site set as default home page with dashboard enabled' => [
5291 'expected' => HOMEPAGE_SITE
,
5292 'defaulthomepage' => HOMEPAGE_SITE
,
5293 'enabledashboard' => 1,
5295 'Logged user. Site set as default home page with dashboard disabled' => [
5297 'expected' => HOMEPAGE_SITE
,
5298 'defaulthomepage' => HOMEPAGE_SITE
,
5299 'enabledashboard' => 0,
5301 'Logged user. URL set as default home page.' => [
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' => [
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' => [
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' => [
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' => [
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.' => [
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
{
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
{
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());
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
{
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.
5415 'type' => 'phpunit',
5416 'resource' => 'lock1',
5421 'type' => 'phpunit',
5422 'resource' => 'lock2',
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];
5436 $this->assertMatchesRegularExpression('~Lock.*Waited.*Obtained.*Held~s', $rows[0]);
5437 // Check both locks.
5438 $this->assertMatchesRegularExpression('~phpunit/lock1.*0\.6.*✓.*6\.0~s', $rows[1]);
5439 $this->assertMatchesRegularExpression('~phpunit/lock2.*0\.9.*❌.*-~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
{
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 {
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',
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',
5497 'Proxybypass contains the last part of the URL 2' => [
5498 'http://store.mydomain.com',
5502 'Proxybypass contains part of the url' => [
5507 'Different IPs used in proxybypass' => [
5508 'http://192.168.5.5',
5512 'Proxybypass and URL matchs' => [
5513 'http://store.mydomain.com',
5514 'store.mydomain.com',
5517 'IP used in proxybypass' => [
5518 'http://192.168.5.5',
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();
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(
5563 moodle_array_keys_filter($array, $filter, $strict),
5568 * Data provider for moodle_array_keys_filter tests.
5572 public static function moodle_array_keys_filter_provider(): array {
5574 [['a', 'b', 'c'], 'b', false, [1]],
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();
5649 $this->assertStringContainsString('Hi ' . $user->firstname
, quoted_printable_decode($result[0]->body
));
5653 * Data provider for test_email_greetings tests.
5657 public static function email_greetings_provider(): array {
5658 $extrasendpasswordchangeconfirmationemail = new \
stdClass();
5659 $extrasendpasswordchangeconfirmationemail->token
= '123';
5662 ['send_password_change_info', null],
5663 ['send_confirmation_email', null],
5664 ['setnew_password_and_mail', false],
5665 ['send_password_change_confirmation_email', $extrasendpasswordchangeconfirmationemail],