Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
/**
18
 * moodlelib.php - Moodle main library
19
 *
20
 * Main library file of miscellaneous general-purpose Moodle functions.
21
 * Other main libraries:
22
 *  - weblib.php      - functions that produce web output
23
 *  - datalib.php     - functions that access the database
24
 *
25
 * @package    core
26
 * @subpackage lib
27
 * @copyright  1999 onwards Martin Dougiamas  http://dougiamas.com
28
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
29
 */
30
 
31
use core\di;
32
use core\hook;
33
 
34
defined('MOODLE_INTERNAL') || die();
35
 
36
// CONSTANTS (Encased in phpdoc proper comments).
37
 
38
// Date and time constants.
39
/**
40
 * Time constant - the number of seconds in a year
41
 */
42
define('YEARSECS', 31536000);
43
 
44
/**
45
 * Time constant - the number of seconds in a week
46
 */
47
define('WEEKSECS', 604800);
48
 
49
/**
50
 * Time constant - the number of seconds in a day
51
 */
52
define('DAYSECS', 86400);
53
 
54
/**
55
 * Time constant - the number of seconds in an hour
56
 */
57
define('HOURSECS', 3600);
58
 
59
/**
60
 * Time constant - the number of seconds in a minute
61
 */
62
define('MINSECS', 60);
63
 
64
/**
65
 * Time constant - the number of minutes in a day
66
 */
67
define('DAYMINS', 1440);
68
 
69
/**
70
 * Time constant - the number of minutes in an hour
71
 */
72
define('HOURMINS', 60);
73
 
74
// Parameter constants - every call to optional_param(), required_param()
75
// or clean_param() should have a specified type of parameter.
76
 
77
// We currently include \core\param manually here to avoid broken upgrades.
78
// This may change after the next LTS release as LTS releases require the previous LTS release.
79
require_once(__DIR__ . '/classes/deprecation.php');
80
require_once(__DIR__ . '/classes/param.php');
81
 
82
/**
83
 * PARAM_ALPHA - contains only English ascii letters [a-zA-Z].
84
 */
85
define('PARAM_ALPHA', \core\param::ALPHA->value);
86
 
87
/**
88
 * PARAM_ALPHAEXT the same contents as PARAM_ALPHA (English ascii letters [a-zA-Z]) plus the chars in quotes: "_-" allowed
89
 * NOTE: originally this allowed "/" too, please use PARAM_SAFEPATH if "/" needed
90
 */
91
define('PARAM_ALPHAEXT', \core\param::ALPHAEXT->value);
92
 
93
/**
94
 * PARAM_ALPHANUM - expected numbers 0-9 and English ascii letters [a-zA-Z] only.
95
 */
96
define('PARAM_ALPHANUM', \core\param::ALPHANUM->value);
97
 
98
/**
99
 * PARAM_ALPHANUMEXT - expected numbers 0-9, letters (English ascii letters [a-zA-Z]) and _- only.
100
 */
101
define('PARAM_ALPHANUMEXT', \core\param::ALPHANUMEXT->value);
102
 
103
/**
104
 * PARAM_AUTH - actually checks to make sure the string is a valid auth plugin
105
 */
106
define('PARAM_AUTH', \core\param::AUTH->value);
107
 
108
/**
109
 * PARAM_BASE64 - Base 64 encoded format
110
 */
111
define('PARAM_BASE64', \core\param::BASE64->value);
112
 
113
/**
114
 * PARAM_BOOL - converts input into 0 or 1, use for switches in forms and urls.
115
 */
116
define('PARAM_BOOL', \core\param::BOOL->value);
117
 
118
/**
119
 * PARAM_CAPABILITY - A capability name, like 'moodle/role:manage'. Actually
120
 * checked against the list of capabilities in the database.
121
 */
122
define('PARAM_CAPABILITY', \core\param::CAPABILITY->value);
123
 
124
/**
125
 * PARAM_CLEANHTML - cleans submitted HTML code. Note that you almost never want
126
 * to use this. The normal mode of operation is to use PARAM_RAW when receiving
127
 * the input (required/optional_param or formslib) and then sanitise the HTML
128
 * using format_text on output. This is for the rare cases when you want to
129
 * sanitise the HTML on input. This cleaning may also fix xhtml strictness.
130
 */
131
define('PARAM_CLEANHTML', \core\param::CLEANHTML->value);
132
 
133
/**
134
 * PARAM_EMAIL - an email address following the RFC
135
 */
136
define('PARAM_EMAIL', \core\param::EMAIL->value);
137
 
138
/**
139
 * PARAM_FILE - safe file name, all dangerous chars are stripped, protects against XSS, SQL injections and directory traversals
140
 */
141
define('PARAM_FILE', \core\param::FILE->value);
142
 
143
/**
144
 * PARAM_FLOAT - a real/floating point number.
145
 *
146
 * Note that you should not use PARAM_FLOAT for numbers typed in by the user.
147
 * It does not work for languages that use , as a decimal separator.
148
 * Use PARAM_LOCALISEDFLOAT instead.
149
 */
150
define('PARAM_FLOAT', \core\param::FLOAT->value);
151
 
152
/**
153
 * PARAM_LOCALISEDFLOAT - a localised real/floating point number.
154
 * This is preferred over PARAM_FLOAT for numbers typed in by the user.
155
 * Cleans localised numbers to computer readable numbers; false for invalid numbers.
156
 */
157
define('PARAM_LOCALISEDFLOAT', \core\param::LOCALISEDFLOAT->value);
158
 
159
/**
160
 * PARAM_HOST - expected fully qualified domain name (FQDN) or an IPv4 dotted quad (IP address)
161
 */
162
define('PARAM_HOST', \core\param::HOST->value);
163
 
164
/**
165
 * PARAM_INT - integers only, use when expecting only numbers.
166
 */
167
define('PARAM_INT', \core\param::INT->value);
168
 
169
/**
170
 * PARAM_LANG - checks to see if the string is a valid installed language in the current site.
171
 */
172
define('PARAM_LANG', \core\param::LANG->value);
173
 
174
/**
175
 * PARAM_LOCALURL - expected properly formatted URL as well as one that refers to the local server itself. (NOT orthogonal to the
176
 * others! Implies PARAM_URL!)
177
 */
178
define('PARAM_LOCALURL', \core\param::LOCALURL->value);
179
 
180
/**
181
 * PARAM_NOTAGS - all html tags are stripped from the text. Do not abuse this type.
182
 */
183
define('PARAM_NOTAGS', \core\param::NOTAGS->value);
184
 
185
/**
186
 * PARAM_PATH - safe relative path name, all dangerous chars are stripped, protects against XSS, SQL injections and directory
187
 * traversals note: the leading slash is not removed, window drive letter is not allowed
188
 */
189
define('PARAM_PATH', \core\param::PATH->value);
190
 
191
/**
192
 * PARAM_PEM - Privacy Enhanced Mail format
193
 */
194
define('PARAM_PEM', \core\param::PEM->value);
195
 
196
/**
197
 * PARAM_PERMISSION - A permission, one of CAP_INHERIT, CAP_ALLOW, CAP_PREVENT or CAP_PROHIBIT.
198
 */
199
define('PARAM_PERMISSION', \core\param::PERMISSION->value);
200
 
201
/**
202
 * PARAM_RAW specifies a parameter that is not cleaned/processed in any way except the discarding of the invalid utf-8 characters
203
 */
204
define('PARAM_RAW', \core\param::RAW->value);
205
 
206
/**
207
 * PARAM_RAW_TRIMMED like PARAM_RAW but leading and trailing whitespace is stripped.
208
 */
209
define('PARAM_RAW_TRIMMED', \core\param::RAW_TRIMMED->value);
210
 
211
/**
212
 * PARAM_SAFEDIR - safe directory name, suitable for include() and require()
213
 */
214
define('PARAM_SAFEDIR', \core\param::SAFEDIR->value);
215
 
216
/**
217
 * PARAM_SAFEPATH - several PARAM_SAFEDIR joined by "/", suitable for include() and require(), plugin paths
218
 * and other references to Moodle code files.
219
 *
220
 * This is NOT intended to be used for absolute paths or any user uploaded files.
221
 */
222
define('PARAM_SAFEPATH', \core\param::SAFEPATH->value);
223
 
224
/**
225
 * PARAM_SEQUENCE - expects a sequence of numbers like 8 to 1,5,6,4,6,8,9.  Numbers and comma only.
226
 */
227
define('PARAM_SEQUENCE', \core\param::SEQUENCE->value);
228
 
229
/**
230
 * PARAM_TAG - one tag (interests, blogs, etc.) - mostly international characters and space, <> not supported
231
 */
232
define('PARAM_TAG', \core\param::TAG->value);
233
 
234
/**
235
 * PARAM_TAGLIST - list of tags separated by commas (interests, blogs, etc.)
236
 */
237
define('PARAM_TAGLIST', \core\param::TAGLIST->value);
238
 
239
/**
240
 * PARAM_TEXT - general plain text compatible with multilang filter, no other html tags. Please note '<', or '>' are allowed here.
241
 */
242
define('PARAM_TEXT', \core\param::TEXT->value);
243
 
244
/**
245
 * PARAM_THEME - Checks to see if the string is a valid theme name in the current site
246
 */
247
define('PARAM_THEME', \core\param::THEME->value);
248
 
249
/**
250
 * PARAM_URL - expected properly formatted URL. Please note that domain part is required, http://localhost/ is not accepted but
251
 * http://localhost.localdomain/ is ok.
252
 */
253
define('PARAM_URL', \core\param::URL->value);
254
 
255
/**
256
 * PARAM_USERNAME - Clean username to only contains allowed characters. This is to be used ONLY when manually creating user
257
 * accounts, do NOT use when syncing with external systems!!
258
 */
259
define('PARAM_USERNAME', \core\param::USERNAME->value);
260
 
261
/**
262
 * PARAM_STRINGID - used to check if the given string is valid string identifier for get_string()
263
 */
264
define('PARAM_STRINGID', \core\param::STRINGID->value);
265
 
266
// DEPRECATED PARAM TYPES OR ALIASES - DO NOT USE FOR NEW CODE.
267
/**
268
 * PARAM_CLEAN - obsoleted, please use a more specific type of parameter.
269
 * It was one of the first types, that is why it is abused so much ;-)
270
 * @deprecated since 2.0
271
 */
272
define('PARAM_CLEAN', \core\param::CLEAN->value);
273
 
274
/**
275
 * PARAM_INTEGER - deprecated alias for PARAM_INT
276
 * @deprecated since 2.0
277
 */
278
define('PARAM_INTEGER', \core\param::INT->value);
279
 
280
/**
281
 * PARAM_NUMBER - deprecated alias of PARAM_FLOAT
282
 * @deprecated since 2.0
283
 */
284
define('PARAM_NUMBER', \core\param::FLOAT->value);
285
 
286
/**
287
 * PARAM_ACTION - deprecated alias for PARAM_ALPHANUMEXT, use for various actions in forms and urls
288
 * NOTE: originally alias for PARAM_APLHA
289
 * @deprecated since 2.0
290
 */
291
define('PARAM_ACTION', \core\param::ALPHANUMEXT->value);
292
 
293
/**
294
 * PARAM_FORMAT - deprecated alias for PARAM_ALPHANUMEXT, use for names of plugins, formats, etc.
295
 * NOTE: originally alias for PARAM_APLHA
296
 * @deprecated since 2.0
297
 */
298
define('PARAM_FORMAT', \core\param::ALPHANUMEXT->value);
299
 
300
/**
301
 * PARAM_MULTILANG - deprecated alias of PARAM_TEXT.
302
 * @deprecated since 2.0
303
 */
304
define('PARAM_MULTILANG', \core\param::TEXT->value);
305
 
306
/**
307
 * PARAM_TIMEZONE - expected timezone. Timezone can be int +-(0-13) or float +-(0.5-12.5) or
308
 * string separated by '/' and can have '-' &/ '_' (eg. America/North_Dakota/New_Salem
309
 * America/Port-au-Prince)
310
 */
311
define('PARAM_TIMEZONE', \core\param::TIMEZONE->value);
312
 
313
/**
314
 * PARAM_CLEANFILE - deprecated alias of PARAM_FILE; originally was removing regional chars too
315
 * @deprecated since 2.0
316
 */
317
define('PARAM_CLEANFILE', \core\param::CLEANFILE->value);
318
 
319
/**
320
 * PARAM_COMPONENT is used for full component names (aka frankenstyle) such as 'mod_forum', 'core_rating', 'auth_ldap'.
321
 * Short legacy subsystem names and module names are accepted too ex: 'forum', 'rating', 'user'.
322
 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
323
 * NOTE: numbers and underscores are strongly discouraged in plugin names!
324
 */
325
define('PARAM_COMPONENT', \core\param::COMPONENT->value);
326
 
327
/**
328
 * PARAM_AREA is a name of area used when addressing files, comments, ratings, etc.
329
 * It is usually used together with context id and component.
330
 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
331
 */
332
define('PARAM_AREA', \core\param::AREA->value);
333
 
334
/**
335
 * PARAM_PLUGIN is used for plugin names such as 'forum', 'glossary', 'ldap', 'paypal', 'completionstatus'.
336
 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
337
 * NOTE: numbers and underscores are strongly discouraged in plugin names! Underscores are forbidden in module names.
338
 */
339
define('PARAM_PLUGIN', \core\param::PLUGIN->value);
340
 
341
 
342
// Web Services.
343
 
344
/**
345
 * VALUE_REQUIRED - if the parameter is not supplied, there is an error
346
 */
347
define('VALUE_REQUIRED', 1);
348
 
349
/**
350
 * VALUE_OPTIONAL - if the parameter is not supplied, then the param has no value
351
 */
352
define('VALUE_OPTIONAL', 2);
353
 
354
/**
355
 * VALUE_DEFAULT - if the parameter is not supplied, then the default value is used
356
 */
357
define('VALUE_DEFAULT', 0);
358
 
359
/**
360
 * NULL_NOT_ALLOWED - the parameter can not be set to null in the database
361
 */
362
define('NULL_NOT_ALLOWED', false);
363
 
364
/**
365
 * NULL_ALLOWED - the parameter can be set to null in the database
366
 */
367
define('NULL_ALLOWED', true);
368
 
369
// Page types.
370
 
371
/**
372
 * PAGE_COURSE_VIEW is a definition of a page type. For more information on the page class see moodle/lib/pagelib.php.
373
 */
374
define('PAGE_COURSE_VIEW', 'course-view');
375
 
376
/** Get remote addr constant */
377
define('GETREMOTEADDR_SKIP_HTTP_CLIENT_IP', '1');
378
/** Get remote addr constant */
379
define('GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR', '2');
380
/**
381
 * GETREMOTEADDR_SKIP_DEFAULT defines the default behavior remote IP address validation.
382
 */
383
define('GETREMOTEADDR_SKIP_DEFAULT', GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR|GETREMOTEADDR_SKIP_HTTP_CLIENT_IP);
384
 
385
// Blog access level constant declaration.
386
define ('BLOG_USER_LEVEL', 1);
387
define ('BLOG_GROUP_LEVEL', 2);
388
define ('BLOG_COURSE_LEVEL', 3);
389
define ('BLOG_SITE_LEVEL', 4);
390
define ('BLOG_GLOBAL_LEVEL', 5);
391
 
392
 
393
// Tag constants.
394
/**
395
 * To prevent problems with multibytes strings,Flag updating in nav not working on the review page. this should not exceed the
396
 * length of "varchar(255) / 3 (bytes / utf-8 character) = 85".
397
 * TODO: this is not correct, varchar(255) are 255 unicode chars ;-)
398
 *
399
 * @todo define(TAG_MAX_LENGTH) this is not correct, varchar(255) are 255 unicode chars ;-)
400
 */
401
define('TAG_MAX_LENGTH', 50);
402
 
403
// Password policy constants.
404
define ('PASSWORD_LOWER', 'abcdefghijklmnopqrstuvwxyz');
405
define ('PASSWORD_UPPER', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
406
define ('PASSWORD_DIGITS', '0123456789');
407
define ('PASSWORD_NONALPHANUM', '.,;:!?_-+/*@#&$');
408
 
409
/**
410
 * Required password pepper entropy.
411
 */
412
define ('PEPPER_ENTROPY', 112);
413
 
414
// Feature constants.
415
// Used for plugin_supports() to report features that are, or are not, supported by a module.
416
 
417
/** True if module can provide a grade */
418
define('FEATURE_GRADE_HAS_GRADE', 'grade_has_grade');
419
/** True if module supports outcomes */
420
define('FEATURE_GRADE_OUTCOMES', 'outcomes');
421
/** True if module supports advanced grading methods */
422
define('FEATURE_ADVANCED_GRADING', 'grade_advanced_grading');
423
/** True if module controls the grade visibility over the gradebook */
424
define('FEATURE_CONTROLS_GRADE_VISIBILITY', 'controlsgradevisbility');
425
/** True if module supports plagiarism plugins */
426
define('FEATURE_PLAGIARISM', 'plagiarism');
427
 
428
/** True if module has code to track whether somebody viewed it */
429
define('FEATURE_COMPLETION_TRACKS_VIEWS', 'completion_tracks_views');
430
/** True if module has custom completion rules */
431
define('FEATURE_COMPLETION_HAS_RULES', 'completion_has_rules');
432
 
433
/** True if module has no 'view' page (like label) */
434
define('FEATURE_NO_VIEW_LINK', 'viewlink');
435
/** True (which is default) if the module wants support for setting the ID number for grade calculation purposes. */
436
define('FEATURE_IDNUMBER', 'idnumber');
437
/** True if module supports groups */
438
define('FEATURE_GROUPS', 'groups');
439
/** True if module supports groupings */
440
define('FEATURE_GROUPINGS', 'groupings');
441
/**
442
 * True if module supports groupmembersonly (which no longer exists)
443
 * @deprecated Since Moodle 2.8
444
 */
445
define('FEATURE_GROUPMEMBERSONLY', 'groupmembersonly');
446
 
447
/** Type of module */
448
define('FEATURE_MOD_ARCHETYPE', 'mod_archetype');
449
/** True if module supports intro editor */
450
define('FEATURE_MOD_INTRO', 'mod_intro');
451
/** True if module has default completion */
452
define('FEATURE_MODEDIT_DEFAULT_COMPLETION', 'modedit_default_completion');
453
 
454
define('FEATURE_COMMENT', 'comment');
455
 
456
define('FEATURE_RATE', 'rate');
457
/** True if module supports backup/restore of moodle2 format */
458
define('FEATURE_BACKUP_MOODLE2', 'backup_moodle2');
459
 
460
/** True if module can show description on course main page */
461
define('FEATURE_SHOW_DESCRIPTION', 'showdescription');
462
 
463
/** True if module uses the question bank */
464
define('FEATURE_USES_QUESTIONS', 'usesquestions');
465
 
466
/**
467
 * Maximum filename char size
468
 */
469
define('MAX_FILENAME_SIZE', 100);
470
 
471
/** Unspecified module archetype */
472
define('MOD_ARCHETYPE_OTHER', 0);
473
/** Resource-like type module */
474
define('MOD_ARCHETYPE_RESOURCE', 1);
475
/** Assignment module archetype */
476
define('MOD_ARCHETYPE_ASSIGNMENT', 2);
477
/** System (not user-addable) module archetype */
478
define('MOD_ARCHETYPE_SYSTEM', 3);
479
 
480
/** Type of module */
481
define('FEATURE_MOD_PURPOSE', 'mod_purpose');
482
/** Module purpose administration */
483
define('MOD_PURPOSE_ADMINISTRATION', 'administration');
484
/** Module purpose assessment */
485
define('MOD_PURPOSE_ASSESSMENT', 'assessment');
486
/** Module purpose communication */
487
define('MOD_PURPOSE_COLLABORATION', 'collaboration');
488
/** Module purpose communication */
489
define('MOD_PURPOSE_COMMUNICATION', 'communication');
490
/** Module purpose content */
491
define('MOD_PURPOSE_CONTENT', 'content');
492
/** Module purpose interactive content */
493
define('MOD_PURPOSE_INTERACTIVECONTENT', 'interactivecontent');
494
/** Module purpose other */
495
define('MOD_PURPOSE_OTHER', 'other');
496
/**
497
 * Module purpose interface
498
 * @deprecated since Moodle 4.4
499
 * @todo MDL-80701 Remove in Moodle 4.8
500
*/
501
define('MOD_PURPOSE_INTERFACE', 'interface');
502
 
503
/**
504
 * Security token used for allowing access
505
 * from external application such as web services.
506
 * Scripts do not use any session, performance is relatively
507
 * low because we need to load access info in each request.
508
 * Scripts are executed in parallel.
509
 */
510
define('EXTERNAL_TOKEN_PERMANENT', 0);
511
 
512
/**
513
 * Security token used for allowing access
514
 * of embedded applications, the code is executed in the
515
 * active user session. Token is invalidated after user logs out.
516
 * Scripts are executed serially - normal session locking is used.
517
 */
518
define('EXTERNAL_TOKEN_EMBEDDED', 1);
519
 
520
/**
521
 * The home page should be the site home
522
 */
523
define('HOMEPAGE_SITE', 0);
524
/**
525
 * The home page should be the users my page
526
 */
527
define('HOMEPAGE_MY', 1);
528
/**
529
 * The home page can be chosen by the user
530
 */
531
define('HOMEPAGE_USER', 2);
532
/**
533
 * The home page should be the users my courses page
534
 */
535
define('HOMEPAGE_MYCOURSES', 3);
536
 
537
/**
538
 * URL of the Moodle sites registration portal.
539
 */
540
defined('HUB_MOODLEORGHUBURL') || define('HUB_MOODLEORGHUBURL', 'https://stats.moodle.org');
541
 
542
/**
543
 * URL of main Moodle site for marketing, products and services.
544
 */
545
defined('MOODLE_PRODUCTURL') || define('MOODLE_PRODUCTURL', 'https://moodle.com');
546
 
547
/**
548
 * URL of the statistic server public key.
549
 */
550
defined('HUB_STATSPUBLICKEY') || define('HUB_STATSPUBLICKEY', 'https://moodle.org/static/statspubkey.pem');
551
 
552
/**
553
 * Moodle mobile app service name
554
 */
555
define('MOODLE_OFFICIAL_MOBILE_SERVICE', 'moodle_mobile_app');
556
 
557
/**
558
 * Indicates the user has the capabilities required to ignore activity and course file size restrictions
559
 */
560
define('USER_CAN_IGNORE_FILE_SIZE_LIMITS', -1);
561
 
562
/**
563
 * Course display settings: display all sections on one page.
564
 */
565
define('COURSE_DISPLAY_SINGLEPAGE', 0);
566
/**
567
 * Course display settings: split pages into a page per section.
568
 */
569
define('COURSE_DISPLAY_MULTIPAGE', 1);
570
 
571
/**
572
 * Authentication constant: String used in password field when password is not stored.
573
 */
574
define('AUTH_PASSWORD_NOT_CACHED', 'not cached');
575
 
576
/**
577
 * Email from header to never include via information.
578
 */
579
define('EMAIL_VIA_NEVER', 0);
580
 
581
/**
582
 * Email from header to always include via information.
583
 */
584
define('EMAIL_VIA_ALWAYS', 1);
585
 
586
/**
587
 * Email from header to only include via information if the address is no-reply.
588
 */
589
define('EMAIL_VIA_NO_REPLY_ONLY', 2);
590
 
591
/**
592
 * Contact site support form/link disabled.
593
 */
594
define('CONTACT_SUPPORT_DISABLED', 0);
595
 
596
/**
597
 * Contact site support form/link only available to authenticated users.
598
 */
599
define('CONTACT_SUPPORT_AUTHENTICATED', 1);
600
 
601
/**
602
 * Contact site support form/link available to anyone visiting the site.
603
 */
604
define('CONTACT_SUPPORT_ANYONE', 2);
605
 
606
/**
607
 * Maximum number of characters for password.
608
 */
609
define('MAX_PASSWORD_CHARACTERS', 128);
610
 
611
/**
612
 * Toggle sensitive feature is disabled. Used for sensitive inputs (passwords, tokens, keys).
613
 */
614
define('TOGGLE_SENSITIVE_DISABLED', 0);
615
 
616
/**
617
 * Toggle sensitive feature is enabled. Used for sensitive inputs (passwords, tokens, keys).
618
 */
619
define('TOGGLE_SENSITIVE_ENABLED', 1);
620
 
621
/**
622
 * Toggle sensitive feature is enabled for small screens only. Used for sensitive inputs (passwords, tokens, keys).
623
 */
624
define('TOGGLE_SENSITIVE_SMALL_SCREENS_ONLY', 2);
625
 
626
// PARAMETER HANDLING.
627
 
628
/**
629
 * Returns a particular value for the named variable, taken from
630
 * POST or GET.  If the parameter doesn't exist then an error is
631
 * thrown because we require this variable.
632
 *
633
 * This function should be used to initialise all required values
634
 * in a script that are based on parameters.  Usually it will be
635
 * used like this:
636
 *    $id = required_param('id', PARAM_INT);
637
 *
638
 * Please note the $type parameter is now required and the value can not be array.
639
 *
640
 * @param string $parname the name of the page parameter we want
641
 * @param string $type expected type of parameter
642
 * @return mixed
643
 * @throws coding_exception
644
 */
645
function required_param($parname, $type) {
646
    return \core\param::from_type($type)->required_param($parname);
647
}
648
 
649
/**
650
 * Returns a particular array value for the named variable, taken from
651
 * POST or GET.  If the parameter doesn't exist then an error is
652
 * thrown because we require this variable.
653
 *
654
 * This function should be used to initialise all required values
655
 * in a script that are based on parameters.  Usually it will be
656
 * used like this:
657
 *    $ids = required_param_array('ids', PARAM_INT);
658
 *
659
 *  Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
660
 *
661
 * @param string $parname the name of the page parameter we want
662
 * @param string $type expected type of parameter
663
 * @return array
664
 * @throws coding_exception
665
 */
666
function required_param_array($parname, $type) {
667
    return \core\param::from_type($type)->required_param_array($parname);
668
}
669
 
670
/**
671
 * Returns a particular value for the named variable, taken from
672
 * POST or GET, otherwise returning a given default.
673
 *
674
 * This function should be used to initialise all optional values
675
 * in a script that are based on parameters.  Usually it will be
676
 * used like this:
677
 *    $name = optional_param('name', 'Fred', PARAM_TEXT);
678
 *
679
 * Please note the $type parameter is now required and the value can not be array.
680
 *
681
 * @param string $parname the name of the page parameter we want
682
 * @param mixed  $default the default value to return if nothing is found
683
 * @param string $type expected type of parameter
684
 * @return mixed
685
 * @throws coding_exception
686
 */
687
function optional_param($parname, $default, $type) {
688
    return \core\param::from_type($type)->optional_param(
689
        paramname: $parname,
690
        default: $default,
691
    );
692
}
693
 
694
/**
695
 * Returns a particular array value for the named variable, taken from
696
 * POST or GET, otherwise returning a given default.
697
 *
698
 * This function should be used to initialise all optional values
699
 * in a script that are based on parameters.  Usually it will be
700
 * used like this:
701
 *    $ids = optional_param('id', array(), PARAM_INT);
702
 *
703
 * Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
704
 *
705
 * @param string $parname the name of the page parameter we want
706
 * @param mixed $default the default value to return if nothing is found
707
 * @param string $type expected type of parameter
708
 * @return array
709
 * @throws coding_exception
710
 */
711
function optional_param_array($parname, $default, $type) {
712
    return \core\param::from_type($type)->optional_param_array(
713
        paramname: $parname,
714
        default: $default,
715
    );
716
}
717
 
718
/**
719
 * Strict validation of parameter values, the values are only converted
720
 * to requested PHP type. Internally it is using clean_param, the values
721
 * before and after cleaning must be equal - otherwise
722
 * an invalid_parameter_exception is thrown.
723
 * Objects and classes are not accepted.
724
 *
725
 * @param mixed $param
726
 * @param string $type PARAM_ constant
727
 * @param bool $allownull are nulls valid value?
728
 * @param string $debuginfo optional debug information
729
 * @return mixed the $param value converted to PHP type
730
 * @throws invalid_parameter_exception if $param is not of given type
731
 */
732
function validate_param($param, $type, $allownull = NULL_NOT_ALLOWED, $debuginfo = '') {
733
    return \core\param::from_type($type)->validate_param(
734
        param: $param,
735
        allownull: $allownull,
736
        debuginfo: $debuginfo,
737
    );
738
}
739
 
740
/**
741
 * Makes sure array contains only the allowed types, this function does not validate array key names!
742
 *
743
 * <code>
744
 * $options = clean_param($options, PARAM_INT);
745
 * </code>
746
 *
747
 * @param array|null $param the variable array we are cleaning
748
 * @param string $type expected format of param after cleaning.
749
 * @param bool $recursive clean recursive arrays
750
 * @return array
751
 * @throws coding_exception
752
 */
753
function clean_param_array(?array $param, $type, $recursive = false) {
754
    return \core\param::from_type($type)->clean_param_array(
755
        param: $param,
756
        recursive: $recursive,
757
    );
758
}
759
 
760
/**
761
 * Used by {@link optional_param()} and {@link required_param()} to
762
 * clean the variables and/or cast to specific types, based on
763
 * an options field.
764
 * <code>
765
 * $course->format = clean_param($course->format, PARAM_ALPHA);
766
 * $selectedgradeitem = clean_param($selectedgradeitem, PARAM_INT);
767
 * </code>
768
 *
769
 * @param mixed $param the variable we are cleaning
770
 * @param string $type expected format of param after cleaning.
771
 * @return mixed
772
 * @throws coding_exception
773
 */
774
function clean_param($param, $type) {
775
    return \core\param::from_type($type)->clean($param);
776
}
777
 
778
/**
779
 * Whether the PARAM_* type is compatible in RTL.
780
 *
781
 * Being compatible with RTL means that the data they contain can flow
782
 * from right-to-left or left-to-right without compromising the user experience.
783
 *
784
 * Take URLs for example, they are not RTL compatible as they should always
785
 * flow from the left to the right. This also applies to numbers, email addresses,
786
 * configuration snippets, base64 strings, etc...
787
 *
788
 * This function tries to best guess which parameters can contain localised strings.
789
 *
790
 * @param string $paramtype Constant PARAM_*.
791
 * @return bool
792
 */
793
function is_rtl_compatible($paramtype) {
794
    return $paramtype == PARAM_TEXT || $paramtype == PARAM_NOTAGS;
795
}
796
 
797
/**
798
 * Makes sure the data is using valid utf8, invalid characters are discarded.
799
 *
800
 * Note: this function is not intended for full objects with methods and private properties.
801
 *
802
 * @param mixed $value
803
 * @return mixed with proper utf-8 encoding
804
 */
805
function fix_utf8($value) {
806
    if (is_null($value) or $value === '') {
807
        return $value;
808
 
809
    } else if (is_string($value)) {
810
        if ((string)(int)$value === $value) {
811
            // Shortcut.
812
            return $value;
813
        }
814
 
815
        // Remove null bytes or invalid Unicode sequences from value.
816
        $value = str_replace(["\0", "\xef\xbf\xbe", "\xef\xbf\xbf"], '', $value);
817
 
818
        // Note: this duplicates min_fix_utf8() intentionally.
819
        static $buggyiconv = null;
820
        if ($buggyiconv === null) {
821
            $buggyiconv = (!function_exists('iconv') or @iconv('UTF-8', 'UTF-8//IGNORE', '100'.chr(130).'€') !== '100€');
822
        }
823
 
824
        if ($buggyiconv) {
825
            if (function_exists('mb_convert_encoding')) {
826
                $subst = mb_substitute_character();
827
                mb_substitute_character('none');
828
                $result = mb_convert_encoding($value, 'utf-8', 'utf-8');
829
                mb_substitute_character($subst);
830
 
831
            } else {
832
                // Warn admins on admin/index.php page.
833
                $result = $value;
834
            }
835
 
836
        } else {
837
            $result = @iconv('UTF-8', 'UTF-8//IGNORE', $value);
838
        }
839
 
840
        return $result;
841
 
842
    } else if (is_array($value)) {
843
        foreach ($value as $k => $v) {
844
            $value[$k] = fix_utf8($v);
845
        }
846
        return $value;
847
 
848
    } else if (is_object($value)) {
849
        // Do not modify original.
850
        $value = clone($value);
851
        foreach ($value as $k => $v) {
852
            $value->$k = fix_utf8($v);
853
        }
854
        return $value;
855
 
856
    } else {
857
        // This is some other type, no utf-8 here.
858
        return $value;
859
    }
860
}
861
 
862
/**
863
 * Return true if given value is integer or string with integer value
864
 *
865
 * @param mixed $value String or Int
866
 * @return bool true if number, false if not
867
 */
868
function is_number($value) {
869
    if (is_int($value)) {
870
        return true;
871
    } else if (is_string($value)) {
872
        return ((string)(int)$value) === $value;
873
    } else {
874
        return false;
875
    }
876
}
877
 
878
/**
879
 * Returns host part from url.
880
 *
881
 * @param string $url full url
882
 * @return string host, null if not found
883
 */
884
function get_host_from_url($url) {
885
    preg_match('|^[a-z]+://([a-zA-Z0-9-.]+)|i', $url, $matches);
886
    if ($matches) {
887
        return $matches[1];
888
    }
889
    return null;
890
}
891
 
892
/**
893
 * Tests whether anything was returned by text editor
894
 *
895
 * This function is useful for testing whether something you got back from
896
 * the HTML editor actually contains anything. Sometimes the HTML editor
897
 * appear to be empty, but actually you get back a <br> tag or something.
898
 *
899
 * @param string $string a string containing HTML.
900
 * @return boolean does the string contain any actual content - that is text,
901
 * images, objects, etc.
902
 */
903
function html_is_blank($string) {
904
    return trim(strip_tags((string)$string, '<img><object><applet><input><select><textarea><hr>')) == '';
905
}
906
 
907
/**
908
 * Set a key in global configuration
909
 *
910
 * Set a key/value pair in both this session's {@link $CFG} global variable
911
 * and in the 'config' database table for future sessions.
912
 *
913
 * Can also be used to update keys for plugin-scoped configs in config_plugin table.
914
 * In that case it doesn't affect $CFG.
915
 *
916
 * A NULL value will delete the entry.
917
 *
918
 * NOTE: this function is called from lib/db/upgrade.php
919
 *
920
 * @param string $name the key to set
921
 * @param string|int|bool|null $value the value to set (without magic quotes),
922
 *               null to unset the value
923
 * @param string $plugin (optional) the plugin scope, default null
924
 * @return bool true or exception
925
 */
926
function set_config($name, $value, $plugin = null) {
927
    global $CFG, $DB;
928
 
929
    // Redirect to appropriate handler when value is null.
930
    if ($value === null) {
931
        return unset_config($name, $plugin);
932
    }
933
 
934
    // Set variables determining conditions and where to store the new config.
935
    // Plugin config goes to {config_plugins}, core config goes to {config}.
936
    $iscore = empty($plugin);
937
    if ($iscore) {
938
        // If it's for core config.
939
        $table = 'config';
940
        $conditions = ['name' => $name];
941
        $invalidatecachekey = 'core';
942
    } else {
943
        // If it's a plugin.
944
        $table = 'config_plugins';
945
        $conditions = ['name' => $name, 'plugin' => $plugin];
946
        $invalidatecachekey = $plugin;
947
    }
948
 
949
    // DB handling - checks for existing config, updating or inserting only if necessary.
950
    $invalidatecache = true;
951
    $inserted = false;
952
    $record = $DB->get_record($table, $conditions, 'id, value');
953
    if ($record === false) {
954
        // Inserts a new config record.
955
        $config = new stdClass();
956
        $config->name  = $name;
957
        $config->value = $value;
958
        if (!$iscore) {
959
            $config->plugin = $plugin;
960
        }
961
        $inserted = $DB->insert_record($table, $config, false);
962
    } else if ($invalidatecache = ($record->value !== $value)) {
963
        // Record exists - Check and only set new value if it has changed.
964
        $DB->set_field($table, 'value', $value, ['id' => $record->id]);
965
    }
966
 
967
    if ($iscore && !isset($CFG->config_php_settings[$name])) {
968
        // So it's defined for this invocation at least.
969
        // Settings from db are always strings.
970
        $CFG->$name = (string) $value;
971
    }
972
 
973
    // When setting config during a Behat test (in the CLI script, not in the web browser
974
    // requests), remember which ones are set so that we can clear them later.
975
    if ($iscore && $inserted && defined('BEHAT_TEST')) {
976
        $CFG->behat_cli_added_config[$name] = true;
977
    }
978
 
979
    // Update siteidentifier cache, if required.
980
    if ($iscore && $name === 'siteidentifier') {
981
        cache_helper::update_site_identifier($value);
982
    }
983
 
984
    // Invalidate cache, if required.
985
    if ($invalidatecache) {
986
        cache_helper::invalidate_by_definition('core', 'config', [], $invalidatecachekey);
987
    }
988
 
989
    return true;
990
}
991
 
992
/**
993
 * Get configuration values from the global config table
994
 * or the config_plugins table.
995
 *
996
 * If called with one parameter, it will load all the config
997
 * variables for one plugin, and return them as an object.
998
 *
999
 * If called with 2 parameters it will return a string single
1000
 * value or false if the value is not found.
1001
 *
1002
 * NOTE: this function is called from lib/db/upgrade.php
1003
 *
1004
 * @param string $plugin full component name
1005
 * @param string $name default null
1006
 * @return mixed hash-like object or single value, return false no config found
1007
 * @throws dml_exception
1008
 */
1009
function get_config($plugin, $name = null) {
1010
    global $CFG, $DB;
1011
 
1012
    if ($plugin === 'moodle' || $plugin === 'core' || empty($plugin)) {
1013
        $forced =& $CFG->config_php_settings;
1014
        $iscore = true;
1015
        $plugin = 'core';
1016
    } else {
1017
        if (array_key_exists($plugin, $CFG->forced_plugin_settings)) {
1018
            $forced =& $CFG->forced_plugin_settings[$plugin];
1019
        } else {
1020
            $forced = array();
1021
        }
1022
        $iscore = false;
1023
    }
1024
 
1025
    if (!isset($CFG->siteidentifier)) {
1026
        try {
1027
            // This may throw an exception during installation, which is how we detect the
1028
            // need to install the database. For more details see {@see initialise_cfg()}.
1029
            $CFG->siteidentifier = $DB->get_field('config', 'value', array('name' => 'siteidentifier'));
1030
        } catch (dml_exception $ex) {
1031
            // Set siteidentifier to false. We don't want to trip this continually.
1032
            $siteidentifier = false;
1033
            throw $ex;
1034
        }
1035
    }
1036
 
1037
    if (!empty($name)) {
1038
        if (array_key_exists($name, $forced)) {
1039
            return (string)$forced[$name];
1040
        } else if ($name === 'siteidentifier' && $plugin == 'core') {
1041
            return $CFG->siteidentifier;
1042
        }
1043
    }
1044
 
1045
    $cache = cache::make('core', 'config');
1046
    $result = $cache->get($plugin);
1047
    if ($result === false) {
1048
        // The user is after a recordset.
1049
        if (!$iscore) {
1050
            $result = $DB->get_records_menu('config_plugins', array('plugin' => $plugin), '', 'name,value');
1051
        } else {
1052
            // This part is not really used any more, but anyway...
1053
            $result = $DB->get_records_menu('config', array(), '', 'name,value');;
1054
        }
1055
        $cache->set($plugin, $result);
1056
    }
1057
 
1058
    if (!empty($name)) {
1059
        if (array_key_exists($name, $result)) {
1060
            return $result[$name];
1061
        }
1062
        return false;
1063
    }
1064
 
1065
    if ($plugin === 'core') {
1066
        $result['siteidentifier'] = $CFG->siteidentifier;
1067
    }
1068
 
1069
    foreach ($forced as $key => $value) {
1070
        if (is_null($value) or is_array($value) or is_object($value)) {
1071
            // We do not want any extra mess here, just real settings that could be saved in db.
1072
            unset($result[$key]);
1073
        } else {
1074
            // Convert to string as if it went through the DB.
1075
            $result[$key] = (string)$value;
1076
        }
1077
    }
1078
 
1079
    return (object)$result;
1080
}
1081
 
1082
/**
1083
 * Removes a key from global configuration.
1084
 *
1085
 * NOTE: this function is called from lib/db/upgrade.php
1086
 *
1087
 * @param string $name the key to set
1088
 * @param string $plugin (optional) the plugin scope
1089
 * @return boolean whether the operation succeeded.
1090
 */
1091
function unset_config($name, $plugin=null) {
1092
    global $CFG, $DB;
1093
 
1094
    if (empty($plugin)) {
1095
        unset($CFG->$name);
1096
        $DB->delete_records('config', array('name' => $name));
1097
        cache_helper::invalidate_by_definition('core', 'config', array(), 'core');
1098
    } else {
1099
        $DB->delete_records('config_plugins', array('name' => $name, 'plugin' => $plugin));
1100
        cache_helper::invalidate_by_definition('core', 'config', array(), $plugin);
1101
    }
1102
 
1103
    return true;
1104
}
1105
 
1106
/**
1107
 * Remove all the config variables for a given plugin.
1108
 *
1109
 * NOTE: this function is called from lib/db/upgrade.php
1110
 *
1111
 * @param string $plugin a plugin, for example 'quiz' or 'qtype_multichoice';
1112
 * @return boolean whether the operation succeeded.
1113
 */
1114
function unset_all_config_for_plugin($plugin) {
1115
    global $DB;
1116
    // Delete from the obvious config_plugins first.
1117
    $DB->delete_records('config_plugins', array('plugin' => $plugin));
1118
    // Next delete any suspect settings from config.
1119
    $like = $DB->sql_like('name', '?', true, true, false, '|');
1120
    $params = array($DB->sql_like_escape($plugin.'_', '|') . '%');
1121
    $DB->delete_records_select('config', $like, $params);
1122
    // Finally clear both the plugin cache and the core cache (suspect settings now removed from core).
1123
    cache_helper::invalidate_by_definition('core', 'config', array(), array('core', $plugin));
1124
 
1125
    return true;
1126
}
1127
 
1128
/**
1129
 * Use this function to get a list of users from a config setting of type admin_setting_users_with_capability.
1130
 *
1131
 * All users are verified if they still have the necessary capability.
1132
 *
1133
 * @param string $value the value of the config setting.
1134
 * @param string $capability the capability - must match the one passed to the admin_setting_users_with_capability constructor.
1135
 * @param bool $includeadmins include administrators.
1136
 * @return array of user objects.
1137
 */
1138
function get_users_from_config($value, $capability, $includeadmins = true) {
1139
    if (empty($value) or $value === '$@NONE@$') {
1140
        return array();
1141
    }
1142
 
1143
    // We have to make sure that users still have the necessary capability,
1144
    // it should be faster to fetch them all first and then test if they are present
1145
    // instead of validating them one-by-one.
1146
    $users = get_users_by_capability(context_system::instance(), $capability);
1147
    if ($includeadmins) {
1148
        $admins = get_admins();
1149
        foreach ($admins as $admin) {
1150
            $users[$admin->id] = $admin;
1151
        }
1152
    }
1153
 
1154
    if ($value === '$@ALL@$') {
1155
        return $users;
1156
    }
1157
 
1158
    $result = array(); // Result in correct order.
1159
    $allowed = explode(',', $value);
1160
    foreach ($allowed as $uid) {
1161
        if (isset($users[$uid])) {
1162
            $user = $users[$uid];
1163
            $result[$user->id] = $user;
1164
        }
1165
    }
1166
 
1167
    return $result;
1168
}
1169
 
1170
 
1171
/**
1172
 * Invalidates browser caches and cached data in temp.
1173
 *
1174
 * @return void
1175
 */
1176
function purge_all_caches() {
1177
    purge_caches();
1178
}
1179
 
1180
/**
1181
 * Selectively invalidate different types of cache.
1182
 *
1183
 * Purges the cache areas specified.  By default, this will purge all caches but can selectively purge specific
1184
 * areas alone or in combination.
1185
 *
1186
 * @param bool[] $options Specific parts of the cache to purge. Valid options are:
1187
 *        'muc'    Purge MUC caches?
11 efrain 1188
 *        'courses' Purge all course caches, or specific course caches (CLI only)
1 efrain 1189
 *        'theme'  Purge theme cache?
1190
 *        'lang'   Purge language string cache?
1191
 *        'js'     Purge javascript cache?
1192
 *        'filter' Purge text filter cache?
1193
 *        'other'  Purge all other caches?
1194
 */
1195
function purge_caches($options = []) {
1196
    $defaults = array_fill_keys(['muc', 'courses', 'theme', 'lang', 'js', 'template', 'filter', 'other'], false);
1197
    if (empty(array_filter($options))) {
1198
        $options = array_fill_keys(array_keys($defaults), true); // Set all options to true.
1199
    } else {
1200
        $options = array_merge($defaults, array_intersect_key($options, $defaults)); // Override defaults with specified options.
1201
    }
1202
    if ($options['muc']) {
1203
        cache_helper::purge_all();
11 efrain 1204
    } else if ($options['courses']) {
1 efrain 1205
        if ($options['courses'] === true) {
1206
            $courseids = [];
1207
        } else {
1208
            $courseids = preg_split('/\s*,\s*/', $options['courses'], -1, PREG_SPLIT_NO_EMPTY);
1209
        }
1210
        course_modinfo::purge_course_caches($courseids);
1211
    }
1212
    if ($options['theme']) {
1213
        theme_reset_all_caches();
1214
    }
1215
    if ($options['lang']) {
1216
        get_string_manager()->reset_caches();
1217
    }
1218
    if ($options['js']) {
1219
        js_reset_all_caches();
1220
    }
1221
    if ($options['template']) {
1222
        template_reset_all_caches();
1223
    }
1224
    if ($options['filter']) {
1225
        reset_text_filters_cache();
1226
    }
1227
    if ($options['other']) {
1228
        purge_other_caches();
1229
    }
1230
}
1231
 
1232
/**
1233
 * Purge all non-MUC caches not otherwise purged in purge_caches.
1234
 *
1235
 * IMPORTANT - If you are adding anything here to do with the cache directory you should also have a look at
1236
 * {@link phpunit_util::reset_dataroot()}
1237
 */
1238
function purge_other_caches() {
1239
    global $DB, $CFG;
1240
    if (class_exists('core_plugin_manager')) {
1241
        core_plugin_manager::reset_caches();
1242
    }
1243
 
1244
    // Bump up cacherev field for all courses.
1245
    try {
1246
        increment_revision_number('course', 'cacherev', '');
1247
    } catch (moodle_exception $e) {
1248
        // Ignore exception since this function is also called before upgrade script when field course.cacherev does not exist yet.
1249
    }
1250
 
1251
    $DB->reset_caches();
1252
 
1253
    // Purge all other caches: rss, simplepie, etc.
1254
    clearstatcache();
1255
    remove_dir($CFG->cachedir.'', true);
1256
 
1257
    // Make sure cache dir is writable, throws exception if not.
1258
    make_cache_directory('');
1259
 
1260
    // This is the only place where we purge local caches, we are only adding files there.
1261
    // The $CFG->localcachedirpurged flag forces local directories to be purged on cluster nodes.
1262
    remove_dir($CFG->localcachedir, true);
1263
    set_config('localcachedirpurged', time());
1264
    make_localcache_directory('', true);
11 efrain 1265
 
1266
    // Rewarm the bootstrap.php files so the siteid is always present after a purge.
1267
    initialise_local_config_cache();
1 efrain 1268
    \core\task\manager::clear_static_caches();
1269
}
1270
 
1271
/**
1272
 * Get volatile flags
1273
 *
1274
 * @param string $type
1275
 * @param int $changedsince default null
1276
 * @return array records array
1277
 */
1278
function get_cache_flags($type, $changedsince = null) {
1279
    global $DB;
1280
 
1281
    $params = array('type' => $type, 'expiry' => time());
1282
    $sqlwhere = "flagtype = :type AND expiry >= :expiry";
1283
    if ($changedsince !== null) {
1284
        $params['changedsince'] = $changedsince;
1285
        $sqlwhere .= " AND timemodified > :changedsince";
1286
    }
1287
    $cf = array();
1288
    if ($flags = $DB->get_records_select('cache_flags', $sqlwhere, $params, '', 'name,value')) {
1289
        foreach ($flags as $flag) {
1290
            $cf[$flag->name] = $flag->value;
1291
        }
1292
    }
1293
    return $cf;
1294
}
1295
 
1296
/**
1297
 * Get volatile flags
1298
 *
1299
 * @param string $type
1300
 * @param string $name
1301
 * @param int $changedsince default null
1302
 * @return string|false The cache flag value or false
1303
 */
1304
function get_cache_flag($type, $name, $changedsince=null) {
1305
    global $DB;
1306
 
1307
    $params = array('type' => $type, 'name' => $name, 'expiry' => time());
1308
 
1309
    $sqlwhere = "flagtype = :type AND name = :name AND expiry >= :expiry";
1310
    if ($changedsince !== null) {
1311
        $params['changedsince'] = $changedsince;
1312
        $sqlwhere .= " AND timemodified > :changedsince";
1313
    }
1314
 
1315
    return $DB->get_field_select('cache_flags', 'value', $sqlwhere, $params);
1316
}
1317
 
1318
/**
1319
 * Set a volatile flag
1320
 *
1321
 * @param string $type the "type" namespace for the key
1322
 * @param string $name the key to set
1323
 * @param string $value the value to set (without magic quotes) - null will remove the flag
1324
 * @param int $expiry (optional) epoch indicating expiry - defaults to now()+ 24hs
1325
 * @return bool Always returns true
1326
 */
1327
function set_cache_flag($type, $name, $value, $expiry = null) {
1328
    global $DB;
1329
 
1330
    $timemodified = time();
1331
    if ($expiry === null || $expiry < $timemodified) {
1332
        $expiry = $timemodified + 24 * 60 * 60;
1333
    } else {
1334
        $expiry = (int)$expiry;
1335
    }
1336
 
1337
    if ($value === null) {
1338
        unset_cache_flag($type, $name);
1339
        return true;
1340
    }
1341
 
1342
    if ($f = $DB->get_record('cache_flags', array('name' => $name, 'flagtype' => $type), '*', IGNORE_MULTIPLE)) {
1343
        // This is a potential problem in DEBUG_DEVELOPER.
1344
        if ($f->value == $value and $f->expiry == $expiry and $f->timemodified == $timemodified) {
1345
            return true; // No need to update.
1346
        }
1347
        $f->value        = $value;
1348
        $f->expiry       = $expiry;
1349
        $f->timemodified = $timemodified;
1350
        $DB->update_record('cache_flags', $f);
1351
    } else {
1352
        $f = new stdClass();
1353
        $f->flagtype     = $type;
1354
        $f->name         = $name;
1355
        $f->value        = $value;
1356
        $f->expiry       = $expiry;
1357
        $f->timemodified = $timemodified;
1358
        $DB->insert_record('cache_flags', $f);
1359
    }
1360
    return true;
1361
}
1362
 
1363
/**
1364
 * Removes a single volatile flag
1365
 *
1366
 * @param string $type the "type" namespace for the key
1367
 * @param string $name the key to set
1368
 * @return bool
1369
 */
1370
function unset_cache_flag($type, $name) {
1371
    global $DB;
1372
    $DB->delete_records('cache_flags', array('name' => $name, 'flagtype' => $type));
1373
    return true;
1374
}
1375
 
1376
/**
1377
 * Garbage-collect volatile flags
1378
 *
1379
 * @return bool Always returns true
1380
 */
1381
function gc_cache_flags() {
1382
    global $DB;
1383
    $DB->delete_records_select('cache_flags', 'expiry < ?', array(time()));
1384
    return true;
1385
}
1386
 
1387
// USER PREFERENCE API.
1388
 
1389
/**
1390
 * Refresh user preference cache. This is used most often for $USER
1391
 * object that is stored in session, but it also helps with performance in cron script.
1392
 *
1393
 * Preferences for each user are loaded on first use on every page, then again after the timeout expires.
1394
 *
1395
 * @package  core
1396
 * @category preference
1397
 * @access   public
1398
 * @param    stdClass         $user          User object. Preferences are preloaded into 'preference' property
1399
 * @param    int              $cachelifetime Cache life time on the current page (in seconds)
1400
 * @throws   coding_exception
1401
 * @return   null
1402
 */
1403
function check_user_preferences_loaded(stdClass $user, $cachelifetime = 120) {
1404
    global $DB;
1405
    // Static cache, we need to check on each page load, not only every 2 minutes.
1406
    static $loadedusers = array();
1407
 
1408
    if (!isset($user->id)) {
1409
        throw new coding_exception('Invalid $user parameter in check_user_preferences_loaded() call, missing id field');
1410
    }
1411
 
1412
    if (empty($user->id) or isguestuser($user->id)) {
1413
        // No permanent storage for not-logged-in users and guest.
1414
        if (!isset($user->preference)) {
1415
            $user->preference = array();
1416
        }
1417
        return;
1418
    }
1419
 
1420
    $timenow = time();
1421
 
1422
    if (isset($loadedusers[$user->id]) and isset($user->preference) and isset($user->preference['_lastloaded'])) {
1423
        // Already loaded at least once on this page. Are we up to date?
1424
        if ($user->preference['_lastloaded'] + $cachelifetime > $timenow) {
1425
            // No need to reload - we are on the same page and we loaded prefs just a moment ago.
1426
            return;
1427
 
1428
        } else if (!get_cache_flag('userpreferenceschanged', $user->id, $user->preference['_lastloaded'])) {
1429
            // No change since the lastcheck on this page.
1430
            $user->preference['_lastloaded'] = $timenow;
1431
            return;
1432
        }
1433
    }
1434
 
1435
    // OK, so we have to reload all preferences.
1436
    $loadedusers[$user->id] = true;
1437
    $user->preference = $DB->get_records_menu('user_preferences', array('userid' => $user->id), '', 'name,value'); // All values.
1438
    $user->preference['_lastloaded'] = $timenow;
1439
}
1440
 
1441
/**
1442
 * Called from set/unset_user_preferences, so that the prefs can be correctly reloaded in different sessions.
1443
 *
1444
 * NOTE: internal function, do not call from other code.
1445
 *
1446
 * @package core
1447
 * @access private
1448
 * @param integer $userid the user whose prefs were changed.
1449
 */
1450
function mark_user_preferences_changed($userid) {
1451
    global $CFG;
1452
 
1453
    if (empty($userid) or isguestuser($userid)) {
1454
        // No cache flags for guest and not-logged-in users.
1455
        return;
1456
    }
1457
 
1458
    set_cache_flag('userpreferenceschanged', $userid, 1, time() + $CFG->sessiontimeout);
1459
}
1460
 
1461
/**
1462
 * Sets a preference for the specified user.
1463
 *
1464
 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1465
 *
1466
 * When additional validation/permission check is needed it is better to use {@see useredit_update_user_preference()}
1467
 *
1468
 * @package  core
1469
 * @category preference
1470
 * @access   public
1471
 * @param    string            $name  The key to set as preference for the specified user
1472
 * @param    string|int|bool|null $value The value to set for the $name key in the specified user's
1473
 *                                    record, null means delete current value.
1474
 * @param    stdClass|int|null $user  A moodle user object or id, null means current user
1475
 * @throws   coding_exception
1476
 * @return   bool                     Always true or exception
1477
 */
1478
function set_user_preference($name, $value, $user = null) {
1479
    global $USER, $DB;
1480
 
1481
    if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
1482
        throw new coding_exception('Invalid preference name in set_user_preference() call');
1483
    }
1484
 
1485
    if (is_null($value)) {
1486
        // Null means delete current.
1487
        return unset_user_preference($name, $user);
1488
    } else if (is_object($value)) {
1489
        throw new coding_exception('Invalid value in set_user_preference() call, objects are not allowed');
1490
    } else if (is_array($value)) {
1491
        throw new coding_exception('Invalid value in set_user_preference() call, arrays are not allowed');
1492
    }
1493
    // Value column maximum length is 1333 characters.
1494
    $value = (string)$value;
1495
    if (core_text::strlen($value) > 1333) {
1496
        throw new coding_exception('Invalid value in set_user_preference() call, value is is too long for the value column');
1497
    }
1498
 
1499
    if (is_null($user)) {
1500
        $user = $USER;
1501
    } else if (isset($user->id)) {
1502
        // It is a valid object.
1503
    } else if (is_numeric($user)) {
1504
        $user = (object)array('id' => (int)$user);
1505
    } else {
1506
        throw new coding_exception('Invalid $user parameter in set_user_preference() call');
1507
    }
1508
 
1509
    check_user_preferences_loaded($user);
1510
 
1511
    if (empty($user->id) or isguestuser($user->id)) {
1512
        // No permanent storage for not-logged-in users and guest.
1513
        $user->preference[$name] = $value;
1514
        return true;
1515
    }
1516
 
1517
    if ($preference = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => $name))) {
1518
        if ($preference->value === $value and isset($user->preference[$name]) and $user->preference[$name] === $value) {
1519
            // Preference already set to this value.
1520
            return true;
1521
        }
1522
        $DB->set_field('user_preferences', 'value', $value, array('id' => $preference->id));
1523
 
1524
    } else {
1525
        $preference = new stdClass();
1526
        $preference->userid = $user->id;
1527
        $preference->name   = $name;
1528
        $preference->value  = $value;
1529
        $DB->insert_record('user_preferences', $preference);
1530
    }
1531
 
1532
    // Update value in cache.
1533
    $user->preference[$name] = $value;
1534
    // Update the $USER in case where we've not a direct reference to $USER.
1535
    if ($user !== $USER && $user->id == $USER->id) {
1536
        $USER->preference[$name] = $value;
1537
    }
1538
 
1539
    // Set reload flag for other sessions.
1540
    mark_user_preferences_changed($user->id);
1541
 
1542
    return true;
1543
}
1544
 
1545
/**
1546
 * Sets a whole array of preferences for the current user
1547
 *
1548
 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1549
 *
1550
 * @package  core
1551
 * @category preference
1552
 * @access   public
1553
 * @param    array             $prefarray An array of key/value pairs to be set
1554
 * @param    stdClass|int|null $user      A moodle user object or id, null means current user
1555
 * @return   bool                         Always true or exception
1556
 */
1557
function set_user_preferences(array $prefarray, $user = null) {
1558
    foreach ($prefarray as $name => $value) {
1559
        set_user_preference($name, $value, $user);
1560
    }
1561
    return true;
1562
}
1563
 
1564
/**
1565
 * Unsets a preference completely by deleting it from the database
1566
 *
1567
 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1568
 *
1569
 * @package  core
1570
 * @category preference
1571
 * @access   public
1572
 * @param    string            $name The key to unset as preference for the specified user
1573
 * @param    stdClass|int|null $user A moodle user object or id, null means current user
1574
 * @throws   coding_exception
1575
 * @return   bool                    Always true or exception
1576
 */
1577
function unset_user_preference($name, $user = null) {
1578
    global $USER, $DB;
1579
 
1580
    if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
1581
        throw new coding_exception('Invalid preference name in unset_user_preference() call');
1582
    }
1583
 
1584
    if (is_null($user)) {
1585
        $user = $USER;
1586
    } else if (isset($user->id)) {
1587
        // It is a valid object.
1588
    } else if (is_numeric($user)) {
1589
        $user = (object)array('id' => (int)$user);
1590
    } else {
1591
        throw new coding_exception('Invalid $user parameter in unset_user_preference() call');
1592
    }
1593
 
1594
    check_user_preferences_loaded($user);
1595
 
1596
    if (empty($user->id) or isguestuser($user->id)) {
1597
        // No permanent storage for not-logged-in user and guest.
1598
        unset($user->preference[$name]);
1599
        return true;
1600
    }
1601
 
1602
    // Delete from DB.
1603
    $DB->delete_records('user_preferences', array('userid' => $user->id, 'name' => $name));
1604
 
1605
    // Delete the preference from cache.
1606
    unset($user->preference[$name]);
1607
    // Update the $USER in case where we've not a direct reference to $USER.
1608
    if ($user !== $USER && $user->id == $USER->id) {
1609
        unset($USER->preference[$name]);
1610
    }
1611
 
1612
    // Set reload flag for other sessions.
1613
    mark_user_preferences_changed($user->id);
1614
 
1615
    return true;
1616
}
1617
 
1618
/**
1619
 * Used to fetch user preference(s)
1620
 *
1621
 * If no arguments are supplied this function will return
1622
 * all of the current user preferences as an array.
1623
 *
1624
 * If a name is specified then this function
1625
 * attempts to return that particular preference value.  If
1626
 * none is found, then the optional value $default is returned,
1627
 * otherwise null.
1628
 *
1629
 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1630
 *
1631
 * @package  core
1632
 * @category preference
1633
 * @access   public
1634
 * @param    string            $name    Name of the key to use in finding a preference value
1635
 * @param    mixed|null        $default Value to be returned if the $name key is not set in the user preferences
1636
 * @param    stdClass|int|null $user    A moodle user object or id, null means current user
1637
 * @throws   coding_exception
1638
 * @return   string|mixed|null          A string containing the value of a single preference. An
1639
 *                                      array with all of the preferences or null
1640
 */
1641
function get_user_preferences($name = null, $default = null, $user = null) {
1642
    global $USER;
1643
 
1644
    if (is_null($name)) {
1645
        // All prefs.
1646
    } else if (is_numeric($name) or $name === '_lastloaded') {
1647
        throw new coding_exception('Invalid preference name in get_user_preferences() call');
1648
    }
1649
 
1650
    if (is_null($user)) {
1651
        $user = $USER;
1652
    } else if (isset($user->id)) {
1653
        // Is a valid object.
1654
    } else if (is_numeric($user)) {
1655
        if ($USER->id == $user) {
1656
            $user = $USER;
1657
        } else {
1658
            $user = (object)array('id' => (int)$user);
1659
        }
1660
    } else {
1661
        throw new coding_exception('Invalid $user parameter in get_user_preferences() call');
1662
    }
1663
 
1664
    check_user_preferences_loaded($user);
1665
 
1666
    if (empty($name)) {
1667
        // All values.
1668
        return $user->preference;
1669
    } else if (isset($user->preference[$name])) {
1670
        // The single string value.
1671
        return $user->preference[$name];
1672
    } else {
1673
        // Default value (null if not specified).
1674
        return $default;
1675
    }
1676
}
1677
 
1678
// FUNCTIONS FOR HANDLING TIME.
1679
 
1680
/**
1681
 * Given Gregorian date parts in user time produce a GMT timestamp.
1682
 *
1683
 * @package core
1684
 * @category time
1685
 * @param int $year The year part to create timestamp of
1686
 * @param int $month The month part to create timestamp of
1687
 * @param int $day The day part to create timestamp of
1688
 * @param int $hour The hour part to create timestamp of
1689
 * @param int $minute The minute part to create timestamp of
1690
 * @param int $second The second part to create timestamp of
1691
 * @param int|float|string $timezone Timezone modifier, used to calculate GMT time offset.
1692
 *             if 99 then default user's timezone is used {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
1693
 * @param bool $applydst Toggle Daylight Saving Time, default true, will be
1694
 *             applied only if timezone is 99 or string.
1695
 * @return int GMT timestamp
1696
 */
1697
function make_timestamp($year, $month=1, $day=1, $hour=0, $minute=0, $second=0, $timezone=99, $applydst=true) {
1698
    $date = new DateTime('now', core_date::get_user_timezone_object($timezone));
1699
    $date->setDate((int)$year, (int)$month, (int)$day);
1700
    $date->setTime((int)$hour, (int)$minute, (int)$second);
1701
 
1702
    $time = $date->getTimestamp();
1703
 
1704
    if ($time === false) {
1705
        throw new coding_exception('getTimestamp() returned false, please ensure you have passed correct values.'.
1706
            ' This can fail if year is more than 2038 and OS is 32 bit windows');
1707
    }
1708
 
1709
    // Moodle BC DST stuff.
1710
    if (!$applydst) {
1711
        $time += dst_offset_on($time, $timezone);
1712
    }
1713
 
1714
    return $time;
1715
 
1716
}
1717
 
1718
/**
1719
 * Format a date/time (seconds) as weeks, days, hours etc as needed
1720
 *
1721
 * Given an amount of time in seconds, returns string
1722
 * formatted nicely as years, days, hours etc as needed
1723
 *
1724
 * @package core
1725
 * @category time
1726
 * @uses MINSECS
1727
 * @uses HOURSECS
1728
 * @uses DAYSECS
1729
 * @uses YEARSECS
1730
 * @param int $totalsecs Time in seconds
1731
 * @param stdClass $str Should be a time object
1732
 * @return string A nicely formatted date/time string
1733
 */
1734
function format_time($totalsecs, $str = null) {
1735
 
1736
    $totalsecs = abs($totalsecs);
1737
 
1738
    if (!$str) {
1739
        // Create the str structure the slow way.
1740
        $str = new stdClass();
1741
        $str->day   = get_string('day');
1742
        $str->days  = get_string('days');
1743
        $str->hour  = get_string('hour');
1744
        $str->hours = get_string('hours');
1745
        $str->min   = get_string('min');
1746
        $str->mins  = get_string('mins');
1747
        $str->sec   = get_string('sec');
1748
        $str->secs  = get_string('secs');
1749
        $str->year  = get_string('year');
1750
        $str->years = get_string('years');
1751
    }
1752
 
1753
    $years     = floor($totalsecs/YEARSECS);
1754
    $remainder = $totalsecs - ($years*YEARSECS);
1755
    $days      = floor($remainder/DAYSECS);
1756
    $remainder = $totalsecs - ($days*DAYSECS);
1757
    $hours     = floor($remainder/HOURSECS);
1758
    $remainder = $remainder - ($hours*HOURSECS);
1759
    $mins      = floor($remainder/MINSECS);
1760
    $secs      = $remainder - ($mins*MINSECS);
1761
 
1762
    $ss = ($secs == 1)  ? $str->sec  : $str->secs;
1763
    $sm = ($mins == 1)  ? $str->min  : $str->mins;
1764
    $sh = ($hours == 1) ? $str->hour : $str->hours;
1765
    $sd = ($days == 1)  ? $str->day  : $str->days;
1766
    $sy = ($years == 1)  ? $str->year  : $str->years;
1767
 
1768
    $oyears = '';
1769
    $odays = '';
1770
    $ohours = '';
1771
    $omins = '';
1772
    $osecs = '';
1773
 
1774
    if ($years) {
1775
        $oyears  = $years .' '. $sy;
1776
    }
1777
    if ($days) {
1778
        $odays  = $days .' '. $sd;
1779
    }
1780
    if ($hours) {
1781
        $ohours = $hours .' '. $sh;
1782
    }
1783
    if ($mins) {
1784
        $omins  = $mins .' '. $sm;
1785
    }
1786
    if ($secs) {
1787
        $osecs  = $secs .' '. $ss;
1788
    }
1789
 
1790
    if ($years) {
1791
        return trim($oyears .' '. $odays);
1792
    }
1793
    if ($days) {
1794
        return trim($odays .' '. $ohours);
1795
    }
1796
    if ($hours) {
1797
        return trim($ohours .' '. $omins);
1798
    }
1799
    if ($mins) {
1800
        return trim($omins .' '. $osecs);
1801
    }
1802
    if ($secs) {
1803
        return $osecs;
1804
    }
1805
    return get_string('now');
1806
}
1807
 
1808
/**
1809
 * Returns a formatted string that represents a date in user time.
1810
 *
1811
 * @package core
1812
 * @category time
1813
 * @param int $date the timestamp in UTC, as obtained from the database.
1814
 * @param string $format strftime format. You should probably get this using
1815
 *        get_string('strftime...', 'langconfig');
1816
 * @param int|float|string $timezone by default, uses the user's time zone. if numeric and
1817
 *        not 99 then daylight saving will not be added.
1818
 *        {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
1819
 * @param bool $fixday If true (default) then the leading zero from %d is removed.
1820
 *        If false then the leading zero is maintained.
1821
 * @param bool $fixhour If true (default) then the leading zero from %I is removed.
1822
 * @return string the formatted date/time.
1823
 */
1824
function userdate($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) {
1825
    $calendartype = \core_calendar\type_factory::get_calendar_instance();
1826
    return $calendartype->timestamp_to_date_string($date, $format, $timezone, $fixday, $fixhour);
1827
}
1828
 
1829
/**
1830
 * Returns a html "time" tag with both the exact user date with timezone information
1831
 * as a datetime attribute in the W3C format, and the user readable date and time as text.
1832
 *
1833
 * @package core
1834
 * @category time
1835
 * @param int $date the timestamp in UTC, as obtained from the database.
1836
 * @param string $format strftime format. You should probably get this using
1837
 *        get_string('strftime...', 'langconfig');
1838
 * @param int|float|string $timezone by default, uses the user's time zone. if numeric and
1839
 *        not 99 then daylight saving will not be added.
1840
 *        {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
1841
 * @param bool $fixday If true (default) then the leading zero from %d is removed.
1842
 *        If false then the leading zero is maintained.
1843
 * @param bool $fixhour If true (default) then the leading zero from %I is removed.
1844
 * @return string the formatted date/time.
1845
 */
1846
function userdate_htmltime($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) {
1847
    $userdatestr = userdate($date, $format, $timezone, $fixday, $fixhour);
1848
    if (CLI_SCRIPT && !PHPUNIT_TEST) {
1849
        return $userdatestr;
1850
    }
1851
    $machinedate = new DateTime();
1852
    $machinedate->setTimestamp(intval($date));
1853
    $machinedate->setTimezone(core_date::get_user_timezone_object());
1854
 
1855
    return html_writer::tag('time', $userdatestr, ['datetime' => $machinedate->format(DateTime::W3C)]);
1856
}
1857
 
1858
/**
1859
 * Returns a formatted date ensuring it is UTF-8.
1860
 *
1861
 * If we are running under Windows convert to Windows encoding and then back to UTF-8
1862
 * (because it's impossible to specify UTF-8 to fetch locale info in Win32).
1863
 *
1864
 * @param int $date the timestamp - since Moodle 2.9 this is a real UTC timestamp
1865
 * @param string $format strftime format.
1866
 * @param int|float|string $tz the user timezone
1867
 * @return string the formatted date/time.
1868
 * @since Moodle 2.3.3
1869
 */
1870
function date_format_string($date, $format, $tz = 99) {
1871
 
1872
    date_default_timezone_set(core_date::get_user_timezone($tz));
1873
 
1874
    if (date('A', 0) === date('A', HOURSECS * 18)) {
1875
        $datearray = getdate($date);
1876
        $format = str_replace([
1877
            '%P',
1878
            '%p',
1879
        ], [
1880
            $datearray['hours'] < 12 ? get_string('am', 'langconfig') : get_string('pm', 'langconfig'),
1881
            $datearray['hours'] < 12 ? get_string('amcaps', 'langconfig') : get_string('pmcaps', 'langconfig'),
1882
        ], $format);
1883
    }
1884
 
1885
    $datestring = core_date::strftime($format, $date);
1886
    core_date::set_default_server_timezone();
1887
 
1888
    return $datestring;
1889
}
1890
 
1891
/**
1892
 * Given a $time timestamp in GMT (seconds since epoch),
1893
 * returns an array that represents the Gregorian date in user time
1894
 *
1895
 * @package core
1896
 * @category time
1897
 * @param int $time Timestamp in GMT
1898
 * @param float|int|string $timezone user timezone
1899
 * @return array An array that represents the date in user time
1900
 */
1901
function usergetdate($time, $timezone=99) {
1902
    if ($time === null) {
1903
        // PHP8 and PHP7 return different results when getdate(null) is called.
1904
        // Display warning and cast to 0 to make sure the usergetdate() behaves consistently on all versions of PHP.
1905
        // In the future versions of Moodle we may consider adding a strict typehint.
1906
        debugging('usergetdate() expects parameter $time to be int, null given', DEBUG_DEVELOPER);
1907
        $time = 0;
1908
    }
1909
 
1910
    date_default_timezone_set(core_date::get_user_timezone($timezone));
1911
    $result = getdate($time);
1912
    core_date::set_default_server_timezone();
1913
 
1914
    return $result;
1915
}
1916
 
1917
/**
1918
 * Given a GMT timestamp (seconds since epoch), offsets it by
1919
 * the timezone.  eg 3pm in India is 3pm GMT - 7 * 3600 seconds
1920
 *
1921
 * NOTE: this function does not include DST properly,
1922
 *       you should use the PHP date stuff instead!
1923
 *
1924
 * @package core
1925
 * @category time
1926
 * @param int $date Timestamp in GMT
1927
 * @param float|int|string $timezone user timezone
1928
 * @return int
1929
 */
1930
function usertime($date, $timezone=99) {
1931
    $userdate = new DateTime('@' . $date);
1932
    $userdate->setTimezone(core_date::get_user_timezone_object($timezone));
1933
    $dst = dst_offset_on($date, $timezone);
1934
 
1935
    return $date - $userdate->getOffset() + $dst;
1936
}
1937
 
1938
/**
1939
 * Get a formatted string representation of an interval between two unix timestamps.
1940
 *
1941
 * E.g.
1942
 * $intervalstring = get_time_interval_string(12345600, 12345660);
1943
 * Will produce the string:
1944
 * '0d 0h 1m'
1945
 *
1946
 * @param int $time1 unix timestamp
1947
 * @param int $time2 unix timestamp
1948
 * @param string $format string (can be lang string) containing format chars: https://www.php.net/manual/en/dateinterval.format.php.
1949
 * @param bool $dropzeroes If format is not provided and this is set to true, do not include zero time units.
1950
 *                         e.g. a duration of 3 days and 2 hours will be displayed as '3d 2h' instead of '3d 2h 0s'
1951
 * @param bool $fullformat If format is not provided and this is set to true, display time units in full format.
1952
 *                         e.g. instead of showing "3d", "3 days" will be returned.
1953
 * @return string the formatted string describing the time difference, e.g. '10d 11h 45m'.
1954
 */
1955
function get_time_interval_string(int $time1, int $time2, string $format = '',
1956
        bool $dropzeroes = false, bool $fullformat = false): string {
1957
    $dtdate = new DateTime();
1958
    $dtdate->setTimeStamp($time1);
1959
    $dtdate2 = new DateTime();
1960
    $dtdate2->setTimeStamp($time2);
1961
    $interval = $dtdate2->diff($dtdate);
1962
 
1963
    if (empty(trim($format))) {
1964
        // Default to this key.
1965
        $formatkey = 'dateintervaldayhrmin';
1966
 
1967
        if ($dropzeroes) {
1968
            $units = [
1969
                'y' => 'yr',
1970
                'm' => 'mo',
1971
                'd' => 'day',
1972
                'h' => 'hr',
1973
                'i' => 'min',
1974
                's' => 'sec',
1975
            ];
1976
            $formatunits = [];
1977
            foreach ($units as $key => $unit) {
1978
                if (empty($interval->$key)) {
1979
                    continue;
1980
                }
1981
                $formatunits[] = $unit;
1982
            }
1983
            if (!empty($formatunits)) {
1984
                $formatkey = 'dateinterval' . implode("", $formatunits);
1985
            }
1986
        }
1987
 
1988
        if ($fullformat) {
1989
            $formatkey .= 'full';
1990
        }
1991
        $format = get_string($formatkey, 'langconfig');
1992
    }
1993
    return $interval->format($format);
1994
}
1995
 
1996
/**
1997
 * Given a time, return the GMT timestamp of the most recent midnight
1998
 * for the current user.
1999
 *
2000
 * @package core
2001
 * @category time
2002
 * @param int $date Timestamp in GMT
2003
 * @param float|int|string $timezone user timezone
2004
 * @return int Returns a GMT timestamp
2005
 */
2006
function usergetmidnight($date, $timezone=99) {
2007
 
2008
    $userdate = usergetdate($date, $timezone);
2009
 
2010
    // Time of midnight of this user's day, in GMT.
2011
    return make_timestamp($userdate['year'], $userdate['mon'], $userdate['mday'], 0, 0, 0, $timezone);
2012
 
2013
}
2014
 
2015
/**
2016
 * Returns a string that prints the user's timezone
2017
 *
2018
 * @package core
2019
 * @category time
2020
 * @param float|int|string $timezone user timezone
2021
 * @return string
2022
 */
2023
function usertimezone($timezone=99) {
2024
    $tz = core_date::get_user_timezone($timezone);
2025
    return core_date::get_localised_timezone($tz);
2026
}
2027
 
2028
/**
2029
 * Returns a float or a string which denotes the user's timezone
2030
 * A float value means that a simple offset from GMT is used, while a string (it will be the name of a timezone in the database)
2031
 * means that for this timezone there are also DST rules to be taken into account
2032
 * Checks various settings and picks the most dominant of those which have a value
2033
 *
2034
 * @package core
2035
 * @category time
2036
 * @param float|int|string $tz timezone to calculate GMT time offset before
2037
 *        calculating user timezone, 99 is default user timezone
2038
 *        {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
2039
 * @return float|string
2040
 */
2041
function get_user_timezone($tz = 99) {
2042
    global $USER, $CFG;
2043
 
2044
    $timezones = array(
2045
        $tz,
2046
        isset($CFG->forcetimezone) ? $CFG->forcetimezone : 99,
2047
        isset($USER->timezone) ? $USER->timezone : 99,
2048
        isset($CFG->timezone) ? $CFG->timezone : 99,
2049
        );
2050
 
2051
    $tz = 99;
2052
 
2053
    // Loop while $tz is, empty but not zero, or 99, and there is another timezone is the array.
2054
    foreach ($timezones as $nextvalue) {
2055
        if ((empty($tz) && !is_numeric($tz)) || $tz == 99) {
2056
            $tz = $nextvalue;
2057
        }
2058
    }
2059
    return is_numeric($tz) ? (float) $tz : $tz;
2060
}
2061
 
2062
/**
2063
 * Calculates the Daylight Saving Offset for a given date/time (timestamp)
2064
 * - Note: Daylight saving only works for string timezones and not for float.
2065
 *
2066
 * @package core
2067
 * @category time
2068
 * @param int $time must NOT be compensated at all, it has to be a pure timestamp
2069
 * @param int|float|string $strtimezone user timezone
2070
 * @return int
2071
 */
2072
function dst_offset_on($time, $strtimezone = null) {
2073
    $tz = core_date::get_user_timezone($strtimezone);
2074
    $date = new DateTime('@' . $time);
2075
    $date->setTimezone(new DateTimeZone($tz));
2076
    if ($date->format('I') == '1') {
2077
        if ($tz === 'Australia/Lord_Howe') {
2078
            return 1800;
2079
        }
2080
        return 3600;
2081
    }
2082
    return 0;
2083
}
2084
 
2085
/**
2086
 * Calculates when the day appears in specific month
2087
 *
2088
 * @package core
2089
 * @category time
2090
 * @param int $startday starting day of the month
2091
 * @param int $weekday The day when week starts (normally taken from user preferences)
2092
 * @param int $month The month whose day is sought
2093
 * @param int $year The year of the month whose day is sought
2094
 * @return int
2095
 */
2096
function find_day_in_month($startday, $weekday, $month, $year) {
2097
    $calendartype = \core_calendar\type_factory::get_calendar_instance();
2098
 
2099
    $daysinmonth = days_in_month($month, $year);
2100
    $daysinweek = count($calendartype->get_weekdays());
2101
 
2102
    if ($weekday == -1) {
2103
        // Don't care about weekday, so return:
2104
        //    abs($startday) if $startday != -1
2105
        //    $daysinmonth otherwise.
2106
        return ($startday == -1) ? $daysinmonth : abs($startday);
2107
    }
2108
 
2109
    // From now on we 're looking for a specific weekday.
2110
    // Give "end of month" its actual value, since we know it.
2111
    if ($startday == -1) {
2112
        $startday = -1 * $daysinmonth;
2113
    }
2114
 
2115
    // Starting from day $startday, the sign is the direction.
2116
    if ($startday < 1) {
2117
        $startday = abs($startday);
2118
        $lastmonthweekday = dayofweek($daysinmonth, $month, $year);
2119
 
2120
        // This is the last such weekday of the month.
2121
        $lastinmonth = $daysinmonth + $weekday - $lastmonthweekday;
2122
        if ($lastinmonth > $daysinmonth) {
2123
            $lastinmonth -= $daysinweek;
2124
        }
2125
 
2126
        // Find the first such weekday <= $startday.
2127
        while ($lastinmonth > $startday) {
2128
            $lastinmonth -= $daysinweek;
2129
        }
2130
 
2131
        return $lastinmonth;
2132
    } else {
2133
        $indexweekday = dayofweek($startday, $month, $year);
2134
 
2135
        $diff = $weekday - $indexweekday;
2136
        if ($diff < 0) {
2137
            $diff += $daysinweek;
2138
        }
2139
 
2140
        // This is the first such weekday of the month equal to or after $startday.
2141
        $firstfromindex = $startday + $diff;
2142
 
2143
        return $firstfromindex;
2144
    }
2145
}
2146
 
2147
/**
2148
 * Calculate the number of days in a given month
2149
 *
2150
 * @package core
2151
 * @category time
2152
 * @param int $month The month whose day count is sought
2153
 * @param int $year The year of the month whose day count is sought
2154
 * @return int
2155
 */
2156
function days_in_month($month, $year) {
2157
    $calendartype = \core_calendar\type_factory::get_calendar_instance();
2158
    return $calendartype->get_num_days_in_month($year, $month);
2159
}
2160
 
2161
/**
2162
 * Calculate the position in the week of a specific calendar day
2163
 *
2164
 * @package core
2165
 * @category time
2166
 * @param int $day The day of the date whose position in the week is sought
2167
 * @param int $month The month of the date whose position in the week is sought
2168
 * @param int $year The year of the date whose position in the week is sought
2169
 * @return int
2170
 */
2171
function dayofweek($day, $month, $year) {
2172
    $calendartype = \core_calendar\type_factory::get_calendar_instance();
2173
    return $calendartype->get_weekday($year, $month, $day);
2174
}
2175
 
2176
// USER AUTHENTICATION AND LOGIN.
2177
 
2178
/**
2179
 * Returns full login url.
2180
 *
2181
 * Any form submissions for authentication to this URL must include username,
2182
 * password as well as a logintoken generated by \core\session\manager::get_login_token().
2183
 *
2184
 * @return string login url
2185
 */
2186
function get_login_url() {
2187
    global $CFG;
2188
 
2189
    return "$CFG->wwwroot/login/index.php";
2190
}
2191
 
2192
/**
2193
 * This function checks that the current user is logged in and has the
2194
 * required privileges
2195
 *
2196
 * This function checks that the current user is logged in, and optionally
2197
 * whether they are allowed to be in a particular course and view a particular
2198
 * course module.
2199
 * If they are not logged in, then it redirects them to the site login unless
2200
 * $autologinguest is set and {@link $CFG}->autologinguests is set to 1 in which
2201
 * case they are automatically logged in as guests.
2202
 * If $courseid is given and the user is not enrolled in that course then the
2203
 * user is redirected to the course enrolment page.
2204
 * If $cm is given and the course module is hidden and the user is not a teacher
2205
 * in the course then the user is redirected to the course home page.
2206
 *
2207
 * When $cm parameter specified, this function sets page layout to 'module'.
2208
 * You need to change it manually later if some other layout needed.
2209
 *
2210
 * @package    core_access
2211
 * @category   access
2212
 *
2213
 * @param mixed $courseorid id of the course or course object
2214
 * @param bool $autologinguest default true
2215
 * @param object $cm course module object
2216
 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
2217
 *             true. Used to avoid (=false) some scripts (file.php...) to set that variable,
2218
 *             in order to keep redirects working properly. MDL-14495
2219
 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
2220
 * @return mixed Void, exit, and die depending on path
2221
 * @throws coding_exception
2222
 * @throws require_login_exception
2223
 * @throws moodle_exception
2224
 */
2225
function require_login($courseorid = null, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) {
2226
    global $CFG, $SESSION, $USER, $PAGE, $SITE, $DB, $OUTPUT;
2227
 
2228
    // Must not redirect when byteserving already started.
2229
    if (!empty($_SERVER['HTTP_RANGE'])) {
2230
        $preventredirect = true;
2231
    }
2232
 
2233
    if (AJAX_SCRIPT) {
2234
        // We cannot redirect for AJAX scripts either.
2235
        $preventredirect = true;
2236
    }
2237
 
2238
    // Setup global $COURSE, themes, language and locale.
2239
    if (!empty($courseorid)) {
2240
        if (is_object($courseorid)) {
2241
            $course = $courseorid;
2242
        } else if ($courseorid == SITEID) {
2243
            $course = clone($SITE);
2244
        } else {
2245
            $course = $DB->get_record('course', array('id' => $courseorid), '*', MUST_EXIST);
2246
        }
2247
        if ($cm) {
2248
            if ($cm->course != $course->id) {
2249
                throw new coding_exception('course and cm parameters in require_login() call do not match!!');
2250
            }
2251
            // Make sure we have a $cm from get_fast_modinfo as this contains activity access details.
2252
            if (!($cm instanceof cm_info)) {
2253
                // Note: nearly all pages call get_fast_modinfo anyway and it does not make any
2254
                // db queries so this is not really a performance concern, however it is obviously
2255
                // better if you use get_fast_modinfo to get the cm before calling this.
2256
                $modinfo = get_fast_modinfo($course);
2257
                $cm = $modinfo->get_cm($cm->id);
2258
            }
2259
        }
2260
    } else {
2261
        // Do not touch global $COURSE via $PAGE->set_course(),
2262
        // the reasons is we need to be able to call require_login() at any time!!
2263
        $course = $SITE;
2264
        if ($cm) {
2265
            throw new coding_exception('cm parameter in require_login() requires valid course parameter!');
2266
        }
2267
    }
2268
 
2269
    // If this is an AJAX request and $setwantsurltome is true then we need to override it and set it to false.
2270
    // Otherwise the AJAX request URL will be set to $SESSION->wantsurl and events such as self enrolment in the future
2271
    // risk leading the user back to the AJAX request URL.
2272
    if ($setwantsurltome && defined('AJAX_SCRIPT') && AJAX_SCRIPT) {
2273
        $setwantsurltome = false;
2274
    }
2275
 
2276
    // Redirect to the login page if session has expired, only with dbsessions enabled (MDL-35029) to maintain current behaviour.
2277
    if ((!isloggedin() or isguestuser()) && !empty($SESSION->has_timed_out) && !empty($CFG->dbsessions)) {
2278
        if ($preventredirect) {
2279
            throw new require_login_session_timeout_exception();
2280
        } else {
2281
            if ($setwantsurltome) {
2282
                $SESSION->wantsurl = qualified_me();
2283
            }
2284
            redirect(get_login_url());
2285
        }
2286
    }
2287
 
2288
    // If the user is not even logged in yet then make sure they are.
2289
    if (!isloggedin()) {
2290
        if ($autologinguest && !empty($CFG->autologinguests)) {
2291
            if (!$guest = get_complete_user_data('id', $CFG->siteguest)) {
2292
                // Misconfigured site guest, just redirect to login page.
2293
                redirect(get_login_url());
2294
                exit; // Never reached.
2295
            }
2296
            $lang = isset($SESSION->lang) ? $SESSION->lang : $CFG->lang;
2297
            complete_user_login($guest);
2298
            $USER->autologinguest = true;
2299
            $SESSION->lang = $lang;
2300
        } else {
2301
            // NOTE: $USER->site check was obsoleted by session test cookie, $USER->confirmed test is in login/index.php.
2302
            if ($preventredirect) {
2303
                throw new require_login_exception('You are not logged in');
2304
            }
2305
 
2306
            if ($setwantsurltome) {
2307
                $SESSION->wantsurl = qualified_me();
2308
            }
2309
 
2310
            // Give auth plugins an opportunity to authenticate or redirect to an external login page
2311
            $authsequence = get_enabled_auth_plugins(); // Auths, in sequence.
2312
            foreach($authsequence as $authname) {
2313
                $authplugin = get_auth_plugin($authname);
2314
                $authplugin->pre_loginpage_hook();
2315
                if (isloggedin()) {
2316
                    if ($cm) {
2317
                        $modinfo = get_fast_modinfo($course);
2318
                        $cm = $modinfo->get_cm($cm->id);
2319
                    }
2320
                    set_access_log_user();
2321
                    break;
2322
                }
2323
            }
2324
 
2325
            // If we're still not logged in then go to the login page
2326
            if (!isloggedin()) {
2327
                redirect(get_login_url());
2328
                exit; // Never reached.
2329
            }
2330
        }
2331
    }
2332
 
2333
    // Loginas as redirection if needed.
2334
    if ($course->id != SITEID and \core\session\manager::is_loggedinas()) {
2335
        if ($USER->loginascontext->contextlevel == CONTEXT_COURSE) {
2336
            if ($USER->loginascontext->instanceid != $course->id) {
2337
                throw new \moodle_exception('loginasonecourse', '',
2338
                    $CFG->wwwroot.'/course/view.php?id='.$USER->loginascontext->instanceid);
2339
            }
2340
        }
2341
    }
2342
 
2343
    // Check whether the user should be changing password (but only if it is REALLY them).
2344
    if (get_user_preferences('auth_forcepasswordchange') && !\core\session\manager::is_loggedinas()) {
2345
        $userauth = get_auth_plugin($USER->auth);
2346
        if ($userauth->can_change_password() and !$preventredirect) {
2347
            if ($setwantsurltome) {
2348
                $SESSION->wantsurl = qualified_me();
2349
            }
2350
            if ($changeurl = $userauth->change_password_url()) {
2351
                // Use plugin custom url.
2352
                redirect($changeurl);
2353
            } else {
2354
                // Use moodle internal method.
2355
                redirect($CFG->wwwroot .'/login/change_password.php');
2356
            }
2357
        } else if ($userauth->can_change_password()) {
2358
            throw new moodle_exception('forcepasswordchangenotice');
2359
        } else {
2360
            throw new moodle_exception('nopasswordchangeforced', 'auth');
2361
        }
2362
    }
2363
 
2364
    // Check that the user account is properly set up. If we can't redirect to
2365
    // edit their profile and this is not a WS request, perform just the lax check.
2366
    // It will allow them to use filepicker on the profile edit page.
2367
 
2368
    if ($preventredirect && !WS_SERVER) {
2369
        $usernotfullysetup = user_not_fully_set_up($USER, false);
2370
    } else {
2371
        $usernotfullysetup = user_not_fully_set_up($USER, true);
2372
    }
2373
 
2374
    if ($usernotfullysetup) {
2375
        if ($preventredirect) {
2376
            throw new moodle_exception('usernotfullysetup');
2377
        }
2378
        if ($setwantsurltome) {
2379
            $SESSION->wantsurl = qualified_me();
2380
        }
2381
        redirect($CFG->wwwroot .'/user/edit.php?id='. $USER->id .'&amp;course='. SITEID);
2382
    }
2383
 
2384
    // Make sure the USER has a sesskey set up. Used for CSRF protection.
2385
    sesskey();
2386
 
2387
    if (\core\session\manager::is_loggedinas()) {
2388
        // During a "logged in as" session we should force all content to be cleaned because the
2389
        // logged in user will be viewing potentially malicious user generated content.
2390
        // See MDL-63786 for more details.
2391
        $CFG->forceclean = true;
2392
    }
2393
 
2394
    $afterlogins = get_plugins_with_function('after_require_login', 'lib.php');
2395
 
2396
    // Do not bother admins with any formalities, except for activities pending deletion.
2397
    if (is_siteadmin() && !($cm && $cm->deletioninprogress)) {
2398
        // Set the global $COURSE.
2399
        if ($cm) {
2400
            $PAGE->set_cm($cm, $course);
2401
            $PAGE->set_pagelayout('incourse');
2402
        } else if (!empty($courseorid)) {
2403
            $PAGE->set_course($course);
2404
        }
2405
        // Set accesstime or the user will appear offline which messes up messaging.
2406
        // Do not update access time for webservice or ajax requests.
2407
        if (!WS_SERVER && !AJAX_SCRIPT) {
2408
            user_accesstime_log($course->id);
2409
        }
2410
 
2411
        foreach ($afterlogins as $plugintype => $plugins) {
2412
            foreach ($plugins as $pluginfunction) {
2413
                $pluginfunction($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2414
            }
2415
        }
2416
        return;
2417
    }
2418
 
2419
    // Scripts have a chance to declare that $USER->policyagreed should not be checked.
2420
    // This is mostly for places where users are actually accepting the policies, to avoid the redirect loop.
2421
    if (!defined('NO_SITEPOLICY_CHECK')) {
2422
        define('NO_SITEPOLICY_CHECK', false);
2423
    }
2424
 
2425
    // Check that the user has agreed to a site policy if there is one - do not test in case of admins.
2426
    // Do not test if the script explicitly asked for skipping the site policies check.
2427
    // Or if the user auth type is webservice.
2428
    if (!$USER->policyagreed && !is_siteadmin() && !NO_SITEPOLICY_CHECK && $USER->auth !== 'webservice') {
2429
        $manager = new \core_privacy\local\sitepolicy\manager();
2430
        if ($policyurl = $manager->get_redirect_url(isguestuser())) {
2431
            if ($preventredirect) {
2432
                throw new moodle_exception('sitepolicynotagreed', 'error', '', $policyurl->out());
2433
            }
2434
            if ($setwantsurltome) {
2435
                $SESSION->wantsurl = qualified_me();
2436
            }
2437
            redirect($policyurl);
2438
        }
2439
    }
2440
 
2441
    // Fetch the system context, the course context, and prefetch its child contexts.
2442
    $sysctx = context_system::instance();
2443
    $coursecontext = context_course::instance($course->id, MUST_EXIST);
2444
    if ($cm) {
2445
        $cmcontext = context_module::instance($cm->id, MUST_EXIST);
2446
    } else {
2447
        $cmcontext = null;
2448
    }
2449
 
2450
    // If the site is currently under maintenance, then print a message.
2451
    if (!empty($CFG->maintenance_enabled) and !has_capability('moodle/site:maintenanceaccess', $sysctx)) {
2452
        if ($preventredirect) {
2453
            throw new require_login_exception('Maintenance in progress');
2454
        }
2455
        $PAGE->set_context(null);
2456
        print_maintenance_message();
2457
    }
2458
 
2459
    // Make sure the course itself is not hidden.
2460
    if ($course->id == SITEID) {
2461
        // Frontpage can not be hidden.
2462
    } else {
2463
        if (is_role_switched($course->id)) {
2464
            // When switching roles ignore the hidden flag - user had to be in course to do the switch.
2465
        } else {
2466
            if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
2467
                // Originally there was also test of parent category visibility, BUT is was very slow in complex queries
2468
                // involving "my courses" now it is also possible to simply hide all courses user is not enrolled in :-).
2469
                if ($preventredirect) {
2470
                    throw new require_login_exception('Course is hidden');
2471
                }
2472
                $PAGE->set_context(null);
2473
                // We need to override the navigation URL as the course won't have been added to the navigation and thus
2474
                // the navigation will mess up when trying to find it.
2475
                navigation_node::override_active_url(new moodle_url('/'));
2476
                notice(get_string('coursehidden'), $CFG->wwwroot .'/');
2477
            }
2478
        }
2479
    }
2480
 
2481
    // Is the user enrolled?
2482
    if ($course->id == SITEID) {
2483
        // Everybody is enrolled on the frontpage.
2484
    } else {
2485
        if (\core\session\manager::is_loggedinas()) {
2486
            // Make sure the REAL person can access this course first.
2487
            $realuser = \core\session\manager::get_realuser();
2488
            if (!is_enrolled($coursecontext, $realuser->id, '', true) and
2489
                !is_viewing($coursecontext, $realuser->id) and !is_siteadmin($realuser->id)) {
2490
                if ($preventredirect) {
2491
                    throw new require_login_exception('Invalid course login-as access');
2492
                }
2493
                $PAGE->set_context(null);
2494
                echo $OUTPUT->header();
2495
                notice(get_string('studentnotallowed', '', fullname($USER, true)), $CFG->wwwroot .'/');
2496
            }
2497
        }
2498
 
2499
        $access = false;
2500
 
2501
        if (is_role_switched($course->id)) {
2502
            // Ok, user had to be inside this course before the switch.
2503
            $access = true;
2504
 
2505
        } else if (is_viewing($coursecontext, $USER)) {
2506
            // Ok, no need to mess with enrol.
2507
            $access = true;
2508
 
2509
        } else {
2510
            if (isset($USER->enrol['enrolled'][$course->id])) {
2511
                if ($USER->enrol['enrolled'][$course->id] > time()) {
2512
                    $access = true;
2513
                    if (isset($USER->enrol['tempguest'][$course->id])) {
2514
                        unset($USER->enrol['tempguest'][$course->id]);
2515
                        remove_temp_course_roles($coursecontext);
2516
                    }
2517
                } else {
2518
                    // Expired.
2519
                    unset($USER->enrol['enrolled'][$course->id]);
2520
                }
2521
            }
2522
            if (isset($USER->enrol['tempguest'][$course->id])) {
2523
                if ($USER->enrol['tempguest'][$course->id] == 0) {
2524
                    $access = true;
2525
                } else if ($USER->enrol['tempguest'][$course->id] > time()) {
2526
                    $access = true;
2527
                } else {
2528
                    // Expired.
2529
                    unset($USER->enrol['tempguest'][$course->id]);
2530
                    remove_temp_course_roles($coursecontext);
2531
                }
2532
            }
2533
 
2534
            if (!$access) {
2535
                // Cache not ok.
2536
                $until = enrol_get_enrolment_end($coursecontext->instanceid, $USER->id);
2537
                if ($until !== false) {
2538
                    // Active participants may always access, a timestamp in the future, 0 (always) or false.
2539
                    if ($until == 0) {
2540
                        $until = ENROL_MAX_TIMESTAMP;
2541
                    }
2542
                    $USER->enrol['enrolled'][$course->id] = $until;
2543
                    $access = true;
2544
 
2545
                } else if (core_course_category::can_view_course_info($course)) {
2546
                    $params = array('courseid' => $course->id, 'status' => ENROL_INSTANCE_ENABLED);
2547
                    $instances = $DB->get_records('enrol', $params, 'sortorder, id ASC');
2548
                    $enrols = enrol_get_plugins(true);
2549
                    // First ask all enabled enrol instances in course if they want to auto enrol user.
2550
                    foreach ($instances as $instance) {
2551
                        if (!isset($enrols[$instance->enrol])) {
2552
                            continue;
2553
                        }
2554
                        // Get a duration for the enrolment, a timestamp in the future, 0 (always) or false.
2555
                        $until = $enrols[$instance->enrol]->try_autoenrol($instance);
2556
                        if ($until !== false) {
2557
                            if ($until == 0) {
2558
                                $until = ENROL_MAX_TIMESTAMP;
2559
                            }
2560
                            $USER->enrol['enrolled'][$course->id] = $until;
2561
                            $access = true;
2562
                            break;
2563
                        }
2564
                    }
2565
                    // If not enrolled yet try to gain temporary guest access.
2566
                    if (!$access) {
2567
                        foreach ($instances as $instance) {
2568
                            if (!isset($enrols[$instance->enrol])) {
2569
                                continue;
2570
                            }
2571
                            // Get a duration for the guest access, a timestamp in the future or false.
2572
                            $until = $enrols[$instance->enrol]->try_guestaccess($instance);
2573
                            if ($until !== false and $until > time()) {
2574
                                $USER->enrol['tempguest'][$course->id] = $until;
2575
                                $access = true;
2576
                                break;
2577
                            }
2578
                        }
2579
                    }
2580
                } else {
2581
                    // User is not enrolled and is not allowed to browse courses here.
2582
                    if ($preventredirect) {
2583
                        throw new require_login_exception('Course is not available');
2584
                    }
2585
                    $PAGE->set_context(null);
2586
                    // We need to override the navigation URL as the course won't have been added to the navigation and thus
2587
                    // the navigation will mess up when trying to find it.
2588
                    navigation_node::override_active_url(new moodle_url('/'));
2589
                    notice(get_string('coursehidden'), $CFG->wwwroot .'/');
2590
                }
2591
            }
2592
        }
2593
 
2594
        if (!$access) {
2595
            if ($preventredirect) {
2596
                throw new require_login_exception('Not enrolled');
2597
            }
2598
            if ($setwantsurltome) {
2599
                $SESSION->wantsurl = qualified_me();
2600
            }
2601
            redirect($CFG->wwwroot .'/enrol/index.php?id='. $course->id);
2602
        }
2603
    }
2604
 
2605
    // Check whether the activity has been scheduled for deletion. If so, then deny access, even for admins.
2606
    if ($cm && $cm->deletioninprogress) {
2607
        if ($preventredirect) {
2608
            throw new moodle_exception('activityisscheduledfordeletion');
2609
        }
2610
        require_once($CFG->dirroot . '/course/lib.php');
2611
        redirect(course_get_url($course), get_string('activityisscheduledfordeletion', 'error'));
2612
    }
2613
 
2614
    // Check visibility of activity to current user; includes visible flag, conditional availability, etc.
2615
    if ($cm && !$cm->uservisible) {
2616
        if ($preventredirect) {
2617
            throw new require_login_exception('Activity is hidden');
2618
        }
2619
        // Get the error message that activity is not available and why (if explanation can be shown to the user).
2620
        $PAGE->set_course($course);
2621
        $renderer = $PAGE->get_renderer('course');
2622
        $message = $renderer->course_section_cm_unavailable_error_message($cm);
2623
        redirect(course_get_url($course), $message, null, \core\output\notification::NOTIFY_ERROR);
2624
    }
2625
 
2626
    // Set the global $COURSE.
2627
    if ($cm) {
2628
        $PAGE->set_cm($cm, $course);
2629
        $PAGE->set_pagelayout('incourse');
2630
    } else if (!empty($courseorid)) {
2631
        $PAGE->set_course($course);
2632
    }
2633
 
2634
    foreach ($afterlogins as $plugintype => $plugins) {
2635
        foreach ($plugins as $pluginfunction) {
2636
            $pluginfunction($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2637
        }
2638
    }
2639
 
2640
    // Finally access granted, update lastaccess times.
2641
    // Do not update access time for webservice or ajax requests.
2642
    if (!WS_SERVER && !AJAX_SCRIPT) {
2643
        user_accesstime_log($course->id);
2644
    }
2645
}
2646
 
2647
/**
2648
 * A convenience function for where we must be logged in as admin
2649
 * @return void
2650
 */
2651
function require_admin() {
2652
    require_login(null, false);
2653
    require_capability('moodle/site:config', context_system::instance());
2654
}
2655
 
2656
/**
2657
 * This function just makes sure a user is logged out.
2658
 *
2659
 * @package    core_access
2660
 * @category   access
2661
 */
2662
function require_logout() {
2663
    global $USER, $DB;
2664
 
2665
    if (!isloggedin()) {
2666
        // This should not happen often, no need for hooks or events here.
2667
        \core\session\manager::terminate_current();
2668
        return;
2669
    }
2670
 
2671
    // Execute hooks before action.
2672
    $authplugins = array();
2673
    $authsequence = get_enabled_auth_plugins();
2674
    foreach ($authsequence as $authname) {
2675
        $authplugins[$authname] = get_auth_plugin($authname);
2676
        $authplugins[$authname]->prelogout_hook();
2677
    }
2678
 
2679
    // Store info that gets removed during logout.
2680
    $sid = session_id();
2681
    $event = \core\event\user_loggedout::create(
2682
        array(
2683
            'userid' => $USER->id,
2684
            'objectid' => $USER->id,
2685
            'other' => array('sessionid' => $sid),
2686
        )
2687
    );
2688
    if ($session = $DB->get_record('sessions', array('sid'=>$sid))) {
2689
        $event->add_record_snapshot('sessions', $session);
2690
    }
2691
 
2692
    // Clone of $USER object to be used by auth plugins.
2693
    $user = fullclone($USER);
2694
 
2695
    // Delete session record and drop $_SESSION content.
2696
    \core\session\manager::terminate_current();
2697
 
2698
    // Trigger event AFTER action.
2699
    $event->trigger();
2700
 
2701
    // Hook to execute auth plugins redirection after event trigger.
2702
    foreach ($authplugins as $authplugin) {
2703
        $authplugin->postlogout_hook($user);
2704
    }
2705
}
2706
 
2707
/**
2708
 * Weaker version of require_login()
2709
 *
2710
 * This is a weaker version of {@link require_login()} which only requires login
2711
 * when called from within a course rather than the site page, unless
2712
 * the forcelogin option is turned on.
2713
 * @see require_login()
2714
 *
2715
 * @package    core_access
2716
 * @category   access
2717
 *
2718
 * @param mixed $courseorid The course object or id in question
2719
 * @param bool $autologinguest Allow autologin guests if that is wanted
2720
 * @param object $cm Course activity module if known
2721
 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
2722
 *             true. Used to avoid (=false) some scripts (file.php...) to set that variable,
2723
 *             in order to keep redirects working properly. MDL-14495
2724
 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
2725
 * @return void
2726
 * @throws coding_exception
2727
 */
2728
function require_course_login($courseorid, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) {
2729
    global $CFG, $PAGE, $SITE;
2730
    $issite = ((is_object($courseorid) and $courseorid->id == SITEID)
2731
          or (!is_object($courseorid) and $courseorid == SITEID));
2732
    if ($issite && !empty($cm) && !($cm instanceof cm_info)) {
2733
        // Note: nearly all pages call get_fast_modinfo anyway and it does not make any
2734
        // db queries so this is not really a performance concern, however it is obviously
2735
        // better if you use get_fast_modinfo to get the cm before calling this.
2736
        if (is_object($courseorid)) {
2737
            $course = $courseorid;
2738
        } else {
2739
            $course = clone($SITE);
2740
        }
2741
        $modinfo = get_fast_modinfo($course);
2742
        $cm = $modinfo->get_cm($cm->id);
2743
    }
2744
    if (!empty($CFG->forcelogin)) {
2745
        // Login required for both SITE and courses.
2746
        require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2747
 
2748
    } else if ($issite && !empty($cm) and !$cm->uservisible) {
2749
        // Always login for hidden activities.
2750
        require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2751
 
2752
    } else if (isloggedin() && !isguestuser()) {
2753
        // User is already logged in. Make sure the login is complete (user is fully setup, policies agreed).
2754
        require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2755
 
2756
    } else if ($issite) {
2757
        // Login for SITE not required.
2758
        // We still need to instatiate PAGE vars properly so that things that rely on it like navigation function correctly.
2759
        if (!empty($courseorid)) {
2760
            if (is_object($courseorid)) {
2761
                $course = $courseorid;
2762
            } else {
2763
                $course = clone $SITE;
2764
            }
2765
            if ($cm) {
2766
                if ($cm->course != $course->id) {
2767
                    throw new coding_exception('course and cm parameters in require_course_login() call do not match!!');
2768
                }
2769
                $PAGE->set_cm($cm, $course);
2770
                $PAGE->set_pagelayout('incourse');
2771
            } else {
2772
                $PAGE->set_course($course);
2773
            }
2774
        } else {
2775
            // If $PAGE->course, and hence $PAGE->context, have not already been set up properly, set them up now.
2776
            $PAGE->set_course($PAGE->course);
2777
        }
2778
        // Do not update access time for webservice or ajax requests.
2779
        if (!WS_SERVER && !AJAX_SCRIPT) {
2780
            user_accesstime_log(SITEID);
2781
        }
2782
        return;
2783
 
2784
    } else {
2785
        // Course login always required.
2786
        require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2787
    }
2788
}
2789
 
2790
/**
2791
 * Validates a user key, checking if the key exists, is not expired and the remote ip is correct.
2792
 *
2793
 * @param  string $keyvalue the key value
2794
 * @param  string $script   unique script identifier
2795
 * @param  int $instance    instance id
2796
 * @return stdClass the key entry in the user_private_key table
2797
 * @since Moodle 3.2
2798
 * @throws moodle_exception
2799
 */
2800
function validate_user_key($keyvalue, $script, $instance) {
2801
    global $DB;
2802
 
2803
    if (!$key = $DB->get_record('user_private_key', array('script' => $script, 'value' => $keyvalue, 'instance' => $instance))) {
2804
        throw new \moodle_exception('invalidkey');
2805
    }
2806
 
2807
    if (!empty($key->validuntil) and $key->validuntil < time()) {
2808
        throw new \moodle_exception('expiredkey');
2809
    }
2810
 
2811
    if ($key->iprestriction) {
2812
        $remoteaddr = getremoteaddr(null);
2813
        if (empty($remoteaddr) or !address_in_subnet($remoteaddr, $key->iprestriction)) {
2814
            throw new \moodle_exception('ipmismatch');
2815
        }
2816
    }
2817
    return $key;
2818
}
2819
 
2820
/**
2821
 * Require key login. Function terminates with error if key not found or incorrect.
2822
 *
2823
 * @uses NO_MOODLE_COOKIES
2824
 * @uses PARAM_ALPHANUM
2825
 * @param string $script unique script identifier
2826
 * @param int $instance optional instance id
2827
 * @param string $keyvalue The key. If not supplied, this will be fetched from the current session.
2828
 * @return int Instance ID
2829
 */
2830
function require_user_key_login($script, $instance = null, $keyvalue = null) {
2831
    global $DB;
2832
 
2833
    if (!NO_MOODLE_COOKIES) {
2834
        throw new \moodle_exception('sessioncookiesdisable');
2835
    }
2836
 
2837
    // Extra safety.
2838
    \core\session\manager::write_close();
2839
 
2840
    if (null === $keyvalue) {
2841
        $keyvalue = required_param('key', PARAM_ALPHANUM);
2842
    }
2843
 
2844
    $key = validate_user_key($keyvalue, $script, $instance);
2845
 
2846
    if (!$user = $DB->get_record('user', array('id' => $key->userid))) {
2847
        throw new \moodle_exception('invaliduserid');
2848
    }
2849
 
2850
    core_user::require_active_user($user, true, true);
2851
 
2852
    // Emulate normal session.
2853
    enrol_check_plugins($user, false);
2854
    \core\session\manager::set_user($user);
2855
 
2856
    // Note we are not using normal login.
2857
    if (!defined('USER_KEY_LOGIN')) {
2858
        define('USER_KEY_LOGIN', true);
2859
    }
2860
 
2861
    // Return instance id - it might be empty.
2862
    return $key->instance;
2863
}
2864
 
2865
/**
2866
 * Creates a new private user access key.
2867
 *
2868
 * @param string $script unique target identifier
2869
 * @param int $userid
2870
 * @param int $instance optional instance id
2871
 * @param string $iprestriction optional ip restricted access
2872
 * @param int $validuntil key valid only until given data
2873
 * @return string access key value
2874
 */
2875
function create_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
2876
    global $DB;
2877
 
2878
    $key = new stdClass();
2879
    $key->script        = $script;
2880
    $key->userid        = $userid;
2881
    $key->instance      = $instance;
2882
    $key->iprestriction = $iprestriction;
2883
    $key->validuntil    = $validuntil;
2884
    $key->timecreated   = time();
2885
 
2886
    // Something long and unique.
2887
    $key->value         = md5($userid.'_'.time().random_string(40));
2888
    while ($DB->record_exists('user_private_key', array('value' => $key->value))) {
2889
        // Must be unique.
2890
        $key->value     = md5($userid.'_'.time().random_string(40));
2891
    }
2892
    $DB->insert_record('user_private_key', $key);
2893
    return $key->value;
2894
}
2895
 
2896
/**
2897
 * Delete the user's new private user access keys for a particular script.
2898
 *
2899
 * @param string $script unique target identifier
2900
 * @param int $userid
2901
 * @return void
2902
 */
2903
function delete_user_key($script, $userid) {
2904
    global $DB;
2905
    $DB->delete_records('user_private_key', array('script' => $script, 'userid' => $userid));
2906
}
2907
 
2908
/**
2909
 * Gets a private user access key (and creates one if one doesn't exist).
2910
 *
2911
 * @param string $script unique target identifier
2912
 * @param int $userid
2913
 * @param int $instance optional instance id
2914
 * @param string $iprestriction optional ip restricted access
2915
 * @param int $validuntil key valid only until given date
2916
 * @return string access key value
2917
 */
2918
function get_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
2919
    global $DB;
2920
 
2921
    if ($key = $DB->get_record('user_private_key', array('script' => $script, 'userid' => $userid,
2922
                                                         'instance' => $instance, 'iprestriction' => $iprestriction,
2923
                                                         'validuntil' => $validuntil))) {
2924
        return $key->value;
2925
    } else {
2926
        return create_user_key($script, $userid, $instance, $iprestriction, $validuntil);
2927
    }
2928
}
2929
 
2930
 
2931
/**
2932
 * Modify the user table by setting the currently logged in user's last login to now.
2933
 *
2934
 * @return bool Always returns true
2935
 */
2936
function update_user_login_times() {
2937
    global $USER, $DB, $SESSION;
2938
 
2939
    if (isguestuser()) {
2940
        // Do not update guest access times/ips for performance.
2941
        return true;
2942
    }
2943
 
2944
    if (defined('USER_KEY_LOGIN') && USER_KEY_LOGIN === true) {
2945
        // Do not update user login time when using user key login.
2946
        return true;
2947
    }
2948
 
2949
    $now = time();
2950
 
2951
    $user = new stdClass();
2952
    $user->id = $USER->id;
2953
 
2954
    // Make sure all users that logged in have some firstaccess.
2955
    if ($USER->firstaccess == 0) {
2956
        $USER->firstaccess = $user->firstaccess = $now;
2957
    }
2958
 
2959
    // Store the previous current as lastlogin.
2960
    $USER->lastlogin = $user->lastlogin = $USER->currentlogin;
2961
 
2962
    $USER->currentlogin = $user->currentlogin = $now;
2963
 
2964
    // Function user_accesstime_log() may not update immediately, better do it here.
2965
    $USER->lastaccess = $user->lastaccess = $now;
2966
    $SESSION->userpreviousip = $USER->lastip;
2967
    $USER->lastip = $user->lastip = getremoteaddr();
2968
 
2969
    // Note: do not call user_update_user() here because this is part of the login process,
2970
    //       the login event means that these fields were updated.
2971
    $DB->update_record('user', $user);
2972
    return true;
2973
}
2974
 
2975
/**
2976
 * Determines if a user has completed setting up their account.
2977
 *
2978
 * The lax mode (with $strict = false) has been introduced for special cases
2979
 * only where we want to skip certain checks intentionally. This is valid in
2980
 * certain mnet or ajax scenarios when the user cannot / should not be
2981
 * redirected to edit their profile. In most cases, you should perform the
2982
 * strict check.
2983
 *
2984
 * @param stdClass $user A {@link $USER} object to test for the existence of a valid name and email
2985
 * @param bool $strict Be more strict and assert id and custom profile fields set, too
2986
 * @return bool
2987
 */
2988
function user_not_fully_set_up($user, $strict = true) {
2989
    global $CFG, $SESSION, $USER;
2990
    require_once($CFG->dirroot.'/user/profile/lib.php');
2991
 
2992
    // If the user is setup then store this in the session to avoid re-checking.
2993
    // Some edge cases are when the users email starts to bounce or the
2994
    // configuration for custom fields has changed while they are logged in so
2995
    // we re-check this fully every hour for the rare cases it has changed.
2996
    if (isset($USER->id) && isset($user->id) && $USER->id === $user->id &&
2997
         isset($SESSION->fullysetupstrict) && (time() - $SESSION->fullysetupstrict) < HOURSECS) {
2998
        return false;
2999
    }
3000
 
3001
    if (isguestuser($user)) {
3002
        return false;
3003
    }
3004
 
3005
    if (empty($user->firstname) or empty($user->lastname) or empty($user->email) or over_bounce_threshold($user)) {
3006
        return true;
3007
    }
3008
 
3009
    if ($strict) {
3010
        if (empty($user->id)) {
3011
            // Strict mode can be used with existing accounts only.
3012
            return true;
3013
        }
3014
        if (!profile_has_required_custom_fields_set($user->id)) {
3015
            return true;
3016
        }
3017
        if (isset($USER->id) && isset($user->id) && $USER->id === $user->id) {
3018
            $SESSION->fullysetupstrict = time();
3019
        }
3020
    }
3021
 
3022
    return false;
3023
}
3024
 
3025
/**
3026
 * Check whether the user has exceeded the bounce threshold
3027
 *
3028
 * @param stdClass $user A {@link $USER} object
3029
 * @return bool true => User has exceeded bounce threshold
3030
 */
3031
function over_bounce_threshold($user) {
3032
    global $CFG, $DB;
3033
 
3034
    if (empty($CFG->handlebounces)) {
3035
        return false;
3036
    }
3037
 
3038
    if (empty($user->id)) {
3039
        // No real (DB) user, nothing to do here.
3040
        return false;
3041
    }
3042
 
3043
    // Set sensible defaults.
3044
    if (empty($CFG->minbounces)) {
3045
        $CFG->minbounces = 10;
3046
    }
3047
    if (empty($CFG->bounceratio)) {
3048
        $CFG->bounceratio = .20;
3049
    }
3050
    $bouncecount = 0;
3051
    $sendcount = 0;
3052
    if ($bounce = $DB->get_record('user_preferences', array ('userid' => $user->id, 'name' => 'email_bounce_count'))) {
3053
        $bouncecount = $bounce->value;
3054
    }
3055
    if ($send = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) {
3056
        $sendcount = $send->value;
3057
    }
3058
    return ($bouncecount >= $CFG->minbounces && $bouncecount/$sendcount >= $CFG->bounceratio);
3059
}
3060
 
3061
/**
3062
 * Used to increment or reset email sent count
3063
 *
3064
 * @param stdClass $user object containing an id
3065
 * @param bool $reset will reset the count to 0
3066
 * @return void
3067
 */
3068
function set_send_count($user, $reset=false) {
3069
    global $DB;
3070
 
3071
    if (empty($user->id)) {
3072
        // No real (DB) user, nothing to do here.
3073
        return;
3074
    }
3075
 
3076
    if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) {
3077
        $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
3078
        $DB->update_record('user_preferences', $pref);
3079
    } else if (!empty($reset)) {
3080
        // If it's not there and we're resetting, don't bother. Make a new one.
3081
        $pref = new stdClass();
3082
        $pref->name   = 'email_send_count';
3083
        $pref->value  = 1;
3084
        $pref->userid = $user->id;
3085
        $DB->insert_record('user_preferences', $pref, false);
3086
    }
3087
}
3088
 
3089
/**
3090
 * Increment or reset user's email bounce count
3091
 *
3092
 * @param stdClass $user object containing an id
3093
 * @param bool $reset will reset the count to 0
3094
 */
3095
function set_bounce_count($user, $reset=false) {
3096
    global $DB;
3097
 
3098
    if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_bounce_count'))) {
3099
        $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
3100
        $DB->update_record('user_preferences', $pref);
3101
    } else if (!empty($reset)) {
3102
        // If it's not there and we're resetting, don't bother. Make a new one.
3103
        $pref = new stdClass();
3104
        $pref->name   = 'email_bounce_count';
3105
        $pref->value  = 1;
3106
        $pref->userid = $user->id;
3107
        $DB->insert_record('user_preferences', $pref, false);
3108
    }
3109
}
3110
 
3111
/**
3112
 * Determines if the logged in user is currently moving an activity
3113
 *
3114
 * @param int $courseid The id of the course being tested
3115
 * @return bool
3116
 */
3117
function ismoving($courseid) {
3118
    global $USER;
3119
 
3120
    if (!empty($USER->activitycopy)) {
3121
        return ($USER->activitycopycourse == $courseid);
3122
    }
3123
    return false;
3124
}
3125
 
3126
/**
3127
 * Returns a persons full name
3128
 *
3129
 * Given an object containing all of the users name values, this function returns a string with the full name of the person.
3130
 * The result may depend on system settings or language. 'override' will force the alternativefullnameformat to be used. In
3131
 * English, fullname as well as alternativefullnameformat is set to 'firstname lastname' by default. But you could have
3132
 * fullname set to 'firstname lastname' and alternativefullnameformat set to 'firstname middlename alternatename lastname'.
3133
 *
3134
 * @param stdClass $user A {@link $USER} object to get full name of.
3135
 * @param bool $override If true then the alternativefullnameformat format rather than fullnamedisplay format will be used.
3136
 * @return string
3137
 */
3138
function fullname($user, $override=false) {
3139
    // Note: We do not intend to deprecate this function any time soon as it is too widely used at this time.
3140
    // Uses of it should be updated to use the new API and pass updated arguments.
3141
 
3142
    // Return an empty string if there is no user.
3143
    if (empty($user)) {
3144
        return '';
3145
    }
3146
 
3147
    $options = ['override' => $override];
3148
    return core_user::get_fullname($user, null, $options);
3149
}
3150
 
3151
/**
3152
 * Reduces lines of duplicated code for getting user name fields.
3153
 *
3154
 * See also {@link user_picture::unalias()}
3155
 *
3156
 * @param object $addtoobject Object to add user name fields to.
3157
 * @param object $secondobject Object that contains user name field information.
3158
 * @param string $prefix prefix to be added to all fields (including $additionalfields) e.g. authorfirstname.
3159
 * @param array $additionalfields Additional fields to be matched with data in the second object.
3160
 * The key can be set to the user table field name.
3161
 * @return object User name fields.
3162
 */
3163
function username_load_fields_from_object($addtoobject, $secondobject, $prefix = null, $additionalfields = null) {
3164
    $fields = [];
3165
    foreach (\core_user\fields::get_name_fields() as $field) {
3166
        $fields[$field] = $prefix . $field;
3167
    }
3168
    if ($additionalfields) {
3169
        // Additional fields can specify their own 'alias' such as 'id' => 'userid'. This checks to see if
3170
        // the key is a number and then sets the key to the array value.
3171
        foreach ($additionalfields as $key => $value) {
3172
            if (is_numeric($key)) {
3173
                $additionalfields[$value] = $prefix . $value;
3174
                unset($additionalfields[$key]);
3175
            } else {
3176
                $additionalfields[$key] = $prefix . $value;
3177
            }
3178
        }
3179
        $fields = array_merge($fields, $additionalfields);
3180
    }
3181
    foreach ($fields as $key => $field) {
3182
        // Important that we have all of the user name fields present in the object that we are sending back.
3183
        $addtoobject->$key = '';
3184
        if (isset($secondobject->$field)) {
3185
            $addtoobject->$key = $secondobject->$field;
3186
        }
3187
    }
3188
    return $addtoobject;
3189
}
3190
 
3191
/**
3192
 * Returns an array of values in order of occurance in a provided string.
3193
 * The key in the result is the character postion in the string.
3194
 *
3195
 * @param array $values Values to be found in the string format
3196
 * @param string $stringformat The string which may contain values being searched for.
3197
 * @return array An array of values in order according to placement in the string format.
3198
 */
3199
function order_in_string($values, $stringformat) {
3200
    $valuearray = array();
3201
    foreach ($values as $value) {
3202
        $pattern = "/$value\b/";
3203
        // Using preg_match as strpos() may match values that are similar e.g. firstname and firstnamephonetic.
3204
        if (preg_match($pattern, $stringformat)) {
3205
            $replacement = "thing";
3206
            // Replace the value with something more unique to ensure we get the right position when using strpos().
3207
            $newformat = preg_replace($pattern, $replacement, $stringformat);
3208
            $position = strpos($newformat, $replacement);
3209
            $valuearray[$position] = $value;
3210
        }
3211
    }
3212
    ksort($valuearray);
3213
    return $valuearray;
3214
}
3215
 
3216
/**
3217
 * Returns whether a given authentication plugin exists.
3218
 *
3219
 * @param string $auth Form of authentication to check for. Defaults to the global setting in {@link $CFG}.
3220
 * @return boolean Whether the plugin is available.
3221
 */
3222
function exists_auth_plugin($auth) {
3223
    global $CFG;
3224
 
3225
    if (file_exists("{$CFG->dirroot}/auth/$auth/auth.php")) {
3226
        return is_readable("{$CFG->dirroot}/auth/$auth/auth.php");
3227
    }
3228
    return false;
3229
}
3230
 
3231
/**
3232
 * Checks if a given plugin is in the list of enabled authentication plugins.
3233
 *
3234
 * @param string $auth Authentication plugin.
3235
 * @return boolean Whether the plugin is enabled.
3236
 */
3237
function is_enabled_auth($auth) {
3238
    if (empty($auth)) {
3239
        return false;
3240
    }
3241
 
3242
    $enabled = get_enabled_auth_plugins();
3243
 
3244
    return in_array($auth, $enabled);
3245
}
3246
 
3247
/**
3248
 * Returns an authentication plugin instance.
3249
 *
3250
 * @param string $auth name of authentication plugin
3251
 * @return auth_plugin_base An instance of the required authentication plugin.
3252
 */
3253
function get_auth_plugin($auth) {
3254
    global $CFG;
3255
 
3256
    // Check the plugin exists first.
3257
    if (! exists_auth_plugin($auth)) {
3258
        throw new \moodle_exception('authpluginnotfound', 'debug', '', $auth);
3259
    }
3260
 
3261
    // Return auth plugin instance.
3262
    require_once("{$CFG->dirroot}/auth/$auth/auth.php");
3263
    $class = "auth_plugin_$auth";
3264
    return new $class;
3265
}
3266
 
3267
/**
3268
 * Returns array of active auth plugins.
3269
 *
3270
 * @param bool $fix fix $CFG->auth if needed. Only set if logged in as admin.
3271
 * @return array
3272
 */
3273
function get_enabled_auth_plugins($fix=false) {
3274
    global $CFG;
3275
 
3276
    $default = array('manual', 'nologin');
3277
 
3278
    if (empty($CFG->auth)) {
3279
        $auths = array();
3280
    } else {
3281
        $auths = explode(',', $CFG->auth);
3282
    }
3283
 
3284
    $auths = array_unique($auths);
3285
    $oldauthconfig = implode(',', $auths);
3286
    foreach ($auths as $k => $authname) {
3287
        if (in_array($authname, $default)) {
3288
            // The manual and nologin plugin never need to be stored.
3289
            unset($auths[$k]);
3290
        } else if (!exists_auth_plugin($authname)) {
3291
            debugging(get_string('authpluginnotfound', 'debug', $authname));
3292
            unset($auths[$k]);
3293
        }
3294
    }
3295
 
3296
    // Ideally only explicit interaction from a human admin should trigger a
3297
    // change in auth config, see MDL-70424 for details.
3298
    if ($fix) {
3299
        $newconfig = implode(',', $auths);
3300
        if (!isset($CFG->auth) or $newconfig != $CFG->auth) {
3301
            add_to_config_log('auth', $oldauthconfig, $newconfig, 'core');
3302
            set_config('auth', $newconfig);
3303
        }
3304
    }
3305
 
3306
    return (array_merge($default, $auths));
3307
}
3308
 
3309
/**
3310
 * Returns true if an internal authentication method is being used.
3311
 * if method not specified then, global default is assumed
3312
 *
3313
 * @param string $auth Form of authentication required
3314
 * @return bool
3315
 */
3316
function is_internal_auth($auth) {
3317
    // Throws error if bad $auth.
3318
    $authplugin = get_auth_plugin($auth);
3319
    return $authplugin->is_internal();
3320
}
3321
 
3322
/**
3323
 * Returns true if the user is a 'restored' one.
3324
 *
3325
 * Used in the login process to inform the user and allow him/her to reset the password
3326
 *
3327
 * @param string $username username to be checked
3328
 * @return bool
3329
 */
3330
function is_restored_user($username) {
3331
    global $CFG, $DB;
3332
 
3333
    return $DB->record_exists('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id, 'password' => 'restored'));
3334
}
3335
 
3336
/**
3337
 * Returns an array of user fields
3338
 *
3339
 * @return array User field/column names
3340
 */
3341
function get_user_fieldnames() {
3342
    global $DB;
3343
 
3344
    $fieldarray = $DB->get_columns('user');
3345
    unset($fieldarray['id']);
3346
    $fieldarray = array_keys($fieldarray);
3347
 
3348
    return $fieldarray;
3349
}
3350
 
3351
/**
3352
 * Returns the string of the language for the new user.
3353
 *
3354
 * @return string language for the new user
3355
 */
3356
function get_newuser_language() {
3357
    global $CFG, $SESSION;
3358
    return (!empty($CFG->autolangusercreation) && !empty($SESSION->lang)) ? $SESSION->lang : $CFG->lang;
3359
}
3360
 
3361
/**
3362
 * Creates a bare-bones user record
3363
 *
3364
 * @todo Outline auth types and provide code example
3365
 *
3366
 * @param string $username New user's username to add to record
3367
 * @param string $password New user's password to add to record
3368
 * @param string $auth Form of authentication required
3369
 * @return stdClass A complete user object
3370
 */
3371
function create_user_record($username, $password, $auth = 'manual') {
3372
    global $CFG, $DB, $SESSION;
3373
    require_once($CFG->dirroot.'/user/profile/lib.php');
3374
    require_once($CFG->dirroot.'/user/lib.php');
3375
 
3376
    // Just in case check text case.
3377
    $username = trim(core_text::strtolower($username));
3378
 
3379
    $authplugin = get_auth_plugin($auth);
3380
    $customfields = $authplugin->get_custom_user_profile_fields();
3381
    $newuser = new stdClass();
3382
    if ($newinfo = $authplugin->get_userinfo($username)) {
3383
        $newinfo = truncate_userinfo($newinfo);
3384
        foreach ($newinfo as $key => $value) {
3385
            if (in_array($key, $authplugin->userfields) || (in_array($key, $customfields))) {
3386
                $newuser->$key = $value;
3387
            }
3388
        }
3389
    }
3390
 
3391
    if (!empty($newuser->email)) {
3392
        if (email_is_not_allowed($newuser->email)) {
3393
            unset($newuser->email);
3394
        }
3395
    }
3396
 
3397
    $newuser->auth = $auth;
3398
    $newuser->username = $username;
3399
 
3400
    // Fix for MDL-8480
3401
    // user CFG lang for user if $newuser->lang is empty
3402
    // or $user->lang is not an installed language.
3403
    if (empty($newuser->lang) || !get_string_manager()->translation_exists($newuser->lang)) {
3404
        $newuser->lang = get_newuser_language();
3405
    }
3406
    $newuser->confirmed = 1;
3407
    $newuser->lastip = getremoteaddr();
3408
    $newuser->timecreated = time();
3409
    $newuser->timemodified = $newuser->timecreated;
3410
    $newuser->mnethostid = $CFG->mnet_localhost_id;
3411
 
3412
    $newuser->id = user_create_user($newuser, false, false);
3413
 
3414
    // Save user profile data.
3415
    profile_save_data($newuser);
3416
 
3417
    $user = get_complete_user_data('id', $newuser->id);
3418
    if (!empty($CFG->{'auth_'.$newuser->auth.'_forcechangepassword'})) {
3419
        set_user_preference('auth_forcepasswordchange', 1, $user);
3420
    }
3421
    // Set the password.
3422
    update_internal_user_password($user, $password);
3423
 
3424
    // Trigger event.
3425
    \core\event\user_created::create_from_userid($newuser->id)->trigger();
3426
 
3427
    return $user;
3428
}
3429
 
3430
/**
3431
 * Will update a local user record from an external source (MNET users can not be updated using this method!).
3432
 *
3433
 * @param string $username user's username to update the record
3434
 * @return stdClass A complete user object
3435
 */
3436
function update_user_record($username) {
3437
    global $DB, $CFG;
3438
    // Just in case check text case.
3439
    $username = trim(core_text::strtolower($username));
3440
 
3441
    $oldinfo = $DB->get_record('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id), '*', MUST_EXIST);
3442
    return update_user_record_by_id($oldinfo->id);
3443
}
3444
 
3445
/**
3446
 * Will update a local user record from an external source (MNET users can not be updated using this method!).
3447
 *
3448
 * @param int $id user id
3449
 * @return stdClass A complete user object
3450
 */
3451
function update_user_record_by_id($id) {
3452
    global $DB, $CFG;
3453
    require_once($CFG->dirroot."/user/profile/lib.php");
3454
    require_once($CFG->dirroot.'/user/lib.php');
3455
 
3456
    $params = array('mnethostid' => $CFG->mnet_localhost_id, 'id' => $id, 'deleted' => 0);
3457
    $oldinfo = $DB->get_record('user', $params, '*', MUST_EXIST);
3458
 
3459
    $newuser = array();
3460
    $userauth = get_auth_plugin($oldinfo->auth);
3461
 
3462
    if ($newinfo = $userauth->get_userinfo($oldinfo->username)) {
3463
        $newinfo = truncate_userinfo($newinfo);
3464
        $customfields = $userauth->get_custom_user_profile_fields();
3465
 
3466
        foreach ($newinfo as $key => $value) {
3467
            $iscustom = in_array($key, $customfields);
3468
            if (!$iscustom) {
3469
                $key = strtolower($key);
3470
            }
3471
            if ((!property_exists($oldinfo, $key) && !$iscustom) or $key === 'username' or $key === 'id'
3472
                    or $key === 'auth' or $key === 'mnethostid' or $key === 'deleted') {
3473
                // Unknown or must not be changed.
3474
                continue;
3475
            }
3476
            if (empty($userauth->config->{'field_updatelocal_' . $key}) || empty($userauth->config->{'field_lock_' . $key})) {
3477
                continue;
3478
            }
3479
            $confval = $userauth->config->{'field_updatelocal_' . $key};
3480
            $lockval = $userauth->config->{'field_lock_' . $key};
3481
            if ($confval === 'onlogin') {
3482
                // MDL-4207 Don't overwrite modified user profile values with
3483
                // empty LDAP values when 'unlocked if empty' is set. The purpose
3484
                // of the setting 'unlocked if empty' is to allow the user to fill
3485
                // in a value for the selected field _if LDAP is giving
3486
                // nothing_ for this field. Thus it makes sense to let this value
3487
                // stand in until LDAP is giving a value for this field.
3488
                if (!(empty($value) && $lockval === 'unlockedifempty')) {
3489
                    if ($iscustom || (in_array($key, $userauth->userfields) &&
3490
                            ((string)$oldinfo->$key !== (string)$value))) {
3491
                        $newuser[$key] = (string)$value;
3492
                    }
3493
                }
3494
            }
3495
        }
3496
        if ($newuser) {
3497
            $newuser['id'] = $oldinfo->id;
3498
            $newuser['timemodified'] = time();
3499
            user_update_user((object) $newuser, false, false);
3500
 
3501
            // Save user profile data.
3502
            profile_save_data((object) $newuser);
3503
 
3504
            // Trigger event.
3505
            \core\event\user_updated::create_from_userid($newuser['id'])->trigger();
3506
        }
3507
    }
3508
 
3509
    return get_complete_user_data('id', $oldinfo->id);
3510
}
3511
 
3512
/**
3513
 * Will truncate userinfo as it comes from auth_get_userinfo (from external auth) which may have large fields.
3514
 *
3515
 * @param array $info Array of user properties to truncate if needed
3516
 * @return array The now truncated information that was passed in
3517
 */
3518
function truncate_userinfo(array $info) {
3519
    // Define the limits.
3520
    $limit = array(
3521
        'username'    => 100,
3522
        'idnumber'    => 255,
3523
        'firstname'   => 100,
3524
        'lastname'    => 100,
3525
        'email'       => 100,
3526
        'phone1'      =>  20,
3527
        'phone2'      =>  20,
3528
        'institution' => 255,
3529
        'department'  => 255,
3530
        'address'     => 255,
3531
        'city'        => 120,
3532
        'country'     =>   2,
3533
    );
3534
 
3535
    // Apply where needed.
3536
    foreach (array_keys($info) as $key) {
3537
        if (!empty($limit[$key])) {
3538
            $info[$key] = trim(core_text::substr($info[$key], 0, $limit[$key]));
3539
        }
3540
    }
3541
 
3542
    return $info;
3543
}
3544
 
3545
/**
3546
 * Marks user deleted in internal user database and notifies the auth plugin.
3547
 * Also unenrols user from all roles and does other cleanup.
3548
 *
3549
 * Any plugin that needs to purge user data should register the 'user_deleted' event.
3550
 *
3551
 * @param stdClass $user full user object before delete
3552
 * @return boolean success
3553
 * @throws coding_exception if invalid $user parameter detected
3554
 */
3555
function delete_user(stdClass $user) {
3556
    global $CFG, $DB, $SESSION;
3557
    require_once($CFG->libdir.'/grouplib.php');
3558
    require_once($CFG->libdir.'/gradelib.php');
3559
    require_once($CFG->dirroot.'/message/lib.php');
3560
    require_once($CFG->dirroot.'/user/lib.php');
3561
 
3562
    // Make sure nobody sends bogus record type as parameter.
3563
    if (!property_exists($user, 'id') or !property_exists($user, 'username')) {
3564
        throw new coding_exception('Invalid $user parameter in delete_user() detected');
3565
    }
3566
 
3567
    // Better not trust the parameter and fetch the latest info this will be very expensive anyway.
3568
    if (!$user = $DB->get_record('user', array('id' => $user->id))) {
3569
        debugging('Attempt to delete unknown user account.');
3570
        return false;
3571
    }
3572
 
3573
    // There must be always exactly one guest record, originally the guest account was identified by username only,
3574
    // now we use $CFG->siteguest for performance reasons.
3575
    if ($user->username === 'guest' or isguestuser($user)) {
3576
        debugging('Guest user account can not be deleted.');
3577
        return false;
3578
    }
3579
 
3580
    // Admin can be theoretically from different auth plugin, but we want to prevent deletion of internal accoutns only,
3581
    // if anything goes wrong ppl may force somebody to be admin via config.php setting $CFG->siteadmins.
3582
    if ($user->auth === 'manual' and is_siteadmin($user)) {
3583
        debugging('Local administrator accounts can not be deleted.');
3584
        return false;
3585
    }
3586
    // Allow plugins to use this user object before we completely delete it.
3587
    if ($pluginsfunction = get_plugins_with_function('pre_user_delete')) {
3588
        foreach ($pluginsfunction as $plugintype => $plugins) {
3589
            foreach ($plugins as $pluginfunction) {
3590
                $pluginfunction($user);
3591
            }
3592
        }
3593
    }
3594
 
3595
    // Dispatch the hook for pre user update actions.
3596
    $hook = new \core_user\hook\before_user_deleted(
3597
        user: $user,
3598
    );
3599
    di::get(hook\manager::class)->dispatch($hook);
3600
 
3601
    // Keep user record before updating it, as we have to pass this to user_deleted event.
3602
    $olduser = clone $user;
3603
 
3604
    // Keep a copy of user context, we need it for event.
3605
    $usercontext = context_user::instance($user->id);
3606
 
3607
    // Delete all grades - backup is kept in grade_grades_history table.
3608
    grade_user_delete($user->id);
3609
 
3610
    // TODO: remove from cohorts using standard API here.
3611
 
3612
    // Remove user tags.
3613
    core_tag_tag::remove_all_item_tags('core', 'user', $user->id);
3614
 
3615
    // Unconditionally unenrol from all courses.
3616
    enrol_user_delete($user);
3617
 
3618
    // Unenrol from all roles in all contexts.
3619
    // This might be slow but it is really needed - modules might do some extra cleanup!
3620
    role_unassign_all(array('userid' => $user->id));
3621
 
3622
    // Notify the competency subsystem.
3623
    \core_competency\api::hook_user_deleted($user->id);
3624
 
3625
    // Now do a brute force cleanup.
3626
 
3627
    // Delete all user events and subscription events.
3628
    $DB->delete_records_select('event', 'userid = :userid AND subscriptionid IS NOT NULL', ['userid' => $user->id]);
3629
 
3630
    // Now, delete all calendar subscription from the user.
3631
    $DB->delete_records('event_subscriptions', ['userid' => $user->id]);
3632
 
3633
    // Remove from all cohorts.
3634
    $DB->delete_records('cohort_members', array('userid' => $user->id));
3635
 
3636
    // Remove from all groups.
3637
    $DB->delete_records('groups_members', array('userid' => $user->id));
3638
 
3639
    // Brute force unenrol from all courses.
3640
    $DB->delete_records('user_enrolments', array('userid' => $user->id));
3641
 
3642
    // Purge user preferences.
3643
    $DB->delete_records('user_preferences', array('userid' => $user->id));
3644
 
3645
    // Purge user extra profile info.
3646
    $DB->delete_records('user_info_data', array('userid' => $user->id));
3647
 
3648
    // Purge log of previous password hashes.
3649
    $DB->delete_records('user_password_history', array('userid' => $user->id));
3650
 
3651
    // Last course access not necessary either.
3652
    $DB->delete_records('user_lastaccess', array('userid' => $user->id));
3653
    // Remove all user tokens.
3654
    $DB->delete_records('external_tokens', array('userid' => $user->id));
3655
 
3656
    // Unauthorise the user for all services.
3657
    $DB->delete_records('external_services_users', array('userid' => $user->id));
3658
 
3659
    // Remove users private keys.
3660
    $DB->delete_records('user_private_key', array('userid' => $user->id));
3661
 
3662
    // Remove users customised pages.
3663
    $DB->delete_records('my_pages', array('userid' => $user->id, 'private' => 1));
3664
 
3665
    // Remove user's oauth2 refresh tokens, if present.
3666
    $DB->delete_records('oauth2_refresh_token', array('userid' => $user->id));
3667
 
3668
    // Delete user from $SESSION->bulk_users.
3669
    if (isset($SESSION->bulk_users[$user->id])) {
3670
        unset($SESSION->bulk_users[$user->id]);
3671
    }
3672
 
3673
    // Force logout - may fail if file based sessions used, sorry.
3674
    \core\session\manager::kill_user_sessions($user->id);
3675
 
3676
    // Generate username from email address, or a fake email.
3677
    $delemail = !empty($user->email) ? $user->email : $user->username . '.' . $user->id . '@unknownemail.invalid';
3678
 
3679
    $deltime = time();
3680
 
3681
    // Max username length is 100 chars. Select up to limit - (length of current time + 1 [period character]) from users email.
3682
    $delnameprefix = clean_param($delemail, PARAM_USERNAME);
3683
    $delnamesuffix = $deltime;
3684
    $delnamesuffixlength = 10;
3685
    do {
3686
        // Workaround for bulk deletes of users with the same email address.
3687
        $delname = sprintf(
3688
            "%s.%10d",
3689
            core_text::substr(
3690
                $delnameprefix,
3691
                0,
3692
                // 100 Character maximum, with a '.' character, and a 10-digit timestamp.
3693
                100 - 1 - $delnamesuffixlength,
3694
            ),
3695
            $delnamesuffix,
3696
        );
3697
        $delnamesuffix++;
3698
 
3699
        // No need to use mnethostid here.
3700
    } while ($DB->record_exists('user', ['username' => $delname]));
3701
 
3702
    // Mark internal user record as "deleted".
3703
    $updateuser = new stdClass();
3704
    $updateuser->id           = $user->id;
3705
    $updateuser->deleted      = 1;
3706
    $updateuser->username     = $delname;            // Remember it just in case.
3707
    $updateuser->email        = md5($user->username);// Store hash of username, useful importing/restoring users.
3708
    $updateuser->idnumber     = '';                  // Clear this field to free it up.
3709
    $updateuser->picture      = 0;
3710
    $updateuser->timemodified = $deltime;
3711
 
3712
    // Don't trigger update event, as user is being deleted.
3713
    user_update_user($updateuser, false, false);
3714
 
3715
    // Delete all content associated with the user context, but not the context itself.
3716
    $usercontext->delete_content();
3717
 
3718
    // Delete any search data.
3719
    \core_search\manager::context_deleted($usercontext);
3720
 
3721
    // Any plugin that needs to cleanup should register this event.
3722
    // Trigger event.
3723
    $event = \core\event\user_deleted::create(
3724
            array(
3725
                'objectid' => $user->id,
3726
                'relateduserid' => $user->id,
3727
                'context' => $usercontext,
3728
                'other' => array(
3729
                    'username' => $user->username,
3730
                    'email' => $user->email,
3731
                    'idnumber' => $user->idnumber,
3732
                    'picture' => $user->picture,
3733
                    'mnethostid' => $user->mnethostid
3734
                    )
3735
                )
3736
            );
3737
    $event->add_record_snapshot('user', $olduser);
3738
    $event->trigger();
3739
 
3740
    // We will update the user's timemodified, as it will be passed to the user_deleted event, which
3741
    // should know about this updated property persisted to the user's table.
3742
    $user->timemodified = $updateuser->timemodified;
3743
 
3744
    // Notify auth plugin - do not block the delete even when plugin fails.
3745
    $authplugin = get_auth_plugin($user->auth);
3746
    $authplugin->user_delete($user);
3747
 
3748
    return true;
3749
}
3750
 
3751
/**
3752
 * Retrieve the guest user object.
3753
 *
3754
 * @return stdClass A {@link $USER} object
3755
 */
3756
function guest_user() {
3757
    global $CFG, $DB;
3758
 
3759
    if ($newuser = $DB->get_record('user', array('id' => $CFG->siteguest))) {
3760
        $newuser->confirmed = 1;
3761
        $newuser->lang = get_newuser_language();
3762
        $newuser->lastip = getremoteaddr();
3763
    }
3764
 
3765
    return $newuser;
3766
}
3767
 
3768
/**
3769
 * Authenticates a user against the chosen authentication mechanism
3770
 *
3771
 * Given a username and password, this function looks them
3772
 * up using the currently selected authentication mechanism,
3773
 * and if the authentication is successful, it returns a
3774
 * valid $user object from the 'user' table.
3775
 *
3776
 * Uses auth_ functions from the currently active auth module
3777
 *
3778
 * After authenticate_user_login() returns success, you will need to
3779
 * log that the user has logged in, and call complete_user_login() to set
3780
 * the session up.
3781
 *
3782
 * Note: this function works only with non-mnet accounts!
3783
 *
3784
 * @param string $username  User's username (or also email if $CFG->authloginviaemail enabled)
3785
 * @param string $password  User's password
3786
 * @param bool $ignorelockout useful when guessing is prevented by other mechanism such as captcha or SSO
3787
 * @param int $failurereason login failure reason, can be used in renderers (it may disclose if account exists)
3788
 * @param string|bool $logintoken If this is set to a string it is validated against the login token for the session.
3789
 * @param string|bool $loginrecaptcha If this is set to a string it is validated against Google reCaptcha.
3790
 * @return stdClass|false A {@link $USER} object or false if error
3791
 */
3792
function authenticate_user_login(
3793
    $username,
3794
    $password,
3795
    $ignorelockout = false,
3796
    &$failurereason = null,
3797
    $logintoken = false,
3798
    string|bool $loginrecaptcha = false,
3799
) {
3800
    global $CFG, $DB, $PAGE, $SESSION;
3801
    require_once("$CFG->libdir/authlib.php");
3802
 
3803
    if ($user = get_complete_user_data('username', $username, $CFG->mnet_localhost_id)) {
3804
        // we have found the user
3805
 
3806
    } else if (!empty($CFG->authloginviaemail)) {
3807
        if ($email = clean_param($username, PARAM_EMAIL)) {
3808
            $select = "mnethostid = :mnethostid AND LOWER(email) = LOWER(:email) AND deleted = 0";
3809
            $params = array('mnethostid' => $CFG->mnet_localhost_id, 'email' => $email);
3810
            $users = $DB->get_records_select('user', $select, $params, 'id', 'id', 0, 2);
3811
            if (count($users) === 1) {
3812
                // Use email for login only if unique.
3813
                $user = reset($users);
3814
                $user = get_complete_user_data('id', $user->id);
3815
                $username = $user->username;
3816
            }
3817
            unset($users);
3818
        }
3819
    }
3820
 
3821
    // Make sure this request came from the login form.
3822
    if (!\core\session\manager::validate_login_token($logintoken)) {
3823
        $failurereason = AUTH_LOGIN_FAILED;
3824
 
3825
        // Trigger login failed event (specifying the ID of the found user, if available).
3826
        \core\event\user_login_failed::create([
3827
            'userid' => ($user->id ?? 0),
3828
            'other' => [
3829
                'username' => $username,
3830
                'reason' => $failurereason,
3831
            ],
3832
        ])->trigger();
3833
 
3834
        error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Invalid Login Token:  $username  ".$_SERVER['HTTP_USER_AGENT']);
3835
        return false;
3836
    }
3837
 
3838
    // Login reCaptcha.
3839
    if (login_captcha_enabled() && !validate_login_captcha($loginrecaptcha)) {
3840
        $failurereason = AUTH_LOGIN_FAILED_RECAPTCHA;
3841
        // Trigger login failed event (specifying the ID of the found user, if available).
3842
        \core\event\user_login_failed::create([
3843
            'userid' => ($user->id ?? 0),
3844
            'other' => [
3845
                'username' => $username,
3846
                'reason' => $failurereason,
3847
            ],
3848
        ])->trigger();
3849
        return false;
3850
    }
3851
 
3852
    $authsenabled = get_enabled_auth_plugins();
3853
 
3854
    if ($user) {
3855
        // Use manual if auth not set.
3856
        $auth = empty($user->auth) ? 'manual' : $user->auth;
3857
 
3858
        if (in_array($user->auth, $authsenabled)) {
3859
            $authplugin = get_auth_plugin($user->auth);
3860
            $authplugin->pre_user_login_hook($user);
3861
        }
3862
 
3863
        if (!empty($user->suspended)) {
3864
            $failurereason = AUTH_LOGIN_SUSPENDED;
3865
 
3866
            // Trigger login failed event.
3867
            $event = \core\event\user_login_failed::create(array('userid' => $user->id,
3868
                    'other' => array('username' => $username, 'reason' => $failurereason)));
3869
            $event->trigger();
3870
            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Suspended Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
3871
            return false;
3872
        }
3873
        if ($auth=='nologin' or !is_enabled_auth($auth)) {
3874
            // Legacy way to suspend user.
3875
            $failurereason = AUTH_LOGIN_SUSPENDED;
3876
 
3877
            // Trigger login failed event.
3878
            $event = \core\event\user_login_failed::create(array('userid' => $user->id,
3879
                    'other' => array('username' => $username, 'reason' => $failurereason)));
3880
            $event->trigger();
3881
            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Disabled Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
3882
            return false;
3883
        }
3884
        $auths = array($auth);
3885
 
3886
    } else {
3887
        // Check if there's a deleted record (cheaply), this should not happen because we mangle usernames in delete_user().
3888
        if ($DB->get_field('user', 'id', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id,  'deleted' => 1))) {
3889
            $failurereason = AUTH_LOGIN_NOUSER;
3890
 
3891
            // Trigger login failed event.
3892
            $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
3893
                    'reason' => $failurereason)));
3894
            $event->trigger();
3895
            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Deleted Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
3896
            return false;
3897
        }
3898
 
3899
        // User does not exist.
3900
        $auths = $authsenabled;
3901
        $user = new stdClass();
3902
        $user->id = 0;
3903
    }
3904
 
3905
    if ($ignorelockout) {
3906
        // Some other mechanism protects against brute force password guessing, for example login form might include reCAPTCHA
3907
        // or this function is called from a SSO script.
3908
    } else if ($user->id) {
3909
        // Verify login lockout after other ways that may prevent user login.
3910
        if (login_is_lockedout($user)) {
3911
            $failurereason = AUTH_LOGIN_LOCKOUT;
3912
 
3913
            // Trigger login failed event.
3914
            $event = \core\event\user_login_failed::create(array('userid' => $user->id,
3915
                    'other' => array('username' => $username, 'reason' => $failurereason)));
3916
            $event->trigger();
3917
 
3918
            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Login lockout:  $username  ".$_SERVER['HTTP_USER_AGENT']);
3919
            $SESSION->loginerrormsg = get_string('accountlocked', 'admin');
3920
 
3921
            return false;
3922
        }
3923
    } else {
3924
        // We can not lockout non-existing accounts.
3925
    }
3926
 
3927
    foreach ($auths as $auth) {
3928
        $authplugin = get_auth_plugin($auth);
3929
 
3930
        // On auth fail fall through to the next plugin.
3931
        if (!$authplugin->user_login($username, $password)) {
3932
            continue;
3933
        }
3934
 
3935
        // Before performing login actions, check if user still passes password policy, if admin setting is enabled.
3936
        if (!empty($CFG->passwordpolicycheckonlogin)) {
3937
            $errmsg = '';
3938
            $passed = check_password_policy($password, $errmsg, $user);
3939
            if (!$passed) {
3940
                // First trigger event for failure.
3941
                $failedevent = \core\event\user_password_policy_failed::create_from_user($user);
3942
                $failedevent->trigger();
3943
 
3944
                // If able to change password, set flag and move on.
3945
                if ($authplugin->can_change_password()) {
3946
                    // Check if we are on internal change password page, or service is external, don't show notification.
3947
                    $internalchangeurl = new moodle_url('/login/change_password.php');
3948
                    if (!($PAGE->has_set_url() && $internalchangeurl->compare($PAGE->url)) && $authplugin->is_internal()) {
3949
                        \core\notification::error(get_string('passwordpolicynomatch', '', $errmsg));
3950
                    }
3951
                    set_user_preference('auth_forcepasswordchange', 1, $user);
3952
                } else if ($authplugin->can_reset_password()) {
3953
                    // Else force a reset if possible.
3954
                    \core\notification::error(get_string('forcepasswordresetnotice', '', $errmsg));
3955
                    redirect(new moodle_url('/login/forgot_password.php'));
3956
                } else {
3957
                    $notifymsg = get_string('forcepasswordresetfailurenotice', '', $errmsg);
3958
                    // If support page is set, add link for help.
3959
                    if (!empty($CFG->supportpage)) {
3960
                        $link = \html_writer::link($CFG->supportpage, $CFG->supportpage);
3961
                        $link = \html_writer::tag('p', $link);
3962
                        $notifymsg .= $link;
3963
                    }
3964
 
3965
                    // If no change or reset is possible, add a notification for user.
3966
                    \core\notification::error($notifymsg);
3967
                }
3968
            }
3969
        }
3970
 
3971
        // Successful authentication.
3972
        if ($user->id) {
3973
            // User already exists in database.
3974
            if (empty($user->auth)) {
3975
                // For some reason auth isn't set yet.
3976
                $DB->set_field('user', 'auth', $auth, array('id' => $user->id));
3977
                $user->auth = $auth;
3978
            }
3979
 
3980
            // If the existing hash is using an out-of-date algorithm (or the legacy md5 algorithm), then we should update to
3981
            // the current hash algorithm while we have access to the user's password.
3982
            update_internal_user_password($user, $password);
3983
 
3984
            if ($authplugin->is_synchronised_with_external()) {
3985
                // Update user record from external DB.
3986
                $user = update_user_record_by_id($user->id);
3987
            }
3988
        } else {
3989
            // The user is authenticated but user creation may be disabled.
3990
            if (!empty($CFG->authpreventaccountcreation)) {
3991
                $failurereason = AUTH_LOGIN_UNAUTHORISED;
3992
 
3993
                // Trigger login failed event.
3994
                $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
3995
                        'reason' => $failurereason)));
3996
                $event->trigger();
3997
 
3998
                error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Unknown user, can not create new accounts:  $username  ".
3999
                        $_SERVER['HTTP_USER_AGENT']);
4000
                return false;
4001
            } else {
4002
                $user = create_user_record($username, $password, $auth);
4003
            }
4004
        }
4005
 
4006
        $authplugin->sync_roles($user);
4007
 
4008
        foreach ($authsenabled as $hau) {
4009
            $hauth = get_auth_plugin($hau);
4010
            $hauth->user_authenticated_hook($user, $username, $password);
4011
        }
4012
 
4013
        if (empty($user->id)) {
4014
            $failurereason = AUTH_LOGIN_NOUSER;
4015
            // Trigger login failed event.
4016
            $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4017
                    'reason' => $failurereason)));
4018
            $event->trigger();
4019
            return false;
4020
        }
4021
 
4022
        if (!empty($user->suspended)) {
4023
            // Just in case some auth plugin suspended account.
4024
            $failurereason = AUTH_LOGIN_SUSPENDED;
4025
            // Trigger login failed event.
4026
            $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4027
                    'other' => array('username' => $username, 'reason' => $failurereason)));
4028
            $event->trigger();
4029
            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Suspended Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4030
            return false;
4031
        }
4032
 
4033
        login_attempt_valid($user);
4034
        $failurereason = AUTH_LOGIN_OK;
4035
        return $user;
4036
    }
4037
 
4038
    // Failed if all the plugins have failed.
4039
    if (debugging('', DEBUG_ALL)) {
4040
        error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Failed Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4041
    }
4042
 
4043
    if ($user->id) {
4044
        login_attempt_failed($user);
4045
        $failurereason = AUTH_LOGIN_FAILED;
4046
        // Trigger login failed event.
4047
        $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4048
                'other' => array('username' => $username, 'reason' => $failurereason)));
4049
        $event->trigger();
4050
    } else {
4051
        $failurereason = AUTH_LOGIN_NOUSER;
4052
        // Trigger login failed event.
4053
        $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4054
                'reason' => $failurereason)));
4055
        $event->trigger();
4056
    }
4057
 
4058
    return false;
4059
}
4060
 
4061
/**
4062
 * Call to complete the user login process after authenticate_user_login()
4063
 * has succeeded. It will setup the $USER variable and other required bits
4064
 * and pieces.
4065
 *
4066
 * NOTE:
4067
 * - It will NOT log anything -- up to the caller to decide what to log.
4068
 * - this function does not set any cookies any more!
4069
 *
4070
 * @param stdClass $user
4071
 * @param array $extrauserinfo
4072
 * @return stdClass A {@link $USER} object - BC only, do not use
4073
 */
4074
function complete_user_login($user, array $extrauserinfo = []) {
4075
    global $CFG, $DB, $USER, $SESSION;
4076
 
4077
    \core\session\manager::login_user($user);
4078
 
4079
    // Reload preferences from DB.
4080
    unset($USER->preference);
4081
    check_user_preferences_loaded($USER);
4082
 
4083
    // Update login times.
4084
    update_user_login_times();
4085
 
4086
    // Extra session prefs init.
4087
    set_login_session_preferences();
4088
 
4089
    // Trigger login event.
4090
    $event = \core\event\user_loggedin::create(
4091
        array(
4092
            'userid' => $USER->id,
4093
            'objectid' => $USER->id,
4094
            'other' => [
4095
                'username' => $USER->username,
4096
                'extrauserinfo' => $extrauserinfo
4097
            ]
4098
        )
4099
    );
4100
    $event->trigger();
4101
 
4102
    // Allow plugins to callback as soon possible after user has completed login.
4103
    di::get(\core\hook\manager::class)->dispatch(new \core_user\hook\after_login_completed());
4104
 
4105
    // Check if the user is using a new browser or session (a new MoodleSession cookie is set in that case).
4106
    // If the user is accessing from the same IP, ignore everything (most of the time will be a new session in the same browser).
4107
    // Skip Web Service requests, CLI scripts, AJAX scripts, and request from the mobile app itself.
4108
    $loginip = getremoteaddr();
4109
    $isnewip = isset($SESSION->userpreviousip) && $SESSION->userpreviousip != $loginip;
4110
    $isvalidenv = (!WS_SERVER && !CLI_SCRIPT && !NO_MOODLE_COOKIES) || PHPUNIT_TEST;
4111
 
4112
    if (!empty($SESSION->isnewsessioncookie) && $isnewip && $isvalidenv && !\core_useragent::is_moodle_app()) {
4113
 
4114
        $logintime = time();
4115
        $ismoodleapp = false;
4116
        $useragent = \core_useragent::get_user_agent_string();
4117
 
4118
        $sitepreferences = get_message_output_default_preferences();
4119
        // Check if new login notification is disabled at system level.
4120
        $newlogindisabled = $sitepreferences->moodle_newlogin_disable ?? 0;
4121
        // Check if message providers (web, email, mobile) are enabled at system level.
4122
        $msgproviderenabled = isset($sitepreferences->message_provider_moodle_newlogin_enabled);
4123
        // Get message providers enabled for a user.
4124
        $userpreferences = get_user_preferences('message_provider_moodle_newlogin_enabled');
4125
        // Check if notification processor plugins (web, email, mobile) are enabled at system level.
4126
        $msgprocessorsready = !empty(get_message_processors(true));
4127
        // If new login notification is enabled at system level then go for other conditions check.
4128
        $newloginenabled = $newlogindisabled ? 0 : ($userpreferences != 'none' && $msgproviderenabled);
4129
 
4130
        if ($newloginenabled && $msgprocessorsready) {
4131
            // Schedule adhoc task to send a login notification to the user.
4132
            $task = new \core\task\send_login_notifications();
4133
            $task->set_userid($USER->id);
4134
            $task->set_custom_data(compact('ismoodleapp', 'useragent', 'loginip', 'logintime'));
4135
            $task->set_component('core');
4136
            \core\task\manager::queue_adhoc_task($task);
4137
        }
4138
    }
4139
 
4140
    // Queue migrating the messaging data, if we need to.
4141
    if (!get_user_preferences('core_message_migrate_data', false, $USER->id)) {
4142
        // Check if there are any legacy messages to migrate.
4143
        if (\core_message\helper::legacy_messages_exist($USER->id)) {
4144
            \core_message\task\migrate_message_data::queue_task($USER->id);
4145
        } else {
4146
            set_user_preference('core_message_migrate_data', true, $USER->id);
4147
        }
4148
    }
4149
 
4150
    if (isguestuser()) {
4151
        // No need to continue when user is THE guest.
4152
        return $USER;
4153
    }
4154
 
4155
    if (CLI_SCRIPT) {
4156
        // We can redirect to password change URL only in browser.
4157
        return $USER;
4158
    }
4159
 
4160
    // Select password change url.
4161
    $userauth = get_auth_plugin($USER->auth);
4162
 
4163
    // Check whether the user should be changing password.
4164
    if (get_user_preferences('auth_forcepasswordchange', false)) {
4165
        if ($userauth->can_change_password()) {
4166
            if ($changeurl = $userauth->change_password_url()) {
4167
                redirect($changeurl);
4168
            } else {
4169
                require_once($CFG->dirroot . '/login/lib.php');
4170
                $SESSION->wantsurl = core_login_get_return_url();
4171
                redirect($CFG->wwwroot.'/login/change_password.php');
4172
            }
4173
        } else {
4174
            throw new \moodle_exception('nopasswordchangeforced', 'auth');
4175
        }
4176
    }
4177
    return $USER;
4178
}
4179
 
4180
/**
4181
 * Check a password hash to see if it was hashed using the legacy hash algorithm (bcrypt).
4182
 *
4183
 * @param string $password String to check.
4184
 * @return bool True if the $password matches the format of a bcrypt hash.
4185
 */
4186
function password_is_legacy_hash(#[\SensitiveParameter] string $password): bool {
4187
    return (bool) preg_match('/^\$2y\$[\d]{2}\$[A-Za-z0-9\.\/]{53}$/', $password);
4188
}
4189
 
4190
/**
4191
 * Calculate the Shannon entropy of a string.
4192
 *
4193
 * @param string $pepper The pepper to calculate the entropy of.
4194
 * @return float The Shannon entropy of the string.
4195
 */
4196
function calculate_entropy(#[\SensitiveParameter] string $pepper): float {
4197
    // Initialize entropy.
4198
    $h = 0;
4199
 
4200
    // Calculate the length of the string.
4201
    $size = strlen($pepper);
4202
 
4203
    // For each unique character in the string.
4204
    foreach (count_chars($pepper, 1) as $v) {
4205
        // Calculate the probability of the character.
4206
        $p = $v / $size;
4207
 
4208
        // Add the character's contribution to the total entropy.
4209
        // This uses the formula for the entropy of a discrete random variable.
4210
        $h -= $p * log($p) / log(2);
4211
    }
4212
 
4213
    // Instead of returning the average entropy per symbol (Shannon entropy),
4214
    // we multiply by the length of the string to get total entropy.
4215
    return $h * $size;
4216
}
4217
 
4218
/**
4219
 * Get the available password peppers.
4220
 * The latest pepper is checked for minimum entropy as part of this function.
4221
 * We only calculate the entropy of the most recent pepper,
4222
 * because passwords are always updated to the latest pepper,
4223
 * and in the past we may have enforced a lower minimum entropy.
4224
 * Also, we allow the latest pepper to be empty, to allow admins to migrate off peppers.
4225
 *
4226
 * @return array The password peppers.
4227
 * @throws coding_exception If the entropy of the password pepper is less than the recommended minimum.
4228
 */
4229
function get_password_peppers(): array {
4230
    global $CFG;
4231
 
4232
    // Get all available peppers.
4233
    if (isset($CFG->passwordpeppers) && is_array($CFG->passwordpeppers)) {
4234
        // Sort the array in descending order of keys (numerical).
4235
        $peppers = $CFG->passwordpeppers;
4236
        krsort($peppers, SORT_NUMERIC);
4237
    } else {
4238
        $peppers = [];  // Set an empty array if no peppers are found.
4239
    }
4240
 
4241
    // Check if the entropy of the most recent pepper is less than the minimum.
4242
    // Also, we allow the most recent pepper to be empty, to allow admins to migrate off peppers.
4243
    $lastpepper = reset($peppers);
4244
    if (!empty($peppers) && $lastpepper !== '' && calculate_entropy($lastpepper) < PEPPER_ENTROPY) {
4245
        throw new coding_exception(
4246
                'password pepper below minimum',
4247
                'The entropy of the password pepper is less than the recommended minimum.');
4248
    }
4249
    return $peppers;
4250
}
4251
 
4252
/**
4253
 * Compare password against hash stored in user object to determine if it is valid.
4254
 *
4255
 * If necessary it also updates the stored hash to the current format.
4256
 *
4257
 * @param stdClass $user (Password property may be updated).
4258
 * @param string $password Plain text password.
4259
 * @return bool True if password is valid.
4260
 */
4261
function validate_internal_user_password(stdClass $user, #[\SensitiveParameter] string $password): bool {
4262
 
4263
    if (exceeds_password_length($password)) {
4264
        // Password cannot be more than MAX_PASSWORD_CHARACTERS characters.
4265
        return false;
4266
    }
4267
 
4268
    if ($user->password === AUTH_PASSWORD_NOT_CACHED) {
4269
        // Internal password is not used at all, it can not validate.
4270
        return false;
4271
    }
4272
 
4273
    $peppers = get_password_peppers(); // Get the array of available peppers.
4274
    $islegacy = password_is_legacy_hash($user->password); // Check if the password is a legacy bcrypt hash.
4275
 
4276
    // If the password is a legacy hash, no peppers were used, so verify and update directly.
4277
    if ($islegacy && password_verify($password, $user->password)) {
4278
        update_internal_user_password($user, $password);
4279
        return true;
4280
    }
4281
 
4282
    // If the password is not a legacy hash, iterate through the peppers.
4283
    $latestpepper = reset($peppers);
4284
    // Add an empty pepper to the beginning of the array. To make it easier to check if the password matches without any pepper.
4285
    $peppers = [-1 => ''] + $peppers;
4286
    foreach ($peppers as $pepper) {
4287
        $pepperedpassword = $password . $pepper;
4288
 
4289
        // If the peppered password is correct, update (if necessary) and return true.
4290
        if (password_verify($pepperedpassword, $user->password)) {
4291
            // If the pepper used is not the latest one, update the password.
4292
            if ($pepper !== $latestpepper) {
4293
                update_internal_user_password($user, $password);
4294
            }
4295
            return true;
4296
        }
4297
    }
4298
 
4299
    // If no peppered password was correct, the password is wrong.
4300
    return false;
4301
}
4302
 
4303
/**
4304
 * Calculate hash for a plain text password.
4305
 *
4306
 * @param string $password Plain text password to be hashed.
4307
 * @param bool $fasthash If true, use a low number of rounds when generating the hash
4308
 *                       This is faster to generate but makes the hash less secure.
4309
 *                       It is used when lots of hashes need to be generated quickly.
4310
 * @param int $pepperlength Lenght of the peppers
4311
 * @return string The hashed password.
4312
 *
4313
 * @throws moodle_exception If a problem occurs while generating the hash.
4314
 */
4315
function hash_internal_user_password(#[\SensitiveParameter] string $password, $fasthash = false, $pepperlength = 0): string {
4316
    if (exceeds_password_length($password, $pepperlength)) {
4317
        // Password cannot be more than MAX_PASSWORD_CHARACTERS.
4318
        throw new \moodle_exception(get_string("passwordexceeded", 'error', MAX_PASSWORD_CHARACTERS));
4319
    }
4320
 
4321
    // Set the cost factor to 5000 for fast hashing, otherwise use default cost.
4322
    $rounds = $fasthash ? 5000 : 10000;
4323
 
4324
    // First generate a cryptographically suitable salt.
4325
    $randombytes = random_bytes(16);
4326
    $salt = substr(strtr(base64_encode($randombytes), '+', '.'), 0, 16);
4327
 
4328
    // Now construct the password string with the salt and number of rounds.
4329
    // The password string is in the format $algorithm$rounds$salt$hash. ($6 is the SHA512 algorithm).
4330
    $generatedhash = crypt($password, implode('$', [
4331
        '',
4332
        // The SHA512 Algorithm
4333
        '6',
4334
        "rounds={$rounds}",
4335
        $salt,
4336
        '',
4337
    ]));
4338
 
4339
    if ($generatedhash === false || $generatedhash === null) {
4340
        throw new moodle_exception('Failed to generate password hash.');
4341
    }
4342
 
4343
    return $generatedhash;
4344
}
4345
 
4346
/**
4347
 * Update password hash in user object (if necessary).
4348
 *
4349
 * The password is updated if:
4350
 * 1. The password has changed (the hash of $user->password is different
4351
 *    to the hash of $password).
4352
 * 2. The existing hash is using an out-of-date algorithm (or the legacy
4353
 *    md5 algorithm).
4354
 *
4355
 * The password is peppered with the latest pepper before hashing,
4356
 * if peppers are available.
4357
 * Updating the password will modify the $user object and the database
4358
 * record to use the current hashing algorithm.
4359
 * It will remove Web Services user tokens too.
4360
 *
4361
 * @param stdClass $user User object (password property may be updated).
11 efrain 4362
 * @param string|null $password Plain text password.
1 efrain 4363
 * @param bool $fasthash If true, use a low cost factor when generating the hash
4364
 *                       This is much faster to generate but makes the hash
4365
 *                       less secure. It is used when lots of hashes need to
4366
 *                       be generated quickly.
4367
 * @return bool Always returns true.
4368
 */
4369
function update_internal_user_password(
4370
        stdClass $user,
11 efrain 4371
        #[\SensitiveParameter] ?string $password,
1 efrain 4372
        bool $fasthash = false
4373
): bool {
4374
    global $CFG, $DB;
4375
 
4376
    // Add the latest password pepper to the password before further processing.
4377
    $peppers = get_password_peppers();
4378
    if (!empty($peppers)) {
4379
        $password = $password . reset($peppers);
4380
    }
4381
 
4382
    // Figure out what the hashed password should be.
4383
    if (!isset($user->auth)) {
4384
        debugging('User record in update_internal_user_password() must include field auth',
4385
                DEBUG_DEVELOPER);
4386
        $user->auth = $DB->get_field('user', 'auth', array('id' => $user->id));
4387
    }
4388
    $authplugin = get_auth_plugin($user->auth);
4389
    if ($authplugin->prevent_local_passwords()) {
4390
        $hashedpassword = AUTH_PASSWORD_NOT_CACHED;
4391
    } else {
4392
        $hashedpassword = hash_internal_user_password($password, $fasthash);
4393
    }
4394
 
4395
    $algorithmchanged = false;
4396
 
4397
    if ($hashedpassword === AUTH_PASSWORD_NOT_CACHED) {
4398
        // Password is not cached, update it if not set to AUTH_PASSWORD_NOT_CACHED.
4399
        $passwordchanged = ($user->password !== $hashedpassword);
4400
 
4401
    } else if (isset($user->password)) {
4402
        // If verification fails then it means the password has changed.
4403
        $passwordchanged = !password_verify($password, $user->password);
4404
        $algorithmchanged = password_is_legacy_hash($user->password);
4405
    } else {
4406
        // While creating new user, password in unset in $user object, to avoid
4407
        // saving it with user_create()
4408
        $passwordchanged = true;
4409
    }
4410
 
4411
    if ($passwordchanged || $algorithmchanged) {
4412
        $DB->set_field('user', 'password',  $hashedpassword, array('id' => $user->id));
4413
        $user->password = $hashedpassword;
4414
 
4415
        // Trigger event.
4416
        $user = $DB->get_record('user', array('id' => $user->id));
4417
        \core\event\user_password_updated::create_from_user($user)->trigger();
4418
 
4419
        // Remove WS user tokens.
4420
        if (!empty($CFG->passwordchangetokendeletion)) {
4421
            require_once($CFG->dirroot.'/webservice/lib.php');
4422
            webservice::delete_user_ws_tokens($user->id);
4423
        }
4424
    }
4425
 
4426
    return true;
4427
}
4428
 
4429
/**
4430
 * Get a complete user record, which includes all the info in the user record.
4431
 *
4432
 * Intended for setting as $USER session variable
4433
 *
4434
 * @param string $field The user field to be checked for a given value.
4435
 * @param string $value The value to match for $field.
4436
 * @param int $mnethostid
4437
 * @param bool $throwexception If true, it will throw an exception when there's no record found or when there are multiple records
4438
 *                              found. Otherwise, it will just return false.
4439
 * @return mixed False, or A {@link $USER} object.
4440
 */
4441
function get_complete_user_data($field, $value, $mnethostid = null, $throwexception = false) {
4442
    global $CFG, $DB;
4443
 
4444
    if (!$field || !$value) {
4445
        return false;
4446
    }
4447
 
4448
    // Change the field to lowercase.
4449
    $field = core_text::strtolower($field);
4450
 
4451
    // List of case insensitive fields.
4452
    $caseinsensitivefields = ['email'];
4453
 
4454
    // Username input is forced to lowercase and should be case sensitive.
4455
    if ($field == 'username') {
4456
        $value = core_text::strtolower($value);
4457
    }
4458
 
4459
    // Build the WHERE clause for an SQL query.
4460
    $params = array('fieldval' => $value);
4461
 
4462
    // Do a case-insensitive query, if necessary. These are generally very expensive. The performance can be improved on some DBs
4463
    // such as MySQL by pre-filtering users with accent-insensitive subselect.
4464
    if (in_array($field, $caseinsensitivefields)) {
4465
        $fieldselect = $DB->sql_equal($field, ':fieldval', false);
4466
        $idsubselect = $DB->sql_equal($field, ':fieldval2', false, false);
4467
        $params['fieldval2'] = $value;
4468
    } else {
4469
        $fieldselect = "$field = :fieldval";
4470
        $idsubselect = '';
4471
    }
4472
    $constraints = "$fieldselect AND deleted <> 1";
4473
 
4474
    // If we are loading user data based on anything other than id,
4475
    // we must also restrict our search based on mnet host.
4476
    if ($field != 'id') {
4477
        if (empty($mnethostid)) {
4478
            // If empty, we restrict to local users.
4479
            $mnethostid = $CFG->mnet_localhost_id;
4480
        }
4481
    }
4482
    if (!empty($mnethostid)) {
4483
        $params['mnethostid'] = $mnethostid;
4484
        $constraints .= " AND mnethostid = :mnethostid";
4485
    }
4486
 
4487
    if ($idsubselect) {
4488
        $constraints .= " AND id IN (SELECT id FROM {user} WHERE {$idsubselect})";
4489
    }
4490
 
4491
    // Get all the basic user data.
4492
    try {
4493
        // Make sure that there's only a single record that matches our query.
4494
        // For example, when fetching by email, multiple records might match the query as there's no guarantee that email addresses
4495
        // are unique. Therefore we can't reliably tell whether the user profile data that we're fetching is the correct one.
4496
        $user = $DB->get_record_select('user', $constraints, $params, '*', MUST_EXIST);
4497
    } catch (dml_exception $exception) {
4498
        if ($throwexception) {
4499
            throw $exception;
4500
        } else {
4501
            // Return false when no records or multiple records were found.
4502
            return false;
4503
        }
4504
    }
4505
 
4506
    // Get various settings and preferences.
4507
 
4508
    // Preload preference cache.
4509
    check_user_preferences_loaded($user);
4510
 
4511
    // Load course enrolment related stuff.
4512
    $user->lastcourseaccess    = array(); // During last session.
4513
    $user->currentcourseaccess = array(); // During current session.
4514
    if ($lastaccesses = $DB->get_records('user_lastaccess', array('userid' => $user->id))) {
4515
        foreach ($lastaccesses as $lastaccess) {
4516
            $user->lastcourseaccess[$lastaccess->courseid] = $lastaccess->timeaccess;
4517
        }
4518
    }
4519
 
4520
    // Add cohort theme.
4521
    if (!empty($CFG->allowcohortthemes)) {
4522
        require_once($CFG->dirroot . '/cohort/lib.php');
4523
        if ($cohorttheme = cohort_get_user_cohort_theme($user->id)) {
4524
            $user->cohorttheme = $cohorttheme;
4525
        }
4526
    }
4527
 
4528
    // Add the custom profile fields to the user record.
4529
    $user->profile = array();
4530
    if (!isguestuser($user)) {
4531
        require_once($CFG->dirroot.'/user/profile/lib.php');
4532
        profile_load_custom_fields($user);
4533
    }
4534
 
4535
    // Rewrite some variables if necessary.
4536
    if (!empty($user->description)) {
4537
        // No need to cart all of it around.
4538
        $user->description = true;
4539
    }
4540
    if (isguestuser($user)) {
4541
        // Guest language always same as site.
4542
        $user->lang = get_newuser_language();
4543
        // Name always in current language.
4544
        $user->firstname = get_string('guestuser');
4545
        $user->lastname = ' ';
4546
    }
4547
 
4548
    return $user;
4549
}
4550
 
4551
/**
4552
 * Validate a password against the configured password policy
4553
 *
4554
 * @param string $password the password to be checked against the password policy
4555
 * @param string|null $errmsg the error message to display when the password doesn't comply with the policy.
4556
 * @param stdClass|null $user the user object to perform password validation against. Defaults to null if not provided.
4557
 *
4558
 * @return bool true if the password is valid according to the policy. false otherwise.
4559
 */
4560
function check_password_policy(string $password, ?string &$errmsg, ?stdClass $user = null) {
4561
    global $CFG;
4562
    if (!empty($CFG->passwordpolicy) && !isguestuser($user)) {
4563
        $errors = get_password_policy_errors($password, $user);
4564
 
4565
        foreach ($errors as $error) {
4566
            $errmsg .= '<div>' . $error . '</div>';
4567
        }
4568
    }
4569
 
4570
    return $errmsg == '';
4571
}
4572
 
4573
/**
4574
 * Validate a password against the configured password policy.
4575
 * Note: This function is unaffected by whether the password policy is enabled or not.
4576
 *
4577
 * @param string $password the password to be checked against the password policy
4578
 * @param stdClass|null $user the user object to perform password validation against. Defaults to null if not provided.
4579
 *
4580
 * @return string[] Array of error messages.
4581
 */
4582
function get_password_policy_errors(string $password, ?stdClass $user = null) : array {
4583
    global $CFG;
4584
 
4585
    $errors = [];
4586
 
4587
    if (core_text::strlen($password) < $CFG->minpasswordlength) {
4588
        $errors[] = get_string('errorminpasswordlength', 'auth', $CFG->minpasswordlength);
4589
    }
4590
    if (preg_match_all('/[[:digit:]]/u', $password, $matches) < $CFG->minpassworddigits) {
4591
        $errors[] = get_string('errorminpassworddigits', 'auth', $CFG->minpassworddigits);
4592
    }
4593
    if (preg_match_all('/[[:lower:]]/u', $password, $matches) < $CFG->minpasswordlower) {
4594
        $errors[] = get_string('errorminpasswordlower', 'auth', $CFG->minpasswordlower);
4595
    }
4596
    if (preg_match_all('/[[:upper:]]/u', $password, $matches) < $CFG->minpasswordupper) {
4597
        $errors[] = get_string('errorminpasswordupper', 'auth', $CFG->minpasswordupper);
4598
    }
4599
    if (preg_match_all('/[^[:upper:][:lower:][:digit:]]/u', $password, $matches) < $CFG->minpasswordnonalphanum) {
4600
        $errors[] = get_string('errorminpasswordnonalphanum', 'auth', $CFG->minpasswordnonalphanum);
4601
    }
4602
    if (!check_consecutive_identical_characters($password, $CFG->maxconsecutiveidentchars)) {
4603
        $errors[] = get_string('errormaxconsecutiveidentchars', 'auth', $CFG->maxconsecutiveidentchars);
4604
    }
4605
 
4606
    // Fire any additional password policy functions from plugins.
4607
    // Plugin functions should output an error message string or empty string for success.
4608
    $pluginsfunction = get_plugins_with_function('check_password_policy');
4609
    foreach ($pluginsfunction as $plugintype => $plugins) {
4610
        foreach ($plugins as $pluginfunction) {
4611
            $pluginerr = $pluginfunction($password, $user);
4612
            if ($pluginerr) {
4613
                $errors[] = $pluginerr;
4614
            }
4615
        }
4616
    }
4617
 
4618
    return $errors;
4619
}
4620
 
4621
/**
4622
 * When logging in, this function is run to set certain preferences for the current SESSION.
4623
 */
4624
function set_login_session_preferences() {
4625
    global $SESSION;
4626
 
4627
    $SESSION->justloggedin = true;
4628
 
4629
    unset($SESSION->lang);
4630
    unset($SESSION->forcelang);
4631
    unset($SESSION->load_navigation_admin);
4632
}
4633
 
4634
 
4635
/**
4636
 * Delete a course, including all related data from the database, and any associated files.
4637
 *
4638
 * @param mixed $courseorid The id of the course or course object to delete.
4639
 * @param bool $showfeedback Whether to display notifications of each action the function performs.
4640
 * @return bool true if all the removals succeeded. false if there were any failures. If this
4641
 *             method returns false, some of the removals will probably have succeeded, and others
4642
 *             failed, but you have no way of knowing which.
4643
 */
4644
function delete_course($courseorid, $showfeedback = true) {
4645
    global $DB, $CFG;
4646
 
4647
    if (is_object($courseorid)) {
4648
        $courseid = $courseorid->id;
4649
        $course   = $courseorid;
4650
    } else {
4651
        $courseid = $courseorid;
4652
        if (!$course = $DB->get_record('course', array('id' => $courseid))) {
4653
            return false;
4654
        }
4655
    }
4656
    $context = context_course::instance($courseid);
4657
 
4658
    // Frontpage course can not be deleted!!
4659
    if ($courseid == SITEID) {
4660
        return false;
4661
    }
4662
 
4663
    // Allow plugins to use this course before we completely delete it.
4664
    if ($pluginsfunction = get_plugins_with_function('pre_course_delete')) {
4665
        foreach ($pluginsfunction as $plugintype => $plugins) {
4666
            foreach ($plugins as $pluginfunction) {
4667
                $pluginfunction($course);
4668
            }
4669
        }
4670
    }
4671
 
4672
    // Dispatch the hook for pre course delete actions.
4673
    $hook = new \core_course\hook\before_course_deleted(
4674
        course: $course,
4675
    );
4676
    \core\di::get(\core\hook\manager::class)->dispatch($hook);
4677
 
4678
    // Tell the search manager we are about to delete a course. This prevents us sending updates
4679
    // for each individual context being deleted.
4680
    \core_search\manager::course_deleting_start($courseid);
4681
 
4682
    $handler = core_course\customfield\course_handler::create();
4683
    $handler->delete_instance($courseid);
4684
 
4685
    // Make the course completely empty.
4686
    remove_course_contents($courseid, $showfeedback);
4687
 
4688
    // Delete the course and related context instance.
4689
    context_helper::delete_instance(CONTEXT_COURSE, $courseid);
4690
 
4691
    $DB->delete_records("course", array("id" => $courseid));
4692
    $DB->delete_records("course_format_options", array("courseid" => $courseid));
4693
 
4694
    // Reset all course related caches here.
4695
    core_courseformat\base::reset_course_cache($courseid);
4696
 
4697
    // Tell search that we have deleted the course so it can delete course data from the index.
4698
    \core_search\manager::course_deleting_finish($courseid);
4699
 
4700
    // Trigger a course deleted event.
4701
    $event = \core\event\course_deleted::create(array(
4702
        'objectid' => $course->id,
4703
        'context' => $context,
4704
        'other' => array(
4705
            'shortname' => $course->shortname,
4706
            'fullname' => $course->fullname,
4707
            'idnumber' => $course->idnumber
4708
            )
4709
    ));
4710
    $event->add_record_snapshot('course', $course);
4711
    $event->trigger();
4712
 
4713
    return true;
4714
}
4715
 
4716
/**
4717
 * Clear a course out completely, deleting all content but don't delete the course itself.
4718
 *
4719
 * This function does not verify any permissions.
4720
 *
4721
 * Please note this function also deletes all user enrolments,
4722
 * enrolment instances and role assignments by default.
4723
 *
4724
 * $options:
4725
 *  - 'keep_roles_and_enrolments' - false by default
4726
 *  - 'keep_groups_and_groupings' - false by default
4727
 *
4728
 * @param int $courseid The id of the course that is being deleted
4729
 * @param bool $showfeedback Whether to display notifications of each action the function performs.
4730
 * @param array $options extra options
4731
 * @return bool true if all the removals succeeded. false if there were any failures. If this
4732
 *             method returns false, some of the removals will probably have succeeded, and others
4733
 *             failed, but you have no way of knowing which.
4734
 */
4735
function remove_course_contents($courseid, $showfeedback = true, array $options = null) {
4736
    global $CFG, $DB, $OUTPUT;
4737
 
4738
    require_once($CFG->libdir.'/badgeslib.php');
4739
    require_once($CFG->libdir.'/completionlib.php');
4740
    require_once($CFG->libdir.'/questionlib.php');
4741
    require_once($CFG->libdir.'/gradelib.php');
4742
    require_once($CFG->dirroot.'/group/lib.php');
4743
    require_once($CFG->dirroot.'/comment/lib.php');
4744
    require_once($CFG->dirroot.'/rating/lib.php');
4745
    require_once($CFG->dirroot.'/notes/lib.php');
4746
 
4747
    // Handle course badges.
4748
    badges_handle_course_deletion($courseid);
4749
 
4750
    // NOTE: these concatenated strings are suboptimal, but it is just extra info...
4751
    $strdeleted = get_string('deleted').' - ';
4752
 
4753
    // Some crazy wishlist of stuff we should skip during purging of course content.
4754
    $options = (array)$options;
4755
 
4756
    $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
4757
    $coursecontext = context_course::instance($courseid);
4758
    $fs = get_file_storage();
4759
 
4760
    // Delete course completion information, this has to be done before grades and enrols.
4761
    $cc = new completion_info($course);
4762
    $cc->clear_criteria();
4763
    if ($showfeedback) {
4764
        echo $OUTPUT->notification($strdeleted.get_string('completion', 'completion'), 'notifysuccess');
4765
    }
4766
 
4767
    // Remove all data from gradebook - this needs to be done before course modules
4768
    // because while deleting this information, the system may need to reference
4769
    // the course modules that own the grades.
4770
    remove_course_grades($courseid, $showfeedback);
4771
    remove_grade_letters($coursecontext, $showfeedback);
4772
 
4773
    // Delete course blocks in any all child contexts,
4774
    // they may depend on modules so delete them first.
4775
    $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2.
4776
    foreach ($childcontexts as $childcontext) {
4777
        blocks_delete_all_for_context($childcontext->id);
4778
    }
4779
    unset($childcontexts);
4780
    blocks_delete_all_for_context($coursecontext->id);
4781
    if ($showfeedback) {
4782
        echo $OUTPUT->notification($strdeleted.get_string('type_block_plural', 'plugin'), 'notifysuccess');
4783
    }
4784
 
4785
    $DB->set_field('course_modules', 'deletioninprogress', '1', ['course' => $courseid]);
4786
    rebuild_course_cache($courseid, true);
4787
 
4788
    // Get the list of all modules that are properly installed.
4789
    $allmodules = $DB->get_records_menu('modules', array(), '', 'name, id');
4790
 
4791
    // Delete every instance of every module,
4792
    // this has to be done before deleting of course level stuff.
4793
    $locations = core_component::get_plugin_list('mod');
4794
    foreach ($locations as $modname => $moddir) {
4795
        if ($modname === 'NEWMODULE') {
4796
            continue;
4797
        }
4798
        if (array_key_exists($modname, $allmodules)) {
4799
            $sql = "SELECT cm.*, m.id AS modinstance, m.name, '$modname' AS modname
4800
              FROM {".$modname."} m
4801
                   LEFT JOIN {course_modules} cm ON cm.instance = m.id AND cm.module = :moduleid
4802
             WHERE m.course = :courseid";
4803
            $instances = $DB->get_records_sql($sql, array('courseid' => $course->id,
4804
                'modulename' => $modname, 'moduleid' => $allmodules[$modname]));
4805
 
4806
            include_once("$moddir/lib.php");                 // Shows php warning only if plugin defective.
4807
            $moddelete = $modname .'_delete_instance';       // Delete everything connected to an instance.
4808
 
4809
            if ($instances) {
4810
                foreach ($instances as $cm) {
4811
                    if ($cm->id) {
4812
                        // Delete activity context questions and question categories.
4813
                        question_delete_activity($cm);
4814
                        // Notify the competency subsystem.
4815
                        \core_competency\api::hook_course_module_deleted($cm);
4816
 
4817
                        // Delete all tag instances associated with the instance of this module.
4818
                        core_tag_tag::delete_instances("mod_{$modname}", null, context_module::instance($cm->id)->id);
4819
                        core_tag_tag::remove_all_item_tags('core', 'course_modules', $cm->id);
4820
                    }
4821
                    if (function_exists($moddelete)) {
4822
                        // This purges all module data in related tables, extra user prefs, settings, etc.
4823
                        $moddelete($cm->modinstance);
4824
                    } else {
4825
                        // NOTE: we should not allow installation of modules with missing delete support!
4826
                        debugging("Defective module '$modname' detected when deleting course contents: missing function $moddelete()!");
4827
                        $DB->delete_records($modname, array('id' => $cm->modinstance));
4828
                    }
4829
 
4830
                    if ($cm->id) {
4831
                        // Delete cm and its context - orphaned contexts are purged in cron in case of any race condition.
4832
                        context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
4833
                        $DB->delete_records('course_modules_completion', ['coursemoduleid' => $cm->id]);
4834
                        $DB->delete_records('course_modules_viewed', ['coursemoduleid' => $cm->id]);
4835
                        $DB->delete_records('course_modules', array('id' => $cm->id));
4836
                        rebuild_course_cache($cm->course, true);
4837
                    }
4838
                }
4839
            }
4840
            if ($instances and $showfeedback) {
4841
                echo $OUTPUT->notification($strdeleted.get_string('pluginname', $modname), 'notifysuccess');
4842
            }
4843
        } else {
4844
            // Ooops, this module is not properly installed, force-delete it in the next block.
4845
        }
4846
    }
4847
 
4848
    // We have tried to delete everything the nice way - now let's force-delete any remaining module data.
4849
 
4850
    // Delete completion defaults.
4851
    $DB->delete_records("course_completion_defaults", array("course" => $courseid));
4852
 
4853
    // Remove all data from availability and completion tables that is associated
4854
    // with course-modules belonging to this course. Note this is done even if the
4855
    // features are not enabled now, in case they were enabled previously.
4856
    $DB->delete_records_subquery('course_modules_completion', 'coursemoduleid', 'id',
4857
            'SELECT id from {course_modules} WHERE course = ?', [$courseid]);
4858
    $DB->delete_records_subquery('course_modules_viewed', 'coursemoduleid', 'id',
4859
        'SELECT id from {course_modules} WHERE course = ?', [$courseid]);
4860
 
4861
    // Remove course-module data that has not been removed in modules' _delete_instance callbacks.
4862
    $cms = $DB->get_records('course_modules', array('course' => $course->id));
4863
    $allmodulesbyid = array_flip($allmodules);
4864
    foreach ($cms as $cm) {
4865
        if (array_key_exists($cm->module, $allmodulesbyid)) {
4866
            try {
4867
                $DB->delete_records($allmodulesbyid[$cm->module], array('id' => $cm->instance));
4868
            } catch (Exception $e) {
4869
                // Ignore weird or missing table problems.
4870
            }
4871
        }
4872
        context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
4873
        $DB->delete_records('course_modules', array('id' => $cm->id));
4874
        rebuild_course_cache($cm->course, true);
4875
    }
4876
 
4877
    if ($showfeedback) {
4878
        echo $OUTPUT->notification($strdeleted.get_string('type_mod_plural', 'plugin'), 'notifysuccess');
4879
    }
4880
 
4881
    // Delete questions and question categories.
4882
    question_delete_course($course);
4883
    if ($showfeedback) {
4884
        echo $OUTPUT->notification($strdeleted.get_string('questions', 'question'), 'notifysuccess');
4885
    }
4886
 
4887
    // Delete content bank contents.
4888
    $cb = new \core_contentbank\contentbank();
4889
    $cbdeleted = $cb->delete_contents($coursecontext);
4890
    if ($showfeedback && $cbdeleted) {
4891
        echo $OUTPUT->notification($strdeleted.get_string('contentbank', 'contentbank'), 'notifysuccess');
4892
    }
4893
 
4894
    // Make sure there are no subcontexts left - all valid blocks and modules should be already gone.
4895
    $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2.
4896
    foreach ($childcontexts as $childcontext) {
4897
        $childcontext->delete();
4898
    }
4899
    unset($childcontexts);
4900
 
4901
    // Remove roles and enrolments by default.
4902
    if (empty($options['keep_roles_and_enrolments'])) {
4903
        // This hack is used in restore when deleting contents of existing course.
4904
        // During restore, we should remove only enrolment related data that the user performing the restore has a
4905
        // permission to remove.
4906
        $userid = $options['userid'] ?? null;
4907
        enrol_course_delete($course, $userid);
4908
        role_unassign_all(array('contextid' => $coursecontext->id, 'component' => ''), true);
4909
        if ($showfeedback) {
4910
            echo $OUTPUT->notification($strdeleted.get_string('type_enrol_plural', 'plugin'), 'notifysuccess');
4911
        }
4912
    }
4913
 
4914
    // Delete any groups, removing members and grouping/course links first.
4915
    if (empty($options['keep_groups_and_groupings'])) {
4916
        groups_delete_groupings($course->id, $showfeedback);
4917
        groups_delete_groups($course->id, $showfeedback);
4918
    }
4919
 
4920
    // Filters be gone!
4921
    filter_delete_all_for_context($coursecontext->id);
4922
 
4923
    // Notes, you shall not pass!
4924
    note_delete_all($course->id);
4925
 
4926
    // Die comments!
4927
    comment::delete_comments($coursecontext->id);
4928
 
4929
    // Ratings are history too.
4930
    $delopt = new stdclass();
4931
    $delopt->contextid = $coursecontext->id;
4932
    $rm = new rating_manager();
4933
    $rm->delete_ratings($delopt);
4934
 
4935
    // Delete course tags.
4936
    core_tag_tag::remove_all_item_tags('core', 'course', $course->id);
4937
 
4938
    // Give the course format the opportunity to remove its obscure data.
4939
    $format = course_get_format($course);
4940
    $format->delete_format_data();
4941
 
4942
    // Notify the competency subsystem.
4943
    \core_competency\api::hook_course_deleted($course);
4944
 
4945
    // Delete calendar events.
4946
    $DB->delete_records('event', array('courseid' => $course->id));
4947
    $fs->delete_area_files($coursecontext->id, 'calendar');
4948
 
4949
    // Delete all related records in other core tables that may have a courseid
4950
    // This array stores the tables that need to be cleared, as
4951
    // table_name => column_name that contains the course id.
4952
    $tablestoclear = array(
4953
        'backup_courses' => 'courseid',  // Scheduled backup stuff.
4954
        'user_lastaccess' => 'courseid', // User access info.
4955
    );
4956
    foreach ($tablestoclear as $table => $col) {
4957
        $DB->delete_records($table, array($col => $course->id));
4958
    }
4959
 
4960
    // Delete all course backup files.
4961
    $fs->delete_area_files($coursecontext->id, 'backup');
4962
 
4963
    // Cleanup course record - remove links to deleted stuff.
4964
    // Do not wipe cacherev, as this course might be reused and we need to ensure that it keeps
4965
    // increasing.
4966
    $oldcourse = new stdClass();
4967
    $oldcourse->id               = $course->id;
4968
    $oldcourse->summary          = '';
4969
    $oldcourse->legacyfiles      = 0;
4970
    if (!empty($options['keep_groups_and_groupings'])) {
4971
        $oldcourse->defaultgroupingid = 0;
4972
    }
4973
    $DB->update_record('course', $oldcourse);
4974
 
4975
    // Delete course sections.
4976
    $DB->delete_records('course_sections', array('course' => $course->id));
4977
 
4978
    // Delete legacy, section and any other course files.
4979
    $fs->delete_area_files($coursecontext->id, 'course'); // Files from summary and section.
4980
 
4981
    // Delete all remaining stuff linked to context such as files, comments, ratings, etc.
4982
    if (empty($options['keep_roles_and_enrolments']) and empty($options['keep_groups_and_groupings'])) {
4983
        // Easy, do not delete the context itself...
4984
        $coursecontext->delete_content();
4985
    } else {
4986
        // Hack alert!!!!
4987
        // We can not drop all context stuff because it would bork enrolments and roles,
4988
        // there might be also files used by enrol plugins...
4989
    }
4990
 
4991
    // Delete legacy files - just in case some files are still left there after conversion to new file api,
4992
    // also some non-standard unsupported plugins may try to store something there.
4993
    fulldelete($CFG->dataroot.'/'.$course->id);
4994
 
4995
    // Delete from cache to reduce the cache size especially makes sense in case of bulk course deletion.
4996
    course_modinfo::purge_course_cache($courseid);
4997
 
4998
    // Trigger a course content deleted event.
4999
    $event = \core\event\course_content_deleted::create(array(
5000
        'objectid' => $course->id,
5001
        'context' => $coursecontext,
5002
        'other' => array('shortname' => $course->shortname,
5003
                         'fullname' => $course->fullname,
5004
                         'options' => $options) // Passing this for legacy reasons.
5005
    ));
5006
    $event->add_record_snapshot('course', $course);
5007
    $event->trigger();
5008
 
5009
    return true;
5010
}
5011
 
5012
/**
5013
 * Change dates in module - used from course reset.
5014
 *
5015
 * @param string $modname forum, assignment, etc
5016
 * @param array $fields array of date fields from mod table
5017
 * @param int $timeshift time difference
5018
 * @param int $courseid
5019
 * @param int $modid (Optional) passed if specific mod instance in course needs to be updated.
5020
 * @return bool success
5021
 */
5022
function shift_course_mod_dates($modname, $fields, $timeshift, $courseid, $modid = 0) {
5023
    global $CFG, $DB;
5024
    include_once($CFG->dirroot.'/mod/'.$modname.'/lib.php');
5025
 
5026
    $return = true;
5027
    $params = array($timeshift, $courseid);
5028
    foreach ($fields as $field) {
5029
        $updatesql = "UPDATE {".$modname."}
5030
                          SET $field = $field + ?
5031
                        WHERE course=? AND $field<>0";
5032
        if ($modid) {
5033
            $updatesql .= ' AND id=?';
5034
            $params[] = $modid;
5035
        }
5036
        $return = $DB->execute($updatesql, $params) && $return;
5037
    }
5038
 
5039
    return $return;
5040
}
5041
 
5042
/**
5043
 * This function will empty a course of user data.
5044
 * It will retain the activities and the structure of the course.
5045
 *
5046
 * @param object $data an object containing all the settings including courseid (without magic quotes)
5047
 * @return array status array of array component, item, error
5048
 */
5049
function reset_course_userdata($data) {
5050
    global $CFG, $DB;
5051
    require_once($CFG->libdir.'/gradelib.php');
5052
    require_once($CFG->libdir.'/completionlib.php');
5053
    require_once($CFG->dirroot.'/completion/criteria/completion_criteria_date.php');
5054
    require_once($CFG->dirroot.'/group/lib.php');
5055
 
5056
    $data->courseid = $data->id;
5057
    $context = context_course::instance($data->courseid);
5058
 
5059
    $eventparams = array(
5060
        'context' => $context,
5061
        'courseid' => $data->id,
5062
        'other' => array(
5063
            'reset_options' => (array) $data
5064
        )
5065
    );
5066
    $event = \core\event\course_reset_started::create($eventparams);
5067
    $event->trigger();
5068
 
5069
    // Calculate the time shift of dates.
5070
    if (!empty($data->reset_start_date)) {
5071
        // Time part of course startdate should be zero.
5072
        $data->timeshift = $data->reset_start_date - usergetmidnight($data->reset_start_date_old);
5073
    } else {
5074
        $data->timeshift = 0;
5075
    }
5076
 
5077
    // Result array: component, item, error.
5078
    $status = array();
5079
 
5080
    // Start the resetting.
5081
    $componentstr = get_string('general');
5082
 
5083
    // Move the course start time.
5084
    if (!empty($data->reset_start_date) and $data->timeshift) {
5085
        // Change course start data.
5086
        $DB->set_field('course', 'startdate', $data->reset_start_date, array('id' => $data->courseid));
5087
        // Update all course and group events - do not move activity events.
5088
        $updatesql = "UPDATE {event}
5089
                         SET timestart = timestart + ?
5090
                       WHERE courseid=? AND instance=0";
5091
        $DB->execute($updatesql, array($data->timeshift, $data->courseid));
5092
 
5093
        // Update any date activity restrictions.
5094
        if ($CFG->enableavailability) {
5095
            \availability_date\condition::update_all_dates($data->courseid, $data->timeshift);
5096
        }
5097
 
5098
        // Update completion expected dates.
5099
        if ($CFG->enablecompletion) {
5100
            $modinfo = get_fast_modinfo($data->courseid);
5101
            $changed = false;
5102
            foreach ($modinfo->get_cms() as $cm) {
5103
                if ($cm->completion && !empty($cm->completionexpected)) {
5104
                    $DB->set_field('course_modules', 'completionexpected', $cm->completionexpected + $data->timeshift,
5105
                        array('id' => $cm->id));
5106
                    $changed = true;
5107
                }
5108
            }
5109
 
5110
            // Clear course cache if changes made.
5111
            if ($changed) {
5112
                rebuild_course_cache($data->courseid, true);
5113
            }
5114
 
5115
            // Update course date completion criteria.
5116
            \completion_criteria_date::update_date($data->courseid, $data->timeshift);
5117
        }
5118
 
5119
        $status[] = array('component' => $componentstr, 'item' => get_string('datechanged'), 'error' => false);
5120
    }
5121
 
5122
    if (!empty($data->reset_end_date)) {
5123
        // If the user set a end date value respect it.
5124
        $DB->set_field('course', 'enddate', $data->reset_end_date, array('id' => $data->courseid));
5125
    } else if ($data->timeshift > 0 && $data->reset_end_date_old) {
5126
        // If there is a time shift apply it to the end date as well.
5127
        $enddate = $data->reset_end_date_old + $data->timeshift;
5128
        $DB->set_field('course', 'enddate', $enddate, array('id' => $data->courseid));
5129
    }
5130
 
5131
    if (!empty($data->reset_events)) {
5132
        $DB->delete_records('event', array('courseid' => $data->courseid));
5133
        $status[] = array('component' => $componentstr, 'item' => get_string('deleteevents', 'calendar'), 'error' => false);
5134
    }
5135
 
5136
    if (!empty($data->reset_notes)) {
5137
        require_once($CFG->dirroot.'/notes/lib.php');
5138
        note_delete_all($data->courseid);
5139
        $status[] = array('component' => $componentstr, 'item' => get_string('deletenotes', 'notes'), 'error' => false);
5140
    }
5141
 
5142
    if (!empty($data->delete_blog_associations)) {
5143
        require_once($CFG->dirroot.'/blog/lib.php');
5144
        blog_remove_associations_for_course($data->courseid);
5145
        $status[] = array('component' => $componentstr, 'item' => get_string('deleteblogassociations', 'blog'), 'error' => false);
5146
    }
5147
 
5148
    if (!empty($data->reset_completion)) {
5149
        // Delete course and activity completion information.
5150
        $course = $DB->get_record('course', array('id' => $data->courseid));
5151
        $cc = new completion_info($course);
5152
        $cc->delete_all_completion_data();
5153
        $status[] = array('component' => $componentstr,
5154
                'item' => get_string('deletecompletiondata', 'completion'), 'error' => false);
5155
    }
5156
 
5157
    if (!empty($data->reset_competency_ratings)) {
5158
        \core_competency\api::hook_course_reset_competency_ratings($data->courseid);
5159
        $status[] = array('component' => $componentstr,
5160
            'item' => get_string('deletecompetencyratings', 'core_competency'), 'error' => false);
5161
    }
5162
 
5163
    $componentstr = get_string('roles');
5164
 
5165
    if (!empty($data->reset_roles_overrides)) {
5166
        $children = $context->get_child_contexts();
5167
        foreach ($children as $child) {
5168
            $child->delete_capabilities();
5169
        }
5170
        $context->delete_capabilities();
5171
        $status[] = array('component' => $componentstr, 'item' => get_string('deletecourseoverrides', 'role'), 'error' => false);
5172
    }
5173
 
5174
    if (!empty($data->reset_roles_local)) {
5175
        $children = $context->get_child_contexts();
5176
        foreach ($children as $child) {
5177
            role_unassign_all(array('contextid' => $child->id));
5178
        }
5179
        $status[] = array('component' => $componentstr, 'item' => get_string('deletelocalroles', 'role'), 'error' => false);
5180
    }
5181
 
5182
    // First unenrol users - this cleans some of related user data too, such as forum subscriptions, tracking, etc.
5183
    $data->unenrolled = array();
5184
    if (!empty($data->unenrol_users)) {
5185
        $plugins = enrol_get_plugins(true);
5186
        $instances = enrol_get_instances($data->courseid, true);
5187
        foreach ($instances as $key => $instance) {
5188
            if (!isset($plugins[$instance->enrol])) {
5189
                unset($instances[$key]);
5190
                continue;
5191
            }
5192
        }
5193
 
5194
        $usersroles = enrol_get_course_users_roles($data->courseid);
5195
        foreach ($data->unenrol_users as $withroleid) {
5196
            if ($withroleid) {
5197
                $sql = "SELECT ue.*
5198
                          FROM {user_enrolments} ue
5199
                          JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5200
                          JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5201
                          JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.roleid = :roleid AND ra.userid = ue.userid)";
5202
                $params = array('courseid' => $data->courseid, 'roleid' => $withroleid, 'courselevel' => CONTEXT_COURSE);
5203
 
5204
            } else {
5205
                // Without any role assigned at course context.
5206
                $sql = "SELECT ue.*
5207
                          FROM {user_enrolments} ue
5208
                          JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5209
                          JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5210
                     LEFT JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.userid = ue.userid)
5211
                         WHERE ra.id IS null";
5212
                $params = array('courseid' => $data->courseid, 'courselevel' => CONTEXT_COURSE);
5213
            }
5214
 
5215
            $rs = $DB->get_recordset_sql($sql, $params);
5216
            foreach ($rs as $ue) {
5217
                if (!isset($instances[$ue->enrolid])) {
5218
                    continue;
5219
                }
5220
                $instance = $instances[$ue->enrolid];
5221
                $plugin = $plugins[$instance->enrol];
5222
                if (!$plugin->allow_unenrol($instance) and !$plugin->allow_unenrol_user($instance, $ue)) {
5223
                    continue;
5224
                }
5225
 
5226
                if ($withroleid && count($usersroles[$ue->userid]) > 1) {
5227
                    // If we don't remove all roles and user has more than one role, just remove this role.
5228
                    role_unassign($withroleid, $ue->userid, $context->id);
5229
 
5230
                    unset($usersroles[$ue->userid][$withroleid]);
5231
                } else {
5232
                    // If we remove all roles or user has only one role, unenrol user from course.
5233
                    $plugin->unenrol_user($instance, $ue->userid);
5234
                }
5235
                $data->unenrolled[$ue->userid] = $ue->userid;
5236
            }
5237
            $rs->close();
5238
        }
5239
    }
5240
    if (!empty($data->unenrolled)) {
5241
        $status[] = array(
5242
            'component' => $componentstr,
5243
            'item' => get_string('unenrol', 'enrol').' ('.count($data->unenrolled).')',
5244
            'error' => false
5245
        );
5246
    }
5247
 
5248
    $componentstr = get_string('groups');
5249
 
5250
    // Remove all group members.
5251
    if (!empty($data->reset_groups_members)) {
5252
        groups_delete_group_members($data->courseid);
5253
        $status[] = array('component' => $componentstr, 'item' => get_string('removegroupsmembers', 'group'), 'error' => false);
5254
    }
5255
 
5256
    // Remove all groups.
5257
    if (!empty($data->reset_groups_remove)) {
5258
        groups_delete_groups($data->courseid, false);
5259
        $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroups', 'group'), 'error' => false);
5260
    }
5261
 
5262
    // Remove all grouping members.
5263
    if (!empty($data->reset_groupings_members)) {
5264
        groups_delete_groupings_groups($data->courseid, false);
5265
        $status[] = array('component' => $componentstr, 'item' => get_string('removegroupingsmembers', 'group'), 'error' => false);
5266
    }
5267
 
5268
    // Remove all groupings.
5269
    if (!empty($data->reset_groupings_remove)) {
5270
        groups_delete_groupings($data->courseid, false);
5271
        $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroupings', 'group'), 'error' => false);
5272
    }
5273
 
5274
    // Look in every instance of every module for data to delete.
5275
    $unsupportedmods = array();
5276
    if ($allmods = $DB->get_records('modules') ) {
5277
        foreach ($allmods as $mod) {
5278
            $modname = $mod->name;
5279
            $modfile = $CFG->dirroot.'/mod/'. $modname.'/lib.php';
5280
            $moddeleteuserdata = $modname.'_reset_userdata';   // Function to delete user data.
5281
            if (file_exists($modfile)) {
5282
                if (!$DB->count_records($modname, array('course' => $data->courseid))) {
5283
                    continue; // Skip mods with no instances.
5284
                }
5285
                include_once($modfile);
5286
                if (function_exists($moddeleteuserdata)) {
5287
                    $modstatus = $moddeleteuserdata($data);
5288
                    if (is_array($modstatus)) {
5289
                        $status = array_merge($status, $modstatus);
5290
                    } else {
5291
                        debugging('Module '.$modname.' returned incorrect staus - must be an array!');
5292
                    }
5293
                } else {
5294
                    $unsupportedmods[] = $mod;
5295
                }
5296
            } else {
5297
                debugging('Missing lib.php in '.$modname.' module!');
5298
            }
5299
            // Update calendar events for all modules.
5300
            course_module_bulk_update_calendar_events($modname, $data->courseid);
5301
        }
5302
        // Purge the course cache after resetting course start date. MDL-76936
5303
        if ($data->timeshift) {
5304
            course_modinfo::purge_course_cache($data->courseid);
5305
        }
5306
    }
5307
 
5308
    // Mention unsupported mods.
5309
    if (!empty($unsupportedmods)) {
5310
        foreach ($unsupportedmods as $mod) {
5311
            $status[] = array(
5312
                'component' => get_string('modulenameplural', $mod->name),
5313
                'item' => '',
5314
                'error' => get_string('resetnotimplemented')
5315
            );
5316
        }
5317
    }
5318
 
5319
    $componentstr = get_string('gradebook', 'grades');
5320
    // Reset gradebook,.
5321
    if (!empty($data->reset_gradebook_items)) {
5322
        remove_course_grades($data->courseid, false);
5323
        grade_grab_course_grades($data->courseid);
5324
        grade_regrade_final_grades($data->courseid);
5325
        $status[] = array('component' => $componentstr, 'item' => get_string('removeallcourseitems', 'grades'), 'error' => false);
5326
 
5327
    } else if (!empty($data->reset_gradebook_grades)) {
5328
        grade_course_reset($data->courseid);
5329
        $status[] = array('component' => $componentstr, 'item' => get_string('removeallcoursegrades', 'grades'), 'error' => false);
5330
    }
5331
    // Reset comments.
5332
    if (!empty($data->reset_comments)) {
5333
        require_once($CFG->dirroot.'/comment/lib.php');
5334
        comment::reset_course_page_comments($context);
5335
    }
5336
 
5337
    $event = \core\event\course_reset_ended::create($eventparams);
5338
    $event->trigger();
5339
 
5340
    return $status;
5341
}
5342
 
5343
/**
5344
 * Generate an email processing address.
5345
 *
5346
 * @param int $modid
5347
 * @param string $modargs
5348
 * @return string Returns email processing address
5349
 */
5350
function generate_email_processing_address($modid, $modargs) {
5351
    global $CFG;
5352
 
5353
    $header = $CFG->mailprefix . substr(base64_encode(pack('C', $modid)), 0, 2).$modargs;
5354
    return $header . substr(md5($header.get_site_identifier()), 0, 16).'@'.$CFG->maildomain;
5355
}
5356
 
5357
/**
5358
 * ?
5359
 *
5360
 * @todo Finish documenting this function
5361
 *
5362
 * @param string $modargs
5363
 * @param string $body Currently unused
5364
 */
5365
function moodle_process_email($modargs, $body) {
5366
    global $DB;
5367
 
5368
    // The first char should be an unencoded letter. We'll take this as an action.
5369
    switch ($modargs[0]) {
5370
        case 'B': { // Bounce.
5371
            list(, $userid) = unpack('V', base64_decode(substr($modargs, 1, 8)));
5372
            if ($user = $DB->get_record("user", array('id' => $userid), "id,email")) {
5373
                // Check the half md5 of their email.
5374
                $md5check = substr(md5($user->email), 0, 16);
5375
                if ($md5check == substr($modargs, -16)) {
5376
                    set_bounce_count($user);
5377
                }
5378
                // Else maybe they've already changed it?
5379
            }
5380
        }
5381
        break;
5382
        // Maybe more later?
5383
    }
5384
}
5385
 
5386
// CORRESPONDENCE.
5387
 
5388
/**
5389
 * Get mailer instance, enable buffering, flush buffer or disable buffering.
5390
 *
5391
 * @param string $action 'get', 'buffer', 'close' or 'flush'
5392
 * @return moodle_phpmailer|null mailer instance if 'get' used or nothing
5393
 */
5394
function get_mailer($action='get') {
5395
    global $CFG;
5396
 
5397
    /** @var moodle_phpmailer $mailer */
5398
    static $mailer  = null;
5399
    static $counter = 0;
5400
 
5401
    if (!isset($CFG->smtpmaxbulk)) {
5402
        $CFG->smtpmaxbulk = 1;
5403
    }
5404
 
5405
    if ($action == 'get') {
5406
        $prevkeepalive = false;
5407
 
5408
        if (isset($mailer) and $mailer->Mailer == 'smtp') {
5409
            if ($counter < $CFG->smtpmaxbulk and !$mailer->isError()) {
5410
                $counter++;
5411
                // Reset the mailer.
5412
                $mailer->Priority         = 3;
5413
                $mailer->CharSet          = 'UTF-8'; // Our default.
5414
                $mailer->ContentType      = "text/plain";
5415
                $mailer->Encoding         = "8bit";
5416
                $mailer->From             = "root@localhost";
5417
                $mailer->FromName         = "Root User";
5418
                $mailer->Sender           = "";
5419
                $mailer->Subject          = "";
5420
                $mailer->Body             = "";
5421
                $mailer->AltBody          = "";
5422
                $mailer->ConfirmReadingTo = "";
5423
 
5424
                $mailer->clearAllRecipients();
5425
                $mailer->clearReplyTos();
5426
                $mailer->clearAttachments();
5427
                $mailer->clearCustomHeaders();
5428
                return $mailer;
5429
            }
5430
 
5431
            $prevkeepalive = $mailer->SMTPKeepAlive;
5432
            get_mailer('flush');
5433
        }
5434
 
5435
        require_once($CFG->libdir.'/phpmailer/moodle_phpmailer.php');
5436
        $mailer = new moodle_phpmailer();
5437
 
5438
        $counter = 1;
5439
 
5440
        if ($CFG->smtphosts == 'qmail') {
5441
            // Use Qmail system.
5442
            $mailer->isQmail();
5443
 
5444
        } else if (empty($CFG->smtphosts)) {
5445
            // Use PHP mail() = sendmail.
5446
            $mailer->isMail();
5447
 
5448
        } else {
5449
            // Use SMTP directly.
5450
            $mailer->isSMTP();
5451
            if (!empty($CFG->debugsmtp) && (!empty($CFG->debugdeveloper))) {
5452
                $mailer->SMTPDebug = 3;
5453
            }
5454
            // Specify main and backup servers.
5455
            $mailer->Host          = $CFG->smtphosts;
5456
            // Specify secure connection protocol.
5457
            $mailer->SMTPSecure    = $CFG->smtpsecure;
5458
            // Use previous keepalive.
5459
            $mailer->SMTPKeepAlive = $prevkeepalive;
5460
 
5461
            if ($CFG->smtpuser) {
5462
                // Use SMTP authentication.
5463
                $mailer->SMTPAuth = true;
5464
                $mailer->Username = $CFG->smtpuser;
5465
                $mailer->Password = $CFG->smtppass;
5466
            }
5467
        }
5468
 
5469
        return $mailer;
5470
    }
5471
 
5472
    $nothing = null;
5473
 
5474
    // Keep smtp session open after sending.
5475
    if ($action == 'buffer') {
5476
        if (!empty($CFG->smtpmaxbulk)) {
5477
            get_mailer('flush');
5478
            $m = get_mailer();
5479
            if ($m->Mailer == 'smtp') {
5480
                $m->SMTPKeepAlive = true;
5481
            }
5482
        }
5483
        return $nothing;
5484
    }
5485
 
5486
    // Close smtp session, but continue buffering.
5487
    if ($action == 'flush') {
5488
        if (isset($mailer) and $mailer->Mailer == 'smtp') {
5489
            if (!empty($mailer->SMTPDebug)) {
5490
                echo '<pre>'."\n";
5491
            }
5492
            $mailer->SmtpClose();
5493
            if (!empty($mailer->SMTPDebug)) {
5494
                echo '</pre>';
5495
            }
5496
        }
5497
        return $nothing;
5498
    }
5499
 
5500
    // Close smtp session, do not buffer anymore.
5501
    if ($action == 'close') {
5502
        if (isset($mailer) and $mailer->Mailer == 'smtp') {
5503
            get_mailer('flush');
5504
            $mailer->SMTPKeepAlive = false;
5505
        }
5506
        $mailer = null; // Better force new instance.
5507
        return $nothing;
5508
    }
5509
}
5510
 
5511
/**
5512
 * A helper function to test for email diversion
5513
 *
5514
 * @param string $email
5515
 * @return bool Returns true if the email should be diverted
5516
 */
5517
function email_should_be_diverted($email) {
5518
    global $CFG;
5519
 
5520
    if (empty($CFG->divertallemailsto)) {
5521
        return false;
5522
    }
5523
 
5524
    if (empty($CFG->divertallemailsexcept)) {
5525
        return true;
5526
    }
5527
 
5528
    $patterns = array_map('trim', preg_split("/[\s,]+/", $CFG->divertallemailsexcept, -1, PREG_SPLIT_NO_EMPTY));
5529
    foreach ($patterns as $pattern) {
5530
        if (preg_match("/{$pattern}/i", $email)) {
5531
            return false;
5532
        }
5533
    }
5534
 
5535
    return true;
5536
}
5537
 
5538
/**
5539
 * Generate a unique email Message-ID using the moodle domain and install path
5540
 *
5541
 * @param string $localpart An optional unique message id prefix.
5542
 * @return string The formatted ID ready for appending to the email headers.
5543
 */
5544
function generate_email_messageid($localpart = null) {
5545
    global $CFG;
5546
 
5547
    $urlinfo = parse_url($CFG->wwwroot);
5548
    $base = '@' . $urlinfo['host'];
5549
 
5550
    // If multiple moodles are on the same domain we want to tell them
5551
    // apart so we add the install path to the local part. This means
5552
    // that the id local part should never contain a / character so
5553
    // we can correctly parse the id to reassemble the wwwroot.
5554
    if (isset($urlinfo['path'])) {
5555
        $base = $urlinfo['path'] . $base;
5556
    }
5557
 
5558
    if (empty($localpart)) {
5559
        $localpart = uniqid('', true);
5560
    }
5561
 
5562
    // Because we may have an option /installpath suffix to the local part
5563
    // of the id we need to escape any / chars which are in the $localpart.
5564
    $localpart = str_replace('/', '%2F', $localpart);
5565
 
5566
    return '<' . $localpart . $base . '>';
5567
}
5568
 
5569
/**
5570
 * Send an email to a specified user
5571
 *
5572
 * @param stdClass $user  A {@link $USER} object
5573
 * @param stdClass $from A {@link $USER} object
5574
 * @param string $subject plain text subject line of the email
5575
 * @param string $messagetext plain text version of the message
5576
 * @param string $messagehtml complete html version of the message (optional)
5577
 * @param string $attachment a file on the filesystem, either relative to $CFG->dataroot or a full path to a file in one of
5578
 *          the following directories: $CFG->cachedir, $CFG->dataroot, $CFG->dirroot, $CFG->localcachedir, $CFG->tempdir
5579
 * @param string $attachname the name of the file (extension indicates MIME)
5580
 * @param bool $usetrueaddress determines whether $from email address should
5581
 *          be sent out. Will be overruled by user profile setting for maildisplay
5582
 * @param string $replyto Email address to reply to
5583
 * @param string $replytoname Name of reply to recipient
5584
 * @param int $wordwrapwidth custom word wrap width, default 79
5585
 * @return bool Returns true if mail was sent OK and false if there was an error.
5586
 */
5587
function email_to_user($user, $from, $subject, $messagetext, $messagehtml = '', $attachment = '', $attachname = '',
5588
                       $usetrueaddress = true, $replyto = '', $replytoname = '', $wordwrapwidth = 79) {
5589
 
5590
    global $CFG, $PAGE, $SITE;
5591
 
5592
    if (empty($user) or empty($user->id)) {
5593
        debugging('Can not send email to null user', DEBUG_DEVELOPER);
5594
        return false;
5595
    }
5596
 
5597
    if (empty($user->email)) {
5598
        debugging('Can not send email to user without email: '.$user->id, DEBUG_DEVELOPER);
5599
        return false;
5600
    }
5601
 
5602
    if (!empty($user->deleted)) {
5603
        debugging('Can not send email to deleted user: '.$user->id, DEBUG_DEVELOPER);
5604
        return false;
5605
    }
5606
 
5607
    if (defined('BEHAT_SITE_RUNNING')) {
5608
        // Fake email sending in behat.
5609
        return true;
5610
    }
5611
 
5612
    if (!empty($CFG->noemailever)) {
5613
        // Hidden setting for development sites, set in config.php if needed.
5614
        debugging('Not sending email due to $CFG->noemailever config setting', DEBUG_NORMAL);
5615
        return true;
5616
    }
5617
 
5618
    if (email_should_be_diverted($user->email)) {
5619
        $subject = "[DIVERTED {$user->email}] $subject";
5620
        $user = clone($user);
5621
        $user->email = $CFG->divertallemailsto;
5622
    }
5623
 
5624
    // Skip mail to suspended users.
5625
    if ((isset($user->auth) && $user->auth=='nologin') or (isset($user->suspended) && $user->suspended)) {
5626
        return true;
5627
    }
5628
 
5629
    if (!validate_email($user->email)) {
5630
        // We can not send emails to invalid addresses - it might create security issue or confuse the mailer.
5631
        debugging("email_to_user: User $user->id (".fullname($user).") email ($user->email) is invalid! Not sending.");
5632
        return false;
5633
    }
5634
 
5635
    if (over_bounce_threshold($user)) {
5636
        debugging("email_to_user: User $user->id (".fullname($user).") is over bounce threshold! Not sending.");
5637
        return false;
5638
    }
5639
 
5640
    // TLD .invalid  is specifically reserved for invalid domain names.
5641
    // For More information, see {@link http://tools.ietf.org/html/rfc2606#section-2}.
5642
    if (substr($user->email, -8) == '.invalid') {
5643
        debugging("email_to_user: User $user->id (".fullname($user).") email domain ($user->email) is invalid! Not sending.");
5644
        return true; // This is not an error.
5645
    }
5646
 
5647
    // If the user is a remote mnet user, parse the email text for URL to the
5648
    // wwwroot and modify the url to direct the user's browser to login at their
5649
    // home site (identity provider - idp) before hitting the link itself.
5650
    if (is_mnet_remote_user($user)) {
5651
        require_once($CFG->dirroot.'/mnet/lib.php');
5652
 
5653
        $jumpurl = mnet_get_idp_jump_url($user);
5654
        $callback = partial('mnet_sso_apply_indirection', $jumpurl);
5655
 
5656
        $messagetext = preg_replace_callback("%($CFG->wwwroot[^[:space:]]*)%",
5657
                $callback,
5658
                $messagetext);
5659
        $messagehtml = preg_replace_callback("%href=[\"'`]($CFG->wwwroot[\w_:\?=#&@/;.~-]*)[\"'`]%",
5660
                $callback,
5661
                $messagehtml);
5662
    }
5663
    $mail = get_mailer();
5664
 
5665
    if (!empty($mail->SMTPDebug)) {
5666
        echo '<pre>' . "\n";
5667
    }
5668
 
5669
    $temprecipients = array();
5670
    $tempreplyto = array();
5671
 
5672
    // Make sure that we fall back onto some reasonable no-reply address.
5673
    $noreplyaddressdefault = 'noreply@' . get_host_from_url($CFG->wwwroot);
5674
    $noreplyaddress = empty($CFG->noreplyaddress) ? $noreplyaddressdefault : $CFG->noreplyaddress;
5675
 
5676
    if (!validate_email($noreplyaddress)) {
5677
        debugging('email_to_user: Invalid noreply-email '.s($noreplyaddress));
5678
        $noreplyaddress = $noreplyaddressdefault;
5679
    }
5680
 
5681
    // Make up an email address for handling bounces.
5682
    if (!empty($CFG->handlebounces)) {
5683
        $modargs = 'B'.base64_encode(pack('V', $user->id)).substr(md5($user->email), 0, 16);
5684
        $mail->Sender = generate_email_processing_address(0, $modargs);
5685
    } else {
5686
        $mail->Sender = $noreplyaddress;
5687
    }
5688
 
5689
    // Make sure that the explicit replyto is valid, fall back to the implicit one.
5690
    if (!empty($replyto) && !validate_email($replyto)) {
5691
        debugging('email_to_user: Invalid replyto-email '.s($replyto));
5692
        $replyto = $noreplyaddress;
5693
    }
5694
 
5695
    if (is_string($from)) { // So we can pass whatever we want if there is need.
5696
        $mail->From     = $noreplyaddress;
5697
        $mail->FromName = $from;
5698
    // Check if using the true address is true, and the email is in the list of allowed domains for sending email,
5699
    // and that the senders email setting is either displayed to everyone, or display to only other users that are enrolled
5700
    // in a course with the sender.
5701
    } else if ($usetrueaddress && can_send_from_real_email_address($from, $user)) {
5702
        if (!validate_email($from->email)) {
5703
            debugging('email_to_user: Invalid from-email '.s($from->email).' - not sending');
5704
            // Better not to use $noreplyaddress in this case.
5705
            return false;
5706
        }
5707
        $mail->From = $from->email;
5708
        $fromdetails = new stdClass();
5709
        $fromdetails->name = fullname($from);
5710
        $fromdetails->url = preg_replace('#^https?://#', '', $CFG->wwwroot);
5711
        $fromdetails->siteshortname = format_string($SITE->shortname);
5712
        $fromstring = $fromdetails->name;
5713
        if ($CFG->emailfromvia == EMAIL_VIA_ALWAYS) {
5714
            $fromstring = get_string('emailvia', 'core', $fromdetails);
5715
        }
5716
        $mail->FromName = $fromstring;
5717
        if (empty($replyto)) {
5718
            $tempreplyto[] = array($from->email, fullname($from));
5719
        }
5720
    } else {
5721
        $mail->From = $noreplyaddress;
5722
        $fromdetails = new stdClass();
5723
        $fromdetails->name = fullname($from);
5724
        $fromdetails->url = preg_replace('#^https?://#', '', $CFG->wwwroot);
5725
        $fromdetails->siteshortname = format_string($SITE->shortname);
5726
        $fromstring = $fromdetails->name;
5727
        if ($CFG->emailfromvia != EMAIL_VIA_NEVER) {
5728
            $fromstring = get_string('emailvia', 'core', $fromdetails);
5729
        }
5730
        $mail->FromName = $fromstring;
5731
        if (empty($replyto)) {
5732
            $tempreplyto[] = array($noreplyaddress, get_string('noreplyname'));
5733
        }
5734
    }
5735
 
5736
    if (!empty($replyto)) {
5737
        $tempreplyto[] = array($replyto, $replytoname);
5738
    }
5739
 
5740
    $temprecipients[] = array($user->email, fullname($user));
5741
 
5742
    // Set word wrap.
5743
    $mail->WordWrap = $wordwrapwidth;
5744
 
5745
    if (!empty($from->customheaders)) {
5746
        // Add custom headers.
5747
        if (is_array($from->customheaders)) {
5748
            foreach ($from->customheaders as $customheader) {
5749
                $mail->addCustomHeader($customheader);
5750
            }
5751
        } else {
5752
            $mail->addCustomHeader($from->customheaders);
5753
        }
5754
    }
5755
 
5756
    // If the X-PHP-Originating-Script email header is on then also add an additional
5757
    // header with details of where exactly in moodle the email was triggered from,
5758
    // either a call to message_send() or to email_to_user().
5759
    if (ini_get('mail.add_x_header')) {
5760
 
5761
        $stack = debug_backtrace(false);
5762
        $origin = $stack[0];
5763
 
5764
        foreach ($stack as $depth => $call) {
5765
            if ($call['function'] == 'message_send') {
5766
                $origin = $call;
5767
            }
5768
        }
5769
 
5770
        $originheader = $CFG->wwwroot . ' => ' . gethostname() . ':'
5771
             . str_replace($CFG->dirroot . '/', '', $origin['file']) . ':' . $origin['line'];
5772
        $mail->addCustomHeader('X-Moodle-Originating-Script: ' . $originheader);
5773
    }
5774
 
5775
    if (!empty($CFG->emailheaders)) {
5776
        $headers = array_map('trim', explode("\n", $CFG->emailheaders));
5777
        foreach ($headers as $header) {
5778
            if (!empty($header)) {
5779
                $mail->addCustomHeader($header);
5780
            }
5781
        }
5782
    }
5783
 
5784
    if (!empty($from->priority)) {
5785
        $mail->Priority = $from->priority;
5786
    }
5787
 
5788
    $renderer = $PAGE->get_renderer('core');
5789
    $context = array(
5790
        'sitefullname' => $SITE->fullname,
5791
        'siteshortname' => $SITE->shortname,
5792
        'sitewwwroot' => $CFG->wwwroot,
5793
        'subject' => $subject,
5794
        'prefix' => $CFG->emailsubjectprefix,
5795
        'to' => $user->email,
5796
        'toname' => fullname($user),
5797
        'from' => $mail->From,
5798
        'fromname' => $mail->FromName,
5799
    );
5800
    if (!empty($tempreplyto[0])) {
5801
        $context['replyto'] = $tempreplyto[0][0];
5802
        $context['replytoname'] = $tempreplyto[0][1];
5803
    }
5804
    if ($user->id > 0) {
5805
        $context['touserid'] = $user->id;
5806
        $context['tousername'] = $user->username;
5807
    }
5808
 
5809
    if (!empty($user->mailformat) && $user->mailformat == 1) {
5810
        // Only process html templates if the user preferences allow html email.
5811
 
5812
        if (!$messagehtml) {
5813
            // If no html has been given, BUT there is an html wrapping template then
5814
            // auto convert the text to html and then wrap it.
5815
            $messagehtml = trim(text_to_html($messagetext));
5816
        }
5817
        $context['body'] = $messagehtml;
5818
        $messagehtml = $renderer->render_from_template('core/email_html', $context);
5819
    }
5820
 
5821
    $context['body'] = html_to_text(nl2br($messagetext));
5822
    $mail->Subject = $renderer->render_from_template('core/email_subject', $context);
5823
    $mail->FromName = $renderer->render_from_template('core/email_fromname', $context);
5824
    $messagetext = $renderer->render_from_template('core/email_text', $context);
5825
 
5826
    // Autogenerate a MessageID if it's missing.
5827
    if (empty($mail->MessageID)) {
5828
        $mail->MessageID = generate_email_messageid();
5829
    }
5830
 
5831
    if ($messagehtml && !empty($user->mailformat) && $user->mailformat == 1) {
5832
        // Don't ever send HTML to users who don't want it.
5833
        $mail->isHTML(true);
5834
        $mail->Encoding = 'quoted-printable';
5835
        $mail->Body    =  $messagehtml;
5836
        $mail->AltBody =  "\n$messagetext\n";
5837
    } else {
5838
        $mail->IsHTML(false);
5839
        $mail->Body =  "\n$messagetext\n";
5840
    }
5841
 
5842
    if ($attachment && $attachname) {
5843
        if (preg_match( "~\\.\\.~" , $attachment )) {
5844
            // Security check for ".." in dir path.
5845
            $supportuser = core_user::get_support_user();
5846
            $temprecipients[] = array($supportuser->email, fullname($supportuser, true));
5847
            $mail->addStringAttachment('Error in attachment.  User attempted to attach a filename with a unsafe name.', 'error.txt', '8bit', 'text/plain');
5848
        } else {
5849
            require_once($CFG->libdir.'/filelib.php');
5850
            $mimetype = mimeinfo('type', $attachname);
5851
 
5852
            // Before doing the comparison, make sure that the paths are correct (Windows uses slashes in the other direction).
5853
            // The absolute (real) path is also fetched to ensure that comparisons to allowed paths are compared equally.
5854
            $attachpath = str_replace('\\', '/', realpath($attachment));
5855
 
5856
            // Build an array of all filepaths from which attachments can be added (normalised slashes, absolute/real path).
5857
            $allowedpaths = array_map(function(string $path): string {
5858
                return str_replace('\\', '/', realpath($path));
5859
            }, [
5860
                $CFG->cachedir,
5861
                $CFG->dataroot,
5862
                $CFG->dirroot,
5863
                $CFG->localcachedir,
5864
                $CFG->tempdir,
5865
                $CFG->localrequestdir,
5866
            ]);
5867
 
5868
            // Set addpath to true.
5869
            $addpath = true;
5870
 
5871
            // Check if attachment includes one of the allowed paths.
5872
            foreach (array_filter($allowedpaths) as $allowedpath) {
5873
                // Set addpath to false if the attachment includes one of the allowed paths.
5874
                if (strpos($attachpath, $allowedpath) === 0) {
5875
                    $addpath = false;
5876
                    break;
5877
                }
5878
            }
5879
 
5880
            // If the attachment is a full path to a file in the multiple allowed paths, use it as is,
5881
            // otherwise assume it is a relative path from the dataroot (for backwards compatibility reasons).
5882
            if ($addpath == true) {
5883
                $attachment = $CFG->dataroot . '/' . $attachment;
5884
            }
5885
 
5886
            $mail->addAttachment($attachment, $attachname, 'base64', $mimetype);
5887
        }
5888
    }
5889
 
5890
    // Check if the email should be sent in an other charset then the default UTF-8.
5891
    if ((!empty($CFG->sitemailcharset) || !empty($CFG->allowusermailcharset))) {
5892
 
5893
        // Use the defined site mail charset or eventually the one preferred by the recipient.
5894
        $charset = $CFG->sitemailcharset;
5895
        if (!empty($CFG->allowusermailcharset)) {
5896
            if ($useremailcharset = get_user_preferences('mailcharset', '0', $user->id)) {
5897
                $charset = $useremailcharset;
5898
            }
5899
        }
5900
 
5901
        // Convert all the necessary strings if the charset is supported.
5902
        $charsets = get_list_of_charsets();
5903
        unset($charsets['UTF-8']);
5904
        if (in_array($charset, $charsets)) {
5905
            $mail->CharSet  = $charset;
5906
            $mail->FromName = core_text::convert($mail->FromName, 'utf-8', strtolower($charset));
5907
            $mail->Subject  = core_text::convert($mail->Subject, 'utf-8', strtolower($charset));
5908
            $mail->Body     = core_text::convert($mail->Body, 'utf-8', strtolower($charset));
5909
            $mail->AltBody  = core_text::convert($mail->AltBody, 'utf-8', strtolower($charset));
5910
 
5911
            foreach ($temprecipients as $key => $values) {
5912
                $temprecipients[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset));
5913
            }
5914
            foreach ($tempreplyto as $key => $values) {
5915
                $tempreplyto[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset));
5916
            }
5917
        }
5918
    }
5919
 
5920
    foreach ($temprecipients as $values) {
5921
        $mail->addAddress($values[0], $values[1]);
5922
    }
5923
    foreach ($tempreplyto as $values) {
5924
        $mail->addReplyTo($values[0], $values[1]);
5925
    }
5926
 
5927
    if (!empty($CFG->emaildkimselector)) {
5928
        $domain = substr(strrchr($mail->From, "@"), 1);
5929
        $pempath = "{$CFG->dataroot}/dkim/{$domain}/{$CFG->emaildkimselector}.private";
5930
        if (file_exists($pempath)) {
5931
            $mail->DKIM_domain      = $domain;
5932
            $mail->DKIM_private     = $pempath;
5933
            $mail->DKIM_selector    = $CFG->emaildkimselector;
5934
            $mail->DKIM_identity    = $mail->From;
5935
        } else {
5936
            debugging("Email DKIM selector chosen due to {$mail->From} but no certificate found at $pempath", DEBUG_DEVELOPER);
5937
        }
5938
    }
5939
 
5940
    if ($mail->send()) {
5941
        set_send_count($user);
5942
        if (!empty($mail->SMTPDebug)) {
5943
            echo '</pre>';
5944
        }
5945
        return true;
5946
    } else {
5947
        // Trigger event for failing to send email.
5948
        $event = \core\event\email_failed::create(array(
5949
            'context' => context_system::instance(),
5950
            'userid' => $from->id,
5951
            'relateduserid' => $user->id,
5952
            'other' => array(
5953
                'subject' => $subject,
5954
                'message' => $messagetext,
5955
                'errorinfo' => $mail->ErrorInfo
5956
            )
5957
        ));
5958
        $event->trigger();
5959
        if (CLI_SCRIPT) {
5960
            mtrace('Error: lib/moodlelib.php email_to_user(): '.$mail->ErrorInfo);
5961
        }
5962
        if (!empty($mail->SMTPDebug)) {
5963
            echo '</pre>';
5964
        }
5965
        return false;
5966
    }
5967
}
5968
 
5969
/**
5970
 * Check to see if a user's real email address should be used for the "From" field.
5971
 *
5972
 * @param  object $from The user object for the user we are sending the email from.
5973
 * @param  object $user The user object that we are sending the email to.
5974
 * @param  array $unused No longer used.
5975
 * @return bool Returns true if we can use the from user's email adress in the "From" field.
5976
 */
5977
function can_send_from_real_email_address($from, $user, $unused = null) {
5978
    global $CFG;
5979
    if (!isset($CFG->allowedemaildomains) || empty(trim($CFG->allowedemaildomains))) {
5980
        return false;
5981
    }
5982
    $alloweddomains = array_map('trim', explode("\n", $CFG->allowedemaildomains));
5983
    // Email is in the list of allowed domains for sending email,
5984
    // and the senders email setting is either displayed to everyone, or display to only other users that are enrolled
5985
    // in a course with the sender.
5986
    if (\core\ip_utils::is_domain_in_allowed_list(substr($from->email, strpos($from->email, '@') + 1), $alloweddomains)
5987
                && ($from->maildisplay == core_user::MAILDISPLAY_EVERYONE
5988
                || ($from->maildisplay == core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY
5989
                && enrol_get_shared_courses($user, $from, false, true)))) {
5990
        return true;
5991
    }
5992
    return false;
5993
}
5994
 
5995
/**
5996
 * Generate a signoff for emails based on support settings
5997
 *
5998
 * @return string
5999
 */
6000
function generate_email_signoff() {
6001
    global $CFG, $OUTPUT;
6002
 
6003
    $signoff = "\n";
6004
    if (!empty($CFG->supportname)) {
6005
        $signoff .= $CFG->supportname."\n";
6006
    }
6007
 
6008
    $supportemail = $OUTPUT->supportemail(['class' => 'font-weight-bold']);
6009
 
6010
    if ($supportemail) {
6011
        $signoff .= "\n" . $supportemail . "\n";
6012
    }
6013
 
6014
    return $signoff;
6015
}
6016
 
6017
/**
6018
 * Sets specified user's password and send the new password to the user via email.
6019
 *
6020
 * @param stdClass $user A {@link $USER} object
6021
 * @param bool $fasthash If true, use a low cost factor when generating the hash for speed.
6022
 * @return bool|string Returns "true" if mail was sent OK and "false" if there was an error
6023
 */
6024
function setnew_password_and_mail($user, $fasthash = false) {
6025
    global $CFG, $DB;
6026
 
6027
    // We try to send the mail in language the user understands,
6028
    // unfortunately the filter_string() does not support alternative langs yet
6029
    // so multilang will not work properly for site->fullname.
6030
    $lang = empty($user->lang) ? get_newuser_language() : $user->lang;
6031
 
6032
    $site  = get_site();
6033
 
6034
    $supportuser = core_user::get_support_user();
6035
 
6036
    $newpassword = generate_password();
6037
 
6038
    update_internal_user_password($user, $newpassword, $fasthash);
6039
 
6040
    $a = new stdClass();
6041
    $a->firstname   = fullname($user, true);
6042
    $a->sitename    = format_string($site->fullname);
6043
    $a->username    = $user->username;
6044
    $a->newpassword = $newpassword;
6045
    $a->link        = $CFG->wwwroot .'/login/?lang='.$lang;
6046
    $a->signoff     = generate_email_signoff();
6047
 
6048
    $message = (string)new lang_string('newusernewpasswordtext', '', $a, $lang);
6049
 
6050
    $subject = format_string($site->fullname) .': '. (string)new lang_string('newusernewpasswordsubj', '', $a, $lang);
6051
 
6052
    // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6053
    return email_to_user($user, $supportuser, $subject, $message);
6054
 
6055
}
6056
 
6057
/**
6058
 * Resets specified user's password and send the new password to the user via email.
6059
 *
6060
 * @param stdClass $user A {@link $USER} object
6061
 * @return bool Returns true if mail was sent OK and false if there was an error.
6062
 */
6063
function reset_password_and_mail($user) {
6064
    global $CFG;
6065
 
6066
    $site  = get_site();
6067
    $supportuser = core_user::get_support_user();
6068
 
6069
    $userauth = get_auth_plugin($user->auth);
6070
    if (!$userauth->can_reset_password() or !is_enabled_auth($user->auth)) {
6071
        trigger_error("Attempt to reset user password for user $user->username with Auth $user->auth.");
6072
        return false;
6073
    }
6074
 
6075
    $newpassword = generate_password();
6076
 
6077
    if (!$userauth->user_update_password($user, $newpassword)) {
6078
        throw new \moodle_exception("cannotsetpassword");
6079
    }
6080
 
6081
    $a = new stdClass();
6082
    $a->firstname   = $user->firstname;
6083
    $a->lastname    = $user->lastname;
6084
    $a->sitename    = format_string($site->fullname);
6085
    $a->username    = $user->username;
6086
    $a->newpassword = $newpassword;
6087
    $a->link        = $CFG->wwwroot .'/login/change_password.php';
6088
    $a->signoff     = generate_email_signoff();
6089
 
6090
    $message = get_string('newpasswordtext', '', $a);
6091
 
6092
    $subject  = format_string($site->fullname) .': '. get_string('changedpassword');
6093
 
6094
    unset_user_preference('create_password', $user); // Prevent cron from generating the password.
6095
 
6096
    // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6097
    return email_to_user($user, $supportuser, $subject, $message);
6098
}
6099
 
6100
/**
6101
 * Send email to specified user with confirmation text and activation link.
6102
 *
6103
 * @param stdClass $user A {@link $USER} object
6104
 * @param string $confirmationurl user confirmation URL
6105
 * @return bool Returns true if mail was sent OK and false if there was an error.
6106
 */
6107
function send_confirmation_email($user, $confirmationurl = null) {
6108
    global $CFG;
6109
 
6110
    $site = get_site();
6111
    $supportuser = core_user::get_support_user();
6112
 
6113
    $data = new stdClass();
6114
    $data->sitename  = format_string($site->fullname);
6115
    $data->admin     = generate_email_signoff();
6116
 
6117
    $subject = get_string('emailconfirmationsubject', '', format_string($site->fullname));
6118
 
6119
    if (empty($confirmationurl)) {
6120
        $confirmationurl = '/login/confirm.php';
6121
    }
6122
 
6123
    $confirmationurl = new moodle_url($confirmationurl);
6124
    // Remove data parameter just in case it was included in the confirmation so we can add it manually later.
6125
    $confirmationurl->remove_params('data');
6126
    $confirmationpath = $confirmationurl->out(false);
6127
 
6128
    // We need to custom encode the username to include trailing dots in the link.
6129
    // Because of this custom encoding we can't use moodle_url directly.
6130
    // Determine if a query string is present in the confirmation url.
6131
    $hasquerystring = strpos($confirmationpath, '?') !== false;
6132
    // Perform normal url encoding of the username first.
6133
    $username = urlencode($user->username);
6134
    // Prevent problems with trailing dots not being included as part of link in some mail clients.
6135
    $username = str_replace('.', '%2E', $username);
6136
 
6137
    $data->link = $confirmationpath . ( $hasquerystring ? '&' : '?') . 'data='. $user->secret .'/'. $username;
6138
 
6139
    $message     = get_string('emailconfirmation', '', $data);
6140
    $messagehtml = text_to_html(get_string('emailconfirmation', '', $data), false, false, true);
6141
 
6142
    // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6143
    return email_to_user($user, $supportuser, $subject, $message, $messagehtml);
6144
}
6145
 
6146
/**
6147
 * Sends a password change confirmation email.
6148
 *
6149
 * @param stdClass $user A {@link $USER} object
6150
 * @param stdClass $resetrecord An object tracking metadata regarding password reset request
6151
 * @return bool Returns true if mail was sent OK and false if there was an error.
6152
 */
6153
function send_password_change_confirmation_email($user, $resetrecord) {
6154
    global $CFG;
6155
 
6156
    $site = get_site();
6157
    $supportuser = core_user::get_support_user();
6158
    $pwresetmins = isset($CFG->pwresettime) ? floor($CFG->pwresettime / MINSECS) : 30;
6159
 
6160
    $data = new stdClass();
6161
    $data->firstname = $user->firstname;
6162
    $data->lastname  = $user->lastname;
6163
    $data->username  = $user->username;
6164
    $data->sitename  = format_string($site->fullname);
6165
    $data->link      = $CFG->wwwroot .'/login/forgot_password.php?token='. $resetrecord->token;
6166
    $data->admin     = generate_email_signoff();
6167
    $data->resetminutes = $pwresetmins;
6168
 
6169
    $message = get_string('emailresetconfirmation', '', $data);
6170
    $subject = get_string('emailresetconfirmationsubject', '', format_string($site->fullname));
6171
 
6172
    // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6173
    return email_to_user($user, $supportuser, $subject, $message);
6174
 
6175
}
6176
 
6177
/**
6178
 * Sends an email containing information on how to change your password.
6179
 *
6180
 * @param stdClass $user A {@link $USER} object
6181
 * @return bool Returns true if mail was sent OK and false if there was an error.
6182
 */
6183
function send_password_change_info($user) {
6184
    $site = get_site();
6185
    $supportuser = core_user::get_support_user();
6186
 
6187
    $data = new stdClass();
6188
    $data->firstname = $user->firstname;
6189
    $data->lastname  = $user->lastname;
6190
    $data->username  = $user->username;
6191
    $data->sitename  = format_string($site->fullname);
6192
    $data->admin     = generate_email_signoff();
6193
 
6194
    if (!is_enabled_auth($user->auth)) {
6195
        $message = get_string('emailpasswordchangeinfodisabled', '', $data);
6196
        $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
6197
        // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6198
        return email_to_user($user, $supportuser, $subject, $message);
6199
    }
6200
 
6201
    $userauth = get_auth_plugin($user->auth);
6202
    ['subject' => $subject, 'message' => $message] = $userauth->get_password_change_info($user);
6203
 
6204
    // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6205
    return email_to_user($user, $supportuser, $subject, $message);
6206
}
6207
 
6208
/**
6209
 * Check that an email is allowed.  It returns an error message if there was a problem.
6210
 *
6211
 * @param string $email Content of email
6212
 * @return string|false
6213
 */
6214
function email_is_not_allowed($email) {
6215
    global $CFG;
6216
 
6217
    // Comparing lowercase domains.
6218
    $email = strtolower($email);
6219
    if (!empty($CFG->allowemailaddresses)) {
6220
        $allowed = explode(' ', strtolower($CFG->allowemailaddresses));
6221
        foreach ($allowed as $allowedpattern) {
6222
            $allowedpattern = trim($allowedpattern);
6223
            if (!$allowedpattern) {
6224
                continue;
6225
            }
6226
            if (strpos($allowedpattern, '.') === 0) {
6227
                if (strpos(strrev($email), strrev($allowedpattern)) === 0) {
6228
                    // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com".
6229
                    return false;
6230
                }
6231
 
6232
            } else if (strpos(strrev($email), strrev('@'.$allowedpattern)) === 0) {
6233
                return false;
6234
            }
6235
        }
6236
        return get_string('emailonlyallowed', '', $CFG->allowemailaddresses);
6237
 
6238
    } else if (!empty($CFG->denyemailaddresses)) {
6239
        $denied = explode(' ', strtolower($CFG->denyemailaddresses));
6240
        foreach ($denied as $deniedpattern) {
6241
            $deniedpattern = trim($deniedpattern);
6242
            if (!$deniedpattern) {
6243
                continue;
6244
            }
6245
            if (strpos($deniedpattern, '.') === 0) {
6246
                if (strpos(strrev($email), strrev($deniedpattern)) === 0) {
6247
                    // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com".
6248
                    return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
6249
                }
6250
 
6251
            } else if (strpos(strrev($email), strrev('@'.$deniedpattern)) === 0) {
6252
                return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
6253
            }
6254
        }
6255
    }
6256
 
6257
    return false;
6258
}
6259
 
6260
// FILE HANDLING.
6261
 
6262
/**
6263
 * Returns local file storage instance
6264
 *
6265
 * @return ?file_storage
6266
 */
6267
function get_file_storage($reset = false) {
6268
    global $CFG;
6269
 
6270
    static $fs = null;
6271
 
6272
    if ($reset) {
6273
        $fs = null;
6274
        return;
6275
    }
6276
 
6277
    if ($fs) {
6278
        return $fs;
6279
    }
6280
 
6281
    require_once("$CFG->libdir/filelib.php");
6282
 
6283
    $fs = new file_storage();
6284
 
6285
    return $fs;
6286
}
6287
 
6288
/**
6289
 * Returns local file storage instance
6290
 *
6291
 * @return file_browser
6292
 */
6293
function get_file_browser() {
6294
    global $CFG;
6295
 
6296
    static $fb = null;
6297
 
6298
    if ($fb) {
6299
        return $fb;
6300
    }
6301
 
6302
    require_once("$CFG->libdir/filelib.php");
6303
 
6304
    $fb = new file_browser();
6305
 
6306
    return $fb;
6307
}
6308
 
6309
/**
6310
 * Returns file packer
6311
 *
6312
 * @param string $mimetype default application/zip
6313
 * @return file_packer|false
6314
 */
6315
function get_file_packer($mimetype='application/zip') {
6316
    global $CFG;
6317
 
6318
    static $fp = array();
6319
 
6320
    if (isset($fp[$mimetype])) {
6321
        return $fp[$mimetype];
6322
    }
6323
 
6324
    switch ($mimetype) {
6325
        case 'application/zip':
6326
        case 'application/vnd.moodle.profiling':
6327
            $classname = 'zip_packer';
6328
            break;
6329
 
6330
        case 'application/x-gzip' :
6331
            $classname = 'tgz_packer';
6332
            break;
6333
 
6334
        case 'application/vnd.moodle.backup':
6335
            $classname = 'mbz_packer';
6336
            break;
6337
 
6338
        default:
6339
            return false;
6340
    }
6341
 
6342
    require_once("$CFG->libdir/filestorage/$classname.php");
6343
    $fp[$mimetype] = new $classname();
6344
 
6345
    return $fp[$mimetype];
6346
}
6347
 
6348
/**
6349
 * Returns current name of file on disk if it exists.
6350
 *
6351
 * @param string $newfile File to be verified
6352
 * @return string Current name of file on disk if true
6353
 */
6354
function valid_uploaded_file($newfile) {
6355
    if (empty($newfile)) {
6356
        return '';
6357
    }
6358
    if (is_uploaded_file($newfile['tmp_name']) and $newfile['size'] > 0) {
6359
        return $newfile['tmp_name'];
6360
    } else {
6361
        return '';
6362
    }
6363
}
6364
 
6365
/**
6366
 * Returns the maximum size for uploading files.
6367
 *
6368
 * There are seven possible upload limits:
6369
 * 1. in Apache using LimitRequestBody (no way of checking or changing this)
6370
 * 2. in php.ini for 'upload_max_filesize' (can not be changed inside PHP)
6371
 * 3. in .htaccess for 'upload_max_filesize' (can not be changed inside PHP)
6372
 * 4. in php.ini for 'post_max_size' (can not be changed inside PHP)
6373
 * 5. by the Moodle admin in $CFG->maxbytes
6374
 * 6. by the teacher in the current course $course->maxbytes
6375
 * 7. by the teacher for the current module, eg $assignment->maxbytes
6376
 *
6377
 * These last two are passed to this function as arguments (in bytes).
6378
 * Anything defined as 0 is ignored.
6379
 * The smallest of all the non-zero numbers is returned.
6380
 *
6381
 * @todo Finish documenting this function
6382
 *
6383
 * @param int $sitebytes Set maximum size
6384
 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6385
 * @param int $modulebytes Current module ->maxbytes (in bytes)
6386
 * @param bool $unused This parameter has been deprecated and is not used any more.
6387
 * @return int The maximum size for uploading files.
6388
 */
6389
function get_max_upload_file_size($sitebytes=0, $coursebytes=0, $modulebytes=0, $unused = false) {
6390
 
6391
    if (! $filesize = ini_get('upload_max_filesize')) {
6392
        $filesize = '5M';
6393
    }
6394
    $minimumsize = get_real_size($filesize);
6395
 
6396
    if ($postsize = ini_get('post_max_size')) {
6397
        $postsize = get_real_size($postsize);
6398
        if ($postsize < $minimumsize) {
6399
            $minimumsize = $postsize;
6400
        }
6401
    }
6402
 
6403
    if (($sitebytes > 0) and ($sitebytes < $minimumsize)) {
6404
        $minimumsize = $sitebytes;
6405
    }
6406
 
6407
    if (($coursebytes > 0) and ($coursebytes < $minimumsize)) {
6408
        $minimumsize = $coursebytes;
6409
    }
6410
 
6411
    if (($modulebytes > 0) and ($modulebytes < $minimumsize)) {
6412
        $minimumsize = $modulebytes;
6413
    }
6414
 
6415
    return $minimumsize;
6416
}
6417
 
6418
/**
6419
 * Returns the maximum size for uploading files for the current user
6420
 *
6421
 * This function takes in account {@link get_max_upload_file_size()} the user's capabilities
6422
 *
6423
 * @param context $context The context in which to check user capabilities
6424
 * @param int $sitebytes Set maximum size
6425
 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6426
 * @param int $modulebytes Current module ->maxbytes (in bytes)
6427
 * @param stdClass|int|null $user The user
6428
 * @param bool $unused This parameter has been deprecated and is not used any more.
6429
 * @return int The maximum size for uploading files.
6430
 */
6431
function get_user_max_upload_file_size($context, $sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $user = null,
6432
        $unused = false) {
6433
    global $USER;
6434
 
6435
    if (empty($user)) {
6436
        $user = $USER;
6437
    }
6438
 
6439
    if (has_capability('moodle/course:ignorefilesizelimits', $context, $user)) {
6440
        return USER_CAN_IGNORE_FILE_SIZE_LIMITS;
6441
    }
6442
 
6443
    return get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes);
6444
}
6445
 
6446
/**
6447
 * Returns an array of possible sizes in local language
6448
 *
6449
 * Related to {@link get_max_upload_file_size()} - this function returns an
6450
 * array of possible sizes in an array, translated to the
6451
 * local language.
6452
 *
6453
 * The list of options will go up to the minimum of $sitebytes, $coursebytes or $modulebytes.
6454
 *
6455
 * If $coursebytes or $sitebytes is not 0, an option will be included for "Course/Site upload limit (X)"
6456
 * with the value set to 0. This option will be the first in the list.
6457
 *
6458
 * @uses SORT_NUMERIC
6459
 * @param int $sitebytes Set maximum size
6460
 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6461
 * @param int $modulebytes Current module ->maxbytes (in bytes)
6462
 * @param int|array $custombytes custom upload size/s which will be added to list,
6463
 *        Only value/s smaller then maxsize will be added to list.
6464
 * @return array
6465
 */
6466
function get_max_upload_sizes($sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $custombytes = null) {
6467
    global $CFG;
6468
 
6469
    if (!$maxsize = get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes)) {
6470
        return array();
6471
    }
6472
 
6473
    if ($sitebytes == 0) {
6474
        // Will get the minimum of upload_max_filesize or post_max_size.
6475
        $sitebytes = get_max_upload_file_size();
6476
    }
6477
 
6478
    $filesize = array();
6479
    $sizelist = array(10240, 51200, 102400, 512000, 1048576, 2097152,
6480
                      5242880, 10485760, 20971520, 52428800, 104857600,
6481
                      262144000, 524288000, 786432000, 1073741824,
6482
                      2147483648, 4294967296, 8589934592);
6483
 
6484
    // If custombytes is given and is valid then add it to the list.
6485
    if (is_number($custombytes) and $custombytes > 0) {
6486
        $custombytes = (int)$custombytes;
6487
        if (!in_array($custombytes, $sizelist)) {
6488
            $sizelist[] = $custombytes;
6489
        }
6490
    } else if (is_array($custombytes)) {
6491
        $sizelist = array_unique(array_merge($sizelist, $custombytes));
6492
    }
6493
 
6494
    // Allow maxbytes to be selected if it falls outside the above boundaries.
6495
    if (isset($CFG->maxbytes) && !in_array(get_real_size($CFG->maxbytes), $sizelist)) {
6496
        // Note: get_real_size() is used in order to prevent problems with invalid values.
6497
        $sizelist[] = get_real_size($CFG->maxbytes);
6498
    }
6499
 
6500
    foreach ($sizelist as $sizebytes) {
6501
        if ($sizebytes < $maxsize && $sizebytes > 0) {
6502
            $filesize[(string)intval($sizebytes)] = display_size($sizebytes, 0);
6503
        }
6504
    }
6505
 
6506
    $limitlevel = '';
6507
    $displaysize = '';
6508
    if ($modulebytes &&
6509
        (($modulebytes < $coursebytes || $coursebytes == 0) &&
6510
         ($modulebytes < $sitebytes || $sitebytes == 0))) {
6511
        $limitlevel = get_string('activity', 'core');
6512
        $displaysize = display_size($modulebytes, 0);
6513
        $filesize[$modulebytes] = $displaysize; // Make sure the limit is also included in the list.
6514
 
6515
    } else if ($coursebytes && ($coursebytes < $sitebytes || $sitebytes == 0)) {
6516
        $limitlevel = get_string('course', 'core');
6517
        $displaysize = display_size($coursebytes, 0);
6518
        $filesize[$coursebytes] = $displaysize; // Make sure the limit is also included in the list.
6519
 
6520
    } else if ($sitebytes) {
6521
        $limitlevel = get_string('site', 'core');
6522
        $displaysize = display_size($sitebytes, 0);
6523
        $filesize[$sitebytes] = $displaysize; // Make sure the limit is also included in the list.
6524
    }
6525
 
6526
    krsort($filesize, SORT_NUMERIC);
6527
    if ($limitlevel) {
6528
        $params = (object) array('contextname' => $limitlevel, 'displaysize' => $displaysize);
6529
        $filesize  = array('0' => get_string('uploadlimitwithsize', 'core', $params)) + $filesize;
6530
    }
6531
 
6532
    return $filesize;
6533
}
6534
 
6535
/**
6536
 * Returns an array with all the filenames in all subdirectories, relative to the given rootdir.
6537
 *
6538
 * If excludefiles is defined, then that file/directory is ignored
6539
 * If getdirs is true, then (sub)directories are included in the output
6540
 * If getfiles is true, then files are included in the output
6541
 * (at least one of these must be true!)
6542
 *
6543
 * @todo Finish documenting this function. Add examples of $excludefile usage.
6544
 *
6545
 * @param string $rootdir A given root directory to start from
6546
 * @param string|array $excludefiles If defined then the specified file/directory is ignored
6547
 * @param bool $descend If true then subdirectories are recursed as well
6548
 * @param bool $getdirs If true then (sub)directories are included in the output
6549
 * @param bool $getfiles  If true then files are included in the output
6550
 * @return array An array with all the filenames in all subdirectories, relative to the given rootdir
6551
 */
6552
function get_directory_list($rootdir, $excludefiles='', $descend=true, $getdirs=false, $getfiles=true) {
6553
 
6554
    $dirs = array();
6555
 
6556
    if (!$getdirs and !$getfiles) {   // Nothing to show.
6557
        return $dirs;
6558
    }
6559
 
6560
    if (!is_dir($rootdir)) {          // Must be a directory.
6561
        return $dirs;
6562
    }
6563
 
6564
    if (!$dir = opendir($rootdir)) {  // Can't open it for some reason.
6565
        return $dirs;
6566
    }
6567
 
6568
    if (!is_array($excludefiles)) {
6569
        $excludefiles = array($excludefiles);
6570
    }
6571
 
6572
    while (false !== ($file = readdir($dir))) {
6573
        $firstchar = substr($file, 0, 1);
6574
        if ($firstchar == '.' or $file == 'CVS' or in_array($file, $excludefiles)) {
6575
            continue;
6576
        }
6577
        $fullfile = $rootdir .'/'. $file;
6578
        if (filetype($fullfile) == 'dir') {
6579
            if ($getdirs) {
6580
                $dirs[] = $file;
6581
            }
6582
            if ($descend) {
6583
                $subdirs = get_directory_list($fullfile, $excludefiles, $descend, $getdirs, $getfiles);
6584
                foreach ($subdirs as $subdir) {
6585
                    $dirs[] = $file .'/'. $subdir;
6586
                }
6587
            }
6588
        } else if ($getfiles) {
6589
            $dirs[] = $file;
6590
        }
6591
    }
6592
    closedir($dir);
6593
 
6594
    asort($dirs);
6595
 
6596
    return $dirs;
6597
}
6598
 
6599
 
6600
/**
6601
 * Adds up all the files in a directory and works out the size.
6602
 *
6603
 * @param string $rootdir  The directory to start from
6604
 * @param string $excludefile A file to exclude when summing directory size
6605
 * @return int The summed size of all files and subfiles within the root directory
6606
 */
6607
function get_directory_size($rootdir, $excludefile='') {
6608
    global $CFG;
6609
 
6610
    // Do it this way if we can, it's much faster.
6611
    if (!empty($CFG->pathtodu) && is_executable(trim($CFG->pathtodu))) {
6612
        $command = trim($CFG->pathtodu).' -sk '.escapeshellarg($rootdir);
6613
        $output = null;
6614
        $return = null;
6615
        exec($command, $output, $return);
6616
        if (is_array($output)) {
6617
            // We told it to return k.
6618
            return get_real_size(intval($output[0]).'k');
6619
        }
6620
    }
6621
 
6622
    if (!is_dir($rootdir)) {
6623
        // Must be a directory.
6624
        return 0;
6625
    }
6626
 
6627
    if (!$dir = @opendir($rootdir)) {
6628
        // Can't open it for some reason.
6629
        return 0;
6630
    }
6631
 
6632
    $size = 0;
6633
 
6634
    while (false !== ($file = readdir($dir))) {
6635
        $firstchar = substr($file, 0, 1);
6636
        if ($firstchar == '.' or $file == 'CVS' or $file == $excludefile) {
6637
            continue;
6638
        }
6639
        $fullfile = $rootdir .'/'. $file;
6640
        if (filetype($fullfile) == 'dir') {
6641
            $size += get_directory_size($fullfile, $excludefile);
6642
        } else {
6643
            $size += filesize($fullfile);
6644
        }
6645
    }
6646
    closedir($dir);
6647
 
6648
    return $size;
6649
}
6650
 
6651
/**
6652
 * Converts bytes into display form
6653
 *
6654
 * @param int $size  The size to convert to human readable form
6655
 * @param int $decimalplaces If specified, uses fixed number of decimal places
6656
 * @param string $fixedunits If specified, uses fixed units (e.g. 'KB')
6657
 * @return string Display version of size
6658
 */
6659
function display_size($size, int $decimalplaces = 1, string $fixedunits = ''): string {
6660
 
6661
    static $units;
6662
 
6663
    if ($size === USER_CAN_IGNORE_FILE_SIZE_LIMITS) {
6664
        return get_string('unlimited');
6665
    }
6666
 
6667
    if (empty($units)) {
6668
        $units[] = get_string('sizeb');
6669
        $units[] = get_string('sizekb');
6670
        $units[] = get_string('sizemb');
6671
        $units[] = get_string('sizegb');
6672
        $units[] = get_string('sizetb');
6673
        $units[] = get_string('sizepb');
6674
    }
6675
 
6676
    switch ($fixedunits) {
6677
        case 'PB' :
6678
            $magnitude = 5;
6679
            break;
6680
        case 'TB' :
6681
            $magnitude = 4;
6682
            break;
6683
        case 'GB' :
6684
            $magnitude = 3;
6685
            break;
6686
        case 'MB' :
6687
            $magnitude = 2;
6688
            break;
6689
        case 'KB' :
6690
            $magnitude = 1;
6691
            break;
6692
        case 'B' :
6693
            $magnitude = 0;
6694
            break;
6695
        case '':
6696
            $magnitude = floor(log($size, 1024));
6697
            $magnitude = max(0, min(5, $magnitude));
6698
            break;
6699
        default:
6700
            throw new coding_exception('Unknown fixed units value: ' . $fixedunits);
6701
    }
6702
 
6703
    // Special case for magnitude 0 (bytes) - never use decimal places.
6704
    $nbsp = "\xc2\xa0";
6705
    if ($magnitude === 0) {
6706
        return round($size) . $nbsp . $units[$magnitude];
6707
    }
6708
 
6709
    // Convert to specified units.
6710
    $sizeinunit = $size / 1024 ** $magnitude;
6711
 
6712
    // Fixed decimal places.
6713
    return sprintf('%.' . $decimalplaces . 'f', $sizeinunit) . $nbsp . $units[$magnitude];
6714
}
6715
 
6716
/**
6717
 * Cleans a given filename by removing suspicious or troublesome characters
6718
 *
6719
 * @see clean_param()
6720
 * @param string $string file name
6721
 * @return string cleaned file name
6722
 */
6723
function clean_filename($string) {
6724
    return clean_param($string, PARAM_FILE);
6725
}
6726
 
6727
// STRING TRANSLATION.
6728
 
6729
/**
6730
 * Returns the code for the current language
6731
 *
6732
 * @category string
6733
 * @return string
6734
 */
6735
function current_language() {
6736
    global $CFG, $PAGE, $SESSION, $USER;
6737
 
6738
    if (!empty($SESSION->forcelang)) {
6739
        // Allows overriding course-forced language (useful for admins to check
6740
        // issues in courses whose language they don't understand).
6741
        // Also used by some code to temporarily get language-related information in a
6742
        // specific language (see force_current_language()).
6743
        $return = $SESSION->forcelang;
6744
 
6745
    } else if (!empty($PAGE->cm->lang)) {
6746
        // Activity language, if set.
6747
        $return = $PAGE->cm->lang;
6748
 
6749
    } else if (!empty($PAGE->course->id) && $PAGE->course->id != SITEID && !empty($PAGE->course->lang)) {
6750
        // Course language can override all other settings for this page.
6751
        $return = $PAGE->course->lang;
6752
 
6753
    } else if (!empty($SESSION->lang)) {
6754
        // Session language can override other settings.
6755
        $return = $SESSION->lang;
6756
 
6757
    } else if (!empty($USER->lang)) {
6758
        $return = $USER->lang;
6759
 
6760
    } else if (isset($CFG->lang)) {
6761
        $return = $CFG->lang;
6762
 
6763
    } else {
6764
        $return = 'en';
6765
    }
6766
 
6767
    // Just in case this slipped in from somewhere by accident.
6768
    $return = str_replace('_utf8', '', $return);
6769
 
6770
    return $return;
6771
}
6772
 
6773
/**
6774
 * Fix the current language to the given language code.
6775
 *
6776
 * @param string $lang The language code to use.
6777
 * @return void
6778
 */
6779
function fix_current_language(string $lang): void {
6780
    global $CFG, $COURSE, $SESSION, $USER;
6781
 
6782
    if (!get_string_manager()->translation_exists($lang)) {
6783
        throw new coding_exception("The language pack for $lang is not available");
6784
    }
6785
 
6786
    $fixglobal = '';
6787
    $fixlang = 'lang';
6788
    if (!empty($SESSION->forcelang)) {
6789
        $fixglobal = $SESSION;
6790
        $fixlang = 'forcelang';
6791
    } else if (!empty($COURSE->id) && $COURSE->id != SITEID && !empty($COURSE->lang)) {
6792
        $fixglobal = $COURSE;
6793
    } else if (!empty($SESSION->lang)) {
6794
        $fixglobal = $SESSION;
6795
    } else if (!empty($USER->lang)) {
6796
        $fixglobal = $USER;
6797
    } else if (isset($CFG->lang)) {
6798
        set_config('lang', $lang);
6799
    }
6800
 
6801
    if ($fixglobal) {
6802
        $fixglobal->$fixlang = $lang;
6803
    }
6804
}
6805
 
6806
/**
6807
 * Returns parent language of current active language if defined
6808
 *
6809
 * @category string
6810
 * @param string $lang null means current language
6811
 * @return string
6812
 */
6813
function get_parent_language($lang=null) {
6814
 
6815
    $parentlang = get_string_manager()->get_string('parentlanguage', 'langconfig', null, $lang);
6816
 
6817
    if ($parentlang === 'en') {
6818
        $parentlang = '';
6819
    }
6820
 
6821
    return $parentlang;
6822
}
6823
 
6824
/**
6825
 * Force the current language to get strings and dates localised in the given language.
6826
 *
6827
 * After calling this function, all strings will be provided in the given language
6828
 * until this function is called again, or equivalent code is run.
6829
 *
6830
 * @param string $language
6831
 * @return string previous $SESSION->forcelang value
6832
 */
6833
function force_current_language($language) {
6834
    global $SESSION;
6835
    $sessionforcelang = isset($SESSION->forcelang) ? $SESSION->forcelang : '';
6836
    if ($language !== $sessionforcelang) {
6837
        // Setting forcelang to null or an empty string disables its effect.
6838
        if (empty($language) || get_string_manager()->translation_exists($language, false)) {
6839
            $SESSION->forcelang = $language;
6840
            moodle_setlocale();
6841
        }
6842
    }
6843
    return $sessionforcelang;
6844
}
6845
 
6846
/**
6847
 * Returns current string_manager instance.
6848
 *
6849
 * The param $forcereload is needed for CLI installer only where the string_manager instance
6850
 * must be replaced during the install.php script life time.
6851
 *
6852
 * @category string
6853
 * @param bool $forcereload shall the singleton be released and new instance created instead?
6854
 * @return core_string_manager
6855
 */
6856
function get_string_manager($forcereload=false) {
6857
    global $CFG;
6858
 
6859
    static $singleton = null;
6860
 
6861
    if ($forcereload) {
6862
        $singleton = null;
6863
    }
6864
    if ($singleton === null) {
6865
        if (empty($CFG->early_install_lang)) {
6866
 
6867
            $transaliases = array();
6868
            if (empty($CFG->langlist)) {
6869
                 $translist = array();
6870
            } else {
6871
                $translist = explode(',', $CFG->langlist);
6872
                $translist = array_map('trim', $translist);
6873
                // Each language in the $CFG->langlist can has an "alias" that would substitute the default language name.
6874
                foreach ($translist as $i => $value) {
6875
                    $parts = preg_split('/\s*\|\s*/', $value, 2);
6876
                    if (count($parts) == 2) {
6877
                        $transaliases[$parts[0]] = $parts[1];
6878
                        $translist[$i] = $parts[0];
6879
                    }
6880
                }
6881
            }
6882
 
6883
            if (!empty($CFG->config_php_settings['customstringmanager'])) {
6884
                $classname = $CFG->config_php_settings['customstringmanager'];
6885
 
6886
                if (class_exists($classname)) {
6887
                    $implements = class_implements($classname);
6888
 
6889
                    if (isset($implements['core_string_manager'])) {
6890
                        $singleton = new $classname($CFG->langotherroot, $CFG->langlocalroot, $translist, $transaliases);
6891
                        return $singleton;
6892
 
6893
                    } else {
6894
                        debugging('Unable to instantiate custom string manager: class '.$classname.
6895
                            ' does not implement the core_string_manager interface.');
6896
                    }
6897
 
6898
                } else {
6899
                    debugging('Unable to instantiate custom string manager: class '.$classname.' can not be found.');
6900
                }
6901
            }
6902
 
6903
            $singleton = new core_string_manager_standard($CFG->langotherroot, $CFG->langlocalroot, $translist, $transaliases);
6904
 
6905
        } else {
6906
            $singleton = new core_string_manager_install();
6907
        }
6908
    }
6909
 
6910
    return $singleton;
6911
}
6912
 
6913
/**
6914
 * Returns a localized string.
6915
 *
6916
 * Returns the translated string specified by $identifier as
6917
 * for $module.  Uses the same format files as STphp.
6918
 * $a is an object, string or number that can be used
6919
 * within translation strings
6920
 *
6921
 * eg 'hello {$a->firstname} {$a->lastname}'
6922
 * or 'hello {$a}'
6923
 *
6924
 * If you would like to directly echo the localized string use
6925
 * the function {@link print_string()}
6926
 *
6927
 * Example usage of this function involves finding the string you would
6928
 * like a local equivalent of and using its identifier and module information
6929
 * to retrieve it.<br/>
6930
 * If you open moodle/lang/en/moodle.php and look near line 278
6931
 * you will find a string to prompt a user for their word for 'course'
6932
 * <code>
6933
 * $string['course'] = 'Course';
6934
 * </code>
6935
 * So if you want to display the string 'Course'
6936
 * in any language that supports it on your site
6937
 * you just need to use the identifier 'course'
6938
 * <code>
6939
 * $mystring = '<strong>'. get_string('course') .'</strong>';
6940
 * or
6941
 * </code>
6942
 * If the string you want is in another file you'd take a slightly
6943
 * different approach. Looking in moodle/lang/en/calendar.php you find
6944
 * around line 75:
6945
 * <code>
6946
 * $string['typecourse'] = 'Course event';
6947
 * </code>
6948
 * If you want to display the string "Course event" in any language
6949
 * supported you would use the identifier 'typecourse' and the module 'calendar'
6950
 * (because it is in the file calendar.php):
6951
 * <code>
6952
 * $mystring = '<h1>'. get_string('typecourse', 'calendar') .'</h1>';
6953
 * </code>
6954
 *
6955
 * As a last resort, should the identifier fail to map to a string
6956
 * the returned string will be [[ $identifier ]]
6957
 *
6958
 * In Moodle 2.3 there is a new argument to this function $lazyload.
6959
 * Setting $lazyload to true causes get_string to return a lang_string object
6960
 * rather than the string itself. The fetching of the string is then put off until
6961
 * the string object is first used. The object can be used by calling it's out
6962
 * method or by casting the object to a string, either directly e.g.
6963
 *     (string)$stringobject
6964
 * or indirectly by using the string within another string or echoing it out e.g.
6965
 *     echo $stringobject
6966
 *     return "<p>{$stringobject}</p>";
6967
 * It is worth noting that using $lazyload and attempting to use the string as an
6968
 * array key will cause a fatal error as objects cannot be used as array keys.
6969
 * But you should never do that anyway!
6970
 * For more information {@link lang_string}
6971
 *
6972
 * @category string
6973
 * @param string $identifier The key identifier for the localized string
6974
 * @param string $component The module where the key identifier is stored,
6975
 *      usually expressed as the filename in the language pack without the
6976
 *      .php on the end but can also be written as mod/forum or grade/export/xls.
6977
 *      If none is specified then moodle.php is used.
6978
 * @param string|object|array|int $a An object, string or number that can be used
6979
 *      within translation strings
6980
 * @param bool $lazyload If set to true a string object is returned instead of
6981
 *      the string itself. The string then isn't calculated until it is first used.
6982
 * @return string The localized string.
6983
 * @throws coding_exception
6984
 */
6985
function get_string($identifier, $component = '', $a = null, $lazyload = false) {
6986
    global $CFG;
6987
 
6988
    // If the lazy load argument has been supplied return a lang_string object
6989
    // instead.
6990
    // We need to make sure it is true (and a bool) as you will see below there
6991
    // used to be a forth argument at one point.
6992
    if ($lazyload === true) {
6993
        return new lang_string($identifier, $component, $a);
6994
    }
6995
 
6996
    if ($CFG->debugdeveloper && clean_param($identifier, PARAM_STRINGID) === '') {
6997
        throw new coding_exception('Invalid string identifier. The identifier cannot be empty. Please fix your get_string() call.', DEBUG_DEVELOPER);
6998
    }
6999
 
7000
    // There is now a forth argument again, this time it is a boolean however so
7001
    // we can still check for the old extralocations parameter.
7002
    if (!is_bool($lazyload) && !empty($lazyload)) {
7003
        debugging('extralocations parameter in get_string() is not supported any more, please use standard lang locations only.');
7004
    }
7005
 
7006
    if (strpos((string)$component, '/') !== false) {
7007
        debugging('The module name you passed to get_string is the deprecated format ' .
7008
                'like mod/mymod or block/myblock. The correct form looks like mymod, or block_myblock.' , DEBUG_DEVELOPER);
7009
        $componentpath = explode('/', $component);
7010
 
7011
        switch ($componentpath[0]) {
7012
            case 'mod':
7013
                $component = $componentpath[1];
7014
                break;
7015
            case 'blocks':
7016
            case 'block':
7017
                $component = 'block_'.$componentpath[1];
7018
                break;
7019
            case 'enrol':
7020
                $component = 'enrol_'.$componentpath[1];
7021
                break;
7022
            case 'format':
7023
                $component = 'format_'.$componentpath[1];
7024
                break;
7025
            case 'grade':
7026
                $component = 'grade'.$componentpath[1].'_'.$componentpath[2];
7027
                break;
7028
        }
7029
    }
7030
 
7031
    $result = get_string_manager()->get_string($identifier, $component, $a);
7032
 
7033
    // Debugging feature lets you display string identifier and component.
7034
    if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
7035
        $result .= ' {' . $identifier . '/' . $component . '}';
7036
    }
7037
    return $result;
7038
}
7039
 
7040
/**
7041
 * Converts an array of strings to their localized value.
7042
 *
7043
 * @param array $array An array of strings
7044
 * @param string $component The language module that these strings can be found in.
7045
 * @return stdClass translated strings.
7046
 */
7047
function get_strings($array, $component = '') {
7048
    $string = new stdClass;
7049
    foreach ($array as $item) {
7050
        $string->$item = get_string($item, $component);
7051
    }
7052
    return $string;
7053
}
7054
 
7055
/**
7056
 * Prints out a translated string.
7057
 *
7058
 * Prints out a translated string using the return value from the {@link get_string()} function.
7059
 *
7060
 * Example usage of this function when the string is in the moodle.php file:<br/>
7061
 * <code>
7062
 * echo '<strong>';
7063
 * print_string('course');
7064
 * echo '</strong>';
7065
 * </code>
7066
 *
7067
 * Example usage of this function when the string is not in the moodle.php file:<br/>
7068
 * <code>
7069
 * echo '<h1>';
7070
 * print_string('typecourse', 'calendar');
7071
 * echo '</h1>';
7072
 * </code>
7073
 *
7074
 * @category string
7075
 * @param string $identifier The key identifier for the localized string
7076
 * @param string $component The module where the key identifier is stored. If none is specified then moodle.php is used.
7077
 * @param string|object|array $a An object, string or number that can be used within translation strings
7078
 */
7079
function print_string($identifier, $component = '', $a = null) {
7080
    echo get_string($identifier, $component, $a);
7081
}
7082
 
7083
/**
7084
 * Returns a list of charset codes
7085
 *
7086
 * Returns a list of charset codes. It's hardcoded, so they should be added manually
7087
 * (checking that such charset is supported by the texlib library!)
7088
 *
7089
 * @return array And associative array with contents in the form of charset => charset
7090
 */
7091
function get_list_of_charsets() {
7092
 
7093
    $charsets = array(
7094
        'EUC-JP'     => 'EUC-JP',
7095
        'ISO-2022-JP'=> 'ISO-2022-JP',
7096
        'ISO-8859-1' => 'ISO-8859-1',
7097
        'SHIFT-JIS'  => 'SHIFT-JIS',
7098
        'GB2312'     => 'GB2312',
7099
        'GB18030'    => 'GB18030', // GB18030 not supported by typo and mbstring.
7100
        'UTF-8'      => 'UTF-8');
7101
 
7102
    asort($charsets);
7103
 
7104
    return $charsets;
7105
}
7106
 
7107
/**
7108
 * Returns a list of valid and compatible themes
7109
 *
7110
 * @return array
7111
 */
7112
function get_list_of_themes() {
7113
    global $CFG;
7114
 
7115
    $themes = array();
7116
 
7117
    if (!empty($CFG->themelist)) {       // Use admin's list of themes.
7118
        $themelist = explode(',', $CFG->themelist);
7119
    } else {
7120
        $themelist = array_keys(core_component::get_plugin_list("theme"));
7121
    }
7122
 
7123
    foreach ($themelist as $key => $themename) {
7124
        $theme = theme_config::load($themename);
7125
        $themes[$themename] = $theme;
7126
    }
7127
 
7128
    core_collator::asort_objects_by_method($themes, 'get_theme_name');
7129
 
7130
    return $themes;
7131
}
7132
 
7133
/**
7134
 * Factory function for emoticon_manager
7135
 *
7136
 * @return emoticon_manager singleton
7137
 */
7138
function get_emoticon_manager() {
7139
    static $singleton = null;
7140
 
7141
    if (is_null($singleton)) {
7142
        $singleton = new emoticon_manager();
7143
    }
7144
 
7145
    return $singleton;
7146
}
7147
 
7148
/**
7149
 * Provides core support for plugins that have to deal with emoticons (like HTML editor or emoticon filter).
7150
 *
7151
 * Whenever this manager mentiones 'emoticon object', the following data
7152
 * structure is expected: stdClass with properties text, imagename, imagecomponent,
7153
 * altidentifier and altcomponent
7154
 *
7155
 * @see admin_setting_emoticons
7156
 *
7157
 * @copyright 2010 David Mudrak
7158
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
7159
 */
7160
class emoticon_manager {
7161
 
7162
    /**
7163
     * Returns the currently enabled emoticons
7164
     *
7165
     * @param boolean $selectable - If true, only return emoticons that should be selectable from a list.
7166
     * @return array of emoticon objects
7167
     */
7168
    public function get_emoticons($selectable = false) {
7169
        global $CFG;
7170
        $notselectable = ['martin', 'egg'];
7171
 
7172
        if (empty($CFG->emoticons)) {
7173
            return array();
7174
        }
7175
 
7176
        $emoticons = $this->decode_stored_config($CFG->emoticons);
7177
 
7178
        if (!is_array($emoticons)) {
7179
            // Something is wrong with the format of stored setting.
7180
            debugging('Invalid format of emoticons setting, please resave the emoticons settings form', DEBUG_NORMAL);
7181
            return array();
7182
        }
7183
        if ($selectable) {
7184
            foreach ($emoticons as $index => $emote) {
7185
                if (in_array($emote->altidentifier, $notselectable)) {
7186
                    // Skip this one.
7187
                    unset($emoticons[$index]);
7188
                }
7189
            }
7190
        }
7191
 
7192
        return $emoticons;
7193
    }
7194
 
7195
    /**
7196
     * Converts emoticon object into renderable pix_emoticon object
7197
     *
7198
     * @param stdClass $emoticon emoticon object
7199
     * @param array $attributes explicit HTML attributes to set
7200
     * @return pix_emoticon
7201
     */
7202
    public function prepare_renderable_emoticon(stdClass $emoticon, array $attributes = array()) {
7203
        $stringmanager = get_string_manager();
7204
        if ($stringmanager->string_exists($emoticon->altidentifier, $emoticon->altcomponent)) {
7205
            $alt = get_string($emoticon->altidentifier, $emoticon->altcomponent);
7206
        } else {
7207
            $alt = s($emoticon->text);
7208
        }
7209
        return new pix_emoticon($emoticon->imagename, $alt, $emoticon->imagecomponent, $attributes);
7210
    }
7211
 
7212
    /**
7213
     * Encodes the array of emoticon objects into a string storable in config table
7214
     *
7215
     * @see self::decode_stored_config()
7216
     * @param array $emoticons array of emtocion objects
7217
     * @return string
7218
     */
7219
    public function encode_stored_config(array $emoticons) {
7220
        return json_encode($emoticons);
7221
    }
7222
 
7223
    /**
7224
     * Decodes the string into an array of emoticon objects
7225
     *
7226
     * @see self::encode_stored_config()
7227
     * @param string $encoded
7228
     * @return array|null
7229
     */
7230
    public function decode_stored_config($encoded) {
7231
        $decoded = json_decode($encoded);
7232
        if (!is_array($decoded)) {
7233
            return null;
7234
        }
7235
        return $decoded;
7236
    }
7237
 
7238
    /**
7239
     * Returns default set of emoticons supported by Moodle
7240
     *
7241
     * @return array of sdtClasses
7242
     */
7243
    public function default_emoticons() {
7244
        return array(
7245
            $this->prepare_emoticon_object(":-)", 's/smiley', 'smiley'),
7246
            $this->prepare_emoticon_object(":)", 's/smiley', 'smiley'),
7247
            $this->prepare_emoticon_object(":-D", 's/biggrin', 'biggrin'),
7248
            $this->prepare_emoticon_object(";-)", 's/wink', 'wink'),
7249
            $this->prepare_emoticon_object(":-/", 's/mixed', 'mixed'),
7250
            $this->prepare_emoticon_object("V-.", 's/thoughtful', 'thoughtful'),
7251
            $this->prepare_emoticon_object(":-P", 's/tongueout', 'tongueout'),
7252
            $this->prepare_emoticon_object(":-p", 's/tongueout', 'tongueout'),
7253
            $this->prepare_emoticon_object("B-)", 's/cool', 'cool'),
7254
            $this->prepare_emoticon_object("^-)", 's/approve', 'approve'),
7255
            $this->prepare_emoticon_object("8-)", 's/wideeyes', 'wideeyes'),
7256
            $this->prepare_emoticon_object(":o)", 's/clown', 'clown'),
7257
            $this->prepare_emoticon_object(":-(", 's/sad', 'sad'),
7258
            $this->prepare_emoticon_object(":(", 's/sad', 'sad'),
7259
            $this->prepare_emoticon_object("8-.", 's/shy', 'shy'),
7260
            $this->prepare_emoticon_object(":-I", 's/blush', 'blush'),
7261
            $this->prepare_emoticon_object(":-X", 's/kiss', 'kiss'),
7262
            $this->prepare_emoticon_object("8-o", 's/surprise', 'surprise'),
7263
            $this->prepare_emoticon_object("P-|", 's/blackeye', 'blackeye'),
7264
            $this->prepare_emoticon_object("8-[", 's/angry', 'angry'),
7265
            $this->prepare_emoticon_object("(grr)", 's/angry', 'angry'),
7266
            $this->prepare_emoticon_object("xx-P", 's/dead', 'dead'),
7267
            $this->prepare_emoticon_object("|-.", 's/sleepy', 'sleepy'),
7268
            $this->prepare_emoticon_object("}-]", 's/evil', 'evil'),
7269
            $this->prepare_emoticon_object("(h)", 's/heart', 'heart'),
7270
            $this->prepare_emoticon_object("(heart)", 's/heart', 'heart'),
7271
            $this->prepare_emoticon_object("(y)", 's/yes', 'yes', 'core'),
7272
            $this->prepare_emoticon_object("(n)", 's/no', 'no', 'core'),
7273
            $this->prepare_emoticon_object("(martin)", 's/martin', 'martin'),
7274
            $this->prepare_emoticon_object("( )", 's/egg', 'egg'),
7275
        );
7276
    }
7277
 
7278
    /**
7279
     * Helper method preparing the stdClass with the emoticon properties
7280
     *
7281
     * @param string|array $text or array of strings
7282
     * @param string $imagename to be used by {@link pix_emoticon}
7283
     * @param string $altidentifier alternative string identifier, null for no alt
7284
     * @param string $altcomponent where the alternative string is defined
7285
     * @param string $imagecomponent to be used by {@link pix_emoticon}
7286
     * @return stdClass
7287
     */
7288
    protected function prepare_emoticon_object($text, $imagename, $altidentifier = null,
7289
                                               $altcomponent = 'core_pix', $imagecomponent = 'core') {
7290
        return (object)array(
7291
            'text'           => $text,
7292
            'imagename'      => $imagename,
7293
            'imagecomponent' => $imagecomponent,
7294
            'altidentifier'  => $altidentifier,
7295
            'altcomponent'   => $altcomponent,
7296
        );
7297
    }
7298
}
7299
 
7300
// ENCRYPTION.
7301
 
7302
/**
7303
 * rc4encrypt
7304
 *
7305
 * @param string $data        Data to encrypt.
7306
 * @return string             The now encrypted data.
7307
 */
7308
function rc4encrypt($data) {
7309
    return endecrypt(get_site_identifier(), $data, '');
7310
}
7311
 
7312
/**
7313
 * rc4decrypt
7314
 *
7315
 * @param string $data        Data to decrypt.
7316
 * @return string             The now decrypted data.
7317
 */
7318
function rc4decrypt($data) {
7319
    return endecrypt(get_site_identifier(), $data, 'de');
7320
}
7321
 
7322
/**
7323
 * Based on a class by Mukul Sabharwal [mukulsabharwal @ yahoo.com]
7324
 *
7325
 * @todo Finish documenting this function
7326
 *
7327
 * @param string $pwd The password to use when encrypting or decrypting
7328
 * @param string $data The data to be decrypted/encrypted
7329
 * @param string $case Either 'de' for decrypt or '' for encrypt
7330
 * @return string
7331
 */
7332
function endecrypt ($pwd, $data, $case) {
7333
 
7334
    if ($case == 'de') {
7335
        $data = urldecode($data);
7336
    }
7337
 
7338
    $key[] = '';
7339
    $box[] = '';
7340
    $pwdlength = strlen($pwd);
7341
 
7342
    for ($i = 0; $i <= 255; $i++) {
7343
        $key[$i] = ord(substr($pwd, ($i % $pwdlength), 1));
7344
        $box[$i] = $i;
7345
    }
7346
 
7347
    $x = 0;
7348
 
7349
    for ($i = 0; $i <= 255; $i++) {
7350
        $x = ($x + $box[$i] + $key[$i]) % 256;
7351
        $tempswap = $box[$i];
7352
        $box[$i] = $box[$x];
7353
        $box[$x] = $tempswap;
7354
    }
7355
 
7356
    $cipher = '';
7357
 
7358
    $a = 0;
7359
    $j = 0;
7360
 
7361
    for ($i = 0; $i < strlen($data); $i++) {
7362
        $a = ($a + 1) % 256;
7363
        $j = ($j + $box[$a]) % 256;
7364
        $temp = $box[$a];
7365
        $box[$a] = $box[$j];
7366
        $box[$j] = $temp;
7367
        $k = $box[(($box[$a] + $box[$j]) % 256)];
7368
        $cipherby = ord(substr($data, $i, 1)) ^ $k;
7369
        $cipher .= chr($cipherby);
7370
    }
7371
 
7372
    if ($case == 'de') {
7373
        $cipher = urldecode(urlencode($cipher));
7374
    } else {
7375
        $cipher = urlencode($cipher);
7376
    }
7377
 
7378
    return $cipher;
7379
}
7380
 
7381
// ENVIRONMENT CHECKING.
7382
 
7383
/**
7384
 * This method validates a plug name. It is much faster than calling clean_param.
7385
 *
7386
 * @param string $name a string that might be a plugin name.
7387
 * @return bool if this string is a valid plugin name.
7388
 */
7389
function is_valid_plugin_name($name) {
7390
    // This does not work for 'mod', bad luck, use any other type.
7391
    return core_component::is_valid_plugin_name('tool', $name);
7392
}
7393
 
7394
/**
7395
 * Get a list of all the plugins of a given type that define a certain API function
7396
 * in a certain file. The plugin component names and function names are returned.
7397
 *
7398
 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
7399
 * @param string $function the part of the name of the function after the
7400
 *      frankenstyle prefix. e.g 'hook' if you are looking for functions with
7401
 *      names like report_courselist_hook.
7402
 * @param string $file the name of file within the plugin that defines the
7403
 *      function. Defaults to lib.php.
7404
 * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
7405
 *      and the function names as values (e.g. 'report_courselist_hook', 'forum_hook').
7406
 */
7407
function get_plugin_list_with_function($plugintype, $function, $file = 'lib.php') {
7408
    global $CFG;
7409
 
7410
    // We don't include here as all plugin types files would be included.
7411
    $plugins = get_plugins_with_function($function, $file, false);
7412
 
7413
    if (empty($plugins[$plugintype])) {
7414
        return array();
7415
    }
7416
 
7417
    $allplugins = core_component::get_plugin_list($plugintype);
7418
 
7419
    // Reformat the array and include the files.
7420
    $pluginfunctions = array();
7421
    foreach ($plugins[$plugintype] as $pluginname => $functionname) {
7422
 
7423
        // Check that it has not been removed and the file is still available.
7424
        if (!empty($allplugins[$pluginname])) {
7425
 
7426
            $filepath = $allplugins[$pluginname] . DIRECTORY_SEPARATOR . $file;
7427
            if (file_exists($filepath)) {
7428
                include_once($filepath);
7429
 
7430
                // Now that the file is loaded, we must verify the function still exists.
7431
                if (function_exists($functionname)) {
7432
                    $pluginfunctions[$plugintype . '_' . $pluginname] = $functionname;
7433
                } else {
7434
                    // Invalidate the cache for next run.
7435
                    \cache_helper::invalidate_by_definition('core', 'plugin_functions');
7436
                }
7437
            }
7438
        }
7439
    }
7440
 
7441
    return $pluginfunctions;
7442
}
7443
 
7444
/**
7445
 * Get a list of all the plugins that define a certain API function in a certain file.
7446
 *
7447
 * @param string $function the part of the name of the function after the
7448
 *      frankenstyle prefix. e.g 'hook' if you are looking for functions with
7449
 *      names like report_courselist_hook.
7450
 * @param string $file the name of file within the plugin that defines the
7451
 *      function. Defaults to lib.php.
7452
 * @param bool $include Whether to include the files that contain the functions or not.
7453
 * @param bool $migratedtohook if true this is a deprecated lib.php callback, if hook callback is present then do nothing
7454
 * @return array with [plugintype][plugin] = functionname
7455
 */
7456
function get_plugins_with_function($function, $file = 'lib.php', $include = true, bool $migratedtohook = false) {
7457
    global $CFG;
7458
 
7459
    if (during_initial_install() || isset($CFG->upgraderunning)) {
7460
        // API functions _must not_ be called during an installation or upgrade.
7461
        return [];
7462
    }
7463
 
7464
    $plugincallback = $function;
7465
    $filtermigrated = function($plugincallback, $pluginfunctions): array {
7466
        foreach ($pluginfunctions as $plugintype => $plugins) {
7467
            foreach ($plugins as $plugin => $unusedfunction) {
7468
                $component = $plugintype . '_' . $plugin;
7469
                if ($hooks = di::get(hook\manager::class)->get_hooks_deprecating_plugin_callback($plugincallback)) {
7470
                    if (di::get(hook\manager::class)->is_deprecating_hook_present($component, $plugincallback)) {
7471
                        // Ignore the old callback, it is there only for older Moodle versions.
7472
                        unset($pluginfunctions[$plugintype][$plugin]);
7473
                    } else {
7474
                        $hookmessage = count($hooks) == 1 ? reset($hooks) : 'one of  ' . implode(', ', $hooks);
7475
                        debugging(
7476
                            "Callback $plugincallback in $component component should be migrated to new " .
7477
                                "hook callback for $hookmessage",
7478
                            DEBUG_DEVELOPER
7479
                        );
7480
                    }
7481
                }
7482
            }
7483
        }
7484
        return $pluginfunctions;
7485
    };
7486
 
7487
    $cache = \cache::make('core', 'plugin_functions');
7488
 
7489
    // Including both although I doubt that we will find two functions definitions with the same name.
7490
    // Clean the filename as cache_helper::hash_key only allows a-zA-Z0-9_.
7491
    $pluginfunctions = false;
7492
    if (!empty($CFG->allversionshash)) {
7493
        $key = $CFG->allversionshash . '_' . $function . '_' . clean_param($file, PARAM_ALPHA);
7494
        $pluginfunctions = $cache->get($key);
7495
    }
7496
    $dirty = false;
7497
 
7498
    // Use the plugin manager to check that plugins are currently installed.
7499
    $pluginmanager = \core_plugin_manager::instance();
7500
 
7501
    if ($pluginfunctions !== false) {
7502
 
7503
        // Checking that the files are still available.
7504
        foreach ($pluginfunctions as $plugintype => $plugins) {
7505
 
7506
            $allplugins = \core_component::get_plugin_list($plugintype);
7507
            $installedplugins = $pluginmanager->get_installed_plugins($plugintype);
7508
            foreach ($plugins as $plugin => $function) {
7509
                if (!isset($installedplugins[$plugin])) {
7510
                    // Plugin code is still present on disk but it is not installed.
7511
                    $dirty = true;
7512
                    break 2;
7513
                }
7514
 
7515
                // Cache might be out of sync with the codebase, skip the plugin if it is not available.
7516
                if (empty($allplugins[$plugin])) {
7517
                    $dirty = true;
7518
                    break 2;
7519
                }
7520
 
7521
                $fileexists = file_exists($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file);
7522
                if ($include && $fileexists) {
7523
                    // Include the files if it was requested.
7524
                    include_once($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file);
7525
                } else if (!$fileexists) {
7526
                    // If the file is not available any more it should not be returned.
7527
                    $dirty = true;
7528
                    break 2;
7529
                }
7530
 
7531
                // Check if the function still exists in the file.
7532
                if ($include && !function_exists($function)) {
7533
                    $dirty = true;
7534
                    break 2;
7535
                }
7536
            }
7537
        }
7538
 
7539
        // If the cache is dirty, we should fall through and let it rebuild.
7540
        if (!$dirty) {
7541
            if ($migratedtohook && $file === 'lib.php') {
7542
                $pluginfunctions = $filtermigrated($plugincallback, $pluginfunctions);
7543
            }
7544
            return $pluginfunctions;
7545
        }
7546
    }
7547
 
7548
    $pluginfunctions = array();
7549
 
7550
    // To fill the cached. Also, everything should continue working with cache disabled.
7551
    $plugintypes = \core_component::get_plugin_types();
7552
    foreach ($plugintypes as $plugintype => $unused) {
7553
 
7554
        // We need to include files here.
7555
        $pluginswithfile = \core_component::get_plugin_list_with_file($plugintype, $file, true);
7556
        $installedplugins = $pluginmanager->get_installed_plugins($plugintype);
7557
        foreach ($pluginswithfile as $plugin => $notused) {
7558
 
7559
            if (!isset($installedplugins[$plugin])) {
7560
                continue;
7561
            }
7562
 
7563
            $fullfunction = $plugintype . '_' . $plugin . '_' . $function;
7564
 
7565
            $pluginfunction = false;
7566
            if (function_exists($fullfunction)) {
7567
                // Function exists with standard name. Store, indexed by frankenstyle name of plugin.
7568
                $pluginfunction = $fullfunction;
7569
 
7570
            } else if ($plugintype === 'mod') {
7571
                // For modules, we also allow plugin without full frankenstyle but just starting with the module name.
7572
                $shortfunction = $plugin . '_' . $function;
7573
                if (function_exists($shortfunction)) {
7574
                    $pluginfunction = $shortfunction;
7575
                }
7576
            }
7577
 
7578
            if ($pluginfunction) {
7579
                if (empty($pluginfunctions[$plugintype])) {
7580
                    $pluginfunctions[$plugintype] = array();
7581
                }
7582
                $pluginfunctions[$plugintype][$plugin] = $pluginfunction;
7583
            }
7584
 
7585
        }
7586
    }
7587
    if (!empty($CFG->allversionshash)) {
7588
        $cache->set($key, $pluginfunctions);
7589
    }
7590
 
7591
    if ($migratedtohook && $file === 'lib.php') {
7592
        $pluginfunctions = $filtermigrated($plugincallback, $pluginfunctions);
7593
    }
7594
 
7595
    return $pluginfunctions;
7596
 
7597
}
7598
 
7599
/**
7600
 * Lists plugin-like directories within specified directory
7601
 *
7602
 * This function was originally used for standard Moodle plugins, please use
7603
 * new core_component::get_plugin_list() now.
7604
 *
7605
 * This function is used for general directory listing and backwards compatility.
7606
 *
7607
 * @param string $directory relative directory from root
7608
 * @param string $exclude dir name to exclude from the list (defaults to none)
7609
 * @param string $basedir full path to the base dir where $plugin resides (defaults to $CFG->dirroot)
7610
 * @return array Sorted array of directory names found under the requested parameters
7611
 */
7612
function get_list_of_plugins($directory='mod', $exclude='', $basedir='') {
7613
    global $CFG;
7614
 
7615
    $plugins = array();
7616
 
7617
    if (empty($basedir)) {
7618
        $basedir = $CFG->dirroot .'/'. $directory;
7619
 
7620
    } else {
7621
        $basedir = $basedir .'/'. $directory;
7622
    }
7623
 
7624
    if ($CFG->debugdeveloper and empty($exclude)) {
7625
        // Make sure devs do not use this to list normal plugins,
7626
        // this is intended for general directories that are not plugins!
7627
 
7628
        $subtypes = core_component::get_plugin_types();
7629
        if (in_array($basedir, $subtypes)) {
7630
            debugging('get_list_of_plugins() should not be used to list real plugins, use core_component::get_plugin_list() instead!', DEBUG_DEVELOPER);
7631
        }
7632
        unset($subtypes);
7633
    }
7634
 
7635
    $ignorelist = array_flip(array_filter([
7636
        'CVS',
7637
        '_vti_cnf',
7638
        'amd',
7639
        'classes',
7640
        'simpletest',
7641
        'tests',
7642
        'templates',
7643
        'yui',
7644
        $exclude,
7645
    ]));
7646
 
7647
    if (file_exists($basedir) && filetype($basedir) == 'dir') {
7648
        if (!$dirhandle = opendir($basedir)) {
7649
            debugging("Directory permission error for plugin ({$directory}). Directory exists but cannot be read.", DEBUG_DEVELOPER);
7650
            return array();
7651
        }
7652
        while (false !== ($dir = readdir($dirhandle))) {
7653
            if (strpos($dir, '.') === 0) {
7654
                // Ignore directories starting with .
7655
                // These are treated as hidden directories.
7656
                continue;
7657
            }
7658
            if (array_key_exists($dir, $ignorelist)) {
7659
                // This directory features on the ignore list.
7660
                continue;
7661
            }
7662
            if (filetype($basedir .'/'. $dir) != 'dir') {
7663
                continue;
7664
            }
7665
            $plugins[] = $dir;
7666
        }
7667
        closedir($dirhandle);
7668
    }
7669
    if ($plugins) {
7670
        asort($plugins);
7671
    }
7672
    return $plugins;
7673
}
7674
 
7675
/**
7676
 * Invoke plugin's callback functions
7677
 *
7678
 * @param string $type plugin type e.g. 'mod'
7679
 * @param string $name plugin name
7680
 * @param string $feature feature name
7681
 * @param string $action feature's action
7682
 * @param array $params parameters of callback function, should be an array
7683
 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
7684
 * @param bool $migratedtohook if true this is a deprecated callback, if hook callback is present then do nothing
7685
 * @return mixed
7686
 *
7687
 * @todo Decide about to deprecate and drop plugin_callback() - MDL-30743
7688
 */
7689
function plugin_callback($type, $name, $feature, $action, $params = null, $default = null, bool $migratedtohook = false) {
7690
    return component_callback($type . '_' . $name, $feature . '_' . $action, (array) $params, $default, $migratedtohook);
7691
}
7692
 
7693
/**
7694
 * Invoke component's callback functions
7695
 *
7696
 * @param string $component frankenstyle component name, e.g. 'mod_quiz'
7697
 * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
7698
 * @param array $params parameters of callback function
7699
 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
7700
 * @param bool $migratedtohook if true this is a deprecated callback, if hook callback is present then do nothing
7701
 * @return mixed
7702
 */
7703
function component_callback($component, $function, array $params = array(), $default = null, bool $migratedtohook = false) {
7704
    $functionname = component_callback_exists($component, $function);
7705
 
7706
    if ($functionname) {
7707
        if ($migratedtohook) {
7708
            $hookmanager = di::get(hook\manager::class);
7709
            if ($hooks = $hookmanager->get_hooks_deprecating_plugin_callback($function)) {
7710
                if ($hookmanager->is_deprecating_hook_present($component, $function)) {
7711
                    // Do not call the old lib.php callback,
7712
                    // it is there for compatibility with older Moodle versions only.
7713
                    return null;
7714
                } else {
7715
                    $hookmessage = count($hooks) == 1 ? reset($hooks) : 'one of  ' . implode(', ', $hooks);
7716
                    debugging(
7717
                        "Callback $function in $component component should be migrated to new hook callback for $hookmessage",
7718
                        DEBUG_DEVELOPER);
7719
                }
7720
            }
7721
        }
7722
 
7723
        // Function exists, so just return function result.
7724
        $ret = call_user_func_array($functionname, $params);
7725
        if (is_null($ret)) {
7726
            return $default;
7727
        } else {
7728
            return $ret;
7729
        }
7730
    }
7731
    return $default;
7732
}
7733
 
7734
/**
7735
 * Determine if a component callback exists and return the function name to call. Note that this
7736
 * function will include the required library files so that the functioname returned can be
7737
 * called directly.
7738
 *
7739
 * @param string $component frankenstyle component name, e.g. 'mod_quiz'
7740
 * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
7741
 * @return mixed Complete function name to call if the callback exists or false if it doesn't.
7742
 * @throws coding_exception if invalid component specfied
7743
 */
7744
function component_callback_exists($component, $function) {
7745
    global $CFG; // This is needed for the inclusions.
7746
 
7747
    $cleancomponent = clean_param($component, PARAM_COMPONENT);
7748
    if (empty($cleancomponent)) {
7749
        throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
7750
    }
7751
    $component = $cleancomponent;
7752
 
7753
    list($type, $name) = core_component::normalize_component($component);
7754
    $component = $type . '_' . $name;
7755
 
7756
    $oldfunction = $name.'_'.$function;
7757
    $function = $component.'_'.$function;
7758
 
7759
    $dir = core_component::get_component_directory($component);
7760
    if (empty($dir)) {
7761
        throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
7762
    }
7763
 
7764
    // Load library and look for function.
7765
    if (file_exists($dir.'/lib.php')) {
7766
        require_once($dir.'/lib.php');
7767
    }
7768
 
7769
    if (!function_exists($function) and function_exists($oldfunction)) {
7770
        if ($type !== 'mod' and $type !== 'core') {
7771
            debugging("Please use new function name $function instead of legacy $oldfunction", DEBUG_DEVELOPER);
7772
        }
7773
        $function = $oldfunction;
7774
    }
7775
 
7776
    if (function_exists($function)) {
7777
        return $function;
7778
    }
7779
    return false;
7780
}
7781
 
7782
/**
7783
 * Call the specified callback method on the provided class.
7784
 *
7785
 * If the callback returns null, then the default value is returned instead.
7786
 * If the class does not exist, then the default value is returned.
7787
 *
7788
 * @param   string      $classname The name of the class to call upon.
7789
 * @param   string      $methodname The name of the staticically defined method on the class.
7790
 * @param   array       $params The arguments to pass into the method.
7791
 * @param   mixed       $default The default value.
7792
 * @param   bool        $migratedtohook True if the callback has been migrated to a hook.
7793
 * @return  mixed       The return value.
7794
 */
7795
function component_class_callback($classname, $methodname, array $params, $default = null, bool $migratedtohook = false) {
7796
    if (!class_exists($classname)) {
7797
        return $default;
7798
    }
7799
 
7800
    if (!method_exists($classname, $methodname)) {
7801
        return $default;
7802
    }
7803
 
7804
    $fullfunction = $classname . '::' . $methodname;
7805
 
7806
    if ($migratedtohook) {
7807
        $functionparts = explode('\\', trim($fullfunction, '\\'));
7808
        $component = $functionparts[0];
7809
        $callback = end($functionparts);
7810
        $hookmanager = di::get(hook\manager::class);
7811
        if ($hooks = $hookmanager->get_hooks_deprecating_plugin_callback($callback)) {
7812
            if ($hookmanager->is_deprecating_hook_present($component, $callback)) {
7813
                // Do not call the old class callback,
7814
                // it is there for compatibility with older Moodle versions only.
7815
                return null;
7816
            } else {
7817
                $hookmessage = count($hooks) == 1 ? reset($hooks) : 'one of  ' . implode(', ', $hooks);
7818
                debugging("Callback $callback in $component component should be migrated to new hook callback for $hookmessage",
7819
                        DEBUG_DEVELOPER);
7820
            }
7821
        }
7822
    }
7823
 
7824
    $result = call_user_func_array($fullfunction, $params);
7825
 
7826
    if (null === $result) {
7827
        return $default;
7828
    } else {
7829
        return $result;
7830
    }
7831
}
7832
 
7833
/**
7834
 * Checks whether a plugin supports a specified feature.
7835
 *
7836
 * @param string $type Plugin type e.g. 'mod'
7837
 * @param string $name Plugin name e.g. 'forum'
7838
 * @param string $feature Feature code (FEATURE_xx constant)
7839
 * @param mixed $default default value if feature support unknown
7840
 * @return mixed Feature result (false if not supported, null if feature is unknown,
7841
 *         otherwise usually true but may have other feature-specific value such as array)
7842
 * @throws coding_exception
7843
 */
7844
function plugin_supports($type, $name, $feature, $default = null) {
7845
    global $CFG;
7846
 
7847
    if ($type === 'mod' and $name === 'NEWMODULE') {
7848
        // Somebody forgot to rename the module template.
7849
        return false;
7850
    }
7851
 
7852
    $component = clean_param($type . '_' . $name, PARAM_COMPONENT);
7853
    if (empty($component)) {
7854
        throw new coding_exception('Invalid component used in plugin_supports():' . $type . '_' . $name);
7855
    }
7856
 
7857
    $function = null;
7858
 
7859
    if ($type === 'mod') {
7860
        // We need this special case because we support subplugins in modules,
7861
        // otherwise it would end up in infinite loop.
7862
        if (file_exists("$CFG->dirroot/mod/$name/lib.php")) {
7863
            include_once("$CFG->dirroot/mod/$name/lib.php");
7864
            $function = $component.'_supports';
7865
            if (!function_exists($function)) {
7866
                // Legacy non-frankenstyle function name.
7867
                $function = $name.'_supports';
7868
            }
7869
        }
7870
 
7871
    } else {
7872
        if (!$path = core_component::get_plugin_directory($type, $name)) {
7873
            // Non existent plugin type.
7874
            return false;
7875
        }
7876
        if (file_exists("$path/lib.php")) {
7877
            include_once("$path/lib.php");
7878
            $function = $component.'_supports';
7879
        }
7880
    }
7881
 
7882
    if ($function and function_exists($function)) {
7883
        $supports = $function($feature);
7884
        if (is_null($supports)) {
7885
            // Plugin does not know - use default.
7886
            return $default;
7887
        } else {
7888
            return $supports;
7889
        }
7890
    }
7891
 
7892
    // Plugin does not care, so use default.
7893
    return $default;
7894
}
7895
 
7896
/**
7897
 * Returns true if the current version of PHP is greater that the specified one.
7898
 *
7899
 * @todo Check PHP version being required here is it too low?
7900
 *
7901
 * @param string $version The version of php being tested.
7902
 * @return bool
7903
 */
7904
function check_php_version($version='5.2.4') {
7905
    return (version_compare(phpversion(), $version) >= 0);
7906
}
7907
 
7908
/**
7909
 * Determine if moodle installation requires update.
7910
 *
7911
 * Checks version numbers of main code and all plugins to see
7912
 * if there are any mismatches.
7913
 *
7914
 * @param bool $checkupgradeflag check the outagelessupgrade flag to see if an upgrade is running.
7915
 * @return bool
7916
 */
7917
function moodle_needs_upgrading($checkupgradeflag = true) {
7918
    global $CFG, $DB;
7919
 
7920
    // Say no if there is already an upgrade running.
7921
    if ($checkupgradeflag) {
7922
        $lock = $DB->get_field('config', 'value', ['name' => 'outagelessupgrade']);
7923
        $currentprocessrunningupgrade = (defined('CLI_UPGRADE_RUNNING') && CLI_UPGRADE_RUNNING);
7924
        // If we ARE locked, but this PHP process is NOT the process running the upgrade,
7925
        // We should always return false.
7926
        // This means the upgrade is running from CLI somewhere, or about to.
7927
        if (!empty($lock) && !$currentprocessrunningupgrade) {
7928
            return false;
7929
        }
7930
    }
7931
 
7932
    if (empty($CFG->version)) {
7933
        return true;
7934
    }
7935
 
7936
    // There is no need to purge plugininfo caches here because
7937
    // these caches are not used during upgrade and they are purged after
7938
    // every upgrade.
7939
 
7940
    if (empty($CFG->allversionshash)) {
7941
        return true;
7942
    }
7943
 
7944
    $hash = core_component::get_all_versions_hash();
7945
 
7946
    return ($hash !== $CFG->allversionshash);
7947
}
7948
 
7949
/**
7950
 * Returns the major version of this site
7951
 *
7952
 * Moodle version numbers consist of three numbers separated by a dot, for
7953
 * example 1.9.11 or 2.0.2. The first two numbers, like 1.9 or 2.0, represent so
7954
 * called major version. This function extracts the major version from either
7955
 * $CFG->release (default) or eventually from the $release variable defined in
7956
 * the main version.php.
7957
 *
7958
 * @param bool $fromdisk should the version if source code files be used
7959
 * @return string|false the major version like '2.3', false if could not be determined
7960
 */
7961
function moodle_major_version($fromdisk = false) {
7962
    global $CFG;
7963
 
7964
    if ($fromdisk) {
7965
        $release = null;
7966
        require($CFG->dirroot.'/version.php');
7967
        if (empty($release)) {
7968
            return false;
7969
        }
7970
 
7971
    } else {
7972
        if (empty($CFG->release)) {
7973
            return false;
7974
        }
7975
        $release = $CFG->release;
7976
    }
7977
 
7978
    if (preg_match('/^[0-9]+\.[0-9]+/', $release, $matches)) {
7979
        return $matches[0];
7980
    } else {
7981
        return false;
7982
    }
7983
}
7984
 
7985
// MISCELLANEOUS.
7986
 
7987
/**
7988
 * Gets the system locale
7989
 *
7990
 * @return string Retuns the current locale.
7991
 */
7992
function moodle_getlocale() {
7993
    global $CFG;
7994
 
7995
    // Fetch the correct locale based on ostype.
7996
    if ($CFG->ostype == 'WINDOWS') {
7997
        $stringtofetch = 'localewin';
7998
    } else {
7999
        $stringtofetch = 'locale';
8000
    }
8001
 
8002
    if (!empty($CFG->locale)) { // Override locale for all language packs.
8003
        return $CFG->locale;
8004
    }
8005
 
8006
    return get_string($stringtofetch, 'langconfig');
8007
}
8008
 
8009
/**
8010
 * Sets the system locale
8011
 *
8012
 * @category string
8013
 * @param string $locale Can be used to force a locale
8014
 */
8015
function moodle_setlocale($locale='') {
8016
    global $CFG;
8017
 
8018
    static $currentlocale = ''; // Last locale caching.
8019
 
8020
    $oldlocale = $currentlocale;
8021
 
8022
    // The priority is the same as in get_string() - parameter, config, course, session, user, global language.
8023
    if (!empty($locale)) {
8024
        $currentlocale = $locale;
8025
    } else {
8026
        $currentlocale = moodle_getlocale();
8027
    }
8028
 
8029
    // Do nothing if locale already set up.
8030
    if ($oldlocale == $currentlocale) {
8031
        return;
8032
    }
8033
 
8034
    // Due to some strange BUG we cannot set the LC_TIME directly, so we fetch current values,
8035
    // set LC_ALL and then set values again. Just wondering why we cannot set LC_ALL only??? - stronk7
8036
    // Some day, numeric, monetary and other categories should be set too, I think. :-/.
8037
 
8038
    // Get current values.
8039
    $monetary= setlocale (LC_MONETARY, 0);
8040
    $numeric = setlocale (LC_NUMERIC, 0);
8041
    $ctype   = setlocale (LC_CTYPE, 0);
8042
    if ($CFG->ostype != 'WINDOWS') {
8043
        $messages= setlocale (LC_MESSAGES, 0);
8044
    }
8045
    // Set locale to all.
8046
    $result = setlocale (LC_ALL, $currentlocale);
8047
    // If setting of locale fails try the other utf8 or utf-8 variant,
8048
    // some operating systems support both (Debian), others just one (OSX).
8049
    if ($result === false) {
8050
        if (stripos($currentlocale, '.UTF-8') !== false) {
8051
            $newlocale = str_ireplace('.UTF-8', '.UTF8', $currentlocale);
8052
            setlocale (LC_ALL, $newlocale);
8053
        } else if (stripos($currentlocale, '.UTF8') !== false) {
8054
            $newlocale = str_ireplace('.UTF8', '.UTF-8', $currentlocale);
8055
            setlocale (LC_ALL, $newlocale);
8056
        }
8057
    }
8058
    // Set old values.
8059
    setlocale (LC_MONETARY, $monetary);
8060
    setlocale (LC_NUMERIC, $numeric);
8061
    if ($CFG->ostype != 'WINDOWS') {
8062
        setlocale (LC_MESSAGES, $messages);
8063
    }
8064
    if ($currentlocale == 'tr_TR' or $currentlocale == 'tr_TR.UTF-8') {
8065
        // To workaround a well-known PHP problem with Turkish letter Ii.
8066
        setlocale (LC_CTYPE, $ctype);
8067
    }
8068
}
8069
 
8070
/**
8071
 * Count words in a string.
8072
 *
8073
 * Words are defined as things between whitespace.
8074
 *
8075
 * @category string
8076
 * @param string $string The text to be searched for words. May be HTML.
8077
 * @param int|null $format
8078
 * @return int The count of words in the specified string
8079
 */
8080
function count_words($string, $format = null) {
8081
    // Before stripping tags, add a space after the close tag of anything that is not obviously inline.
8082
    // Also, br is a special case because it definitely delimits a word, but has no close tag.
8083
    $string = preg_replace('~
8084
            (                                   # Capture the tag we match.
8085
                </                              # Start of close tag.
8086
                (?!                             # Do not match any of these specific close tag names.
8087
                    a> | b> | del> | em> | i> |
8088
                    ins> | s> | small> | span> |
8089
                    strong> | sub> | sup> | u>
8090
                )
8091
                \w+                             # But, apart from those execptions, match any tag name.
8092
                >                               # End of close tag.
8093
            |
8094
                <br> | <br\s*/>                 # Special cases that are not close tags.
8095
            )
8096
            ~x', '$1 ', $string); // Add a space after the close tag.
8097
    if ($format !== null && $format != FORMAT_PLAIN) {
8098
        // Match the usual text cleaning before display.
8099
        // Ideally we should apply multilang filter only here, other filters might add extra text.
8100
        $string = format_text($string, $format, ['filter' => false, 'noclean' => false, 'para' => false]);
8101
    }
8102
    // Now remove HTML tags.
8103
    $string = strip_tags($string);
8104
    // Decode HTML entities.
8105
    $string = html_entity_decode($string, ENT_COMPAT);
8106
 
8107
    // Now, the word count is the number of blocks of characters separated
8108
    // by any sort of space. That seems to be the definition used by all other systems.
8109
    // To be precise about what is considered to separate words:
8110
    // * Anything that Unicode considers a 'Separator'
8111
    // * Anything that Unicode considers a 'Control character'
8112
    // * An em- or en- dash.
8113
    return count(preg_split('~[\p{Z}\p{Cc}—–]+~u', $string, -1, PREG_SPLIT_NO_EMPTY));
8114
}
8115
 
8116
/**
8117
 * Count letters in a string.
8118
 *
8119
 * Letters are defined as chars not in tags and different from whitespace.
8120
 *
8121
 * @category string
8122
 * @param string $string The text to be searched for letters. May be HTML.
8123
 * @param int|null $format
8124
 * @return int The count of letters in the specified text.
8125
 */
8126
function count_letters($string, $format = null) {
8127
    if ($format !== null && $format != FORMAT_PLAIN) {
8128
        // Match the usual text cleaning before display.
8129
        // Ideally we should apply multilang filter only here, other filters might add extra text.
8130
        $string = format_text($string, $format, ['filter' => false, 'noclean' => false, 'para' => false]);
8131
    }
8132
    $string = strip_tags($string); // Tags are out now.
8133
    $string = html_entity_decode($string, ENT_COMPAT);
8134
    $string = preg_replace('/[[:space:]]*/', '', $string); // Whitespace are out now.
8135
 
8136
    return core_text::strlen($string);
8137
}
8138
 
8139
/**
8140
 * Generate and return a random string of the specified length.
8141
 *
8142
 * @param int $length The length of the string to be created.
8143
 * @return string
8144
 */
8145
function random_string($length=15) {
8146
    $randombytes = random_bytes($length);
8147
    $pool  = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
8148
    $pool .= 'abcdefghijklmnopqrstuvwxyz';
8149
    $pool .= '0123456789';
8150
    $poollen = strlen($pool);
8151
    $string = '';
8152
    for ($i = 0; $i < $length; $i++) {
8153
        $rand = ord($randombytes[$i]);
8154
        $string .= substr($pool, ($rand%($poollen)), 1);
8155
    }
8156
    return $string;
8157
}
8158
 
8159
/**
8160
 * Generate a complex random string (useful for md5 salts)
8161
 *
8162
 * This function is based on the above {@link random_string()} however it uses a
8163
 * larger pool of characters and generates a string between 24 and 32 characters
8164
 *
8165
 * @param int $length Optional if set generates a string to exactly this length
8166
 * @return string
8167
 */
8168
function complex_random_string($length=null) {
8169
    $pool  = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
8170
    $pool .= '`~!@#%^&*()_+-=[];,./<>?:{} ';
8171
    $poollen = strlen($pool);
8172
    if ($length===null) {
8173
        $length = floor(rand(24, 32));
8174
    }
8175
    $randombytes = random_bytes($length);
8176
    $string = '';
8177
    for ($i = 0; $i < $length; $i++) {
8178
        $rand = ord($randombytes[$i]);
8179
        $string .= $pool[($rand%$poollen)];
8180
    }
8181
    return $string;
8182
}
8183
 
8184
/**
8185
 * Given some text (which may contain HTML) and an ideal length,
8186
 * this function truncates the text neatly on a word boundary if possible
8187
 *
8188
 * @category string
8189
 * @param string $text text to be shortened
8190
 * @param int $ideal ideal string length
8191
 * @param boolean $exact if false, $text will not be cut mid-word
8192
 * @param string $ending The string to append if the passed string is truncated
8193
 * @return string $truncate shortened string
8194
 */
8195
function shorten_text($text, $ideal=30, $exact = false, $ending='...') {
8196
    // If the plain text is shorter than the maximum length, return the whole text.
8197
    if (core_text::strlen(preg_replace('/<.*?>/', '', $text)) <= $ideal) {
8198
        return $text;
8199
    }
8200
 
8201
    // Splits on HTML tags. Each open/close/empty tag will be the first thing
8202
    // and only tag in its 'line'.
8203
    preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER);
8204
 
8205
    $totallength = core_text::strlen($ending);
8206
    $truncate = '';
8207
 
8208
    // This array stores information about open and close tags and their position
8209
    // in the truncated string. Each item in the array is an object with fields
8210
    // ->open (true if open), ->tag (tag name in lower case), and ->pos
8211
    // (byte position in truncated text).
8212
    $tagdetails = array();
8213
 
8214
    foreach ($lines as $linematchings) {
8215
        // If there is any html-tag in this line, handle it and add it (uncounted) to the output.
8216
        if (!empty($linematchings[1])) {
8217
            // If it's an "empty element" with or without xhtml-conform closing slash (f.e. <br/>).
8218
            if (!preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $linematchings[1])) {
8219
                if (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $linematchings[1], $tagmatchings)) {
8220
                    // Record closing tag.
8221
                    $tagdetails[] = (object) array(
8222
                            'open' => false,
8223
                            'tag'  => core_text::strtolower($tagmatchings[1]),
8224
                            'pos'  => core_text::strlen($truncate),
8225
                        );
8226
 
8227
                } else if (preg_match('/^<\s*([^\s>!]+).*?>$/s', $linematchings[1], $tagmatchings)) {
8228
                    // Record opening tag.
8229
                    $tagdetails[] = (object) array(
8230
                            'open' => true,
8231
                            'tag'  => core_text::strtolower($tagmatchings[1]),
8232
                            'pos'  => core_text::strlen($truncate),
8233
                        );
8234
                } else if (preg_match('/^<!--\[if\s.*?\]>$/s', $linematchings[1], $tagmatchings)) {
8235
                    $tagdetails[] = (object) array(
8236
                            'open' => true,
8237
                            'tag'  => core_text::strtolower('if'),
8238
                            'pos'  => core_text::strlen($truncate),
8239
                    );
8240
                } else if (preg_match('/^<!--<!\[endif\]-->$/s', $linematchings[1], $tagmatchings)) {
8241
                    $tagdetails[] = (object) array(
8242
                            'open' => false,
8243
                            'tag'  => core_text::strtolower('if'),
8244
                            'pos'  => core_text::strlen($truncate),
8245
                    );
8246
                }
8247
            }
8248
            // Add html-tag to $truncate'd text.
8249
            $truncate .= $linematchings[1];
8250
        }
8251
 
8252
        // Calculate the length of the plain text part of the line; handle entities as one character.
8253
        $contentlength = core_text::strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', ' ', $linematchings[2]));
8254
        if ($totallength + $contentlength > $ideal) {
8255
            // The number of characters which are left.
8256
            $left = $ideal - $totallength;
8257
            $entitieslength = 0;
8258
            // Search for html entities.
8259
            if (preg_match_all('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', $linematchings[2], $entities, PREG_OFFSET_CAPTURE)) {
8260
                // Calculate the real length of all entities in the legal range.
8261
                foreach ($entities[0] as $entity) {
8262
                    if ($entity[1]+1-$entitieslength <= $left) {
8263
                        $left--;
8264
                        $entitieslength += core_text::strlen($entity[0]);
8265
                    } else {
8266
                        // No more characters left.
8267
                        break;
8268
                    }
8269
                }
8270
            }
8271
            $breakpos = $left + $entitieslength;
8272
 
8273
            // If the words shouldn't be cut in the middle...
8274
            if (!$exact) {
8275
                // Search the last occurence of a space.
8276
                for (; $breakpos > 0; $breakpos--) {
8277
                    if ($char = core_text::substr($linematchings[2], $breakpos, 1)) {
8278
                        if ($char === '.' or $char === ' ') {
8279
                            $breakpos += 1;
8280
                            break;
8281
                        } else if (strlen($char) > 2) {
8282
                            // Chinese/Japanese/Korean text can be truncated at any UTF-8 character boundary.
8283
                            $breakpos += 1;
8284
                            break;
8285
                        }
8286
                    }
8287
                }
8288
            }
8289
            if ($breakpos == 0) {
8290
                // This deals with the test_shorten_text_no_spaces case.
8291
                $breakpos = $left + $entitieslength;
8292
            } else if ($breakpos > $left + $entitieslength) {
8293
                // This deals with the previous for loop breaking on the first char.
8294
                $breakpos = $left + $entitieslength;
8295
            }
8296
 
8297
            $truncate .= core_text::substr($linematchings[2], 0, $breakpos);
8298
            // Maximum length is reached, so get off the loop.
8299
            break;
8300
        } else {
8301
            $truncate .= $linematchings[2];
8302
            $totallength += $contentlength;
8303
        }
8304
 
8305
        // If the maximum length is reached, get off the loop.
8306
        if ($totallength >= $ideal) {
8307
            break;
8308
        }
8309
    }
8310
 
8311
    // Add the defined ending to the text.
8312
    $truncate .= $ending;
8313
 
8314
    // Now calculate the list of open html tags based on the truncate position.
8315
    $opentags = array();
8316
    foreach ($tagdetails as $taginfo) {
8317
        if ($taginfo->open) {
8318
            // Add tag to the beginning of $opentags list.
8319
            array_unshift($opentags, $taginfo->tag);
8320
        } else {
8321
            // Can have multiple exact same open tags, close the last one.
8322
            $pos = array_search($taginfo->tag, array_reverse($opentags, true));
8323
            if ($pos !== false) {
8324
                unset($opentags[$pos]);
8325
            }
8326
        }
8327
    }
8328
 
8329
    // Close all unclosed html-tags.
8330
    foreach ($opentags as $tag) {
8331
        if ($tag === 'if') {
8332
            $truncate .= '<!--<![endif]-->';
8333
        } else {
8334
            $truncate .= '</' . $tag . '>';
8335
        }
8336
    }
8337
 
8338
    return $truncate;
8339
}
8340
 
8341
/**
8342
 * Shortens a given filename by removing characters positioned after the ideal string length.
8343
 * When the filename is too long, the file cannot be created on the filesystem due to exceeding max byte size.
8344
 * Limiting the filename to a certain size (considering multibyte characters) will prevent this.
8345
 *
8346
 * @param string $filename file name
8347
 * @param int $length ideal string length
8348
 * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness.
8349
 * @return string $shortened shortened file name
8350
 */
8351
function shorten_filename($filename, $length = MAX_FILENAME_SIZE, $includehash = false) {
8352
    $shortened = $filename;
8353
    // Extract a part of the filename if it's char size exceeds the ideal string length.
8354
    if (core_text::strlen($filename) > $length) {
8355
        // Exclude extension if present in filename.
8356
        $mimetypes = get_mimetypes_array();
8357
        $extension = pathinfo($filename, PATHINFO_EXTENSION);
8358
        if ($extension && !empty($mimetypes[$extension])) {
8359
            $basename = pathinfo($filename, PATHINFO_FILENAME);
8360
            $hash = empty($includehash) ? '' : ' - ' . substr(sha1($basename), 0, 10);
8361
            $shortened = core_text::substr($basename, 0, $length - strlen($hash)) . $hash;
8362
            $shortened .= '.' . $extension;
8363
        } else {
8364
            $hash = empty($includehash) ? '' : ' - ' . substr(sha1($filename), 0, 10);
8365
            $shortened = core_text::substr($filename, 0, $length - strlen($hash)) . $hash;
8366
        }
8367
    }
8368
    return $shortened;
8369
}
8370
 
8371
/**
8372
 * Shortens a given array of filenames by removing characters positioned after the ideal string length.
8373
 *
8374
 * @param array $path The paths to reduce the length.
8375
 * @param int $length Ideal string length
8376
 * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness.
8377
 * @return array $result Shortened paths in array.
8378
 */
8379
function shorten_filenames(array $path, $length = MAX_FILENAME_SIZE, $includehash = false) {
8380
    $result = null;
8381
 
8382
    $result = array_reduce($path, function($carry, $singlepath) use ($length, $includehash) {
8383
        $carry[] = shorten_filename($singlepath, $length, $includehash);
8384
        return $carry;
8385
    }, []);
8386
 
8387
    return $result;
8388
}
8389
 
8390
/**
8391
 * Given dates in seconds, how many weeks is the date from startdate
8392
 * The first week is 1, the second 2 etc ...
8393
 *
8394
 * @param int $startdate Timestamp for the start date
8395
 * @param int $thedate Timestamp for the end date
8396
 * @return string
8397
 */
8398
function getweek ($startdate, $thedate) {
8399
    if ($thedate < $startdate) {
8400
        return 0;
8401
    }
8402
 
8403
    return floor(($thedate - $startdate) / WEEKSECS) + 1;
8404
}
8405
 
8406
/**
8407
 * Returns a randomly generated password of length $maxlen.  inspired by
8408
 *
8409
 * {@link http://www.phpbuilder.com/columns/jesus19990502.php3} and
8410
 * {@link http://es2.php.net/manual/en/function.str-shuffle.php#73254}
8411
 *
8412
 * @param int $maxlen  The maximum size of the password being generated.
8413
 * @return string
8414
 */
8415
function generate_password($maxlen=10) {
8416
    global $CFG;
8417
 
8418
    if (empty($CFG->passwordpolicy)) {
8419
        $fillers = PASSWORD_DIGITS;
8420
        $wordlist = file($CFG->wordlist);
8421
        $word1 = trim($wordlist[rand(0, count($wordlist) - 1)]);
8422
        $word2 = trim($wordlist[rand(0, count($wordlist) - 1)]);
8423
        $filler1 = $fillers[rand(0, strlen($fillers) - 1)];
8424
        $password = $word1 . $filler1 . $word2;
8425
    } else {
8426
        $minlen = !empty($CFG->minpasswordlength) ? $CFG->minpasswordlength : 0;
8427
        $digits = $CFG->minpassworddigits;
8428
        $lower = $CFG->minpasswordlower;
8429
        $upper = $CFG->minpasswordupper;
8430
        $nonalphanum = $CFG->minpasswordnonalphanum;
8431
        $total = $lower + $upper + $digits + $nonalphanum;
8432
        // Var minlength should be the greater one of the two ( $minlen and $total ).
8433
        $minlen = $minlen < $total ? $total : $minlen;
8434
        // Var maxlen can never be smaller than minlen.
8435
        $maxlen = $minlen > $maxlen ? $minlen : $maxlen;
8436
        $additional = $maxlen - $total;
8437
 
8438
        // Make sure we have enough characters to fulfill
8439
        // complexity requirements.
8440
        $passworddigits = PASSWORD_DIGITS;
8441
        while ($digits > strlen($passworddigits)) {
8442
            $passworddigits .= PASSWORD_DIGITS;
8443
        }
8444
        $passwordlower = PASSWORD_LOWER;
8445
        while ($lower > strlen($passwordlower)) {
8446
            $passwordlower .= PASSWORD_LOWER;
8447
        }
8448
        $passwordupper = PASSWORD_UPPER;
8449
        while ($upper > strlen($passwordupper)) {
8450
            $passwordupper .= PASSWORD_UPPER;
8451
        }
8452
        $passwordnonalphanum = PASSWORD_NONALPHANUM;
8453
        while ($nonalphanum > strlen($passwordnonalphanum)) {
8454
            $passwordnonalphanum .= PASSWORD_NONALPHANUM;
8455
        }
8456
 
8457
        // Now mix and shuffle it all.
8458
        $password = str_shuffle (substr(str_shuffle ($passwordlower), 0, $lower) .
8459
                                 substr(str_shuffle ($passwordupper), 0, $upper) .
8460
                                 substr(str_shuffle ($passworddigits), 0, $digits) .
8461
                                 substr(str_shuffle ($passwordnonalphanum), 0 , $nonalphanum) .
8462
                                 substr(str_shuffle ($passwordlower .
8463
                                                     $passwordupper .
8464
                                                     $passworddigits .
8465
                                                     $passwordnonalphanum), 0 , $additional));
8466
    }
8467
 
8468
    return substr ($password, 0, $maxlen);
8469
}
8470
 
8471
/**
8472
 * Given a float, prints it nicely.
8473
 * Localized floats must not be used in calculations!
8474
 *
8475
 * The stripzeros feature is intended for making numbers look nicer in small
8476
 * areas where it is not necessary to indicate the degree of accuracy by showing
8477
 * ending zeros. If you turn it on with $decimalpoints set to 3, for example,
8478
 * then it will display '5.4' instead of '5.400' or '5' instead of '5.000'.
8479
 *
8480
 * @param float $float The float to print
8481
 * @param int $decimalpoints The number of decimal places to print. -1 is a special value for auto detect (full precision).
8482
 * @param bool $localized use localized decimal separator
8483
 * @param bool $stripzeros If true, removes final zeros after decimal point. It will be ignored and the trailing zeros after
8484
 *                         the decimal point are always striped if $decimalpoints is -1.
8485
 * @return string locale float
8486
 */
8487
function format_float($float, $decimalpoints=1, $localized=true, $stripzeros=false) {
8488
    if (is_null($float)) {
8489
        return '';
8490
    }
8491
    if ($localized) {
8492
        $separator = get_string('decsep', 'langconfig');
8493
    } else {
8494
        $separator = '.';
8495
    }
8496
    if ($decimalpoints == -1) {
8497
        // The following counts the number of decimals.
8498
        // It is safe as both floatval() and round() functions have same behaviour when non-numeric values are provided.
8499
        $floatval = floatval($float);
8500
        for ($decimalpoints = 0; $floatval != round($float, $decimalpoints); $decimalpoints++);
8501
    }
8502
 
8503
    $result = number_format($float, $decimalpoints, $separator, '');
8504
    if ($stripzeros && $decimalpoints > 0) {
8505
        // Remove zeros and final dot if not needed.
8506
        // However, only do this if there is a decimal point!
8507
        $result = preg_replace('~(' . preg_quote($separator, '~') . ')?0+$~', '', $result);
8508
    }
8509
    return $result;
8510
}
8511
 
8512
/**
8513
 * Converts locale specific floating point/comma number back to standard PHP float value
8514
 * Do NOT try to do any math operations before this conversion on any user submitted floats!
8515
 *
8516
 * @param string $localefloat locale aware float representation
8517
 * @param bool $strict If true, then check the input and return false if it is not a valid number.
8518
 * @return mixed float|bool - false or the parsed float.
8519
 */
8520
function unformat_float($localefloat, $strict = false) {
8521
    $localefloat = trim((string)$localefloat);
8522
 
8523
    if ($localefloat == '') {
8524
        return null;
8525
    }
8526
 
8527
    $localefloat = str_replace(' ', '', $localefloat); // No spaces - those might be used as thousand separators.
8528
    $localefloat = str_replace(get_string('decsep', 'langconfig'), '.', $localefloat);
8529
 
8530
    if ($strict && !is_numeric($localefloat)) {
8531
        return false;
8532
    }
8533
 
8534
    return (float)$localefloat;
8535
}
8536
 
8537
/**
8538
 * Given a simple array, this shuffles it up just like shuffle()
8539
 * Unlike PHP's shuffle() this function works on any machine.
8540
 *
8541
 * @param array $array The array to be rearranged
8542
 * @return array
8543
 */
8544
function swapshuffle($array) {
8545
 
8546
    $last = count($array) - 1;
8547
    for ($i = 0; $i <= $last; $i++) {
8548
        $from = rand(0, $last);
8549
        $curr = $array[$i];
8550
        $array[$i] = $array[$from];
8551
        $array[$from] = $curr;
8552
    }
8553
    return $array;
8554
}
8555
 
8556
/**
8557
 * Like {@link swapshuffle()}, but works on associative arrays
8558
 *
8559
 * @param array $array The associative array to be rearranged
8560
 * @return array
8561
 */
8562
function swapshuffle_assoc($array) {
8563
 
8564
    $newarray = array();
8565
    $newkeys = swapshuffle(array_keys($array));
8566
 
8567
    foreach ($newkeys as $newkey) {
8568
        $newarray[$newkey] = $array[$newkey];
8569
    }
8570
    return $newarray;
8571
}
8572
 
8573
/**
8574
 * Given an arbitrary array, and a number of draws,
8575
 * this function returns an array with that amount
8576
 * of items.  The indexes are retained.
8577
 *
8578
 * @todo Finish documenting this function
8579
 *
8580
 * @param array $array
8581
 * @param int $draws
8582
 * @return array
8583
 */
8584
function draw_rand_array($array, $draws) {
8585
 
8586
    $return = array();
8587
 
8588
    $last = count($array);
8589
 
8590
    if ($draws > $last) {
8591
        $draws = $last;
8592
    }
8593
 
8594
    while ($draws > 0) {
8595
        $last--;
8596
 
8597
        $keys = array_keys($array);
8598
        $rand = rand(0, $last);
8599
 
8600
        $return[$keys[$rand]] = $array[$keys[$rand]];
8601
        unset($array[$keys[$rand]]);
8602
 
8603
        $draws--;
8604
    }
8605
 
8606
    return $return;
8607
}
8608
 
8609
/**
8610
 * Calculate the difference between two microtimes
8611
 *
8612
 * @param string $a The first Microtime
8613
 * @param string $b The second Microtime
8614
 * @return string
8615
 */
8616
function microtime_diff($a, $b) {
8617
    list($adec, $asec) = explode(' ', $a);
8618
    list($bdec, $bsec) = explode(' ', $b);
8619
    return $bsec - $asec + $bdec - $adec;
8620
}
8621
 
8622
/**
8623
 * Given a list (eg a,b,c,d,e) this function returns
8624
 * an array of 1->a, 2->b, 3->c etc
8625
 *
8626
 * @param string $list The string to explode into array bits
8627
 * @param string $separator The separator used within the list string
8628
 * @return array The now assembled array
8629
 */
8630
function make_menu_from_list($list, $separator=',') {
8631
 
8632
    $array = array_reverse(explode($separator, $list), true);
8633
    foreach ($array as $key => $item) {
8634
        $outarray[$key+1] = trim($item);
8635
    }
8636
    return $outarray;
8637
}
8638
 
8639
/**
8640
 * Creates an array that represents all the current grades that
8641
 * can be chosen using the given grading type.
8642
 *
8643
 * Negative numbers
8644
 * are scales, zero is no grade, and positive numbers are maximum
8645
 * grades.
8646
 *
8647
 * @todo Finish documenting this function or better deprecated this completely!
8648
 *
8649
 * @param int $gradingtype
8650
 * @return array
8651
 */
8652
function make_grades_menu($gradingtype) {
8653
    global $DB;
8654
 
8655
    $grades = array();
8656
    if ($gradingtype < 0) {
8657
        if ($scale = $DB->get_record('scale', array('id'=> (-$gradingtype)))) {
8658
            return make_menu_from_list($scale->scale);
8659
        }
8660
    } else if ($gradingtype > 0) {
8661
        for ($i=$gradingtype; $i>=0; $i--) {
8662
            $grades[$i] = $i .' / '. $gradingtype;
8663
        }
8664
        return $grades;
8665
    }
8666
    return $grades;
8667
}
8668
 
8669
/**
8670
 * make_unique_id_code
8671
 *
8672
 * @todo Finish documenting this function
8673
 *
8674
 * @uses $_SERVER
8675
 * @param string $extra Extra string to append to the end of the code
8676
 * @return string
8677
 */
8678
function make_unique_id_code($extra = '') {
8679
 
8680
    $hostname = 'unknownhost';
8681
    if (!empty($_SERVER['HTTP_HOST'])) {
8682
        $hostname = $_SERVER['HTTP_HOST'];
8683
    } else if (!empty($_ENV['HTTP_HOST'])) {
8684
        $hostname = $_ENV['HTTP_HOST'];
8685
    } else if (!empty($_SERVER['SERVER_NAME'])) {
8686
        $hostname = $_SERVER['SERVER_NAME'];
8687
    } else if (!empty($_ENV['SERVER_NAME'])) {
8688
        $hostname = $_ENV['SERVER_NAME'];
8689
    }
8690
 
8691
    $date = gmdate("ymdHis");
8692
 
8693
    $random =  random_string(6);
8694
 
8695
    if ($extra) {
8696
        return $hostname .'+'. $date .'+'. $random .'+'. $extra;
8697
    } else {
8698
        return $hostname .'+'. $date .'+'. $random;
8699
    }
8700
}
8701
 
8702
 
8703
/**
8704
 * Function to check the passed address is within the passed subnet
8705
 *
8706
 * The parameter is a comma separated string of subnet definitions.
8707
 * Subnet strings can be in one of three formats:
8708
 *   1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn          (number of bits in net mask)
8709
 *   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)
8710
 *   3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx.                  (incomplete address, a bit non-technical ;-)
8711
 * Code for type 1 modified from user posted comments by mediator at
8712
 * {@link http://au.php.net/manual/en/function.ip2long.php}
8713
 *
8714
 * @param string $addr    The address you are checking
8715
 * @param string $subnetstr    The string of subnet addresses
8716
 * @param bool $checkallzeros    The state to whether check for 0.0.0.0
8717
 * @return bool
8718
 */
8719
function address_in_subnet($addr, $subnetstr, $checkallzeros = false) {
8720
 
8721
    if ($addr == '0.0.0.0' && !$checkallzeros) {
8722
        return false;
8723
    }
8724
    $subnets = explode(',', $subnetstr);
8725
    $found = false;
8726
    $addr = trim($addr);
8727
    $addr = cleanremoteaddr($addr, false); // Normalise.
8728
    if ($addr === null) {
8729
        return false;
8730
    }
8731
    $addrparts = explode(':', $addr);
8732
 
8733
    $ipv6 = strpos($addr, ':');
8734
 
8735
    foreach ($subnets as $subnet) {
8736
        $subnet = trim($subnet);
8737
        if ($subnet === '') {
8738
            continue;
8739
        }
8740
 
8741
        if (strpos($subnet, '/') !== false) {
8742
            // 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn.
8743
            list($ip, $mask) = explode('/', $subnet);
8744
            $mask = trim($mask);
8745
            if (!is_number($mask)) {
8746
                continue; // Incorect mask number, eh?
8747
            }
8748
            $ip = cleanremoteaddr($ip, false); // Normalise.
8749
            if ($ip === null) {
8750
                continue;
8751
            }
8752
            if (strpos($ip, ':') !== false) {
8753
                // IPv6.
8754
                if (!$ipv6) {
8755
                    continue;
8756
                }
8757
                if ($mask > 128 or $mask < 0) {
8758
                    continue; // Nonsense.
8759
                }
8760
                if ($mask == 0) {
8761
                    return true; // Any address.
8762
                }
8763
                if ($mask == 128) {
8764
                    if ($ip === $addr) {
8765
                        return true;
8766
                    }
8767
                    continue;
8768
                }
8769
                $ipparts = explode(':', $ip);
8770
                $modulo  = $mask % 16;
8771
                $ipnet   = array_slice($ipparts, 0, ($mask-$modulo)/16);
8772
                $addrnet = array_slice($addrparts, 0, ($mask-$modulo)/16);
8773
                if (implode(':', $ipnet) === implode(':', $addrnet)) {
8774
                    if ($modulo == 0) {
8775
                        return true;
8776
                    }
8777
                    $pos     = ($mask-$modulo)/16;
8778
                    $ipnet   = hexdec($ipparts[$pos]);
8779
                    $addrnet = hexdec($addrparts[$pos]);
8780
                    $mask    = 0xffff << (16 - $modulo);
8781
                    if (($addrnet & $mask) == ($ipnet & $mask)) {
8782
                        return true;
8783
                    }
8784
                }
8785
 
8786
            } else {
8787
                // IPv4.
8788
                if ($ipv6) {
8789
                    continue;
8790
                }
8791
                if ($mask > 32 or $mask < 0) {
8792
                    continue; // Nonsense.
8793
                }
8794
                if ($mask == 0) {
8795
                    return true;
8796
                }
8797
                if ($mask == 32) {
8798
                    if ($ip === $addr) {
8799
                        return true;
8800
                    }
8801
                    continue;
8802
                }
8803
                $mask = 0xffffffff << (32 - $mask);
8804
                if (((ip2long($addr) & $mask) == (ip2long($ip) & $mask))) {
8805
                    return true;
8806
                }
8807
            }
8808
 
8809
        } else if (strpos($subnet, '-') !== false) {
8810
            // 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.
8811
            $parts = explode('-', $subnet);
8812
            if (count($parts) != 2) {
8813
                continue;
8814
            }
8815
 
8816
            if (strpos($subnet, ':') !== false) {
8817
                // IPv6.
8818
                if (!$ipv6) {
8819
                    continue;
8820
                }
8821
                $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise.
8822
                if ($ipstart === null) {
8823
                    continue;
8824
                }
8825
                $ipparts = explode(':', $ipstart);
8826
                $start = hexdec(array_pop($ipparts));
8827
                $ipparts[] = trim($parts[1]);
8828
                $ipend = cleanremoteaddr(implode(':', $ipparts), false); // Normalise.
8829
                if ($ipend === null) {
8830
                    continue;
8831
                }
8832
                $ipparts[7] = '';
8833
                $ipnet = implode(':', $ipparts);
8834
                if (strpos($addr, $ipnet) !== 0) {
8835
                    continue;
8836
                }
8837
                $ipparts = explode(':', $ipend);
8838
                $end = hexdec($ipparts[7]);
8839
 
8840
                $addrend = hexdec($addrparts[7]);
8841
 
8842
                if (($addrend >= $start) and ($addrend <= $end)) {
8843
                    return true;
8844
                }
8845
 
8846
            } else {
8847
                // IPv4.
8848
                if ($ipv6) {
8849
                    continue;
8850
                }
8851
                $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise.
8852
                if ($ipstart === null) {
8853
                    continue;
8854
                }
8855
                $ipparts = explode('.', $ipstart);
8856
                $ipparts[3] = trim($parts[1]);
8857
                $ipend = cleanremoteaddr(implode('.', $ipparts), false); // Normalise.
8858
                if ($ipend === null) {
8859
                    continue;
8860
                }
8861
 
8862
                if ((ip2long($addr) >= ip2long($ipstart)) and (ip2long($addr) <= ip2long($ipend))) {
8863
                    return true;
8864
                }
8865
            }
8866
 
8867
        } else {
8868
            // 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx.
8869
            if (strpos($subnet, ':') !== false) {
8870
                // IPv6.
8871
                if (!$ipv6) {
8872
                    continue;
8873
                }
8874
                $parts = explode(':', $subnet);
8875
                $count = count($parts);
8876
                if ($parts[$count-1] === '') {
8877
                    unset($parts[$count-1]); // Trim trailing :'s.
8878
                    $count--;
8879
                    $subnet = implode('.', $parts);
8880
                }
8881
                $isip = cleanremoteaddr($subnet, false); // Normalise.
8882
                if ($isip !== null) {
8883
                    if ($isip === $addr) {
8884
                        return true;
8885
                    }
8886
                    continue;
8887
                } else if ($count > 8) {
8888
                    continue;
8889
                }
8890
                $zeros = array_fill(0, 8-$count, '0');
8891
                $subnet = $subnet.':'.implode(':', $zeros).'/'.($count*16);
8892
                if (address_in_subnet($addr, $subnet)) {
8893
                    return true;
8894
                }
8895
 
8896
            } else {
8897
                // IPv4.
8898
                if ($ipv6) {
8899
                    continue;
8900
                }
8901
                $parts = explode('.', $subnet);
8902
                $count = count($parts);
8903
                if ($parts[$count-1] === '') {
8904
                    unset($parts[$count-1]); // Trim trailing .
8905
                    $count--;
8906
                    $subnet = implode('.', $parts);
8907
                }
8908
                if ($count == 4) {
8909
                    $subnet = cleanremoteaddr($subnet, false); // Normalise.
8910
                    if ($subnet === $addr) {
8911
                        return true;
8912
                    }
8913
                    continue;
8914
                } else if ($count > 4) {
8915
                    continue;
8916
                }
8917
                $zeros = array_fill(0, 4-$count, '0');
8918
                $subnet = $subnet.'.'.implode('.', $zeros).'/'.($count*8);
8919
                if (address_in_subnet($addr, $subnet)) {
8920
                    return true;
8921
                }
8922
            }
8923
        }
8924
    }
8925
 
8926
    return false;
8927
}
8928
 
8929
/**
8930
 * For outputting debugging info
8931
 *
8932
 * @param string $string The string to write
8933
 * @param string $eol The end of line char(s) to use
8934
 * @param string $sleep Period to make the application sleep
8935
 *                      This ensures any messages have time to display before redirect
8936
 */
8937
function mtrace($string, $eol="\n", $sleep=0) {
8938
    global $CFG;
8939
 
8940
    if (isset($CFG->mtrace_wrapper) && function_exists($CFG->mtrace_wrapper)) {
8941
        $fn = $CFG->mtrace_wrapper;
8942
        $fn($string, $eol);
8943
        return;
8944
    } else if (defined('STDOUT') && !PHPUNIT_TEST && !defined('BEHAT_TEST')) {
8945
        // We must explicitly call the add_line function here.
8946
        // Uses of fwrite to STDOUT are not picked up by ob_start.
8947
        if ($output = \core\task\logmanager::add_line("{$string}{$eol}")) {
8948
            fwrite(STDOUT, $output);
8949
        }
8950
    } else {
8951
        echo $string . $eol;
8952
    }
8953
 
8954
    // Flush again.
8955
    flush();
8956
 
8957
    // Delay to keep message on user's screen in case of subsequent redirect.
8958
    if ($sleep) {
8959
        sleep($sleep);
8960
    }
8961
}
8962
 
8963
/**
8964
 * Helper to {@see mtrace()} an exception or throwable, including all relevant information.
8965
 *
8966
 * @param Throwable $e the error to ouptput.
8967
 */
8968
function mtrace_exception(Throwable $e): void {
8969
    $info = get_exception_info($e);
8970
 
8971
    $message = $info->message;
8972
    if ($info->debuginfo) {
8973
        $message .= "\n\n" . $info->debuginfo;
8974
    }
8975
    if ($info->backtrace) {
8976
        $message .= "\n\n" . format_backtrace($info->backtrace, true);
8977
    }
8978
 
8979
    mtrace($message);
8980
}
8981
 
8982
/**
8983
 * Replace 1 or more slashes or backslashes to 1 slash
8984
 *
8985
 * @param string $path The path to strip
8986
 * @return string the path with double slashes removed
8987
 */
8988
function cleardoubleslashes ($path) {
8989
    return preg_replace('/(\/|\\\){1,}/', '/', $path);
8990
}
8991
 
8992
/**
8993
 * Is the current ip in a given list?
8994
 *
8995
 * @param string $list
8996
 * @return bool
8997
 */
8998
function remoteip_in_list($list) {
8999
    $clientip = getremoteaddr(null);
9000
 
9001
    if (!$clientip) {
9002
        // Ensure access on cli.
9003
        return true;
9004
    }
9005
    return \core\ip_utils::is_ip_in_subnet_list($clientip, $list);
9006
}
9007
 
9008
/**
9009
 * Returns most reliable client address
9010
 *
9011
 * @param string $default If an address can't be determined, then return this
9012
 * @return string The remote IP address
9013
 */
9014
function getremoteaddr($default='0.0.0.0') {
9015
    global $CFG;
9016
 
9017
    if (!isset($CFG->getremoteaddrconf)) {
9018
        // This will happen, for example, before just after the upgrade, as the
9019
        // user is redirected to the admin screen.
9020
        $variablestoskip = GETREMOTEADDR_SKIP_DEFAULT;
9021
    } else {
9022
        $variablestoskip = $CFG->getremoteaddrconf;
9023
    }
9024
    if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_CLIENT_IP)) {
9025
        if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
9026
            $address = cleanremoteaddr($_SERVER['HTTP_CLIENT_IP']);
9027
            return $address ? $address : $default;
9028
        }
9029
    }
9030
    if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR)) {
9031
        if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
9032
            $forwardedaddresses = explode(",", $_SERVER['HTTP_X_FORWARDED_FOR']);
9033
 
9034
            $forwardedaddresses = array_filter($forwardedaddresses, function($ip) {
9035
                global $CFG;
9036
                return !\core\ip_utils::is_ip_in_subnet_list($ip, $CFG->reverseproxyignore ?? '', ',');
9037
            });
9038
 
9039
            // Multiple proxies can append values to this header including an
9040
            // untrusted original request header so we must only trust the last ip.
9041
            $address = end($forwardedaddresses);
9042
 
9043
            if (substr_count($address, ":") > 1) {
9044
                // Remove port and brackets from IPv6.
9045
                if (preg_match("/\[(.*)\]:/", $address, $matches)) {
9046
                    $address = $matches[1];
9047
                }
9048
            } else {
9049
                // Remove port from IPv4.
9050
                if (substr_count($address, ":") == 1) {
9051
                    $parts = explode(":", $address);
9052
                    $address = $parts[0];
9053
                }
9054
            }
9055
 
9056
            $address = cleanremoteaddr($address);
9057
            return $address ? $address : $default;
9058
        }
9059
    }
9060
    if (!empty($_SERVER['REMOTE_ADDR'])) {
9061
        $address = cleanremoteaddr($_SERVER['REMOTE_ADDR']);
9062
        return $address ? $address : $default;
9063
    } else {
9064
        return $default;
9065
    }
9066
}
9067
 
9068
/**
9069
 * Cleans an ip address. Internal addresses are now allowed.
9070
 * (Originally local addresses were not allowed.)
9071
 *
9072
 * @param string $addr IPv4 or IPv6 address
9073
 * @param bool $compress use IPv6 address compression
9074
 * @return string normalised ip address string, null if error
9075
 */
9076
function cleanremoteaddr($addr, $compress=false) {
9077
    $addr = trim($addr);
9078
 
9079
    if (strpos($addr, ':') !== false) {
9080
        // Can be only IPv6.
9081
        $parts = explode(':', $addr);
9082
        $count = count($parts);
9083
 
9084
        if (strpos($parts[$count-1], '.') !== false) {
9085
            // Legacy ipv4 notation.
9086
            $last = array_pop($parts);
9087
            $ipv4 = cleanremoteaddr($last, true);
9088
            if ($ipv4 === null) {
9089
                return null;
9090
            }
9091
            $bits = explode('.', $ipv4);
9092
            $parts[] = dechex($bits[0]).dechex($bits[1]);
9093
            $parts[] = dechex($bits[2]).dechex($bits[3]);
9094
            $count = count($parts);
9095
            $addr = implode(':', $parts);
9096
        }
9097
 
9098
        if ($count < 3 or $count > 8) {
9099
            return null; // Severly malformed.
9100
        }
9101
 
9102
        if ($count != 8) {
9103
            if (strpos($addr, '::') === false) {
9104
                return null; // Malformed.
9105
            }
9106
            // Uncompress.
9107
            $insertat = array_search('', $parts, true);
9108
            $missing = array_fill(0, 1 + 8 - $count, '0');
9109
            array_splice($parts, $insertat, 1, $missing);
9110
            foreach ($parts as $key => $part) {
9111
                if ($part === '') {
9112
                    $parts[$key] = '0';
9113
                }
9114
            }
9115
        }
9116
 
9117
        $adr = implode(':', $parts);
9118
        if (!preg_match('/^([0-9a-f]{1,4})(:[0-9a-f]{1,4})*$/i', $adr)) {
9119
            return null; // Incorrect format - sorry.
9120
        }
9121
 
9122
        // Normalise 0s and case.
9123
        $parts = array_map('hexdec', $parts);
9124
        $parts = array_map('dechex', $parts);
9125
 
9126
        $result = implode(':', $parts);
9127
 
9128
        if (!$compress) {
9129
            return $result;
9130
        }
9131
 
9132
        if ($result === '0:0:0:0:0:0:0:0') {
9133
            return '::'; // All addresses.
9134
        }
9135
 
9136
        $compressed = preg_replace('/(:0)+:0$/', '::', $result, 1);
9137
        if ($compressed !== $result) {
9138
            return $compressed;
9139
        }
9140
 
9141
        $compressed = preg_replace('/^(0:){2,7}/', '::', $result, 1);
9142
        if ($compressed !== $result) {
9143
            return $compressed;
9144
        }
9145
 
9146
        $compressed = preg_replace('/(:0){2,6}:/', '::', $result, 1);
9147
        if ($compressed !== $result) {
9148
            return $compressed;
9149
        }
9150
 
9151
        return $result;
9152
    }
9153
 
9154
    // First get all things that look like IPv4 addresses.
9155
    $parts = array();
9156
    if (!preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $addr, $parts)) {
9157
        return null;
9158
    }
9159
    unset($parts[0]);
9160
 
9161
    foreach ($parts as $key => $match) {
9162
        if ($match > 255) {
9163
            return null;
9164
        }
9165
        $parts[$key] = (int)$match; // Normalise 0s.
9166
    }
9167
 
9168
    return implode('.', $parts);
9169
}
9170
 
9171
 
9172
/**
9173
 * Is IP address a public address?
9174
 *
9175
 * @param string $ip The ip to check
9176
 * @return bool true if the ip is public
9177
 */
9178
function ip_is_public($ip) {
9179
    return (bool) filter_var($ip, FILTER_VALIDATE_IP, (FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE));
9180
}
9181
 
9182
/**
9183
 * This function will make a complete copy of anything it's given,
9184
 * regardless of whether it's an object or not.
9185
 *
9186
 * @param mixed $thing Something you want cloned
9187
 * @return mixed What ever it is you passed it
9188
 */
9189
function fullclone($thing) {
9190
    return unserialize(serialize($thing));
9191
}
9192
 
9193
/**
9194
 * Used to make sure that $min <= $value <= $max
9195
 *
9196
 * Make sure that value is between min, and max
9197
 *
9198
 * @param int $min The minimum value
9199
 * @param int $value The value to check
9200
 * @param int $max The maximum value
9201
 * @return int
9202
 */
9203
function bounded_number($min, $value, $max) {
9204
    if ($value < $min) {
9205
        return $min;
9206
    }
9207
    if ($value > $max) {
9208
        return $max;
9209
    }
9210
    return $value;
9211
}
9212
 
9213
/**
9214
 * Check if there is a nested array within the passed array
9215
 *
9216
 * @param array $array
9217
 * @return bool true if there is a nested array false otherwise
9218
 */
9219
function array_is_nested($array) {
9220
    foreach ($array as $value) {
9221
        if (is_array($value)) {
9222
            return true;
9223
        }
9224
    }
9225
    return false;
9226
}
9227
 
9228
/**
9229
 * get_performance_info() pairs up with init_performance_info()
9230
 * loaded in setup.php. Returns an array with 'html' and 'txt'
9231
 * values ready for use, and each of the individual stats provided
9232
 * separately as well.
9233
 *
9234
 * @return array
9235
 */
9236
function get_performance_info() {
9237
    global $CFG, $PERF, $DB, $PAGE;
9238
 
9239
    $info = array();
9240
    $info['txt']  = me() . ' '; // Holds log-friendly representation.
9241
 
9242
    $info['html'] = '';
9243
    if (!empty($CFG->themedesignermode)) {
9244
        // Attempt to avoid devs debugging peformance issues, when its caused by css building and so on.
9245
        $info['html'] .= '<p><strong>Warning: Theme designer mode is enabled.</strong></p>';
9246
    }
9247
    $info['html'] .= '<ul class="list-unstyled row mx-md-0">';         // Holds userfriendly HTML representation.
9248
 
9249
    $info['realtime'] = microtime_diff($PERF->starttime, microtime());
9250
 
9251
    $info['html'] .= '<li class="timeused col-sm-4">'.$info['realtime'].' secs</li> ';
9252
    $info['txt'] .= 'time: '.$info['realtime'].'s ';
9253
 
9254
    // GET/POST (or NULL if $_SERVER['REQUEST_METHOD'] is undefined) is useful for txt logged information.
9255
    $info['txt'] .= 'method: ' . ($_SERVER['REQUEST_METHOD'] ?? "NULL") . ' ';
9256
 
9257
    if (function_exists('memory_get_usage')) {
9258
        $info['memory_total'] = memory_get_usage();
9259
        $info['memory_growth'] = memory_get_usage() - $PERF->startmemory;
9260
        $info['html'] .= '<li class="memoryused col-sm-4">RAM: '.display_size($info['memory_total']).'</li> ';
9261
        $info['txt']  .= 'memory_total: '.$info['memory_total'].'B (' . display_size($info['memory_total']).') memory_growth: '.
9262
            $info['memory_growth'].'B ('.display_size($info['memory_growth']).') ';
9263
    }
9264
 
9265
    if (function_exists('memory_get_peak_usage')) {
9266
        $info['memory_peak'] = memory_get_peak_usage();
9267
        $info['html'] .= '<li class="memoryused col-sm-4">RAM peak: '.display_size($info['memory_peak']).'</li> ';
9268
        $info['txt']  .= 'memory_peak: '.$info['memory_peak'].'B (' . display_size($info['memory_peak']).') ';
9269
    }
9270
 
9271
    $info['html'] .= '</ul><ul class="list-unstyled row mx-md-0">';
9272
    $inc = get_included_files();
9273
    $info['includecount'] = count($inc);
9274
    $info['html'] .= '<li class="included col-sm-4">Included '.$info['includecount'].' files</li> ';
9275
    $info['txt']  .= 'includecount: '.$info['includecount'].' ';
9276
 
9277
    if (!empty($CFG->early_install_lang) or empty($PAGE)) {
9278
        // We can not track more performance before installation or before PAGE init, sorry.
9279
        return $info;
9280
    }
9281
 
9282
    $filtermanager = filter_manager::instance();
9283
    if (method_exists($filtermanager, 'get_performance_summary')) {
9284
        list($filterinfo, $nicenames) = $filtermanager->get_performance_summary();
9285
        $info = array_merge($filterinfo, $info);
9286
        foreach ($filterinfo as $key => $value) {
9287
            $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> ";
9288
            $info['txt'] .= "$key: $value ";
9289
        }
9290
    }
9291
 
9292
    $stringmanager = get_string_manager();
9293
    if (method_exists($stringmanager, 'get_performance_summary')) {
9294
        list($filterinfo, $nicenames) = $stringmanager->get_performance_summary();
9295
        $info = array_merge($filterinfo, $info);
9296
        foreach ($filterinfo as $key => $value) {
9297
            $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> ";
9298
            $info['txt'] .= "$key: $value ";
9299
        }
9300
    }
9301
 
9302
    $info['dbqueries'] = $DB->perf_get_reads().'/'.$DB->perf_get_writes();
9303
    $info['html'] .= '<li class="dbqueries col-sm-4">DB reads/writes: '.$info['dbqueries'].'</li> ';
9304
    $info['txt'] .= 'db reads/writes: '.$info['dbqueries'].' ';
9305
 
9306
    if ($DB->want_read_slave()) {
9307
        $info['dbreads_slave'] = $DB->perf_get_reads_slave();
9308
        $info['html'] .= '<li class="dbqueries col-sm-4">DB reads from slave: '.$info['dbreads_slave'].'</li> ';
9309
        $info['txt'] .= 'db reads from slave: '.$info['dbreads_slave'].' ';
9310
    }
9311
 
9312
    $info['dbtime'] = round($DB->perf_get_queries_time(), 5);
9313
    $info['html'] .= '<li class="dbtime col-sm-4">DB queries time: '.$info['dbtime'].' secs</li> ';
9314
    $info['txt'] .= 'db queries time: ' . $info['dbtime'] . 's ';
9315
 
9316
    if (function_exists('posix_times')) {
9317
        $ptimes = posix_times();
9318
        if (is_array($ptimes)) {
9319
            foreach ($ptimes as $key => $val) {
9320
                $info[$key] = $ptimes[$key] -  $PERF->startposixtimes[$key];
9321
            }
9322
            $info['html'] .= "<li class=\"posixtimes col-sm-4\">ticks: $info[ticks] user: $info[utime]";
9323
            $info['html'] .= "sys: $info[stime] cuser: $info[cutime] csys: $info[cstime]</li> ";
9324
            $info['txt'] .= "ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime] ";
9325
        }
9326
    }
9327
 
9328
    // Grab the load average for the last minute.
9329
    // /proc will only work under some linux configurations
9330
    // while uptime is there under MacOSX/Darwin and other unices.
9331
    if (is_readable('/proc/loadavg') && $loadavg = @file('/proc/loadavg')) {
9332
        list($serverload) = explode(' ', $loadavg[0]);
9333
        unset($loadavg);
9334
    } else if ( function_exists('is_executable') && is_executable('/usr/bin/uptime') && $loadavg = `/usr/bin/uptime` ) {
9335
        if (preg_match('/load averages?: (\d+[\.,:]\d+)/', $loadavg, $matches)) {
9336
            $serverload = $matches[1];
9337
        } else {
9338
            trigger_error('Could not parse uptime output!');
9339
        }
9340
    }
9341
    if (!empty($serverload)) {
9342
        $info['serverload'] = $serverload;
9343
        $info['html'] .= '<li class="serverload col-sm-4">Load average: '.$info['serverload'].'</li> ';
9344
        $info['txt'] .= "serverload: {$info['serverload']} ";
9345
    }
9346
 
9347
    // Display size of session if session started.
9348
    if ($si = \core\session\manager::get_performance_info()) {
9349
        $info['sessionsize'] = $si['size'];
9350
        $info['html'] .= "<li class=\"serverload col-sm-4\">" . $si['html'] . "</li>";
9351
        $info['txt'] .= $si['txt'];
9352
    }
9353
 
9354
    // Display time waiting for session if applicable.
9355
    if (!empty($PERF->sessionlock['wait'])) {
9356
        $sessionwait = number_format($PERF->sessionlock['wait'], 3) . ' secs';
9357
        $info['html'] .= html_writer::tag('li', 'Session wait: ' . $sessionwait, [
9358
            'class' => 'sessionwait col-sm-4'
9359
        ]);
9360
        $info['txt'] .= 'sessionwait: ' . $sessionwait . ' ';
9361
    }
9362
 
9363
    $info['html'] .= '</ul>';
9364
    $html = '';
9365
    if ($stats = cache_helper::get_stats()) {
9366
 
9367
        $table = new html_table();
9368
        $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered';
9369
        $table->head = ['Mode', 'Cache item', 'Static', 'H', 'M', get_string('mappingprimary', 'cache'), 'H', 'M', 'S', 'I/O'];
9370
        $table->data = [];
9371
        $table->align = ['left', 'left', 'left', 'right', 'right', 'left', 'right', 'right', 'right', 'right'];
9372
 
9373
        $text = 'Caches used (hits/misses/sets): ';
9374
        $hits = 0;
9375
        $misses = 0;
9376
        $sets = 0;
9377
        $maxstores = 0;
9378
 
9379
        // We want to align static caches into their own column.
9380
        $hasstatic = false;
9381
        foreach ($stats as $definition => $details) {
9382
            $numstores = count($details['stores']);
9383
            $first = key($details['stores']);
9384
            if ($first !== cache_store::STATIC_ACCEL) {
9385
                $numstores++; // Add a blank space for the missing static store.
9386
            }
9387
            $maxstores = max($maxstores, $numstores);
9388
        }
9389
 
9390
        $storec = 0;
9391
 
9392
        while ($storec++ < ($maxstores - 2)) {
9393
            if ($storec == ($maxstores - 2)) {
9394
                $table->head[] = get_string('mappingfinal', 'cache');
9395
            } else {
9396
                $table->head[] = "Store $storec";
9397
            }
9398
            $table->align[] = 'left';
9399
            $table->align[] = 'right';
9400
            $table->align[] = 'right';
9401
            $table->align[] = 'right';
9402
            $table->align[] = 'right';
9403
            $table->head[] = 'H';
9404
            $table->head[] = 'M';
9405
            $table->head[] = 'S';
9406
            $table->head[] = 'I/O';
9407
        }
9408
 
9409
        ksort($stats);
9410
 
9411
        foreach ($stats as $definition => $details) {
9412
            switch ($details['mode']) {
9413
                case cache_store::MODE_APPLICATION:
9414
                    $modeclass = 'application';
9415
                    $mode = ' <span title="application cache">App</span>';
9416
                    break;
9417
                case cache_store::MODE_SESSION:
9418
                    $modeclass = 'session';
9419
                    $mode = ' <span title="session cache">Ses</span>';
9420
                    break;
9421
                case cache_store::MODE_REQUEST:
9422
                    $modeclass = 'request';
9423
                    $mode = ' <span title="request cache">Req</span>';
9424
                    break;
9425
            }
9426
            $row = [$mode, $definition];
9427
 
9428
            $text .= "$definition {";
9429
 
9430
            $storec = 0;
9431
            foreach ($details['stores'] as $store => $data) {
9432
 
9433
                if ($storec == 0 && $store !== cache_store::STATIC_ACCEL) {
9434
                    $row[] = '';
9435
                    $row[] = '';
9436
                    $row[] = '';
9437
                    $storec++;
9438
                }
9439
 
9440
                $hits   += $data['hits'];
9441
                $misses += $data['misses'];
9442
                $sets   += $data['sets'];
9443
                if ($data['hits'] == 0 and $data['misses'] > 0) {
9444
                    $cachestoreclass = 'nohits bg-danger';
9445
                } else if ($data['hits'] < $data['misses']) {
9446
                    $cachestoreclass = 'lowhits bg-warning text-dark';
9447
                } else {
9448
                    $cachestoreclass = 'hihits';
9449
                }
9450
                $text .= "$store($data[hits]/$data[misses]/$data[sets]) ";
9451
                $cell = new html_table_cell($store);
9452
                $cell->attributes = ['class' => $cachestoreclass];
9453
                $row[] = $cell;
9454
                $cell = new html_table_cell($data['hits']);
9455
                $cell->attributes = ['class' => $cachestoreclass];
9456
                $row[] = $cell;
9457
                $cell = new html_table_cell($data['misses']);
9458
                $cell->attributes = ['class' => $cachestoreclass];
9459
                $row[] = $cell;
9460
 
9461
                if ($store !== cache_store::STATIC_ACCEL) {
9462
                    // The static cache is never set.
9463
                    $cell = new html_table_cell($data['sets']);
9464
                    $cell->attributes = ['class' => $cachestoreclass];
9465
                    $row[] = $cell;
9466
 
9467
                    if ($data['hits'] || $data['sets']) {
9468
                        if ($data['iobytes'] === cache_store::IO_BYTES_NOT_SUPPORTED) {
9469
                            $size = '-';
9470
                        } else {
9471
                            $size = display_size($data['iobytes'], 1, 'KB');
9472
                            if ($data['iobytes'] >= 10 * 1024) {
9473
                                $cachestoreclass = ' bg-warning text-dark';
9474
                            }
9475
                        }
9476
                    } else {
9477
                        $size = '';
9478
                    }
9479
                    $cell = new html_table_cell($size);
9480
                    $cell->attributes = ['class' => $cachestoreclass];
9481
                    $row[] = $cell;
9482
                }
9483
                $storec++;
9484
            }
9485
            while ($storec++ < $maxstores) {
9486
                $row[] = '';
9487
                $row[] = '';
9488
                $row[] = '';
9489
                $row[] = '';
9490
                $row[] = '';
9491
            }
9492
            $text .= '} ';
9493
 
9494
            $table->data[] = $row;
9495
        }
9496
 
9497
        $html .= html_writer::table($table);
9498
 
9499
        // Now lets also show sub totals for each cache store.
9500
        $storetotals = [];
9501
        $storetotal = ['hits' => 0, 'misses' => 0, 'sets' => 0, 'iobytes' => 0];
9502
        foreach ($stats as $definition => $details) {
9503
            foreach ($details['stores'] as $store => $data) {
9504
                if (!array_key_exists($store, $storetotals)) {
9505
                    $storetotals[$store] = ['hits' => 0, 'misses' => 0, 'sets' => 0, 'iobytes' => 0];
9506
                }
9507
                $storetotals[$store]['class']   = $data['class'];
9508
                $storetotals[$store]['hits']   += $data['hits'];
9509
                $storetotals[$store]['misses'] += $data['misses'];
9510
                $storetotals[$store]['sets']   += $data['sets'];
9511
                $storetotal['hits']   += $data['hits'];
9512
                $storetotal['misses'] += $data['misses'];
9513
                $storetotal['sets']   += $data['sets'];
9514
                if ($data['iobytes'] !== cache_store::IO_BYTES_NOT_SUPPORTED) {
9515
                    $storetotals[$store]['iobytes'] += $data['iobytes'];
9516
                    $storetotal['iobytes'] += $data['iobytes'];
9517
                }
9518
            }
9519
        }
9520
 
9521
        $table = new html_table();
9522
        $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered';
9523
        $table->head = [get_string('storename', 'cache'), get_string('type_cachestore', 'plugin'), 'H', 'M', 'S', 'I/O'];
9524
        $table->data = [];
9525
        $table->align = ['left', 'left', 'right', 'right', 'right', 'right'];
9526
 
9527
        ksort($storetotals);
9528
 
9529
        foreach ($storetotals as $store => $data) {
9530
            $row = [];
9531
            if ($data['hits'] == 0 and $data['misses'] > 0) {
9532
                $cachestoreclass = 'nohits bg-danger';
9533
            } else if ($data['hits'] < $data['misses']) {
9534
                $cachestoreclass = 'lowhits bg-warning text-dark';
9535
            } else {
9536
                $cachestoreclass = 'hihits';
9537
            }
9538
            $cell = new html_table_cell($store);
9539
            $cell->attributes = ['class' => $cachestoreclass];
9540
            $row[] = $cell;
9541
            $cell = new html_table_cell($data['class']);
9542
            $cell->attributes = ['class' => $cachestoreclass];
9543
            $row[] = $cell;
9544
            $cell = new html_table_cell($data['hits']);
9545
            $cell->attributes = ['class' => $cachestoreclass];
9546
            $row[] = $cell;
9547
            $cell = new html_table_cell($data['misses']);
9548
            $cell->attributes = ['class' => $cachestoreclass];
9549
            $row[] = $cell;
9550
            $cell = new html_table_cell($data['sets']);
9551
            $cell->attributes = ['class' => $cachestoreclass];
9552
            $row[] = $cell;
9553
            if ($data['hits'] || $data['sets']) {
9554
                if ($data['iobytes']) {
9555
                    $size = display_size($data['iobytes'], 1, 'KB');
9556
                } else {
9557
                    $size = '-';
9558
                }
9559
            } else {
9560
                $size = '';
9561
            }
9562
            $cell = new html_table_cell($size);
9563
            $cell->attributes = ['class' => $cachestoreclass];
9564
            $row[] = $cell;
9565
            $table->data[] = $row;
9566
        }
9567
        if (!empty($storetotal['iobytes'])) {
9568
            $size = display_size($storetotal['iobytes'], 1, 'KB');
9569
        } else if (!empty($storetotal['hits']) || !empty($storetotal['sets'])) {
9570
            $size = '-';
9571
        } else {
9572
            $size = '';
9573
        }
9574
        $row = [
9575
            get_string('total'),
9576
            '',
9577
            $storetotal['hits'],
9578
            $storetotal['misses'],
9579
            $storetotal['sets'],
9580
            $size,
9581
        ];
9582
        $table->data[] = $row;
9583
 
9584
        $html .= html_writer::table($table);
9585
 
9586
        $info['cachesused'] = "$hits / $misses / $sets";
9587
        $info['html'] .= $html;
9588
        $info['txt'] .= $text.'. ';
9589
    } else {
9590
        $info['cachesused'] = '0 / 0 / 0';
9591
        $info['html'] .= '<div class="cachesused">Caches used (hits/misses/sets): 0/0/0</div>';
9592
        $info['txt'] .= 'Caches used (hits/misses/sets): 0/0/0 ';
9593
    }
9594
 
9595
    // Display lock information if any.
9596
    if (!empty($PERF->locks)) {
9597
        $table = new html_table();
9598
        $table->attributes['class'] = 'locktimings table table-dark table-sm w-auto table-bordered';
9599
        $table->head = ['Lock', 'Waited (s)', 'Obtained', 'Held for (s)'];
9600
        $table->align = ['left', 'right', 'center', 'right'];
9601
        $table->data = [];
9602
        $text = 'Locks (waited/obtained/held):';
9603
        foreach ($PERF->locks as $locktiming) {
9604
            $row = [];
9605
            $row[] = s($locktiming->type . '/' . $locktiming->resource);
9606
            $text .= ' ' . $locktiming->type . '/' . $locktiming->resource . ' (';
9607
 
9608
            // The time we had to wait to get the lock.
9609
            $roundedtime = number_format($locktiming->wait, 1);
9610
            $cell = new html_table_cell($roundedtime);
9611
            if ($locktiming->wait > 0.5) {
9612
                $cell->attributes = ['class' => 'bg-warning text-dark'];
9613
            }
9614
            $row[] = $cell;
9615
            $text .= $roundedtime . '/';
9616
 
9617
            // Show a tick or cross for success.
9618
            $row[] = $locktiming->success ? '&#x2713;' : '&#x274c;';
9619
            $text .= ($locktiming->success ? 'y' : 'n') . '/';
9620
 
9621
            // If applicable, show how long we held the lock before releasing it.
9622
            if (property_exists($locktiming, 'held')) {
9623
                $roundedtime = number_format($locktiming->held, 1);
9624
                $cell = new html_table_cell($roundedtime);
9625
                if ($locktiming->held > 0.5) {
9626
                    $cell->attributes = ['class' => 'bg-warning text-dark'];
9627
                }
9628
                $row[] = $cell;
9629
                $text .= $roundedtime;
9630
            } else {
9631
                $row[] = '-';
9632
                $text .= '-';
9633
            }
9634
            $text .= ')';
9635
 
9636
            $table->data[] = $row;
9637
        }
9638
        $info['html'] .= html_writer::table($table);
9639
        $info['txt'] .= $text . '. ';
9640
    }
9641
 
9642
    $info['html'] = '<div class="performanceinfo siteinfo container-fluid px-md-0 overflow-auto pt-3">'.$info['html'].'</div>';
9643
    return $info;
9644
}
9645
 
9646
/**
9647
 * Renames a file or directory to a unique name within the same directory.
9648
 *
9649
 * This function is designed to avoid any potential race conditions, and select an unused name.
9650
 *
9651
 * @param string $filepath Original filepath
9652
 * @param string $prefix Prefix to use for the temporary name
9653
 * @return string|bool New file path or false if failed
9654
 * @since Moodle 3.10
9655
 */
9656
function rename_to_unused_name(string $filepath, string $prefix = '_temp_') {
9657
    $dir = dirname($filepath);
9658
    $basename = $dir . '/' . $prefix;
9659
    $limit = 0;
9660
    while ($limit < 100) {
9661
        // Select a new name based on a random number.
9662
        $newfilepath = $basename . md5(mt_rand());
9663
 
9664
        // Attempt a rename to that new name.
9665
        if (@rename($filepath, $newfilepath)) {
9666
            return $newfilepath;
9667
        }
9668
 
9669
        // The first time, do some sanity checks, maybe it is failing for a good reason and there
9670
        // is no point trying 100 times if so.
9671
        if ($limit === 0 && (!file_exists($filepath) || !is_writable($dir))) {
9672
            return false;
9673
        }
9674
        $limit++;
9675
    }
9676
    return false;
9677
}
9678
 
9679
/**
9680
 * Delete directory or only its content
9681
 *
9682
 * @param string $dir directory path
9683
 * @param bool $contentonly
9684
 * @return bool success, true also if dir does not exist
9685
 */
9686
function remove_dir($dir, $contentonly=false) {
9687
    if (!is_dir($dir)) {
9688
        // Nothing to do.
9689
        return true;
9690
    }
9691
 
9692
    if (!$contentonly) {
9693
        // Start by renaming the directory; this will guarantee that other processes don't write to it
9694
        // while it is in the process of being deleted.
9695
        $tempdir = rename_to_unused_name($dir);
9696
        if ($tempdir) {
9697
            // If the rename was successful then delete the $tempdir instead.
9698
            $dir = $tempdir;
9699
        }
9700
        // If the rename fails, we will continue through and attempt to delete the directory
9701
        // without renaming it since that is likely to at least delete most of the files.
9702
    }
9703
 
9704
    if (!$handle = opendir($dir)) {
9705
        return false;
9706
    }
9707
    $result = true;
9708
    while (false!==($item = readdir($handle))) {
9709
        if ($item != '.' && $item != '..') {
9710
            if (is_dir($dir.'/'.$item)) {
9711
                $result = remove_dir($dir.'/'.$item) && $result;
9712
            } else {
9713
                $result = unlink($dir.'/'.$item) && $result;
9714
            }
9715
        }
9716
    }
9717
    closedir($handle);
9718
    if ($contentonly) {
9719
        clearstatcache(); // Make sure file stat cache is properly invalidated.
9720
        return $result;
9721
    }
9722
    $result = rmdir($dir); // If anything left the result will be false, no need for && $result.
9723
    clearstatcache(); // Make sure file stat cache is properly invalidated.
9724
    return $result;
9725
}
9726
 
9727
/**
9728
 * Detect if an object or a class contains a given property
9729
 * will take an actual object or the name of a class
9730
 *
9731
 * @param mixed $obj Name of class or real object to test
9732
 * @param string $property name of property to find
9733
 * @return bool true if property exists
9734
 */
9735
function object_property_exists( $obj, $property ) {
9736
    if (is_string( $obj )) {
9737
        $properties = get_class_vars( $obj );
9738
    } else {
9739
        $properties = get_object_vars( $obj );
9740
    }
9741
    return array_key_exists( $property, $properties );
9742
}
9743
 
9744
/**
9745
 * Converts an object into an associative array
9746
 *
9747
 * This function converts an object into an associative array by iterating
9748
 * over its public properties. Because this function uses the foreach
9749
 * construct, Iterators are respected. It works recursively on arrays of objects.
9750
 * Arrays and simple values are returned as is.
9751
 *
9752
 * If class has magic properties, it can implement IteratorAggregate
9753
 * and return all available properties in getIterator()
9754
 *
9755
 * @param mixed $var
9756
 * @return array
9757
 */
9758
function convert_to_array($var) {
9759
    $result = array();
9760
 
9761
    // Loop over elements/properties.
9762
    foreach ($var as $key => $value) {
9763
        // Recursively convert objects.
9764
        if (is_object($value) || is_array($value)) {
9765
            $result[$key] = convert_to_array($value);
9766
        } else {
9767
            // Simple values are untouched.
9768
            $result[$key] = $value;
9769
        }
9770
    }
9771
    return $result;
9772
}
9773
 
9774
/**
9775
 * Detect a custom script replacement in the data directory that will
9776
 * replace an existing moodle script
9777
 *
9778
 * @return string|bool full path name if a custom script exists, false if no custom script exists
9779
 */
9780
function custom_script_path() {
9781
    global $CFG, $SCRIPT;
9782
 
9783
    if ($SCRIPT === null) {
9784
        // Probably some weird external script.
9785
        return false;
9786
    }
9787
 
9788
    $scriptpath = $CFG->customscripts . $SCRIPT;
9789
 
9790
    // Check the custom script exists.
9791
    if (file_exists($scriptpath) and is_file($scriptpath)) {
9792
        return $scriptpath;
9793
    } else {
9794
        return false;
9795
    }
9796
}
9797
 
9798
/**
9799
 * Returns whether or not the user object is a remote MNET user. This function
9800
 * is in moodlelib because it does not rely on loading any of the MNET code.
9801
 *
9802
 * @param object $user A valid user object
9803
 * @return bool        True if the user is from a remote Moodle.
9804
 */
9805
function is_mnet_remote_user($user) {
9806
    global $CFG;
9807
 
9808
    if (!isset($CFG->mnet_localhost_id)) {
9809
        include_once($CFG->dirroot . '/mnet/lib.php');
9810
        $env = new mnet_environment();
9811
        $env->init();
9812
        unset($env);
9813
    }
9814
 
9815
    return (!empty($user->mnethostid) && $user->mnethostid != $CFG->mnet_localhost_id);
9816
}
9817
 
9818
/**
9819
 * This function will search for browser prefereed languages, setting Moodle
9820
 * to use the best one available if $SESSION->lang is undefined
9821
 */
9822
function setup_lang_from_browser() {
9823
    global $CFG, $SESSION, $USER;
9824
 
9825
    if (!empty($SESSION->lang) or !empty($USER->lang) or empty($CFG->autolang)) {
9826
        // Lang is defined in session or user profile, nothing to do.
9827
        return;
9828
    }
9829
 
9830
    if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { // There isn't list of browser langs, nothing to do.
9831
        return;
9832
    }
9833
 
9834
    // Extract and clean langs from headers.
9835
    $rawlangs = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
9836
    $rawlangs = str_replace('-', '_', $rawlangs);         // We are using underscores.
9837
    $rawlangs = explode(',', $rawlangs);                  // Convert to array.
9838
    $langs = array();
9839
 
9840
    $order = 1.0;
9841
    foreach ($rawlangs as $lang) {
9842
        if (strpos($lang, ';') === false) {
9843
            $langs[(string)$order] = $lang;
9844
            $order = $order-0.01;
9845
        } else {
9846
            $parts = explode(';', $lang);
9847
            $pos = strpos($parts[1], '=');
9848
            $langs[substr($parts[1], $pos+1)] = $parts[0];
9849
        }
9850
    }
9851
    krsort($langs, SORT_NUMERIC);
9852
 
9853
    // Look for such langs under standard locations.
9854
    foreach ($langs as $lang) {
9855
        // Clean it properly for include.
9856
        $lang = strtolower(clean_param($lang, PARAM_SAFEDIR));
9857
        if (get_string_manager()->translation_exists($lang, false)) {
9858
            // If the translation for this language exists then try to set it
9859
            // for the rest of the session, if this is a read only session then
9860
            // we can only set it temporarily in $CFG.
9861
            if (defined('READ_ONLY_SESSION') && !empty($CFG->enable_read_only_sessions)) {
9862
                $CFG->lang = $lang;
9863
            } else {
9864
                $SESSION->lang = $lang;
9865
            }
9866
            // We have finished. Go out.
9867
            break;
9868
        }
9869
    }
9870
    return;
9871
}
9872
 
9873
/**
9874
 * Check if $url matches anything in proxybypass list
9875
 *
9876
 * Any errors just result in the proxy being used (least bad)
9877
 *
9878
 * @param string $url url to check
9879
 * @return boolean true if we should bypass the proxy
9880
 */
9881
function is_proxybypass( $url ) {
9882
    global $CFG;
9883
 
9884
    // Sanity check.
9885
    if (empty($CFG->proxyhost) or empty($CFG->proxybypass)) {
9886
        return false;
9887
    }
9888
 
9889
    // Get the host part out of the url.
9890
    if (!$host = parse_url( $url, PHP_URL_HOST )) {
9891
        return false;
9892
    }
9893
 
9894
    // Get the possible bypass hosts into an array.
9895
    $matches = explode( ',', $CFG->proxybypass );
9896
 
9897
    // Check for a exact match on the IP or in the domains.
9898
    $isdomaininallowedlist = \core\ip_utils::is_domain_in_allowed_list($host, $matches);
9899
    $isipinsubnetlist = \core\ip_utils::is_ip_in_subnet_list($host, $CFG->proxybypass, ',');
9900
 
9901
    if ($isdomaininallowedlist || $isipinsubnetlist) {
9902
        return true;
9903
    }
9904
 
9905
    // Nothing matched.
9906
    return false;
9907
}
9908
 
9909
/**
9910
 * Check if the passed navigation is of the new style
9911
 *
9912
 * @param mixed $navigation
9913
 * @return bool true for yes false for no
9914
 */
9915
function is_newnav($navigation) {
9916
    if (is_array($navigation) && !empty($navigation['newnav'])) {
9917
        return true;
9918
    } else {
9919
        return false;
9920
    }
9921
}
9922
 
9923
/**
9924
 * Checks whether the given variable name is defined as a variable within the given object.
9925
 *
9926
 * This will NOT work with stdClass objects, which have no class variables.
9927
 *
9928
 * @param string $var The variable name
9929
 * @param object $object The object to check
9930
 * @return boolean
9931
 */
9932
function in_object_vars($var, $object) {
9933
    $classvars = get_class_vars(get_class($object));
9934
    $classvars = array_keys($classvars);
9935
    return in_array($var, $classvars);
9936
}
9937
 
9938
/**
9939
 * Returns an array without repeated objects.
9940
 * This function is similar to array_unique, but for arrays that have objects as values
9941
 *
9942
 * @param array $array
9943
 * @param bool $keepkeyassoc
9944
 * @return array
9945
 */
9946
function object_array_unique($array, $keepkeyassoc = true) {
9947
    $duplicatekeys = array();
9948
    $tmp         = array();
9949
 
9950
    foreach ($array as $key => $val) {
9951
        // Convert objects to arrays, in_array() does not support objects.
9952
        if (is_object($val)) {
9953
            $val = (array)$val;
9954
        }
9955
 
9956
        if (!in_array($val, $tmp)) {
9957
            $tmp[] = $val;
9958
        } else {
9959
            $duplicatekeys[] = $key;
9960
        }
9961
    }
9962
 
9963
    foreach ($duplicatekeys as $key) {
9964
        unset($array[$key]);
9965
    }
9966
 
9967
    return $keepkeyassoc ? $array : array_values($array);
9968
}
9969
 
9970
/**
9971
 * Is a userid the primary administrator?
9972
 *
9973
 * @param int $userid int id of user to check
9974
 * @return boolean
9975
 */
9976
function is_primary_admin($userid) {
9977
    $primaryadmin =  get_admin();
9978
 
9979
    if ($userid == $primaryadmin->id) {
9980
        return true;
9981
    } else {
9982
        return false;
9983
    }
9984
}
9985
 
9986
/**
9987
 * Returns the site identifier
9988
 *
9989
 * @return string $CFG->siteidentifier, first making sure it is properly initialised.
9990
 */
9991
function get_site_identifier() {
9992
    global $CFG;
9993
    // Check to see if it is missing. If so, initialise it.
9994
    if (empty($CFG->siteidentifier)) {
9995
        set_config('siteidentifier', random_string(32) . $_SERVER['HTTP_HOST']);
9996
    }
9997
    // Return it.
9998
    return $CFG->siteidentifier;
9999
}
10000
 
10001
/**
10002
 * Check whether the given password has no more than the specified
10003
 * number of consecutive identical characters.
10004
 *
10005
 * @param string $password   password to be checked against the password policy
10006
 * @param integer $maxchars  maximum number of consecutive identical characters
10007
 * @return bool
10008
 */
10009
function check_consecutive_identical_characters($password, $maxchars) {
10010
 
10011
    if ($maxchars < 1) {
10012
        return true; // Zero 0 is to disable this check.
10013
    }
10014
    if (strlen($password) <= $maxchars) {
10015
        return true; // Too short to fail this test.
10016
    }
10017
 
10018
    $previouschar = '';
10019
    $consecutivecount = 1;
10020
    foreach (str_split($password) as $char) {
10021
        if ($char != $previouschar) {
10022
            $consecutivecount = 1;
10023
        } else {
10024
            $consecutivecount++;
10025
            if ($consecutivecount > $maxchars) {
10026
                return false; // Check failed already.
10027
            }
10028
        }
10029
 
10030
        $previouschar = $char;
10031
    }
10032
 
10033
    return true;
10034
}
10035
 
10036
/**
10037
 * Helper function to do partial function binding.
10038
 * so we can use it for preg_replace_callback, for example
10039
 * this works with php functions, user functions, static methods and class methods
10040
 * it returns you a callback that you can pass on like so:
10041
 *
10042
 * $callback = partial('somefunction', $arg1, $arg2);
10043
 *     or
10044
 * $callback = partial(array('someclass', 'somestaticmethod'), $arg1, $arg2);
10045
 *     or even
10046
 * $obj = new someclass();
10047
 * $callback = partial(array($obj, 'somemethod'), $arg1, $arg2);
10048
 *
10049
 * and then the arguments that are passed through at calltime are appended to the argument list.
10050
 *
10051
 * @param mixed $function a php callback
10052
 * @param mixed $arg1,... $argv arguments to partially bind with
10053
 * @return array Array callback
10054
 */
10055
function partial() {
10056
    if (!class_exists('partial')) {
10057
        /**
10058
         * Used to manage function binding.
10059
         * @copyright  2009 Penny Leach
10060
         * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
10061
         */
10062
        class partial{
10063
            /** @var array */
10064
            public $values = array();
10065
            /** @var string The function to call as a callback. */
10066
            public $func;
10067
            /**
10068
             * Constructor
10069
             * @param string $func
10070
             * @param array $args
10071
             */
10072
            public function __construct($func, $args) {
10073
                $this->values = $args;
10074
                $this->func = $func;
10075
            }
10076
            /**
10077
             * Calls the callback function.
10078
             * @return mixed
10079
             */
10080
            public function method() {
10081
                $args = func_get_args();
10082
                return call_user_func_array($this->func, array_merge($this->values, $args));
10083
            }
10084
        }
10085
    }
10086
    $args = func_get_args();
10087
    $func = array_shift($args);
10088
    $p = new partial($func, $args);
10089
    return array($p, 'method');
10090
}
10091
 
10092
/**
10093
 * helper function to load up and initialise the mnet environment
10094
 * this must be called before you use mnet functions.
10095
 *
10096
 * @return mnet_environment the equivalent of old $MNET global
10097
 */
10098
function get_mnet_environment() {
10099
    global $CFG;
10100
    require_once($CFG->dirroot . '/mnet/lib.php');
10101
    static $instance = null;
10102
    if (empty($instance)) {
10103
        $instance = new mnet_environment();
10104
        $instance->init();
10105
    }
10106
    return $instance;
10107
}
10108
 
10109
/**
10110
 * during xmlrpc server code execution, any code wishing to access
10111
 * information about the remote peer must use this to get it.
10112
 *
10113
 * @return mnet_remote_client|false the equivalent of old $MNETREMOTE_CLIENT global
10114
 */
10115
function get_mnet_remote_client() {
10116
    if (!defined('MNET_SERVER')) {
10117
        debugging(get_string('notinxmlrpcserver', 'mnet'));
10118
        return false;
10119
    }
10120
    global $MNET_REMOTE_CLIENT;
10121
    if (isset($MNET_REMOTE_CLIENT)) {
10122
        return $MNET_REMOTE_CLIENT;
10123
    }
10124
    return false;
10125
}
10126
 
10127
/**
10128
 * during the xmlrpc server code execution, this will be called
10129
 * to setup the object returned by {@link get_mnet_remote_client}
10130
 *
10131
 * @param mnet_remote_client $client the client to set up
10132
 * @throws moodle_exception
10133
 */
10134
function set_mnet_remote_client($client) {
10135
    if (!defined('MNET_SERVER')) {
10136
        throw new moodle_exception('notinxmlrpcserver', 'mnet');
10137
    }
10138
    global $MNET_REMOTE_CLIENT;
10139
    $MNET_REMOTE_CLIENT = $client;
10140
}
10141
 
10142
/**
10143
 * return the jump url for a given remote user
10144
 * this is used for rewriting forum post links in emails, etc
10145
 *
10146
 * @param stdclass $user the user to get the idp url for
10147
 */
10148
function mnet_get_idp_jump_url($user) {
10149
    global $CFG;
10150
 
10151
    static $mnetjumps = array();
10152
    if (!array_key_exists($user->mnethostid, $mnetjumps)) {
10153
        $idp = mnet_get_peer_host($user->mnethostid);
10154
        $idpjumppath = mnet_get_app_jumppath($idp->applicationid);
10155
        $mnetjumps[$user->mnethostid] = $idp->wwwroot . $idpjumppath . '?hostwwwroot=' . $CFG->wwwroot . '&wantsurl=';
10156
    }
10157
    return $mnetjumps[$user->mnethostid];
10158
}
10159
 
10160
/**
10161
 * Gets the homepage to use for the current user
10162
 *
10163
 * @return int One of HOMEPAGE_*
10164
 */
10165
function get_home_page() {
10166
    global $CFG;
10167
 
10168
    if (isloggedin() && !isguestuser() && !empty($CFG->defaulthomepage)) {
10169
        // If dashboard is disabled, home will be set to default page.
10170
        $defaultpage = get_default_home_page();
10171
        if ($CFG->defaulthomepage == HOMEPAGE_MY) {
10172
            if (!empty($CFG->enabledashboard)) {
10173
                return HOMEPAGE_MY;
10174
            } else {
10175
                return $defaultpage;
10176
            }
10177
        } else if ($CFG->defaulthomepage == HOMEPAGE_MYCOURSES) {
10178
            return HOMEPAGE_MYCOURSES;
10179
        } else {
10180
            $userhomepage = (int) get_user_preferences('user_home_page_preference', $defaultpage);
10181
            if (empty($CFG->enabledashboard) && $userhomepage == HOMEPAGE_MY) {
10182
                // If the user was using the dashboard but it's disabled, return the default home page.
10183
                $userhomepage = $defaultpage;
10184
            }
10185
            return $userhomepage;
10186
        }
10187
    }
10188
    return HOMEPAGE_SITE;
10189
}
10190
 
10191
/**
10192
 * Returns the default home page to display if current one is not defined or can't be applied.
10193
 * The default behaviour is to return Dashboard if it's enabled or My courses page if it isn't.
10194
 *
10195
 * @return int The default home page.
10196
 */
10197
function get_default_home_page(): int {
10198
    global $CFG;
10199
 
10200
    return (!isset($CFG->enabledashboard) || $CFG->enabledashboard) ? HOMEPAGE_MY : HOMEPAGE_MYCOURSES;
10201
}
10202
 
10203
/**
10204
 * Gets the name of a course to be displayed when showing a list of courses.
10205
 * By default this is just $course->fullname but user can configure it. The
10206
 * result of this function should be passed through print_string.
10207
 * @param stdClass|core_course_list_element $course Moodle course object
10208
 * @return string Display name of course (either fullname or short + fullname)
10209
 */
10210
function get_course_display_name_for_list($course) {
10211
    global $CFG;
10212
    if (!empty($CFG->courselistshortnames)) {
10213
        if (!($course instanceof stdClass)) {
10214
            $course = (object)convert_to_array($course);
10215
        }
10216
        return get_string('courseextendednamedisplay', '', $course);
10217
    } else {
10218
        return $course->fullname;
10219
    }
10220
}
10221
 
10222
/**
10223
 * Safe analogue of unserialize() that can only parse arrays
10224
 *
10225
 * Arrays may contain only integers or strings as both keys and values. Nested arrays are allowed.
10226
 *
10227
 * @param string $expression
10228
 * @return array|bool either parsed array or false if parsing was impossible.
10229
 */
10230
function unserialize_array($expression) {
10231
 
10232
    // Check the expression is an array.
10233
    if (!preg_match('/^a:(\d+):/', $expression)) {
10234
        return false;
10235
    }
10236
 
10237
    $values = (array) unserialize_object($expression);
10238
 
10239
    // Callback that returns true if the given value is an unserialized object, executes recursively.
10240
    $invalidvaluecallback = static function($value) use (&$invalidvaluecallback): bool {
10241
        if (is_array($value)) {
10242
            return (bool) array_filter($value, $invalidvaluecallback);
10243
        }
10244
        return ($value instanceof stdClass) || ($value instanceof __PHP_Incomplete_Class);
10245
    };
10246
 
10247
    // Iterate over the result to ensure there are no stray objects.
10248
    if (array_filter($values, $invalidvaluecallback)) {
10249
        return false;
10250
    }
10251
 
10252
    return $values;
10253
}
10254
 
10255
/**
10256
 * Safe method for unserializing given input that is expected to contain only a serialized instance of an stdClass object
10257
 *
10258
 * If any class type other than stdClass is included in the input string, it will not be instantiated and will be cast to an
10259
 * stdClass object. The initial cast to array, then back to object is to ensure we are always returning the correct type,
10260
 * otherwise we would return an instances of {@see __PHP_Incomplete_class} for malformed strings
10261
 *
10262
 * @param string $input
10263
 * @return stdClass
10264
 */
10265
function unserialize_object(string $input): stdClass {
10266
    $instance = (array) unserialize($input, ['allowed_classes' => [stdClass::class]]);
10267
    return (object) $instance;
10268
}
10269
 
10270
/**
10271
 * The lang_string class
10272
 *
10273
 * This special class is used to create an object representation of a string request.
10274
 * It is special because processing doesn't occur until the object is first used.
10275
 * The class was created especially to aid performance in areas where strings were
10276
 * required to be generated but were not necessarily used.
10277
 * As an example the admin tree when generated uses over 1500 strings, of which
10278
 * normally only 1/3 are ever actually printed at any time.
10279
 * The performance advantage is achieved by not actually processing strings that
10280
 * arn't being used, as such reducing the processing required for the page.
10281
 *
10282
 * How to use the lang_string class?
10283
 *     There are two methods of using the lang_string class, first through the
10284
 *     forth argument of the get_string function, and secondly directly.
10285
 *     The following are examples of both.
10286
 * 1. Through get_string calls e.g.
10287
 *     $string = get_string($identifier, $component, $a, true);
10288
 *     $string = get_string('yes', 'moodle', null, true);
10289
 * 2. Direct instantiation
10290
 *     $string = new lang_string($identifier, $component, $a, $lang);
10291
 *     $string = new lang_string('yes');
10292
 *
10293
 * How do I use a lang_string object?
10294
 *     The lang_string object makes use of a magic __toString method so that you
10295
 *     are able to use the object exactly as you would use a string in most cases.
10296
 *     This means you are able to collect it into a variable and then directly
10297
 *     echo it, or concatenate it into another string, or similar.
10298
 *     The other thing you can do is manually get the string by calling the
10299
 *     lang_strings out method e.g.
10300
 *         $string = new lang_string('yes');
10301
 *         $string->out();
10302
 *     Also worth noting is that the out method can take one argument, $lang which
10303
 *     allows the developer to change the language on the fly.
10304
 *
10305
 * When should I use a lang_string object?
10306
 *     The lang_string object is designed to be used in any situation where a
10307
 *     string may not be needed, but needs to be generated.
10308
 *     The admin tree is a good example of where lang_string objects should be
10309
 *     used.
10310
 *     A more practical example would be any class that requries strings that may
10311
 *     not be printed (after all classes get renderer by renderers and who knows
10312
 *     what they will do ;))
10313
 *
10314
 * When should I not use a lang_string object?
10315
 *     Don't use lang_strings when you are going to use a string immediately.
10316
 *     There is no need as it will be processed immediately and there will be no
10317
 *     advantage, and in fact perhaps a negative hit as a class has to be
10318
 *     instantiated for a lang_string object, however get_string won't require
10319
 *     that.
10320
 *
10321
 * Limitations:
10322
 * 1. You cannot use a lang_string object as an array offset. Doing so will
10323
 *     result in PHP throwing an error. (You can use it as an object property!)
10324
 *
10325
 * @package    core
10326
 * @category   string
10327
 * @copyright  2011 Sam Hemelryk
10328
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
10329
 */
10330
class lang_string {
10331
 
10332
    /** @var string The strings identifier */
10333
    protected $identifier;
10334
    /** @var string The strings component. Default '' */
10335
    protected $component = '';
10336
    /** @var array|stdClass Any arguments required for the string. Default null */
10337
    protected $a = null;
10338
    /** @var string The language to use when processing the string. Default null */
10339
    protected $lang = null;
10340
 
10341
    /** @var string The processed string (once processed) */
10342
    protected $string = null;
10343
 
10344
    /**
10345
     * A special boolean. If set to true then the object has been woken up and
10346
     * cannot be regenerated. If this is set then $this->string MUST be used.
10347
     * @var bool
10348
     */
10349
    protected $forcedstring = false;
10350
 
10351
    /**
10352
     * Constructs a lang_string object
10353
     *
10354
     * This function should do as little processing as possible to ensure the best
10355
     * performance for strings that won't be used.
10356
     *
10357
     * @param string $identifier The strings identifier
10358
     * @param string $component The strings component
10359
     * @param stdClass|array|mixed $a Any arguments the string requires
10360
     * @param string $lang The language to use when processing the string.
10361
     * @throws coding_exception
10362
     */
10363
    public function __construct($identifier, $component = '', $a = null, $lang = null) {
10364
        if (empty($component)) {
10365
            $component = 'moodle';
10366
        }
10367
 
10368
        $this->identifier = $identifier;
10369
        $this->component = $component;
10370
        $this->lang = $lang;
10371
 
10372
        // We MUST duplicate $a to ensure that it if it changes by reference those
10373
        // changes are not carried across.
10374
        // To do this we always ensure $a or its properties/values are strings
10375
        // and that any properties/values that arn't convertable are forgotten.
10376
        if ($a !== null) {
10377
            if (is_scalar($a)) {
10378
                $this->a = $a;
10379
            } else if ($a instanceof lang_string) {
10380
                $this->a = $a->out();
10381
            } else if (is_object($a) or is_array($a)) {
10382
                $a = (array)$a;
10383
                $this->a = array();
10384
                foreach ($a as $key => $value) {
10385
                    // Make sure conversion errors don't get displayed (results in '').
10386
                    if (is_array($value)) {
10387
                        $this->a[$key] = '';
10388
                    } else if (is_object($value)) {
10389
                        if (method_exists($value, '__toString')) {
10390
                            $this->a[$key] = $value->__toString();
10391
                        } else {
10392
                            $this->a[$key] = '';
10393
                        }
10394
                    } else {
10395
                        $this->a[$key] = (string)$value;
10396
                    }
10397
                }
10398
            }
10399
        }
10400
 
10401
        if (debugging(false, DEBUG_DEVELOPER)) {
10402
            if (clean_param($this->identifier, PARAM_STRINGID) == '') {
10403
                throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please check your string definition');
10404
            }
10405
            if (!empty($this->component) && clean_param($this->component, PARAM_COMPONENT) == '') {
10406
                throw new coding_exception('Invalid string compontent. Please check your string definition');
10407
            }
10408
            if (!get_string_manager()->string_exists($this->identifier, $this->component)) {
10409
                debugging('String does not exist. Please check your string definition for '.$this->identifier.'/'.$this->component, DEBUG_DEVELOPER);
10410
            }
10411
        }
10412
    }
10413
 
10414
    /**
10415
     * Processes the string.
10416
     *
10417
     * This function actually processes the string, stores it in the string property
10418
     * and then returns it.
10419
     * You will notice that this function is VERY similar to the get_string method.
10420
     * That is because it is pretty much doing the same thing.
10421
     * However as this function is an upgrade it isn't as tolerant to backwards
10422
     * compatibility.
10423
     *
10424
     * @return string
10425
     * @throws coding_exception
10426
     */
10427
    protected function get_string() {
10428
        global $CFG;
10429
 
10430
        // Check if we need to process the string.
10431
        if ($this->string === null) {
10432
            // Check the quality of the identifier.
10433
            if ($CFG->debugdeveloper && clean_param($this->identifier, PARAM_STRINGID) === '') {
10434
                throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please check your string definition', DEBUG_DEVELOPER);
10435
            }
10436
 
10437
            // Process the string.
10438
            $this->string = get_string_manager()->get_string($this->identifier, $this->component, $this->a, $this->lang);
10439
            // Debugging feature lets you display string identifier and component.
10440
            if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
10441
                $this->string .= ' {' . $this->identifier . '/' . $this->component . '}';
10442
            }
10443
        }
10444
        // Return the string.
10445
        return $this->string;
10446
    }
10447
 
10448
    /**
10449
     * Returns the string
10450
     *
10451
     * @param string $lang The langauge to use when processing the string
10452
     * @return string
10453
     */
10454
    public function out($lang = null) {
10455
        if ($lang !== null && $lang != $this->lang && ($this->lang == null && $lang != current_language())) {
10456
            if ($this->forcedstring) {
10457
                debugging('lang_string objects that have been used cannot be printed in another language. ('.$this->lang.' used)', DEBUG_DEVELOPER);
10458
                return $this->get_string();
10459
            }
10460
            $translatedstring = new lang_string($this->identifier, $this->component, $this->a, $lang);
10461
            return $translatedstring->out();
10462
        }
10463
        return $this->get_string();
10464
    }
10465
 
10466
    /**
10467
     * Magic __toString method for printing a string
10468
     *
10469
     * @return string
10470
     */
10471
    public function __toString() {
10472
        return $this->get_string();
10473
    }
10474
 
10475
    /**
10476
     * Magic __set_state method used for var_export
10477
     *
10478
     * @param array $array
10479
     * @return self
10480
     */
10481
    public static function __set_state(array $array): self {
10482
        $tmp = new lang_string($array['identifier'], $array['component'], $array['a'], $array['lang']);
10483
        $tmp->string = $array['string'];
10484
        $tmp->forcedstring = $array['forcedstring'];
10485
        return $tmp;
10486
    }
10487
 
10488
    /**
10489
     * Prepares the lang_string for sleep and stores only the forcedstring and
10490
     * string properties... the string cannot be regenerated so we need to ensure
10491
     * it is generated for this.
10492
     *
10493
     * @return array
10494
     */
10495
    public function __sleep() {
10496
        $this->get_string();
10497
        $this->forcedstring = true;
10498
        return array('forcedstring', 'string', 'lang');
10499
    }
10500
 
10501
    /**
10502
     * Returns the identifier.
10503
     *
10504
     * @return string
10505
     */
10506
    public function get_identifier() {
10507
        return $this->identifier;
10508
    }
10509
 
10510
    /**
10511
     * Returns the component.
10512
     *
10513
     * @return string
10514
     */
10515
    public function get_component() {
10516
        return $this->component;
10517
    }
10518
}
10519
 
10520
/**
10521
 * Get human readable name describing the given callable.
10522
 *
10523
 * This performs syntax check only to see if the given param looks like a valid function, method or closure.
10524
 * It does not check if the callable actually exists.
10525
 *
10526
 * @param callable|string|array $callable
10527
 * @return string|bool Human readable name of callable, or false if not a valid callable.
10528
 */
10529
function get_callable_name($callable) {
10530
 
10531
    if (!is_callable($callable, true, $name)) {
10532
        return false;
10533
 
10534
    } else {
10535
        return $name;
10536
    }
10537
}
10538
 
10539
/**
10540
 * Tries to guess if $CFG->wwwroot is publicly accessible or not.
10541
 * Never put your faith on this function and rely on its accuracy as there might be false positives.
10542
 * It just performs some simple checks, and mainly is used for places where we want to hide some options
10543
 * such as site registration when $CFG->wwwroot is not publicly accessible.
10544
 * Good thing is there is no false negative.
10545
 * Note that it's possible to force the result of this check by specifying $CFG->site_is_public in config.php
10546
 *
10547
 * @return bool
10548
 */
10549
function site_is_public() {
10550
    global $CFG;
10551
 
10552
    // Return early if site admin has forced this setting.
10553
    if (isset($CFG->site_is_public)) {
10554
        return (bool)$CFG->site_is_public;
10555
    }
10556
 
10557
    $host = parse_url($CFG->wwwroot, PHP_URL_HOST);
10558
 
10559
    if ($host === 'localhost' || preg_match('|^127\.\d+\.\d+\.\d+$|', $host)) {
10560
        $ispublic = false;
10561
    } else if (\core\ip_utils::is_ip_address($host) && !ip_is_public($host)) {
10562
        $ispublic = false;
10563
    } else if (($address = \core\ip_utils::get_ip_address($host)) && !ip_is_public($address)) {
10564
        $ispublic = false;
10565
    } else {
10566
        $ispublic = true;
10567
    }
10568
 
10569
    return $ispublic;
10570
}
10571
 
10572
/**
10573
 * Validates user's password length.
10574
 *
10575
 * @param string $password
10576
 * @param int $pepperlength The length of the used peppers
10577
 * @return bool
10578
 */
10579
function exceeds_password_length(string $password, int $pepperlength = 0): bool {
10580
    return (strlen($password) > (MAX_PASSWORD_CHARACTERS + $pepperlength));
10581
}
10582
 
10583
/**
10584
 * A helper to replace PHP 8.3 usage of array_keys with two args.
10585
 *
10586
 * There is an indication that this will become a new method in PHP 8.4, but that has not happened yet.
10587
 * Therefore this non-polyfill has been created with a different naming convention.
10588
 * In the future it can be deprecated if a core PHP method is created.
10589
 *
10590
 * https://wiki.php.net/rfc/deprecate_functions_with_overloaded_signatures#array_keys
10591
 *
10592
 * @param array $array
10593
 * @param mixed $filter The value to filter on
10594
 * @param bool $strict Whether to apply a strit test with the filter
10595
 * @return array
10596
 */
10597
function moodle_array_keys_filter(array $array, mixed $filter, bool $strict = false): array {
10598
    return array_keys(array_filter(
10599
        $array,
10600
        function($value, $key) use ($filter, $strict): bool {
10601
            if ($strict) {
10602
                return $value === $filter;
10603
            }
10604
            return $value == $filter;
10605
        },
10606
        ARRAY_FILTER_USE_BOTH,
10607
    ));
10608
}