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
31defined('MOODLE_INTERNAL') || die();
32
33// CONSTANTS (Encased in phpdoc proper comments).
34
35// Date and time constants.
36/**
37 * Time constant - the number of seconds in a year
38 */
39define('YEARSECS', 31536000);
40
41/**
42 * Time constant - the number of seconds in a week
43 */
44define('WEEKSECS', 604800);
45
46/**
47 * Time constant - the number of seconds in a day
48 */
49define('DAYSECS', 86400);
50
51/**
52 * Time constant - the number of seconds in an hour
53 */
54define('HOURSECS', 3600);
55
56/**
57 * Time constant - the number of seconds in a minute
58 */
59define('MINSECS', 60);
60
61/**
62 * Time constant - the number of minutes in a day
63 */
64define('DAYMINS', 1440);
65
66/**
67 * Time constant - the number of minutes in an hour
68 */
69define('HOURMINS', 60);
70
71// Parameter constants - every call to optional_param(), required_param()
72// or clean_param() should have a specified type of parameter.
73
74/**
75 * PARAM_ALPHA - contains only English ascii letters [a-zA-Z].
76 */
77define('PARAM_ALPHA',    'alpha');
78
79/**
80 * PARAM_ALPHAEXT the same contents as PARAM_ALPHA (English ascii letters [a-zA-Z]) plus the chars in quotes: "_-" allowed
81 * NOTE: originally this allowed "/" too, please use PARAM_SAFEPATH if "/" needed
82 */
83define('PARAM_ALPHAEXT', 'alphaext');
84
85/**
86 * PARAM_ALPHANUM - expected numbers 0-9 and English ascii letters [a-zA-Z] only.
87 */
88define('PARAM_ALPHANUM', 'alphanum');
89
90/**
91 * PARAM_ALPHANUMEXT - expected numbers 0-9, letters (English ascii letters [a-zA-Z]) and _- only.
92 */
93define('PARAM_ALPHANUMEXT', 'alphanumext');
94
95/**
96 * PARAM_AUTH - actually checks to make sure the string is a valid auth plugin
97 */
98define('PARAM_AUTH',  'auth');
99
100/**
101 * PARAM_BASE64 - Base 64 encoded format
102 */
103define('PARAM_BASE64',   'base64');
104
105/**
106 * PARAM_BOOL - converts input into 0 or 1, use for switches in forms and urls.
107 */
108define('PARAM_BOOL',     'bool');
109
110/**
111 * PARAM_CAPABILITY - A capability name, like 'moodle/role:manage'. Actually
112 * checked against the list of capabilities in the database.
113 */
114define('PARAM_CAPABILITY',   'capability');
115
116/**
117 * PARAM_CLEANHTML - cleans submitted HTML code. Note that you almost never want
118 * to use this. The normal mode of operation is to use PARAM_RAW when receiving
119 * the input (required/optional_param or formslib) and then sanitise the HTML
120 * using format_text on output. This is for the rare cases when you want to
121 * sanitise the HTML on input. This cleaning may also fix xhtml strictness.
122 */
123define('PARAM_CLEANHTML', 'cleanhtml');
124
125/**
126 * PARAM_EMAIL - an email address following the RFC
127 */
128define('PARAM_EMAIL',   'email');
129
130/**
131 * PARAM_FILE - safe file name, all dangerous chars are stripped, protects against XSS, SQL injections and directory traversals
132 */
133define('PARAM_FILE',   'file');
134
135/**
136 * PARAM_FLOAT - a real/floating point number.
137 *
138 * Note that you should not use PARAM_FLOAT for numbers typed in by the user.
139 * It does not work for languages that use , as a decimal separator.
140 * Use PARAM_LOCALISEDFLOAT instead.
141 */
142define('PARAM_FLOAT',  'float');
143
144/**
145 * PARAM_LOCALISEDFLOAT - a localised real/floating point number.
146 * This is preferred over PARAM_FLOAT for numbers typed in by the user.
147 * Cleans localised numbers to computer readable numbers; false for invalid numbers.
148 */
149define('PARAM_LOCALISEDFLOAT',  'localisedfloat');
150
151/**
152 * PARAM_HOST - expected fully qualified domain name (FQDN) or an IPv4 dotted quad (IP address)
153 */
154define('PARAM_HOST',     'host');
155
156/**
157 * PARAM_INT - integers only, use when expecting only numbers.
158 */
159define('PARAM_INT',      'int');
160
161/**
162 * PARAM_LANG - checks to see if the string is a valid installed language in the current site.
163 */
164define('PARAM_LANG',  'lang');
165
166/**
167 * PARAM_LOCALURL - expected properly formatted URL as well as one that refers to the local server itself. (NOT orthogonal to the
168 * others! Implies PARAM_URL!)
169 */
170define('PARAM_LOCALURL', 'localurl');
171
172/**
173 * PARAM_NOTAGS - all html tags are stripped from the text. Do not abuse this type.
174 */
175define('PARAM_NOTAGS',   'notags');
176
177/**
178 * PARAM_PATH - safe relative path name, all dangerous chars are stripped, protects against XSS, SQL injections and directory
179 * traversals note: the leading slash is not removed, window drive letter is not allowed
180 */
181define('PARAM_PATH',     'path');
182
183/**
184 * PARAM_PEM - Privacy Enhanced Mail format
185 */
186define('PARAM_PEM',      'pem');
187
188/**
189 * PARAM_PERMISSION - A permission, one of CAP_INHERIT, CAP_ALLOW, CAP_PREVENT or CAP_PROHIBIT.
190 */
191define('PARAM_PERMISSION',   'permission');
192
193/**
194 * PARAM_RAW specifies a parameter that is not cleaned/processed in any way except the discarding of the invalid utf-8 characters
195 */
196define('PARAM_RAW', 'raw');
197
198/**
199 * PARAM_RAW_TRIMMED like PARAM_RAW but leading and trailing whitespace is stripped.
200 */
201define('PARAM_RAW_TRIMMED', 'raw_trimmed');
202
203/**
204 * PARAM_SAFEDIR - safe directory name, suitable for include() and require()
205 */
206define('PARAM_SAFEDIR',  'safedir');
207
208/**
209 * PARAM_SAFEPATH - several PARAM_SAFEDIR joined by "/", suitable for include() and require(), plugin paths, etc.
210 */
211define('PARAM_SAFEPATH',  'safepath');
212
213/**
214 * PARAM_SEQUENCE - expects a sequence of numbers like 8 to 1,5,6,4,6,8,9.  Numbers and comma only.
215 */
216define('PARAM_SEQUENCE',  'sequence');
217
218/**
219 * PARAM_TAG - one tag (interests, blogs, etc.) - mostly international characters and space, <> not supported
220 */
221define('PARAM_TAG',   'tag');
222
223/**
224 * PARAM_TAGLIST - list of tags separated by commas (interests, blogs, etc.)
225 */
226define('PARAM_TAGLIST',   'taglist');
227
228/**
229 * PARAM_TEXT - general plain text compatible with multilang filter, no other html tags. Please note '<', or '>' are allowed here.
230 */
231define('PARAM_TEXT',  'text');
232
233/**
234 * PARAM_THEME - Checks to see if the string is a valid theme name in the current site
235 */
236define('PARAM_THEME',  'theme');
237
238/**
239 * PARAM_URL - expected properly formatted URL. Please note that domain part is required, http://localhost/ is not accepted but
240 * http://localhost.localdomain/ is ok.
241 */
242define('PARAM_URL',      'url');
243
244/**
245 * PARAM_USERNAME - Clean username to only contains allowed characters. This is to be used ONLY when manually creating user
246 * accounts, do NOT use when syncing with external systems!!
247 */
248define('PARAM_USERNAME',    'username');
249
250/**
251 * PARAM_STRINGID - used to check if the given string is valid string identifier for get_string()
252 */
253define('PARAM_STRINGID',    'stringid');
254
255// DEPRECATED PARAM TYPES OR ALIASES - DO NOT USE FOR NEW CODE.
256/**
257 * PARAM_CLEAN - obsoleted, please use a more specific type of parameter.
258 * It was one of the first types, that is why it is abused so much ;-)
259 * @deprecated since 2.0
260 */
261define('PARAM_CLEAN',    'clean');
262
263/**
264 * PARAM_INTEGER - deprecated alias for PARAM_INT
265 * @deprecated since 2.0
266 */
267define('PARAM_INTEGER',  'int');
268
269/**
270 * PARAM_NUMBER - deprecated alias of PARAM_FLOAT
271 * @deprecated since 2.0
272 */
273define('PARAM_NUMBER',  'float');
274
275/**
276 * PARAM_ACTION - deprecated alias for PARAM_ALPHANUMEXT, use for various actions in forms and urls
277 * NOTE: originally alias for PARAM_APLHA
278 * @deprecated since 2.0
279 */
280define('PARAM_ACTION',   'alphanumext');
281
282/**
283 * PARAM_FORMAT - deprecated alias for PARAM_ALPHANUMEXT, use for names of plugins, formats, etc.
284 * NOTE: originally alias for PARAM_APLHA
285 * @deprecated since 2.0
286 */
287define('PARAM_FORMAT',   'alphanumext');
288
289/**
290 * PARAM_MULTILANG - deprecated alias of PARAM_TEXT.
291 * @deprecated since 2.0
292 */
293define('PARAM_MULTILANG',  'text');
294
295/**
296 * PARAM_TIMEZONE - expected timezone. Timezone can be int +-(0-13) or float +-(0.5-12.5) or
297 * string separated by '/' and can have '-' &/ '_' (eg. America/North_Dakota/New_Salem
298 * America/Port-au-Prince)
299 */
300define('PARAM_TIMEZONE', 'timezone');
301
302/**
303 * PARAM_CLEANFILE - deprecated alias of PARAM_FILE; originally was removing regional chars too
304 */
305define('PARAM_CLEANFILE', 'file');
306
307/**
308 * PARAM_COMPONENT is used for full component names (aka frankenstyle) such as 'mod_forum', 'core_rating', 'auth_ldap'.
309 * Short legacy subsystem names and module names are accepted too ex: 'forum', 'rating', 'user'.
310 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
311 * NOTE: numbers and underscores are strongly discouraged in plugin names!
312 */
313define('PARAM_COMPONENT', 'component');
314
315/**
316 * PARAM_AREA is a name of area used when addressing files, comments, ratings, etc.
317 * It is usually used together with context id and component.
318 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
319 */
320define('PARAM_AREA', 'area');
321
322/**
323 * PARAM_PLUGIN is used for plugin names such as 'forum', 'glossary', 'ldap', 'paypal', 'completionstatus'.
324 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
325 * NOTE: numbers and underscores are strongly discouraged in plugin names! Underscores are forbidden in module names.
326 */
327define('PARAM_PLUGIN', 'plugin');
328
329
330// Web Services.
331
332/**
333 * VALUE_REQUIRED - if the parameter is not supplied, there is an error
334 */
335define('VALUE_REQUIRED', 1);
336
337/**
338 * VALUE_OPTIONAL - if the parameter is not supplied, then the param has no value
339 */
340define('VALUE_OPTIONAL', 2);
341
342/**
343 * VALUE_DEFAULT - if the parameter is not supplied, then the default value is used
344 */
345define('VALUE_DEFAULT', 0);
346
347/**
348 * NULL_NOT_ALLOWED - the parameter can not be set to null in the database
349 */
350define('NULL_NOT_ALLOWED', false);
351
352/**
353 * NULL_ALLOWED - the parameter can be set to null in the database
354 */
355define('NULL_ALLOWED', true);
356
357// Page types.
358
359/**
360 * PAGE_COURSE_VIEW is a definition of a page type. For more information on the page class see moodle/lib/pagelib.php.
361 */
362define('PAGE_COURSE_VIEW', 'course-view');
363
364/** Get remote addr constant */
365define('GETREMOTEADDR_SKIP_HTTP_CLIENT_IP', '1');
366/** Get remote addr constant */
367define('GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR', '2');
368/**
369 * GETREMOTEADDR_SKIP_DEFAULT defines the default behavior remote IP address validation.
370 */
371define('GETREMOTEADDR_SKIP_DEFAULT', GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR|GETREMOTEADDR_SKIP_HTTP_CLIENT_IP);
372
373// Blog access level constant declaration.
374define ('BLOG_USER_LEVEL', 1);
375define ('BLOG_GROUP_LEVEL', 2);
376define ('BLOG_COURSE_LEVEL', 3);
377define ('BLOG_SITE_LEVEL', 4);
378define ('BLOG_GLOBAL_LEVEL', 5);
379
380
381// Tag constants.
382/**
383 * To prevent problems with multibytes strings,Flag updating in nav not working on the review page. this should not exceed the
384 * length of "varchar(255) / 3 (bytes / utf-8 character) = 85".
385 * TODO: this is not correct, varchar(255) are 255 unicode chars ;-)
386 *
387 * @todo define(TAG_MAX_LENGTH) this is not correct, varchar(255) are 255 unicode chars ;-)
388 */
389define('TAG_MAX_LENGTH', 50);
390
391// Password policy constants.
392define ('PASSWORD_LOWER', 'abcdefghijklmnopqrstuvwxyz');
393define ('PASSWORD_UPPER', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
394define ('PASSWORD_DIGITS', '0123456789');
395define ('PASSWORD_NONALPHANUM', '.,;:!?_-+/*@#&$');
396
397// Feature constants.
398// Used for plugin_supports() to report features that are, or are not, supported by a module.
399
400/** True if module can provide a grade */
401define('FEATURE_GRADE_HAS_GRADE', 'grade_has_grade');
402/** True if module supports outcomes */
403define('FEATURE_GRADE_OUTCOMES', 'outcomes');
404/** True if module supports advanced grading methods */
405define('FEATURE_ADVANCED_GRADING', 'grade_advanced_grading');
406/** True if module controls the grade visibility over the gradebook */
407define('FEATURE_CONTROLS_GRADE_VISIBILITY', 'controlsgradevisbility');
408/** True if module supports plagiarism plugins */
409define('FEATURE_PLAGIARISM', 'plagiarism');
410
411/** True if module has code to track whether somebody viewed it */
412define('FEATURE_COMPLETION_TRACKS_VIEWS', 'completion_tracks_views');
413/** True if module has custom completion rules */
414define('FEATURE_COMPLETION_HAS_RULES', 'completion_has_rules');
415
416/** True if module has no 'view' page (like label) */
417define('FEATURE_NO_VIEW_LINK', 'viewlink');
418/** True (which is default) if the module wants support for setting the ID number for grade calculation purposes. */
419define('FEATURE_IDNUMBER', 'idnumber');
420/** True if module supports groups */
421define('FEATURE_GROUPS', 'groups');
422/** True if module supports groupings */
423define('FEATURE_GROUPINGS', 'groupings');
424/**
425 * True if module supports groupmembersonly (which no longer exists)
426 * @deprecated Since Moodle 2.8
427 */
428define('FEATURE_GROUPMEMBERSONLY', 'groupmembersonly');
429
430/** Type of module */
431define('FEATURE_MOD_ARCHETYPE', 'mod_archetype');
432/** True if module supports intro editor */
433define('FEATURE_MOD_INTRO', 'mod_intro');
434/** True if module has default completion */
435define('FEATURE_MODEDIT_DEFAULT_COMPLETION', 'modedit_default_completion');
436
437define('FEATURE_COMMENT', 'comment');
438
439define('FEATURE_RATE', 'rate');
440/** True if module supports backup/restore of moodle2 format */
441define('FEATURE_BACKUP_MOODLE2', 'backup_moodle2');
442
443/** True if module can show description on course main page */
444define('FEATURE_SHOW_DESCRIPTION', 'showdescription');
445
446/** True if module uses the question bank */
447define('FEATURE_USES_QUESTIONS', 'usesquestions');
448
449/**
450 * Maximum filename char size
451 */
452define('MAX_FILENAME_SIZE', 100);
453
454/** Unspecified module archetype */
455define('MOD_ARCHETYPE_OTHER', 0);
456/** Resource-like type module */
457define('MOD_ARCHETYPE_RESOURCE', 1);
458/** Assignment module archetype */
459define('MOD_ARCHETYPE_ASSIGNMENT', 2);
460/** System (not user-addable) module archetype */
461define('MOD_ARCHETYPE_SYSTEM', 3);
462
463/**
464 * Security token used for allowing access
465 * from external application such as web services.
466 * Scripts do not use any session, performance is relatively
467 * low because we need to load access info in each request.
468 * Scripts are executed in parallel.
469 */
470define('EXTERNAL_TOKEN_PERMANENT', 0);
471
472/**
473 * Security token used for allowing access
474 * of embedded applications, the code is executed in the
475 * active user session. Token is invalidated after user logs out.
476 * Scripts are executed serially - normal session locking is used.
477 */
478define('EXTERNAL_TOKEN_EMBEDDED', 1);
479
480/**
481 * The home page should be the site home
482 */
483define('HOMEPAGE_SITE', 0);
484/**
485 * The home page should be the users my page
486 */
487define('HOMEPAGE_MY', 1);
488/**
489 * The home page can be chosen by the user
490 */
491define('HOMEPAGE_USER', 2);
492
493/**
494 * URL of the Moodle sites registration portal.
495 */
496defined('HUB_MOODLEORGHUBURL') || define('HUB_MOODLEORGHUBURL', 'https://stats.moodle.org');
497
498/**
499 * Moodle mobile app service name
500 */
501define('MOODLE_OFFICIAL_MOBILE_SERVICE', 'moodle_mobile_app');
502
503/**
504 * Indicates the user has the capabilities required to ignore activity and course file size restrictions
505 */
506define('USER_CAN_IGNORE_FILE_SIZE_LIMITS', -1);
507
508/**
509 * Course display settings: display all sections on one page.
510 */
511define('COURSE_DISPLAY_SINGLEPAGE', 0);
512/**
513 * Course display settings: split pages into a page per section.
514 */
515define('COURSE_DISPLAY_MULTIPAGE', 1);
516
517/**
518 * Authentication constant: String used in password field when password is not stored.
519 */
520define('AUTH_PASSWORD_NOT_CACHED', 'not cached');
521
522/**
523 * Email from header to never include via information.
524 */
525define('EMAIL_VIA_NEVER', 0);
526
527/**
528 * Email from header to always include via information.
529 */
530define('EMAIL_VIA_ALWAYS', 1);
531
532/**
533 * Email from header to only include via information if the address is no-reply.
534 */
535define('EMAIL_VIA_NO_REPLY_ONLY', 2);
536
537// PARAMETER HANDLING.
538
539/**
540 * Returns a particular value for the named variable, taken from
541 * POST or GET.  If the parameter doesn't exist then an error is
542 * thrown because we require this variable.
543 *
544 * This function should be used to initialise all required values
545 * in a script that are based on parameters.  Usually it will be
546 * used like this:
547 *    $id = required_param('id', PARAM_INT);
548 *
549 * Please note the $type parameter is now required and the value can not be array.
550 *
551 * @param string $parname the name of the page parameter we want
552 * @param string $type expected type of parameter
553 * @return mixed
554 * @throws coding_exception
555 */
556function required_param($parname, $type) {
557    if (func_num_args() != 2 or empty($parname) or empty($type)) {
558        throw new coding_exception('required_param() requires $parname and $type to be specified (parameter: '.$parname.')');
559    }
560    // POST has precedence.
561    if (isset($_POST[$parname])) {
562        $param = $_POST[$parname];
563    } else if (isset($_GET[$parname])) {
564        $param = $_GET[$parname];
565    } else {
566        print_error('missingparam', '', '', $parname);
567    }
568
569    if (is_array($param)) {
570        debugging('Invalid array parameter detected in required_param(): '.$parname);
571        // TODO: switch to fatal error in Moodle 2.3.
572        return required_param_array($parname, $type);
573    }
574
575    return clean_param($param, $type);
576}
577
578/**
579 * Returns a particular array value for the named variable, taken from
580 * POST or GET.  If the parameter doesn't exist then an error is
581 * thrown because we require this variable.
582 *
583 * This function should be used to initialise all required values
584 * in a script that are based on parameters.  Usually it will be
585 * used like this:
586 *    $ids = required_param_array('ids', PARAM_INT);
587 *
588 *  Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
589 *
590 * @param string $parname the name of the page parameter we want
591 * @param string $type expected type of parameter
592 * @return array
593 * @throws coding_exception
594 */
595function required_param_array($parname, $type) {
596    if (func_num_args() != 2 or empty($parname) or empty($type)) {
597        throw new coding_exception('required_param_array() requires $parname and $type to be specified (parameter: '.$parname.')');
598    }
599    // POST has precedence.
600    if (isset($_POST[$parname])) {
601        $param = $_POST[$parname];
602    } else if (isset($_GET[$parname])) {
603        $param = $_GET[$parname];
604    } else {
605        print_error('missingparam', '', '', $parname);
606    }
607    if (!is_array($param)) {
608        print_error('missingparam', '', '', $parname);
609    }
610
611    $result = array();
612    foreach ($param as $key => $value) {
613        if (!preg_match('/^[a-z0-9_-]+$/i', $key)) {
614            debugging('Invalid key name in required_param_array() detected: '.$key.', parameter: '.$parname);
615            continue;
616        }
617        $result[$key] = clean_param($value, $type);
618    }
619
620    return $result;
621}
622
623/**
624 * Returns a particular value for the named variable, taken from
625 * POST or GET, otherwise returning a given default.
626 *
627 * This function should be used to initialise all optional values
628 * in a script that are based on parameters.  Usually it will be
629 * used like this:
630 *    $name = optional_param('name', 'Fred', PARAM_TEXT);
631 *
632 * Please note the $type parameter is now required and the value can not be array.
633 *
634 * @param string $parname the name of the page parameter we want
635 * @param mixed  $default the default value to return if nothing is found
636 * @param string $type expected type of parameter
637 * @return mixed
638 * @throws coding_exception
639 */
640function optional_param($parname, $default, $type) {
641    if (func_num_args() != 3 or empty($parname) or empty($type)) {
642        throw new coding_exception('optional_param requires $parname, $default + $type to be specified (parameter: '.$parname.')');
643    }
644
645    // POST has precedence.
646    if (isset($_POST[$parname])) {
647        $param = $_POST[$parname];
648    } else if (isset($_GET[$parname])) {
649        $param = $_GET[$parname];
650    } else {
651        return $default;
652    }
653
654    if (is_array($param)) {
655        debugging('Invalid array parameter detected in required_param(): '.$parname);
656        // TODO: switch to $default in Moodle 2.3.
657        return optional_param_array($parname, $default, $type);
658    }
659
660    return clean_param($param, $type);
661}
662
663/**
664 * Returns a particular array value for the named variable, taken from
665 * POST or GET, otherwise returning a given default.
666 *
667 * This function should be used to initialise all optional values
668 * in a script that are based on parameters.  Usually it will be
669 * used like this:
670 *    $ids = optional_param('id', array(), PARAM_INT);
671 *
672 * Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
673 *
674 * @param string $parname the name of the page parameter we want
675 * @param mixed $default the default value to return if nothing is found
676 * @param string $type expected type of parameter
677 * @return array
678 * @throws coding_exception
679 */
680function optional_param_array($parname, $default, $type) {
681    if (func_num_args() != 3 or empty($parname) or empty($type)) {
682        throw new coding_exception('optional_param_array requires $parname, $default + $type to be specified (parameter: '.$parname.')');
683    }
684
685    // POST has precedence.
686    if (isset($_POST[$parname])) {
687        $param = $_POST[$parname];
688    } else if (isset($_GET[$parname])) {
689        $param = $_GET[$parname];
690    } else {
691        return $default;
692    }
693    if (!is_array($param)) {
694        debugging('optional_param_array() expects array parameters only: '.$parname);
695        return $default;
696    }
697
698    $result = array();
699    foreach ($param as $key => $value) {
700        if (!preg_match('/^[a-z0-9_-]+$/i', $key)) {
701            debugging('Invalid key name in optional_param_array() detected: '.$key.', parameter: '.$parname);
702            continue;
703        }
704        $result[$key] = clean_param($value, $type);
705    }
706
707    return $result;
708}
709
710/**
711 * Strict validation of parameter values, the values are only converted
712 * to requested PHP type. Internally it is using clean_param, the values
713 * before and after cleaning must be equal - otherwise
714 * an invalid_parameter_exception is thrown.
715 * Objects and classes are not accepted.
716 *
717 * @param mixed $param
718 * @param string $type PARAM_ constant
719 * @param bool $allownull are nulls valid value?
720 * @param string $debuginfo optional debug information
721 * @return mixed the $param value converted to PHP type
722 * @throws invalid_parameter_exception if $param is not of given type
723 */
724function validate_param($param, $type, $allownull=NULL_NOT_ALLOWED, $debuginfo='') {
725    if (is_null($param)) {
726        if ($allownull == NULL_ALLOWED) {
727            return null;
728        } else {
729            throw new invalid_parameter_exception($debuginfo);
730        }
731    }
732    if (is_array($param) or is_object($param)) {
733        throw new invalid_parameter_exception($debuginfo);
734    }
735
736    $cleaned = clean_param($param, $type);
737
738    if ($type == PARAM_FLOAT) {
739        // Do not detect precision loss here.
740        if (is_float($param) or is_int($param)) {
741            // These always fit.
742        } else if (!is_numeric($param) or !preg_match('/^[\+-]?[0-9]*\.?[0-9]*(e[-+]?[0-9]+)?$/i', (string)$param)) {
743            throw new invalid_parameter_exception($debuginfo);
744        }
745    } else if ((string)$param !== (string)$cleaned) {
746        // Conversion to string is usually lossless.
747        throw new invalid_parameter_exception($debuginfo);
748    }
749
750    return $cleaned;
751}
752
753/**
754 * Makes sure array contains only the allowed types, this function does not validate array key names!
755 *
756 * <code>
757 * $options = clean_param($options, PARAM_INT);
758 * </code>
759 *
760 * @param array $param the variable array we are cleaning
761 * @param string $type expected format of param after cleaning.
762 * @param bool $recursive clean recursive arrays
763 * @return array
764 * @throws coding_exception
765 */
766function clean_param_array(array $param = null, $type, $recursive = false) {
767    // Convert null to empty array.
768    $param = (array)$param;
769    foreach ($param as $key => $value) {
770        if (is_array($value)) {
771            if ($recursive) {
772                $param[$key] = clean_param_array($value, $type, true);
773            } else {
774                throw new coding_exception('clean_param_array can not process multidimensional arrays when $recursive is false.');
775            }
776        } else {
777            $param[$key] = clean_param($value, $type);
778        }
779    }
780    return $param;
781}
782
783/**
784 * Used by {@link optional_param()} and {@link required_param()} to
785 * clean the variables and/or cast to specific types, based on
786 * an options field.
787 * <code>
788 * $course->format = clean_param($course->format, PARAM_ALPHA);
789 * $selectedgradeitem = clean_param($selectedgradeitem, PARAM_INT);
790 * </code>
791 *
792 * @param mixed $param the variable we are cleaning
793 * @param string $type expected format of param after cleaning.
794 * @return mixed
795 * @throws coding_exception
796 */
797function clean_param($param, $type) {
798    global $CFG;
799
800    if (is_array($param)) {
801        throw new coding_exception('clean_param() can not process arrays, please use clean_param_array() instead.');
802    } else if (is_object($param)) {
803        if (method_exists($param, '__toString')) {
804            $param = $param->__toString();
805        } else {
806            throw new coding_exception('clean_param() can not process objects, please use clean_param_array() instead.');
807        }
808    }
809
810    switch ($type) {
811        case PARAM_RAW:
812            // No cleaning at all.
813            $param = fix_utf8($param);
814            return $param;
815
816        case PARAM_RAW_TRIMMED:
817            // No cleaning, but strip leading and trailing whitespace.
818            $param = fix_utf8($param);
819            return trim($param);
820
821        case PARAM_CLEAN:
822            // General HTML cleaning, try to use more specific type if possible this is deprecated!
823            // Please use more specific type instead.
824            if (is_numeric($param)) {
825                return $param;
826            }
827            $param = fix_utf8($param);
828            // Sweep for scripts, etc.
829            return clean_text($param);
830
831        case PARAM_CLEANHTML:
832            // Clean html fragment.
833            $param = fix_utf8($param);
834            // Sweep for scripts, etc.
835            $param = clean_text($param, FORMAT_HTML);
836            return trim($param);
837
838        case PARAM_INT:
839            // Convert to integer.
840            return (int)$param;
841
842        case PARAM_FLOAT:
843            // Convert to float.
844            return (float)$param;
845
846        case PARAM_LOCALISEDFLOAT:
847            // Convert to float.
848            return unformat_float($param, true);
849
850        case PARAM_ALPHA:
851            // Remove everything not `a-z`.
852            return preg_replace('/[^a-zA-Z]/i', '', $param);
853
854        case PARAM_ALPHAEXT:
855            // Remove everything not `a-zA-Z_-` (originally allowed "/" too).
856            return preg_replace('/[^a-zA-Z_-]/i', '', $param);
857
858        case PARAM_ALPHANUM:
859            // Remove everything not `a-zA-Z0-9`.
860            return preg_replace('/[^A-Za-z0-9]/i', '', $param);
861
862        case PARAM_ALPHANUMEXT:
863            // Remove everything not `a-zA-Z0-9_-`.
864            return preg_replace('/[^A-Za-z0-9_-]/i', '', $param);
865
866        case PARAM_SEQUENCE:
867            // Remove everything not `0-9,`.
868            return preg_replace('/[^0-9,]/i', '', $param);
869
870        case PARAM_BOOL:
871            // Convert to 1 or 0.
872            $tempstr = strtolower($param);
873            if ($tempstr === 'on' or $tempstr === 'yes' or $tempstr === 'true') {
874                $param = 1;
875            } else if ($tempstr === 'off' or $tempstr === 'no'  or $tempstr === 'false') {
876                $param = 0;
877            } else {
878                $param = empty($param) ? 0 : 1;
879            }
880            return $param;
881
882        case PARAM_NOTAGS:
883            // Strip all tags.
884            $param = fix_utf8($param);
885            return strip_tags($param);
886
887        case PARAM_TEXT:
888            // Leave only tags needed for multilang.
889            $param = fix_utf8($param);
890            // If the multilang syntax is not correct we strip all tags because it would break xhtml strict which is required
891            // for accessibility standards please note this cleaning does not strip unbalanced '>' for BC compatibility reasons.
892            do {
893                if (strpos($param, '</lang>') !== false) {
894                    // Old and future mutilang syntax.
895                    $param = strip_tags($param, '<lang>');
896                    if (!preg_match_all('/<.*>/suU', $param, $matches)) {
897                        break;
898                    }
899                    $open = false;
900                    foreach ($matches[0] as $match) {
901                        if ($match === '</lang>') {
902                            if ($open) {
903                                $open = false;
904                                continue;
905                            } else {
906                                break 2;
907                            }
908                        }
909                        if (!preg_match('/^<lang lang="[a-zA-Z0-9_-]+"\s*>$/u', $match)) {
910                            break 2;
911                        } else {
912                            $open = true;
913                        }
914                    }
915                    if ($open) {
916                        break;
917                    }
918                    return $param;
919
920                } else if (strpos($param, '</span>') !== false) {
921                    // Current problematic multilang syntax.
922                    $param = strip_tags($param, '<span>');
923                    if (!preg_match_all('/<.*>/suU', $param, $matches)) {
924                        break;
925                    }
926                    $open = false;
927                    foreach ($matches[0] as $match) {
928                        if ($match === '</span>') {
929                            if ($open) {
930                                $open = false;
931                                continue;
932                            } else {
933                                break 2;
934                            }
935                        }
936                        if (!preg_match('/^<span(\s+lang="[a-zA-Z0-9_-]+"|\s+class="multilang"){2}\s*>$/u', $match)) {
937                            break 2;
938                        } else {
939                            $open = true;
940                        }
941                    }
942                    if ($open) {
943                        break;
944                    }
945                    return $param;
946                }
947            } while (false);
948            // Easy, just strip all tags, if we ever want to fix orphaned '&' we have to do that in format_string().
949            return strip_tags($param);
950
951        case PARAM_COMPONENT:
952            // We do not want any guessing here, either the name is correct or not
953            // please note only normalised component names are accepted.
954            if (!preg_match('/^[a-z][a-z0-9]*(_[a-z][a-z0-9_]*)?[a-z0-9]+$/', $param)) {
955                return '';
956            }
957            if (strpos($param, '__') !== false) {
958                return '';
959            }
960            if (strpos($param, 'mod_') === 0) {
961                // Module names must not contain underscores because we need to differentiate them from invalid plugin types.
962                if (substr_count($param, '_') != 1) {
963                    return '';
964                }
965            }
966            return $param;
967
968        case PARAM_PLUGIN:
969        case PARAM_AREA:
970            // We do not want any guessing here, either the name is correct or not.
971            if (!is_valid_plugin_name($param)) {
972                return '';
973            }
974            return $param;
975
976        case PARAM_SAFEDIR:
977            // Remove everything not a-zA-Z0-9_- .
978            return preg_replace('/[^a-zA-Z0-9_-]/i', '', $param);
979
980        case PARAM_SAFEPATH:
981            // Remove everything not a-zA-Z0-9/_- .
982            return preg_replace('/[^a-zA-Z0-9\/_-]/i', '', $param);
983
984        case PARAM_FILE:
985            // Strip all suspicious characters from filename.
986            $param = fix_utf8($param);
987            $param = preg_replace('~[[:cntrl:]]|[&<>"`\|\':\\\\/]~u', '', $param);
988            if ($param === '.' || $param === '..') {
989                $param = '';
990            }
991            return $param;
992
993        case PARAM_PATH:
994            // Strip all suspicious characters from file path.
995            $param = fix_utf8($param);
996            $param = str_replace('\\', '/', $param);
997
998            // Explode the path and clean each element using the PARAM_FILE rules.
999            $breadcrumb = explode('/', $param);
1000            foreach ($breadcrumb as $key => $crumb) {
1001                if ($crumb === '.' && $key === 0) {
1002                    // Special condition to allow for relative current path such as ./currentdirfile.txt.
1003                } else {
1004                    $crumb = clean_param($crumb, PARAM_FILE);
1005                }
1006                $breadcrumb[$key] = $crumb;
1007            }
1008            $param = implode('/', $breadcrumb);
1009
1010            // Remove multiple current path (./././) and multiple slashes (///).
1011            $param = preg_replace('~//+~', '/', $param);
1012            $param = preg_replace('~/(\./)+~', '/', $param);
1013            return $param;
1014
1015        case PARAM_HOST:
1016            // Allow FQDN or IPv4 dotted quad.
1017            $param = preg_replace('/[^\.\d\w-]/', '', $param );
1018            // Match ipv4 dotted quad.
1019            if (preg_match('/(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/', $param, $match)) {
1020                // Confirm values are ok.
1021                if ( $match[0] > 255
1022                     || $match[1] > 255
1023                     || $match[3] > 255
1024                     || $match[4] > 255 ) {
1025                    // Hmmm, what kind of dotted quad is this?
1026                    $param = '';
1027                }
1028            } else if ( preg_match('/^[\w\d\.-]+$/', $param) // Dots, hyphens, numbers.
1029                       && !preg_match('/^[\.-]/',  $param) // No leading dots/hyphens.
1030                       && !preg_match('/[\.-]$/',  $param) // No trailing dots/hyphens.
1031                       ) {
1032                // All is ok - $param is respected.
1033            } else {
1034                // All is not ok...
1035                $param='';
1036            }
1037            return $param;
1038
1039        case PARAM_URL:
1040            // Allow safe urls.
1041            $param = fix_utf8($param);
1042            include_once($CFG->dirroot . '/lib/validateurlsyntax.php');
1043            if (!empty($param) && validateUrlSyntax($param, 's?H?S?F?E-u-P-a?I?p?f?q?r?')) {
1044                // All is ok, param is respected.
1045            } else {
1046                // Not really ok.
1047                $param ='';
1048            }
1049            return $param;
1050
1051        case PARAM_LOCALURL:
1052            // Allow http absolute, root relative and relative URLs within wwwroot.
1053            $param = clean_param($param, PARAM_URL);
1054            if (!empty($param)) {
1055
1056                if ($param === $CFG->wwwroot) {
1057                    // Exact match;
1058                } else if (preg_match(':^/:', $param)) {
1059                    // Root-relative, ok!
1060                } else if (preg_match('/^' . preg_quote($CFG->wwwroot . '/', '/') . '/i', $param)) {
1061                    // Absolute, and matches our wwwroot.
1062                } else {
1063                    // Relative - let's make sure there are no tricks.
1064                    if (validateUrlSyntax('/' . $param, 's-u-P-a-p-f+q?r?')) {
1065                        // Looks ok.
1066                    } else {
1067                        $param = '';
1068                    }
1069                }
1070            }
1071            return $param;
1072
1073        case PARAM_PEM:
1074            $param = trim($param);
1075            // PEM formatted strings may contain letters/numbers and the symbols:
1076            //   forward slash: /
1077            //   plus sign:     +
1078            //   equal sign:    =
1079            //   , surrounded by BEGIN and END CERTIFICATE prefix and suffixes.
1080            if (preg_match('/^-----BEGIN CERTIFICATE-----([\s\w\/\+=]+)-----END CERTIFICATE-----$/', trim($param), $matches)) {
1081                list($wholething, $body) = $matches;
1082                unset($wholething, $matches);
1083                $b64 = clean_param($body, PARAM_BASE64);
1084                if (!empty($b64)) {
1085                    return "-----BEGIN CERTIFICATE-----\n$b64\n-----END CERTIFICATE-----\n";
1086                } else {
1087                    return '';
1088                }
1089            }
1090            return '';
1091
1092        case PARAM_BASE64:
1093            if (!empty($param)) {
1094                // PEM formatted strings may contain letters/numbers and the symbols
1095                //   forward slash: /
1096                //   plus sign:     +
1097                //   equal sign:    =.
1098                if (0 >= preg_match('/^([\s\w\/\+=]+)$/', trim($param))) {
1099                    return '';
1100                }
1101                $lines = preg_split('/[\s]+/', $param, -1, PREG_SPLIT_NO_EMPTY);
1102                // Each line of base64 encoded data must be 64 characters in length, except for the last line which may be less
1103                // than (or equal to) 64 characters long.
1104                for ($i=0, $j=count($lines); $i < $j; $i++) {
1105                    if ($i + 1 == $j) {
1106                        if (64 < strlen($lines[$i])) {
1107                            return '';
1108                        }
1109                        continue;
1110                    }
1111
1112                    if (64 != strlen($lines[$i])) {
1113                        return '';
1114                    }
1115                }
1116                return implode("\n", $lines);
1117            } else {
1118                return '';
1119            }
1120
1121        case PARAM_TAG:
1122            $param = fix_utf8($param);
1123            // Please note it is not safe to use the tag name directly anywhere,
1124            // it must be processed with s(), urlencode() before embedding anywhere.
1125            // Remove some nasties.
1126            $param = preg_replace('~[[:cntrl:]]|[<>`]~u', '', $param);
1127            // Convert many whitespace chars into one.
1128            $param = preg_replace('/\s+/u', ' ', $param);
1129            $param = core_text::substr(trim($param), 0, TAG_MAX_LENGTH);
1130            return $param;
1131
1132        case PARAM_TAGLIST:
1133            $param = fix_utf8($param);
1134            $tags = explode(',', $param);
1135            $result = array();
1136            foreach ($tags as $tag) {
1137                $res = clean_param($tag, PARAM_TAG);
1138                if ($res !== '') {
1139                    $result[] = $res;
1140                }
1141            }
1142            if ($result) {
1143                return implode(',', $result);
1144            } else {
1145                return '';
1146            }
1147
1148        case PARAM_CAPABILITY:
1149            if (get_capability_info($param)) {
1150                return $param;
1151            } else {
1152                return '';
1153            }
1154
1155        case PARAM_PERMISSION:
1156            $param = (int)$param;
1157            if (in_array($param, array(CAP_INHERIT, CAP_ALLOW, CAP_PREVENT, CAP_PROHIBIT))) {
1158                return $param;
1159            } else {
1160                return CAP_INHERIT;
1161            }
1162
1163        case PARAM_AUTH:
1164            $param = clean_param($param, PARAM_PLUGIN);
1165            if (empty($param)) {
1166                return '';
1167            } else if (exists_auth_plugin($param)) {
1168                return $param;
1169            } else {
1170                return '';
1171            }
1172
1173        case PARAM_LANG:
1174            $param = clean_param($param, PARAM_SAFEDIR);
1175            if (get_string_manager()->translation_exists($param)) {
1176                return $param;
1177            } else {
1178                // Specified language is not installed or param malformed.
1179                return '';
1180            }
1181
1182        case PARAM_THEME:
1183            $param = clean_param($param, PARAM_PLUGIN);
1184            if (empty($param)) {
1185                return '';
1186            } else if (file_exists("$CFG->dirroot/theme/$param/config.php")) {
1187                return $param;
1188            } else if (!empty($CFG->themedir) and file_exists("$CFG->themedir/$param/config.php")) {
1189                return $param;
1190            } else {
1191                // Specified theme is not installed.
1192                return '';
1193            }
1194
1195        case PARAM_USERNAME:
1196            $param = fix_utf8($param);
1197            $param = trim($param);
1198            // Convert uppercase to lowercase MDL-16919.
1199            $param = core_text::strtolower($param);
1200            if (empty($CFG->extendedusernamechars)) {
1201                $param = str_replace(" " , "", $param);
1202                // Regular expression, eliminate all chars EXCEPT:
1203                // alphanum, dash (-), underscore (_), at sign (@) and period (.) characters.
1204                $param = preg_replace('/[^-\.@_a-z0-9]/', '', $param);
1205            }
1206            return $param;
1207
1208        case PARAM_EMAIL:
1209            $param = fix_utf8($param);
1210            if (validate_email($param)) {
1211                return $param;
1212            } else {
1213                return '';
1214            }
1215
1216        case PARAM_STRINGID:
1217            if (preg_match('|^[a-zA-Z][a-zA-Z0-9\.:/_-]*$|', $param)) {
1218                return $param;
1219            } else {
1220                return '';
1221            }
1222
1223        case PARAM_TIMEZONE:
1224            // Can be int, float(with .5 or .0) or string seperated by '/' and can have '-_'.
1225            $param = fix_utf8($param);
1226            $timezonepattern = '/^(([+-]?(0?[0-9](\.[5|0])?|1[0-3](\.0)?|1[0-2]\.5))|(99)|[[:alnum:]]+(\/?[[:alpha:]_-])+)$/';
1227            if (preg_match($timezonepattern, $param)) {
1228                return $param;
1229            } else {
1230                return '';
1231            }
1232
1233        default:
1234            // Doh! throw error, switched parameters in optional_param or another serious problem.
1235            print_error("unknownparamtype", '', '', $type);
1236    }
1237}
1238
1239/**
1240 * Whether the PARAM_* type is compatible in RTL.
1241 *
1242 * Being compatible with RTL means that the data they contain can flow
1243 * from right-to-left or left-to-right without compromising the user experience.
1244 *
1245 * Take URLs for example, they are not RTL compatible as they should always
1246 * flow from the left to the right. This also applies to numbers, email addresses,
1247 * configuration snippets, base64 strings, etc...
1248 *
1249 * This function tries to best guess which parameters can contain localised strings.
1250 *
1251 * @param string $paramtype Constant PARAM_*.
1252 * @return bool
1253 */
1254function is_rtl_compatible($paramtype) {
1255    return $paramtype == PARAM_TEXT || $paramtype == PARAM_NOTAGS;
1256}
1257
1258/**
1259 * Makes sure the data is using valid utf8, invalid characters are discarded.
1260 *
1261 * Note: this function is not intended for full objects with methods and private properties.
1262 *
1263 * @param mixed $value
1264 * @return mixed with proper utf-8 encoding
1265 */
1266function fix_utf8($value) {
1267    if (is_null($value) or $value === '') {
1268        return $value;
1269
1270    } else if (is_string($value)) {
1271        if ((string)(int)$value === $value) {
1272            // Shortcut.
1273            return $value;
1274        }
1275        // No null bytes expected in our data, so let's remove it.
1276        $value = str_replace("\0", '', $value);
1277
1278        // Note: this duplicates min_fix_utf8() intentionally.
1279        static $buggyiconv = null;
1280        if ($buggyiconv === null) {
1281            $buggyiconv = (!function_exists('iconv') or @iconv('UTF-8', 'UTF-8//IGNORE', '100'.chr(130).'€') !== '100€');
1282        }
1283
1284        if ($buggyiconv) {
1285            if (function_exists('mb_convert_encoding')) {
1286                $subst = mb_substitute_character();
1287                mb_substitute_character('none');
1288                $result = mb_convert_encoding($value, 'utf-8', 'utf-8');
1289                mb_substitute_character($subst);
1290
1291            } else {
1292                // Warn admins on admin/index.php page.
1293                $result = $value;
1294            }
1295
1296        } else {
1297            $result = @iconv('UTF-8', 'UTF-8//IGNORE', $value);
1298        }
1299
1300        return $result;
1301
1302    } else if (is_array($value)) {
1303        foreach ($value as $k => $v) {
1304            $value[$k] = fix_utf8($v);
1305        }
1306        return $value;
1307
1308    } else if (is_object($value)) {
1309        // Do not modify original.
1310        $value = clone($value);
1311        foreach ($value as $k => $v) {
1312            $value->$k = fix_utf8($v);
1313        }
1314        return $value;
1315
1316    } else {
1317        // This is some other type, no utf-8 here.
1318        return $value;
1319    }
1320}
1321
1322/**
1323 * Return true if given value is integer or string with integer value
1324 *
1325 * @param mixed $value String or Int
1326 * @return bool true if number, false if not
1327 */
1328function is_number($value) {
1329    if (is_int($value)) {
1330        return true;
1331    } else if (is_string($value)) {
1332        return ((string)(int)$value) === $value;
1333    } else {
1334        return false;
1335    }
1336}
1337
1338/**
1339 * Returns host part from url.
1340 *
1341 * @param string $url full url
1342 * @return string host, null if not found
1343 */
1344function get_host_from_url($url) {
1345    preg_match('|^[a-z]+://([a-zA-Z0-9-.]+)|i', $url, $matches);
1346    if ($matches) {
1347        return $matches[1];
1348    }
1349    return null;
1350}
1351
1352/**
1353 * Tests whether anything was returned by text editor
1354 *
1355 * This function is useful for testing whether something you got back from
1356 * the HTML editor actually contains anything. Sometimes the HTML editor
1357 * appear to be empty, but actually you get back a <br> tag or something.
1358 *
1359 * @param string $string a string containing HTML.
1360 * @return boolean does the string contain any actual content - that is text,
1361 * images, objects, etc.
1362 */
1363function html_is_blank($string) {
1364    return trim(strip_tags($string, '<img><object><applet><input><select><textarea><hr>')) == '';
1365}
1366
1367/**
1368 * Set a key in global configuration
1369 *
1370 * Set a key/value pair in both this session's {@link $CFG} global variable
1371 * and in the 'config' database table for future sessions.
1372 *
1373 * Can also be used to update keys for plugin-scoped configs in config_plugin table.
1374 * In that case it doesn't affect $CFG.
1375 *
1376 * A NULL value will delete the entry.
1377 *
1378 * NOTE: this function is called from lib/db/upgrade.php
1379 *
1380 * @param string $name the key to set
1381 * @param string $value the value to set (without magic quotes)
1382 * @param string $plugin (optional) the plugin scope, default null
1383 * @return bool true or exception
1384 */
1385function set_config($name, $value, $plugin=null) {
1386    global $CFG, $DB;
1387
1388    if (empty($plugin)) {
1389        if (!array_key_exists($name, $CFG->config_php_settings)) {
1390            // So it's defined for this invocation at least.
1391            if (is_null($value)) {
1392                unset($CFG->$name);
1393            } else {
1394                // Settings from db are always strings.
1395                $CFG->$name = (string)$value;
1396            }
1397        }
1398
1399        if ($DB->get_field('config', 'name', array('name' => $name))) {
1400            if ($value === null) {
1401                $DB->delete_records('config', array('name' => $name));
1402            } else {
1403                $DB->set_field('config', 'value', $value, array('name' => $name));
1404            }
1405        } else {
1406            if ($value !== null) {
1407                $config = new stdClass();
1408                $config->name  = $name;
1409                $config->value = $value;
1410                $DB->insert_record('config', $config, false);
1411            }
1412            // When setting config during a Behat test (in the CLI script, not in the web browser
1413            // requests), remember which ones are set so that we can clear them later.
1414            if (defined('BEHAT_TEST')) {
1415                if (!property_exists($CFG, 'behat_cli_added_config')) {
1416                    $CFG->behat_cli_added_config = [];
1417                }
1418                $CFG->behat_cli_added_config[$name] = true;
1419            }
1420        }
1421        if ($name === 'siteidentifier') {
1422            cache_helper::update_site_identifier($value);
1423        }
1424        cache_helper::invalidate_by_definition('core', 'config', array(), 'core');
1425    } else {
1426        // Plugin scope.
1427        if ($id = $DB->get_field('config_plugins', 'id', array('name' => $name, 'plugin' => $plugin))) {
1428            if ($value===null) {
1429                $DB->delete_records('config_plugins', array('name' => $name, 'plugin' => $plugin));
1430            } else {
1431                $DB->set_field('config_plugins', 'value', $value, array('id' => $id));
1432            }
1433        } else {
1434            if ($value !== null) {
1435                $config = new stdClass();
1436                $config->plugin = $plugin;
1437                $config->name   = $name;
1438                $config->value  = $value;
1439                $DB->insert_record('config_plugins', $config, false);
1440            }
1441        }
1442        cache_helper::invalidate_by_definition('core', 'config', array(), $plugin);
1443    }
1444
1445    return true;
1446}
1447
1448/**
1449 * Get configuration values from the global config table
1450 * or the config_plugins table.
1451 *
1452 * If called with one parameter, it will load all the config
1453 * variables for one plugin, and return them as an object.
1454 *
1455 * If called with 2 parameters it will return a string single
1456 * value or false if the value is not found.
1457 *
1458 * NOTE: this function is called from lib/db/upgrade.php
1459 *
1460 * @static string|false $siteidentifier The site identifier is not cached. We use this static cache so
1461 *     that we need only fetch it once per request.
1462 * @param string $plugin full component name
1463 * @param string $name default null
1464 * @return mixed hash-like object or single value, return false no config found
1465 * @throws dml_exception
1466 */
1467function get_config($plugin, $name = null) {
1468    global $CFG, $DB;
1469
1470    static $siteidentifier = null;
1471
1472    if ($plugin === 'moodle' || $plugin === 'core' || empty($plugin)) {
1473        $forced =& $CFG->config_php_settings;
1474        $iscore = true;
1475        $plugin = 'core';
1476    } else {
1477        if (array_key_exists($plugin, $CFG->forced_plugin_settings)) {
1478            $forced =& $CFG->forced_plugin_settings[$plugin];
1479        } else {
1480            $forced = array();
1481        }
1482        $iscore = false;
1483    }
1484
1485    if ($siteidentifier === null) {
1486        try {
1487            // This may fail during installation.
1488            // If you have a look at {@link initialise_cfg()} you will see that this is how we detect the need to
1489            // install the database.
1490            $siteidentifier = $DB->get_field('config', 'value', array('name' => 'siteidentifier'));
1491        } catch (dml_exception $ex) {
1492            // Set siteidentifier to false. We don't want to trip this continually.
1493            $siteidentifier = false;
1494            throw $ex;
1495        }
1496    }
1497
1498    if (!empty($name)) {
1499        if (array_key_exists($name, $forced)) {
1500            return (string)$forced[$name];
1501        } else if ($name === 'siteidentifier' && $plugin == 'core') {
1502            return $siteidentifier;
1503        }
1504    }
1505
1506    $cache = cache::make('core', 'config');
1507    $result = $cache->get($plugin);
1508    if ($result === false) {
1509        // The user is after a recordset.
1510        if (!$iscore) {
1511            $result = $DB->get_records_menu('config_plugins', array('plugin' => $plugin), '', 'name,value');
1512        } else {
1513            // This part is not really used any more, but anyway...
1514            $result = $DB->get_records_menu('config', array(), '', 'name,value');;
1515        }
1516        $cache->set($plugin, $result);
1517    }
1518
1519    if (!empty($name)) {
1520        if (array_key_exists($name, $result)) {
1521            return $result[$name];
1522        }
1523        return false;
1524    }
1525
1526    if ($plugin === 'core') {
1527        $result['siteidentifier'] = $siteidentifier;
1528    }
1529
1530    foreach ($forced as $key => $value) {
1531        if (is_null($value) or is_array($value) or is_object($value)) {
1532            // We do not want any extra mess here, just real settings that could be saved in db.
1533            unset($result[$key]);
1534        } else {
1535            // Convert to string as if it went through the DB.
1536            $result[$key] = (string)$value;
1537        }
1538    }
1539
1540    return (object)$result;
1541}
1542
1543/**
1544 * Removes a key from global configuration.
1545 *
1546 * NOTE: this function is called from lib/db/upgrade.php
1547 *
1548 * @param string $name the key to set
1549 * @param string $plugin (optional) the plugin scope
1550 * @return boolean whether the operation succeeded.
1551 */
1552function unset_config($name, $plugin=null) {
1553    global $CFG, $DB;
1554
1555    if (empty($plugin)) {
1556        unset($CFG->$name);
1557        $DB->delete_records('config', array('name' => $name));
1558        cache_helper::invalidate_by_definition('core', 'config', array(), 'core');
1559    } else {
1560        $DB->delete_records('config_plugins', array('name' => $name, 'plugin' => $plugin));
1561        cache_helper::invalidate_by_definition('core', 'config', array(), $plugin);
1562    }
1563
1564    return true;
1565}
1566
1567/**
1568 * Remove all the config variables for a given plugin.
1569 *
1570 * NOTE: this function is called from lib/db/upgrade.php
1571 *
1572 * @param string $plugin a plugin, for example 'quiz' or 'qtype_multichoice';
1573 * @return boolean whether the operation succeeded.
1574 */
1575function unset_all_config_for_plugin($plugin) {
1576    global $DB;
1577    // Delete from the obvious config_plugins first.
1578    $DB->delete_records('config_plugins', array('plugin' => $plugin));
1579    // Next delete any suspect settings from config.
1580    $like = $DB->sql_like('name', '?', true, true, false, '|');
1581    $params = array($DB->sql_like_escape($plugin.'_', '|') . '%');
1582    $DB->delete_records_select('config', $like, $params);
1583    // Finally clear both the plugin cache and the core cache (suspect settings now removed from core).
1584    cache_helper::invalidate_by_definition('core', 'config', array(), array('core', $plugin));
1585
1586    return true;
1587}
1588
1589/**
1590 * Use this function to get a list of users from a config setting of type admin_setting_users_with_capability.
1591 *
1592 * All users are verified if they still have the necessary capability.
1593 *
1594 * @param string $value the value of the config setting.
1595 * @param string $capability the capability - must match the one passed to the admin_setting_users_with_capability constructor.
1596 * @param bool $includeadmins include administrators.
1597 * @return array of user objects.
1598 */
1599function get_users_from_config($value, $capability, $includeadmins = true) {
1600    if (empty($value) or $value === '$@NONE@$') {
1601        return array();
1602    }
1603
1604    // We have to make sure that users still have the necessary capability,
1605    // it should be faster to fetch them all first and then test if they are present
1606    // instead of validating them one-by-one.
1607    $users = get_users_by_capability(context_system::instance(), $capability);
1608    if ($includeadmins) {
1609        $admins = get_admins();
1610        foreach ($admins as $admin) {
1611            $users[$admin->id] = $admin;
1612        }
1613    }
1614
1615    if ($value === '$@ALL@$') {
1616        return $users;
1617    }
1618
1619    $result = array(); // Result in correct order.
1620    $allowed = explode(',', $value);
1621    foreach ($allowed as $uid) {
1622        if (isset($users[$uid])) {
1623            $user = $users[$uid];
1624            $result[$user->id] = $user;
1625        }
1626    }
1627
1628    return $result;
1629}
1630
1631
1632/**
1633 * Invalidates browser caches and cached data in temp.
1634 *
1635 * @return void
1636 */
1637function purge_all_caches() {
1638    purge_caches();
1639}
1640
1641/**
1642 * Selectively invalidate different types of cache.
1643 *
1644 * Purges the cache areas specified.  By default, this will purge all caches but can selectively purge specific
1645 * areas alone or in combination.
1646 *
1647 * @param bool[] $options Specific parts of the cache to purge. Valid options are:
1648 *        'muc'    Purge MUC caches?
1649 *        'theme'  Purge theme cache?
1650 *        'lang'   Purge language string cache?
1651 *        'js'     Purge javascript cache?
1652 *        'filter' Purge text filter cache?
1653 *        'other'  Purge all other caches?
1654 */
1655function purge_caches($options = []) {
1656    $defaults = array_fill_keys(['muc', 'theme', 'lang', 'js', 'template', 'filter', 'other'], false);
1657    if (empty(array_filter($options))) {
1658        $options = array_fill_keys(array_keys($defaults), true); // Set all options to true.
1659    } else {
1660        $options = array_merge($defaults, array_intersect_key($options, $defaults)); // Override defaults with specified options.
1661    }
1662    if ($options['muc']) {
1663        cache_helper::purge_all();
1664    }
1665    if ($options['theme']) {
1666        theme_reset_all_caches();
1667    }
1668    if ($options['lang']) {
1669        get_string_manager()->reset_caches();
1670    }
1671    if ($options['js']) {
1672        js_reset_all_caches();
1673    }
1674    if ($options['template']) {
1675        template_reset_all_caches();
1676    }
1677    if ($options['filter']) {
1678        reset_text_filters_cache();
1679    }
1680    if ($options['other']) {
1681        purge_other_caches();
1682    }
1683}
1684
1685/**
1686 * Purge all non-MUC caches not otherwise purged in purge_caches.
1687 *
1688 * IMPORTANT - If you are adding anything here to do with the cache directory you should also have a look at
1689 * {@link phpunit_util::reset_dataroot()}
1690 */
1691function purge_other_caches() {
1692    global $DB, $CFG;
1693    core_text::reset_caches();
1694    if (class_exists('core_plugin_manager')) {
1695        core_plugin_manager::reset_caches();
1696    }
1697
1698    // Bump up cacherev field for all courses.
1699    try {
1700        increment_revision_number('course', 'cacherev', '');
1701    } catch (moodle_exception $e) {
1702        // Ignore exception since this function is also called before upgrade script when field course.cacherev does not exist yet.
1703    }
1704
1705    $DB->reset_caches();
1706
1707    // Purge all other caches: rss, simplepie, etc.
1708    clearstatcache();
1709    remove_dir($CFG->cachedir.'', true);
1710
1711    // Make sure cache dir is writable, throws exception if not.
1712    make_cache_directory('');
1713
1714    // This is the only place where we purge local caches, we are only adding files there.
1715    // The $CFG->localcachedirpurged flag forces local directories to be purged on cluster nodes.
1716    remove_dir($CFG->localcachedir, true);
1717    set_config('localcachedirpurged', time());
1718    make_localcache_directory('', true);
1719    \core\task\manager::clear_static_caches();
1720}
1721
1722/**
1723 * Get volatile flags
1724 *
1725 * @param string $type
1726 * @param int $changedsince default null
1727 * @return array records array
1728 */
1729function get_cache_flags($type, $changedsince = null) {
1730    global $DB;
1731
1732    $params = array('type' => $type, 'expiry' => time());
1733    $sqlwhere = "flagtype = :type AND expiry >= :expiry";
1734    if ($changedsince !== null) {
1735        $params['changedsince'] = $changedsince;
1736        $sqlwhere .= " AND timemodified > :changedsince";
1737    }
1738    $cf = array();
1739    if ($flags = $DB->get_records_select('cache_flags', $sqlwhere, $params, '', 'name,value')) {
1740        foreach ($flags as $flag) {
1741            $cf[$flag->name] = $flag->value;
1742        }
1743    }
1744    return $cf;
1745}
1746
1747/**
1748 * Get volatile flags
1749 *
1750 * @param string $type
1751 * @param string $name
1752 * @param int $changedsince default null
1753 * @return string|false The cache flag value or false
1754 */
1755function get_cache_flag($type, $name, $changedsince=null) {
1756    global $DB;
1757
1758    $params = array('type' => $type, 'name' => $name, 'expiry' => time());
1759
1760    $sqlwhere = "flagtype = :type AND name = :name AND expiry >= :expiry";
1761    if ($changedsince !== null) {
1762        $params['changedsince'] = $changedsince;
1763        $sqlwhere .= " AND timemodified > :changedsince";
1764    }
1765
1766    return $DB->get_field_select('cache_flags', 'value', $sqlwhere, $params);
1767}
1768
1769/**
1770 * Set a volatile flag
1771 *
1772 * @param string $type the "type" namespace for the key
1773 * @param string $name the key to set
1774 * @param string $value the value to set (without magic quotes) - null will remove the flag
1775 * @param int $expiry (optional) epoch indicating expiry - defaults to now()+ 24hs
1776 * @return bool Always returns true
1777 */
1778function set_cache_flag($type, $name, $value, $expiry = null) {
1779    global $DB;
1780
1781    $timemodified = time();
1782    if ($expiry === null || $expiry < $timemodified) {
1783        $expiry = $timemodified + 24 * 60 * 60;
1784    } else {
1785        $expiry = (int)$expiry;
1786    }
1787
1788    if ($value === null) {
1789        unset_cache_flag($type, $name);
1790        return true;
1791    }
1792
1793    if ($f = $DB->get_record('cache_flags', array('name' => $name, 'flagtype' => $type), '*', IGNORE_MULTIPLE)) {
1794        // This is a potential problem in DEBUG_DEVELOPER.
1795        if ($f->value == $value and $f->expiry == $expiry and $f->timemodified == $timemodified) {
1796            return true; // No need to update.
1797        }
1798        $f->value        = $value;
1799        $f->expiry       = $expiry;
1800        $f->timemodified = $timemodified;
1801        $DB->update_record('cache_flags', $f);
1802    } else {
1803        $f = new stdClass();
1804        $f->flagtype     = $type;
1805        $f->name         = $name;
1806        $f->value        = $value;
1807        $f->expiry       = $expiry;
1808        $f->timemodified = $timemodified;
1809        $DB->insert_record('cache_flags', $f);
1810    }
1811    return true;
1812}
1813
1814/**
1815 * Removes a single volatile flag
1816 *
1817 * @param string $type the "type" namespace for the key
1818 * @param string $name the key to set
1819 * @return bool
1820 */
1821function unset_cache_flag($type, $name) {
1822    global $DB;
1823    $DB->delete_records('cache_flags', array('name' => $name, 'flagtype' => $type));
1824    return true;
1825}
1826
1827/**
1828 * Garbage-collect volatile flags
1829 *
1830 * @return bool Always returns true
1831 */
1832function gc_cache_flags() {
1833    global $DB;
1834    $DB->delete_records_select('cache_flags', 'expiry < ?', array(time()));
1835    return true;
1836}
1837
1838// USER PREFERENCE API.
1839
1840/**
1841 * Refresh user preference cache. This is used most often for $USER
1842 * object that is stored in session, but it also helps with performance in cron script.
1843 *
1844 * Preferences for each user are loaded on first use on every page, then again after the timeout expires.
1845 *
1846 * @package  core
1847 * @category preference
1848 * @access   public
1849 * @param    stdClass         $user          User object. Preferences are preloaded into 'preference' property
1850 * @param    int              $cachelifetime Cache life time on the current page (in seconds)
1851 * @throws   coding_exception
1852 * @return   null
1853 */
1854function check_user_preferences_loaded(stdClass $user, $cachelifetime = 120) {
1855    global $DB;
1856    // Static cache, we need to check on each page load, not only every 2 minutes.
1857    static $loadedusers = array();
1858
1859    if (!isset($user->id)) {
1860        throw new coding_exception('Invalid $user parameter in check_user_preferences_loaded() call, missing id field');
1861    }
1862
1863    if (empty($user->id) or isguestuser($user->id)) {
1864        // No permanent storage for not-logged-in users and guest.
1865        if (!isset($user->preference)) {
1866            $user->preference = array();
1867        }
1868        return;
1869    }
1870
1871    $timenow = time();
1872
1873    if (isset($loadedusers[$user->id]) and isset($user->preference) and isset($user->preference['_lastloaded'])) {
1874        // Already loaded at least once on this page. Are we up to date?
1875        if ($user->preference['_lastloaded'] + $cachelifetime > $timenow) {
1876            // No need to reload - we are on the same page and we loaded prefs just a moment ago.
1877            return;
1878
1879        } else if (!get_cache_flag('userpreferenceschanged', $user->id, $user->preference['_lastloaded'])) {
1880            // No change since the lastcheck on this page.
1881            $user->preference['_lastloaded'] = $timenow;
1882            return;
1883        }
1884    }
1885
1886    // OK, so we have to reload all preferences.
1887    $loadedusers[$user->id] = true;
1888    $user->preference = $DB->get_records_menu('user_preferences', array('userid' => $user->id), '', 'name,value'); // All values.
1889    $user->preference['_lastloaded'] = $timenow;
1890}
1891
1892/**
1893 * Called from set/unset_user_preferences, so that the prefs can be correctly reloaded in different sessions.
1894 *
1895 * NOTE: internal function, do not call from other code.
1896 *
1897 * @package core
1898 * @access private
1899 * @param integer $userid the user whose prefs were changed.
1900 */
1901function mark_user_preferences_changed($userid) {
1902    global $CFG;
1903
1904    if (empty($userid) or isguestuser($userid)) {
1905        // No cache flags for guest and not-logged-in users.
1906        return;
1907    }
1908
1909    set_cache_flag('userpreferenceschanged', $userid, 1, time() + $CFG->sessiontimeout);
1910}
1911
1912/**
1913 * Sets a preference for the specified user.
1914 *
1915 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1916 *
1917 * When additional validation/permission check is needed it is better to use {@see useredit_update_user_preference()}
1918 *
1919 * @package  core
1920 * @category preference
1921 * @access   public
1922 * @param    string            $name  The key to set as preference for the specified user
1923 * @param    string            $value The value to set for the $name key in the specified user's
1924 *                                    record, null means delete current value.
1925 * @param    stdClass|int|null $user  A moodle user object or id, null means current user
1926 * @throws   coding_exception
1927 * @return   bool                     Always true or exception
1928 */
1929function set_user_preference($name, $value, $user = null) {
1930    global $USER, $DB;
1931
1932    if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
1933        throw new coding_exception('Invalid preference name in set_user_preference() call');
1934    }
1935
1936    if (is_null($value)) {
1937        // Null means delete current.
1938        return unset_user_preference($name, $user);
1939    } else if (is_object($value)) {
1940        throw new coding_exception('Invalid value in set_user_preference() call, objects are not allowed');
1941    } else if (is_array($value)) {
1942        throw new coding_exception('Invalid value in set_user_preference() call, arrays are not allowed');
1943    }
1944    // Value column maximum length is 1333 characters.
1945    $value = (string)$value;
1946    if (core_text::strlen($value) > 1333) {
1947        throw new coding_exception('Invalid value in set_user_preference() call, value is is too long for the value column');
1948    }
1949
1950    if (is_null($user)) {
1951        $user = $USER;
1952    } else if (isset($user->id)) {
1953        // It is a valid object.
1954    } else if (is_numeric($user)) {
1955        $user = (object)array('id' => (int)$user);
1956    } else {
1957        throw new coding_exception('Invalid $user parameter in set_user_preference() call');
1958    }
1959
1960    check_user_preferences_loaded($user);
1961
1962    if (empty($user->id) or isguestuser($user->id)) {
1963        // No permanent storage for not-logged-in users and guest.
1964        $user->preference[$name] = $value;
1965        return true;
1966    }
1967
1968    if ($preference = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => $name))) {
1969        if ($preference->value === $value and isset($user->preference[$name]) and $user->preference[$name] === $value) {
1970            // Preference already set to this value.
1971            return true;
1972        }
1973        $DB->set_field('user_preferences', 'value', $value, array('id' => $preference->id));
1974
1975    } else {
1976        $preference = new stdClass();
1977        $preference->userid = $user->id;
1978        $preference->name   = $name;
1979        $preference->value  = $value;
1980        $DB->insert_record('user_preferences', $preference);
1981    }
1982
1983    // Update value in cache.
1984    $user->preference[$name] = $value;
1985    // Update the $USER in case where we've not a direct reference to $USER.
1986    if ($user !== $USER && $user->id == $USER->id) {
1987        $USER->preference[$name] = $value;
1988    }
1989
1990    // Set reload flag for other sessions.
1991    mark_user_preferences_changed($user->id);
1992
1993    return true;
1994}
1995
1996/**
1997 * Sets a whole array of preferences for the current user
1998 *
1999 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
2000 *
2001 * @package  core
2002 * @category preference
2003 * @access   public
2004 * @param    array             $prefarray An array of key/value pairs to be set
2005 * @param    stdClass|int|null $user      A moodle user object or id, null means current user
2006 * @return   bool                         Always true or exception
2007 */
2008function set_user_preferences(array $prefarray, $user = null) {
2009    foreach ($prefarray as $name => $value) {
2010        set_user_preference($name, $value, $user);
2011    }
2012    return true;
2013}
2014
2015/**
2016 * Unsets a preference completely by deleting it from the database
2017 *
2018 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
2019 *
2020 * @package  core
2021 * @category preference
2022 * @access   public
2023 * @param    string            $name The key to unset as preference for the specified user
2024 * @param    stdClass|int|null $user A moodle user object or id, null means current user
2025 * @throws   coding_exception
2026 * @return   bool                    Always true or exception
2027 */
2028function unset_user_preference($name, $user = null) {
2029    global $USER, $DB;
2030
2031    if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
2032        throw new coding_exception('Invalid preference name in unset_user_preference() call');
2033    }
2034
2035    if (is_null($user)) {
2036        $user = $USER;
2037    } else if (isset($user->id)) {
2038        // It is a valid object.
2039    } else if (is_numeric($user)) {
2040        $user = (object)array('id' => (int)$user);
2041    } else {
2042        throw new coding_exception('Invalid $user parameter in unset_user_preference() call');
2043    }
2044
2045    check_user_preferences_loaded($user);
2046
2047    if (empty($user->id) or isguestuser($user->id)) {
2048        // No permanent storage for not-logged-in user and guest.
2049        unset($user->preference[$name]);
2050        return true;
2051    }
2052
2053    // Delete from DB.
2054    $DB->delete_records('user_preferences', array('userid' => $user->id, 'name' => $name));
2055
2056    // Delete the preference from cache.
2057    unset($user->preference[$name]);
2058    // Update the $USER in case where we've not a direct reference to $USER.
2059    if ($user !== $USER && $user->id == $USER->id) {
2060        unset($USER->preference[$name]);
2061    }
2062
2063    // Set reload flag for other sessions.
2064    mark_user_preferences_changed($user->id);
2065
2066    return true;
2067}
2068
2069/**
2070 * Used to fetch user preference(s)
2071 *
2072 * If no arguments are supplied this function will return
2073 * all of the current user preferences as an array.
2074 *
2075 * If a name is specified then this function
2076 * attempts to return that particular preference value.  If
2077 * none is found, then the optional value $default is returned,
2078 * otherwise null.
2079 *
2080 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
2081 *
2082 * @package  core
2083 * @category preference
2084 * @access   public
2085 * @param    string            $name    Name of the key to use in finding a preference value
2086 * @param    mixed|null        $default Value to be returned if the $name key is not set in the user preferences
2087 * @param    stdClass|int|null $user    A moodle user object or id, null means current user
2088 * @throws   coding_exception
2089 * @return   string|mixed|null          A string containing the value of a single preference. An
2090 *                                      array with all of the preferences or null
2091 */
2092function get_user_preferences($name = null, $default = null, $user = null) {
2093    global $USER;
2094
2095    if (is_null($name)) {
2096        // All prefs.
2097    } else if (is_numeric($name) or $name === '_lastloaded') {
2098        throw new coding_exception('Invalid preference name in get_user_preferences() call');
2099    }
2100
2101    if (is_null($user)) {
2102        $user = $USER;
2103    } else if (isset($user->id)) {
2104        // Is a valid object.
2105    } else if (is_numeric($user)) {
2106        if ($USER->id == $user) {
2107            $user = $USER;
2108        } else {
2109            $user = (object)array('id' => (int)$user);
2110        }
2111    } else {
2112        throw new coding_exception('Invalid $user parameter in get_user_preferences() call');
2113    }
2114
2115    check_user_preferences_loaded($user);
2116
2117    if (empty($name)) {
2118        // All values.
2119        return $user->preference;
2120    } else if (isset($user->preference[$name])) {
2121        // The single string value.
2122        return $user->preference[$name];
2123    } else {
2124        // Default value (null if not specified).
2125        return $default;
2126    }
2127}
2128
2129// FUNCTIONS FOR HANDLING TIME.
2130
2131/**
2132 * Given Gregorian date parts in user time produce a GMT timestamp.
2133 *
2134 * @package core
2135 * @category time
2136 * @param int $year The year part to create timestamp of
2137 * @param int $month The month part to create timestamp of
2138 * @param int $day The day part to create timestamp of
2139 * @param int $hour The hour part to create timestamp of
2140 * @param int $minute The minute part to create timestamp of
2141 * @param int $second The second part to create timestamp of
2142 * @param int|float|string $timezone Timezone modifier, used to calculate GMT time offset.
2143 *             if 99 then default user's timezone is used {@link http://docs.moodle.org/dev/Time_API#Timezone}
2144 * @param bool $applydst Toggle Daylight Saving Time, default true, will be
2145 *             applied only if timezone is 99 or string.
2146 * @return int GMT timestamp
2147 */
2148function make_timestamp($year, $month=1, $day=1, $hour=0, $minute=0, $second=0, $timezone=99, $applydst=true) {
2149    $date = new DateTime('now', core_date::get_user_timezone_object($timezone));
2150    $date->setDate((int)$year, (int)$month, (int)$day);
2151    $date->setTime((int)$hour, (int)$minute, (int)$second);
2152
2153    $time = $date->getTimestamp();
2154
2155    if ($time === false) {
2156        throw new coding_exception('getTimestamp() returned false, please ensure you have passed correct values.'.
2157            ' This can fail if year is more than 2038 and OS is 32 bit windows');
2158    }
2159
2160    // Moodle BC DST stuff.
2161    if (!$applydst) {
2162        $time += dst_offset_on($time, $timezone);
2163    }
2164
2165    return $time;
2166
2167}
2168
2169/**
2170 * Format a date/time (seconds) as weeks, days, hours etc as needed
2171 *
2172 * Given an amount of time in seconds, returns string
2173 * formatted nicely as years, days, hours etc as needed
2174 *
2175 * @package core
2176 * @category time
2177 * @uses MINSECS
2178 * @uses HOURSECS
2179 * @uses DAYSECS
2180 * @uses YEARSECS
2181 * @param int $totalsecs Time in seconds
2182 * @param stdClass $str Should be a time object
2183 * @return string A nicely formatted date/time string
2184 */
2185function format_time($totalsecs, $str = null) {
2186
2187    $totalsecs = abs($totalsecs);
2188
2189    if (!$str) {
2190        // Create the str structure the slow way.
2191        $str = new stdClass();
2192        $str->day   = get_string('day');
2193        $str->days  = get_string('days');
2194        $str->hour  = get_string('hour');
2195        $str->hours = get_string('hours');
2196        $str->min   = get_string('min');
2197        $str->mins  = get_string('mins');
2198        $str->sec   = get_string('sec');
2199        $str->secs  = get_string('secs');
2200        $str->year  = get_string('year');
2201        $str->years = get_string('years');
2202    }
2203
2204    $years     = floor($totalsecs/YEARSECS);
2205    $remainder = $totalsecs - ($years*YEARSECS);
2206    $days      = floor($remainder/DAYSECS);
2207    $remainder = $totalsecs - ($days*DAYSECS);
2208    $hours     = floor($remainder/HOURSECS);
2209    $remainder = $remainder - ($hours*HOURSECS);
2210    $mins      = floor($remainder/MINSECS);
2211    $secs      = $remainder - ($mins*MINSECS);
2212
2213    $ss = ($secs == 1)  ? $str->sec  : $str->secs;
2214    $sm = ($mins == 1)  ? $str->min  : $str->mins;
2215    $sh = ($hours == 1) ? $str->hour : $str->hours;
2216    $sd = ($days == 1)  ? $str->day  : $str->days;
2217    $sy = ($years == 1)  ? $str->year  : $str->years;
2218
2219    $oyears = '';
2220    $odays = '';
2221    $ohours = '';
2222    $omins = '';
2223    $osecs = '';
2224
2225    if ($years) {
2226        $oyears  = $years .' '. $sy;
2227    }
2228    if ($days) {
2229        $odays  = $days .' '. $sd;
2230    }
2231    if ($hours) {
2232        $ohours = $hours .' '. $sh;
2233    }
2234    if ($mins) {
2235        $omins  = $mins .' '. $sm;
2236    }
2237    if ($secs) {
2238        $osecs  = $secs .' '. $ss;
2239    }
2240
2241    if ($years) {
2242        return trim($oyears .' '. $odays);
2243    }
2244    if ($days) {
2245        return trim($odays .' '. $ohours);
2246    }
2247    if ($hours) {
2248        return trim($ohours .' '. $omins);
2249    }
2250    if ($mins) {
2251        return trim($omins .' '. $osecs);
2252    }
2253    if ($secs) {
2254        return $osecs;
2255    }
2256    return get_string('now');
2257}
2258
2259/**
2260 * Returns a formatted string that represents a date in user time.
2261 *
2262 * @package core
2263 * @category time
2264 * @param int $date the timestamp in UTC, as obtained from the database.
2265 * @param string $format strftime format. You should probably get this using
2266 *        get_string('strftime...', 'langconfig');
2267 * @param int|float|string $timezone by default, uses the user's time zone. if numeric and
2268 *        not 99 then daylight saving will not be added.
2269 *        {@link http://docs.moodle.org/dev/Time_API#Timezone}
2270 * @param bool $fixday If true (default) then the leading zero from %d is removed.
2271 *        If false then the leading zero is maintained.
2272 * @param bool $fixhour If true (default) then the leading zero from %I is removed.
2273 * @return string the formatted date/time.
2274 */
2275function userdate($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) {
2276    $calendartype = \core_calendar\type_factory::get_calendar_instance();
2277    return $calendartype->timestamp_to_date_string($date, $format, $timezone, $fixday, $fixhour);
2278}
2279
2280/**
2281 * Returns a html "time" tag with both the exact user date with timezone information
2282 * as a datetime attribute in the W3C format, and the user readable date and time as text.
2283 *
2284 * @package core
2285 * @category time
2286 * @param int $date the timestamp in UTC, as obtained from the database.
2287 * @param string $format strftime format. You should probably get this using
2288 *        get_string('strftime...', 'langconfig');
2289 * @param int|float|string $timezone by default, uses the user's time zone. if numeric and
2290 *        not 99 then daylight saving will not be added.
2291 *        {@link http://docs.moodle.org/dev/Time_API#Timezone}
2292 * @param bool $fixday If true (default) then the leading zero from %d is removed.
2293 *        If false then the leading zero is maintained.
2294 * @param bool $fixhour If true (default) then the leading zero from %I is removed.
2295 * @return string the formatted date/time.
2296 */
2297function userdate_htmltime($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) {
2298    $userdatestr = userdate($date, $format, $timezone, $fixday, $fixhour);
2299    if (CLI_SCRIPT && !PHPUNIT_TEST) {
2300        return $userdatestr;
2301    }
2302    $machinedate = new DateTime();
2303    $machinedate->setTimestamp(intval($date));
2304    $machinedate->setTimezone(core_date::get_user_timezone_object());
2305
2306    return html_writer::tag('time', $userdatestr, ['datetime' => $machinedate->format(DateTime::W3C)]);
2307}
2308
2309/**
2310 * Returns a formatted date ensuring it is UTF-8.
2311 *
2312 * If we are running under Windows convert to Windows encoding and then back to UTF-8
2313 * (because it's impossible to specify UTF-8 to fetch locale info in Win32).
2314 *
2315 * @param int $date the timestamp - since Moodle 2.9 this is a real UTC timestamp
2316 * @param string $format strftime format.
2317 * @param int|float|string $tz the user timezone
2318 * @return string the formatted date/time.
2319 * @since Moodle 2.3.3
2320 */
2321function date_format_string($date, $format, $tz = 99) {
2322    global $CFG;
2323
2324    $localewincharset = null;
2325    // Get the calendar type user is using.
2326    if ($CFG->ostype == 'WINDOWS') {
2327        $calendartype = \core_calendar\type_factory::get_calendar_instance();
2328        $localewincharset = $calendartype->locale_win_charset();
2329    }
2330
2331    if ($localewincharset) {
2332        $format = core_text::convert($format, 'utf-8', $localewincharset);
2333    }
2334
2335    date_default_timezone_set(core_date::get_user_timezone($tz));
2336    $datestring = strftime($format, $date);
2337    core_date::set_default_server_timezone();
2338
2339    if ($localewincharset) {
2340        $datestring = core_text::convert($datestring, $localewincharset, 'utf-8');
2341    }
2342
2343    return $datestring;
2344}
2345
2346/**
2347 * Given a $time timestamp in GMT (seconds since epoch),
2348 * returns an array that represents the Gregorian date in user time
2349 *
2350 * @package core
2351 * @category time
2352 * @param int $time Timestamp in GMT
2353 * @param float|int|string $timezone user timezone
2354 * @return array An array that represents the date in user time
2355 */
2356function usergetdate($time, $timezone=99) {
2357    date_default_timezone_set(core_date::get_user_timezone($timezone));
2358    $result = getdate($time);
2359    core_date::set_default_server_timezone();
2360
2361    return $result;
2362}
2363
2364/**
2365 * Given a GMT timestamp (seconds since epoch), offsets it by
2366 * the timezone.  eg 3pm in India is 3pm GMT - 7 * 3600 seconds
2367 *
2368 * NOTE: this function does not include DST properly,
2369 *       you should use the PHP date stuff instead!
2370 *
2371 * @package core
2372 * @category time
2373 * @param int $date Timestamp in GMT
2374 * @param float|int|string $timezone user timezone
2375 * @return int
2376 */
2377function usertime($date, $timezone=99) {
2378    $userdate = new DateTime('@' . $date);
2379    $userdate->setTimezone(core_date::get_user_timezone_object($timezone));
2380    $dst = dst_offset_on($date, $timezone);
2381
2382    return $date - $userdate->getOffset() + $dst;
2383}
2384
2385/**
2386 * Get a formatted string representation of an interval between two unix timestamps.
2387 *
2388 * E.g.
2389 * $intervalstring = get_time_interval_string(12345600, 12345660);
2390 * Will produce the string:
2391 * '0d 0h 1m'
2392 *
2393 * @param int $time1 unix timestamp
2394 * @param int $time2 unix timestamp
2395 * @param string $format string (can be lang string) containing format chars: https://www.php.net/manual/en/dateinterval.format.php.
2396 * @return string the formatted string describing the time difference, e.g. '10d 11h 45m'.
2397 */
2398function get_time_interval_string(int $time1, int $time2, string $format = ''): string {
2399    $dtdate = new DateTime();
2400    $dtdate->setTimeStamp($time1);
2401    $dtdate2 = new DateTime();
2402    $dtdate2->setTimeStamp($time2);
2403    $interval = $dtdate2->diff($dtdate);
2404    $format = empty($format) ? get_string('dateintervaldayshoursmins', 'langconfig') : $format;
2405    return $interval->format($format);
2406}
2407
2408/**
2409 * Given a time, return the GMT timestamp of the most recent midnight
2410 * for the current user.
2411 *
2412 * @package core
2413 * @category time
2414 * @param int $date Timestamp in GMT
2415 * @param float|int|string $timezone user timezone
2416 * @return int Returns a GMT timestamp
2417 */
2418function usergetmidnight($date, $timezone=99) {
2419
2420    $userdate = usergetdate($date, $timezone);
2421
2422    // Time of midnight of this user's day, in GMT.
2423    return make_timestamp($userdate['year'], $userdate['mon'], $userdate['mday'], 0, 0, 0, $timezone);
2424
2425}
2426
2427/**
2428 * Returns a string that prints the user's timezone
2429 *
2430 * @package core
2431 * @category time
2432 * @param float|int|string $timezone user timezone
2433 * @return string
2434 */
2435function usertimezone($timezone=99) {
2436    $tz = core_date::get_user_timezone($timezone);
2437    return core_date::get_localised_timezone($tz);
2438}
2439
2440/**
2441 * Returns a float or a string which denotes the user's timezone
2442 * 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)
2443 * means that for this timezone there are also DST rules to be taken into account
2444 * Checks various settings and picks the most dominant of those which have a value
2445 *
2446 * @package core
2447 * @category time
2448 * @param float|int|string $tz timezone to calculate GMT time offset before
2449 *        calculating user timezone, 99 is default user timezone
2450 *        {@link http://docs.moodle.org/dev/Time_API#Timezone}
2451 * @return float|string
2452 */
2453function get_user_timezone($tz = 99) {
2454    global $USER, $CFG;
2455
2456    $timezones = array(
2457        $tz,
2458        isset($CFG->forcetimezone) ? $CFG->forcetimezone : 99,
2459        isset($USER->timezone) ? $USER->timezone : 99,
2460        isset($CFG->timezone) ? $CFG->timezone : 99,
2461        );
2462
2463    $tz = 99;
2464
2465    // Loop while $tz is, empty but not zero, or 99, and there is another timezone is the array.
2466    foreach ($timezones as $nextvalue) {
2467        if ((empty($tz) && !is_numeric($tz)) || $tz == 99) {
2468            $tz = $nextvalue;
2469        }
2470    }
2471    return is_numeric($tz) ? (float) $tz : $tz;
2472}
2473
2474/**
2475 * Calculates the Daylight Saving Offset for a given date/time (timestamp)
2476 * - Note: Daylight saving only works for string timezones and not for float.
2477 *
2478 * @package core
2479 * @category time
2480 * @param int $time must NOT be compensated at all, it has to be a pure timestamp
2481 * @param int|float|string $strtimezone user timezone
2482 * @return int
2483 */
2484function dst_offset_on($time, $strtimezone = null) {
2485    $tz = core_date::get_user_timezone($strtimezone);
2486    $date = new DateTime('@' . $time);
2487    $date->setTimezone(new DateTimeZone($tz));
2488    if ($date->format('I') == '1') {
2489        if ($tz === 'Australia/Lord_Howe') {
2490            return 1800;
2491        }
2492        return 3600;
2493    }
2494    return 0;
2495}
2496
2497/**
2498 * Calculates when the day appears in specific month
2499 *
2500 * @package core
2501 * @category time
2502 * @param int $startday starting day of the month
2503 * @param int $weekday The day when week starts (normally taken from user preferences)
2504 * @param int $month The month whose day is sought
2505 * @param int $year The year of the month whose day is sought
2506 * @return int
2507 */
2508function find_day_in_month($startday, $weekday, $month, $year) {
2509    $calendartype = \core_calendar\type_factory::get_calendar_instance();
2510
2511    $daysinmonth = days_in_month($month, $year);
2512    $daysinweek = count($calendartype->get_weekdays());
2513
2514    if ($weekday == -1) {
2515        // Don't care about weekday, so return:
2516        //    abs($startday) if $startday != -1
2517        //    $daysinmonth otherwise.
2518        return ($startday == -1) ? $daysinmonth : abs($startday);
2519    }
2520
2521    // From now on we 're looking for a specific weekday.
2522    // Give "end of month" its actual value, since we know it.
2523    if ($startday == -1) {
2524        $startday = -1 * $daysinmonth;
2525    }
2526
2527    // Starting from day $startday, the sign is the direction.
2528    if ($startday < 1) {
2529        $startday = abs($startday);
2530        $lastmonthweekday = dayofweek($daysinmonth, $month, $year);
2531
2532        // This is the last such weekday of the month.
2533        $lastinmonth = $daysinmonth + $weekday - $lastmonthweekday;
2534        if ($lastinmonth > $daysinmonth) {
2535            $lastinmonth -= $daysinweek;
2536        }
2537
2538        // Find the first such weekday <= $startday.
2539        while ($lastinmonth > $startday) {
2540            $lastinmonth -= $daysinweek;
2541        }
2542
2543        return $lastinmonth;
2544    } else {
2545        $indexweekday = dayofweek($startday, $month, $year);
2546
2547        $diff = $weekday - $indexweekday;
2548        if ($diff < 0) {
2549            $diff += $daysinweek;
2550        }
2551
2552        // This is the first such weekday of the month equal to or after $startday.
2553        $firstfromindex = $startday + $diff;
2554
2555        return $firstfromindex;
2556    }
2557}
2558
2559/**
2560 * Calculate the number of days in a given month
2561 *
2562 * @package core
2563 * @category time
2564 * @param int $month The month whose day count is sought
2565 * @param int $year The year of the month whose day count is sought
2566 * @return int
2567 */
2568function days_in_month($month, $year) {
2569    $calendartype = \core_calendar\type_factory::get_calendar_instance();
2570    return $calendartype->get_num_days_in_month($year, $month);
2571}
2572
2573/**
2574 * Calculate the position in the week of a specific calendar day
2575 *
2576 * @package core
2577 * @category time
2578 * @param int $day The day of the date whose position in the week is sought
2579 * @param int $month The month of the date whose position in the week is sought
2580 * @param int $year The year of the date whose position in the week is sought
2581 * @return int
2582 */
2583function dayofweek($day, $month, $year) {
2584    $calendartype = \core_calendar\type_factory::get_calendar_instance();
2585    return $calendartype->get_weekday($year, $month, $day);
2586}
2587
2588// USER AUTHENTICATION AND LOGIN.
2589
2590/**
2591 * Returns full login url.
2592 *
2593 * Any form submissions for authentication to this URL must include username,
2594 * password as well as a logintoken generated by \core\session\manager::get_login_token().
2595 *
2596 * @return string login url
2597 */
2598function get_login_url() {
2599    global $CFG;
2600
2601    return "$CFG->wwwroot/login/index.php";
2602}
2603
2604/**
2605 * This function checks that the current user is logged in and has the
2606 * required privileges
2607 *
2608 * This function checks that the current user is logged in, and optionally
2609 * whether they are allowed to be in a particular course and view a particular
2610 * course module.
2611 * If they are not logged in, then it redirects them to the site login unless
2612 * $autologinguest is set and {@link $CFG}->autologinguests is set to 1 in which
2613 * case they are automatically logged in as guests.
2614 * If $courseid is given and the user is not enrolled in that course then the
2615 * user is redirected to the course enrolment page.
2616 * If $cm is given and the course module is hidden and the user is not a teacher
2617 * in the course then the user is redirected to the course home page.
2618 *
2619 * When $cm parameter specified, this function sets page layout to 'module'.
2620 * You need to change it manually later if some other layout needed.
2621 *
2622 * @package    core_access
2623 * @category   access
2624 *
2625 * @param mixed $courseorid id of the course or course object
2626 * @param bool $autologinguest default true
2627 * @param object $cm course module object
2628 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
2629 *             true. Used to avoid (=false) some scripts (file.php...) to set that variable,
2630 *             in order to keep redirects working properly. MDL-14495
2631 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
2632 * @return mixed Void, exit, and die depending on path
2633 * @throws coding_exception
2634 * @throws require_login_exception
2635 * @throws moodle_exception
2636 */
2637function require_login($courseorid = null, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) {
2638    global $CFG, $SESSION, $USER, $PAGE, $SITE, $DB, $OUTPUT;
2639
2640    // Must not redirect when byteserving already started.
2641    if (!empty($_SERVER['HTTP_RANGE'])) {
2642        $preventredirect = true;
2643    }
2644
2645    if (AJAX_SCRIPT) {
2646        // We cannot redirect for AJAX scripts either.
2647        $preventredirect = true;
2648    }
2649
2650    // Setup global $COURSE, themes, language and locale.
2651    if (!empty($courseorid)) {
2652        if (is_object($courseorid)) {
2653            $course = $courseorid;
2654        } else if ($courseorid == SITEID) {
2655            $course = clone($SITE);
2656        } else {
2657            $course = $DB->get_record('course', array('id' => $courseorid), '*', MUST_EXIST);
2658        }
2659        if ($cm) {
2660            if ($cm->course != $course->id) {
2661                throw new coding_exception('course and cm parameters in require_login() call do not match!!');
2662            }
2663            // Make sure we have a $cm from get_fast_modinfo as this contains activity access details.
2664            if (!($cm instanceof cm_info)) {
2665                // Note: nearly all pages call get_fast_modinfo anyway and it does not make any
2666                // db queries so this is not really a performance concern, however it is obviously
2667                // better if you use get_fast_modinfo to get the cm before calling this.
2668                $modinfo = get_fast_modinfo($course);
2669                $cm = $modinfo->get_cm($cm->id);
2670            }
2671        }
2672    } else {
2673        // Do not touch global $COURSE via $PAGE->set_course(),
2674        // the reasons is we need to be able to call require_login() at any time!!
2675        $course = $SITE;
2676        if ($cm) {
2677            throw new coding_exception('cm parameter in require_login() requires valid course parameter!');
2678        }
2679    }
2680
2681    // If this is an AJAX request and $setwantsurltome is true then we need to override it and set it to false.
2682    // Otherwise the AJAX request URL will be set to $SESSION->wantsurl and events such as self enrolment in the future
2683    // risk leading the user back to the AJAX request URL.
2684    if ($setwantsurltome && defined('AJAX_SCRIPT') && AJAX_SCRIPT) {
2685        $setwantsurltome = false;
2686    }
2687
2688    // Redirect to the login page if session has expired, only with dbsessions enabled (MDL-35029) to maintain current behaviour.
2689    if ((!isloggedin() or isguestuser()) && !empty($SESSION->has_timed_out) && !empty($CFG->dbsessions)) {
2690        if ($preventredirect) {
2691            throw new require_login_session_timeout_exception();
2692        } else {
2693            if ($setwantsurltome) {
2694                $SESSION->wantsurl = qualified_me();
2695            }
2696            redirect(get_login_url());
2697        }
2698    }
2699
2700    // If the user is not even logged in yet then make sure they are.
2701    if (!isloggedin()) {
2702        if ($autologinguest and !empty($CFG->guestloginbutton) and !empty($CFG->autologinguests)) {
2703            if (!$guest = get_complete_user_data('id', $CFG->siteguest)) {
2704                // Misconfigured site guest, just redirect to login page.
2705                redirect(get_login_url());
2706                exit; // Never reached.
2707            }
2708            $lang = isset($SESSION->lang) ? $SESSION->lang : $CFG->lang;
2709            complete_user_login($guest);
2710            $USER->autologinguest = true;
2711            $SESSION->lang = $lang;
2712        } else {
2713            // NOTE: $USER->site check was obsoleted by session test cookie, $USER->confirmed test is in login/index.php.
2714            if ($preventredirect) {
2715                throw new require_login_exception('You are not logged in');
2716            }
2717
2718            if ($setwantsurltome) {
2719                $SESSION->wantsurl = qualified_me();
2720            }
2721
2722            $referer = get_local_referer(false);
2723            if (!empty($referer)) {
2724                $SESSION->fromurl = $referer;
2725            }
2726
2727            // Give auth plugins an opportunity to authenticate or redirect to an external login page
2728            $authsequence = get_enabled_auth_plugins(); // Auths, in sequence.
2729            foreach($authsequence as $authname) {
2730                $authplugin = get_auth_plugin($authname);
2731                $authplugin->pre_loginpage_hook();
2732                if (isloggedin()) {
2733                    if ($cm) {
2734                        $modinfo = get_fast_modinfo($course);
2735                        $cm = $modinfo->get_cm($cm->id);
2736                    }
2737                    set_access_log_user();
2738                    break;
2739                }
2740            }
2741
2742            // If we're still not logged in then go to the login page
2743            if (!isloggedin()) {
2744                redirect(get_login_url());
2745                exit; // Never reached.
2746            }
2747        }
2748    }
2749
2750    // Loginas as redirection if needed.
2751    if ($course->id != SITEID and \core\session\manager::is_loggedinas()) {
2752        if ($USER->loginascontext->contextlevel == CONTEXT_COURSE) {
2753            if ($USER->loginascontext->instanceid != $course->id) {
2754                print_error('loginasonecourse', '', $CFG->wwwroot.'/course/view.php?id='.$USER->loginascontext->instanceid);
2755            }
2756        }
2757    }
2758
2759    // Check whether the user should be changing password (but only if it is REALLY them).
2760    if (get_user_preferences('auth_forcepasswordchange') && !\core\session\manager::is_loggedinas()) {
2761        $userauth = get_auth_plugin($USER->auth);
2762        if ($userauth->can_change_password() and !$preventredirect) {
2763            if ($setwantsurltome) {
2764                $SESSION->wantsurl = qualified_me();
2765            }
2766            if ($changeurl = $userauth->change_password_url()) {
2767                // Use plugin custom url.
2768                redirect($changeurl);
2769            } else {
2770                // Use moodle internal method.
2771                redirect($CFG->wwwroot .'/login/change_password.php');
2772            }
2773        } else if ($userauth->can_change_password()) {
2774            throw new moodle_exception('forcepasswordchangenotice');
2775        } else {
2776            throw new moodle_exception('nopasswordchangeforced', 'auth');
2777        }
2778    }
2779
2780    // Check that the user account is properly set up. If we can't redirect to
2781    // edit their profile and this is not a WS request, perform just the lax check.
2782    // It will allow them to use filepicker on the profile edit page.
2783
2784    if ($preventredirect && !WS_SERVER) {
2785        $usernotfullysetup = user_not_fully_set_up($USER, false);
2786    } else {
2787        $usernotfullysetup = user_not_fully_set_up($USER, true);
2788    }
2789
2790    if ($usernotfullysetup) {
2791        if ($preventredirect) {
2792            throw new moodle_exception('usernotfullysetup');
2793        }
2794        if ($setwantsurltome) {
2795            $SESSION->wantsurl = qualified_me();
2796        }
2797        redirect($CFG->wwwroot .'/user/edit.php?id='. $USER->id .'&amp;course='. SITEID);
2798    }
2799
2800    // Make sure the USER has a sesskey set up. Used for CSRF protection.
2801    sesskey();
2802
2803    if (\core\session\manager::is_loggedinas()) {
2804        // During a "logged in as" session we should force all content to be cleaned because the
2805        // logged in user will be viewing potentially malicious user generated content.
2806        // See MDL-63786 for more details.
2807        $CFG->forceclean = true;
2808    }
2809
2810    $afterlogins = get_plugins_with_function('after_require_login', 'lib.php');
2811
2812    // Do not bother admins with any formalities, except for activities pending deletion.
2813    if (is_siteadmin() && !($cm && $cm->deletioninprogress)) {
2814        // Set the global $COURSE.
2815        if ($cm) {
2816            $PAGE->set_cm($cm, $course);
2817            $PAGE->set_pagelayout('incourse');
2818        } else if (!empty($courseorid)) {
2819            $PAGE->set_course($course);
2820        }
2821        // Set accesstime or the user will appear offline which messes up messaging.
2822        // Do not update access time for webservice or ajax requests.
2823        if (!WS_SERVER && !AJAX_SCRIPT) {
2824            user_accesstime_log($course->id);
2825        }
2826
2827        foreach ($afterlogins as $plugintype => $plugins) {
2828            foreach ($plugins as $pluginfunction) {
2829                $pluginfunction($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2830            }
2831        }
2832        return;
2833    }
2834
2835    // Scripts have a chance to declare that $USER->policyagreed should not be checked.
2836    // This is mostly for places where users are actually accepting the policies, to avoid the redirect loop.
2837    if (!defined('NO_SITEPOLICY_CHECK')) {
2838        define('NO_SITEPOLICY_CHECK', false);
2839    }
2840
2841    // Check that the user has agreed to a site policy if there is one - do not test in case of admins.
2842    // Do not test if the script explicitly asked for skipping the site policies check.
2843    if (!$USER->policyagreed && !is_siteadmin() && !NO_SITEPOLICY_CHECK) {
2844        $manager = new \core_privacy\local\sitepolicy\manager();
2845        if ($policyurl = $manager->get_redirect_url(isguestuser())) {
2846            if ($preventredirect) {
2847                throw new moodle_exception('sitepolicynotagreed', 'error', '', $policyurl->out());
2848            }
2849            if ($setwantsurltome) {
2850                $SESSION->wantsurl = qualified_me();
2851            }
2852            redirect($policyurl);
2853        }
2854    }
2855
2856    // Fetch the system context, the course context, and prefetch its child contexts.
2857    $sysctx = context_system::instance();
2858    $coursecontext = context_course::instance($course->id, MUST_EXIST);
2859    if ($cm) {
2860        $cmcontext = context_module::instance($cm->id, MUST_EXIST);
2861    } else {
2862        $cmcontext = null;
2863    }
2864
2865    // If the site is currently under maintenance, then print a message.
2866    if (!empty($CFG->maintenance_enabled) and !has_capability('moodle/site:maintenanceaccess', $sysctx)) {
2867        if ($preventredirect) {
2868            throw new require_login_exception('Maintenance in progress');
2869        }
2870        $PAGE->set_context(null);
2871        print_maintenance_message();
2872    }
2873
2874    // Make sure the course itself is not hidden.
2875    if ($course->id == SITEID) {
2876        // Frontpage can not be hidden.
2877    } else {
2878        if (is_role_switched($course->id)) {
2879            // When switching roles ignore the hidden flag - user had to be in course to do the switch.
2880        } else {
2881            if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
2882                // Originally there was also test of parent category visibility, BUT is was very slow in complex queries
2883                // involving "my courses" now it is also possible to simply hide all courses user is not enrolled in :-).
2884                if ($preventredirect) {
2885                    throw new require_login_exception('Course is hidden');
2886                }
2887                $PAGE->set_context(null);
2888                // We need to override the navigation URL as the course won't have been added to the navigation and thus
2889                // the navigation will mess up when trying to find it.
2890                navigation_node::override_active_url(new moodle_url('/'));
2891                notice(get_string('coursehidden'), $CFG->wwwroot .'/');
2892            }
2893        }
2894    }
2895
2896    // Is the user enrolled?
2897    if ($course->id == SITEID) {
2898        // Everybody is enrolled on the frontpage.
2899    } else {
2900        if (\core\session\manager::is_loggedinas()) {
2901            // Make sure the REAL person can access this course first.
2902            $realuser = \core\session\manager::get_realuser();
2903            if (!is_enrolled($coursecontext, $realuser->id, '', true) and
2904                !is_viewing($coursecontext, $realuser->id) and !is_siteadmin($realuser->id)) {
2905                if ($preventredirect) {
2906                    throw new require_login_exception('Invalid course login-as access');
2907                }
2908                $PAGE->set_context(null);
2909                echo $OUTPUT->header();
2910                notice(get_string('studentnotallowed', '', fullname($USER, true)), $CFG->wwwroot .'/');
2911            }
2912        }
2913
2914        $access = false;
2915
2916        if (is_role_switched($course->id)) {
2917            // Ok, user had to be inside this course before the switch.
2918            $access = true;
2919
2920        } else if (is_viewing($coursecontext, $USER)) {
2921            // Ok, no need to mess with enrol.
2922            $access = true;
2923
2924        } else {
2925            if (isset($USER->enrol['enrolled'][$course->id])) {
2926                if ($USER->enrol['enrolled'][$course->id] > time()) {
2927                    $access = true;
2928                    if (isset($USER->enrol['tempguest'][$course->id])) {
2929                        unset($USER->enrol['tempguest'][$course->id]);
2930                        remove_temp_course_roles($coursecontext);
2931                    }
2932                } else {
2933                    // Expired.
2934                    unset($USER->enrol['enrolled'][$course->id]);
2935                }
2936            }
2937            if (isset($USER->enrol['tempguest'][$course->id])) {
2938                if ($USER->enrol['tempguest'][$course->id] == 0) {
2939                    $access = true;
2940                } else if ($USER->enrol['tempguest'][$course->id] > time()) {
2941                    $access = true;
2942                } else {
2943                    // Expired.
2944                    unset($USER->enrol['tempguest'][$course->id]);
2945                    remove_temp_course_roles($coursecontext);
2946                }
2947            }
2948
2949            if (!$access) {
2950                // Cache not ok.
2951                $until = enrol_get_enrolment_end($coursecontext->instanceid, $USER->id);
2952                if ($until !== false) {
2953                    // Active participants may always access, a timestamp in the future, 0 (always) or false.
2954                    if ($until == 0) {
2955                        $until = ENROL_MAX_TIMESTAMP;
2956                    }
2957                    $USER->enrol['enrolled'][$course->id] = $until;
2958                    $access = true;
2959
2960                } else if (core_course_category::can_view_course_info($course)) {
2961                    $params = array('courseid' => $course->id, 'status' => ENROL_INSTANCE_ENABLED);
2962                    $instances = $DB->get_records('enrol', $params, 'sortorder, id ASC');
2963                    $enrols = enrol_get_plugins(true);
2964                    // First ask all enabled enrol instances in course if they want to auto enrol user.
2965                    foreach ($instances as $instance) {
2966                        if (!isset($enrols[$instance->enrol])) {
2967                            continue;
2968                        }
2969                        // Get a duration for the enrolment, a timestamp in the future, 0 (always) or false.
2970                        $until = $enrols[$instance->enrol]->try_autoenrol($instance);
2971                        if ($until !== false) {
2972                            if ($until == 0) {
2973                                $until = ENROL_MAX_TIMESTAMP;
2974                            }
2975                            $USER->enrol['enrolled'][$course->id] = $until;
2976                            $access = true;
2977                            break;
2978                        }
2979                    }
2980                    // If not enrolled yet try to gain temporary guest access.
2981                    if (!$access) {
2982                        foreach ($instances as $instance) {
2983                            if (!isset($enrols[$instance->enrol])) {
2984                                continue;
2985                            }
2986                            // Get a duration for the guest access, a timestamp in the future or false.
2987                            $until = $enrols[$instance->enrol]->try_guestaccess($instance);
2988                            if ($until !== false and $until > time()) {
2989                                $USER->enrol['tempguest'][$course->id] = $until;
2990                                $access = true;
2991                                break;
2992                            }
2993                        }
2994                    }
2995                } else {
2996                    // User is not enrolled and is not allowed to browse courses here.
2997                    if ($preventredirect) {
2998                        throw new require_login_exception('Course is not available');
2999                    }
3000                    $PAGE->set_context(null);
3001                    // We need to override the navigation URL as the course won't have been added to the navigation and thus
3002                    // the navigation will mess up when trying to find it.
3003                    navigation_node::override_active_url(new moodle_url('/'));
3004                    notice(get_string('coursehidden'), $CFG->wwwroot .'/');
3005                }
3006            }
3007        }
3008
3009        if (!$access) {
3010            if ($preventredirect) {
3011                throw new require_login_exception('Not enrolled');
3012            }
3013            if ($setwantsurltome) {
3014                $SESSION->wantsurl = qualified_me();
3015            }
3016            redirect($CFG->wwwroot .'/enrol/index.php?id='. $course->id);
3017        }
3018    }
3019
3020    // Check whether the activity has been scheduled for deletion. If so, then deny access, even for admins.
3021    if ($cm && $cm->deletioninprogress) {
3022        if ($preventredirect) {
3023            throw new moodle_exception('activityisscheduledfordeletion');
3024        }
3025        require_once($CFG->dirroot . '/course/lib.php');
3026        redirect(course_get_url($course), get_string('activityisscheduledfordeletion', 'error'));
3027    }
3028
3029    // Check visibility of activity to current user; includes visible flag, conditional availability, etc.
3030    if ($cm && !$cm->uservisible) {
3031        if ($preventredirect) {
3032            throw new require_login_exception('Activity is hidden');
3033        }
3034        // Get the error message that activity is not available and why (if explanation can be shown to the user).
3035        $PAGE->set_course($course);
3036        $renderer = $PAGE->get_renderer('course');
3037        $message = $renderer->course_section_cm_unavailable_error_message($cm);
3038        redirect(course_get_url($course), $message, null, \core\output\notification::NOTIFY_ERROR);
3039    }
3040
3041    // Set the global $COURSE.
3042    if ($cm) {
3043        $PAGE->set_cm($cm, $course);
3044        $PAGE->set_pagelayout('incourse');
3045    } else if (!empty($courseorid)) {
3046        $PAGE->set_course($course);
3047    }
3048
3049    foreach ($afterlogins as $plugintype => $plugins) {
3050        foreach ($plugins as $pluginfunction) {
3051            $pluginfunction($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3052        }
3053    }
3054
3055    // Finally access granted, update lastaccess times.
3056    // Do not update access time for webservice or ajax requests.
3057    if (!WS_SERVER && !AJAX_SCRIPT) {
3058        user_accesstime_log($course->id);
3059    }
3060}
3061
3062/**
3063 * A convenience function for where we must be logged in as admin
3064 * @return void
3065 */
3066function require_admin() {
3067    require_login(null, false);
3068    require_capability('moodle/site:config', context_system::instance());
3069}
3070
3071/**
3072 * This function just makes sure a user is logged out.
3073 *
3074 * @package    core_access
3075 * @category   access
3076 */
3077function require_logout() {
3078    global $USER, $DB;
3079
3080    if (!isloggedin()) {
3081        // This should not happen often, no need for hooks or events here.
3082        \core\session\manager::terminate_current();
3083        return;
3084    }
3085
3086    // Execute hooks before action.
3087    $authplugins = array();
3088    $authsequence = get_enabled_auth_plugins();
3089    foreach ($authsequence as $authname) {
3090        $authplugins[$authname] = get_auth_plugin($authname);
3091        $authplugins[$authname]->prelogout_hook();
3092    }
3093
3094    // Store info that gets removed during logout.
3095    $sid = session_id();
3096    $event = \core\event\user_loggedout::create(
3097        array(
3098            'userid' => $USER->id,
3099            'objectid' => $USER->id,
3100            'other' => array('sessionid' => $sid),
3101        )
3102    );
3103    if ($session = $DB->get_record('sessions', array('sid'=>$sid))) {
3104        $event->add_record_snapshot('sessions', $session);
3105    }
3106
3107    // Clone of $USER object to be used by auth plugins.
3108    $user = fullclone($USER);
3109
3110    // Delete session record and drop $_SESSION content.
3111    \core\session\manager::terminate_current();
3112
3113    // Trigger event AFTER action.
3114    $event->trigger();
3115
3116    // Hook to execute auth plugins redirection after event trigger.
3117    foreach ($authplugins as $authplugin) {
3118        $authplugin->postlogout_hook($user);
3119    }
3120}
3121
3122/**
3123 * Weaker version of require_login()
3124 *
3125 * This is a weaker version of {@link require_login()} which only requires login
3126 * when called from within a course rather than the site page, unless
3127 * the forcelogin option is turned on.
3128 * @see require_login()
3129 *
3130 * @package    core_access
3131 * @category   access
3132 *
3133 * @param mixed $courseorid The course object or id in question
3134 * @param bool $autologinguest Allow autologin guests if that is wanted
3135 * @param object $cm Course activity module if known
3136 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
3137 *             true. Used to avoid (=false) some scripts (file.php...) to set that variable,
3138 *             in order to keep redirects working properly. MDL-14495
3139 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
3140 * @return void
3141 * @throws coding_exception
3142 */
3143function require_course_login($courseorid, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) {
3144    global $CFG, $PAGE, $SITE;
3145    $issite = ((is_object($courseorid) and $courseorid->id == SITEID)
3146          or (!is_object($courseorid) and $courseorid == SITEID));
3147    if ($issite && !empty($cm) && !($cm instanceof cm_info)) {
3148        // Note: nearly all pages call get_fast_modinfo anyway and it does not make any
3149        // db queries so this is not really a performance concern, however it is obviously
3150        // better if you use get_fast_modinfo to get the cm before calling this.
3151        if (is_object($courseorid)) {
3152            $course = $courseorid;
3153        } else {
3154            $course = clone($SITE);
3155        }
3156        $modinfo = get_fast_modinfo($course);
3157        $cm = $modinfo->get_cm($cm->id);
3158    }
3159    if (!empty($CFG->forcelogin)) {
3160        // Login required for both SITE and courses.
3161        require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3162
3163    } else if ($issite && !empty($cm) and !$cm->uservisible) {
3164        // Always login for hidden activities.
3165        require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3166
3167    } else if (isloggedin() && !isguestuser()) {
3168        // User is already logged in. Make sure the login is complete (user is fully setup, policies agreed).
3169        require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3170
3171    } else if ($issite) {
3172        // Login for SITE not required.
3173        // We still need to instatiate PAGE vars properly so that things that rely on it like navigation function correctly.
3174        if (!empty($courseorid)) {
3175            if (is_object($courseorid)) {
3176                $course = $courseorid;
3177            } else {
3178                $course = clone $SITE;
3179            }
3180            if ($cm) {
3181                if ($cm->course != $course->id) {
3182                    throw new coding_exception('course and cm parameters in require_course_login() call do not match!!');
3183                }
3184                $PAGE->set_cm($cm, $course);
3185                $PAGE->set_pagelayout('incourse');
3186            } else {
3187                $PAGE->set_course($course);
3188            }
3189        } else {
3190            // If $PAGE->course, and hence $PAGE->context, have not already been set up properly, set them up now.
3191            $PAGE->set_course($PAGE->course);
3192        }
3193        // Do not update access time for webservice or ajax requests.
3194        if (!WS_SERVER && !AJAX_SCRIPT) {
3195            user_accesstime_log(SITEID);
3196        }
3197        return;
3198
3199    } else {
3200        // Course login always required.
3201        require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3202    }
3203}
3204
3205/**
3206 * Validates a user key, checking if the key exists, is not expired and the remote ip is correct.
3207 *
3208 * @param  string $keyvalue the key value
3209 * @param  string $script   unique script identifier
3210 * @param  int $instance    instance id
3211 * @return stdClass the key entry in the user_private_key table
3212 * @since Moodle 3.2
3213 * @throws moodle_exception
3214 */
3215function validate_user_key($keyvalue, $script, $instance) {
3216    global $DB;
3217
3218    if (!$key = $DB->get_record('user_private_key', array('script' => $script, 'value' => $keyvalue, 'instance' => $instance))) {
3219        print_error('invalidkey');
3220    }
3221
3222    if (!empty($key->validuntil) and $key->validuntil < time()) {
3223        print_error('expiredkey');
3224    }
3225
3226    if ($key->iprestriction) {
3227        $remoteaddr = getremoteaddr(null);
3228        if (empty($remoteaddr) or !address_in_subnet($remoteaddr, $key->iprestriction)) {
3229            print_error('ipmismatch');
3230        }
3231    }
3232    return $key;
3233}
3234
3235/**
3236 * Require key login. Function terminates with error if key not found or incorrect.
3237 *
3238 * @uses NO_MOODLE_COOKIES
3239 * @uses PARAM_ALPHANUM
3240 * @param string $script unique script identifier
3241 * @param int $instance optional instance id
3242 * @param string $keyvalue The key. If not supplied, this will be fetched from the current session.
3243 * @return int Instance ID
3244 */
3245function require_user_key_login($script, $instance = null, $keyvalue = null) {
3246    global $DB;
3247
3248    if (!NO_MOODLE_COOKIES) {
3249        print_error('sessioncookiesdisable');
3250    }
3251
3252    // Extra safety.
3253    \core\session\manager::write_close();
3254
3255    if (null === $keyvalue) {
3256        $keyvalue = required_param('key', PARAM_ALPHANUM);
3257    }
3258
3259    $key = validate_user_key($keyvalue, $script, $instance);
3260
3261    if (!$user = $DB->get_record('user', array('id' => $key->userid))) {
3262        print_error('invaliduserid');
3263    }
3264
3265    core_user::require_active_user($user, true, true);
3266
3267    // Emulate normal session.
3268    enrol_check_plugins($user);
3269    \core\session\manager::set_user($user);
3270
3271    // Note we are not using normal login.
3272    if (!defined('USER_KEY_LOGIN')) {
3273        define('USER_KEY_LOGIN', true);
3274    }
3275
3276    // Return instance id - it might be empty.
3277    return $key->instance;
3278}
3279
3280/**
3281 * Creates a new private user access key.
3282 *
3283 * @param string $script unique target identifier
3284 * @param int $userid
3285 * @param int $instance optional instance id
3286 * @param string $iprestriction optional ip restricted access
3287 * @param int $validuntil key valid only until given data
3288 * @return string access key value
3289 */
3290function create_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
3291    global $DB;
3292
3293    $key = new stdClass();
3294    $key->script        = $script;
3295    $key->userid        = $userid;
3296    $key->instance      = $instance;
3297    $key->iprestriction = $iprestriction;
3298    $key->validuntil    = $validuntil;
3299    $key->timecreated   = time();
3300
3301    // Something long and unique.
3302    $key->value         = md5($userid.'_'.time().random_string(40));
3303    while ($DB->record_exists('user_private_key', array('value' => $key->value))) {
3304        // Must be unique.
3305        $key->value     = md5($userid.'_'.time().random_string(40));
3306    }
3307    $DB->insert_record('user_private_key', $key);
3308    return $key->value;
3309}
3310
3311/**
3312 * Delete the user's new private user access keys for a particular script.
3313 *
3314 * @param string $script unique target identifier
3315 * @param int $userid
3316 * @return void
3317 */
3318function delete_user_key($script, $userid) {
3319    global $DB;
3320    $DB->delete_records('user_private_key', array('script' => $script, 'userid' => $userid));
3321}
3322
3323/**
3324 * Gets a private user access key (and creates one if one doesn't exist).
3325 *
3326 * @param string $script unique target identifier
3327 * @param int $userid
3328 * @param int $instance optional instance id
3329 * @param string $iprestriction optional ip restricted access
3330 * @param int $validuntil key valid only until given date
3331 * @return string access key value
3332 */
3333function get_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
3334    global $DB;
3335
3336    if ($key = $DB->get_record('user_private_key', array('script' => $script, 'userid' => $userid,
3337                                                         'instance' => $instance, 'iprestriction' => $iprestriction,
3338                                                         'validuntil' => $validuntil))) {
3339        return $key->value;
3340    } else {
3341        return create_user_key($script, $userid, $instance, $iprestriction, $validuntil);
3342    }
3343}
3344
3345
3346/**
3347 * Modify the user table by setting the currently logged in user's last login to now.
3348 *
3349 * @return bool Always returns true
3350 */
3351function update_user_login_times() {
3352    global $USER, $DB;
3353
3354    if (isguestuser()) {
3355        // Do not update guest access times/ips for performance.
3356        return true;
3357    }
3358
3359    $now = time();
3360
3361    $user = new stdClass();
3362    $user->id = $USER->id;
3363
3364    // Make sure all users that logged in have some firstaccess.
3365    if ($USER->firstaccess == 0) {
3366        $USER->firstaccess = $user->firstaccess = $now;
3367    }
3368
3369    // Store the previous current as lastlogin.
3370    $USER->lastlogin = $user->lastlogin = $USER->currentlogin;
3371
3372    $USER->currentlogin = $user->currentlogin = $now;
3373
3374    // Function user_accesstime_log() may not update immediately, better do it here.
3375    $USER->lastaccess = $user->lastaccess = $now;
3376    $USER->lastip = $user->lastip = getremoteaddr();
3377
3378    // Note: do not call user_update_user() here because this is part of the login process,
3379    //       the login event means that these fields were updated.
3380    $DB->update_record('user', $user);
3381    return true;
3382}
3383
3384/**
3385 * Determines if a user has completed setting up their account.
3386 *
3387 * The lax mode (with $strict = false) has been introduced for special cases
3388 * only where we want to skip certain checks intentionally. This is valid in
3389 * certain mnet or ajax scenarios when the user cannot / should not be
3390 * redirected to edit their profile. In most cases, you should perform the
3391 * strict check.
3392 *
3393 * @param stdClass $user A {@link $USER} object to test for the existence of a valid name and email
3394 * @param bool $strict Be more strict and assert id and custom profile fields set, too
3395 * @return bool
3396 */
3397function user_not_fully_set_up($user, $strict = true) {
3398    global $CFG;
3399    require_once($CFG->dirroot.'/user/profile/lib.php');
3400
3401    if (isguestuser($user)) {
3402        return false;
3403    }
3404
3405    if (empty($user->firstname) or empty($user->lastname) or empty($user->email) or over_bounce_threshold($user)) {
3406        return true;
3407    }
3408
3409    if ($strict) {
3410        if (empty($user->id)) {
3411            // Strict mode can be used with existing accounts only.
3412            return true;
3413        }
3414        if (!profile_has_required_custom_fields_set($user->id)) {
3415            return true;
3416        }
3417    }
3418
3419    return false;
3420}
3421
3422/**
3423 * Check whether the user has exceeded the bounce threshold
3424 *
3425 * @param stdClass $user A {@link $USER} object
3426 * @return bool true => User has exceeded bounce threshold
3427 */
3428function over_bounce_threshold($user) {
3429    global $CFG, $DB;
3430
3431    if (empty($CFG->handlebounces)) {
3432        return false;
3433    }
3434
3435    if (empty($user->id)) {
3436        // No real (DB) user, nothing to do here.
3437        return false;
3438    }
3439
3440    // Set sensible defaults.
3441    if (empty($CFG->minbounces)) {
3442        $CFG->minbounces = 10;
3443    }
3444    if (empty($CFG->bounceratio)) {
3445        $CFG->bounceratio = .20;
3446    }
3447    $bouncecount = 0;
3448    $sendcount = 0;
3449    if ($bounce = $DB->get_record('user_preferences', array ('userid' => $user->id, 'name' => 'email_bounce_count'))) {
3450        $bouncecount = $bounce->value;
3451    }
3452    if ($send = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) {
3453        $sendcount = $send->value;
3454    }
3455    return ($bouncecount >= $CFG->minbounces && $bouncecount/$sendcount >= $CFG->bounceratio);
3456}
3457
3458/**
3459 * Used to increment or reset email sent count
3460 *
3461 * @param stdClass $user object containing an id
3462 * @param bool $reset will reset the count to 0
3463 * @return void
3464 */
3465function set_send_count($user, $reset=false) {
3466    global $DB;
3467
3468    if (empty($user->id)) {
3469        // No real (DB) user, nothing to do here.
3470        return;
3471    }
3472
3473    if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) {
3474        $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
3475        $DB->update_record('user_preferences', $pref);
3476    } else if (!empty($reset)) {
3477        // If it's not there and we're resetting, don't bother. Make a new one.
3478        $pref = new stdClass();
3479        $pref->name   = 'email_send_count';
3480        $pref->value  = 1;
3481        $pref->userid = $user->id;
3482        $DB->insert_record('user_preferences', $pref, false);
3483    }
3484}
3485
3486/**
3487 * Increment or reset user's email bounce count
3488 *
3489 * @param stdClass $user object containing an id
3490 * @param bool $reset will reset the count to 0
3491 */
3492function set_bounce_count($user, $reset=false) {
3493    global $DB;
3494
3495    if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_bounce_count'))) {
3496        $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
3497        $DB->update_record('user_preferences', $pref);
3498    } else if (!empty($reset)) {
3499        // If it's not there and we're resetting, don't bother. Make a new one.
3500        $pref = new stdClass();
3501        $pref->name   = 'email_bounce_count';
3502        $pref->value  = 1;
3503        $pref->userid = $user->id;
3504        $DB->insert_record('user_preferences', $pref, false);
3505    }
3506}
3507
3508/**
3509 * Determines if the logged in user is currently moving an activity
3510 *
3511 * @param int $courseid The id of the course being tested
3512 * @return bool
3513 */
3514function ismoving($courseid) {
3515    global $USER;
3516
3517    if (!empty($USER->activitycopy)) {
3518        return ($USER->activitycopycourse == $courseid);
3519    }
3520    return false;
3521}
3522
3523/**
3524 * Returns a persons full name
3525 *
3526 * Given an object containing all of the users name values, this function returns a string with the full name of the person.
3527 * The result may depend on system settings or language. 'override' will force the alternativefullnameformat to be used. In
3528 * English, fullname as well as alternativefullnameformat is set to 'firstname lastname' by default. But you could have
3529 * fullname set to 'firstname lastname' and alternativefullnameformat set to 'firstname middlename alternatename lastname'.
3530 *
3531 * @param stdClass $user A {@link $USER} object to get full name of.
3532 * @param bool $override If true then the alternativefullnameformat format rather than fullnamedisplay format will be used.
3533 * @return string
3534 */
3535function fullname($user, $override=false) {
3536    global $CFG, $SESSION;
3537
3538    if (!isset($user->firstname) and !isset($user->lastname)) {
3539        return '';
3540    }
3541
3542    // Get all of the name fields.
3543    $allnames = get_all_user_name_fields();
3544    if ($CFG->debugdeveloper) {
3545        foreach ($allnames as $allname) {
3546            if (!property_exists($user, $allname)) {
3547                // If all the user name fields are not set in the user object, then notify the programmer that it needs to be fixed.
3548                debugging('You need to update your sql to include additional name fields in the user object.', DEBUG_DEVELOPER);
3549                // Message has been sent, no point in sending the message multiple times.
3550                break;
3551            }
3552        }
3553    }
3554
3555    if (!$override) {
3556        if (!empty($CFG->forcefirstname)) {
3557            $user->firstname = $CFG->forcefirstname;
3558        }
3559        if (!empty($CFG->forcelastname)) {
3560            $user->lastname = $CFG->forcelastname;
3561        }
3562    }
3563
3564    if (!empty($SESSION->fullnamedisplay)) {
3565        $CFG->fullnamedisplay = $SESSION->fullnamedisplay;
3566    }
3567
3568    $template = null;
3569    // If the fullnamedisplay setting is available, set the template to that.
3570    if (isset($CFG->fullnamedisplay)) {
3571        $template = $CFG->fullnamedisplay;
3572    }
3573    // If the template is empty, or set to language, return the language string.
3574    if ((empty($template) || $template == 'language') && !$override) {
3575        return get_string('fullnamedisplay', null, $user);
3576    }
3577
3578    // Check to see if we are displaying according to the alternative full name format.
3579    if ($override) {
3580        if (empty($CFG->alternativefullnameformat) || $CFG->alternativefullnameformat == 'language') {
3581            // Default to show just the user names according to the fullnamedisplay string.
3582            return get_string('fullnamedisplay', null, $user);
3583        } else {
3584            // If the override is true, then change the template to use the complete name.
3585            $template = $CFG->alternativefullnameformat;
3586        }
3587    }
3588
3589    $requirednames = array();
3590    // With each name, see if it is in the display name template, and add it to the required names array if it is.
3591    foreach ($allnames as $allname) {
3592        if (strpos($template, $allname) !== false) {
3593            $requirednames[] = $allname;
3594        }
3595    }
3596
3597    $displayname = $template;
3598    // Switch in the actual data into the template.
3599    foreach ($requirednames as $altname) {
3600        if (isset($user->$altname)) {
3601            // Using empty() on the below if statement causes breakages.
3602            if ((string)$user->$altname == '') {
3603                $displayname = str_replace($altname, 'EMPTY', $displayname);
3604            } else {
3605                $displayname = str_replace($altname, $user->$altname, $displayname);
3606            }
3607        } else {
3608            $displayname = str_replace($altname, 'EMPTY', $displayname);
3609        }
3610    }
3611    // Tidy up any misc. characters (Not perfect, but gets most characters).
3612    // Don't remove the "u" at the end of the first expression unless you want garbled characters when combining hiragana or
3613    // katakana and parenthesis.
3614    $patterns = array();
3615    // This regular expression replacement is to fix problems such as 'James () Kirk' Where 'Tiberius' (middlename) has not been
3616    // filled in by a user.
3617    // The special characters are Japanese brackets that are common enough to make allowances for them (not covered by :punct:).
3618    $patterns[] = '/[[:punct:]「」]*EMPTY[[:punct:]「」]*/u';
3619    // This regular expression is to remove any double spaces in the display name.
3620    $patterns[] = '/\s{2,}/u';
3621    foreach ($patterns as $pattern) {
3622        $displayname = preg_replace($pattern, ' ', $displayname);
3623    }
3624
3625    // Trimming $displayname will help the next check to ensure that we don't have a display name with spaces.
3626    $displayname = trim($displayname);
3627    if (empty($displayname)) {
3628        // Going with just the first name if no alternate fields are filled out. May be changed later depending on what
3629        // people in general feel is a good setting to fall back on.
3630        $displayname = $user->firstname;
3631    }
3632    return $displayname;
3633}
3634
3635/**
3636 * A centralised location for the all name fields. Returns an array / sql string snippet.
3637 *
3638 * @param bool $returnsql True for an sql select field snippet.
3639 * @param string $tableprefix table query prefix to use in front of each field.
3640 * @param string $prefix prefix added to the name fields e.g. authorfirstname.
3641 * @param string $fieldprefix sql field prefix e.g. id AS userid.
3642 * @param bool $order moves firstname and lastname to the top of the array / start of the string.
3643 * @return array|string All name fields.
3644 */
3645function get_all_user_name_fields($returnsql = false, $tableprefix = null, $prefix = null, $fieldprefix = null, $order = false) {
3646    // This array is provided in this order because when called by fullname() (above) if firstname is before
3647    // firstnamephonetic str_replace() will change the wrong placeholder.
3648    $alternatenames = array('firstnamephonetic' => 'firstnamephonetic',
3649                            'lastnamephonetic' => 'lastnamephonetic',
3650                            'middlename' => 'middlename',
3651                            'alternatename' => 'alternatename',
3652                            'firstname' => 'firstname',
3653                            'lastname' => 'lastname');
3654
3655    // Let's add a prefix to the array of user name fields if provided.
3656    if ($prefix) {
3657        foreach ($alternatenames as $key => $altname) {
3658            $alternatenames[$key] = $prefix . $altname;
3659        }
3660    }
3661
3662    // If we want the end result to have firstname and lastname at the front / top of the result.
3663    if ($order) {
3664        // Move the last two elements (firstname, lastname) off the array and put them at the top.
3665        for ($i = 0; $i < 2; $i++) {
3666            // Get the last element.
3667            $lastelement = end($alternatenames);
3668            // Remove it from the array.
3669            unset($alternatenames[$lastelement]);
3670            // Put the element back on the top of the array.
3671            $alternatenames = array_merge(array($lastelement => $lastelement), $alternatenames);
3672        }
3673    }
3674
3675    // Create an sql field snippet if requested.
3676    if ($returnsql) {
3677        if ($tableprefix) {
3678            if ($fieldprefix) {
3679                foreach ($alternatenames as $key => $altname) {
3680                    $alternatenames[$key] = $tableprefix . '.' . $altname . ' AS ' . $fieldprefix . $altname;
3681                }
3682            } else {
3683                foreach ($alternatenames as $key => $altname) {
3684                    $alternatenames[$key] = $tableprefix . '.' . $altname;
3685                }
3686            }
3687        }
3688        $alternatenames = implode(',', $alternatenames);
3689    }
3690    return $alternatenames;
3691}
3692
3693/**
3694 * Reduces lines of duplicated code for getting user name fields.
3695 *
3696 * See also {@link user_picture::unalias()}
3697 *
3698 * @param object $addtoobject Object to add user name fields to.
3699 * @param object $secondobject Object that contains user name field information.
3700 * @param string $prefix prefix to be added to all fields (including $additionalfields) e.g. authorfirstname.
3701 * @param array $additionalfields Additional fields to be matched with data in the second object.
3702 * The key can be set to the user table field name.
3703 * @return object User name fields.
3704 */
3705function username_load_fields_from_object($addtoobject, $secondobject, $prefix = null, $additionalfields = null) {
3706    $fields = get_all_user_name_fields(false, null, $prefix);
3707    if ($additionalfields) {
3708        // Additional fields can specify their own 'alias' such as 'id' => 'userid'. This checks to see if
3709        // the key is a number and then sets the key to the array value.
3710        foreach ($additionalfields as $key => $value) {
3711            if (is_numeric($key)) {
3712                $additionalfields[$value] = $prefix . $value;
3713                unset($additionalfields[$key]);
3714            } else {
3715                $additionalfields[$key] = $prefix . $value;
3716            }
3717        }
3718        $fields = array_merge($fields, $additionalfields);
3719    }
3720    foreach ($fields as $key => $field) {
3721        // Important that we have all of the user name fields present in the object that we are sending back.
3722        $addtoobject->$key = '';
3723        if (isset($secondobject->$field)) {
3724            $addtoobject->$key = $secondobject->$field;
3725        }
3726    }
3727    return $addtoobject;
3728}
3729
3730/**
3731 * Returns an array of values in order of occurance in a provided string.
3732 * The key in the result is the character postion in the string.
3733 *
3734 * @param array $values Values to be found in the string format
3735 * @param string $stringformat The string which may contain values being searched for.
3736 * @return array An array of values in order according to placement in the string format.
3737 */
3738function order_in_string($values, $stringformat) {
3739    $valuearray = array();
3740    foreach ($values as $value) {
3741        $pattern = "/$value\b/";
3742        // Using preg_match as strpos() may match values that are similar e.g. firstname and firstnamephonetic.
3743        if (preg_match($pattern, $stringformat)) {
3744            $replacement = "thing";
3745            // Replace the value with something more unique to ensure we get the right position when using strpos().
3746            $newformat = preg_replace($pattern, $replacement, $stringformat);
3747            $position = strpos($newformat, $replacement);
3748            $valuearray[$position] = $value;
3749        }
3750    }
3751    ksort($valuearray);
3752    return $valuearray;
3753}
3754
3755/**
3756 * Checks if current user is shown any extra fields when listing users.
3757 *
3758 * @param object $context Context
3759 * @param array $already Array of fields that we're going to show anyway
3760 *   so don't bother listing them
3761 * @return array Array of field names from user table, not including anything
3762 *   listed in $already
3763 */
3764function get_extra_user_fields($context, $already = array()) {
3765    global $CFG;
3766
3767    // Only users with permission get the extra fields.
3768    if (!has_capability('moodle/site:viewuseridentity', $context)) {
3769        return array();
3770    }
3771
3772    // Split showuseridentity on comma (filter needed in case the showuseridentity is empty).
3773    $extra = array_filter(explode(',', $CFG->showuseridentity));
3774
3775    foreach ($extra as $key => $field) {
3776        if (in_array($field, $already)) {
3777            unset($extra[$key]);
3778        }
3779    }
3780
3781    // If the identity fields are also among hidden fields, make sure the user can see them.
3782    $hiddenfields = array_filter(explode(',', $CFG->hiddenuserfields));
3783    $hiddenidentifiers = array_intersect($extra, $hiddenfields);
3784
3785    if ($hiddenidentifiers) {
3786        if ($context->get_course_context(false)) {
3787            // We are somewhere inside a course.
3788            $canviewhiddenuserfields = has_capability('moodle/course:viewhiddenuserfields', $context);
3789
3790        } else {
3791            // We are not inside a course.
3792            $canviewhiddenuserfields = has_capability('moodle/user:viewhiddendetails', $context);
3793        }
3794
3795        if (!$canviewhiddenuserfields) {
3796            // Remove hidden identifiers from the list.
3797            $extra = array_diff($extra, $hiddenidentifiers);
3798        }
3799    }
3800
3801    // Re-index the entries.
3802    $extra = array_values($extra);
3803
3804    return $extra;
3805}
3806
3807/**
3808 * If the current user is to be shown extra user fields when listing or
3809 * selecting users, returns a string suitable for including in an SQL select
3810 * clause to retrieve those fields.
3811 *
3812 * @param context $context Context
3813 * @param string $alias Alias of user table, e.g. 'u' (default none)
3814 * @param string $prefix Prefix for field names using AS, e.g. 'u_' (default none)
3815 * @param array $already Array of fields that we're going to include anyway so don't list them (default none)
3816 * @return string Partial SQL select clause, beginning with comma, for example ',u.idnumber,u.department' unless it is blank
3817 */
3818function get_extra_user_fields_sql($context, $alias='', $prefix='', $already = array()) {
3819    $fields = get_extra_user_fields($context, $already);
3820    $result = '';
3821    // Add punctuation for alias.
3822    if ($alias !== '') {
3823        $alias .= '.';
3824    }
3825    foreach ($fields as $field) {
3826        $result .= ', ' . $alias . $field;
3827        if ($prefix) {
3828            $result .= ' AS ' . $prefix . $field;
3829        }
3830    }
3831    return $result;
3832}
3833
3834/**
3835 * Returns the display name of a field in the user table. Works for most fields that are commonly displayed to users.
3836 * @param string $field Field name, e.g. 'phone1'
3837 * @return string Text description taken from language file, e.g. 'Phone number'
3838 */
3839function get_user_field_name($field) {
3840    // Some fields have language strings which are not the same as field name.
3841    switch ($field) {
3842        case 'url' : {
3843            return get_string('webpage');
3844        }
3845        case 'icq' : {
3846            return get_string('icqnumber');
3847        }
3848        case 'skype' : {
3849            return get_string('skypeid');
3850        }
3851        case 'aim' : {
3852            return get_string('aimid');
3853        }
3854        case 'yahoo' : {
3855            return get_string('yahooid');
3856        }
3857        case 'msn' : {
3858            return get_string('msnid');
3859        }
3860        case 'picture' : {
3861            return get_string('pictureofuser');
3862        }
3863    }
3864    // Otherwise just use the same lang string.
3865    return get_string($field);
3866}
3867
3868/**
3869 * Returns whether a given authentication plugin exists.
3870 *
3871 * @param string $auth Form of authentication to check for. Defaults to the global setting in {@link $CFG}.
3872 * @return boolean Whether the plugin is available.
3873 */
3874function exists_auth_plugin($auth) {
3875    global $CFG;
3876
3877    if (file_exists("{$CFG->dirroot}/auth/$auth/auth.php")) {
3878        return is_readable("{$CFG->dirroot}/auth/$auth/auth.php");
3879    }
3880    return false;
3881}
3882
3883/**
3884 * Checks if a given plugin is in the list of enabled authentication plugins.
3885 *
3886 * @param string $auth Authentication plugin.
3887 * @return boolean Whether the plugin is enabled.
3888 */
3889function is_enabled_auth($auth) {
3890    if (empty($auth)) {
3891        return false;
3892    }
3893
3894    $enabled = get_enabled_auth_plugins();
3895
3896    return in_array($auth, $enabled);
3897}
3898
3899/**
3900 * Returns an authentication plugin instance.
3901 *
3902 * @param string $auth name of authentication plugin
3903 * @return auth_plugin_base An instance of the required authentication plugin.
3904 */
3905function get_auth_plugin($auth) {
3906    global $CFG;
3907
3908    // Check the plugin exists first.
3909    if (! exists_auth_plugin($auth)) {
3910        print_error('authpluginnotfound', 'debug', '', $auth);
3911    }
3912
3913    // Return auth plugin instance.
3914    require_once("{$CFG->dirroot}/auth/$auth/auth.php");
3915    $class = "auth_plugin_$auth";
3916    return new $class;
3917}
3918
3919/**
3920 * Returns array of active auth plugins.
3921 *
3922 * @param bool $fix fix $CFG->auth if needed. Only set if logged in as admin.
3923 * @return array
3924 */
3925function get_enabled_auth_plugins($fix=false) {
3926    global $CFG;
3927
3928    $default = array('manual', 'nologin');
3929
3930    if (empty($CFG->auth)) {
3931        $auths = array();
3932    } else {
3933        $auths = explode(',', $CFG->auth);
3934    }
3935
3936    $auths = array_unique($auths);
3937    $oldauthconfig = implode(',', $auths);
3938    foreach ($auths as $k => $authname) {
3939        if (in_array($authname, $default)) {
3940            // The manual and nologin plugin never need to be stored.
3941            unset($auths[$k]);
3942        } else if (!exists_auth_plugin($authname)) {
3943            debugging(get_string('authpluginnotfound', 'debug', $authname));
3944            unset($auths[$k]);
3945        }
3946    }
3947
3948    // Ideally only explicit interaction from a human admin should trigger a
3949    // change in auth config, see MDL-70424 for details.
3950    if ($fix) {
3951        $newconfig = implode(',', $auths);
3952        if (!isset($CFG->auth) or $newconfig != $CFG->auth) {
3953            set_config('auth', $newconfig);
3954        }
3955    }
3956
3957    return (array_merge($default, $auths));
3958}
3959
3960/**
3961 * Returns true if an internal authentication method is being used.
3962 * if method not specified then, global default is assumed
3963 *
3964 * @param string $auth Form of authentication required
3965 * @return bool
3966 */
3967function is_internal_auth($auth) {
3968    // Throws error if bad $auth.
3969    $authplugin = get_auth_plugin($auth);
3970    return $authplugin->is_internal();
3971}
3972
3973/**
3974 * Returns true if the user is a 'restored' one.
3975 *
3976 * Used in the login process to inform the user and allow him/her to reset the password
3977 *
3978 * @param string $username username to be checked
3979 * @return bool
3980 */
3981function is_restored_user($username) {
3982    global $CFG, $DB;
3983
3984    return $DB->record_exists('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id, 'password' => 'restored'));
3985}
3986
3987/**
3988 * Returns an array of user fields
3989 *
3990 * @return array User field/column names
3991 */
3992function get_user_fieldnames() {
3993    global $DB;
3994
3995    $fieldarray = $DB->get_columns('user');
3996    unset($fieldarray['id']);
3997    $fieldarray = array_keys($fieldarray);
3998
3999    return $fieldarray;
4000}
4001
4002/**
4003 * Returns the string of the language for the new user.
4004 *
4005 * @return string language for the new user
4006 */
4007function get_newuser_language() {
4008    global $CFG, $SESSION;
4009    return (!empty($CFG->autolangusercreation) && !empty($SESSION->lang)) ? $SESSION->lang : $CFG->lang;
4010}
4011
4012/**
4013 * Creates a bare-bones user record
4014 *
4015 * @todo Outline auth types and provide code example
4016 *
4017 * @param string $username New user's username to add to record
4018 * @param string $password New user's password to add to record
4019 * @param string $auth Form of authentication required
4020 * @return stdClass A complete user object
4021 */
4022function create_user_record($username, $password, $auth = 'manual') {
4023    global $CFG, $DB, $SESSION;
4024    require_once($CFG->dirroot.'/user/profile/lib.php');
4025    require_once($CFG->dirroot.'/user/lib.php');
4026
4027    // Just in case check text case.
4028    $username = trim(core_text::strtolower($username));
4029
4030    $authplugin = get_auth_plugin($auth);
4031    $customfields = $authplugin->get_custom_user_profile_fields();
4032    $newuser = new stdClass();
4033    if ($newinfo = $authplugin->get_userinfo($username)) {
4034        $newinfo = truncate_userinfo($newinfo);
4035        foreach ($newinfo as $key => $value) {
4036            if (in_array($key, $authplugin->userfields) || (in_array($key, $customfields))) {
4037                $newuser->$key = $value;
4038            }
4039        }
4040    }
4041
4042    if (!empty($newuser->email)) {
4043        if (email_is_not_allowed($newuser->email)) {
4044            unset($newuser->email);
4045        }
4046    }
4047
4048    if (!isset($newuser->city)) {
4049        $newuser->city = '';
4050    }
4051
4052    $newuser->auth = $auth;
4053    $newuser->username = $username;
4054
4055    // Fix for MDL-8480
4056    // user CFG lang for user if $newuser->lang is empty
4057    // or $user->lang is not an installed language.
4058    if (empty($newuser->lang) || !get_string_manager()->translation_exists($newuser->lang)) {
4059        $newuser->lang = get_newuser_language();
4060    }
4061    $newuser->confirmed = 1;
4062    $newuser->lastip = getremoteaddr();
4063    $newuser->timecreated = time();
4064    $newuser->timemodified = $newuser->timecreated;
4065    $newuser->mnethostid = $CFG->mnet_localhost_id;
4066
4067    $newuser->id = user_create_user($newuser, false, false);
4068
4069    // Save user profile data.
4070    profile_save_data($newuser);
4071
4072    $user = get_complete_user_data('id', $newuser->id);
4073    if (!empty($CFG->{'auth_'.$newuser->auth.'_forcechangepassword'})) {
4074        set_user_preference('auth_forcepasswordchange', 1, $user);
4075    }
4076    // Set the password.
4077    update_internal_user_password($user, $password);
4078
4079    // Trigger event.
4080    \core\event\user_created::create_from_userid($newuser->id)->trigger();
4081
4082    return $user;
4083}
4084
4085/**
4086 * Will update a local user record from an external source (MNET users can not be updated using this method!).
4087 *
4088 * @param string $username user's username to update the record
4089 * @return stdClass A complete user object
4090 */
4091function update_user_record($username) {
4092    global $DB, $CFG;
4093    // Just in case check text case.
4094    $username = trim(core_text::strtolower($username));
4095
4096    $oldinfo = $DB->get_record('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id), '*', MUST_EXIST);
4097    return update_user_record_by_id($oldinfo->id);
4098}
4099
4100/**
4101 * Will update a local user record from an external source (MNET users can not be updated using this method!).
4102 *
4103 * @param int $id user id
4104 * @return stdClass A complete user object
4105 */
4106function update_user_record_by_id($id) {
4107    global $DB, $CFG;
4108    require_once($CFG->dirroot."/user/profile/lib.php");
4109    require_once($CFG->dirroot.'/user/lib.php');
4110
4111    $params = array('mnethostid' => $CFG->mnet_localhost_id, 'id' => $id, 'deleted' => 0);
4112    $oldinfo = $DB->get_record('user', $params, '*', MUST_EXIST);
4113
4114    $newuser = array();
4115    $userauth = get_auth_plugin($oldinfo->auth);
4116
4117    if ($newinfo = $userauth->get_userinfo($oldinfo->username)) {
4118        $newinfo = truncate_userinfo($newinfo);
4119        $customfields = $userauth->get_custom_user_profile_fields();
4120
4121        foreach ($newinfo as $key => $value) {
4122            $iscustom = in_array($key, $customfields);
4123            if (!$iscustom) {
4124                $key = strtolower($key);
4125            }
4126            if ((!property_exists($oldinfo, $key) && !$iscustom) or $key === 'username' or $key === 'id'
4127                    or $key === 'auth' or $key === 'mnethostid' or $key === 'deleted') {
4128                // Unknown or must not be changed.
4129                continue;
4130            }
4131            if (empty($userauth->config->{'field_updatelocal_' . $key}) || empty($userauth->config->{'field_lock_' . $key})) {
4132                continue;
4133            }
4134            $confval = $userauth->config->{'field_updatelocal_' . $key};
4135            $lockval = $userauth->config->{'field_lock_' . $key};
4136            if ($confval === 'onlogin') {
4137                // MDL-4207 Don't overwrite modified user profile values with
4138                // empty LDAP values when 'unlocked if empty' is set. The purpose
4139                // of the setting 'unlocked if empty' is to allow the user to fill
4140                // in a value for the selected field _if LDAP is giving
4141                // nothing_ for this field. Thus it makes sense to let this value
4142                // stand in until LDAP is giving a value for this field.
4143                if (!(empty($value) && $lockval === 'unlockedifempty')) {
4144                    if ($iscustom || (in_array($key, $userauth->userfields) &&
4145                            ((string)$oldinfo->$key !== (string)$value))) {
4146                        $newuser[$key] = (string)$value;
4147                    }
4148                }
4149            }
4150        }
4151        if ($newuser) {
4152            $newuser['id'] = $oldinfo->id;
4153            $newuser['timemodified'] = time();
4154            user_update_user((object) $newuser, false, false);
4155
4156            // Save user profile data.
4157            profile_save_data((object) $newuser);
4158
4159            // Trigger event.
4160            \core\event\user_updated::create_from_userid($newuser['id'])->trigger();
4161        }
4162    }
4163
4164    return get_complete_user_data('id', $oldinfo->id);
4165}
4166
4167/**
4168 * Will truncate userinfo as it comes from auth_get_userinfo (from external auth) which may have large fields.
4169 *
4170 * @param array $info Array of user properties to truncate if needed
4171 * @return array The now truncated information that was passed in
4172 */
4173function truncate_userinfo(array $info) {
4174    // Define the limits.
4175    $limit = array(
4176        'username'    => 100,
4177        'idnumber'    => 255,
4178        'firstname'   => 100,
4179        'lastname'    => 100,
4180        'email'       => 100,
4181        'icq'         =>  15,
4182        'phone1'      =>  20,
4183        'phone2'      =>  20,
4184        'institution' => 255,
4185        'department'  => 255,
4186        'address'     => 255,
4187        'city'        => 120,
4188        'country'     =>   2,
4189        'url'         => 255,
4190    );
4191
4192    // Apply where needed.
4193    foreach (array_keys($info) as $key) {
4194        if (!empty($limit[$key])) {
4195            $info[$key] = trim(core_text::substr($info[$key], 0, $limit[$key]));
4196        }
4197    }
4198
4199    return $info;
4200}
4201
4202/**
4203 * Marks user deleted in internal user database and notifies the auth plugin.
4204 * Also unenrols user from all roles and does other cleanup.
4205 *
4206 * Any plugin that needs to purge user data should register the 'user_deleted' event.
4207 *
4208 * @param stdClass $user full user object before delete
4209 * @return boolean success
4210 * @throws coding_exception if invalid $user parameter detected
4211 */
4212function delete_user(stdClass $user) {
4213    global $CFG, $DB, $SESSION;
4214    require_once($CFG->libdir.'/grouplib.php');
4215    require_once($CFG->libdir.'/gradelib.php');
4216    require_once($CFG->dirroot.'/message/lib.php');
4217    require_once($CFG->dirroot.'/user/lib.php');
4218
4219    // Make sure nobody sends bogus record type as parameter.
4220    if (!property_exists($user, 'id') or !property_exists($user, 'username')) {
4221        throw new coding_exception('Invalid $user parameter in delete_user() detected');
4222    }
4223
4224    // Better not trust the parameter and fetch the latest info this will be very expensive anyway.
4225    if (!$user = $DB->get_record('user', array('id' => $user->id))) {
4226        debugging('Attempt to delete unknown user account.');
4227        return false;
4228    }
4229
4230    // There must be always exactly one guest record, originally the guest account was identified by username only,
4231    // now we use $CFG->siteguest for performance reasons.
4232    if ($user->username === 'guest' or isguestuser($user)) {
4233        debugging('Guest user account can not be deleted.');
4234        return false;
4235    }
4236
4237    // Admin can be theoretically from different auth plugin, but we want to prevent deletion of internal accoutns only,
4238    // if anything goes wrong ppl may force somebody to be admin via config.php setting $CFG->siteadmins.
4239    if ($user->auth === 'manual' and is_siteadmin($user)) {
4240        debugging('Local administrator accounts can not be deleted.');
4241        return false;
4242    }
4243
4244    // Allow plugins to use this user object before we completely delete it.
4245    if ($pluginsfunction = get_plugins_with_function('pre_user_delete')) {
4246        foreach ($pluginsfunction as $plugintype => $plugins) {
4247            foreach ($plugins as $pluginfunction) {
4248                $pluginfunction($user);
4249            }
4250        }
4251    }
4252
4253    // Keep user record before updating it, as we have to pass this to user_deleted event.
4254    $olduser = clone $user;
4255
4256    // Keep a copy of user context, we need it for event.
4257    $usercontext = context_user::instance($user->id);
4258
4259    // Delete all grades - backup is kept in grade_grades_history table.
4260    grade_user_delete($user->id);
4261
4262    // TODO: remove from cohorts using standard API here.
4263
4264    // Remove user tags.
4265    core_tag_tag::remove_all_item_tags('core', 'user', $user->id);
4266
4267    // Unconditionally unenrol from all courses.
4268    enrol_user_delete($user);
4269
4270    // Unenrol from all roles in all contexts.
4271    // This might be slow but it is really needed - modules might do some extra cleanup!
4272    role_unassign_all(array('userid' => $user->id));
4273
4274    // Notify the competency subsystem.
4275    \core_competency\api::hook_user_deleted($user->id);
4276
4277    // Now do a brute force cleanup.
4278
4279    // Delete all user events and subscription events.
4280    $DB->delete_records_select('event', 'userid = :userid AND subscriptionid IS NOT NULL', ['userid' => $user->id]);
4281
4282    // Now, delete all calendar subscription from the user.
4283    $DB->delete_records('event_subscriptions', ['userid' => $user->id]);
4284
4285    // Remove from all cohorts.
4286    $DB->delete_records('cohort_members', array('userid' => $user->id));
4287
4288    // Remove from all groups.
4289    $DB->delete_records('groups_members', array('userid' => $user->id));
4290
4291    // Brute force unenrol from all courses.
4292    $DB->delete_records('user_enrolments', array('userid' => $user->id));
4293
4294    // Purge user preferences.
4295    $DB->delete_records('user_preferences', array('userid' => $user->id));
4296
4297    // Purge user extra profile info.
4298    $DB->delete_records('user_info_data', array('userid' => $user->id));
4299
4300    // Purge log of previous password hashes.
4301    $DB->delete_records('user_password_history', array('userid' => $user->id));
4302
4303    // Last course access not necessary either.
4304    $DB->delete_records('user_lastaccess', array('userid' => $user->id));
4305    // Remove all user tokens.
4306    $DB->delete_records('external_tokens', array('userid' => $user->id));
4307
4308    // Unauthorise the user for all services.
4309    $DB->delete_records('external_services_users', array('userid' => $user->id));
4310
4311    // Remove users private keys.
4312    $DB->delete_records('user_private_key', array('userid' => $user->id));
4313
4314    // Remove users customised pages.
4315    $DB->delete_records('my_pages', array('userid' => $user->id, 'private' => 1));
4316
4317    // Remove user's oauth2 refresh tokens, if present.
4318    $DB->delete_records('oauth2_refresh_token', array('userid' => $user->id));
4319
4320    // Delete user from $SESSION->bulk_users.
4321    if (isset($SESSION->bulk_users[$user->id])) {
4322        unset($SESSION->bulk_users[$user->id]);
4323    }
4324
4325    // Force logout - may fail if file based sessions used, sorry.
4326    \core\session\manager::kill_user_sessions($user->id);
4327
4328    // Generate username from email address, or a fake email.
4329    $delemail = !empty($user->email) ? $user->email : $user->username . '.' . $user->id . '@unknownemail.invalid';
4330
4331    $deltime = time();
4332    $deltimelength = core_text::strlen((string) $deltime);
4333
4334    // Max username length is 100 chars. Select up to limit - (length of current time + 1 [period character]) from users email.
4335    $delname = clean_param($delemail, PARAM_USERNAME);
4336    $delname = core_text::substr($delname, 0, 100 - ($deltimelength + 1)) . ".{$deltime}";
4337
4338    // Workaround for bulk deletes of users with the same email address.
4339    while ($DB->record_exists('user', array('username' => $delname))) { // No need to use mnethostid here.
4340        $delname++;
4341    }
4342
4343    // Mark internal user record as "deleted".
4344    $updateuser = new stdClass();
4345    $updateuser->id           = $user->id;
4346    $updateuser->deleted      = 1;
4347    $updateuser->username     = $delname;            // Remember it just in case.
4348    $updateuser->email        = md5($user->username);// Store hash of username, useful importing/restoring users.
4349    $updateuser->idnumber     = '';                  // Clear this field to free it up.
4350    $updateuser->picture      = 0;
4351    $updateuser->timemodified = $deltime;
4352
4353    // Don't trigger update event, as user is being deleted.
4354    user_update_user($updateuser, false, false);
4355
4356    // Delete all content associated with the user context, but not the context itself.
4357    $usercontext->delete_content();
4358
4359    // Delete any search data.
4360    \core_search\manager::context_deleted($usercontext);
4361
4362    // Any plugin that needs to cleanup should register this event.
4363    // Trigger event.
4364    $event = \core\event\user_deleted::create(
4365            array(
4366                'objectid' => $user->id,
4367                'relateduserid' => $user->id,
4368                'context' => $usercontext,
4369                'other' => array(
4370                    'username' => $user->username,
4371                    'email' => $user->email,
4372                    'idnumber' => $user->idnumber,
4373                    'picture' => $user->picture,
4374                    'mnethostid' => $user->mnethostid
4375                    )
4376                )
4377            );
4378    $event->add_record_snapshot('user', $olduser);
4379    $event->trigger();
4380
4381    // We will update the user's timemodified, as it will be passed to the user_deleted event, which
4382    // should know about this updated property persisted to the user's table.
4383    $user->timemodified = $updateuser->timemodified;
4384
4385    // Notify auth plugin - do not block the delete even when plugin fails.
4386    $authplugin = get_auth_plugin($user->auth);
4387    $authplugin->user_delete($user);
4388
4389    return true;
4390}
4391
4392/**
4393 * Retrieve the guest user object.
4394 *
4395 * @return stdClass A {@link $USER} object
4396 */
4397function guest_user() {
4398    global $CFG, $DB;
4399
4400    if ($newuser = $DB->get_record('user', array('id' => $CFG->siteguest))) {
4401        $newuser->confirmed = 1;
4402        $newuser->lang = get_newuser_language();
4403        $newuser->lastip = getremoteaddr();
4404    }
4405
4406    return $newuser;
4407}
4408
4409/**
4410 * Authenticates a user against the chosen authentication mechanism
4411 *
4412 * Given a username and password, this function looks them
4413 * up using the currently selected authentication mechanism,
4414 * and if the authentication is successful, it returns a
4415 * valid $user object from the 'user' table.
4416 *
4417 * Uses auth_ functions from the currently active auth module
4418 *
4419 * After authenticate_user_login() returns success, you will need to
4420 * log that the user has logged in, and call complete_user_login() to set
4421 * the session up.
4422 *
4423 * Note: this function works only with non-mnet accounts!
4424 *
4425 * @param string $username  User's username (or also email if $CFG->authloginviaemail enabled)
4426 * @param string $password  User's password
4427 * @param bool $ignorelockout useful when guessing is prevented by other mechanism such as captcha or SSO
4428 * @param int $failurereason login failure reason, can be used in renderers (it may disclose if account exists)
4429 * @param mixed logintoken If this is set to a string it is validated against the login token for the session.
4430 * @return stdClass|false A {@link $USER} object or false if error
4431 */
4432function authenticate_user_login($username, $password, $ignorelockout=false, &$failurereason=null, $logintoken=false) {
4433    global $CFG, $DB, $PAGE;
4434    require_once("$CFG->libdir/authlib.php");
4435
4436    if ($user = get_complete_user_data('username', $username, $CFG->mnet_localhost_id)) {
4437        // we have found the user
4438
4439    } else if (!empty($CFG->authloginviaemail)) {
4440        if ($email = clean_param($username, PARAM_EMAIL)) {
4441            $select = "mnethostid = :mnethostid AND LOWER(email) = LOWER(:email) AND deleted = 0";
4442            $params = array('mnethostid' => $CFG->mnet_localhost_id, 'email' => $email);
4443            $users = $DB->get_records_select('user', $select, $params, 'id', 'id', 0, 2);
4444            if (count($users) === 1) {
4445                // Use email for login only if unique.
4446                $user = reset($users);
4447                $user = get_complete_user_data('id', $user->id);
4448                $username = $user->username;
4449            }
4450            unset($users);
4451        }
4452    }
4453
4454    // Make sure this request came from the login form.
4455    if (!\core\session\manager::validate_login_token($logintoken)) {
4456        $failurereason = AUTH_LOGIN_FAILED;
4457
4458        // Trigger login failed event (specifying the ID of the found user, if available).
4459        \core\event\user_login_failed::create([
4460            'userid' => ($user->id ?? 0),
4461            'other' => [
4462                'username' => $username,
4463                'reason' => $failurereason,
4464            ],
4465        ])->trigger();
4466
4467        error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Invalid Login Token:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4468        return false;
4469    }
4470
4471    $authsenabled = get_enabled_auth_plugins();
4472
4473    if ($user) {
4474        // Use manual if auth not set.
4475        $auth = empty($user->auth) ? 'manual' : $user->auth;
4476
4477        if (in_array($user->auth, $authsenabled)) {
4478            $authplugin = get_auth_plugin($user->auth);
4479            $authplugin->pre_user_login_hook($user);
4480        }
4481
4482        if (!empty($user->suspended)) {
4483            $failurereason = AUTH_LOGIN_SUSPENDED;
4484
4485            // Trigger login failed event.
4486            $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4487                    'other' => array('username' => $username, 'reason' => $failurereason)));
4488            $event->trigger();
4489            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Suspended Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4490            return false;
4491        }
4492        if ($auth=='nologin' or !is_enabled_auth($auth)) {
4493            // Legacy way to suspend user.
4494            $failurereason = AUTH_LOGIN_SUSPENDED;
4495
4496            // Trigger login failed event.
4497            $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4498                    'other' => array('username' => $username, 'reason' => $failurereason)));
4499            $event->trigger();
4500            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Disabled Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4501            return false;
4502        }
4503        $auths = array($auth);
4504
4505    } else {
4506        // Check if there's a deleted record (cheaply), this should not happen because we mangle usernames in delete_user().
4507        if ($DB->get_field('user', 'id', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id,  'deleted' => 1))) {
4508            $failurereason = AUTH_LOGIN_NOUSER;
4509
4510            // Trigger login failed event.
4511            $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4512                    'reason' => $failurereason)));
4513            $event->trigger();
4514            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Deleted Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4515            return false;
4516        }
4517
4518        // User does not exist.
4519        $auths = $authsenabled;
4520        $user = new stdClass();
4521        $user->id = 0;
4522    }
4523
4524    if ($ignorelockout) {
4525        // Some other mechanism protects against brute force password guessing, for example login form might include reCAPTCHA
4526        // or this function is called from a SSO script.
4527    } else if ($user->id) {
4528        // Verify login lockout after other ways that may prevent user login.
4529        if (login_is_lockedout($user)) {
4530            $failurereason = AUTH_LOGIN_LOCKOUT;
4531
4532            // Trigger login failed event.
4533            $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4534                    'other' => array('username' => $username, 'reason' => $failurereason)));
4535            $event->trigger();
4536
4537            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Login lockout:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4538            return false;
4539        }
4540    } else {
4541        // We can not lockout non-existing accounts.
4542    }
4543
4544    foreach ($auths as $auth) {
4545        $authplugin = get_auth_plugin($auth);
4546
4547        // On auth fail fall through to the next plugin.
4548        if (!$authplugin->user_login($username, $password)) {
4549            continue;
4550        }
4551
4552        // Before performing login actions, check if user still passes password policy, if admin setting is enabled.
4553        if (!empty($CFG->passwordpolicycheckonlogin)) {
4554            $errmsg = '';
4555            $passed = check_password_policy($password, $errmsg, $user);
4556            if (!$passed) {
4557                // First trigger event for failure.
4558                $failedevent = \core\event\user_password_policy_failed::create_from_user($user);
4559                $failedevent->trigger();
4560
4561                // If able to change password, set flag and move on.
4562                if ($authplugin->can_change_password()) {
4563                    // Check if we are on internal change password page, or service is external, don't show notification.
4564                    $internalchangeurl = new moodle_url('/login/change_password.php');
4565                    if (!($PAGE->has_set_url() && $internalchangeurl->compare($PAGE->url)) && $authplugin->is_internal()) {
4566                        \core\notification::error(get_string('passwordpolicynomatch', '', $errmsg));
4567                    }
4568                    set_user_preference('auth_forcepasswordchange', 1, $user);
4569                } else if ($authplugin->can_reset_password()) {
4570                    // Else force a reset if possible.
4571                    \core\notification::error(get_string('forcepasswordresetnotice', '', $errmsg));
4572                    redirect(new moodle_url('/login/forgot_password.php'));
4573                } else {
4574                    $notifymsg = get_string('forcepasswordresetfailurenotice', '', $errmsg);
4575                    // If support page is set, add link for help.
4576                    if (!empty($CFG->supportpage)) {
4577                        $link = \html_writer::link($CFG->supportpage, $CFG->supportpage);
4578                        $link = \html_writer::tag('p', $link);
4579                        $notifymsg .= $link;
4580                    }
4581
4582                    // If no change or reset is possible, add a notification for user.
4583                    \core\notification::error($notifymsg);
4584                }
4585            }
4586        }
4587
4588        // Successful authentication.
4589        if ($user->id) {
4590            // User already exists in database.
4591            if (empty($user->auth)) {
4592                // For some reason auth isn't set yet.
4593                $DB->set_field('user', 'auth', $auth, array('id' => $user->id));
4594                $user->auth = $auth;
4595            }
4596
4597            // If the existing hash is using an out-of-date algorithm (or the legacy md5 algorithm), then we should update to
4598            // the current hash algorithm while we have access to the user's password.
4599            update_internal_user_password($user, $password);
4600
4601            if ($authplugin->is_synchronised_with_external()) {
4602                // Update user record from external DB.
4603                $user = update_user_record_by_id($user->id);
4604            }
4605        } else {
4606            // The user is authenticated but user creation may be disabled.
4607            if (!empty($CFG->authpreventaccountcreation)) {
4608                $failurereason = AUTH_LOGIN_UNAUTHORISED;
4609
4610                // Trigger login failed event.
4611                $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4612                        'reason' => $failurereason)));
4613                $event->trigger();
4614
4615                error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Unknown user, can not create new accounts:  $username  ".
4616                        $_SERVER['HTTP_USER_AGENT']);
4617                return false;
4618            } else {
4619                $user = create_user_record($username, $password, $auth);
4620            }
4621        }
4622
4623        $authplugin->sync_roles($user);
4624
4625        foreach ($authsenabled as $hau) {
4626            $hauth = get_auth_plugin($hau);
4627            $hauth->user_authenticated_hook($user, $username, $password);
4628        }
4629
4630        if (empty($user->id)) {
4631            $failurereason = AUTH_LOGIN_NOUSER;
4632            // Trigger login failed event.
4633            $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4634                    'reason' => $failurereason)));
4635            $event->trigger();
4636            return false;
4637        }
4638
4639        if (!empty($user->suspended)) {
4640            // Just in case some auth plugin suspended account.
4641            $failurereason = AUTH_LOGIN_SUSPENDED;
4642            // Trigger login failed event.
4643            $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4644                    'other' => array('username' => $username, 'reason' => $failurereason)));
4645            $event->trigger();
4646            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Suspended Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4647            return false;
4648        }
4649
4650        login_attempt_valid($user);
4651        $failurereason = AUTH_LOGIN_OK;
4652        return $user;
4653    }
4654
4655    // Failed if all the plugins have failed.
4656    if (debugging('', DEBUG_ALL)) {
4657        error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Failed Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
4658    }
4659
4660    if ($user->id) {
4661        login_attempt_failed($user);
4662        $failurereason = AUTH_LOGIN_FAILED;
4663        // Trigger login failed event.
4664        $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4665                'other' => array('username' => $username, 'reason' => $failurereason)));
4666        $event->trigger();
4667    } else {
4668        $failurereason = AUTH_LOGIN_NOUSER;
4669        // Trigger login failed event.
4670        $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4671                'reason' => $failurereason)));
4672        $event->trigger();
4673    }
4674
4675    return false;
4676}
4677
4678/**
4679 * Call to complete the user login process after authenticate_user_login()
4680 * has succeeded. It will setup the $USER variable and other required bits
4681 * and pieces.
4682 *
4683 * NOTE:
4684 * - It will NOT log anything -- up to the caller to decide what to log.
4685 * - this function does not set any cookies any more!
4686 *
4687 * @param stdClass $user
4688 * @return stdClass A {@link $USER} object - BC only, do not use
4689 */
4690function complete_user_login($user) {
4691    global $CFG, $DB, $USER, $SESSION;
4692
4693    \core\session\manager::login_user($user);
4694
4695    // Reload preferences from DB.
4696    unset($USER->preference);
4697    check_user_preferences_loaded($USER);
4698
4699    // Update login times.
4700    update_user_login_times();
4701
4702    // Extra session prefs init.
4703    set_login_session_preferences();
4704
4705    // Trigger login event.
4706    $event = \core\event\user_loggedin::create(
4707        array(
4708            'userid' => $USER->id,
4709            'objectid' => $USER->id,
4710            'other' => array('username' => $USER->username),
4711        )
4712    );
4713    $event->trigger();
4714
4715    // Queue migrating the messaging data, if we need to.
4716    if (!get_user_preferences('core_message_migrate_data', false, $USER->id)) {
4717        // Check if there are any legacy messages to migrate.
4718        if (\core_message\helper::legacy_messages_exist($USER->id)) {
4719            \core_message\task\migrate_message_data::queue_task($USER->id);
4720        } else {
4721            set_user_preference('core_message_migrate_data', true, $USER->id);
4722        }
4723    }
4724
4725    if (isguestuser()) {
4726        // No need to continue when user is THE guest.
4727        return $USER;
4728    }
4729
4730    if (CLI_SCRIPT) {
4731        // We can redirect to password change URL only in browser.
4732        return $USER;
4733    }
4734
4735    // Select password change url.
4736    $userauth = get_auth_plugin($USER->auth);
4737
4738    // Check whether the user should be changing password.
4739    if (get_user_preferences('auth_forcepasswordchange', false)) {
4740        if ($userauth->can_change_password()) {
4741            if ($changeurl = $userauth->change_password_url()) {
4742                redirect($changeurl);
4743            } else {
4744                require_once($CFG->dirroot . '/login/lib.php');
4745                $SESSION->wantsurl = core_login_get_return_url();
4746                redirect($CFG->wwwroot.'/login/change_password.php');
4747            }
4748        } else {
4749            print_error('nopasswordchangeforced', 'auth');
4750        }
4751    }
4752    return $USER;
4753}
4754
4755/**
4756 * Check a password hash to see if it was hashed using the legacy hash algorithm (md5).
4757 *
4758 * @param string $password String to check.
4759 * @return boolean True if the $password matches the format of an md5 sum.
4760 */
4761function password_is_legacy_hash($password) {
4762    return (bool) preg_match('/^[0-9a-f]{32}$/', $password);
4763}
4764
4765/**
4766 * Compare password against hash stored in user object to determine if it is valid.
4767 *
4768 * If necessary it also updates the stored hash to the current format.
4769 *
4770 * @param stdClass $user (Password property may be updated).
4771 * @param string $password Plain text password.
4772 * @return bool True if password is valid.
4773 */
4774function validate_internal_user_password($user, $password) {
4775    global $CFG;
4776
4777    if ($user->password === AUTH_PASSWORD_NOT_CACHED) {
4778        // Internal password is not used at all, it can not validate.
4779        return false;
4780    }
4781
4782    // If hash isn't a legacy (md5) hash, validate using the library function.
4783    if (!password_is_legacy_hash($user->password)) {
4784        return password_verify($password, $user->password);
4785    }
4786
4787    // Otherwise we need to check for a legacy (md5) hash instead. If the hash
4788    // is valid we can then update it to the new algorithm.
4789
4790    $sitesalt = isset($CFG->passwordsaltmain) ? $CFG->passwordsaltmain : '';
4791    $validated = false;
4792
4793    if ($user->password === md5($password.$sitesalt)
4794            or $user->password === md5($password)
4795            or $user->password === md5(addslashes($password).$sitesalt)
4796            or $user->password === md5(addslashes($password))) {
4797        // Note: we are intentionally using the addslashes() here because we
4798        //       need to accept old password hashes of passwords with magic quotes.
4799        $validated = true;
4800
4801    } else {
4802        for ($i=1; $i<=20; $i++) { // 20 alternative salts should be enough, right?
4803            $alt = 'passwordsaltalt'.$i;
4804            if (!empty($CFG->$alt)) {
4805                if ($user->password === md5($password.$CFG->$alt) or $user->password === md5(addslashes($password).$CFG->$alt)) {
4806                    $validated = true;
4807                    break;
4808                }
4809            }
4810        }
4811    }
4812
4813    if ($validated) {
4814        // If the password matches the existing md5 hash, update to the
4815        // current hash algorithm while we have access to the user's password.
4816        update_internal_user_password($user, $password);
4817    }
4818
4819    return $validated;
4820}
4821
4822/**
4823 * Calculate hash for a plain text password.
4824 *
4825 * @param string $password Plain text password to be hashed.
4826 * @param bool $fasthash If true, use a low cost factor when generating the hash
4827 *                       This is much faster to generate but makes the hash
4828 *                       less secure. It is used when lots of hashes need to
4829 *                       be generated quickly.
4830 * @return string The hashed password.
4831 *
4832 * @throws moodle_exception If a problem occurs while generating the hash.
4833 */
4834function hash_internal_user_password($password, $fasthash = false) {
4835    global $CFG;
4836
4837    // Set the cost factor to 4 for fast hashing, otherwise use default cost.
4838    $options = ($fasthash) ? array('cost' => 4) : array();
4839
4840    $generatedhash = password_hash($password, PASSWORD_DEFAULT, $options);
4841
4842    if ($generatedhash === false || $generatedhash === null) {
4843        throw new moodle_exception('Failed to generate password hash.');
4844    }
4845
4846    return $generatedhash;
4847}
4848
4849/**
4850 * Update password hash in user object (if necessary).
4851 *
4852 * The password is updated if:
4853 * 1. The password has changed (the hash of $user->password is different
4854 *    to the hash of $password).
4855 * 2. The existing hash is using an out-of-date algorithm (or the legacy
4856 *    md5 algorithm).
4857 *
4858 * Updating the password will modify the $user object and the database
4859 * record to use the current hashing algorithm.
4860 * It will remove Web Services user tokens too.
4861 *
4862 * @param stdClass $user User object (password property may be updated).
4863 * @param string $password Plain text password.
4864 * @param bool $fasthash If true, use a low cost factor when generating the hash
4865 *                       This is much faster to generate but makes the hash
4866 *                       less secure. It is used when lots of hashes need to
4867 *                       be generated quickly.
4868 * @return bool Always returns true.
4869 */
4870function update_internal_user_password($user, $password, $fasthash = false) {
4871    global $CFG, $DB;
4872
4873    // Figure out what the hashed password should be.
4874    if (!isset($user->auth)) {
4875        debugging('User record in update_internal_user_password() must include field auth',
4876                DEBUG_DEVELOPER);
4877        $user->auth = $DB->get_field('user', 'auth', array('id' => $user->id));
4878    }
4879    $authplugin = get_auth_plugin($user->auth);
4880    if ($authplugin->prevent_local_passwords()) {
4881        $hashedpassword = AUTH_PASSWORD_NOT_CACHED;
4882    } else {
4883        $hashedpassword = hash_internal_user_password($password, $fasthash);
4884    }
4885
4886    $algorithmchanged = false;
4887
4888    if ($hashedpassword === AUTH_PASSWORD_NOT_CACHED) {
4889        // Password is not cached, update it if not set to AUTH_PASSWORD_NOT_CACHED.
4890        $passwordchanged = ($user->password !== $hashedpassword);
4891
4892    } else if (isset($user->password)) {
4893        // If verification fails then it means the password has changed.
4894        $passwordchanged = !password_verify($password, $user->password);
4895        $algorithmchanged = password_needs_rehash($user->password, PASSWORD_DEFAULT);
4896    } else {
4897        // While creating new user, password in unset in $user object, to avoid
4898        // saving it with user_create()
4899        $passwordchanged = true;
4900    }
4901
4902    if ($passwordchanged || $algorithmchanged) {
4903        $DB->set_field('user', 'password',  $hashedpassword, array('id' => $user->id));
4904        $user->password = $hashedpassword;
4905
4906        // Trigger event.
4907        $user = $DB->get_record('user', array('id' => $user->id));
4908        \core\event\user_password_updated::create_from_user($user)->trigger();
4909
4910        // Remove WS user tokens.
4911        if (!empty($CFG->passwordchangetokendeletion)) {
4912            require_once($CFG->dirroot.'/webservice/lib.php');
4913            webservice::delete_user_ws_tokens($user->id);
4914        }
4915    }
4916
4917    return true;
4918}
4919
4920/**
4921 * Get a complete user record, which includes all the info in the user record.
4922 *
4923 * Intended for setting as $USER session variable
4924 *
4925 * @param string $field The user field to be checked for a given value.
4926 * @param string $value The value to match for $field.
4927 * @param int $mnethostid
4928 * @param bool $throwexception If true, it will throw an exception when there's no record found or when there are multiple records
4929 *                              found. Otherwise, it will just return false.
4930 * @return mixed False, or A {@link $USER} object.
4931 */
4932function get_complete_user_data($field, $value, $mnethostid = null, $throwexception = false) {
4933    global $CFG, $DB;
4934
4935    if (!$field || !$value) {
4936        return false;
4937    }
4938
4939    // Change the field to lowercase.
4940    $field = core_text::strtolower($field);
4941
4942    // List of case insensitive fields.
4943    $caseinsensitivefields = ['email'];
4944
4945    // Username input is forced to lowercase and should be case sensitive.
4946    if ($field == 'username') {
4947        $value = core_text::strtolower($value);
4948    }
4949
4950    // Build the WHERE clause for an SQL query.
4951    $params = array('fieldval' => $value);
4952
4953    // Do a case-insensitive query, if necessary. These are generally very expensive. The performance can be improved on some DBs
4954    // such as MySQL by pre-filtering users with accent-insensitive subselect.
4955    if (in_array($field, $caseinsensitivefields)) {
4956        $fieldselect = $DB->sql_equal($field, ':fieldval', false);
4957        $idsubselect = $DB->sql_equal($field, ':fieldval2', false, false);
4958        $params['fieldval2'] = $value;
4959    } else {
4960        $fieldselect = "$field = :fieldval";
4961        $idsubselect = '';
4962    }
4963    $constraints = "$fieldselect AND deleted <> 1";
4964
4965    // If we are loading user data based on anything other than id,
4966    // we must also restrict our search based on mnet host.
4967    if ($field != 'id') {
4968        if (empty($mnethostid)) {
4969            // If empty, we restrict to local users.
4970            $mnethostid = $CFG->mnet_localhost_id;
4971        }
4972    }
4973    if (!empty($mnethostid)) {
4974        $params['mnethostid'] = $mnethostid;
4975        $constraints .= " AND mnethostid = :mnethostid";
4976    }
4977
4978    if ($idsubselect) {
4979        $constraints .= " AND id IN (SELECT id FROM {user} WHERE {$idsubselect})";
4980    }
4981
4982    // Get all the basic user data.
4983    try {
4984        // Make sure that there's only a single record that matches our query.
4985        // For example, when fetching by email, multiple records might match the query as there's no guarantee that email addresses
4986        // are unique. Therefore we can't reliably tell whether the user profile data that we're fetching is the correct one.
4987        $user = $DB->get_record_select('user', $constraints, $params, '*', MUST_EXIST);
4988    } catch (dml_exception $exception) {
4989        if ($throwexception) {
4990            throw $exception;
4991        } else {
4992            // Return false when no records or multiple records were found.
4993            return false;
4994        }
4995    }
4996
4997    // Get various settings and preferences.
4998
4999    // Preload preference cache.
5000    check_user_preferences_loaded($user);
5001
5002    // Load course enrolment related stuff.
5003    $user->lastcourseaccess    = array(); // During last session.
5004    $user->currentcourseaccess = array(); // During current session.
5005    if ($lastaccesses = $DB->get_records('user_lastaccess', array('userid' => $user->id))) {
5006        foreach ($lastaccesses as $lastaccess) {
5007            $user->lastcourseaccess[$lastaccess->courseid] = $lastaccess->timeaccess;
5008        }
5009    }
5010
5011    $sql = "SELECT g.id, g.courseid
5012              FROM {groups} g, {groups_members} gm
5013             WHERE gm.groupid=g.id AND gm.userid=?";
5014
5015    // This is a special hack to speedup calendar display.
5016    $user->groupmember = array();
5017    if (!isguestuser($user)) {
5018        if ($groups = $DB->get_records_sql($sql, array($user->id))) {
5019            foreach ($groups as $group) {
5020                if (!array_key_exists($group->courseid, $user->groupmember)) {
5021                    $user->groupmember[$group->courseid] = array();
5022                }
5023                $user->groupmember[$group->courseid][$group->id] = $group->id;
5024            }
5025        }
5026    }
5027
5028    // Add cohort theme.
5029    if (!empty($CFG->allowcohortthemes)) {
5030        require_once($CFG->dirroot . '/cohort/lib.php');
5031        if ($cohorttheme = cohort_get_user_cohort_theme($user->id)) {
5032            $user->cohorttheme = $cohorttheme;
5033        }
5034    }
5035
5036    // Add the custom profile fields to the user record.
5037    $user->profile = array();
5038    if (!isguestuser($user)) {
5039        require_once($CFG->dirroot.'/user/profile/lib.php');
5040        profile_load_custom_fields($user);
5041    }
5042
5043    // Rewrite some variables if necessary.
5044    if (!empty($user->description)) {
5045        // No need to cart all of it around.
5046        $user->description = true;
5047    }
5048    if (isguestuser($user)) {
5049        // Guest language always same as site.
5050        $user->lang = get_newuser_language();
5051        // Name always in current language.
5052        $user->firstname = get_string('guestuser');
5053        $user->lastname = ' ';
5054    }
5055
5056    return $user;
5057}
5058
5059/**
5060 * Validate a password against the configured password policy
5061 *
5062 * @param string $password the password to be checked against the password policy
5063 * @param string $errmsg the error message to display when the password doesn't comply with the policy.
5064 * @param stdClass $user the user object to perform password validation against. Defaults to null if not provided.
5065 *
5066 * @return bool true if the password is valid according to the policy. false otherwise.
5067 */
5068function check_password_policy($password, &$errmsg, $user = null) {
5069    global $CFG;
5070
5071    if (!empty($CFG->passwordpolicy)) {
5072        $errmsg = '';
5073        if (core_text::strlen($password) < $CFG->minpasswordlength) {
5074            $errmsg .= '<div>'. get_string('errorminpasswordlength', 'auth', $CFG->minpasswordlength) .'</div>';
5075        }
5076        if (preg_match_all('/[[:digit:]]/u', $password, $matches) < $CFG->minpassworddigits) {
5077            $errmsg .= '<div>'. get_string('errorminpassworddigits', 'auth', $CFG->minpassworddigits) .'</div>';
5078        }
5079        if (preg_match_all('/[[:lower:]]/u', $password, $matches) < $CFG->minpasswordlower) {
5080            $errmsg .= '<div>'. get_string('errorminpasswordlower', 'auth', $CFG->minpasswordlower) .'</div>';
5081        }
5082        if (preg_match_all('/[[:upper:]]/u', $password, $matches) < $CFG->minpasswordupper) {
5083            $errmsg .= '<div>'. get_string('errorminpasswordupper', 'auth', $CFG->minpasswordupper) .'</div>';
5084        }
5085        if (preg_match_all('/[^[:upper:][:lower:][:digit:]]/u', $password, $matches) < $CFG->minpasswordnonalphanum) {
5086            $errmsg .= '<div>'. get_string('errorminpasswordnonalphanum', 'auth', $CFG->minpasswordnonalphanum) .'</div>';
5087        }
5088        if (!check_consecutive_identical_characters($password, $CFG->maxconsecutiveidentchars)) {
5089            $errmsg .= '<div>'. get_string('errormaxconsecutiveidentchars', 'auth', $CFG->maxconsecutiveidentchars) .'</div>';
5090        }
5091
5092        // Fire any additional password policy functions from plugins.
5093        // Plugin functions should output an error message string or empty string for success.
5094        $pluginsfunction = get_plugins_with_function('check_password_policy');
5095        foreach ($pluginsfunction as $plugintype => $plugins) {
5096            foreach ($plugins as $pluginfunction) {
5097                $pluginerr = $pluginfunction($password, $user);
5098                if ($pluginerr) {
5099                    $errmsg .= '<div>'. $pluginerr .'</div>';
5100                }
5101            }
5102        }
5103    }
5104
5105    if ($errmsg == '') {
5106        return true;
5107    } else {
5108        return false;
5109    }
5110}
5111
5112
5113/**
5114 * When logging in, this function is run to set certain preferences for the current SESSION.
5115 */
5116function set_login_session_preferences() {
5117    global $SESSION;
5118
5119    $SESSION->justloggedin = true;
5120
5121    unset($SESSION->lang);
5122    unset($SESSION->forcelang);
5123    unset($SESSION->load_navigation_admin);
5124}
5125
5126
5127/**
5128 * Delete a course, including all related data from the database, and any associated files.
5129 *
5130 * @param mixed $courseorid The id of the course or course object to delete.
5131 * @param bool $showfeedback Whether to display notifications of each action the function performs.
5132 * @return bool true if all the removals succeeded. false if there were any failures. If this
5133 *             method returns false, some of the removals will probably have succeeded, and others
5134 *             failed, but you have no way of knowing which.
5135 */
5136function delete_course($courseorid, $showfeedback = true) {
5137    global $DB;
5138
5139    if (is_object($courseorid)) {
5140        $courseid = $courseorid->id;
5141        $course   = $courseorid;
5142    } else {
5143        $courseid = $courseorid;
5144        if (!$course = $DB->get_record('course', array('id' => $courseid))) {
5145            return false;
5146        }
5147    }
5148    $context = context_course::instance($courseid);
5149
5150    // Frontpage course can not be deleted!!
5151    if ($courseid == SITEID) {
5152        return false;
5153    }
5154
5155    // Allow plugins to use this course before we completely delete it.
5156    if ($pluginsfunction = get_plugins_with_function('pre_course_delete')) {
5157        foreach ($pluginsfunction as $plugintype => $plugins) {
5158            foreach ($plugins as $pluginfunction) {
5159                $pluginfunction($course);
5160            }
5161        }
5162    }
5163
5164    // Tell the search manager we are about to delete a course. This prevents us sending updates
5165    // for each individual context being deleted.
5166    \core_search\manager::course_deleting_start($courseid);
5167
5168    $handler = core_course\customfield\course_handler::create();
5169    $handler->delete_instance($courseid);
5170
5171    // Make the course completely empty.
5172    remove_course_contents($courseid, $showfeedback);
5173
5174    // Delete the course and related context instance.
5175    context_helper::delete_instance(CONTEXT_COURSE, $courseid);
5176
5177    $DB->delete_records("course", array("id" => $courseid));
5178    $DB->delete_records("course_format_options", array("courseid" => $courseid));
5179
5180    // Reset all course related caches here.
5181    if (class_exists('format_base', false)) {
5182        format_base::reset_course_cache($courseid);
5183    }
5184
5185    // Tell search that we have deleted the course so it can delete course data from the index.
5186    \core_search\manager::course_deleting_finish($courseid);
5187
5188    // Trigger a course deleted event.
5189    $event = \core\event\course_deleted::create(array(
5190        'objectid' => $course->id,
5191        'context' => $context,
5192        'other' => array(
5193            'shortname' => $course->shortname,
5194            'fullname' => $course->fullname,
5195            'idnumber' => $course->idnumber
5196            )
5197    ));
5198    $event->add_record_snapshot('course', $course);
5199    $event->trigger();
5200
5201    return true;
5202}
5203
5204/**
5205 * Clear a course out completely, deleting all content but don't delete the course itself.
5206 *
5207 * This function does not verify any permissions.
5208 *
5209 * Please note this function also deletes all user enrolments,
5210 * enrolment instances and role assignments by default.
5211 *
5212 * $options:
5213 *  - 'keep_roles_and_enrolments' - false by default
5214 *  - 'keep_groups_and_groupings' - false by default
5215 *
5216 * @param int $courseid The id of the course that is being deleted
5217 * @param bool $showfeedback Whether to display notifications of each action the function performs.
5218 * @param array $options extra options
5219 * @return bool true if all the removals succeeded. false if there were any failures. If this
5220 *             method returns false, some of the removals will probably have succeeded, and others
5221 *             failed, but you have no way of knowing which.
5222 */
5223function remove_course_contents($courseid, $showfeedback = true, array $options = null) {
5224    global $CFG, $DB, $OUTPUT;
5225
5226    require_once($CFG->libdir.'/badgeslib.php');
5227    require_once($CFG->libdir.'/completionlib.php');
5228    require_once($CFG->libdir.'/questionlib.php');
5229    require_once($CFG->libdir.'/gradelib.php');
5230    require_once($CFG->dirroot.'/group/lib.php');
5231    require_once($CFG->dirroot.'/comment/lib.php');
5232    require_once($CFG->dirroot.'/rating/lib.php');
5233    require_once($CFG->dirroot.'/notes/lib.php');
5234
5235    // Handle course badges.
5236    badges_handle_course_deletion($courseid);
5237
5238    // NOTE: these concatenated strings are suboptimal, but it is just extra info...
5239    $strdeleted = get_string('deleted').' - ';
5240
5241    // Some crazy wishlist of stuff we should skip during purging of course content.
5242    $options = (array)$options;
5243
5244    $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
5245    $coursecontext = context_course::instance($courseid);
5246    $fs = get_file_storage();
5247
5248    // Delete course completion information, this has to be done before grades and enrols.
5249    $cc = new completion_info($course);
5250    $cc->clear_criteria();
5251    if ($showfeedback) {
5252        echo $OUTPUT->notification($strdeleted.get_string('completion', 'completion'), 'notifysuccess');
5253    }
5254
5255    // Remove all data from gradebook - this needs to be done before course modules
5256    // because while deleting this information, the system may need to reference
5257    // the course modules that own the grades.
5258    remove_course_grades($courseid, $showfeedback);
5259    remove_grade_letters($coursecontext, $showfeedback);
5260
5261    // Delete course blocks in any all child contexts,
5262    // they may depend on modules so delete them first.
5263    $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2.
5264    foreach ($childcontexts as $childcontext) {
5265        blocks_delete_all_for_context($childcontext->id);
5266    }
5267    unset($childcontexts);
5268    blocks_delete_all_for_context($coursecontext->id);
5269    if ($showfeedback) {
5270        echo $OUTPUT->notification($strdeleted.get_string('type_block_plural', 'plugin'), 'notifysuccess');
5271    }
5272
5273    $DB->set_field('course_modules', 'deletioninprogress', '1', ['course' => $courseid]);
5274    rebuild_course_cache($courseid, true);
5275
5276    // Get the list of all modules that are properly installed.
5277    $allmodules = $DB->get_records_menu('modules', array(), '', 'name, id');
5278
5279    // Delete every instance of every module,
5280    // this has to be done before deleting of course level stuff.
5281    $locations = core_component::get_plugin_list('mod');
5282    foreach ($locations as $modname => $moddir) {
5283        if ($modname === 'NEWMODULE') {
5284            continue;
5285        }
5286        if (array_key_exists($modname, $allmodules)) {
5287            $sql = "SELECT cm.*, m.id AS modinstance, m.name, '$modname' AS modname
5288              FROM {".$modname."} m
5289                   LEFT JOIN {course_modules} cm ON cm.instance = m.id AND cm.module = :moduleid
5290             WHERE m.course = :courseid";
5291            $instances = $DB->get_records_sql($sql, array('courseid' => $course->id,
5292                'modulename' => $modname, 'moduleid' => $allmodules[$modname]));
5293
5294            include_once("$moddir/lib.php");                 // Shows php warning only if plugin defective.
5295            $moddelete = $modname .'_delete_instance';       // Delete everything connected to an instance.
5296
5297            if ($instances) {
5298                foreach ($instances as $cm) {
5299                    if ($cm->id) {
5300                        // Delete activity context questions and question categories.
5301                        question_delete_activity($cm);
5302                        // Notify the competency subsystem.
5303                        \core_competency\api::hook_course_module_deleted($cm);
5304                    }
5305                    if (function_exists($moddelete)) {
5306                        // This purges all module data in related tables, extra user prefs, settings, etc.
5307                        $moddelete($cm->modinstance);
5308                    } else {
5309                        // NOTE: we should not allow installation of modules with missing delete support!
5310                        debugging("Defective module '$modname' detected when deleting course contents: missing function $moddelete()!");
5311                        $DB->delete_records($modname, array('id' => $cm->modinstance));
5312                    }
5313
5314                    if ($cm->id) {
5315                        // Delete cm and its context - orphaned contexts are purged in cron in case of any race condition.
5316                        context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
5317                        $DB->delete_records('course_modules_completion', ['coursemoduleid' => $cm->id]);
5318                        $DB->delete_records('course_modules', array('id' => $cm->id));
5319                        rebuild_course_cache($cm->course, true);
5320                    }
5321                }
5322            }
5323            if ($instances and $showfeedback) {
5324                echo $OUTPUT->notification($strdeleted.get_string('pluginname', $modname), 'notifysuccess');
5325            }
5326        } else {
5327            // Ooops, this module is not properly installed, force-delete it in the next block.
5328        }
5329    }
5330
5331    // We have tried to delete everything the nice way - now let's force-delete any remaining module data.
5332
5333    // Delete completion defaults.
5334    $DB->delete_records("course_completion_defaults", array("course" => $courseid));
5335
5336    // Remove all data from availability and completion tables that is associated
5337    // with course-modules belonging to this course. Note this is done even if the
5338    // features are not enabled now, in case they were enabled previously.
5339    $DB->delete_records_subquery('course_modules_completion', 'coursemoduleid', 'id',
5340            'SELECT id from {course_modules} WHERE course = ?', [$courseid]);
5341
5342    // Remove course-module data that has not been removed in modules' _delete_instance callbacks.
5343    $cms = $DB->get_records('course_modules', array('course' => $course->id));
5344    $allmodulesbyid = array_flip($allmodules);
5345    foreach ($cms as $cm) {
5346        if (array_key_exists($cm->module, $allmodulesbyid)) {
5347            try {
5348                $DB->delete_records($allmodulesbyid[$cm->module], array('id' => $cm->instance));
5349            } catch (Exception $e) {
5350                // Ignore weird or missing table problems.
5351            }
5352        }
5353        context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
5354        $DB->delete_records('course_modules', array('id' => $cm->id));
5355        rebuild_course_cache($cm->course, true);
5356    }
5357
5358    if ($showfeedback) {
5359        echo $OUTPUT->notification($strdeleted.get_string('type_mod_plural', 'plugin'), 'notifysuccess');
5360    }
5361
5362    // Delete questions and question categories.
5363    question_delete_course($course);
5364    if ($showfeedback) {
5365        echo $OUTPUT->notification($strdeleted.get_string('questions', 'question'), 'notifysuccess');
5366    }
5367
5368    // Delete content bank contents.
5369    $cb = new \core_contentbank\contentbank();
5370    $cbdeleted = $cb->delete_contents($coursecontext);
5371    if ($showfeedback && $cbdeleted) {
5372        echo $OUTPUT->notification($strdeleted.get_string('contentbank', 'contentbank'), 'notifysuccess');
5373    }
5374
5375    // Make sure there are no subcontexts left - all valid blocks and modules should be already gone.
5376    $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2.
5377    foreach ($childcontexts as $childcontext) {
5378        $childcontext->delete();
5379    }
5380    unset($childcontexts);
5381
5382    // Remove roles and enrolments by default.
5383    if (empty($options['keep_roles_and_enrolments'])) {
5384        // This hack is used in restore when deleting contents of existing course.
5385        // During restore, we should remove only enrolment related data that the user performing the restore has a
5386        // permission to remove.
5387        $userid = $options['userid'] ?? null;
5388        enrol_course_delete($course, $userid);
5389        role_unassign_all(array('contextid' => $coursecontext->id, 'component' => ''), true);
5390        if ($showfeedback) {
5391            echo $OUTPUT->notification($strdeleted.get_string('type_enrol_plural', 'plugin'), 'notifysuccess');
5392        }
5393    }
5394
5395    // Delete any groups, removing members and grouping/course links first.
5396    if (empty($options['keep_groups_and_groupings'])) {
5397        groups_delete_groupings($course->id, $showfeedback);
5398        groups_delete_groups($course->id, $showfeedback);
5399    }
5400
5401    // Filters be gone!
5402    filter_delete_all_for_context($coursecontext->id);
5403
5404    // Notes, you shall not pass!
5405    note_delete_all($course->id);
5406
5407    // Die comments!
5408    comment::delete_comments($coursecontext->id);
5409
5410    // Ratings are history too.
5411    $delopt = new stdclass();
5412    $delopt->contextid = $coursecontext->id;
5413    $rm = new rating_manager();
5414    $rm->delete_ratings($delopt);
5415
5416    // Delete course tags.
5417    core_tag_tag::remove_all_item_tags('core', 'course', $course->id);
5418
5419    // Notify the competency subsystem.
5420    \core_competency\api::hook_course_deleted($course);
5421
5422    // Delete calendar events.
5423    $DB->delete_records('event', array('courseid' => $course->id));
5424    $fs->delete_area_files($coursecontext->id, 'calendar');
5425
5426    // Delete all related records in other core tables that may have a courseid
5427    // This array stores the tables that need to be cleared, as
5428    // table_name => column_name that contains the course id.
5429    $tablestoclear = array(
5430        'backup_courses' => 'courseid',  // Scheduled backup stuff.
5431        'user_lastaccess' => 'courseid', // User access info.
5432    );
5433    foreach ($tablestoclear as $table => $col) {
5434        $DB->delete_records($table, array($col => $course->id));
5435    }
5436
5437    // Delete all course backup files.
5438    $fs->delete_area_files($coursecontext->id, 'backup');
5439
5440    // Cleanup course record - remove links to deleted stuff.
5441    $oldcourse = new stdClass();
5442    $oldcourse->id               = $course->id;
5443    $oldcourse->summary          = '';
5444    $oldcourse->cacherev         = 0;
5445    $oldcourse->legacyfiles      = 0;
5446    if (!empty($options['keep_groups_and_groupings'])) {
5447        $oldcourse->defaultgroupingid = 0;
5448    }
5449    $DB->update_record('course', $oldcourse);
5450
5451    // Delete course sections.
5452    $DB->delete_records('course_sections', array('course' => $course->id));
5453
5454    // Delete legacy, section and any other course files.
5455    $fs->delete_area_files($coursecontext->id, 'course'); // Files from summary and section.
5456
5457    // Delete all remaining stuff linked to context such as files, comments, ratings, etc.
5458    if (empty($options['keep_roles_and_enrolments']) and empty($options['keep_groups_and_groupings'])) {
5459        // Easy, do not delete the context itself...
5460        $coursecontext->delete_content();
5461    } else {
5462        // Hack alert!!!!
5463        // We can not drop all context stuff because it would bork enrolments and roles,
5464        // there might be also files used by enrol plugins...
5465    }
5466
5467    // Delete legacy files - just in case some files are still left there after conversion to new file api,
5468    // also some non-standard unsupported plugins may try to store something there.
5469    fulldelete($CFG->dataroot.'/'.$course->id);
5470
5471    // Delete from cache to reduce the cache size especially makes sense in case of bulk course deletion.
5472    $cachemodinfo = cache::make('core', 'coursemodinfo');
5473    $cachemodinfo->delete($courseid);
5474
5475    // Trigger a course content deleted event.
5476    $event = \core\event\course_content_deleted::create(array(
5477        'objectid' => $course->id,
5478        'context' => $coursecontext,
5479        'other' => array('shortname' => $course->shortname,
5480                         'fullname' => $course->fullname,
5481                         'options' => $options) // Passing this for legacy reasons.
5482    ));
5483    $event->add_record_snapshot('course', $course);
5484    $event->trigger();
5485
5486    return true;
5487}
5488
5489/**
5490 * Change dates in module - used from course reset.
5491 *
5492 * @param string $modname forum, assignment, etc
5493 * @param array $fields array of date fields from mod table
5494 * @param int $timeshift time difference
5495 * @param int $courseid
5496 * @param int $modid (Optional) passed if specific mod instance in course needs to be updated.
5497 * @return bool success
5498 */
5499function shift_course_mod_dates($modname, $fields, $timeshift, $courseid, $modid = 0) {
5500    global $CFG, $DB;
5501    include_once($CFG->dirroot.'/mod/'.$modname.'/lib.php');
5502
5503    $return = true;
5504    $params = array($timeshift, $courseid);
5505    foreach ($fields as $field) {
5506        $updatesql = "UPDATE {".$modname."}
5507                          SET $field = $field + ?
5508                        WHERE course=? AND $field<>0";
5509        if ($modid) {
5510            $updatesql .= ' AND id=?';
5511            $params[] = $modid;
5512        }
5513        $return = $DB->execute($updatesql, $params) && $return;
5514    }
5515
5516    return $return;
5517}
5518
5519/**
5520 * This function will empty a course of user data.
5521 * It will retain the activities and the structure of the course.
5522 *
5523 * @param object $data an object containing all the settings including courseid (without magic quotes)
5524 * @return array status array of array component, item, error
5525 */
5526function reset_course_userdata($data) {
5527    global $CFG, $DB;
5528    require_once($CFG->libdir.'/gradelib.php');
5529    require_once($CFG->libdir.'/completionlib.php');
5530    require_once($CFG->dirroot.'/completion/criteria/completion_criteria_date.php');
5531    require_once($CFG->dirroot.'/group/lib.php');
5532
5533    $data->courseid = $data->id;
5534    $context = context_course::instance($data->courseid);
5535
5536    $eventparams = array(
5537        'context' => $context,
5538        'courseid' => $data->id,
5539        'other' => array(
5540            'reset_options' => (array) $data
5541        )
5542    );
5543    $event = \core\event\course_reset_started::create($eventparams);
5544    $event->trigger();
5545
5546    // Calculate the time shift of dates.
5547    if (!empty($data->reset_start_date)) {
5548        // Time part of course startdate should be zero.
5549        $data->timeshift = $data->reset_start_date - usergetmidnight($data->reset_start_date_old);
5550    } else {
5551        $data->timeshift = 0;
5552    }
5553
5554    // Result array: component, item, error.
5555    $status = array();
5556
5557    // Start the resetting.
5558    $componentstr = get_string('general');
5559
5560    // Move the course start time.
5561    if (!empty($data->reset_start_date) and $data->timeshift) {
5562        // Change course start data.
5563        $DB->set_field('course', 'startdate', $data->reset_start_date, array('id' => $data->courseid));
5564        // Update all course and group events - do not move activity events.
5565        $updatesql = "UPDATE {event}
5566                         SET timestart = timestart + ?
5567                       WHERE courseid=? AND instance=0";
5568        $DB->execute($updatesql, array($data->timeshift, $data->courseid));
5569
5570        // Update any date activity restrictions.
5571        if ($CFG->enableavailability) {
5572            \availability_date\condition::update_all_dates($data->courseid, $data->timeshift);
5573        }
5574
5575        // Update completion expected dates.
5576        if ($CFG->enablecompletion) {
5577            $modinfo = get_fast_modinfo($data->courseid);
5578            $changed = false;
5579            foreach ($modinfo->get_cms() as $cm) {
5580                if ($cm->completion && !empty($cm->completionexpected)) {
5581                    $DB->set_field('course_modules', 'completionexpected', $cm->completionexpected + $data->timeshift,
5582                        array('id' => $cm->id));
5583                    $changed = true;
5584                }
5585            }
5586
5587            // Clear course cache if changes made.
5588            if ($changed) {
5589                rebuild_course_cache($data->courseid, true);
5590            }
5591
5592            // Update course date completion criteria.
5593            \completion_criteria_date::update_date($data->courseid, $data->timeshift);
5594        }
5595
5596        $status[] = array('component' => $componentstr, 'item' => get_string('datechanged'), 'error' => false);
5597    }
5598
5599    if (!empty($data->reset_end_date)) {
5600        // If the user set a end date value respect it.
5601        $DB->set_field('course', 'enddate', $data->reset_end_date, array('id' => $data->courseid));
5602    } else if ($data->timeshift > 0 && $data->reset_end_date_old) {
5603        // If there is a time shift apply it to the end date as well.
5604        $enddate = $data->reset_end_date_old + $data->timeshift;
5605        $DB->set_field('course', 'enddate', $enddate, array('id' => $data->courseid));
5606    }
5607
5608    if (!empty($data->reset_events)) {
5609        $DB->delete_records('event', array('courseid' => $data->courseid));
5610        $status[] = array('component' => $componentstr, 'item' => get_string('deleteevents', 'calendar'), 'error' => false);
5611    }
5612
5613    if (!empty($data->reset_notes)) {
5614        require_once($CFG->dirroot.'/notes/lib.php');
5615        note_delete_all($data->courseid);
5616        $status[] = array('component' => $componentstr, 'item' => get_string('deletenotes', 'notes'), 'error' => false);
5617    }
5618
5619    if (!empty($data->delete_blog_associations)) {
5620        require_once($CFG->dirroot.'/blog/lib.php');
5621        blog_remove_associations_for_course($data->courseid);
5622        $status[] = array('component' => $componentstr, 'item' => get_string('deleteblogassociations', 'blog'), 'error' => false);
5623    }
5624
5625    if (!empty($data->reset_completion)) {
5626        // Delete course and activity completion information.
5627        $course = $DB->get_record('course', array('id' => $data->courseid));
5628        $cc = new completion_info($course);
5629        $cc->delete_all_completion_data();
5630        $status[] = array('component' => $componentstr,
5631                'item' => get_string('deletecompletiondata', 'completion'), 'error' => false);
5632    }
5633
5634    if (!empty($data->reset_competency_ratings)) {
5635        \core_competency\api::hook_course_reset_competency_ratings($data->courseid);
5636        $status[] = array('component' => $componentstr,
5637            'item' => get_string('deletecompetencyratings', 'core_competency'), 'error' => false);
5638    }
5639
5640    $componentstr = get_string('roles');
5641
5642    if (!empty($data->reset_roles_overrides)) {
5643        $children = $context->get_child_contexts();
5644        foreach ($children as $child) {
5645            $child->delete_capabilities();
5646        }
5647        $context->delete_capabilities();
5648        $status[] = array('component' => $componentstr, 'item' => get_string('deletecourseoverrides', 'role'), 'error' => false);
5649    }
5650
5651    if (!empty($data->reset_roles_local)) {
5652        $children = $context->get_child_contexts();
5653        foreach ($children as $child) {
5654            role_unassign_all(array('contextid' => $child->id));
5655        }
5656        $status[] = array('component' => $componentstr, 'item' => get_string('deletelocalroles', 'role'), 'error' => false);
5657    }
5658
5659    // First unenrol users - this cleans some of related user data too, such as forum subscriptions, tracking, etc.
5660    $data->unenrolled = array();
5661    if (!empty($data->unenrol_users)) {
5662        $plugins = enrol_get_plugins(true);
5663        $instances = enrol_get_instances($data->courseid, true);
5664        foreach ($instances as $key => $instance) {
5665            if (!isset($plugins[$instance->enrol])) {
5666                unset($instances[$key]);
5667                continue;
5668            }
5669        }
5670
5671        $usersroles = enrol_get_course_users_roles($data->courseid);
5672        foreach ($data->unenrol_users as $withroleid) {
5673            if ($withroleid) {
5674                $sql = "SELECT ue.*
5675                          FROM {user_enrolments} ue
5676                          JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5677                          JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5678                          JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.roleid = :roleid AND ra.userid = ue.userid)";
5679                $params = array('courseid' => $data->courseid, 'roleid' => $withroleid, 'courselevel' => CONTEXT_COURSE);
5680
5681            } else {
5682                // Without any role assigned at course context.
5683                $sql = "SELECT ue.*
5684                          FROM {user_enrolments} ue
5685                          JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5686                          JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5687                     LEFT JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.userid = ue.userid)
5688                         WHERE ra.id IS null";
5689                $params = array('courseid' => $data->courseid, 'courselevel' => CONTEXT_COURSE);
5690            }
5691
5692            $rs = $DB->get_recordset_sql($sql, $params);
5693            foreach ($rs as $ue) {
5694                if (!isset($instances[$ue->enrolid])) {
5695                    continue;
5696                }
5697                $instance = $instances[$ue->enrolid];
5698                $plugin = $plugins[$instance->enrol];
5699                if (!$plugin->allow_unenrol($instance) and !$plugin->allow_unenrol_user($instance, $ue)) {
5700                    continue;
5701                }
5702
5703                if ($withroleid && count($usersroles[$ue->userid]) > 1) {
5704                    // If we don't remove all roles and user has more than one role, just remove this role.
5705                    role_unassign($withroleid, $ue->userid, $context->id);
5706
5707                    unset($usersroles[$ue->userid][$withroleid]);
5708                } else {
5709                    // If we remove all roles or user has only one role, unenrol user from course.
5710                    $plugin->unenrol_user($instance, $ue->userid);
5711                }
5712                $data->unenrolled[$ue->userid] = $ue->userid;
5713            }
5714            $rs->close();
5715        }
5716    }
5717    if (!empty($data->unenrolled)) {
5718        $status[] = array(
5719            'component' => $componentstr,
5720            'item' => get_string('unenrol', 'enrol').' ('.count($data->unenrolled).')',
5721            'error' => false
5722        );
5723    }
5724
5725    $componentstr = get_string('groups');
5726
5727    // Remove all group members.
5728    if (!empty($data->reset_groups_members)) {
5729        groups_delete_group_members($data->courseid);
5730        $status[] = array('component' => $componentstr, 'item' => get_string('removegroupsmembers', 'group'), 'error' => false);
5731    }
5732
5733    // Remove all groups.
5734    if (!empty($data->reset_groups_remove)) {
5735        groups_delete_groups($data->courseid, false);
5736        $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroups', 'group'), 'error' => false);
5737    }
5738
5739    // Remove all grouping members.
5740    if (!empty($data->reset_groupings_members)) {
5741        groups_delete_groupings_groups($data->courseid, false);
5742        $status[] = array('component' => $componentstr, 'item' => get_string('removegroupingsmembers', 'group'), 'error' => false);
5743    }
5744
5745    // Remove all groupings.
5746    if (!empty($data->reset_groupings_remove)) {
5747        groups_delete_groupings($data->courseid, false);
5748        $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroupings', 'group'), 'error' => false);
5749    }
5750
5751    // Look in every instance of every module for data to delete.
5752    $unsupportedmods = array();
5753    if ($allmods = $DB->get_records('modules') ) {
5754        foreach ($allmods as $mod) {
5755            $modname = $mod->name;
5756            $modfile = $CFG->dirroot.'/mod/'. $modname.'/lib.php';
5757            $moddeleteuserdata = $modname.'_reset_userdata';   // Function to delete user data.
5758            if (file_exists($modfile)) {
5759                if (!$DB->count_records($modname, array('course' => $data->courseid))) {
5760                    continue; // Skip mods with no instances.
5761                }
5762                include_once($modfile);
5763                if (function_exists($moddeleteuserdata)) {
5764                    $modstatus = $moddeleteuserdata($data);
5765                    if (is_array($modstatus)) {
5766                        $status = array_merge($status, $modstatus);
5767                    } else {
5768                        debugging('Module '.$modname.' returned incorrect staus - must be an array!');
5769                    }
5770                } else {
5771                    $unsupportedmods[] = $mod;
5772                }
5773            } else {
5774                debugging('Missing lib.php in '.$modname.' module!');
5775            }
5776            // Update calendar events for all modules.
5777            course_module_bulk_update_calendar_events($modname, $data->courseid);
5778        }
5779    }
5780
5781    // Mention unsupported mods.
5782    if (!empty($unsupportedmods)) {
5783        foreach ($unsupportedmods as $mod) {
5784            $status[] = array(
5785                'component' => get_string('modulenameplural', $mod->name),
5786                'item' => '',
5787                'error' => get_string('resetnotimplemented')
5788            );
5789        }
5790    }
5791
5792    $componentstr = get_string('gradebook', 'grades');
5793    // Reset gradebook,.
5794    if (!empty($data->reset_gradebook_items)) {
5795        remove_course_grades($data->courseid, false);
5796        grade_grab_course_grades($data->courseid);
5797        grade_regrade_final_grades($data->courseid);
5798        $status[] = array('component' => $componentstr, 'item' => get_string('removeallcourseitems', 'grades'), 'error' => false);
5799
5800    } else if (!empty($data->reset_gradebook_grades)) {
5801        grade_course_reset($data->courseid);
5802        $status[] = array('component' => $componentstr, 'item' => get_string('removeallcoursegrades', 'grades'), 'error' => false);
5803    }
5804    // Reset comments.
5805    if (!empty($data->reset_comments)) {
5806        require_once($CFG->dirroot.'/comment/lib.php');
5807        comment::reset_course_page_comments($context);
5808    }
5809
5810    $event = \core\event\course_reset_ended::create($eventparams);
5811    $event->trigger();
5812
5813    return $status;
5814}
5815
5816/**
5817 * Generate an email processing address.
5818 *
5819 * @param int $modid
5820 * @param string $modargs
5821 * @return string Returns email processing address
5822 */
5823function generate_email_processing_address($modid, $modargs) {
5824    global $CFG;
5825
5826    $header = $CFG->mailprefix . substr(base64_encode(pack('C', $modid)), 0, 2).$modargs;
5827    return $header . substr(md5($header.get_site_identifier()), 0, 16).'@'.$CFG->maildomain;
5828}
5829
5830/**
5831 * ?
5832 *
5833 * @todo Finish documenting this function
5834 *
5835 * @param string $modargs
5836 * @param string $body Currently unused
5837 */
5838function moodle_process_email($modargs, $body) {
5839    global $DB;
5840
5841    // The first char should be an unencoded letter. We'll take this as an action.
5842    switch ($modargs[0]) {
5843        case 'B': { // Bounce.
5844            list(, $userid) = unpack('V', base64_decode(substr($modargs, 1, 8)));
5845            if ($user = $DB->get_record("user", array('id' => $userid), "id,email")) {
5846                // Check the half md5 of their email.
5847                $md5check = substr(md5($user->email), 0, 16);
5848                if ($md5check == substr($modargs, -16)) {
5849                    set_bounce_count($user);
5850                }
5851                // Else maybe they've already changed it?
5852            }
5853        }
5854        break;
5855        // Maybe more later?
5856    }
5857}
5858
5859// CORRESPONDENCE.
5860
5861/**
5862 * Get mailer instance, enable buffering, flush buffer or disable buffering.
5863 *
5864 * @param string $action 'get', 'buffer', 'close' or 'flush'
5865 * @return moodle_phpmailer|null mailer instance if 'get' used or nothing
5866 */
5867function get_mailer($action='get') {
5868    global $CFG;
5869
5870    /** @var moodle_phpmailer $mailer */
5871    static $mailer  = null;
5872    static $counter = 0;
5873
5874    if (!isset($CFG->smtpmaxbulk)) {
5875        $CFG->smtpmaxbulk = 1;
5876    }
5877
5878    if ($action == 'get') {
5879        $prevkeepalive = false;
5880
5881        if (isset($mailer) and $mailer->Mailer == 'smtp') {
5882            if ($counter < $CFG->smtpmaxbulk and !$mailer->isError()) {
5883                $counter++;
5884                // Reset the mailer.
5885                $mailer->Priority         = 3;
5886                $mailer->CharSet          = 'UTF-8'; // Our default.
5887                $mailer->ContentType      = "text/plain";
5888                $mailer->Encoding         = "8bit";
5889                $mailer->From             = "root@localhost";
5890                $mailer->FromName         = "Root User";
5891                $mailer->Sender           = "";
5892                $mailer->Subject          = "";
5893                $mailer->Body             = "";
5894                $mailer->AltBody          = "";
5895                $mailer->ConfirmReadingTo = "";
5896
5897                $mailer->clearAllRecipients();
5898                $mailer->clearReplyTos();
5899                $mailer->clearAttachments();
5900                $mailer->clearCustomHeaders();
5901                return $mailer;
5902            }
5903
5904            $prevkeepalive = $mailer->SMTPKeepAlive;
5905            get_mailer('flush');
5906        }
5907
5908        require_once($CFG->libdir.'/phpmailer/moodle_phpmailer.php');
5909        $mailer = new moodle_phpmailer();
5910
5911        $counter = 1;
5912
5913        if ($CFG->smtphosts == 'qmail') {
5914            // Use Qmail system.
5915            $mailer->isQmail();
5916
5917        } else if (empty($CFG->smtphosts)) {
5918            // Use PHP mail() = sendmail.
5919            $mailer->isMail();
5920
5921        } else {
5922            // Use SMTP directly.
5923            $mailer->isSMTP();
5924            if (!empty($CFG->debugsmtp) && (!empty($CFG->debugdeveloper))) {
5925                $mailer->SMTPDebug = 3;
5926            }
5927            // Specify main and backup servers.
5928            $mailer->Host          = $CFG->smtphosts;
5929            // Specify secure connection protocol.
5930            $mailer->SMTPSecure    = $CFG->smtpsecure;
5931            // Use previous keepalive.
5932            $mailer->SMTPKeepAlive = $prevkeepalive;
5933
5934            if ($CFG->smtpuser) {
5935                // Use SMTP authentication.
5936                $mailer->SMTPAuth = true;
5937                $mailer->Username = $CFG->smtpuser;
5938                $mailer->Password = $CFG->smtppass;
5939            }
5940        }
5941
5942        return $mailer;
5943    }
5944
5945    $nothing = null;
5946
5947    // Keep smtp session open after sending.
5948    if ($action == 'buffer') {
5949        if (!empty($CFG->smtpmaxbulk)) {
5950            get_mailer('flush');
5951            $m = get_mailer();
5952            if ($m->Mailer == 'smtp') {
5953                $m->SMTPKeepAlive = true;
5954            }
5955        }
5956        return $nothing;
5957    }
5958
5959    // Close smtp session, but continue buffering.
5960    if ($action == 'flush') {
5961        if (isset($mailer) and $mailer->Mailer == 'smtp') {
5962            if (!empty($mailer->SMTPDebug)) {
5963                echo '<pre>'."\n";
5964            }
5965            $mailer->SmtpClose();
5966            if (!empty($mailer->SMTPDebug)) {
5967                echo '</pre>';
5968            }
5969        }
5970        return $nothing;
5971    }
5972
5973    // Close smtp session, do not buffer anymore.
5974    if ($action == 'close') {
5975        if (isset($mailer) and $mailer->Mailer == 'smtp') {
5976            get_mailer('flush');
5977            $mailer->SMTPKeepAlive = false;
5978        }
5979        $mailer = null; // Better force new instance.
5980        return $nothing;
5981    }
5982}
5983
5984/**
5985 * A helper function to test for email diversion
5986 *
5987 * @param string $email
5988 * @return bool Returns true if the email should be diverted
5989 */
5990function email_should_be_diverted($email) {
5991    global $CFG;
5992
5993    if (empty($CFG->divertallemailsto)) {
5994        return false;
5995    }
5996
5997    if (empty($CFG->divertallemailsexcept)) {
5998        return true;
5999    }
6000
6001    $patterns = array_map('trim', preg_split("/[\s,]+/", $CFG->divertallemailsexcept));
6002    foreach ($patterns as $pattern) {
6003        if (preg_match("/$pattern/", $email)) {
6004            return false;
6005        }
6006    }
6007
6008    return true;
6009}
6010
6011/**
6012 * Generate a unique email Message-ID using the moodle domain and install path
6013 *
6014 * @param string $localpart An optional unique message id prefix.
6015 * @return string The formatted ID ready for appending to the email headers.
6016 */
6017function generate_email_messageid($localpart = null) {
6018    global $CFG;
6019
6020    $urlinfo = parse_url($CFG->wwwroot);
6021    $base = '@' . $urlinfo['host'];
6022
6023    // If multiple moodles are on the same domain we want to tell them
6024    // apart so we add the install path to the local part. This means
6025    // that the id local part should never contain a / character so
6026    // we can correctly parse the id to reassemble the wwwroot.
6027    if (isset($urlinfo['path'])) {
6028        $base = $urlinfo['path'] . $base;
6029    }
6030
6031    if (empty($localpart)) {
6032        $localpart = uniqid('', true);
6033    }
6034
6035    // Because we may have an option /installpath suffix to the local part
6036    // of the id we need to escape any / chars which are in the $localpart.
6037    $localpart = str_replace('/', '%2F', $localpart);
6038
6039    return '<' . $localpart . $base . '>';
6040}
6041
6042/**
6043 * Send an email to a specified user
6044 *
6045 * @param stdClass $user  A {@link $USER} object
6046 * @param stdClass $from A {@link $USER} object
6047 * @param string $subject plain text subject line of the email
6048 * @param string $messagetext plain text version of the message
6049 * @param string $messagehtml complete html version of the message (optional)
6050 * @param string $attachment a file on the filesystem, either relative to $CFG->dataroot or a full path to a file in one of
6051 *          the following directories: $CFG->cachedir, $CFG->dataroot, $CFG->dirroot, $CFG->localcachedir, $CFG->tempdir
6052 * @param string $attachname the name of the file (extension indicates MIME)
6053 * @param bool $usetrueaddress determines whether $from email address should
6054 *          be sent out. Will be overruled by user profile setting for maildisplay
6055 * @param string $replyto Email address to reply to
6056 * @param string $replytoname Name of reply to recipient
6057 * @param int $wordwrapwidth custom word wrap width, default 79
6058 * @return bool Returns true if mail was sent OK and false if there was an error.
6059 */
6060function email_to_user($user, $from, $subject, $messagetext, $messagehtml = '', $attachment = '', $attachname = '',
6061                       $usetrueaddress = true, $replyto = '', $replytoname = '', $wordwrapwidth = 79) {
6062
6063    global $CFG, $PAGE, $SITE;
6064
6065    if (empty($user) or empty($user->id)) {
6066        debugging('Can not send email to null user', DEBUG_DEVELOPER);
6067        return false;
6068    }
6069
6070    if (empty($user->email)) {
6071        debugging('Can not send email to user without email: '.$user->id, DEBUG_DEVELOPER);
6072        return false;
6073    }
6074
6075    if (!empty($user->deleted)) {
6076        debugging('Can not send email to deleted user: '.$user->id, DEBUG_DEVELOPER);
6077        return false;
6078    }
6079
6080    if (defined('BEHAT_SITE_RUNNING')) {
6081        // Fake email sending in behat.
6082        return true;
6083    }
6084
6085    if (!empty($CFG->noemailever)) {
6086        // Hidden setting for development sites, set in config.php if needed.
6087        debugging('Not sending email due to $CFG->noemailever config setting', DEBUG_NORMAL);
6088        return true;
6089    }
6090
6091    if (email_should_be_diverted($user->email)) {
6092        $subject = "[DIVERTED {$user->email}] $subject";
6093        $user = clone($user);
6094        $user->email = $CFG->divertallemailsto;
6095    }
6096
6097    // Skip mail to suspended users.
6098    if ((isset($user->auth) && $user->auth=='nologin') or (isset($user->suspended) && $user->suspended)) {
6099        return true;
6100    }
6101
6102    if (!validate_email($user->email)) {
6103        // We can not send emails to invalid addresses - it might create security issue or confuse the mailer.
6104        debugging("email_to_user: User $user->id (".fullname($user).") email ($user->email) is invalid! Not sending.");
6105        return false;
6106    }
6107
6108    if (over_bounce_threshold($user)) {
6109        debugging("email_to_user: User $user->id (".fullname($user).") is over bounce threshold! Not sending.");
6110        return false;
6111    }
6112
6113    // TLD .invalid  is specifically reserved for invalid domain names.
6114    // For More information, see {@link http://tools.ietf.org/html/rfc2606#section-2}.
6115    if (substr($user->email, -8) == '.invalid') {
6116        debugging("email_to_user: User $user->id (".fullname($user).") email domain ($user->email) is invalid! Not sending.");
6117        return true; // This is not an error.
6118    }
6119
6120    // If the user is a remote mnet user, parse the email text for URL to the
6121    // wwwroot and modify the url to direct the user's browser to login at their
6122    // home site (identity provider - idp) before hitting the link itself.
6123    if (is_mnet_remote_user($user)) {
6124        require_once($CFG->dirroot.'/mnet/lib.php');
6125
6126        $jumpurl = mnet_get_idp_jump_url($user);
6127        $callback = partial('mnet_sso_apply_indirection', $jumpurl);
6128
6129        $messagetext = preg_replace_callback("%($CFG->wwwroot[^[:space:]]*)%",
6130                $callback,
6131                $messagetext);
6132        $messagehtml = preg_replace_callback("%href=[\"'`]($CFG->wwwroot[\w_:\?=#&@/;.~-]*)[\"'`]%",
6133                $callback,
6134                $messagehtml);
6135    }
6136    $mail = get_mailer();
6137
6138    if (!empty($mail->SMTPDebug)) {
6139        echo '<pre>' . "\n";
6140    }
6141
6142    $temprecipients = array();
6143    $tempreplyto = array();
6144
6145    // Make sure that we fall back onto some reasonable no-reply address.
6146    $noreplyaddressdefault = 'noreply@' . get_host_from_url($CFG->wwwroot);
6147    $noreplyaddress = empty($CFG->noreplyaddress) ? $noreplyaddressdefault : $CFG->noreplyaddress;
6148
6149    if (!validate_email($noreplyaddress)) {
6150        debugging('email_to_user: Invalid noreply-email '.s($noreplyaddress));
6151        $noreplyaddress = $noreplyaddressdefault;
6152    }
6153
6154    // Make up an email address for handling bounces.
6155    if (!empty($CFG->handlebounces)) {
6156        $modargs = 'B'.base64_encode(pack('V', $user->id)).substr(md5($user->email), 0, 16);
6157        $mail->Sender = generate_email_processing_address(0, $modargs);
6158    } else {
6159        $mail->Sender = $noreplyaddress;
6160    }
6161
6162    // Make sure that the explicit replyto is valid, fall back to the implicit one.
6163    if (!empty($replyto) && !validate_email($replyto)) {
6164        debugging('email_to_user: Invalid replyto-email '.s($replyto));
6165        $replyto = $noreplyaddress;
6166    }
6167
6168    if (is_string($from)) { // So we can pass whatever we want if there is need.
6169        $mail->From     = $noreplyaddress;
6170        $mail->FromName = $from;
6171    // Check if using the true address is true, and the email is in the list of allowed domains for sending email,
6172    // and that the senders email setting is either displayed to everyone, or display to only other users that are enrolled
6173    // in a course with the sender.
6174    } else if ($usetrueaddress && can_send_from_real_email_address($from, $user)) {
6175        if (!validate_email($from->email)) {
6176            debugging('email_to_user: Invalid from-email '.s($from->email).' - not sending');
6177            // Better not to use $noreplyaddress in this case.
6178            return false;
6179        }
6180        $mail->From = $from->email;
6181        $fromdetails = new stdClass();
6182        $fromdetails->name = fullname($from);
6183        $fromdetails->url = preg_replace('#^https?://#', '', $CFG->wwwroot);
6184        $fromdetails->siteshortname = format_string($SITE->shortname);
6185        $fromstring = $fromdetails->name;
6186        if ($CFG->emailfromvia == EMAIL_VIA_ALWAYS) {
6187            $fromstring = get_string('emailvia', 'core', $fromdetails);
6188        }
6189        $mail->FromName = $fromstring;
6190        if (empty($replyto)) {
6191            $tempreplyto[] = array($from->email, fullname($from));
6192        }
6193    } else {
6194        $mail->From = $noreplyaddress;
6195        $fromdetails = new stdClass();
6196        $fromdetails->name = fullname($from);
6197        $fromdetails->url = preg_replace('#^https?://#', '', $CFG->wwwroot);
6198        $fromdetails->siteshortname = format_string($SITE->shortname);
6199        $fromstring = $fromdetails->name;
6200        if ($CFG->emailfromvia != EMAIL_VIA_NEVER) {
6201            $fromstring = get_string('emailvia', 'core', $fromdetails);
6202        }
6203        $mail->FromName = $fromstring;
6204        if (empty($replyto)) {
6205            $tempreplyto[] = array($noreplyaddress, get_string('noreplyname'));
6206        }
6207    }
6208
6209    if (!empty($replyto)) {
6210        $tempreplyto[] = array($replyto, $replytoname);
6211    }
6212
6213    $temprecipients[] = array($user->email, fullname($user));
6214
6215    // Set word wrap.
6216    $mail->WordWrap = $wordwrapwidth;
6217
6218    if (!empty($from->customheaders)) {
6219        // Add custom headers.
6220        if (is_array($from->customheaders)) {
6221            foreach ($from->customheaders as $customheader) {
6222                $mail->addCustomHeader($customheader);
6223            }
6224        } else {
6225            $mail->addCustomHeader($from->customheaders);
6226        }
6227    }
6228
6229    // If the X-PHP-Originating-Script email header is on then also add an additional
6230    // header with details of where exactly in moodle the email was triggered from,
6231    // either a call to message_send() or to email_to_user().
6232    if (ini_get('mail.add_x_header')) {
6233
6234        $stack = debug_backtrace(false);
6235        $origin = $stack[0];
6236
6237        foreach ($stack as $depth => $call) {
6238            if ($call['function'] == 'message_send') {
6239                $origin = $call;
6240            }
6241        }
6242
6243        $originheader = $CFG->wwwroot . ' => ' . gethostname() . ':'
6244             . str_replace($CFG->dirroot . '/', '', $origin['file']) . ':' . $origin['line'];
6245        $mail->addCustomHeader('X-Moodle-Originating-Script: ' . $originheader);
6246    }
6247
6248    if (!empty($CFG->emailheaders)) {
6249        $headers = array_map('trim', explode("\n", $CFG->emailheaders));
6250        foreach ($headers as $header) {
6251            if (!empty($header)) {
6252                $mail->addCustomHeader($header);
6253            }
6254        }
6255    }
6256
6257    if (!empty($from->priority)) {
6258        $mail->Priority = $from->priority;
6259    }
6260
6261    $renderer = $PAGE->get_renderer('core');
6262    $context = array(
6263        'sitefullname' => $SITE->fullname,
6264        'siteshortname' => $SITE->shortname,
6265        'sitewwwroot' => $CFG->wwwroot,
6266        'subject' => $subject,
6267        'prefix' => $CFG->emailsubjectprefix,
6268        'to' => $user->email,
6269        'toname' => fullname($user),
6270        'from' => $mail->From,
6271        'fromname' => $mail->FromName,
6272    );
6273    if (!empty($tempreplyto[0])) {
6274        $context['replyto'] = $tempreplyto[0][0];
6275        $context['replytoname'] = $tempreplyto[0][1];
6276    }
6277    if ($user->id > 0) {
6278        $context['touserid'] = $user->id;
6279        $context['tousername'] = $user->username;
6280    }
6281
6282    if (!empty($user->mailformat) && $user->mailformat == 1) {
6283        // Only process html templates if the user preferences allow html email.
6284
6285        if (!$messagehtml) {
6286            // If no html has been given, BUT there is an html wrapping template then
6287            // auto convert the text to html and then wrap it.
6288            $messagehtml = trim(text_to_html($messagetext));
6289        }
6290        $context['body'] = $messagehtml;
6291        $messagehtml = $renderer->render_from_template('core/email_html', $context);
6292    }
6293
6294    $context['body'] = html_to_text(nl2br($messagetext));
6295    $mail->Subject = $renderer->render_from_template('core/email_subject', $context);
6296    $mail->FromName = $renderer->render_from_template('core/email_fromname', $context);
6297    $messagetext = $renderer->render_from_template('core/email_text', $context);
6298
6299    // Autogenerate a MessageID if it's missing.
6300    if (empty($mail->MessageID)) {
6301        $mail->MessageID = generate_email_messageid();
6302    }
6303
6304    if ($messagehtml && !empty($user->mailformat) && $user->mailformat == 1) {
6305        // Don't ever send HTML to users who don't want it.
6306        $mail->isHTML(true);
6307        $mail->Encoding = 'quoted-printable';
6308        $mail->Body    =  $messagehtml;
6309        $mail->AltBody =  "\n$messagetext\n";
6310    } else {
6311        $mail->IsHTML(false);
6312        $mail->Body =  "\n$messagetext\n";
6313    }
6314
6315    if ($attachment && $attachname) {
6316        if (preg_match( "~\\.\\.~" , $attachment )) {
6317            // Security check for ".." in dir path.
6318            $supportuser = core_user::get_support_user();
6319            $temprecipients[] = array($supportuser->email, fullname($supportuser, true));
6320            $mail->addStringAttachment('Error in attachment.  User attempted to attach a filename with a unsafe name.', 'error.txt', '8bit', 'text/plain');
6321        } else {
6322            require_once($CFG->libdir.'/filelib.php');
6323            $mimetype = mimeinfo('type', $attachname);
6324
6325            // Before doing the comparison, make sure that the paths are correct (Windows uses slashes in the other direction).
6326            // The absolute (real) path is also fetched to ensure that comparisons to allowed paths are compared equally.
6327            $attachpath = str_replace('\\', '/', realpath($attachment));
6328
6329            // Build an array of all filepaths from which attachments can be added (normalised slashes, absolute/real path).
6330            $allowedpaths = array_map(function(string $path): string {
6331                return str_replace('\\', '/', realpath($path));
6332            }, [
6333                $CFG->cachedir,
6334                $CFG->dataroot,
6335                $CFG->dirroot,
6336                $CFG->localcachedir,
6337                $CFG->tempdir,
6338                $CFG->localrequestdir,
6339            ]);
6340
6341            // Set addpath to true.
6342            $addpath = true;
6343
6344            // Check if attachment includes one of the allowed paths.
6345            foreach (array_filter($allowedpaths) as $allowedpath) {
6346                // Set addpath to false if the attachment includes one of the allowed paths.
6347                if (strpos($attachpath, $allowedpath) === 0) {
6348                    $addpath = false;
6349                    break;
6350                }
6351            }
6352
6353            // If the attachment is a full path to a file in the multiple allowed paths, use it as is,
6354            // otherwise assume it is a relative path from the dataroot (for backwards compatibility reasons).
6355            if ($addpath == true) {
6356                $attachment = $CFG->dataroot . '/' . $attachment;
6357            }
6358
6359            $mail->addAttachment($attachment, $attachname, 'base64', $mimetype);
6360        }
6361    }
6362
6363    // Check if the email should be sent in an other charset then the default UTF-8.
6364    if ((!empty($CFG->sitemailcharset) || !empty($CFG->allowusermailcharset))) {
6365
6366        // Use the defined site mail charset or eventually the one preferred by the recipient.
6367        $charset = $CFG->sitemailcharset;
6368        if (!empty($CFG->allowusermailcharset)) {
6369            if ($useremailcharset = get_user_preferences('mailcharset', '0', $user->id)) {
6370                $charset = $useremailcharset;
6371            }
6372        }
6373
6374        // Convert all the necessary strings if the charset is supported.
6375        $charsets = get_list_of_charsets();
6376        unset($charsets['UTF-8']);
6377        if (in_array($charset, $charsets)) {
6378            $mail->CharSet  = $charset;
6379            $mail->FromName = core_text::convert($mail->FromName, 'utf-8', strtolower($charset));
6380            $mail->Subject  = core_text::convert($mail->Subject, 'utf-8', strtolower($charset));
6381            $mail->Body     = core_text::convert($mail->Body, 'utf-8', strtolower($charset));
6382            $mail->AltBody  = core_text::convert($mail->AltBody, 'utf-8', strtolower($charset));
6383
6384            foreach ($temprecipients as $key => $values) {
6385                $temprecipients[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset));
6386            }
6387            foreach ($tempreplyto as $key => $values) {
6388                $tempreplyto[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset));
6389            }
6390        }
6391    }
6392
6393    foreach ($temprecipients as $values) {
6394        $mail->addAddress($values[0], $values[1]);
6395    }
6396    foreach ($tempreplyto as $values) {
6397        $mail->addReplyTo($values[0], $values[1]);
6398    }
6399
6400    if (!empty($CFG->emaildkimselector)) {
6401        $domain = substr(strrchr($mail->From, "@"), 1);
6402        $pempath = "{$CFG->dataroot}/dkim/{$domain}/{$CFG->emaildkimselector}.private";
6403        if (file_exists($pempath)) {
6404            $mail->DKIM_domain      = $domain;
6405            $mail->DKIM_private     = $pempath;
6406            $mail->DKIM_selector    = $CFG->emaildkimselector;
6407            $mail->DKIM_identity    = $mail->From;
6408        } else {
6409            debugging("Email DKIM selector chosen due to {$mail->From} but no certificate found at $pempath", DEBUG_DEVELOPER);
6410        }
6411    }
6412
6413    if ($mail->send()) {
6414        set_send_count($user);
6415        if (!empty($mail->SMTPDebug)) {
6416            echo '</pre>';
6417        }
6418        return true;
6419    } else {
6420        // Trigger event for failing to send email.
6421        $event = \core\event\email_failed::create(array(
6422            'context' => context_system::instance(),
6423            'userid' => $from->id,
6424            'relateduserid' => $user->id,
6425            'other' => array(
6426                'subject' => $subject,
6427                'message' => $messagetext,
6428                'errorinfo' => $mail->ErrorInfo
6429            )
6430        ));
6431        $event->trigger();
6432        if (CLI_SCRIPT) {
6433            mtrace('Error: lib/moodlelib.php email_to_user(): '.$mail->ErrorInfo);
6434        }
6435        if (!empty($mail->SMTPDebug)) {
6436            echo '</pre>';
6437        }
6438        return false;
6439    }
6440}
6441
6442/**
6443 * Check to see if a user's real email address should be used for the "From" field.
6444 *
6445 * @param  object $from The user object for the user we are sending the email from.
6446 * @param  object $user The user object that we are sending the email to.
6447 * @param  array $unused No longer used.
6448 * @return bool Returns true if we can use the from user's email adress in the "From" field.
6449 */
6450function can_send_from_real_email_address($from, $user, $unused = null) {
6451    global $CFG;
6452    if (!isset($CFG->allowedemaildomains) || empty(trim($CFG->allowedemaildomains))) {
6453        return false;
6454    }
6455    $alloweddomains = array_map('trim', explode("\n", $CFG->allowedemaildomains));
6456    // Email is in the list of allowed domains for sending email,
6457    // and the senders email setting is either displayed to everyone, or display to only other users that are enrolled
6458    // in a course with the sender.
6459    if (\core\ip_utils::is_domain_in_allowed_list(substr($from->email, strpos($from->email, '@') + 1), $alloweddomains)
6460                && ($from->maildisplay == core_user::MAILDISPLAY_EVERYONE
6461                || ($from->maildisplay == core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY
6462                && enrol_get_shared_courses($user, $from, false, true)))) {
6463        return true;
6464    }
6465    return false;
6466}
6467
6468/**
6469 * Generate a signoff for emails based on support settings
6470 *
6471 * @return string
6472 */
6473function generate_email_signoff() {
6474    global $CFG;
6475
6476    $signoff = "\n";
6477    if (!empty($CFG->supportname)) {
6478        $signoff .= $CFG->supportname."\n";
6479    }
6480    if (!empty($CFG->supportemail)) {
6481        $signoff .= $CFG->supportemail."\n";
6482    }
6483    if (!empty($CFG->supportpage)) {
6484        $signoff .= $CFG->supportpage."\n";
6485    }
6486    return $signoff;
6487}
6488
6489/**
6490 * Sets specified user's password and send the new password to the user via email.
6491 *
6492 * @param stdClass $user A {@link $USER} object
6493 * @param bool $fasthash If true, use a low cost factor when generating the hash for speed.
6494 * @return bool|string Returns "true" if mail was sent OK and "false" if there was an error
6495 */
6496function setnew_password_and_mail($user, $fasthash = false) {
6497    global $CFG, $DB;
6498
6499    // We try to send the mail in language the user understands,
6500    // unfortunately the filter_string() does not support alternative langs yet
6501    // so multilang will not work properly for site->fullname.
6502    $lang = empty($user->lang) ? get_newuser_language() : $user->lang;
6503
6504    $site  = get_site();
6505
6506    $supportuser = core_user::get_support_user();
6507
6508    $newpassword = generate_password();
6509
6510    update_internal_user_password($user, $newpassword, $fasthash);
6511
6512    $a = new stdClass();
6513    $a->firstname   = fullname($user, true);
6514    $a->sitename    = format_string($site->fullname);
6515    $a->username    = $user->username;
6516    $a->newpassword = $newpassword;
6517    $a->link        = $CFG->wwwroot .'/login/?lang='.$lang;
6518    $a->signoff     = generate_email_signoff();
6519
6520    $message = (string)new lang_string('newusernewpasswordtext', '', $a, $lang);
6521
6522    $subject = format_string($site->fullname) .': '. (string)new lang_string('newusernewpasswordsubj', '', $a, $lang);
6523
6524    // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6525    return email_to_user($user, $supportuser, $subject, $message);
6526
6527}
6528
6529/**
6530 * Resets specified user's password and send the new password to the user via email.
6531 *
6532 * @param stdClass $user A {@link $USER} object
6533 * @return bool Returns true if mail was sent OK and false if there was an error.
6534 */
6535function reset_password_and_mail($user) {
6536    global $CFG;
6537
6538    $site  = get_site();
6539    $supportuser = core_user::get_support_user();
6540
6541    $userauth = get_auth_plugin($user->auth);
6542    if (!$userauth->can_reset_password() or !is_enabled_auth($user->auth)) {
6543        trigger_error("Attempt to reset user password for user $user->username with Auth $user->auth.");
6544        return false;
6545    }
6546
6547    $newpassword = generate_password();
6548
6549    if (!$userauth->user_update_password($user, $newpassword)) {
6550        print_error("cannotsetpassword");
6551    }
6552
6553    $a = new stdClass();
6554    $a->firstname   = $user->firstname;
6555    $a->lastname    = $user->lastname;
6556    $a->sitename    = format_string($site->fullname);
6557    $a->username    = $user->username;
6558    $a->newpassword = $newpassword;
6559    $a->link        = $CFG->wwwroot .'/login/change_password.php';
6560    $a->signoff     = generate_email_signoff();
6561
6562    $message = get_string('newpasswordtext', '', $a);
6563
6564    $subject  = format_string($site->fullname) .': '. get_string('changedpassword');
6565
6566    unset_user_preference('create_password', $user); // Prevent cron from generating the password.
6567
6568    // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6569    return email_to_user($user, $supportuser, $subject, $message);
6570}
6571
6572/**
6573 * Send email to specified user with confirmation text and activation link.
6574 *
6575 * @param stdClass $user A {@link $USER} object
6576 * @param string $confirmationurl user confirmation URL
6577 * @return bool Returns true if mail was sent OK and false if there was an error.
6578 */
6579function send_confirmation_email($user, $confirmationurl = null) {
6580    global $CFG;
6581
6582    $site = get_site();
6583    $supportuser = core_user::get_support_user();
6584
6585    $data = new stdClass();
6586    $data->sitename  = format_string($site->fullname);
6587    $data->admin     = generate_email_signoff();
6588
6589    $subject = get_string('emailconfirmationsubject', '', format_string($site->fullname));
6590
6591    if (empty($confirmationurl)) {
6592        $confirmationurl = '/login/confirm.php';
6593    }
6594
6595    $confirmationurl = new moodle_url($confirmationurl);
6596    // Remove data parameter just in case it was included in the confirmation so we can add it manually later.
6597    $confirmationurl->remove_params('data');
6598    $confirmationpath = $confirmationurl->out(false);
6599
6600    // We need to custom encode the username to include trailing dots in the link.
6601    // Because of this custom encoding we can't use moodle_url directly.
6602    // Determine if a query string is present in the confirmation url.
6603    $hasquerystring = strpos($confirmationpath, '?') !== false;
6604    // Perform normal url encoding of the username first.
6605    $username = urlencode($user->username);
6606    // Prevent problems with trailing dots not being included as part of link in some mail clients.
6607    $username = str_replace('.', '%2E', $username);
6608
6609    $data->link = $confirmationpath . ( $hasquerystring ? '&' : '?') . 'data='. $user->secret .'/'. $username;
6610
6611    $message     = get_string('emailconfirmation', '', $data);
6612    $messagehtml = text_to_html(get_string('emailconfirmation', '', $data), false, false, true);
6613
6614    // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6615    return email_to_user($user, $supportuser, $subject, $message, $messagehtml);
6616}
6617
6618/**
6619 * Sends a password change confirmation email.
6620 *
6621 * @param stdClass $user A {@link $USER} object
6622 * @param stdClass $resetrecord An object tracking metadata regarding password reset request
6623 * @return bool Returns true if mail was sent OK and false if there was an error.
6624 */
6625function send_password_change_confirmation_email($user, $resetrecord) {
6626    global $CFG;
6627
6628    $site = get_site();
6629    $supportuser = core_user::get_support_user();
6630    $pwresetmins = isset($CFG->pwresettime) ? floor($CFG->pwresettime / MINSECS) : 30;
6631
6632    $data = new stdClass();
6633    $data->firstname = $user->firstname;
6634    $data->lastname  = $user->lastname;
6635    $data->username  = $user->username;
6636    $data->sitename  = format_string($site->fullname);
6637    $data->link      = $CFG->wwwroot .'/login/forgot_password.php?token='. $resetrecord->token;
6638    $data->admin     = generate_email_signoff();
6639    $data->resetminutes = $pwresetmins;
6640
6641    $message = get_string('emailresetconfirmation', '', $data);
6642    $subject = get_string('emailresetconfirmationsubject', '', format_string($site->fullname));
6643
6644    // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6645    return email_to_user($user, $supportuser, $subject, $message);
6646
6647}
6648
6649/**
6650 * Sends an email containing information on how to change your password.
6651 *
6652 * @param stdClass $user A {@link $USER} object
6653 * @return bool Returns true if mail was sent OK and false if there was an error.
6654 */
6655function send_password_change_info($user) {
6656    $site = get_site();
6657    $supportuser = core_user::get_support_user();
6658
6659    $data = new stdClass();
6660    $data->firstname = $user->firstname;
6661    $data->lastname  = $user->lastname;
6662    $data->username  = $user->username;
6663    $data->sitename  = format_string($site->fullname);
6664    $data->admin     = generate_email_signoff();
6665
6666    if (!is_enabled_auth($user->auth)) {
6667        $message = get_string('emailpasswordchangeinfodisabled', '', $data);
6668        $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
6669        // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6670        return email_to_user($user, $supportuser, $subject, $message);
6671    }
6672
6673    $userauth = get_auth_plugin($user->auth);
6674    ['subject' => $subject, 'message' => $message] = $userauth->get_password_change_info($user);
6675
6676    // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6677    return email_to_user($user, $supportuser, $subject, $message);
6678}
6679
6680/**
6681 * Check that an email is allowed.  It returns an error message if there was a problem.
6682 *
6683 * @param string $email Content of email
6684 * @return string|false
6685 */
6686function email_is_not_allowed($email) {
6687    global $CFG;
6688
6689    // Comparing lowercase domains.
6690    $email = strtolower($email);
6691    if (!empty($CFG->allowemailaddresses)) {
6692        $allowed = explode(' ', strtolower($CFG->allowemailaddresses));
6693        foreach ($allowed as $allowedpattern) {
6694            $allowedpattern = trim($allowedpattern);
6695            if (!$allowedpattern) {
6696                continue;
6697            }
6698            if (strpos($allowedpattern, '.') === 0) {
6699                if (strpos(strrev($email), strrev($allowedpattern)) === 0) {
6700                    // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com".
6701                    return false;
6702                }
6703
6704            } else if (strpos(strrev($email), strrev('@'.$allowedpattern)) === 0) {
6705                return false;
6706            }
6707        }
6708        return get_string('emailonlyallowed', '', $CFG->allowemailaddresses);
6709
6710    } else if (!empty($CFG->denyemailaddresses)) {
6711        $denied = explode(' ', strtolower($CFG->denyemailaddresses));
6712        foreach ($denied as $deniedpattern) {
6713            $deniedpattern = trim($deniedpattern);
6714            if (!$deniedpattern) {
6715                continue;
6716            }
6717            if (strpos($deniedpattern, '.') === 0) {
6718                if (strpos(strrev($email), strrev($deniedpattern)) === 0) {
6719                    // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com".
6720                    return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
6721                }
6722
6723            } else if (strpos(strrev($email), strrev('@'.$deniedpattern)) === 0) {
6724                return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
6725            }
6726        }
6727    }
6728
6729    return false;
6730}
6731
6732// FILE HANDLING.
6733
6734/**
6735 * Returns local file storage instance
6736 *
6737 * @return file_storage
6738 */
6739function get_file_storage($reset = false) {
6740    global $CFG;
6741
6742    static $fs = null;
6743
6744    if ($reset) {
6745        $fs = null;
6746        return;
6747    }
6748
6749    if ($fs) {
6750        return $fs;
6751    }
6752
6753    require_once("$CFG->libdir/filelib.php");
6754
6755    $fs = new file_storage();
6756
6757    return $fs;
6758}
6759
6760/**
6761 * Returns local file storage instance
6762 *
6763 * @return file_browser
6764 */
6765function get_file_browser() {
6766    global $CFG;
6767
6768    static $fb = null;
6769
6770    if ($fb) {
6771        return $fb;
6772    }
6773
6774    require_once("$CFG->libdir/filelib.php");
6775
6776    $fb = new file_browser();
6777
6778    return $fb;
6779}
6780
6781/**
6782 * Returns file packer
6783 *
6784 * @param string $mimetype default application/zip
6785 * @return file_packer
6786 */
6787function get_file_packer($mimetype='application/zip') {
6788    global $CFG;
6789
6790    static $fp = array();
6791
6792    if (isset($fp[$mimetype])) {
6793        return $fp[$mimetype];
6794    }
6795
6796    switch ($mimetype) {
6797        case 'application/zip':
6798        case 'application/vnd.moodle.profiling':
6799            $classname = 'zip_packer';
6800            break;
6801
6802        case 'application/x-gzip' :
6803            $classname = 'tgz_packer';
6804            break;
6805
6806        case 'application/vnd.moodle.backup':
6807            $classname = 'mbz_packer';
6808            break;
6809
6810        default:
6811            return false;
6812    }
6813
6814    require_once("$CFG->libdir/filestorage/$classname.php");
6815    $fp[$mimetype] = new $classname();
6816
6817    return $fp[$mimetype];
6818}
6819
6820/**
6821 * Returns current name of file on disk if it exists.
6822 *
6823 * @param string $newfile File to be verified
6824 * @return string Current name of file on disk if true
6825 */
6826function valid_uploaded_file($newfile) {
6827    if (empty($newfile)) {
6828        return '';
6829    }
6830    if (is_uploaded_file($newfile['tmp_name']) and $newfile['size'] > 0) {
6831        return $newfile['tmp_name'];
6832    } else {
6833        return '';
6834    }
6835}
6836
6837/**
6838 * Returns the maximum size for uploading files.
6839 *
6840 * There are seven possible upload limits:
6841 * 1. in Apache using LimitRequestBody (no way of checking or changing this)
6842 * 2. in php.ini for 'upload_max_filesize' (can not be changed inside PHP)
6843 * 3. in .htaccess for 'upload_max_filesize' (can not be changed inside PHP)
6844 * 4. in php.ini for 'post_max_size' (can not be changed inside PHP)
6845 * 5. by the Moodle admin in $CFG->maxbytes
6846 * 6. by the teacher in the current course $course->maxbytes
6847 * 7. by the teacher for the current module, eg $assignment->maxbytes
6848 *
6849 * These last two are passed to this function as arguments (in bytes).
6850 * Anything defined as 0 is ignored.
6851 * The smallest of all the non-zero numbers is returned.
6852 *
6853 * @todo Finish documenting this function
6854 *
6855 * @param int $sitebytes Set maximum size
6856 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6857 * @param int $modulebytes Current module ->maxbytes (in bytes)
6858 * @param bool $unused This parameter has been deprecated and is not used any more.
6859 * @return int The maximum size for uploading files.
6860 */
6861function get_max_upload_file_size($sitebytes=0, $coursebytes=0, $modulebytes=0, $unused = false) {
6862
6863    if (! $filesize = ini_get('upload_max_filesize')) {
6864        $filesize = '5M';
6865    }
6866    $minimumsize = get_real_size($filesize);
6867
6868    if ($postsize = ini_get('post_max_size')) {
6869        $postsize = get_real_size($postsize);
6870        if ($postsize < $minimumsize) {
6871            $minimumsize = $postsize;
6872        }
6873    }
6874
6875    if (($sitebytes > 0) and ($sitebytes < $minimumsize)) {
6876        $minimumsize = $sitebytes;
6877    }
6878
6879    if (($coursebytes > 0) and ($coursebytes < $minimumsize)) {
6880        $minimumsize = $coursebytes;
6881    }
6882
6883    if (($modulebytes > 0) and ($modulebytes < $minimumsize)) {
6884        $minimumsize = $modulebytes;
6885    }
6886
6887    return $minimumsize;
6888}
6889
6890/**
6891 * Returns the maximum size for uploading files for the current user
6892 *
6893 * This function takes in account {@link get_max_upload_file_size()} the user's capabilities
6894 *
6895 * @param context $context The context in which to check user capabilities
6896 * @param int $sitebytes Set maximum size
6897 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6898 * @param int $modulebytes Current module ->maxbytes (in bytes)
6899 * @param stdClass $user The user
6900 * @param bool $unused This parameter has been deprecated and is not used any more.
6901 * @return int The maximum size for uploading files.
6902 */
6903function get_user_max_upload_file_size($context, $sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $user = null,
6904        $unused = false) {
6905    global $USER;
6906
6907    if (empty($user)) {
6908        $user = $USER;
6909    }
6910
6911    if (has_capability('moodle/course:ignorefilesizelimits', $context, $user)) {
6912        return USER_CAN_IGNORE_FILE_SIZE_LIMITS;
6913    }
6914
6915    return get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes);
6916}
6917
6918/**
6919 * Returns an array of possible sizes in local language
6920 *
6921 * Related to {@link get_max_upload_file_size()} - this function returns an
6922 * array of possible sizes in an array, translated to the
6923 * local language.
6924 *
6925 * The list of options will go up to the minimum of $sitebytes, $coursebytes or $modulebytes.
6926 *
6927 * If $coursebytes or $sitebytes is not 0, an option will be included for "Course/Site upload limit (X)"
6928 * with the value set to 0. This option will be the first in the list.
6929 *
6930 * @uses SORT_NUMERIC
6931 * @param int $sitebytes Set maximum size
6932 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6933 * @param int $modulebytes Current module ->maxbytes (in bytes)
6934 * @param int|array $custombytes custom upload size/s which will be added to list,
6935 *        Only value/s smaller then maxsize will be added to list.
6936 * @return array
6937 */
6938function get_max_upload_sizes($sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $custombytes = null) {
6939    global $CFG;
6940
6941    if (!$maxsize = get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes)) {
6942        return array();
6943    }
6944
6945    if ($sitebytes == 0) {
6946        // Will get the minimum of upload_max_filesize or post_max_size.
6947        $sitebytes = get_max_upload_file_size();
6948    }
6949
6950    $filesize = array();
6951    $sizelist = array(10240, 51200, 102400, 512000, 1048576, 2097152,
6952                      5242880, 10485760, 20971520, 52428800, 104857600,
6953                      262144000, 524288000, 786432000, 1073741824,
6954                      2147483648, 4294967296, 8589934592);
6955
6956    // If custombytes is given and is valid then add it to the list.
6957    if (is_number($custombytes) and $custombytes > 0) {
6958        $custombytes = (int)$custombytes;
6959        if (!in_array($custombytes, $sizelist)) {
6960            $sizelist[] = $custombytes;
6961        }
6962    } else if (is_array($custombytes)) {
6963        $sizelist = array_unique(array_merge($sizelist, $custombytes));
6964    }
6965
6966    // Allow maxbytes to be selected if it falls outside the above boundaries.
6967    if (isset($CFG->maxbytes) && !in_array(get_real_size($CFG->maxbytes), $sizelist)) {
6968        // Note: get_real_size() is used in order to prevent problems with invalid values.
6969        $sizelist[] = get_real_size($CFG->maxbytes);
6970    }
6971
6972    foreach ($sizelist as $sizebytes) {
6973        if ($sizebytes < $maxsize && $sizebytes > 0) {
6974            $filesize[(string)intval($sizebytes)] = display_size($sizebytes);
6975        }
6976    }
6977
6978    $limitlevel = '';
6979    $displaysize = '';
6980    if ($modulebytes &&
6981        (($modulebytes < $coursebytes || $coursebytes == 0) &&
6982         ($modulebytes < $sitebytes || $sitebytes == 0))) {
6983        $limitlevel = get_string('activity', 'core');
6984        $displaysize = display_size($modulebytes);
6985        $filesize[$modulebytes] = $displaysize; // Make sure the limit is also included in the list.
6986
6987    } else if ($coursebytes && ($coursebytes < $sitebytes || $sitebytes == 0)) {
6988        $limitlevel = get_string('course', 'core');
6989        $displaysize = display_size($coursebytes);
6990        $filesize[$coursebytes] = $displaysize; // Make sure the limit is also included in the list.
6991
6992    } else if ($sitebytes) {
6993        $limitlevel = get_string('site', 'core');
6994        $displaysize = display_size($sitebytes);
6995        $filesize[$sitebytes] = $displaysize; // Make sure the limit is also included in the list.
6996    }
6997
6998    krsort($filesize, SORT_NUMERIC);
6999    if ($limitlevel) {
7000        $params = (object) array('contextname' => $limitlevel, 'displaysize' => $displaysize);
7001        $filesize  = array('0' => get_string('uploadlimitwithsize', 'core', $params)) + $filesize;
7002    }
7003
7004    return $filesize;
7005}
7006
7007/**
7008 * Returns an array with all the filenames in all subdirectories, relative to the given rootdir.
7009 *
7010 * If excludefiles is defined, then that file/directory is ignored
7011 * If getdirs is true, then (sub)directories are included in the output
7012 * If getfiles is true, then files are included in the output
7013 * (at least one of these must be true!)
7014 *
7015 * @todo Finish documenting this function. Add examples of $excludefile usage.
7016 *
7017 * @param string $rootdir A given root directory to start from
7018 * @param string|array $excludefiles If defined then the specified file/directory is ignored
7019 * @param bool $descend If true then subdirectories are recursed as well
7020 * @param bool $getdirs If true then (sub)directories are included in the output
7021 * @param bool $getfiles  If true then files are included in the output
7022 * @return array An array with all the filenames in all subdirectories, relative to the given rootdir
7023 */
7024function get_directory_list($rootdir, $excludefiles='', $descend=true, $getdirs=false, $getfiles=true) {
7025
7026    $dirs = array();
7027
7028    if (!$getdirs and !$getfiles) {   // Nothing to show.
7029        return $dirs;
7030    }
7031
7032    if (!is_dir($rootdir)) {          // Must be a directory.
7033        return $dirs;
7034    }
7035
7036    if (!$dir = opendir($rootdir)) {  // Can't open it for some reason.
7037        return $dirs;
7038    }
7039
7040    if (!is_array($excludefiles)) {
7041        $excludefiles = array($excludefiles);
7042    }
7043
7044    while (false !== ($file = readdir($dir))) {
7045        $firstchar = substr($file, 0, 1);
7046        if ($firstchar == '.' or $file == 'CVS' or in_array($file, $excludefiles)) {
7047            continue;
7048        }
7049        $fullfile = $rootdir .'/'. $file;
7050        if (filetype($fullfile) == 'dir') {
7051            if ($getdirs) {
7052                $dirs[] = $file;
7053            }
7054            if ($descend) {
7055                $subdirs = get_directory_list($fullfile, $excludefiles, $descend, $getdirs, $getfiles);
7056                foreach ($subdirs as $subdir) {
7057                    $dirs[] = $file .'/'. $subdir;
7058                }
7059            }
7060        } else if ($getfiles) {
7061            $dirs[] = $file;
7062        }
7063    }
7064    closedir($dir);
7065
7066    asort($dirs);
7067
7068    return $dirs;
7069}
7070
7071
7072/**
7073 * Adds up all the files in a directory and works out the size.
7074 *
7075 * @param string $rootdir  The directory to start from
7076 * @param string $excludefile A file to exclude when summing directory size
7077 * @return int The summed size of all files and subfiles within the root directory
7078 */
7079function get_directory_size($rootdir, $excludefile='') {
7080    global $CFG;
7081
7082    // Do it this way if we can, it's much faster.
7083    if (!empty($CFG->pathtodu) && is_executable(trim($CFG->pathtodu))) {
7084        $command = trim($CFG->pathtodu).' -sk '.escapeshellarg($rootdir);
7085        $output = null;
7086        $return = null;
7087        exec($command, $output, $return);
7088        if (is_array($output)) {
7089            // We told it to return k.
7090            return get_real_size(intval($output[0]).'k');
7091        }
7092    }
7093
7094    if (!is_dir($rootdir)) {
7095        // Must be a directory.
7096        return 0;
7097    }
7098
7099    if (!$dir = @opendir($rootdir)) {
7100        // Can't open it for some reason.
7101        return 0;
7102    }
7103
7104    $size = 0;
7105
7106    while (false !== ($file = readdir($dir))) {
7107        $firstchar = substr($file, 0, 1);
7108        if ($firstchar == '.' or $file == 'CVS' or $file == $excludefile) {
7109            continue;
7110        }
7111        $fullfile = $rootdir .'/'. $file;
7112        if (filetype($fullfile) == 'dir') {
7113            $size += get_directory_size($fullfile, $excludefile);
7114        } else {
7115            $size += filesize($fullfile);
7116        }
7117    }
7118    closedir($dir);
7119
7120    return $size;
7121}
7122
7123/**
7124 * Converts bytes into display form
7125 *
7126 * @static string $gb Localized string for size in gigabytes
7127 * @static string $mb Localized string for size in megabytes
7128 * @static string $kb Localized string for size in kilobytes
7129 * @static string $b Localized string for size in bytes
7130 * @param int $size  The size to convert to human readable form
7131 * @return string
7132 */
7133function display_size($size) {
7134
7135    static $units;
7136
7137    if ($size === USER_CAN_IGNORE_FILE_SIZE_LIMITS) {
7138        return get_string('unlimited');
7139    }
7140
7141    if (empty($units)) {
7142        $units[] = get_string('sizeb');
7143        $units[] = get_string('sizekb');
7144        $units[] = get_string('sizemb');
7145        $units[] = get_string('sizegb');
7146        $units[] = get_string('sizetb');
7147        $units[] = get_string('sizepb');
7148    }
7149
7150    if ($size >= 1024 ** 5) {
7151        $size = round($size / 1024 ** 5 * 10) / 10 . $units[5];
7152    } else if ($size >= 1024 ** 4) {
7153        $size = round($size / 1024 ** 4 * 10) / 10 . $units[4];
7154    } else if ($size >= 1024 ** 3) {
7155        $size = round($size / 1024 ** 3 * 10) / 10 . $units[3];
7156    } else if ($size >= 1024 ** 2) {
7157        $size = round($size / 1024 ** 2 * 10) / 10 . $units[2];
7158    } else if ($size >= 1024 ** 1) {
7159        $size = round($size / 1024 ** 1 * 10) / 10 . $units[1];
7160    } else {
7161        $size = intval($size) .' '. $units[0]; // File sizes over 2GB can not work in 32bit PHP anyway.
7162    }
7163    return $size;
7164}
7165
7166/**
7167 * Cleans a given filename by removing suspicious or troublesome characters
7168 *
7169 * @see clean_param()
7170 * @param string $string file name
7171 * @return string cleaned file name
7172 */
7173function clean_filename($string) {
7174    return clean_param($string, PARAM_FILE);
7175}
7176
7177// STRING TRANSLATION.
7178
7179/**
7180 * Returns the code for the current language
7181 *
7182 * @category string
7183 * @return string
7184 */
7185function current_language() {
7186    global $CFG, $USER, $SESSION, $COURSE;
7187
7188    if (!empty($SESSION->forcelang)) {
7189        // Allows overriding course-forced language (useful for admins to check
7190        // issues in courses whose language they don't understand).
7191        // Also used by some code to temporarily get language-related information in a
7192        // specific language (see force_current_language()).
7193        $return = $SESSION->forcelang;
7194
7195    } else if (!empty($COURSE->id) and $COURSE->id != SITEID and !empty($COURSE->lang)) {
7196        // Course language can override all other settings for this page.
7197        $return = $COURSE->lang;
7198
7199    } else if (!empty($SESSION->lang)) {
7200        // Session language can override other settings.
7201        $return = $SESSION->lang;
7202
7203    } else if (!empty($USER->lang)) {
7204        $return = $USER->lang;
7205
7206    } else if (isset($CFG->lang)) {
7207        $return = $CFG->lang;
7208
7209    } else {
7210        $return = 'en';
7211    }
7212
7213    // Just in case this slipped in from somewhere by accident.
7214    $return = str_replace('_utf8', '', $return);
7215
7216    return $return;
7217}
7218
7219/**
7220 * Returns parent language of current active language if defined
7221 *
7222 * @category string
7223 * @param string $lang null means current language
7224 * @return string
7225 */
7226function get_parent_language($lang=null) {
7227
7228    $parentlang = get_string_manager()->get_string('parentlanguage', 'langconfig', null, $lang);
7229
7230    if ($parentlang === 'en') {
7231        $parentlang = '';
7232    }
7233
7234    return $parentlang;
7235}
7236
7237/**
7238 * Force the current language to get strings and dates localised in the given language.
7239 *
7240 * After calling this function, all strings will be provided in the given language
7241 * until this function is called again, or equivalent code is run.
7242 *
7243 * @param string $language
7244 * @return string previous $SESSION->forcelang value
7245 */
7246function force_current_language($language) {
7247    global $SESSION;
7248    $sessionforcelang = isset($SESSION->forcelang) ? $SESSION->forcelang : '';
7249    if ($language !== $sessionforcelang) {
7250        // Seting forcelang to null or an empty string disables it's effect.
7251        if (empty($language) || get_string_manager()->translation_exists($language, false)) {
7252            $SESSION->forcelang = $language;
7253            moodle_setlocale();
7254        }
7255    }
7256    return $sessionforcelang;
7257}
7258
7259/**
7260 * Returns current string_manager instance.
7261 *
7262 * The param $forcereload is needed for CLI installer only where the string_manager instance
7263 * must be replaced during the install.php script life time.
7264 *
7265 * @category string
7266 * @param bool $forcereload shall the singleton be released and new instance created instead?
7267 * @return core_string_manager
7268 */
7269function get_string_manager($forcereload=false) {
7270    global $CFG;
7271
7272    static $singleton = null;
7273
7274    if ($forcereload) {
7275        $singleton = null;
7276    }
7277    if ($singleton === null) {
7278        if (empty($CFG->early_install_lang)) {
7279
7280            $transaliases = array();
7281            if (empty($CFG->langlist)) {
7282                 $translist = array();
7283            } else {
7284                $translist = explode(',', $CFG->langlist);
7285                $translist = array_map('trim', $translist);
7286                // Each language in the $CFG->langlist can has an "alias" that would substitute the default language name.
7287                foreach ($translist as $i => $value) {
7288                    $parts = preg_split('/\s*\|\s*/', $value, 2);
7289                    if (count($parts) == 2) {
7290                        $transaliases[$parts[0]] = $parts[1];
7291                        $translist[$i] = $parts[0];
7292                    }
7293                }
7294            }
7295
7296            if (!empty($CFG->config_php_settings['customstringmanager'])) {
7297                $classname = $CFG->config_php_settings['customstringmanager'];
7298
7299                if (class_exists($classname)) {
7300                    $implements = class_implements($classname);
7301
7302                    if (isset($implements['core_string_manager'])) {
7303                        $singleton = new $classname($CFG->langotherroot, $CFG->langlocalroot, $translist, $transaliases);
7304                        return $singleton;
7305
7306                    } else {
7307                        debugging('Unable to instantiate custom string manager: class '.$classname.
7308                            ' does not implement the core_string_manager interface.');
7309                    }
7310
7311                } else {
7312                    debugging('Unable to instantiate custom string manager: class '.$classname.' can not be found.');
7313                }
7314            }
7315
7316            $singleton = new core_string_manager_standard($CFG->langotherroot, $CFG->langlocalroot, $translist, $transaliases);
7317
7318        } else {
7319            $singleton = new core_string_manager_install();
7320        }
7321    }
7322
7323    return $singleton;
7324}
7325
7326/**
7327 * Returns a localized string.
7328 *
7329 * Returns the translated string specified by $identifier as
7330 * for $module.  Uses the same format files as STphp.
7331 * $a is an object, string or number that can be used
7332 * within translation strings
7333 *
7334 * eg 'hello {$a->firstname} {$a->lastname}'
7335 * or 'hello {$a}'
7336 *
7337 * If you would like to directly echo the localized string use
7338 * the function {@link print_string()}
7339 *
7340 * Example usage of this function involves finding the string you would
7341 * like a local equivalent of and using its identifier and module information
7342 * to retrieve it.<br/>
7343 * If you open moodle/lang/en/moodle.php and look near line 278
7344 * you will find a string to prompt a user for their word for 'course'
7345 * <code>
7346 * $string['course'] = 'Course';
7347 * </code>
7348 * So if you want to display the string 'Course'
7349 * in any language that supports it on your site
7350 * you just need to use the identifier 'course'
7351 * <code>
7352 * $mystring = '<strong>'. get_string('course') .'</strong>';
7353 * or
7354 * </code>
7355 * If the string you want is in another file you'd take a slightly
7356 * different approach. Looking in moodle/lang/en/calendar.php you find
7357 * around line 75:
7358 * <code>
7359 * $string['typecourse'] = 'Course event';
7360 * </code>
7361 * If you want to display the string "Course event" in any language
7362 * supported you would use the identifier 'typecourse' and the module 'calendar'
7363 * (because it is in the file calendar.php):
7364 * <code>
7365 * $mystring = '<h1>'. get_string('typecourse', 'calendar') .'</h1>';
7366 * </code>
7367 *
7368 * As a last resort, should the identifier fail to map to a string
7369 * the returned string will be [[ $identifier ]]
7370 *
7371 * In Moodle 2.3 there is a new argument to this function $lazyload.
7372 * Setting $lazyload to true causes get_string to return a lang_string object
7373 * rather than the string itself. The fetching of the string is then put off until
7374 * the string object is first used. The object can be used by calling it's out
7375 * method or by casting the object to a string, either directly e.g.
7376 *     (string)$stringobject
7377 * or indirectly by using the string within another string or echoing it out e.g.
7378 *     echo $stringobject
7379 *     return "<p>{$stringobject}</p>";
7380 * It is worth noting that using $lazyload and attempting to use the string as an
7381 * array key will cause a fatal error as objects cannot be used as array keys.
7382 * But you should never do that anyway!
7383 * For more information {@link lang_string}
7384 *
7385 * @category string
7386 * @param string $identifier The key identifier for the localized string
7387 * @param string $component The module where the key identifier is stored,
7388 *      usually expressed as the filename in the language pack without the
7389 *      .php on the end but can also be written as mod/forum or grade/export/xls.
7390 *      If none is specified then moodle.php is used.
7391 * @param string|object|array $a An object, string or number that can be used
7392 *      within translation strings
7393 * @param bool $lazyload If set to true a string object is returned instead of
7394 *      the string itself. The string then isn't calculated until it is first used.
7395 * @return string The localized string.
7396 * @throws coding_exception
7397 */
7398function get_string($identifier, $component = '', $a = null, $lazyload = false) {
7399    global $CFG;
7400
7401    // If the lazy load argument has been supplied return a lang_string object
7402    // instead.
7403    // We need to make sure it is true (and a bool) as you will see below there
7404    // used to be a forth argument at one point.
7405    if ($lazyload === true) {
7406        return new lang_string($identifier, $component, $a);
7407    }
7408
7409    if ($CFG->debugdeveloper && clean_param($identifier, PARAM_STRINGID) === '') {
7410        throw new coding_exception('Invalid string identifier. The identifier cannot be empty. Please fix your get_string() call.', DEBUG_DEVELOPER);
7411    }
7412
7413    // There is now a forth argument again, this time it is a boolean however so
7414    // we can still check for the old extralocations parameter.
7415    if (!is_bool($lazyload) && !empty($lazyload)) {
7416        debugging('extralocations parameter in get_string() is not supported any more, please use standard lang locations only.');
7417    }
7418
7419    if (strpos($component, '/') !== false) {
7420        debugging('The module name you passed to get_string is the deprecated format ' .
7421                'like mod/mymod or block/myblock. The correct form looks like mymod, or block_myblock.' , DEBUG_DEVELOPER);
7422        $componentpath = explode('/', $component);
7423
7424        switch ($componentpath[0]) {
7425            case 'mod':
7426                $component = $componentpath[1];
7427                break;
7428            case 'blocks':
7429            case 'block':
7430                $component = 'block_'.$componentpath[1];
7431                break;
7432            case 'enrol':
7433                $component = 'enrol_'.$componentpath[1];
7434                break;
7435            case 'format':
7436                $component = 'format_'.$componentpath[1];
7437                break;
7438            case 'grade':
7439                $component = 'grade'.$componentpath[1].'_'.$componentpath[2];
7440                break;
7441        }
7442    }
7443
7444    $result = get_string_manager()->get_string($identifier, $component, $a);
7445
7446    // Debugging feature lets you display string identifier and component.
7447    if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
7448        $result .= ' {' . $identifier . '/' . $component . '}';
7449    }
7450    return $result;
7451}
7452
7453/**
7454 * Converts an array of strings to their localized value.
7455 *
7456 * @param array $array An array of strings
7457 * @param string $component The language module that these strings can be found in.
7458 * @return stdClass translated strings.
7459 */
7460function get_strings($array, $component = '') {
7461    $string = new stdClass;
7462    foreach ($array as $item) {
7463        $string->$item = get_string($item, $component);
7464    }
7465    return $string;
7466}
7467
7468/**
7469 * Prints out a translated string.
7470 *
7471 * Prints out a translated string using the return value from the {@link get_string()} function.
7472 *
7473 * Example usage of this function when the string is in the moodle.php file:<br/>
7474 * <code>
7475 * echo '<strong>';
7476 * print_string('course');
7477 * echo '</strong>';
7478 * </code>
7479 *
7480 * Example usage of this function when the string is not in the moodle.php file:<br/>
7481 * <code>
7482 * echo '<h1>';
7483 * print_string('typecourse', 'calendar');
7484 * echo '</h1>';
7485 * </code>
7486 *
7487 * @category string
7488 * @param string $identifier The key identifier for the localized string
7489 * @param string $component The module where the key identifier is stored. If none is specified then moodle.php is used.
7490 * @param string|object|array $a An object, string or number that can be used within translation strings
7491 */
7492function print_string($identifier, $component = '', $a = null) {
7493    echo get_string($identifier, $component, $a);
7494}
7495
7496/**
7497 * Returns a list of charset codes
7498 *
7499 * Returns a list of charset codes. It's hardcoded, so they should be added manually
7500 * (checking that such charset is supported by the texlib library!)
7501 *
7502 * @return array And associative array with contents in the form of charset => charset
7503 */
7504function get_list_of_charsets() {
7505
7506    $charsets = array(
7507        'EUC-JP'     => 'EUC-JP',
7508        'ISO-2022-JP'=> 'ISO-2022-JP',
7509        'ISO-8859-1' => 'ISO-8859-1',
7510        'SHIFT-JIS'  => 'SHIFT-JIS',
7511        'GB2312'     => 'GB2312',
7512        'GB18030'    => 'GB18030', // GB18030 not supported by typo and mbstring.
7513        'UTF-8'      => 'UTF-8');
7514
7515    asort($charsets);
7516
7517    return $charsets;
7518}
7519
7520/**
7521 * Returns a list of valid and compatible themes
7522 *
7523 * @return array
7524 */
7525function get_list_of_themes() {
7526    global $CFG;
7527
7528    $themes = array();
7529
7530    if (!empty($CFG->themelist)) {       // Use admin's list of themes.
7531        $themelist = explode(',', $CFG->themelist);
7532    } else {
7533        $themelist = array_keys(core_component::get_plugin_list("theme"));
7534    }
7535
7536    foreach ($themelist as $key => $themename) {
7537        $theme = theme_config::load($themename);
7538        $themes[$themename] = $theme;
7539    }
7540
7541    core_collator::asort_objects_by_method($themes, 'get_theme_name');
7542
7543    return $themes;
7544}
7545
7546/**
7547 * Factory function for emoticon_manager
7548 *
7549 * @return emoticon_manager singleton
7550 */
7551function get_emoticon_manager() {
7552    static $singleton = null;
7553
7554    if (is_null($singleton)) {
7555        $singleton = new emoticon_manager();
7556    }
7557
7558    return $singleton;
7559}
7560
7561/**
7562 * Provides core support for plugins that have to deal with emoticons (like HTML editor or emoticon filter).
7563 *
7564 * Whenever this manager mentiones 'emoticon object', the following data
7565 * structure is expected: stdClass with properties text, imagename, imagecomponent,
7566 * altidentifier and altcomponent
7567 *
7568 * @see admin_setting_emoticons
7569 *
7570 * @copyright 2010 David Mudrak
7571 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
7572 */
7573class emoticon_manager {
7574
7575    /**
7576     * Returns the currently enabled emoticons
7577     *
7578     * @param boolean $selectable - If true, only return emoticons that should be selectable from a list.
7579     * @return array of emoticon objects
7580     */
7581    public function get_emoticons($selectable = false) {
7582        global $CFG;
7583        $notselectable = ['martin', 'egg'];
7584
7585        if (empty($CFG->emoticons)) {
7586            return array();
7587        }
7588
7589        $emoticons = $this->decode_stored_config($CFG->emoticons);
7590
7591        if (!is_array($emoticons)) {
7592            // Something is wrong with the format of stored setting.
7593            debugging('Invalid format of emoticons setting, please resave the emoticons settings form', DEBUG_NORMAL);
7594            return array();
7595        }
7596        if ($selectable) {
7597            foreach ($emoticons as $index => $emote) {
7598                if (in_array($emote->altidentifier, $notselectable)) {
7599                    // Skip this one.
7600                    unset($emoticons[$index]);
7601                }
7602            }
7603        }
7604
7605        return $emoticons;
7606    }
7607
7608    /**
7609     * Converts emoticon object into renderable pix_emoticon object
7610     *
7611     * @param stdClass $emoticon emoticon object
7612     * @param array $attributes explicit HTML attributes to set
7613     * @return pix_emoticon
7614     */
7615    public function prepare_renderable_emoticon(stdClass $emoticon, array $attributes = array()) {
7616        $stringmanager = get_string_manager();
7617        if ($stringmanager->string_exists($emoticon->altidentifier, $emoticon->altcomponent)) {
7618            $alt = get_string($emoticon->altidentifier, $emoticon->altcomponent);
7619        } else {
7620            $alt = s($emoticon->text);
7621        }
7622        return new pix_emoticon($emoticon->imagename, $alt, $emoticon->imagecomponent, $attributes);
7623    }
7624
7625    /**
7626     * Encodes the array of emoticon objects into a string storable in config table
7627     *
7628     * @see self::decode_stored_config()
7629     * @param array $emoticons array of emtocion objects
7630     * @return string
7631     */
7632    public function encode_stored_config(array $emoticons) {
7633        return json_encode($emoticons);
7634    }
7635
7636    /**
7637     * Decodes the string into an array of emoticon objects
7638     *
7639     * @see self::encode_stored_config()
7640     * @param string $encoded
7641     * @return string|null
7642     */
7643    public function decode_stored_config($encoded) {
7644        $decoded = json_decode($encoded);
7645        if (!is_array($decoded)) {
7646            return null;
7647        }
7648        return $decoded;
7649    }
7650
7651    /**
7652     * Returns default set of emoticons supported by Moodle
7653     *
7654     * @return array of sdtClasses
7655     */
7656    public function default_emoticons() {
7657        return array(
7658            $this->prepare_emoticon_object(":-)", 's/smiley', 'smiley'),
7659            $this->prepare_emoticon_object(":)", 's/smiley', 'smiley'),
7660            $this->prepare_emoticon_object(":-D", 's/biggrin', 'biggrin'),
7661            $this->prepare_emoticon_object(";-)", 's/wink', 'wink'),
7662            $this->prepare_emoticon_object(":-/", 's/mixed', 'mixed'),
7663            $this->prepare_emoticon_object("V-.", 's/thoughtful', 'thoughtful'),
7664            $this->prepare_emoticon_object(":-P", 's/tongueout', 'tongueout'),
7665            $this->prepare_emoticon_object(":-p", 's/tongueout', 'tongueout'),
7666            $this->prepare_emoticon_object("B-)", 's/cool', 'cool'),
7667            $this->prepare_emoticon_object("^-)", 's/approve', 'approve'),
7668            $this->prepare_emoticon_object("8-)", 's/wideeyes', 'wideeyes'),
7669            $this->prepare_emoticon_object(":o)", 's/clown', 'clown'),
7670            $this->prepare_emoticon_object(":-(", 's/sad', 'sad'),
7671            $this->prepare_emoticon_object(":(", 's/sad', 'sad'),
7672            $this->prepare_emoticon_object("8-.", 's/shy', 'shy'),
7673            $this->prepare_emoticon_object(":-I", 's/blush', 'blush'),
7674            $this->prepare_emoticon_object(":-X", 's/kiss', 'kiss'),
7675            $this->prepare_emoticon_object("8-o", 's/surprise', 'surprise'),
7676            $this->prepare_emoticon_object("P-|", 's/blackeye', 'blackeye'),
7677            $this->prepare_emoticon_object("8-[", 's/angry', 'angry'),
7678            $this->prepare_emoticon_object("(grr)", 's/angry', 'angry'),
7679            $this->prepare_emoticon_object("xx-P", 's/dead', 'dead'),
7680            $this->prepare_emoticon_object("|-.", 's/sleepy', 'sleepy'),
7681            $this->prepare_emoticon_object("}-]", 's/evil', 'evil'),
7682            $this->prepare_emoticon_object("(h)", 's/heart', 'heart'),
7683            $this->prepare_emoticon_object("(heart)", 's/heart', 'heart'),
7684            $this->prepare_emoticon_object("(y)", 's/yes', 'yes', 'core'),
7685            $this->prepare_emoticon_object("(n)", 's/no', 'no', 'core'),
7686            $this->prepare_emoticon_object("(martin)", 's/martin', 'martin'),
7687            $this->prepare_emoticon_object("( )", 's/egg', 'egg'),
7688        );
7689    }
7690
7691    /**
7692     * Helper method preparing the stdClass with the emoticon properties
7693     *
7694     * @param string|array $text or array of strings
7695     * @param string $imagename to be used by {@link pix_emoticon}
7696     * @param string $altidentifier alternative string identifier, null for no alt
7697     * @param string $altcomponent where the alternative string is defined
7698     * @param string $imagecomponent to be used by {@link pix_emoticon}
7699     * @return stdClass
7700     */
7701    protected function prepare_emoticon_object($text, $imagename, $altidentifier = null,
7702                                               $altcomponent = 'core_pix', $imagecomponent = 'core') {
7703        return (object)array(
7704            'text'           => $text,
7705            'imagename'      => $imagename,
7706            'imagecomponent' => $imagecomponent,
7707            'altidentifier'  => $altidentifier,
7708            'altcomponent'   => $altcomponent,
7709        );
7710    }
7711}
7712
7713// ENCRYPTION.
7714
7715/**
7716 * rc4encrypt
7717 *
7718 * @param string $data        Data to encrypt.
7719 * @return string             The now encrypted data.
7720 */
7721function rc4encrypt($data) {
7722    return endecrypt(get_site_identifier(), $data, '');
7723}
7724
7725/**
7726 * rc4decrypt
7727 *
7728 * @param string $data        Data to decrypt.
7729 * @return string             The now decrypted data.
7730 */
7731function rc4decrypt($data) {
7732    return endecrypt(get_site_identifier(), $data, 'de');
7733}
7734
7735/**
7736 * Based on a class by Mukul Sabharwal [mukulsabharwal @ yahoo.com]
7737 *
7738 * @todo Finish documenting this function
7739 *
7740 * @param string $pwd The password to use when encrypting or decrypting
7741 * @param string $data The data to be decrypted/encrypted
7742 * @param string $case Either 'de' for decrypt or '' for encrypt
7743 * @return string
7744 */
7745function endecrypt ($pwd, $data, $case) {
7746
7747    if ($case == 'de') {
7748        $data = urldecode($data);
7749    }
7750
7751    $key[] = '';
7752    $box[] = '';
7753    $pwdlength = strlen($pwd);
7754
7755    for ($i = 0; $i <= 255; $i++) {
7756        $key[$i] = ord(substr($pwd, ($i % $pwdlength), 1));
7757        $box[$i] = $i;
7758    }
7759
7760    $x = 0;
7761
7762    for ($i = 0; $i <= 255; $i++) {
7763        $x = ($x + $box[$i] + $key[$i]) % 256;
7764        $tempswap = $box[$i];
7765        $box[$i] = $box[$x];
7766        $box[$x] = $tempswap;
7767    }
7768
7769    $cipher = '';
7770
7771    $a = 0;
7772    $j = 0;
7773
7774    for ($i = 0; $i < strlen($data); $i++) {
7775        $a = ($a + 1) % 256;
7776        $j = ($j + $box[$a]) % 256;
7777        $temp = $box[$a];
7778        $box[$a] = $box[$j];
7779        $box[$j] = $temp;
7780        $k = $box[(($box[$a] + $box[$j]) % 256)];
7781        $cipherby = ord(substr($data, $i, 1)) ^ $k;
7782        $cipher .= chr($cipherby);
7783    }
7784
7785    if ($case == 'de') {
7786        $cipher = urldecode(urlencode($cipher));
7787    } else {
7788        $cipher = urlencode($cipher);
7789    }
7790
7791    return $cipher;
7792}
7793
7794// ENVIRONMENT CHECKING.
7795
7796/**
7797 * This method validates a plug name. It is much faster than calling clean_param.
7798 *
7799 * @param string $name a string that might be a plugin name.
7800 * @return bool if this string is a valid plugin name.
7801 */
7802function is_valid_plugin_name($name) {
7803    // This does not work for 'mod', bad luck, use any other type.
7804    return core_component::is_valid_plugin_name('tool', $name);
7805}
7806
7807/**
7808 * Get a list of all the plugins of a given type that define a certain API function
7809 * in a certain file. The plugin component names and function names are returned.
7810 *
7811 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
7812 * @param string $function the part of the name of the function after the
7813 *      frankenstyle prefix. e.g 'hook' if you are looking for functions with
7814 *      names like report_courselist_hook.
7815 * @param string $file the name of file within the plugin that defines the
7816 *      function. Defaults to lib.php.
7817 * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
7818 *      and the function names as values (e.g. 'report_courselist_hook', 'forum_hook').
7819 */
7820function get_plugin_list_with_function($plugintype, $function, $file = 'lib.php') {
7821    global $CFG;
7822
7823    // We don't include here as all plugin types files would be included.
7824    $plugins = get_plugins_with_function($function, $file, false);
7825
7826    if (empty($plugins[$plugintype])) {
7827        return array();
7828    }
7829
7830    $allplugins = core_component::get_plugin_list($plugintype);
7831
7832    // Reformat the array and include the files.
7833    $pluginfunctions = array();
7834    foreach ($plugins[$plugintype] as $pluginname => $functionname) {
7835
7836        // Check that it has not been removed and the file is still available.
7837        if (!empty($allplugins[$pluginname])) {
7838
7839            $filepath = $allplugins[$pluginname] . DIRECTORY_SEPARATOR . $file;
7840            if (file_exists($filepath)) {
7841                include_once($filepath);
7842
7843                // Now that the file is loaded, we must verify the function still exists.
7844                if (function_exists($functionname)) {
7845                    $pluginfunctions[$plugintype . '_' . $pluginname] = $functionname;
7846                } else {
7847                    // Invalidate the cache for next run.
7848                    \cache_helper::invalidate_by_definition('core', 'plugin_functions');
7849                }
7850            }
7851        }
7852    }
7853
7854    return $pluginfunctions;
7855}
7856
7857/**
7858 * Get a list of all the plugins that define a certain API function in a certain file.
7859 *
7860 * @param string $function the part of the name of the function after the
7861 *      frankenstyle prefix. e.g 'hook' if you are looking for functions with
7862 *      names like report_courselist_hook.
7863 * @param string $file the name of file within the plugin that defines the
7864 *      function. Defaults to lib.php.
7865 * @param bool $include Whether to include the files that contain the functions or not.
7866 * @return array with [plugintype][plugin] = functionname
7867 */
7868function get_plugins_with_function($function, $file = 'lib.php', $include = true) {
7869    global $CFG;
7870
7871    if (during_initial_install() || isset($CFG->upgraderunning)) {
7872        // API functions _must not_ be called during an installation or upgrade.
7873        return [];
7874    }
7875
7876    $cache = \cache::make('core', 'plugin_functions');
7877
7878    // Including both although I doubt that we will find two functions definitions with the same name.
7879    // Clearning the filename as cache_helper::hash_key only allows a-zA-Z0-9_.
7880    $key = $function . '_' . clean_param($file, PARAM_ALPHA);
7881    $pluginfunctions = $cache->get($key);
7882    $dirty = false;
7883
7884    // Use the plugin manager to check that plugins are currently installed.
7885    $pluginmanager = \core_plugin_manager::instance();
7886
7887    if ($pluginfunctions !== false) {
7888
7889        // Checking that the files are still available.
7890        foreach ($pluginfunctions as $plugintype => $plugins) {
7891
7892            $allplugins = \core_component::get_plugin_list($plugintype);
7893            $installedplugins = $pluginmanager->get_installed_plugins($plugintype);
7894            foreach ($plugins as $plugin => $function) {
7895                if (!isset($installedplugins[$plugin])) {
7896                    // Plugin code is still present on disk but it is not installed.
7897                    $dirty = true;
7898                    break 2;
7899                }
7900
7901                // Cache might be out of sync with the codebase, skip the plugin if it is not available.
7902                if (empty($allplugins[$plugin])) {
7903                    $dirty = true;
7904                    break 2;
7905                }
7906
7907                $fileexists = file_exists($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file);
7908                if ($include && $fileexists) {
7909                    // Include the files if it was requested.
7910                    include_once($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file);
7911                } else if (!$fileexists) {
7912                    // If the file is not available any more it should not be returned.
7913                    $dirty = true;
7914                    break 2;
7915                }
7916
7917                // Check if the function still exists in the file.
7918                if ($include && !function_exists($function)) {
7919                    $dirty = true;
7920                    break 2;
7921                }
7922            }
7923        }
7924
7925        // If the cache is dirty, we should fall through and let it rebuild.
7926        if (!$dirty) {
7927            return $pluginfunctions;
7928        }
7929    }
7930
7931    $pluginfunctions = array();
7932
7933    // To fill the cached. Also, everything should continue working with cache disabled.
7934    $plugintypes = \core_component::get_plugin_types();
7935    foreach ($plugintypes as $plugintype => $unused) {
7936
7937        // We need to include files here.
7938        $pluginswithfile = \core_component::get_plugin_list_with_file($plugintype, $file, true);
7939        $installedplugins = $pluginmanager->get_installed_plugins($plugintype);
7940        foreach ($pluginswithfile as $plugin => $notused) {
7941
7942            if (!isset($installedplugins[$plugin])) {
7943                continue;
7944            }
7945
7946            $fullfunction = $plugintype . '_' . $plugin . '_' . $function;
7947
7948            $pluginfunction = false;
7949            if (function_exists($fullfunction)) {
7950                // Function exists with standard name. Store, indexed by frankenstyle name of plugin.
7951                $pluginfunction = $fullfunction;
7952
7953            } else if ($plugintype === 'mod') {
7954                // For modules, we also allow plugin without full frankenstyle but just starting with the module name.
7955                $shortfunction = $plugin . '_' . $function;
7956                if (function_exists($shortfunction)) {
7957                    $pluginfunction = $shortfunction;
7958                }
7959            }
7960
7961            if ($pluginfunction) {
7962                if (empty($pluginfunctions[$plugintype])) {
7963                    $pluginfunctions[$plugintype] = array();
7964                }
7965                $pluginfunctions[$plugintype][$plugin] = $pluginfunction;
7966            }
7967
7968        }
7969    }
7970    $cache->set($key, $pluginfunctions);
7971
7972    return $pluginfunctions;
7973
7974}
7975
7976/**
7977 * Lists plugin-like directories within specified directory
7978 *
7979 * This function was originally used for standard Moodle plugins, please use
7980 * new core_component::get_plugin_list() now.
7981 *
7982 * This function is used for general directory listing and backwards compatility.
7983 *
7984 * @param string $directory relative directory from root
7985 * @param string $exclude dir name to exclude from the list (defaults to none)
7986 * @param string $basedir full path to the base dir where $plugin resides (defaults to $CFG->dirroot)
7987 * @return array Sorted array of directory names found under the requested parameters
7988 */
7989function get_list_of_plugins($directory='mod', $exclude='', $basedir='') {
7990    global $CFG;
7991
7992    $plugins = array();
7993
7994    if (empty($basedir)) {
7995        $basedir = $CFG->dirroot .'/'. $directory;
7996
7997    } else {
7998        $basedir = $basedir .'/'. $directory;
7999    }
8000
8001    if ($CFG->debugdeveloper and empty($exclude)) {
8002        // Make sure devs do not use this to list normal plugins,
8003        // this is intended for general directories that are not plugins!
8004
8005        $subtypes = core_component::get_plugin_types();
8006        if (in_array($basedir, $subtypes)) {
8007            debugging('get_list_of_plugins() should not be used to list real plugins, use core_component::get_plugin_list() instead!', DEBUG_DEVELOPER);
8008        }
8009        unset($subtypes);
8010    }
8011
8012    if (file_exists($basedir) && filetype($basedir) == 'dir') {
8013        if (!$dirhandle = opendir($basedir)) {
8014            debugging("Directory permission error for plugin ({$directory}). Directory exists but cannot be read.", DEBUG_DEVELOPER);
8015            return array();
8016        }
8017        while (false !== ($dir = readdir($dirhandle))) {
8018            // Func: strpos is marginally but reliably faster than substr($dir, 0, 1).
8019            if (strpos($dir, '.') === 0 or $dir === 'CVS' or $dir === '_vti_cnf' or $dir === 'simpletest' or $dir === 'yui' or
8020                $dir === 'tests' or $dir === 'classes' or $dir === $exclude) {
8021                continue;
8022            }
8023            if (filetype($basedir .'/'. $dir) != 'dir') {
8024                continue;
8025            }
8026            $plugins[] = $dir;
8027        }
8028        closedir($dirhandle);
8029    }
8030    if ($plugins) {
8031        asort($plugins);
8032    }
8033    return $plugins;
8034}
8035
8036/**
8037 * Invoke plugin's callback functions
8038 *
8039 * @param string $type plugin type e.g. 'mod'
8040 * @param string $name plugin name
8041 * @param string $feature feature name
8042 * @param string $action feature's action
8043 * @param array $params parameters of callback function, should be an array
8044 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
8045 * @return mixed
8046 *
8047 * @todo Decide about to deprecate and drop plugin_callback() - MDL-30743
8048 */
8049function plugin_callback($type, $name, $feature, $action, $params = null, $default = null) {
8050    return component_callback($type . '_' . $name, $feature . '_' . $action, (array) $params, $default);
8051}
8052
8053/**
8054 * Invoke component's callback functions
8055 *
8056 * @param string $component frankenstyle component name, e.g. 'mod_quiz'
8057 * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
8058 * @param array $params parameters of callback function
8059 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
8060 * @return mixed
8061 */
8062function component_callback($component, $function, array $params = array(), $default = null) {
8063
8064    $functionname = component_callback_exists($component, $function);
8065
8066    if ($functionname) {
8067        // Function exists, so just return function result.
8068        $ret = call_user_func_array($functionname, $params);
8069        if (is_null($ret)) {
8070            return $default;
8071        } else {
8072            return $ret;
8073        }
8074    }
8075    return $default;
8076}
8077
8078/**
8079 * Determine if a component callback exists and return the function name to call. Note that this
8080 * function will include the required library files so that the functioname returned can be
8081 * called directly.
8082 *
8083 * @param string $component frankenstyle component name, e.g. 'mod_quiz'
8084 * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
8085 * @return mixed Complete function name to call if the callback exists or false if it doesn't.
8086 * @throws coding_exception if invalid component specfied
8087 */
8088function component_callback_exists($component, $function) {
8089    global $CFG; // This is needed for the inclusions.
8090
8091    $cleancomponent = clean_param($component, PARAM_COMPONENT);
8092    if (empty($cleancomponent)) {
8093        throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
8094    }
8095    $component = $cleancomponent;
8096
8097    list($type, $name) = core_component::normalize_component($component);
8098    $component = $type . '_' . $name;
8099
8100    $oldfunction = $name.'_'.$function;
8101    $function = $component.'_'.$function;
8102
8103    $dir = core_component::get_component_directory($component);
8104    if (empty($dir)) {
8105        throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
8106    }
8107
8108    // Load library and look for function.
8109    if (file_exists($dir.'/lib.php')) {
8110        require_once($dir.'/lib.php');
8111    }
8112
8113    if (!function_exists($function) and function_exists($oldfunction)) {
8114        if ($type !== 'mod' and $type !== 'core') {
8115            debugging("Please use new function name $function instead of legacy $oldfunction", DEBUG_DEVELOPER);
8116        }
8117        $function = $oldfunction;
8118    }
8119
8120    if (function_exists($function)) {
8121        return $function;
8122    }
8123    return false;
8124}
8125
8126/**
8127 * Call the specified callback method on the provided class.
8128 *
8129 * If the callback returns null, then the default value is returned instead.
8130 * If the class does not exist, then the default value is returned.
8131 *
8132 * @param   string      $classname The name of the class to call upon.
8133 * @param   string      $methodname The name of the staticically defined method on the class.
8134 * @param   array       $params The arguments to pass into the method.
8135 * @param   mixed       $default The default value.
8136 * @return  mixed       The return value.
8137 */
8138function component_class_callback($classname, $methodname, array $params, $default = null) {
8139    if (!class_exists($classname)) {
8140        return $default;
8141    }
8142
8143    if (!method_exists($classname, $methodname)) {
8144        return $default;
8145    }
8146
8147    $fullfunction = $classname . '::' . $methodname;
8148    $result = call_user_func_array($fullfunction, $params);
8149
8150    if (null === $result) {
8151        return $default;
8152    } else {
8153        return $result;
8154    }
8155}
8156
8157/**
8158 * Checks whether a plugin supports a specified feature.
8159 *
8160 * @param string $type Plugin type e.g. 'mod'
8161 * @param string $name Plugin name e.g. 'forum'
8162 * @param string $feature Feature code (FEATURE_xx constant)
8163 * @param mixed $default default value if feature support unknown
8164 * @return mixed Feature result (false if not supported, null if feature is unknown,
8165 *         otherwise usually true but may have other feature-specific value such as array)
8166 * @throws coding_exception
8167 */
8168function plugin_supports($type, $name, $feature, $default = null) {
8169    global $CFG;
8170
8171    if ($type === 'mod' and $name === 'NEWMODULE') {
8172        // Somebody forgot to rename the module template.
8173        return false;
8174    }
8175
8176    $component = clean_param($type . '_' . $name, PARAM_COMPONENT);
8177    if (empty($component)) {
8178        throw new coding_exception('Invalid component used in plugin_supports():' . $type . '_' . $name);
8179    }
8180
8181    $function = null;
8182
8183    if ($type === 'mod') {
8184        // We need this special case because we support subplugins in modules,
8185        // otherwise it would end up in infinite loop.
8186        if (file_exists("$CFG->dirroot/mod/$name/lib.php")) {
8187            include_once("$CFG->dirroot/mod/$name/lib.php");
8188            $function = $component.'_supports';
8189            if (!function_exists($function)) {
8190                // Legacy non-frankenstyle function name.
8191                $function = $name.'_supports';
8192            }
8193        }
8194
8195    } else {
8196        if (!$path = core_component::get_plugin_directory($type, $name)) {
8197            // Non existent plugin type.
8198            return false;
8199        }
8200        if (file_exists("$path/lib.php")) {
8201            include_once("$path/lib.php");
8202            $function = $component.'_supports';
8203        }
8204    }
8205
8206    if ($function and function_exists($function)) {
8207        $supports = $function($feature);
8208        if (is_null($supports)) {
8209            // Plugin does not know - use default.
8210            return $default;
8211        } else {
8212            return $supports;
8213        }
8214    }
8215
8216    // Plugin does not care, so use default.
8217    return $default;
8218}
8219
8220/**
8221 * Returns true if the current version of PHP is greater that the specified one.
8222 *
8223 * @todo Check PHP version being required here is it too low?
8224 *
8225 * @param string $version The version of php being tested.
8226 * @return bool
8227 */
8228function check_php_version($version='5.2.4') {
8229    return (version_compare(phpversion(), $version) >= 0);
8230}
8231
8232/**
8233 * Determine if moodle installation requires update.
8234 *
8235 * Checks version numbers of main code and all plugins to see
8236 * if there are any mismatches.
8237 *
8238 * @return bool
8239 */
8240function moodle_needs_upgrading() {
8241    global $CFG;
8242
8243    if (empty($CFG->version)) {
8244        return true;
8245    }
8246
8247    // There is no need to purge plugininfo caches here because
8248    // these caches are not used during upgrade and they are purged after
8249    // every upgrade.
8250
8251    if (empty($CFG->allversionshash)) {
8252        return true;
8253    }
8254
8255    $hash = core_component::get_all_versions_hash();
8256
8257    return ($hash !== $CFG->allversionshash);
8258}
8259
8260/**
8261 * Returns the major version of this site
8262 *
8263 * Moodle version numbers consist of three numbers separated by a dot, for
8264 * example 1.9.11 or 2.0.2. The first two numbers, like 1.9 or 2.0, represent so
8265 * called major version. This function extracts the major version from either
8266 * $CFG->release (default) or eventually from the $release variable defined in
8267 * the main version.php.
8268 *
8269 * @param bool $fromdisk should the version if source code files be used
8270 * @return string|false the major version like '2.3', false if could not be determined
8271 */
8272function moodle_major_version($fromdisk = false) {
8273    global $CFG;
8274
8275    if ($fromdisk) {
8276        $release = null;
8277        require($CFG->dirroot.'/version.php');
8278        if (empty($release)) {
8279            return false;
8280        }
8281
8282    } else {
8283        if (empty($CFG->release)) {
8284            return false;
8285        }
8286        $release = $CFG->release;
8287    }
8288
8289    if (preg_match('/^[0-9]+\.[0-9]+/', $release, $matches)) {
8290        return $matches[0];
8291    } else {
8292        return false;
8293    }
8294}
8295
8296// MISCELLANEOUS.
8297
8298/**
8299 * Gets the system locale
8300 *
8301 * @return string Retuns the current locale.
8302 */
8303function moodle_getlocale() {
8304    global $CFG;
8305
8306    // Fetch the correct locale based on ostype.
8307    if ($CFG->ostype == 'WINDOWS') {
8308        $stringtofetch = 'localewin';
8309    } else {
8310        $stringtofetch = 'locale';
8311    }
8312
8313    if (!empty($CFG->locale)) { // Override locale for all language packs.
8314        return $CFG->locale;
8315    }
8316
8317    return get_string($stringtofetch, 'langconfig');
8318}
8319
8320/**
8321 * Sets the system locale
8322 *
8323 * @category string
8324 * @param string $locale Can be used to force a locale
8325 */
8326function moodle_setlocale($locale='') {
8327    global $CFG;
8328
8329    static $currentlocale = ''; // Last locale caching.
8330
8331    $oldlocale = $currentlocale;
8332
8333    // The priority is the same as in get_string() - parameter, config, course, session, user, global language.
8334    if (!empty($locale)) {
8335        $currentlocale = $locale;
8336    } else {
8337        $currentlocale = moodle_getlocale();
8338    }
8339
8340    // Do nothing if locale already set up.
8341    if ($oldlocale == $currentlocale) {
8342        return;
8343    }
8344
8345    // Due to some strange BUG we cannot set the LC_TIME directly, so we fetch current values,
8346    // set LC_ALL and then set values again. Just wondering why we cannot set LC_ALL only??? - stronk7
8347    // Some day, numeric, monetary and other categories should be set too, I think. :-/.
8348
8349    // Get current values.
8350    $monetary= setlocale (LC_MONETARY, 0);
8351    $numeric = setlocale (LC_NUMERIC, 0);
8352    $ctype   = setlocale (LC_CTYPE, 0);
8353    if ($CFG->ostype != 'WINDOWS') {
8354        $messages= setlocale (LC_MESSAGES, 0);
8355    }
8356    // Set locale to all.
8357    $result = setlocale (LC_ALL, $currentlocale);
8358    // If setting of locale fails try the other utf8 or utf-8 variant,
8359    // some operating systems support both (Debian), others just one (OSX).
8360    if ($result === false) {
8361        if (stripos($currentlocale, '.UTF-8') !== false) {
8362            $newlocale = str_ireplace('.UTF-8', '.UTF8', $currentlocale);
8363            setlocale (LC_ALL, $newlocale);
8364        } else if (stripos($currentlocale, '.UTF8') !== false) {
8365            $newlocale = str_ireplace('.UTF8', '.UTF-8', $currentlocale);
8366            setlocale (LC_ALL, $newlocale);
8367        }
8368    }
8369    // Set old values.
8370    setlocale (LC_MONETARY, $monetary);
8371    setlocale (LC_NUMERIC, $numeric);
8372    if ($CFG->ostype != 'WINDOWS') {
8373        setlocale (LC_MESSAGES, $messages);
8374    }
8375    if ($currentlocale == 'tr_TR' or $currentlocale == 'tr_TR.UTF-8') {
8376        // To workaround a well-known PHP problem with Turkish letter Ii.
8377        setlocale (LC_CTYPE, $ctype);
8378    }
8379}
8380
8381/**
8382 * Count words in a string.
8383 *
8384 * Words are defined as things between whitespace.
8385 *
8386 * @category string
8387 * @param string $string The text to be searched for words. May be HTML.
8388 * @return int The count of words in the specified string
8389 */
8390function count_words($string) {
8391    // Before stripping tags, add a space after the close tag of anything that is not obviously inline.
8392    // Also, br is a special case because it definitely delimits a word, but has no close tag.
8393    $string = preg_replace('~
8394            (                                   # Capture the tag we match.
8395                </                              # Start of close tag.
8396                (?!                             # Do not match any of these specific close tag names.
8397                    a> | b> | del> | em> | i> |
8398                    ins> | s> | small> |
8399                    strong> | sub> | sup> | u>
8400                )
8401                \w+                             # But, apart from those execptions, match any tag name.
8402                >                               # End of close tag.
8403            |
8404                <br> | <br\s*/>                 # Special cases that are not close tags.
8405            )
8406            ~x', '$1 ', $string); // Add a space after the close tag.
8407    // Now remove HTML tags.
8408    $string = strip_tags($string);
8409    // Decode HTML entities.
8410    $string = html_entity_decode($string);
8411
8412    // Now, the word count is the number of blocks of characters separated
8413    // by any sort of space. That seems to be the definition used by all other systems.
8414    // To be precise about what is considered to separate words:
8415    // * Anything that Unicode considers a 'Separator'
8416    // * Anything that Unicode considers a 'Control character'
8417    // * An em- or en- dash.
8418    return count(preg_split('~[\p{Z}\p{Cc}—–]+~u', $string, -1, PREG_SPLIT_NO_EMPTY));
8419}
8420
8421/**
8422 * Count letters in a string.
8423 *
8424 * Letters are defined as chars not in tags and different from whitespace.
8425 *
8426 * @category string
8427 * @param string $string The text to be searched for letters. May be HTML.
8428 * @return int The count of letters in the specified text.
8429 */
8430function count_letters($string) {
8431    $string = strip_tags($string); // Tags are out now.
8432    $string = html_entity_decode($string);
8433    $string = preg_replace('/[[:space:]]*/', '', $string); // Whitespace are out now.
8434
8435    return core_text::strlen($string);
8436}
8437
8438/**
8439 * Generate and return a random string of the specified length.
8440 *
8441 * @param int $length The length of the string to be created.
8442 * @return string
8443 */
8444function random_string($length=15) {
8445    $randombytes = random_bytes_emulate($length);
8446    $pool  = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
8447    $pool .= 'abcdefghijklmnopqrstuvwxyz';
8448    $pool .= '0123456789';
8449    $poollen = strlen($pool);
8450    $string = '';
8451    for ($i = 0; $i < $length; $i++) {
8452        $rand = ord($randombytes[$i]);
8453        $string .= substr($pool, ($rand%($poollen)), 1);
8454    }
8455    return $string;
8456}
8457
8458/**
8459 * Generate a complex random string (useful for md5 salts)
8460 *
8461 * This function is based on the above {@link random_string()} however it uses a
8462 * larger pool of characters and generates a string between 24 and 32 characters
8463 *
8464 * @param int $length Optional if set generates a string to exactly this length
8465 * @return string
8466 */
8467function complex_random_string($length=null) {
8468    $pool  = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
8469    $pool .= '`~!@#%^&*()_+-=[];,./<>?:{} ';
8470    $poollen = strlen($pool);
8471    if ($length===null) {
8472        $length = floor(rand(24, 32));
8473    }
8474    $randombytes = random_bytes_emulate($length);
8475    $string = '';
8476    for ($i = 0; $i < $length; $i++) {
8477        $rand = ord($randombytes[$i]);
8478        $string .= $pool[($rand%$poollen)];
8479    }
8480    return $string;
8481}
8482
8483/**
8484 * Try to generates cryptographically secure pseudo-random bytes.
8485 *
8486 * Note this is achieved by fallbacking between:
8487 *  - PHP 7 random_bytes().
8488 *  - OpenSSL openssl_random_pseudo_bytes().
8489 *  - In house random generator getting its entropy from various, hard to guess, pseudo-random sources.
8490 *
8491 * @param int $length requested length in bytes
8492 * @return string binary data
8493 */
8494function random_bytes_emulate($length) {
8495    global $CFG;
8496    if ($length <= 0) {
8497        debugging('Invalid random bytes length', DEBUG_DEVELOPER);
8498        return '';
8499    }
8500    if (function_exists('random_bytes')) {
8501        // Use PHP 7 goodness.
8502        $hash = @random_bytes($length);
8503        if ($hash !== false) {
8504            return $hash;
8505        }
8506    }
8507    if (function_exists('openssl_random_pseudo_bytes')) {
8508        // If you have the openssl extension enabled.
8509        $hash = openssl_random_pseudo_bytes($length);
8510        if ($hash !== false) {
8511            return $hash;
8512        }
8513    }
8514
8515    // Bad luck, there is no reliable random generator, let's just slowly hash some unique stuff that is hard to guess.
8516    $staticdata = serialize($CFG) . serialize($_SERVER);
8517    $hash = '';
8518    do {
8519        $hash .= sha1($staticdata . microtime(true) . uniqid('', true), true);
8520    } while (strlen($hash) < $length);
8521
8522    return substr($hash, 0, $length);
8523}
8524
8525/**
8526 * Given some text (which may contain HTML) and an ideal length,
8527 * this function truncates the text neatly on a word boundary if possible
8528 *
8529 * @category string
8530 * @param string $text text to be shortened
8531 * @param int $ideal ideal string length
8532 * @param boolean $exact if false, $text will not be cut mid-word
8533 * @param string $ending The string to append if the passed string is truncated
8534 * @return string $truncate shortened string
8535 */
8536function shorten_text($text, $ideal=30, $exact = false, $ending='...') {
8537    // If the plain text is shorter than the maximum length, return the whole text.
8538    if (core_text::strlen(preg_replace('/<.*?>/', '', $text)) <= $ideal) {
8539        return $text;
8540    }
8541
8542    // Splits on HTML tags. Each open/close/empty tag will be the first thing
8543    // and only tag in its 'line'.
8544    preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER);
8545
8546    $totallength = core_text::strlen($ending);
8547    $truncate = '';
8548
8549    // This array stores information about open and close tags and their position
8550    // in the truncated string. Each item in the array is an object with fields
8551    // ->open (true if open), ->tag (tag name in lower case), and ->pos
8552    // (byte position in truncated text).
8553    $tagdetails = array();
8554
8555    foreach ($lines as $linematchings) {
8556        // If there is any html-tag in this line, handle it and add it (uncounted) to the output.
8557        if (!empty($linematchings[1])) {
8558            // If it's an "empty element" with or without xhtml-conform closing slash (f.e. <br/>).
8559            if (!preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $linematchings[1])) {
8560                if (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $linematchings[1], $tagmatchings)) {
8561                    // Record closing tag.
8562                    $tagdetails[] = (object) array(
8563                            'open' => false,
8564                            'tag'  => core_text::strtolower($tagmatchings[1]),
8565                            'pos'  => core_text::strlen($truncate),
8566                        );
8567
8568                } else if (preg_match('/^<\s*([^\s>!]+).*?>$/s', $linematchings[1], $tagmatchings)) {
8569                    // Record opening tag.
8570                    $tagdetails[] = (object) array(
8571                            'open' => true,
8572                            'tag'  => core_text::strtolower($tagmatchings[1]),
8573                            'pos'  => core_text::strlen($truncate),
8574                        );
8575                } else if (preg_match('/^<!--\[if\s.*?\]>$/s', $linematchings[1], $tagmatchings)) {
8576                    $tagdetails[] = (object) array(
8577                            'open' => true,
8578                            'tag'  => core_text::strtolower('if'),
8579                            'pos'  => core_text::strlen($truncate),
8580                    );
8581                } else if (preg_match('/^<!--<!\[endif\]-->$/s', $linematchings[1], $tagmatchings)) {
8582                    $tagdetails[] = (object) array(
8583                            'open' => false,
8584                            'tag'  => core_text::strtolower('if'),
8585                            'pos'  => core_text::strlen($truncate),
8586                    );
8587                }
8588            }
8589            // Add html-tag to $truncate'd text.
8590            $truncate .= $linematchings[1];
8591        }
8592
8593        // Calculate the length of the plain text part of the line; handle entities as one character.
8594        $contentlength = core_text::strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', ' ', $linematchings[2]));
8595        if ($totallength + $contentlength > $ideal) {
8596            // The number of characters which are left.
8597            $left = $ideal - $totallength;
8598            $entitieslength = 0;
8599            // Search for html entities.
8600            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)) {
8601                // Calculate the real length of all entities in the legal range.
8602                foreach ($entities[0] as $entity) {
8603                    if ($entity[1]+1-$entitieslength <= $left) {
8604                        $left--;
8605                        $entitieslength += core_text::strlen($entity[0]);
8606                    } else {
8607                        // No more characters left.
8608                        break;
8609                    }
8610                }
8611            }
8612            $breakpos = $left + $entitieslength;
8613
8614            // If the words shouldn't be cut in the middle...
8615            if (!$exact) {
8616                // Search the last occurence of a space.
8617                for (; $breakpos > 0; $breakpos--) {
8618                    if ($char = core_text::substr($linematchings[2], $breakpos, 1)) {
8619                        if ($char === '.' or $char === ' ') {
8620                            $breakpos += 1;
8621                            break;
8622                        } else if (strlen($char) > 2) {
8623                            // Chinese/Japanese/Korean text can be truncated at any UTF-8 character boundary.
8624                            $breakpos += 1;
8625                            break;
8626                        }
8627                    }
8628                }
8629            }
8630            if ($breakpos == 0) {
8631                // This deals with the test_shorten_text_no_spaces case.
8632                $breakpos = $left + $entitieslength;
8633            } else if ($breakpos > $left + $entitieslength) {
8634                // This deals with the previous for loop breaking on the first char.
8635                $breakpos = $left + $entitieslength;
8636            }
8637
8638            $truncate .= core_text::substr($linematchings[2], 0, $breakpos);
8639            // Maximum length is reached, so get off the loop.
8640            break;
8641        } else {
8642            $truncate .= $linematchings[2];
8643            $totallength += $contentlength;
8644        }
8645
8646        // If the maximum length is reached, get off the loop.
8647        if ($totallength >= $ideal) {
8648            break;
8649        }
8650    }
8651
8652    // Add the defined ending to the text.
8653    $truncate .= $ending;
8654
8655    // Now calculate the list of open html tags based on the truncate position.
8656    $opentags = array();
8657    foreach ($tagdetails as $taginfo) {
8658        if ($taginfo->open) {
8659            // Add tag to the beginning of $opentags list.
8660            array_unshift($opentags, $taginfo->tag);
8661        } else {
8662            // Can have multiple exact same open tags, close the last one.
8663            $pos = array_search($taginfo->tag, array_reverse($opentags, true));
8664            if ($pos !== false) {
8665                unset($opentags[$pos]);
8666            }
8667        }
8668    }
8669
8670    // Close all unclosed html-tags.
8671    foreach ($opentags as $tag) {
8672        if ($tag === 'if') {
8673            $truncate .= '<!--<![endif]-->';
8674        } else {
8675            $truncate .= '</' . $tag . '>';
8676        }
8677    }
8678
8679    return $truncate;
8680}
8681
8682/**
8683 * Shortens a given filename by removing characters positioned after the ideal string length.
8684 * When the filename is too long, the file cannot be created on the filesystem due to exceeding max byte size.
8685 * Limiting the filename to a certain size (considering multibyte characters) will prevent this.
8686 *
8687 * @param string $filename file name
8688 * @param int $length ideal string length
8689 * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness.
8690 * @return string $shortened shortened file name
8691 */
8692function shorten_filename($filename, $length = MAX_FILENAME_SIZE, $includehash = false) {
8693    $shortened = $filename;
8694    // Extract a part of the filename if it's char size exceeds the ideal string length.
8695    if (core_text::strlen($filename) > $length) {
8696        // Exclude extension if present in filename.
8697        $mimetypes = get_mimetypes_array();
8698        $extension = pathinfo($filename, PATHINFO_EXTENSION);
8699        if ($extension && !empty($mimetypes[$extension])) {
8700            $basename = pathinfo($filename, PATHINFO_FILENAME);
8701            $hash = empty($includehash) ? '' : ' - ' . substr(sha1($basename), 0, 10);
8702            $shortened = core_text::substr($basename, 0, $length - strlen($hash)) . $hash;
8703            $shortened .= '.' . $extension;
8704        } else {
8705            $hash = empty($includehash) ? '' : ' - ' . substr(sha1($filename), 0, 10);
8706            $shortened = core_text::substr($filename, 0, $length - strlen($hash)) . $hash;
8707        }
8708    }
8709    return $shortened;
8710}
8711
8712/**
8713 * Shortens a given array of filenames by removing characters positioned after the ideal string length.
8714 *
8715 * @param array $path The paths to reduce the length.
8716 * @param int $length Ideal string length
8717 * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness.
8718 * @return array $result Shortened paths in array.
8719 */
8720function shorten_filenames(array $path, $length = MAX_FILENAME_SIZE, $includehash = false) {
8721    $result = null;
8722
8723    $result = array_reduce($path, function($carry, $singlepath) use ($length, $includehash) {
8724        $carry[] = shorten_filename($singlepath, $length, $includehash);
8725        return $carry;
8726    }, []);
8727
8728    return $result;
8729}
8730
8731/**
8732 * Given dates in seconds, how many weeks is the date from startdate
8733 * The first week is 1, the second 2 etc ...
8734 *
8735 * @param int $startdate Timestamp for the start date
8736 * @param int $thedate Timestamp for the end date
8737 * @return string
8738 */
8739function getweek ($startdate, $thedate) {
8740    if ($thedate < $startdate) {
8741        return 0;
8742    }
8743
8744    return floor(($thedate - $startdate) / WEEKSECS) + 1;
8745}
8746
8747/**
8748 * Returns a randomly generated password of length $maxlen.  inspired by
8749 *
8750 * {@link http://www.phpbuilder.com/columns/jesus19990502.php3} and
8751 * {@link http://es2.php.net/manual/en/function.str-shuffle.php#73254}
8752 *
8753 * @param int $maxlen  The maximum size of the password being generated.
8754 * @return string
8755 */
8756function generate_password($maxlen=10) {
8757    global $CFG;
8758
8759    if (empty($CFG->passwordpolicy)) {
8760        $fillers = PASSWORD_DIGITS;
8761        $wordlist = file($CFG->wordlist);
8762        $word1 = trim($wordlist[rand(0, count($wordlist) - 1)]);
8763        $word2 = trim($wordlist[rand(0, count($wordlist) - 1)]);
8764        $filler1 = $fillers[rand(0, strlen($fillers) - 1)];
8765        $password = $word1 . $filler1 . $word2;
8766    } else {
8767        $minlen = !empty($CFG->minpasswordlength) ? $CFG->minpasswordlength : 0;
8768        $digits = $CFG->minpassworddigits;
8769        $lower = $CFG->minpasswordlower;
8770        $upper = $CFG->minpasswordupper;
8771        $nonalphanum = $CFG->minpasswordnonalphanum;
8772        $total = $lower + $upper + $digits + $nonalphanum;
8773        // Var minlength should be the greater one of the two ( $minlen and $total ).
8774        $minlen = $minlen < $total ? $total : $minlen;
8775        // Var maxlen can never be smaller than minlen.
8776        $maxlen = $minlen > $maxlen ? $minlen : $maxlen;
8777        $additional = $maxlen - $total;
8778
8779        // Make sure we have enough characters to fulfill
8780        // complexity requirements.
8781        $passworddigits = PASSWORD_DIGITS;
8782        while ($digits > strlen($passworddigits)) {
8783            $passworddigits .= PASSWORD_DIGITS;
8784        }
8785        $passwordlower = PASSWORD_LOWER;
8786        while ($lower > strlen($passwordlower)) {
8787            $passwordlower .= PASSWORD_LOWER;
8788        }
8789        $passwordupper = PASSWORD_UPPER;
8790        while ($upper > strlen($passwordupper)) {
8791            $passwordupper .= PASSWORD_UPPER;
8792        }
8793        $passwordnonalphanum = PASSWORD_NONALPHANUM;
8794        while ($nonalphanum > strlen($passwordnonalphanum)) {
8795            $passwordnonalphanum .= PASSWORD_NONALPHANUM;
8796        }
8797
8798        // Now mix and shuffle it all.
8799        $password = str_shuffle (substr(str_shuffle ($passwordlower), 0, $lower) .
8800                                 substr(str_shuffle ($passwordupper), 0, $upper) .
8801                                 substr(str_shuffle ($passworddigits), 0, $digits) .
8802                                 substr(str_shuffle ($passwordnonalphanum), 0 , $nonalphanum) .
8803                                 substr(str_shuffle ($passwordlower .
8804                                                     $passwordupper .
8805                                                     $passworddigits .
8806                                                     $passwordnonalphanum), 0 , $additional));
8807    }
8808
8809    return substr ($password, 0, $maxlen);
8810}
8811
8812/**
8813 * Given a float, prints it nicely.
8814 * Localized floats must not be used in calculations!
8815 *
8816 * The stripzeros feature is intended for making numbers look nicer in small
8817 * areas where it is not necessary to indicate the degree of accuracy by showing
8818 * ending zeros. If you turn it on with $decimalpoints set to 3, for example,
8819 * then it will display '5.4' instead of '5.400' or '5' instead of '5.000'.
8820 *
8821 * @param float $float The float to print
8822 * @param int $decimalpoints The number of decimal places to print. -1 is a special value for auto detect (full precision).
8823 * @param bool $localized use localized decimal separator
8824 * @param bool $stripzeros If true, removes final zeros after decimal point. It will be ignored and the trailing zeros after
8825 *                         the decimal point are always striped if $decimalpoints is -1.
8826 * @return string locale float
8827 */
8828function format_float($float, $decimalpoints=1, $localized=true, $stripzeros=false) {
8829    if (is_null($float)) {
8830        return '';
8831    }
8832    if ($localized) {
8833        $separator = get_string('decsep', 'langconfig');
8834    } else {
8835        $separator = '.';
8836    }
8837    if ($decimalpoints == -1) {
8838        // The following counts the number of decimals.
8839        // It is safe as both floatval() and round() functions have same behaviour when non-numeric values are provided.
8840        $floatval = floatval($float);
8841        for ($decimalpoints = 0; $floatval != round($float, $decimalpoints); $decimalpoints++);
8842    }
8843
8844    $result = number_format($float, $decimalpoints, $separator, '');
8845    if ($stripzeros) {
8846        // Remove zeros and final dot if not needed.
8847        $result = preg_replace('~(' . preg_quote($separator, '~') . ')?0+$~', '', $result);
8848    }
8849    return $result;
8850}
8851
8852/**
8853 * Converts locale specific floating point/comma number back to standard PHP float value
8854 * Do NOT try to do any math operations before this conversion on any user submitted floats!
8855 *
8856 * @param string $localefloat locale aware float representation
8857 * @param bool $strict If true, then check the input and return false if it is not a valid number.
8858 * @return mixed float|bool - false or the parsed float.
8859 */
8860function unformat_float($localefloat, $strict = false) {
8861    $localefloat = trim($localefloat);
8862
8863    if ($localefloat == '') {
8864        return null;
8865    }
8866
8867    $localefloat = str_replace(' ', '', $localefloat); // No spaces - those might be used as thousand separators.
8868    $localefloat = str_replace(get_string('decsep', 'langconfig'), '.', $localefloat);
8869
8870    if ($strict && !is_numeric($localefloat)) {
8871        return false;
8872    }
8873
8874    return (float)$localefloat;
8875}
8876
8877/**
8878 * Given a simple array, this shuffles it up just like shuffle()
8879 * Unlike PHP's shuffle() this function works on any machine.
8880 *
8881 * @param array $array The array to be rearranged
8882 * @return array
8883 */
8884function swapshuffle($array) {
8885
8886    $last = count($array) - 1;
8887    for ($i = 0; $i <= $last; $i++) {
8888        $from = rand(0, $last);
8889        $curr = $array[$i];
8890        $array[$i] = $array[$from];
8891        $array[$from] = $curr;
8892    }
8893    return $array;
8894}
8895
8896/**
8897 * Like {@link swapshuffle()}, but works on associative arrays
8898 *
8899 * @param array $array The associative array to be rearranged
8900 * @return array
8901 */
8902function swapshuffle_assoc($array) {
8903
8904    $newarray = array();
8905    $newkeys = swapshuffle(array_keys($array));
8906
8907    foreach ($newkeys as $newkey) {
8908        $newarray[$newkey] = $array[$newkey];
8909    }
8910    return $newarray;
8911}
8912
8913/**
8914 * Given an arbitrary array, and a number of draws,
8915 * this function returns an array with that amount
8916 * of items.  The indexes are retained.
8917 *
8918 * @todo Finish documenting this function
8919 *
8920 * @param array $array
8921 * @param int $draws
8922 * @return array
8923 */
8924function draw_rand_array($array, $draws) {
8925
8926    $return = array();
8927
8928    $last = count($array);
8929
8930    if ($draws > $last) {
8931        $draws = $last;
8932    }
8933
8934    while ($draws > 0) {
8935        $last--;
8936
8937        $keys = array_keys($array);
8938        $rand = rand(0, $last);
8939
8940        $return[$keys[$rand]] = $array[$keys[$rand]];
8941        unset($array[$keys[$rand]]);
8942
8943        $draws--;
8944    }
8945
8946    return $return;
8947}
8948
8949/**
8950 * Calculate the difference between two microtimes
8951 *
8952 * @param string $a The first Microtime
8953 * @param string $b The second Microtime
8954 * @return string
8955 */
8956function microtime_diff($a, $b) {
8957    list($adec, $asec) = explode(' ', $a);
8958    list($bdec, $bsec) = explode(' ', $b);
8959    return $bsec - $asec + $bdec - $adec;
8960}
8961
8962/**
8963 * Given a list (eg a,b,c,d,e) this function returns
8964 * an array of 1->a, 2->b, 3->c etc
8965 *
8966 * @param string $list The string to explode into array bits
8967 * @param string $separator The separator used within the list string
8968 * @return array The now assembled array
8969 */
8970function make_menu_from_list($list, $separator=',') {
8971
8972    $array = array_reverse(explode($separator, $list), true);
8973    foreach ($array as $key => $item) {
8974        $outarray[$key+1] = trim($item);
8975    }
8976    return $outarray;
8977}
8978
8979/**
8980 * Creates an array that represents all the current grades that
8981 * can be chosen using the given grading type.
8982 *
8983 * Negative numbers
8984 * are scales, zero is no grade, and positive numbers are maximum
8985 * grades.
8986 *
8987 * @todo Finish documenting this function or better deprecated this completely!
8988 *
8989 * @param int $gradingtype
8990 * @return array
8991 */
8992function make_grades_menu($gradingtype) {
8993    global $DB;
8994
8995    $grades = array();
8996    if ($gradingtype < 0) {
8997        if ($scale = $DB->get_record('scale', array('id'=> (-$gradingtype)))) {
8998            return make_menu_from_list($scale->scale);
8999        }
9000    } else if ($gradingtype > 0) {
9001        for ($i=$gradingtype; $i>=0; $i--) {
9002            $grades[$i] = $i .' / '. $gradingtype;
9003        }
9004        return $grades;
9005    }
9006    return $grades;
9007}
9008
9009/**
9010 * make_unique_id_code
9011 *
9012 * @todo Finish documenting this function
9013 *
9014 * @uses $_SERVER
9015 * @param string $extra Extra string to append to the end of the code
9016 * @return string
9017 */
9018function make_unique_id_code($extra = '') {
9019
9020    $hostname = 'unknownhost';
9021    if (!empty($_SERVER['HTTP_HOST'])) {
9022        $hostname = $_SERVER['HTTP_HOST'];
9023    } else if (!empty($_ENV['HTTP_HOST'])) {
9024        $hostname = $_ENV['HTTP_HOST'];
9025    } else if (!empty($_SERVER['SERVER_NAME'])) {
9026        $hostname = $_SERVER['SERVER_NAME'];
9027    } else if (!empty($_ENV['SERVER_NAME'])) {
9028        $hostname = $_ENV['SERVER_NAME'];
9029    }
9030
9031    $date = gmdate("ymdHis");
9032
9033    $random =  random_string(6);
9034
9035    if ($extra) {
9036        return $hostname .'+'. $date .'+'. $random .'+'. $extra;
9037    } else {
9038        return $hostname .'+'. $date .'+'. $random;
9039    }
9040}
9041
9042
9043/**
9044 * Function to check the passed address is within the passed subnet
9045 *
9046 * The parameter is a comma separated string of subnet definitions.
9047 * Subnet strings can be in one of three formats:
9048 *   1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn          (number of bits in net mask)
9049 *   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)
9050 *   3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx.                  (incomplete address, a bit non-technical ;-)
9051 * Code for type 1 modified from user posted comments by mediator at
9052 * {@link http://au.php.net/manual/en/function.ip2long.php}
9053 *
9054 * @param string $addr    The address you are checking
9055 * @param string $subnetstr    The string of subnet addresses
9056 * @return bool
9057 */
9058function address_in_subnet($addr, $subnetstr) {
9059
9060    if ($addr == '0.0.0.0') {
9061        return false;
9062    }
9063    $subnets = explode(',', $subnetstr);
9064    $found = false;
9065    $addr = trim($addr);
9066    $addr = cleanremoteaddr($addr, false); // Normalise.
9067    if ($addr === null) {
9068        return false;
9069    }
9070    $addrparts = explode(':', $addr);
9071
9072    $ipv6 = strpos($addr, ':');
9073
9074    foreach ($subnets as $subnet) {
9075        $subnet = trim($subnet);
9076        if ($subnet === '') {
9077            continue;
9078        }
9079
9080        if (strpos($subnet, '/') !== false) {
9081            // 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn.
9082            list($ip, $mask) = explode('/', $subnet);
9083            $mask = trim($mask);
9084            if (!is_number($mask)) {
9085                continue; // Incorect mask number, eh?
9086            }
9087            $ip = cleanremoteaddr($ip, false); // Normalise.
9088            if ($ip === null) {
9089                continue;
9090            }
9091            if (strpos($ip, ':') !== false) {
9092                // IPv6.
9093                if (!$ipv6) {
9094                    continue;
9095                }
9096                if ($mask > 128 or $mask < 0) {
9097                    continue; // Nonsense.
9098                }
9099                if ($mask == 0) {
9100                    return true; // Any address.
9101                }
9102                if ($mask == 128) {
9103                    if ($ip === $addr) {
9104                        return true;
9105                    }
9106                    continue;
9107                }
9108                $ipparts = explode(':', $ip);
9109                $modulo  = $mask % 16;
9110                $ipnet   = array_slice($ipparts, 0, ($mask-$modulo)/16);
9111                $addrnet = array_slice($addrparts, 0, ($mask-$modulo)/16);
9112                if (implode(':', $ipnet) === implode(':', $addrnet)) {
9113                    if ($modulo == 0) {
9114                        return true;
9115                    }
9116                    $pos     = ($mask-$modulo)/16;
9117                    $ipnet   = hexdec($ipparts[$pos]);
9118                    $addrnet = hexdec($addrparts[$pos]);
9119                    $mask    = 0xffff << (16 - $modulo);
9120                    if (($addrnet & $mask) == ($ipnet & $mask)) {
9121                        return true;
9122                    }
9123                }
9124
9125            } else {
9126                // IPv4.
9127                if ($ipv6) {
9128                    continue;
9129                }
9130                if ($mask > 32 or $mask < 0) {
9131                    continue; // Nonsense.
9132                }
9133                if ($mask == 0) {
9134                    return true;
9135                }
9136                if ($mask == 32) {
9137                    if ($ip === $addr) {
9138                        return true;
9139                    }
9140                    continue;
9141                }
9142                $mask = 0xffffffff << (32 - $mask);
9143                if (((ip2long($addr) & $mask) == (ip2long($ip) & $mask))) {
9144                    return true;
9145                }
9146            }
9147
9148        } else if (strpos($subnet, '-') !== false) {
9149            // 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.
9150            $parts = explode('-', $subnet);
9151            if (count($parts) != 2) {
9152                continue;
9153            }
9154
9155            if (strpos($subnet, ':') !== false) {
9156                // IPv6.
9157                if (!$ipv6) {
9158                    continue;
9159                }
9160                $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise.
9161                if ($ipstart === null) {
9162                    continue;
9163                }
9164                $ipparts = explode(':', $ipstart);
9165                $start = hexdec(array_pop($ipparts));
9166                $ipparts[] = trim($parts[1]);
9167                $ipend = cleanremoteaddr(implode(':', $ipparts), false); // Normalise.
9168                if ($ipend === null) {
9169                    continue;
9170                }
9171                $ipparts[7] = '';
9172                $ipnet = implode(':', $ipparts);
9173                if (strpos($addr, $ipnet) !== 0) {
9174                    continue;
9175                }
9176                $ipparts = explode(':', $ipend);
9177                $end = hexdec($ipparts[7]);
9178
9179                $addrend = hexdec($addrparts[7]);
9180
9181                if (($addrend >= $start) and ($addrend <= $end)) {
9182                    return true;
9183                }
9184
9185            } else {
9186                // IPv4.
9187                if ($ipv6) {
9188                    continue;
9189                }
9190                $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise.
9191                if ($ipstart === null) {
9192                    continue;
9193                }
9194                $ipparts = explode('.', $ipstart);
9195                $ipparts[3] = trim($parts[1]);
9196                $ipend = cleanremoteaddr(implode('.', $ipparts), false); // Normalise.
9197                if ($ipend === null) {
9198                    continue;
9199                }
9200
9201                if ((ip2long($addr) >= ip2long($ipstart)) and (ip2long($addr) <= ip2long($ipend))) {
9202                    return true;
9203                }
9204            }
9205
9206        } else {
9207            // 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx.
9208            if (strpos($subnet, ':') !== false) {
9209                // IPv6.
9210                if (!$ipv6) {
9211                    continue;
9212                }
9213                $parts = explode(':', $subnet);
9214                $count = count($parts);
9215                if ($parts[$count-1] === '') {
9216                    unset($parts[$count-1]); // Trim trailing :'s.
9217                    $count--;
9218                    $subnet = implode('.', $parts);
9219                }
9220                $isip = cleanremoteaddr($subnet, false); // Normalise.
9221                if ($isip !== null) {
9222                    if ($isip === $addr) {
9223                        return true;
9224                    }
9225                    continue;
9226                } else if ($count > 8) {
9227                    continue;
9228                }
9229                $zeros = array_fill(0, 8-$count, '0');
9230                $subnet = $subnet.':'.implode(':', $zeros).'/'.($count*16);
9231                if (address_in_subnet($addr, $subnet)) {
9232                    return true;
9233                }
9234
9235            } else {
9236                // IPv4.
9237                if ($ipv6) {
9238                    continue;
9239                }
9240                $parts = explode('.', $subnet);
9241                $count = count($parts);
9242                if ($parts[$count-1] === '') {
9243                    unset($parts[$count-1]); // Trim trailing .
9244                    $count--;
9245                    $subnet = implode('.', $parts);
9246                }
9247                if ($count == 4) {
9248                    $subnet = cleanremoteaddr($subnet, false); // Normalise.
9249                    if ($subnet === $addr) {
9250                        return true;
9251                    }
9252                    continue;
9253                } else if ($count > 4) {
9254                    continue;
9255                }
9256                $zeros = array_fill(0, 4-$count, '0');
9257                $subnet = $subnet.'.'.implode('.', $zeros).'/'.($count*8);
9258                if (address_in_subnet($addr, $subnet)) {
9259                    return true;
9260                }
9261            }
9262        }
9263    }
9264
9265    return false;
9266}
9267
9268/**
9269 * For outputting debugging info
9270 *
9271 * @param string $string The string to write
9272 * @param string $eol The end of line char(s) to use
9273 * @param string $sleep Period to make the application sleep
9274 *                      This ensures any messages have time to display before redirect
9275 */
9276function mtrace($string, $eol="\n", $sleep=0) {
9277    global $CFG;
9278
9279    if (isset($CFG->mtrace_wrapper) && function_exists($CFG->mtrace_wrapper)) {
9280        $fn = $CFG->mtrace_wrapper;
9281        $fn($string, $eol);
9282        return;
9283    } else if (defined('STDOUT') && !PHPUNIT_TEST && !defined('BEHAT_TEST')) {
9284        // We must explicitly call the add_line function here.
9285        // Uses of fwrite to STDOUT are not picked up by ob_start.
9286        if ($output = \core\task\logmanager::add_line("{$string}{$eol}")) {
9287            fwrite(STDOUT, $output);
9288        }
9289    } else {
9290        echo $string . $eol;
9291    }
9292
9293    // Flush again.
9294    flush();
9295
9296    // Delay to keep message on user's screen in case of subsequent redirect.
9297    if ($sleep) {
9298        sleep($sleep);
9299    }
9300}
9301
9302/**
9303 * Replace 1 or more slashes or backslashes to 1 slash
9304 *
9305 * @param string $path The path to strip
9306 * @return string the path with double slashes removed
9307 */
9308function cleardoubleslashes ($path) {
9309    return preg_replace('/(\/|\\\){1,}/', '/', $path);
9310}
9311
9312/**
9313 * Is the current ip in a given list?
9314 *
9315 * @param string $list
9316 * @return bool
9317 */
9318function remoteip_in_list($list) {
9319    $clientip = getremoteaddr(null);
9320
9321    if (!$clientip) {
9322        // Ensure access on cli.
9323        return true;
9324    }
9325    return \core\ip_utils::is_ip_in_subnet_list($clientip, $list);
9326}
9327
9328/**
9329 * Returns most reliable client address
9330 *
9331 * @param string $default If an address can't be determined, then return this
9332 * @return string The remote IP address
9333 */
9334function getremoteaddr($default='0.0.0.0') {
9335    global $CFG;
9336
9337    if (!isset($CFG->getremoteaddrconf)) {
9338        // This will happen, for example, before just after the upgrade, as the
9339        // user is redirected to the admin screen.
9340        $variablestoskip = GETREMOTEADDR_SKIP_DEFAULT;
9341    } else {
9342        $variablestoskip = $CFG->getremoteaddrconf;
9343    }
9344    if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_CLIENT_IP)) {
9345        if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
9346            $address = cleanremoteaddr($_SERVER['HTTP_CLIENT_IP']);
9347            return $address ? $address : $default;
9348        }
9349    }
9350    if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR)) {
9351        if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
9352            $forwardedaddresses = explode(",", $_SERVER['HTTP_X_FORWARDED_FOR']);
9353
9354            $forwardedaddresses = array_filter($forwardedaddresses, function($ip) {
9355                global $CFG;
9356                return !\core\ip_utils::is_ip_in_subnet_list($ip, $CFG->reverseproxyignore ?? '', ',');
9357            });
9358
9359            // Multiple proxies can append values to this header including an
9360            // untrusted original request header so we must only trust the last ip.
9361            $address = end($forwardedaddresses);
9362
9363            if (substr_count($address, ":") > 1) {
9364                // Remove port and brackets from IPv6.
9365                if (preg_match("/\[(.*)\]:/", $address, $matches)) {
9366                    $address = $matches[1];
9367                }
9368            } else {
9369                // Remove port from IPv4.
9370                if (substr_count($address, ":") == 1) {
9371                    $parts = explode(":", $address);
9372                    $address = $parts[0];
9373                }
9374            }
9375
9376            $address = cleanremoteaddr($address);
9377            return $address ? $address : $default;
9378        }
9379    }
9380    if (!empty($_SERVER['REMOTE_ADDR'])) {
9381        $address = cleanremoteaddr($_SERVER['REMOTE_ADDR']);
9382        return $address ? $address : $default;
9383    } else {
9384        return $default;
9385    }
9386}
9387
9388/**
9389 * Cleans an ip address. Internal addresses are now allowed.
9390 * (Originally local addresses were not allowed.)
9391 *
9392 * @param string $addr IPv4 or IPv6 address
9393 * @param bool $compress use IPv6 address compression
9394 * @return string normalised ip address string, null if error
9395 */
9396function cleanremoteaddr($addr, $compress=false) {
9397    $addr = trim($addr);
9398
9399    if (strpos($addr, ':') !== false) {
9400        // Can be only IPv6.
9401        $parts = explode(':', $addr);
9402        $count = count($parts);
9403
9404        if (strpos($parts[$count-1], '.') !== false) {
9405            // Legacy ipv4 notation.
9406            $last = array_pop($parts);
9407            $ipv4 = cleanremoteaddr($last, true);
9408            if ($ipv4 === null) {
9409                return null;
9410            }
9411            $bits = explode('.', $ipv4);
9412            $parts[] = dechex($bits[0]).dechex($bits[1]);
9413            $parts[] = dechex($bits[2]).dechex($bits[3]);
9414            $count = count($parts);
9415            $addr = implode(':', $parts);
9416        }
9417
9418        if ($count < 3 or $count > 8) {
9419            return null; // Severly malformed.
9420        }
9421
9422        if ($count != 8) {
9423            if (strpos($addr, '::') === false) {
9424                return null; // Malformed.
9425            }
9426            // Uncompress.
9427            $insertat = array_search('', $parts, true);
9428            $missing = array_fill(0, 1 + 8 - $count, '0');
9429            array_splice($parts, $insertat, 1, $missing);
9430            foreach ($parts as $key => $part) {
9431                if ($part === '') {
9432                    $parts[$key] = '0';
9433                }
9434            }
9435        }
9436
9437        $adr = implode(':', $parts);
9438        if (!preg_match('/^([0-9a-f]{1,4})(:[0-9a-f]{1,4})*$/i', $adr)) {
9439            return null; // Incorrect format - sorry.
9440        }
9441
9442        // Normalise 0s and case.
9443        $parts = array_map('hexdec', $parts);
9444        $parts = array_map('dechex', $parts);
9445
9446        $result = implode(':', $parts);
9447
9448        if (!$compress) {
9449            return $result;
9450        }
9451
9452        if ($result === '0:0:0:0:0:0:0:0') {
9453            return '::'; // All addresses.
9454        }
9455
9456        $compressed = preg_replace('/(:0)+:0$/', '::', $result, 1);
9457        if ($compressed !== $result) {
9458            return $compressed;
9459        }
9460
9461        $compressed = preg_replace('/^(0:){2,7}/', '::', $result, 1);
9462        if ($compressed !== $result) {
9463            return $compressed;
9464        }
9465
9466        $compressed = preg_replace('/(:0){2,6}:/', '::', $result, 1);
9467        if ($compressed !== $result) {
9468            return $compressed;
9469        }
9470
9471        return $result;
9472    }
9473
9474    // First get all things that look like IPv4 addresses.
9475    $parts = array();
9476    if (!preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $addr, $parts)) {
9477        return null;
9478    }
9479    unset($parts[0]);
9480
9481    foreach ($parts as $key => $match) {
9482        if ($match > 255) {
9483            return null;
9484        }
9485        $parts[$key] = (int)$match; // Normalise 0s.
9486    }
9487
9488    return implode('.', $parts);
9489}
9490
9491
9492/**
9493 * Is IP address a public address?
9494 *
9495 * @param string $ip The ip to check
9496 * @return bool true if the ip is public
9497 */
9498function ip_is_public($ip) {
9499    return (bool) filter_var($ip, FILTER_VALIDATE_IP, (FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE));
9500}
9501
9502/**
9503 * This function will make a complete copy of anything it's given,
9504 * regardless of whether it's an object or not.
9505 *
9506 * @param mixed $thing Something you want cloned
9507 * @return mixed What ever it is you passed it
9508 */
9509function fullclone($thing) {
9510    return unserialize(serialize($thing));
9511}
9512
9513/**
9514 * Used to make sure that $min <= $value <= $max
9515 *
9516 * Make sure that value is between min, and max
9517 *
9518 * @param int $min The minimum value
9519 * @param int $value The value to check
9520 * @param int $max The maximum value
9521 * @return int
9522 */
9523function bounded_number($min, $value, $max) {
9524    if ($value < $min) {
9525        return $min;
9526    }
9527    if ($value > $max) {
9528        return $max;
9529    }
9530    return $value;
9531}
9532
9533/**
9534 * Check if there is a nested array within the passed array
9535 *
9536 * @param array $array
9537 * @return bool true if there is a nested array false otherwise
9538 */
9539function array_is_nested($array) {
9540    foreach ($array as $value) {
9541        if (is_array($value)) {
9542            return true;
9543        }
9544    }
9545    return false;
9546}
9547
9548/**
9549 * get_performance_info() pairs up with init_performance_info()
9550 * loaded in setup.php. Returns an array with 'html' and 'txt'
9551 * values ready for use, and each of the individual stats provided
9552 * separately as well.
9553 *
9554 * @return array
9555 */
9556function get_performance_info() {
9557    global $CFG, $PERF, $DB, $PAGE;
9558
9559    $info = array();
9560    $info['txt']  = me() . ' '; // Holds log-friendly representation.
9561
9562    $info['html'] = '';
9563    if (!empty($CFG->themedesignermode)) {
9564        // Attempt to avoid devs debugging peformance issues, when its caused by css building and so on.
9565        $info['html'] .= '<p><strong>Warning: Theme designer mode is enabled.</strong></p>';
9566    }
9567    $info['html'] .= '<ul class="list-unstyled row mx-md-0">';         // Holds userfriendly HTML representation.
9568
9569    $info['realtime'] = microtime_diff($PERF->starttime, microtime());
9570
9571    $info['html'] .= '<li class="timeused col-sm-4">'.$info['realtime'].' secs</li> ';
9572    $info['txt'] .= 'time: '.$info['realtime'].'s ';
9573
9574    // GET/POST (or NULL if $_SERVER['REQUEST_METHOD'] is undefined) is useful for txt logged information.
9575    $info['txt'] .= 'method: ' . ($_SERVER['REQUEST_METHOD'] ?? "NULL") . ' ';
9576
9577    if (function_exists('memory_get_usage')) {
9578        $info['memory_total'] = memory_get_usage();
9579        $info['memory_growth'] = memory_get_usage() - $PERF->startmemory;
9580        $info['html'] .= '<li class="memoryused col-sm-4">RAM: '.display_size($info['memory_total']).'</li> ';
9581        $info['txt']  .= 'memory_total: '.$info['memory_total'].'B (' . display_size($info['memory_total']).') memory_growth: '.
9582            $info['memory_growth'].'B ('.display_size($info['memory_growth']).') ';
9583    }
9584
9585    if (function_exists('memory_get_peak_usage')) {
9586        $info['memory_peak'] = memory_get_peak_usage();
9587        $info['html'] .= '<li class="memoryused col-sm-4">RAM peak: '.display_size($info['memory_peak']).'</li> ';
9588        $info['txt']  .= 'memory_peak: '.$info['memory_peak'].'B (' . display_size($info['memory_peak']).') ';
9589    }
9590
9591    $info['html'] .= '</ul><ul class="list-unstyled row mx-md-0">';
9592    $inc = get_included_files();
9593    $info['includecount'] = count($inc);
9594    $info['html'] .= '<li class="included col-sm-4">Included '.$info['includecount'].' files</li> ';
9595    $info['txt']  .= 'includecount: '.$info['includecount'].' ';
9596
9597    if (!empty($CFG->early_install_lang) or empty($PAGE)) {
9598        // We can not track more performance before installation or before PAGE init, sorry.
9599        return $info;
9600    }
9601
9602    $filtermanager = filter_manager::instance();
9603    if (method_exists($filtermanager, 'get_performance_summary')) {
9604        list($filterinfo, $nicenames) = $filtermanager->get_performance_summary();
9605        $info = array_merge($filterinfo, $info);
9606        foreach ($filterinfo as $key => $value) {
9607            $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> ";
9608            $info['txt'] .= "$key: $value ";
9609        }
9610    }
9611
9612    $stringmanager = get_string_manager();
9613    if (method_exists($stringmanager, 'get_performance_summary')) {
9614        list($filterinfo, $nicenames) = $stringmanager->get_performance_summary();
9615        $info = array_merge($filterinfo, $info);
9616        foreach ($filterinfo as $key => $value) {
9617            $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> ";
9618            $info['txt'] .= "$key: $value ";
9619        }
9620    }
9621
9622    if (!empty($PERF->logwrites)) {
9623        $info['logwrites'] = $PERF->logwrites;
9624        $info['html'] .= '<li class="logwrites col-sm-4">Log DB writes '.$info['logwrites'].'</li> ';
9625        $info['txt'] .= 'logwrites: '.$info['logwrites'].' ';
9626    }
9627
9628    $info['dbqueries'] = $DB->perf_get_reads().'/'.($DB->perf_get_writes() - $PERF->logwrites);
9629    $info['html'] .= '<li class="dbqueries col-sm-4">DB reads/writes: '.$info['dbqueries'].'</li> ';
9630    $info['txt'] .= 'db reads/writes: '.$info['dbqueries'].' ';
9631
9632    if ($DB->want_read_slave()) {
9633        $info['dbreads_slave'] = $DB->perf_get_reads_slave();
9634        $info['html'] .= '<li class="dbqueries col-sm-4">DB reads from slave: '.$info['dbreads_slave'].'</li> ';
9635        $info['txt'] .= 'db reads from slave: '.$info['dbreads_slave'].' ';
9636    }
9637
9638    $info['dbtime'] = round($DB->perf_get_queries_time(), 5);
9639    $info['html'] .= '<li class="dbtime col-sm-4">DB queries time: '.$info['dbtime'].' secs</li> ';
9640    $info['txt'] .= 'db queries time: ' . $info['dbtime'] . 's ';
9641
9642    if (function_exists('posix_times')) {
9643        $ptimes = posix_times();
9644        if (is_array($ptimes)) {
9645            foreach ($ptimes as $key => $val) {
9646                $info[$key] = $ptimes[$key] -  $PERF->startposixtimes[$key];
9647            }
9648            $info['html'] .= "<li class=\"posixtimes col-sm-4\">ticks: $info[ticks] user: $info[utime]";
9649            $info['html'] .= "sys: $info[stime] cuser: $info[cutime] csys: $info[cstime]</li> ";
9650            $info['txt'] .= "ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime] ";
9651        }
9652    }
9653
9654    // Grab the load average for the last minute.
9655    // /proc will only work under some linux configurations
9656    // while uptime is there under MacOSX/Darwin and other unices.
9657    if (is_readable('/proc/loadavg') && $loadavg = @file('/proc/loadavg')) {
9658        list($serverload) = explode(' ', $loadavg[0]);
9659        unset($loadavg);
9660    } else if ( function_exists('is_executable') && is_executable('/usr/bin/uptime') && $loadavg = `/usr/bin/uptime` ) {
9661        if (preg_match('/load averages?: (\d+[\.,:]\d+)/', $loadavg, $matches)) {
9662            $serverload = $matches[1];
9663        } else {
9664            trigger_error('Could not parse uptime output!');
9665        }
9666    }
9667    if (!empty($serverload)) {
9668        $info['serverload'] = $serverload;
9669        $info['html'] .= '<li class="serverload col-sm-4">Load average: '.$info['serverload'].'</li> ';
9670        $info['txt'] .= "serverload: {$info['serverload']} ";
9671    }
9672
9673    // Display size of session if session started.
9674    if ($si = \core\session\manager::get_performance_info()) {
9675        $info['sessionsize'] = $si['size'];
9676        $info['html'] .= "<li class=\"serverload col-sm-4\">" . $si['html'] . "</li>";
9677        $info['txt'] .= $si['txt'];
9678    }
9679
9680    $info['html'] .= '</ul>';
9681    $html = '';
9682    if ($stats = cache_helper::get_stats()) {
9683
9684        $table = new html_table();
9685        $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered';
9686        $table->head = ['Mode', 'Cache item', 'Static', 'H', 'M', get_string('mappingprimary', 'cache'), 'H', 'M', 'S'];
9687        $table->data = [];
9688        $table->align = ['left', 'left', 'left', 'right', 'right', 'left', 'right', 'right', 'right'];
9689
9690        $text = 'Caches used (hits/misses/sets): ';
9691        $hits = 0;
9692        $misses = 0;
9693        $sets = 0;
9694        $maxstores = 0;
9695
9696        // We want to align static caches into their own column.
9697        $hasstatic = false;
9698        foreach ($stats as $definition => $details) {
9699            $numstores = count($details['stores']);
9700            $first = key($details['stores']);
9701            if ($first !== cache_store::STATIC_ACCEL) {
9702                $numstores++; // Add a blank space for the missing static store.
9703            }
9704            $maxstores = max($maxstores, $numstores);
9705        }
9706
9707        $storec = 0;
9708
9709        while ($storec++ < ($maxstores - 2)) {
9710            if ($storec == ($maxstores - 2)) {
9711                $table->head[] = get_string('mappingfinal', 'cache');
9712            } else {
9713                $table->head[] = "Store $storec";
9714            }
9715            $table->align[] = 'left';
9716            $table->align[] = 'right';
9717            $table->align[] = 'right';
9718            $table->align[] = 'right';
9719            $table->head[] = 'H';
9720            $table->head[] = 'M';
9721            $table->head[] = 'S';
9722        }
9723
9724        ksort($stats);
9725
9726        foreach ($stats as $definition => $details) {
9727            switch ($details['mode']) {
9728                case cache_store::MODE_APPLICATION:
9729                    $modeclass = 'application';
9730                    $mode = ' <span title="application cache">App</span>';
9731                    break;
9732                case cache_store::MODE_SESSION:
9733                    $modeclass = 'session';
9734                    $mode = ' <span title="session cache">Ses</span>';
9735                    break;
9736                case cache_store::MODE_REQUEST:
9737                    $modeclass = 'request';
9738                    $mode = ' <span title="request cache">Req</span>';
9739                    break;
9740            }
9741            $row = [$mode, $definition];
9742
9743            $text .= "$definition {";
9744
9745            $storec = 0;
9746            foreach ($details['stores'] as $store => $data) {
9747
9748                if ($storec == 0 && $store !== cache_store::STATIC_ACCEL) {
9749                    $row[] = '';
9750                    $row[] = '';
9751                    $row[] = '';
9752                    $storec++;
9753                }
9754
9755                $hits   += $data['hits'];
9756                $misses += $data['misses'];
9757                $sets   += $data['sets'];
9758                if ($data['hits'] == 0 and $data['misses'] > 0) {
9759                    $cachestoreclass = 'nohits bg-danger';
9760                } else if ($data['hits'] < $data['misses']) {
9761                    $cachestoreclass = 'lowhits bg-warning text-dark';
9762                } else {
9763                    $cachestoreclass = 'hihits';
9764                }
9765                $text .= "$store($data[hits]/$data[misses]/$data[sets]) ";
9766                $cell = new html_table_cell($store);
9767                $cell->attributes = ['class' => $cachestoreclass];
9768                $row[] = $cell;
9769                $cell = new html_table_cell($data['hits']);
9770                $cell->attributes = ['class' => $cachestoreclass];
9771                $row[] = $cell;
9772                $cell = new html_table_cell($data['misses']);
9773                $cell->attributes = ['class' => $cachestoreclass];
9774                $row[] = $cell;
9775
9776                if ($store !== cache_store::STATIC_ACCEL) {
9777                    // The static cache is never set.
9778                    $cell = new html_table_cell($data['sets']);
9779                    $cell->attributes = ['class' => $cachestoreclass];
9780                    $row[] = $cell;
9781                }
9782                $storec++;
9783            }
9784            while ($storec++ < $maxstores) {
9785                $row[] = '';
9786                $row[] = '';
9787                $row[] = '';
9788                $row[] = '';
9789            }
9790            $text .= '} ';
9791
9792            $table->data[] = $row;
9793        }
9794
9795        $html .= html_writer::table($table);
9796
9797        // Now lets also show sub totals for each cache store.
9798        $storetotals = [];
9799        $storetotal = ['hits' => 0, 'misses' => 0, 'sets' => 0];
9800        foreach ($stats as $definition => $details) {
9801            foreach ($details['stores'] as $store => $data) {
9802                if (!array_key_exists($store, $storetotals)) {
9803                    $storetotals[$store] = ['hits' => 0, 'misses' => 0, 'sets' => 0];
9804                }
9805                $storetotals[$store]['class']   = $data['class'];
9806                $storetotals[$store]['hits']   += $data['hits'];
9807                $storetotals[$store]['misses'] += $data['misses'];
9808                $storetotals[$store]['sets']   += $data['sets'];
9809                $storetotal['hits']   += $data['hits'];
9810                $storetotal['misses'] += $data['misses'];
9811                $storetotal['sets']   += $data['sets'];
9812            }
9813        }
9814
9815        $table = new html_table();
9816        $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered';
9817        $table->head = [get_string('storename', 'cache'), get_string('type_cachestore', 'plugin'), 'H', 'M', 'S'];
9818        $table->data = [];
9819        $table->align = ['left', 'left', 'right', 'right', 'right'];
9820
9821        ksort($storetotals);
9822
9823        foreach ($storetotals as $store => $data) {
9824            $row = [];
9825            if ($data['hits'] == 0 and $data['misses'] > 0) {
9826                $cachestoreclass = 'nohits bg-danger';
9827            } else if ($data['hits'] < $data['misses']) {
9828                $cachestoreclass = 'lowhits bg-warning text-dark';
9829            } else {
9830                $cachestoreclass = 'hihits';
9831            }
9832            $cell = new html_table_cell($store);
9833            $cell->attributes = ['class' => $cachestoreclass];
9834            $row[] = $cell;
9835            $cell = new html_table_cell($data['class']);
9836            $cell->attributes = ['class' => $cachestoreclass];
9837            $row[] = $cell;
9838            $cell = new html_table_cell($data['hits']);
9839            $cell->attributes = ['class' => $cachestoreclass];
9840            $row[] = $cell;
9841            $cell = new html_table_cell($data['misses']);
9842            $cell->attributes = ['class' => $cachestoreclass];
9843            $row[] = $cell;
9844            $cell = new html_table_cell($data['sets']);
9845            $cell->attributes = ['class' => $cachestoreclass];
9846            $row[] = $cell;
9847            $table->data[] = $row;
9848        }
9849        $row = [
9850            get_string('total'),
9851            '',
9852            $storetotal['hits'],
9853            $storetotal['misses'],
9854            $storetotal['sets'],
9855        ];
9856        $table->data[] = $row;
9857
9858        $html .= html_writer::table($table);
9859
9860        $info['cachesused'] = "$hits / $misses / $sets";
9861        $info['html'] .= $html;
9862        $info['txt'] .= $text.'. ';
9863    } else {
9864        $info['cachesused'] = '0 / 0 / 0';
9865        $info['html'] .= '<div class="cachesused">Caches used (hits/misses/sets): 0/0/0</div>';
9866        $info['txt'] .= 'Caches used (hits/misses/sets): 0/0/0 ';
9867    }
9868
9869    $info['html'] = '<div class="performanceinfo siteinfo container-fluid px-md-0 overflow-auto mt-3">'.$info['html'].'</div>';
9870    return $info;
9871}
9872
9873/**
9874 * Renames a file or directory to a unique name within the same directory.
9875 *
9876 * This function is designed to avoid any potential race conditions, and select an unused name.
9877 *
9878 * @param string $filepath Original filepath
9879 * @param string $prefix Prefix to use for the temporary name
9880 * @return string|bool New file path or false if failed
9881 * @since Moodle 3.10
9882 */
9883function rename_to_unused_name(string $filepath, string $prefix = '_temp_') {
9884    $dir = dirname($filepath);
9885    $basename = $dir . '/' . $prefix;
9886    $limit = 0;
9887    while ($limit < 100) {
9888        // Select a new name based on a random number.
9889        $newfilepath = $basename . md5(mt_rand());
9890
9891        // Attempt a rename to that new name.
9892        if (@rename($filepath, $newfilepath)) {
9893            return $newfilepath;
9894        }
9895
9896        // The first time, do some sanity checks, maybe it is failing for a good reason and there
9897        // is no point trying 100 times if so.
9898        if ($limit === 0 && (!file_exists($filepath) || !is_writable($dir))) {
9899            return false;
9900        }
9901        $limit++;
9902    }
9903    return false;
9904}
9905
9906/**
9907 * Delete directory or only its content
9908 *
9909 * @param string $dir directory path
9910 * @param bool $contentonly
9911 * @return bool success, true also if dir does not exist
9912 */
9913function remove_dir($dir, $contentonly=false) {
9914    if (!is_dir($dir)) {
9915        // Nothing to do.
9916        return true;
9917    }
9918
9919    if (!$contentonly) {
9920        // Start by renaming the directory; this will guarantee that other processes don't write to it
9921        // while it is in the process of being deleted.
9922        $tempdir = rename_to_unused_name($dir);
9923        if ($tempdir) {
9924            // If the rename was successful then delete the $tempdir instead.
9925            $dir = $tempdir;
9926        }
9927        // If the rename fails, we will continue through and attempt to delete the directory
9928        // without renaming it since that is likely to at least delete most of the files.
9929    }
9930
9931    if (!$handle = opendir($dir)) {
9932        return false;
9933    }
9934    $result = true;
9935    while (false!==($item = readdir($handle))) {
9936        if ($item != '.' && $item != '..') {
9937            if (is_dir($dir.'/'.$item)) {
9938                $result = remove_dir($dir.'/'.$item) && $result;
9939            } else {
9940                $result = unlink($dir.'/'.$item) && $result;
9941            }
9942        }
9943    }
9944    closedir($handle);
9945    if ($contentonly) {
9946        clearstatcache(); // Make sure file stat cache is properly invalidated.
9947        return $result;
9948    }
9949    $result = rmdir($dir); // If anything left the result will be false, no need for && $result.
9950    clearstatcache(); // Make sure file stat cache is properly invalidated.
9951    return $result;
9952}
9953
9954/**
9955 * Detect if an object or a class contains a given property
9956 * will take an actual object or the name of a class
9957 *
9958 * @param mix $obj Name of class or real object to test
9959 * @param string $property name of property to find
9960 * @return bool true if property exists
9961 */
9962function object_property_exists( $obj, $property ) {
9963    if (is_string( $obj )) {
9964        $properties = get_class_vars( $obj );
9965    } else {
9966        $properties = get_object_vars( $obj );
9967    }
9968    return array_key_exists( $property, $properties );
9969}
9970
9971/**
9972 * Converts an object into an associative array
9973 *
9974 * This function converts an object into an associative array by iterating
9975 * over its public properties. Because this function uses the foreach
9976 * construct, Iterators are respected. It works recursively on arrays of objects.
9977 * Arrays and simple values are returned as is.
9978 *
9979 * If class has magic properties, it can implement IteratorAggregate
9980 * and return all available properties in getIterator()
9981 *
9982 * @param mixed $var
9983 * @return array
9984 */
9985function convert_to_array($var) {
9986    $result = array();
9987
9988    // Loop over elements/properties.
9989    foreach ($var as $key => $value) {
9990        // Recursively convert objects.
9991        if (is_object($value) || is_array($value)) {
9992            $result[$key] = convert_to_array($value);
9993        } else {
9994            // Simple values are untouched.
9995            $result[$key] = $value;
9996        }
9997    }
9998    return $result;
9999}
10000
10001/**
10002 * Detect a custom script replacement in the data directory that will
10003 * replace an existing moodle script
10004 *
10005 * @return string|bool full path name if a custom script exists, false if no custom script exists
10006 */
10007function custom_script_path() {
10008    global $CFG, $SCRIPT;
10009
10010    if ($SCRIPT === null) {
10011        // Probably some weird external script.
10012        return false;
10013    }
10014
10015    $scriptpath = $CFG->customscripts . $SCRIPT;
10016
10017    // Check the custom script exists.
10018    if (file_exists($scriptpath) and is_file($scriptpath)) {
10019        return $scriptpath;
10020    } else {
10021        return false;
10022    }
10023}
10024
10025/**
10026 * Returns whether or not the user object is a remote MNET user. This function
10027 * is in moodlelib because it does not rely on loading any of the MNET code.
10028 *
10029 * @param object $user A valid user object
10030 * @return bool        True if the user is from a remote Moodle.
10031 */
10032function is_mnet_remote_user($user) {
10033    global $CFG;
10034
10035    if (!isset($CFG->mnet_localhost_id)) {
10036        include_once($CFG->dirroot . '/mnet/lib.php');
10037        $env = new mnet_environment();
10038        $env->init();
10039        unset($env);
10040    }
10041
10042    return (!empty($user->mnethostid) && $user->mnethostid != $CFG->mnet_localhost_id);
10043}
10044
10045/**
10046 * This function will search for browser prefereed languages, setting Moodle
10047 * to use the best one available if $SESSION->lang is undefined
10048 */
10049function setup_lang_from_browser() {
10050    global $CFG, $SESSION, $USER;
10051
10052    if (!empty($SESSION->lang) or !empty($USER->lang) or empty($CFG->autolang)) {
10053        // Lang is defined in session or user profile, nothing to do.
10054        return;
10055    }
10056
10057    if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { // There isn't list of browser langs, nothing to do.
10058        return;
10059    }
10060
10061    // Extract and clean langs from headers.
10062    $rawlangs = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
10063    $rawlangs = str_replace('-', '_', $rawlangs);         // We are using underscores.
10064    $rawlangs = explode(',', $rawlangs);                  // Convert to array.
10065    $langs = array();
10066
10067    $order = 1.0;
10068    foreach ($rawlangs as $lang) {
10069        if (strpos($lang, ';') === false) {
10070            $langs[(string)$order] = $lang;
10071            $order = $order-0.01;
10072        } else {
10073            $parts = explode(';', $lang);
10074            $pos = strpos($parts[1], '=');
10075            $langs[substr($parts[1], $pos+1)] = $parts[0];
10076        }
10077    }
10078    krsort($langs, SORT_NUMERIC);
10079
10080    // Look for such langs under standard locations.
10081    foreach ($langs as $lang) {
10082        // Clean it properly for include.
10083        $lang = strtolower(clean_param($lang, PARAM_SAFEDIR));
10084        if (get_string_manager()->translation_exists($lang, false)) {
10085            // Lang exists, set it in session.
10086            $SESSION->lang = $lang;
10087            // We have finished. Go out.
10088            break;
10089        }
10090    }
10091    return;
10092}
10093
10094/**
10095 * Check if $url matches anything in proxybypass list
10096 *
10097 * Any errors just result in the proxy being used (least bad)
10098 *
10099 * @param string $url url to check
10100 * @return boolean true if we should bypass the proxy
10101 */
10102function is_proxybypass( $url ) {
10103    global $CFG;
10104
10105    // Sanity check.
10106    if (empty($CFG->proxyhost) or empty($CFG->proxybypass)) {
10107        return false;
10108    }
10109
10110    // Get the host part out of the url.
10111    if (!$host = parse_url( $url, PHP_URL_HOST )) {
10112        return false;
10113    }
10114
10115    // Get the possible bypass hosts into an array.
10116    $matches = explode( ',', $CFG->proxybypass );
10117
10118    // Check for a match.
10119    // (IPs need to match the left hand side and hosts the right of the url,
10120    // but we can recklessly check both as there can't be a false +ve).
10121    foreach ($matches as $match) {
10122        $match = trim($match);
10123
10124        // Try for IP match (Left side).
10125        $lhs = substr($host, 0, strlen($match));
10126        if (strcasecmp($match, $lhs)==0) {
10127            return true;
10128        }
10129
10130        // Try for host match (Right side).
10131        $rhs = substr($host, -strlen($match));
10132        if (strcasecmp($match, $rhs)==0) {
10133            return true;
10134        }
10135    }
10136
10137    // Nothing matched.
10138    return false;
10139}
10140
10141/**
10142 * Check if the passed navigation is of the new style
10143 *
10144 * @param mixed $navigation
10145 * @return bool true for yes false for no
10146 */
10147function is_newnav($navigation) {
10148    if (is_array($navigation) && !empty($navigation['newnav'])) {
10149        return true;
10150    } else {
10151        return false;
10152    }
10153}
10154
10155/**
10156 * Checks whether the given variable name is defined as a variable within the given object.
10157 *
10158 * This will NOT work with stdClass objects, which have no class variables.
10159 *
10160 * @param string $var The variable name
10161 * @param object $object The object to check
10162 * @return boolean
10163 */
10164function in_object_vars($var, $object) {
10165    $classvars = get_class_vars(get_class($object));
10166    $classvars = array_keys($classvars);
10167    return in_array($var, $classvars);
10168}
10169
10170/**
10171 * Returns an array without repeated objects.
10172 * This function is similar to array_unique, but for arrays that have objects as values
10173 *
10174 * @param array $array
10175 * @param bool $keepkeyassoc
10176 * @return array
10177 */
10178function object_array_unique($array, $keepkeyassoc = true) {
10179    $duplicatekeys = array();
10180    $tmp         = array();
10181
10182    foreach ($array as $key => $val) {
10183        // Convert objects to arrays, in_array() does not support objects.
10184        if (is_object($val)) {
10185            $val = (array)$val;
10186        }
10187
10188        if (!in_array($val, $tmp)) {
10189            $tmp[] = $val;
10190        } else {
10191            $duplicatekeys[] = $key;
10192        }
10193    }
10194
10195    foreach ($duplicatekeys as $key) {
10196        unset($array[$key]);
10197    }
10198
10199    return $keepkeyassoc ? $array : array_values($array);
10200}
10201
10202/**
10203 * Is a userid the primary administrator?
10204 *
10205 * @param int $userid int id of user to check
10206 * @return boolean
10207 */
10208function is_primary_admin($userid) {
10209    $primaryadmin =  get_admin();
10210
10211    if ($userid == $primaryadmin->id) {
10212        return true;
10213    } else {
10214        return false;
10215    }
10216}
10217
10218/**
10219 * Returns the site identifier
10220 *
10221 * @return string $CFG->siteidentifier, first making sure it is properly initialised.
10222 */
10223function get_site_identifier() {
10224    global $CFG;
10225    // Check to see if it is missing. If so, initialise it.
10226    if (empty($CFG->siteidentifier)) {
10227        set_config('siteidentifier', random_string(32) . $_SERVER['HTTP_HOST']);
10228    }
10229    // Return it.
10230    return $CFG->siteidentifier;
10231}
10232
10233/**
10234 * Check whether the given password has no more than the specified
10235 * number of consecutive identical characters.
10236 *
10237 * @param string $password   password to be checked against the password policy
10238 * @param integer $maxchars  maximum number of consecutive identical characters
10239 * @return bool
10240 */
10241function check_consecutive_identical_characters($password, $maxchars) {
10242
10243    if ($maxchars < 1) {
10244        return true; // Zero 0 is to disable this check.
10245    }
10246    if (strlen($password) <= $maxchars) {
10247        return true; // Too short to fail this test.
10248    }
10249
10250    $previouschar = '';
10251    $consecutivecount = 1;
10252    foreach (str_split($password) as $char) {
10253        if ($char != $previouschar) {
10254            $consecutivecount = 1;
10255        } else {
10256            $consecutivecount++;
10257            if ($consecutivecount > $maxchars) {
10258                return false; // Check failed already.
10259            }
10260        }
10261
10262        $previouschar = $char;
10263    }
10264
10265    return true;
10266}
10267
10268/**
10269 * Helper function to do partial function binding.
10270 * so we can use it for preg_replace_callback, for example
10271 * this works with php functions, user functions, static methods and class methods
10272 * it returns you a callback that you can pass on like so:
10273 *
10274 * $callback = partial('somefunction', $arg1, $arg2);
10275 *     or
10276 * $callback = partial(array('someclass', 'somestaticmethod'), $arg1, $arg2);
10277 *     or even
10278 * $obj = new someclass();
10279 * $callback = partial(array($obj, 'somemethod'), $arg1, $arg2);
10280 *
10281 * and then the arguments that are passed through at calltime are appended to the argument list.
10282 *
10283 * @param mixed $function a php callback
10284 * @param mixed $arg1,... $argv arguments to partially bind with
10285 * @return array Array callback
10286 */
10287function partial() {
10288    if (!class_exists('partial')) {
10289        /**
10290         * Used to manage function binding.
10291         * @copyright  2009 Penny Leach
10292         * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
10293         */
10294        class partial{
10295            /** @var array */
10296            public $values = array();
10297            /** @var string The function to call as a callback. */
10298            public $func;
10299            /**
10300             * Constructor
10301             * @param string $func
10302             * @param array $args
10303             */
10304            public function __construct($func, $args) {
10305                $this->values = $args;
10306                $this->func = $func;
10307            }
10308            /**
10309             * Calls the callback function.
10310             * @return mixed
10311             */
10312            public function method() {
10313                $args = func_get_args();
10314                return call_user_func_array($this->func, array_merge($this->values, $args));
10315            }
10316        }
10317    }
10318    $args = func_get_args();
10319    $func = array_shift($args);
10320    $p = new partial($func, $args);
10321    return array($p, 'method');
10322}
10323
10324/**
10325 * helper function to load up and initialise the mnet environment
10326 * this must be called before you use mnet functions.
10327 *
10328 * @return mnet_environment the equivalent of old $MNET global
10329 */
10330function get_mnet_environment() {
10331    global $CFG;
10332    require_once($CFG->dirroot . '/mnet/lib.php');
10333    static $instance = null;
10334    if (empty($instance)) {
10335        $instance = new mnet_environment();
10336        $instance->init();
10337    }
10338    return $instance;
10339}
10340
10341/**
10342 * during xmlrpc server code execution, any code wishing to access
10343 * information about the remote peer must use this to get it.
10344 *
10345 * @return mnet_remote_client the equivalent of old $MNETREMOTE_CLIENT global
10346 */
10347function get_mnet_remote_client() {
10348    if (!defined('MNET_SERVER')) {
10349        debugging(get_string('notinxmlrpcserver', 'mnet'));
10350        return false;
10351    }
10352    global $MNET_REMOTE_CLIENT;
10353    if (isset($MNET_REMOTE_CLIENT)) {
10354        return $MNET_REMOTE_CLIENT;
10355    }
10356    return false;
10357}
10358
10359/**
10360 * during the xmlrpc server code execution, this will be called
10361 * to setup the object returned by {@link get_mnet_remote_client}
10362 *
10363 * @param mnet_remote_client $client the client to set up
10364 * @throws moodle_exception
10365 */
10366function set_mnet_remote_client($client) {
10367    if (!defined('MNET_SERVER')) {
10368        throw new moodle_exception('notinxmlrpcserver', 'mnet');
10369    }
10370    global $MNET_REMOTE_CLIENT;
10371    $MNET_REMOTE_CLIENT = $client;
10372}
10373
10374/**
10375 * return the jump url for a given remote user
10376 * this is used for rewriting forum post links in emails, etc
10377 *
10378 * @param stdclass $user the user to get the idp url for
10379 */
10380function mnet_get_idp_jump_url($user) {
10381    global $CFG;
10382
10383    static $mnetjumps = array();
10384    if (!array_key_exists($user->mnethostid, $mnetjumps)) {
10385        $idp = mnet_get_peer_host($user->mnethostid);
10386        $idpjumppath = mnet_get_app_jumppath($idp->applicationid);
10387        $mnetjumps[$user->mnethostid] = $idp->wwwroot . $idpjumppath . '?hostwwwroot=' . $CFG->wwwroot . '&wantsurl=';
10388    }
10389    return $mnetjumps[$user->mnethostid];
10390}
10391
10392/**
10393 * Gets the homepage to use for the current user
10394 *
10395 * @return int One of HOMEPAGE_*
10396 */
10397function get_home_page() {
10398    global $CFG;
10399
10400    if (isloggedin() && !isguestuser() && !empty($CFG->defaulthomepage)) {
10401        if ($CFG->defaulthomepage == HOMEPAGE_MY) {
10402            return HOMEPAGE_MY;
10403        } else {
10404            return (int)get_user_preferences('user_home_page_preference', HOMEPAGE_MY);
10405        }
10406    }
10407    return HOMEPAGE_SITE;
10408}
10409
10410/**
10411 * Gets the name of a course to be displayed when showing a list of courses.
10412 * By default this is just $course->fullname but user can configure it. The
10413 * result of this function should be passed through print_string.
10414 * @param stdClass|core_course_list_element $course Moodle course object
10415 * @return string Display name of course (either fullname or short + fullname)
10416 */
10417function get_course_display_name_for_list($course) {
10418    global $CFG;
10419    if (!empty($CFG->courselistshortnames)) {
10420        if (!($course instanceof stdClass)) {
10421            $course = (object)convert_to_array($course);
10422        }
10423        return get_string('courseextendednamedisplay', '', $course);
10424    } else {
10425        return $course->fullname;
10426    }
10427}
10428
10429/**
10430 * Safe analogue of unserialize() that can only parse arrays
10431 *
10432 * Arrays may contain only integers or strings as both keys and values. Nested arrays are allowed.
10433 * Note: If any string (key or value) has semicolon (;) as part of the string parsing will fail.
10434 * This is a simple method to substitute unnecessary unserialize() in code and not intended to cover all possible cases.
10435 *
10436 * @param string $expression
10437 * @return array|bool either parsed array or false if parsing was impossible.
10438 */
10439function unserialize_array($expression) {
10440    $subs = [];
10441    // Find nested arrays, parse them and store in $subs , substitute with special string.
10442    while (preg_match('/([\^;\}])(a:\d+:\{[^\{\}]*\})/', $expression, $matches) && strlen($matches[2]) < strlen($expression)) {
10443        $key = '--SUB' . count($subs) . '--';
10444        $subs[$key] = unserialize_array($matches[2]);
10445        if ($subs[$key] === false) {
10446            return false;
10447        }
10448        $expression = str_replace($matches[2], $key . ';', $expression);
10449    }
10450
10451    // Check the expression is an array.
10452    if (!preg_match('/^a:(\d+):\{([^\}]*)\}$/', $expression, $matches1)) {
10453        return false;
10454    }
10455    // Get the size and elements of an array (key;value;key;value;....).
10456    $parts = explode(';', $matches1[2]);
10457    $size = intval($matches1[1]);
10458    if (count($parts) < $size * 2 + 1) {
10459        return false;
10460    }
10461    // Analyze each part and make sure it is an integer or string or a substitute.
10462    $value = [];
10463    for ($i = 0; $i < $size * 2; $i++) {
10464        if (preg_match('/^i:(\d+)$/', $parts[$i], $matches2)) {
10465            $parts[$i] = (int)$matches2[1];
10466        } else if (preg_match('/^s:(\d+):"(.*)"$/', $parts[$i], $matches3) && strlen($matches3[2]) == (int)$matches3[1]) {
10467            $parts[$i] = $matches3[2];
10468        } else if (preg_match('/^--SUB\d+--$/', $parts[$i])) {
10469            $parts[$i] = $subs[$parts[$i]];
10470        } else {
10471            return false;
10472        }
10473    }
10474    // Combine keys and values.
10475    for ($i = 0; $i < $size * 2; $i += 2) {
10476        $value[$parts[$i]] = $parts[$i+1];
10477    }
10478    return $value;
10479}
10480
10481/**
10482 * Safe method for unserializing given input that is expected to contain only a serialized instance of an stdClass object
10483 *
10484 * If any class type other than stdClass is included in the input string, it will not be instantiated and will be cast to an
10485 * stdClass object. The initial cast to array, then back to object is to ensure we are always returning the correct type,
10486 * otherwise we would return an instances of {@see __PHP_Incomplete_class} for malformed strings
10487 *
10488 * @param string $input
10489 * @return stdClass
10490 */
10491function unserialize_object(string $input): stdClass {
10492    $instance = (array) unserialize($input, ['allowed_classes' => [stdClass::class]]);
10493    return (object) $instance;
10494}
10495
10496/**
10497 * The lang_string class
10498 *
10499 * This special class is used to create an object representation of a string request.
10500 * It is special because processing doesn't occur until the object is first used.
10501 * The class was created especially to aid performance in areas where strings were
10502 * required to be generated but were not necessarily used.
10503 * As an example the admin tree when generated uses over 1500 strings, of which
10504 * normally only 1/3 are ever actually printed at any time.
10505 * The performance advantage is achieved by not actually processing strings that
10506 * arn't being used, as such reducing the processing required for the page.
10507 *
10508 * How to use the lang_string class?
10509 *     There are two methods of using the lang_string class, first through the
10510 *     forth argument of the get_string function, and secondly directly.
10511 *     The following are examples of both.
10512 * 1. Through get_string calls e.g.
10513 *     $string = get_string($identifier, $component, $a, true);
10514 *     $string = get_string('yes', 'moodle', null, true);
10515 * 2. Direct instantiation
10516 *     $string = new lang_string($identifier, $component, $a, $lang);
10517 *     $string = new lang_string('yes');
10518 *
10519 * How do I use a lang_string object?
10520 *     The lang_string object makes use of a magic __toString method so that you
10521 *     are able to use the object exactly as you would use a string in most cases.
10522 *     This means you are able to collect it into a variable and then directly
10523 *     echo it, or concatenate it into another string, or similar.
10524 *     The other thing you can do is manually get the string by calling the
10525 *     lang_strings out method e.g.
10526 *         $string = new lang_string('yes');
10527 *         $string->out();
10528 *     Also worth noting is that the out method can take one argument, $lang which
10529 *     allows the developer to change the language on the fly.
10530 *
10531 * When should I use a lang_string object?
10532 *     The lang_string object is designed to be used in any situation where a
10533 *     string may not be needed, but needs to be generated.
10534 *     The admin tree is a good example of where lang_string objects should be
10535 *     used.
10536 *     A more practical example would be any class that requries strings that may
10537 *     not be printed (after all classes get renderer by renderers and who knows
10538 *     what they will do ;))
10539 *
10540 * When should I not use a lang_string object?
10541 *     Don't use lang_strings when you are going to use a string immediately.
10542 *     There is no need as it will be processed immediately and there will be no
10543 *     advantage, and in fact perhaps a negative hit as a class has to be
10544 *     instantiated for a lang_string object, however get_string won't require
10545 *     that.
10546 *
10547 * Limitations:
10548 * 1. You cannot use a lang_string object as an array offset. Doing so will
10549 *     result in PHP throwing an error. (You can use it as an object property!)
10550 *
10551 * @package    core
10552 * @category   string
10553 * @copyright  2011 Sam Hemelryk
10554 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
10555 */
10556class lang_string {
10557
10558    /** @var string The strings identifier */
10559    protected $identifier;
10560    /** @var string The strings component. Default '' */
10561    protected $component = '';
10562    /** @var array|stdClass Any arguments required for the string. Default null */
10563    protected $a = null;
10564    /** @var string The language to use when processing the string. Default null */
10565    protected $lang = null;
10566
10567    /** @var string The processed string (once processed) */
10568    protected $string = null;
10569
10570    /**
10571     * A special boolean. If set to true then the object has been woken up and
10572     * cannot be regenerated. If this is set then $this->string MUST be used.
10573     * @var bool
10574     */
10575    protected $forcedstring = false;
10576
10577    /**
10578     * Constructs a lang_string object
10579     *
10580     * This function should do as little processing as possible to ensure the best
10581     * performance for strings that won't be used.
10582     *
10583     * @param string $identifier The strings identifier
10584     * @param string $component The strings component
10585     * @param stdClass|array $a Any arguments the string requires
10586     * @param string $lang The language to use when processing the string.
10587     * @throws coding_exception
10588     */
10589    public function __construct($identifier, $component = '', $a = null, $lang = null) {
10590        if (empty($component)) {
10591            $component = 'moodle';
10592        }
10593
10594        $this->identifier = $identifier;
10595        $this->component = $component;
10596        $this->lang = $lang;
10597
10598        // We MUST duplicate $a to ensure that it if it changes by reference those
10599        // changes are not carried across.
10600        // To do this we always ensure $a or its properties/values are strings
10601        // and that any properties/values that arn't convertable are forgotten.
10602        if ($a !== null) {
10603            if (is_scalar($a)) {
10604                $this->a = $a;
10605            } else if ($a instanceof lang_string) {
10606                $this->a = $a->out();
10607            } else if (is_object($a) or is_array($a)) {
10608                $a = (array)$a;
10609                $this->a = array();
10610                foreach ($a as $key => $value) {
10611                    // Make sure conversion errors don't get displayed (results in '').
10612                    if (is_array($value)) {
10613                        $this->a[$key] = '';
10614                    } else if (is_object($value)) {
10615                        if (method_exists($value, '__toString')) {
10616                            $this->a[$key] = $value->__toString();
10617                        } else {
10618                            $this->a[$key] = '';
10619                        }
10620                    } else {
10621                        $this->a[$key] = (string)$value;
10622                    }
10623                }
10624            }
10625        }
10626
10627        if (debugging(false, DEBUG_DEVELOPER)) {
10628            if (clean_param($this->identifier, PARAM_STRINGID) == '') {
10629                throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please check your string definition');
10630            }
10631            if (!empty($this->component) && clean_param($this->component, PARAM_COMPONENT) == '') {
10632                throw new coding_exception('Invalid string compontent. Please check your string definition');
10633            }
10634            if (!get_string_manager()->string_exists($this->identifier, $this->component)) {
10635                debugging('String does not exist. Please check your string definition for '.$this->identifier.'/'.$this->component, DEBUG_DEVELOPER);
10636            }
10637        }
10638    }
10639
10640    /**
10641     * Processes the string.
10642     *
10643     * This function actually processes the string, stores it in the string property
10644     * and then returns it.
10645     * You will notice that this function is VERY similar to the get_string method.
10646     * That is because it is pretty much doing the same thing.
10647     * However as this function is an upgrade it isn't as tolerant to backwards
10648     * compatibility.
10649     *
10650     * @return string
10651     * @throws coding_exception
10652     */
10653    protected function get_string() {
10654        global $CFG;
10655
10656        // Check if we need to process the string.
10657        if ($this->string === null) {
10658            // Check the quality of the identifier.
10659            if ($CFG->debugdeveloper && clean_param($this->identifier, PARAM_STRINGID) === '') {
10660                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);
10661            }
10662
10663            // Process the string.
10664            $this->string = get_string_manager()->get_string($this->identifier, $this->component, $this->a, $this->lang);
10665            // Debugging feature lets you display string identifier and component.
10666            if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
10667                $this->string .= ' {' . $this->identifier . '/' . $this->component . '}';
10668            }
10669        }
10670        // Return the string.
10671        return $this->string;
10672    }
10673
10674    /**
10675     * Returns the string
10676     *
10677     * @param string $lang The langauge to use when processing the string
10678     * @return string
10679     */
10680    public function out($lang = null) {
10681        if ($lang !== null && $lang != $this->lang && ($this->lang == null && $lang != current_language())) {
10682            if ($this->forcedstring) {
10683                debugging('lang_string objects that have been used cannot be printed in another language. ('.$this->lang.' used)', DEBUG_DEVELOPER);
10684                return $this->get_string();
10685            }
10686            $translatedstring = new lang_string($this->identifier, $this->component, $this->a, $lang);
10687            return $translatedstring->out();
10688        }
10689        return $this->get_string();
10690    }
10691
10692    /**
10693     * Magic __toString method for printing a string
10694     *
10695     * @return string
10696     */
10697    public function __toString() {
10698        return $this->get_string();
10699    }
10700
10701    /**
10702     * Magic __set_state method used for var_export
10703     *
10704     * @return string
10705     */
10706    public function __set_state() {
10707        return $this->get_string();
10708    }
10709
10710    /**
10711     * Prepares the lang_string for sleep and stores only the forcedstring and
10712     * string properties... the string cannot be regenerated so we need to ensure
10713     * it is generated for this.
10714     *
10715     * @return string
10716     */
10717    public function __sleep() {
10718        $this->get_string();
10719        $this->forcedstring = true;
10720        return array('forcedstring', 'string', 'lang');
10721    }
10722
10723    /**
10724     * Returns the identifier.
10725     *
10726     * @return string
10727     */
10728    public function get_identifier() {
10729        return $this->identifier;
10730    }
10731
10732    /**
10733     * Returns the component.
10734     *
10735     * @return string
10736     */
10737    public function get_component() {
10738        return $this->component;
10739    }
10740}
10741
10742/**
10743 * Get human readable name describing the given callable.
10744 *
10745 * This performs syntax check only to see if the given param looks like a valid function, method or closure.
10746 * It does not check if the callable actually exists.
10747 *
10748 * @param callable|string|array $callable
10749 * @return string|bool Human readable name of callable, or false if not a valid callable.
10750 */
10751function get_callable_name($callable) {
10752
10753    if (!is_callable($callable, true, $name)) {
10754        return false;
10755
10756    } else {
10757        return $name;
10758    }
10759}
10760
10761/**
10762 * Tries to guess if $CFG->wwwroot is publicly accessible or not.
10763 * Never put your faith on this function and rely on its accuracy as there might be false positives.
10764 * It just performs some simple checks, and mainly is used for places where we want to hide some options
10765 * such as site registration when $CFG->wwwroot is not publicly accessible.
10766 * Good thing is there is no false negative.
10767 * Note that it's possible to force the result of this check by specifying $CFG->site_is_public in config.php
10768 *
10769 * @return bool
10770 */
10771function site_is_public() {
10772    global $CFG;
10773
10774    // Return early if site admin has forced this setting.
10775    if (isset($CFG->site_is_public)) {
10776        return (bool)$CFG->site_is_public;
10777    }
10778
10779    $host = parse_url($CFG->wwwroot, PHP_URL_HOST);
10780
10781    if ($host === 'localhost' || preg_match('|^127\.\d+\.\d+\.\d+$|', $host)) {
10782        $ispublic = false;
10783    } else if (\core\ip_utils::is_ip_address($host) && !ip_is_public($host)) {
10784        $ispublic = false;
10785    } else if (($address = \core\ip_utils::get_ip_address($host)) && !ip_is_public($address)) {
10786        $ispublic = false;
10787    } else {
10788        $ispublic = true;
10789    }
10790
10791    return $ispublic;
10792}
10793