MDL-78962 core/loadingicon: remove jQuery requirement in the API
[moodle.git] / lib / moodlelib.php
blob9229f23dbe47b1a6be6414d19184cf4af68714ff
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 /**
18 * moodlelib.php - Moodle main library
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
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
31 defined('MOODLE_INTERNAL') || die();
33 // CONSTANTS (Encased in phpdoc proper comments).
35 // Date and time constants.
36 /**
37 * Time constant - the number of seconds in a year
39 define('YEARSECS', 31536000);
41 /**
42 * Time constant - the number of seconds in a week
44 define('WEEKSECS', 604800);
46 /**
47 * Time constant - the number of seconds in a day
49 define('DAYSECS', 86400);
51 /**
52 * Time constant - the number of seconds in an hour
54 define('HOURSECS', 3600);
56 /**
57 * Time constant - the number of seconds in a minute
59 define('MINSECS', 60);
61 /**
62 * Time constant - the number of minutes in a day
64 define('DAYMINS', 1440);
66 /**
67 * Time constant - the number of minutes in an hour
69 define('HOURMINS', 60);
71 // Parameter constants - every call to optional_param(), required_param()
72 // or clean_param() should have a specified type of parameter.
74 /**
75 * PARAM_ALPHA - contains only English ascii letters [a-zA-Z].
77 define('PARAM_ALPHA', 'alpha');
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
83 define('PARAM_ALPHAEXT', 'alphaext');
85 /**
86 * PARAM_ALPHANUM - expected numbers 0-9 and English ascii letters [a-zA-Z] only.
88 define('PARAM_ALPHANUM', 'alphanum');
90 /**
91 * PARAM_ALPHANUMEXT - expected numbers 0-9, letters (English ascii letters [a-zA-Z]) and _- only.
93 define('PARAM_ALPHANUMEXT', 'alphanumext');
95 /**
96 * PARAM_AUTH - actually checks to make sure the string is a valid auth plugin
98 define('PARAM_AUTH', 'auth');
101 * PARAM_BASE64 - Base 64 encoded format
103 define('PARAM_BASE64', 'base64');
106 * PARAM_BOOL - converts input into 0 or 1, use for switches in forms and urls.
108 define('PARAM_BOOL', 'bool');
111 * PARAM_CAPABILITY - A capability name, like 'moodle/role:manage'. Actually
112 * checked against the list of capabilities in the database.
114 define('PARAM_CAPABILITY', 'capability');
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.
123 define('PARAM_CLEANHTML', 'cleanhtml');
126 * PARAM_EMAIL - an email address following the RFC
128 define('PARAM_EMAIL', 'email');
131 * PARAM_FILE - safe file name, all dangerous chars are stripped, protects against XSS, SQL injections and directory traversals
133 define('PARAM_FILE', 'file');
136 * PARAM_FLOAT - a real/floating point number.
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.
142 define('PARAM_FLOAT', 'float');
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.
149 define('PARAM_LOCALISEDFLOAT', 'localisedfloat');
152 * PARAM_HOST - expected fully qualified domain name (FQDN) or an IPv4 dotted quad (IP address)
154 define('PARAM_HOST', 'host');
157 * PARAM_INT - integers only, use when expecting only numbers.
159 define('PARAM_INT', 'int');
162 * PARAM_LANG - checks to see if the string is a valid installed language in the current site.
164 define('PARAM_LANG', 'lang');
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!)
170 define('PARAM_LOCALURL', 'localurl');
173 * PARAM_NOTAGS - all html tags are stripped from the text. Do not abuse this type.
175 define('PARAM_NOTAGS', 'notags');
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
181 define('PARAM_PATH', 'path');
184 * PARAM_PEM - Privacy Enhanced Mail format
186 define('PARAM_PEM', 'pem');
189 * PARAM_PERMISSION - A permission, one of CAP_INHERIT, CAP_ALLOW, CAP_PREVENT or CAP_PROHIBIT.
191 define('PARAM_PERMISSION', 'permission');
194 * PARAM_RAW specifies a parameter that is not cleaned/processed in any way except the discarding of the invalid utf-8 characters
196 define('PARAM_RAW', 'raw');
199 * PARAM_RAW_TRIMMED like PARAM_RAW but leading and trailing whitespace is stripped.
201 define('PARAM_RAW_TRIMMED', 'raw_trimmed');
204 * PARAM_SAFEDIR - safe directory name, suitable for include() and require()
206 define('PARAM_SAFEDIR', 'safedir');
209 * PARAM_SAFEPATH - several PARAM_SAFEDIR joined by "/", suitable for include() and require(), plugin paths
210 * and other references to Moodle code files.
212 * This is NOT intended to be used for absolute paths or any user uploaded files.
214 define('PARAM_SAFEPATH', 'safepath');
217 * PARAM_SEQUENCE - expects a sequence of numbers like 8 to 1,5,6,4,6,8,9. Numbers and comma only.
219 define('PARAM_SEQUENCE', 'sequence');
222 * PARAM_TAG - one tag (interests, blogs, etc.) - mostly international characters and space, <> not supported
224 define('PARAM_TAG', 'tag');
227 * PARAM_TAGLIST - list of tags separated by commas (interests, blogs, etc.)
229 define('PARAM_TAGLIST', 'taglist');
232 * PARAM_TEXT - general plain text compatible with multilang filter, no other html tags. Please note '<', or '>' are allowed here.
234 define('PARAM_TEXT', 'text');
237 * PARAM_THEME - Checks to see if the string is a valid theme name in the current site
239 define('PARAM_THEME', 'theme');
242 * PARAM_URL - expected properly formatted URL. Please note that domain part is required, http://localhost/ is not accepted but
243 * http://localhost.localdomain/ is ok.
245 define('PARAM_URL', 'url');
248 * PARAM_USERNAME - Clean username to only contains allowed characters. This is to be used ONLY when manually creating user
249 * accounts, do NOT use when syncing with external systems!!
251 define('PARAM_USERNAME', 'username');
254 * PARAM_STRINGID - used to check if the given string is valid string identifier for get_string()
256 define('PARAM_STRINGID', 'stringid');
258 // DEPRECATED PARAM TYPES OR ALIASES - DO NOT USE FOR NEW CODE.
260 * PARAM_CLEAN - obsoleted, please use a more specific type of parameter.
261 * It was one of the first types, that is why it is abused so much ;-)
262 * @deprecated since 2.0
264 define('PARAM_CLEAN', 'clean');
267 * PARAM_INTEGER - deprecated alias for PARAM_INT
268 * @deprecated since 2.0
270 define('PARAM_INTEGER', 'int');
273 * PARAM_NUMBER - deprecated alias of PARAM_FLOAT
274 * @deprecated since 2.0
276 define('PARAM_NUMBER', 'float');
279 * PARAM_ACTION - deprecated alias for PARAM_ALPHANUMEXT, use for various actions in forms and urls
280 * NOTE: originally alias for PARAM_APLHA
281 * @deprecated since 2.0
283 define('PARAM_ACTION', 'alphanumext');
286 * PARAM_FORMAT - deprecated alias for PARAM_ALPHANUMEXT, use for names of plugins, formats, etc.
287 * NOTE: originally alias for PARAM_APLHA
288 * @deprecated since 2.0
290 define('PARAM_FORMAT', 'alphanumext');
293 * PARAM_MULTILANG - deprecated alias of PARAM_TEXT.
294 * @deprecated since 2.0
296 define('PARAM_MULTILANG', 'text');
299 * PARAM_TIMEZONE - expected timezone. Timezone can be int +-(0-13) or float +-(0.5-12.5) or
300 * string separated by '/' and can have '-' &/ '_' (eg. America/North_Dakota/New_Salem
301 * America/Port-au-Prince)
303 define('PARAM_TIMEZONE', 'timezone');
306 * PARAM_CLEANFILE - deprecated alias of PARAM_FILE; originally was removing regional chars too
308 define('PARAM_CLEANFILE', 'file');
311 * PARAM_COMPONENT is used for full component names (aka frankenstyle) such as 'mod_forum', 'core_rating', 'auth_ldap'.
312 * Short legacy subsystem names and module names are accepted too ex: 'forum', 'rating', 'user'.
313 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
314 * NOTE: numbers and underscores are strongly discouraged in plugin names!
316 define('PARAM_COMPONENT', 'component');
319 * PARAM_AREA is a name of area used when addressing files, comments, ratings, etc.
320 * It is usually used together with context id and component.
321 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
323 define('PARAM_AREA', 'area');
326 * PARAM_PLUGIN is used for plugin names such as 'forum', 'glossary', 'ldap', 'paypal', 'completionstatus'.
327 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
328 * NOTE: numbers and underscores are strongly discouraged in plugin names! Underscores are forbidden in module names.
330 define('PARAM_PLUGIN', 'plugin');
333 // Web Services.
336 * VALUE_REQUIRED - if the parameter is not supplied, there is an error
338 define('VALUE_REQUIRED', 1);
341 * VALUE_OPTIONAL - if the parameter is not supplied, then the param has no value
343 define('VALUE_OPTIONAL', 2);
346 * VALUE_DEFAULT - if the parameter is not supplied, then the default value is used
348 define('VALUE_DEFAULT', 0);
351 * NULL_NOT_ALLOWED - the parameter can not be set to null in the database
353 define('NULL_NOT_ALLOWED', false);
356 * NULL_ALLOWED - the parameter can be set to null in the database
358 define('NULL_ALLOWED', true);
360 // Page types.
363 * PAGE_COURSE_VIEW is a definition of a page type. For more information on the page class see moodle/lib/pagelib.php.
365 define('PAGE_COURSE_VIEW', 'course-view');
367 /** Get remote addr constant */
368 define('GETREMOTEADDR_SKIP_HTTP_CLIENT_IP', '1');
369 /** Get remote addr constant */
370 define('GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR', '2');
372 * GETREMOTEADDR_SKIP_DEFAULT defines the default behavior remote IP address validation.
374 define('GETREMOTEADDR_SKIP_DEFAULT', GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR|GETREMOTEADDR_SKIP_HTTP_CLIENT_IP);
376 // Blog access level constant declaration.
377 define ('BLOG_USER_LEVEL', 1);
378 define ('BLOG_GROUP_LEVEL', 2);
379 define ('BLOG_COURSE_LEVEL', 3);
380 define ('BLOG_SITE_LEVEL', 4);
381 define ('BLOG_GLOBAL_LEVEL', 5);
384 // Tag constants.
386 * To prevent problems with multibytes strings,Flag updating in nav not working on the review page. this should not exceed the
387 * length of "varchar(255) / 3 (bytes / utf-8 character) = 85".
388 * TODO: this is not correct, varchar(255) are 255 unicode chars ;-)
390 * @todo define(TAG_MAX_LENGTH) this is not correct, varchar(255) are 255 unicode chars ;-)
392 define('TAG_MAX_LENGTH', 50);
394 // Password policy constants.
395 define ('PASSWORD_LOWER', 'abcdefghijklmnopqrstuvwxyz');
396 define ('PASSWORD_UPPER', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
397 define ('PASSWORD_DIGITS', '0123456789');
398 define ('PASSWORD_NONALPHANUM', '.,;:!?_-+/*@#&$');
400 // Feature constants.
401 // Used for plugin_supports() to report features that are, or are not, supported by a module.
403 /** True if module can provide a grade */
404 define('FEATURE_GRADE_HAS_GRADE', 'grade_has_grade');
405 /** True if module supports outcomes */
406 define('FEATURE_GRADE_OUTCOMES', 'outcomes');
407 /** True if module supports advanced grading methods */
408 define('FEATURE_ADVANCED_GRADING', 'grade_advanced_grading');
409 /** True if module controls the grade visibility over the gradebook */
410 define('FEATURE_CONTROLS_GRADE_VISIBILITY', 'controlsgradevisbility');
411 /** True if module supports plagiarism plugins */
412 define('FEATURE_PLAGIARISM', 'plagiarism');
414 /** True if module has code to track whether somebody viewed it */
415 define('FEATURE_COMPLETION_TRACKS_VIEWS', 'completion_tracks_views');
416 /** True if module has custom completion rules */
417 define('FEATURE_COMPLETION_HAS_RULES', 'completion_has_rules');
419 /** True if module has no 'view' page (like label) */
420 define('FEATURE_NO_VIEW_LINK', 'viewlink');
421 /** True (which is default) if the module wants support for setting the ID number for grade calculation purposes. */
422 define('FEATURE_IDNUMBER', 'idnumber');
423 /** True if module supports groups */
424 define('FEATURE_GROUPS', 'groups');
425 /** True if module supports groupings */
426 define('FEATURE_GROUPINGS', 'groupings');
428 * True if module supports groupmembersonly (which no longer exists)
429 * @deprecated Since Moodle 2.8
431 define('FEATURE_GROUPMEMBERSONLY', 'groupmembersonly');
433 /** Type of module */
434 define('FEATURE_MOD_ARCHETYPE', 'mod_archetype');
435 /** True if module supports intro editor */
436 define('FEATURE_MOD_INTRO', 'mod_intro');
437 /** True if module has default completion */
438 define('FEATURE_MODEDIT_DEFAULT_COMPLETION', 'modedit_default_completion');
440 define('FEATURE_COMMENT', 'comment');
442 define('FEATURE_RATE', 'rate');
443 /** True if module supports backup/restore of moodle2 format */
444 define('FEATURE_BACKUP_MOODLE2', 'backup_moodle2');
446 /** True if module can show description on course main page */
447 define('FEATURE_SHOW_DESCRIPTION', 'showdescription');
449 /** True if module uses the question bank */
450 define('FEATURE_USES_QUESTIONS', 'usesquestions');
453 * Maximum filename char size
455 define('MAX_FILENAME_SIZE', 100);
457 /** Unspecified module archetype */
458 define('MOD_ARCHETYPE_OTHER', 0);
459 /** Resource-like type module */
460 define('MOD_ARCHETYPE_RESOURCE', 1);
461 /** Assignment module archetype */
462 define('MOD_ARCHETYPE_ASSIGNMENT', 2);
463 /** System (not user-addable) module archetype */
464 define('MOD_ARCHETYPE_SYSTEM', 3);
466 /** Type of module */
467 define('FEATURE_MOD_PURPOSE', 'mod_purpose');
468 /** Module purpose administration */
469 define('MOD_PURPOSE_ADMINISTRATION', 'administration');
470 /** Module purpose assessment */
471 define('MOD_PURPOSE_ASSESSMENT', 'assessment');
472 /** Module purpose communication */
473 define('MOD_PURPOSE_COLLABORATION', 'collaboration');
474 /** Module purpose communication */
475 define('MOD_PURPOSE_COMMUNICATION', 'communication');
476 /** Module purpose content */
477 define('MOD_PURPOSE_CONTENT', 'content');
478 /** Module purpose interface */
479 define('MOD_PURPOSE_INTERFACE', 'interface');
480 /** Module purpose other */
481 define('MOD_PURPOSE_OTHER', 'other');
484 * Security token used for allowing access
485 * from external application such as web services.
486 * Scripts do not use any session, performance is relatively
487 * low because we need to load access info in each request.
488 * Scripts are executed in parallel.
490 define('EXTERNAL_TOKEN_PERMANENT', 0);
493 * Security token used for allowing access
494 * of embedded applications, the code is executed in the
495 * active user session. Token is invalidated after user logs out.
496 * Scripts are executed serially - normal session locking is used.
498 define('EXTERNAL_TOKEN_EMBEDDED', 1);
501 * The home page should be the site home
503 define('HOMEPAGE_SITE', 0);
505 * The home page should be the users my page
507 define('HOMEPAGE_MY', 1);
509 * The home page can be chosen by the user
511 define('HOMEPAGE_USER', 2);
513 * The home page should be the users my courses page
515 define('HOMEPAGE_MYCOURSES', 3);
518 * URL of the Moodle sites registration portal.
520 defined('HUB_MOODLEORGHUBURL') || define('HUB_MOODLEORGHUBURL', 'https://stats.moodle.org');
523 * URL of the statistic server public key.
525 defined('HUB_STATSPUBLICKEY') || define('HUB_STATSPUBLICKEY', 'https://moodle.org/static/statspubkey.pem');
528 * Moodle mobile app service name
530 define('MOODLE_OFFICIAL_MOBILE_SERVICE', 'moodle_mobile_app');
533 * Indicates the user has the capabilities required to ignore activity and course file size restrictions
535 define('USER_CAN_IGNORE_FILE_SIZE_LIMITS', -1);
538 * Course display settings: display all sections on one page.
540 define('COURSE_DISPLAY_SINGLEPAGE', 0);
542 * Course display settings: split pages into a page per section.
544 define('COURSE_DISPLAY_MULTIPAGE', 1);
547 * Authentication constant: String used in password field when password is not stored.
549 define('AUTH_PASSWORD_NOT_CACHED', 'not cached');
552 * Email from header to never include via information.
554 define('EMAIL_VIA_NEVER', 0);
557 * Email from header to always include via information.
559 define('EMAIL_VIA_ALWAYS', 1);
562 * Email from header to only include via information if the address is no-reply.
564 define('EMAIL_VIA_NO_REPLY_ONLY', 2);
567 * Contact site support form/link disabled.
569 define('CONTACT_SUPPORT_DISABLED', 0);
572 * Contact site support form/link only available to authenticated users.
574 define('CONTACT_SUPPORT_AUTHENTICATED', 1);
577 * Contact site support form/link available to anyone visiting the site.
579 define('CONTACT_SUPPORT_ANYONE', 2);
581 // PARAMETER HANDLING.
584 * Returns a particular value for the named variable, taken from
585 * POST or GET. If the parameter doesn't exist then an error is
586 * thrown because we require this variable.
588 * This function should be used to initialise all required values
589 * in a script that are based on parameters. Usually it will be
590 * used like this:
591 * $id = required_param('id', PARAM_INT);
593 * Please note the $type parameter is now required and the value can not be array.
595 * @param string $parname the name of the page parameter we want
596 * @param string $type expected type of parameter
597 * @return mixed
598 * @throws coding_exception
600 function required_param($parname, $type) {
601 if (func_num_args() != 2 or empty($parname) or empty($type)) {
602 throw new coding_exception('required_param() requires $parname and $type to be specified (parameter: '.$parname.')');
604 // POST has precedence.
605 if (isset($_POST[$parname])) {
606 $param = $_POST[$parname];
607 } else if (isset($_GET[$parname])) {
608 $param = $_GET[$parname];
609 } else {
610 throw new \moodle_exception('missingparam', '', '', $parname);
613 if (is_array($param)) {
614 debugging('Invalid array parameter detected in required_param(): '.$parname);
615 // TODO: switch to fatal error in Moodle 2.3.
616 return required_param_array($parname, $type);
619 return clean_param($param, $type);
623 * Returns a particular array value for the named variable, taken from
624 * POST or GET. If the parameter doesn't exist then an error is
625 * thrown because we require this variable.
627 * This function should be used to initialise all required values
628 * in a script that are based on parameters. Usually it will be
629 * used like this:
630 * $ids = required_param_array('ids', PARAM_INT);
632 * Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
634 * @param string $parname the name of the page parameter we want
635 * @param string $type expected type of parameter
636 * @return array
637 * @throws coding_exception
639 function required_param_array($parname, $type) {
640 if (func_num_args() != 2 or empty($parname) or empty($type)) {
641 throw new coding_exception('required_param_array() requires $parname and $type to be specified (parameter: '.$parname.')');
643 // POST has precedence.
644 if (isset($_POST[$parname])) {
645 $param = $_POST[$parname];
646 } else if (isset($_GET[$parname])) {
647 $param = $_GET[$parname];
648 } else {
649 throw new \moodle_exception('missingparam', '', '', $parname);
651 if (!is_array($param)) {
652 throw new \moodle_exception('missingparam', '', '', $parname);
655 $result = array();
656 foreach ($param as $key => $value) {
657 if (!preg_match('/^[a-z0-9_-]+$/i', $key)) {
658 debugging('Invalid key name in required_param_array() detected: '.$key.', parameter: '.$parname);
659 continue;
661 $result[$key] = clean_param($value, $type);
664 return $result;
668 * Returns a particular value for the named variable, taken from
669 * POST or GET, otherwise returning a given default.
671 * This function should be used to initialise all optional values
672 * in a script that are based on parameters. Usually it will be
673 * used like this:
674 * $name = optional_param('name', 'Fred', PARAM_TEXT);
676 * Please note the $type parameter is now required and the value can not be array.
678 * @param string $parname the name of the page parameter we want
679 * @param mixed $default the default value to return if nothing is found
680 * @param string $type expected type of parameter
681 * @return mixed
682 * @throws coding_exception
684 function optional_param($parname, $default, $type) {
685 if (func_num_args() != 3 or empty($parname) or empty($type)) {
686 throw new coding_exception('optional_param requires $parname, $default + $type to be specified (parameter: '.$parname.')');
689 // POST has precedence.
690 if (isset($_POST[$parname])) {
691 $param = $_POST[$parname];
692 } else if (isset($_GET[$parname])) {
693 $param = $_GET[$parname];
694 } else {
695 return $default;
698 if (is_array($param)) {
699 debugging('Invalid array parameter detected in required_param(): '.$parname);
700 // TODO: switch to $default in Moodle 2.3.
701 return optional_param_array($parname, $default, $type);
704 return clean_param($param, $type);
708 * Returns a particular array value for the named variable, taken from
709 * POST or GET, otherwise returning a given default.
711 * This function should be used to initialise all optional values
712 * in a script that are based on parameters. Usually it will be
713 * used like this:
714 * $ids = optional_param('id', array(), PARAM_INT);
716 * Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
718 * @param string $parname the name of the page parameter we want
719 * @param mixed $default the default value to return if nothing is found
720 * @param string $type expected type of parameter
721 * @return array
722 * @throws coding_exception
724 function optional_param_array($parname, $default, $type) {
725 if (func_num_args() != 3 or empty($parname) or empty($type)) {
726 throw new coding_exception('optional_param_array requires $parname, $default + $type to be specified (parameter: '.$parname.')');
729 // POST has precedence.
730 if (isset($_POST[$parname])) {
731 $param = $_POST[$parname];
732 } else if (isset($_GET[$parname])) {
733 $param = $_GET[$parname];
734 } else {
735 return $default;
737 if (!is_array($param)) {
738 debugging('optional_param_array() expects array parameters only: '.$parname);
739 return $default;
742 $result = array();
743 foreach ($param as $key => $value) {
744 if (!preg_match('/^[a-z0-9_-]+$/i', $key)) {
745 debugging('Invalid key name in optional_param_array() detected: '.$key.', parameter: '.$parname);
746 continue;
748 $result[$key] = clean_param($value, $type);
751 return $result;
755 * Strict validation of parameter values, the values are only converted
756 * to requested PHP type. Internally it is using clean_param, the values
757 * before and after cleaning must be equal - otherwise
758 * an invalid_parameter_exception is thrown.
759 * Objects and classes are not accepted.
761 * @param mixed $param
762 * @param string $type PARAM_ constant
763 * @param bool $allownull are nulls valid value?
764 * @param string $debuginfo optional debug information
765 * @return mixed the $param value converted to PHP type
766 * @throws invalid_parameter_exception if $param is not of given type
768 function validate_param($param, $type, $allownull=NULL_NOT_ALLOWED, $debuginfo='') {
769 if (is_null($param)) {
770 if ($allownull == NULL_ALLOWED) {
771 return null;
772 } else {
773 throw new invalid_parameter_exception($debuginfo);
776 if (is_array($param) or is_object($param)) {
777 throw new invalid_parameter_exception($debuginfo);
780 $cleaned = clean_param($param, $type);
782 if ($type == PARAM_FLOAT) {
783 // Do not detect precision loss here.
784 if (is_float($param) or is_int($param)) {
785 // These always fit.
786 } else if (!is_numeric($param) or !preg_match('/^[\+-]?[0-9]*\.?[0-9]*(e[-+]?[0-9]+)?$/i', (string)$param)) {
787 throw new invalid_parameter_exception($debuginfo);
789 } else if ((string)$param !== (string)$cleaned) {
790 // Conversion to string is usually lossless.
791 throw new invalid_parameter_exception($debuginfo);
794 return $cleaned;
798 * Makes sure array contains only the allowed types, this function does not validate array key names!
800 * <code>
801 * $options = clean_param($options, PARAM_INT);
802 * </code>
804 * @param array|null $param the variable array we are cleaning
805 * @param string $type expected format of param after cleaning.
806 * @param bool $recursive clean recursive arrays
807 * @return array
808 * @throws coding_exception
810 function clean_param_array(?array $param, $type, $recursive = false) {
811 // Convert null to empty array.
812 $param = (array)$param;
813 foreach ($param as $key => $value) {
814 if (is_array($value)) {
815 if ($recursive) {
816 $param[$key] = clean_param_array($value, $type, true);
817 } else {
818 throw new coding_exception('clean_param_array can not process multidimensional arrays when $recursive is false.');
820 } else {
821 $param[$key] = clean_param($value, $type);
824 return $param;
828 * Used by {@link optional_param()} and {@link required_param()} to
829 * clean the variables and/or cast to specific types, based on
830 * an options field.
831 * <code>
832 * $course->format = clean_param($course->format, PARAM_ALPHA);
833 * $selectedgradeitem = clean_param($selectedgradeitem, PARAM_INT);
834 * </code>
836 * @param mixed $param the variable we are cleaning
837 * @param string $type expected format of param after cleaning.
838 * @return mixed
839 * @throws coding_exception
841 function clean_param($param, $type) {
842 global $CFG;
844 if (is_array($param)) {
845 throw new coding_exception('clean_param() can not process arrays, please use clean_param_array() instead.');
846 } else if (is_object($param)) {
847 if (method_exists($param, '__toString')) {
848 $param = $param->__toString();
849 } else {
850 throw new coding_exception('clean_param() can not process objects, please use clean_param_array() instead.');
854 switch ($type) {
855 case PARAM_RAW:
856 // No cleaning at all.
857 $param = fix_utf8($param);
858 return $param;
860 case PARAM_RAW_TRIMMED:
861 // No cleaning, but strip leading and trailing whitespace.
862 $param = (string)fix_utf8($param);
863 return trim($param);
865 case PARAM_CLEAN:
866 // General HTML cleaning, try to use more specific type if possible this is deprecated!
867 // Please use more specific type instead.
868 if (is_numeric($param)) {
869 return $param;
871 $param = fix_utf8($param);
872 // Sweep for scripts, etc.
873 return clean_text($param);
875 case PARAM_CLEANHTML:
876 // Clean html fragment.
877 $param = (string)fix_utf8($param);
878 // Sweep for scripts, etc.
879 $param = clean_text($param, FORMAT_HTML);
880 return trim($param);
882 case PARAM_INT:
883 // Convert to integer.
884 return (int)$param;
886 case PARAM_FLOAT:
887 // Convert to float.
888 return (float)$param;
890 case PARAM_LOCALISEDFLOAT:
891 // Convert to float.
892 return unformat_float($param, true);
894 case PARAM_ALPHA:
895 // Remove everything not `a-z`.
896 return preg_replace('/[^a-zA-Z]/i', '', (string)$param);
898 case PARAM_ALPHAEXT:
899 // Remove everything not `a-zA-Z_-` (originally allowed "/" too).
900 return preg_replace('/[^a-zA-Z_-]/i', '', (string)$param);
902 case PARAM_ALPHANUM:
903 // Remove everything not `a-zA-Z0-9`.
904 return preg_replace('/[^A-Za-z0-9]/i', '', (string)$param);
906 case PARAM_ALPHANUMEXT:
907 // Remove everything not `a-zA-Z0-9_-`.
908 return preg_replace('/[^A-Za-z0-9_-]/i', '', (string)$param);
910 case PARAM_SEQUENCE:
911 // Remove everything not `0-9,`.
912 return preg_replace('/[^0-9,]/i', '', (string)$param);
914 case PARAM_BOOL:
915 // Convert to 1 or 0.
916 $tempstr = strtolower((string)$param);
917 if ($tempstr === 'on' or $tempstr === 'yes' or $tempstr === 'true') {
918 $param = 1;
919 } else if ($tempstr === 'off' or $tempstr === 'no' or $tempstr === 'false') {
920 $param = 0;
921 } else {
922 $param = empty($param) ? 0 : 1;
924 return $param;
926 case PARAM_NOTAGS:
927 // Strip all tags.
928 $param = fix_utf8($param);
929 return strip_tags((string)$param);
931 case PARAM_TEXT:
932 // Leave only tags needed for multilang.
933 $param = fix_utf8($param);
934 // If the multilang syntax is not correct we strip all tags because it would break xhtml strict which is required
935 // for accessibility standards please note this cleaning does not strip unbalanced '>' for BC compatibility reasons.
936 do {
937 if (strpos((string)$param, '</lang>') !== false) {
938 // Old and future mutilang syntax.
939 $param = strip_tags($param, '<lang>');
940 if (!preg_match_all('/<.*>/suU', $param, $matches)) {
941 break;
943 $open = false;
944 foreach ($matches[0] as $match) {
945 if ($match === '</lang>') {
946 if ($open) {
947 $open = false;
948 continue;
949 } else {
950 break 2;
953 if (!preg_match('/^<lang lang="[a-zA-Z0-9_-]+"\s*>$/u', $match)) {
954 break 2;
955 } else {
956 $open = true;
959 if ($open) {
960 break;
962 return $param;
964 } else if (strpos((string)$param, '</span>') !== false) {
965 // Current problematic multilang syntax.
966 $param = strip_tags($param, '<span>');
967 if (!preg_match_all('/<.*>/suU', $param, $matches)) {
968 break;
970 $open = false;
971 foreach ($matches[0] as $match) {
972 if ($match === '</span>') {
973 if ($open) {
974 $open = false;
975 continue;
976 } else {
977 break 2;
980 if (!preg_match('/^<span(\s+lang="[a-zA-Z0-9_-]+"|\s+class="multilang"){2}\s*>$/u', $match)) {
981 break 2;
982 } else {
983 $open = true;
986 if ($open) {
987 break;
989 return $param;
991 } while (false);
992 // Easy, just strip all tags, if we ever want to fix orphaned '&' we have to do that in format_string().
993 return strip_tags((string)$param);
995 case PARAM_COMPONENT:
996 // We do not want any guessing here, either the name is correct or not
997 // please note only normalised component names are accepted.
998 $param = (string)$param;
999 if (!preg_match('/^[a-z][a-z0-9]*(_[a-z][a-z0-9_]*)?[a-z0-9]+$/', $param)) {
1000 return '';
1002 if (strpos($param, '__') !== false) {
1003 return '';
1005 if (strpos($param, 'mod_') === 0) {
1006 // Module names must not contain underscores because we need to differentiate them from invalid plugin types.
1007 if (substr_count($param, '_') != 1) {
1008 return '';
1011 return $param;
1013 case PARAM_PLUGIN:
1014 case PARAM_AREA:
1015 // We do not want any guessing here, either the name is correct or not.
1016 if (!is_valid_plugin_name($param)) {
1017 return '';
1019 return $param;
1021 case PARAM_SAFEDIR:
1022 // Remove everything not a-zA-Z0-9_- .
1023 return preg_replace('/[^a-zA-Z0-9_-]/i', '', (string)$param);
1025 case PARAM_SAFEPATH:
1026 // Remove everything not a-zA-Z0-9/_- .
1027 return preg_replace('/[^a-zA-Z0-9\/_-]/i', '', (string)$param);
1029 case PARAM_FILE:
1030 // Strip all suspicious characters from filename.
1031 $param = (string)fix_utf8($param);
1032 $param = preg_replace('~[[:cntrl:]]|[&<>"`\|\':\\\\/]~u', '', $param);
1033 if ($param === '.' || $param === '..') {
1034 $param = '';
1036 return $param;
1038 case PARAM_PATH:
1039 // Strip all suspicious characters from file path.
1040 $param = (string)fix_utf8($param);
1041 $param = str_replace('\\', '/', $param);
1043 // Explode the path and clean each element using the PARAM_FILE rules.
1044 $breadcrumb = explode('/', $param);
1045 foreach ($breadcrumb as $key => $crumb) {
1046 if ($crumb === '.' && $key === 0) {
1047 // Special condition to allow for relative current path such as ./currentdirfile.txt.
1048 } else {
1049 $crumb = clean_param($crumb, PARAM_FILE);
1051 $breadcrumb[$key] = $crumb;
1053 $param = implode('/', $breadcrumb);
1055 // Remove multiple current path (./././) and multiple slashes (///).
1056 $param = preg_replace('~//+~', '/', $param);
1057 $param = preg_replace('~/(\./)+~', '/', $param);
1058 return $param;
1060 case PARAM_HOST:
1061 // Allow FQDN or IPv4 dotted quad.
1062 $param = preg_replace('/[^\.\d\w-]/', '', (string)$param );
1063 // Match ipv4 dotted quad.
1064 if (preg_match('/(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/', $param, $match)) {
1065 // Confirm values are ok.
1066 if ( $match[0] > 255
1067 || $match[1] > 255
1068 || $match[3] > 255
1069 || $match[4] > 255 ) {
1070 // Hmmm, what kind of dotted quad is this?
1071 $param = '';
1073 } else if ( preg_match('/^[\w\d\.-]+$/', $param) // Dots, hyphens, numbers.
1074 && !preg_match('/^[\.-]/', $param) // No leading dots/hyphens.
1075 && !preg_match('/[\.-]$/', $param) // No trailing dots/hyphens.
1077 // All is ok - $param is respected.
1078 } else {
1079 // All is not ok...
1080 $param='';
1082 return $param;
1084 case PARAM_URL:
1085 // Allow safe urls.
1086 $param = (string)fix_utf8($param);
1087 include_once($CFG->dirroot . '/lib/validateurlsyntax.php');
1088 if (!empty($param) && validateUrlSyntax($param, 's?H?S?F?E-u-P-a?I?p?f?q?r?')) {
1089 // All is ok, param is respected.
1090 } else {
1091 // Not really ok.
1092 $param ='';
1094 return $param;
1096 case PARAM_LOCALURL:
1097 // Allow http absolute, root relative and relative URLs within wwwroot.
1098 $param = clean_param($param, PARAM_URL);
1099 if (!empty($param)) {
1101 if ($param === $CFG->wwwroot) {
1102 // Exact match;
1103 } else if (preg_match(':^/:', $param)) {
1104 // Root-relative, ok!
1105 } else if (preg_match('/^' . preg_quote($CFG->wwwroot . '/', '/') . '/i', $param)) {
1106 // Absolute, and matches our wwwroot.
1107 } else {
1109 // Relative - let's make sure there are no tricks.
1110 if (validateUrlSyntax('/' . $param, 's-u-P-a-p-f+q?r?') && !preg_match('/javascript:/i', $param)) {
1111 // Looks ok.
1112 } else {
1113 $param = '';
1117 return $param;
1119 case PARAM_PEM:
1120 $param = trim((string)$param);
1121 // PEM formatted strings may contain letters/numbers and the symbols:
1122 // forward slash: /
1123 // plus sign: +
1124 // equal sign: =
1125 // , surrounded by BEGIN and END CERTIFICATE prefix and suffixes.
1126 if (preg_match('/^-----BEGIN CERTIFICATE-----([\s\w\/\+=]+)-----END CERTIFICATE-----$/', trim($param), $matches)) {
1127 list($wholething, $body) = $matches;
1128 unset($wholething, $matches);
1129 $b64 = clean_param($body, PARAM_BASE64);
1130 if (!empty($b64)) {
1131 return "-----BEGIN CERTIFICATE-----\n$b64\n-----END CERTIFICATE-----\n";
1132 } else {
1133 return '';
1136 return '';
1138 case PARAM_BASE64:
1139 if (!empty($param)) {
1140 // PEM formatted strings may contain letters/numbers and the symbols
1141 // forward slash: /
1142 // plus sign: +
1143 // equal sign: =.
1144 if (0 >= preg_match('/^([\s\w\/\+=]+)$/', trim($param))) {
1145 return '';
1147 $lines = preg_split('/[\s]+/', $param, -1, PREG_SPLIT_NO_EMPTY);
1148 // Each line of base64 encoded data must be 64 characters in length, except for the last line which may be less
1149 // than (or equal to) 64 characters long.
1150 for ($i=0, $j=count($lines); $i < $j; $i++) {
1151 if ($i + 1 == $j) {
1152 if (64 < strlen($lines[$i])) {
1153 return '';
1155 continue;
1158 if (64 != strlen($lines[$i])) {
1159 return '';
1162 return implode("\n", $lines);
1163 } else {
1164 return '';
1167 case PARAM_TAG:
1168 $param = (string)fix_utf8($param);
1169 // Please note it is not safe to use the tag name directly anywhere,
1170 // it must be processed with s(), urlencode() before embedding anywhere.
1171 // Remove some nasties.
1172 $param = preg_replace('~[[:cntrl:]]|[<>`]~u', '', $param);
1173 // Convert many whitespace chars into one.
1174 $param = preg_replace('/\s+/u', ' ', $param);
1175 $param = core_text::substr(trim($param), 0, TAG_MAX_LENGTH);
1176 return $param;
1178 case PARAM_TAGLIST:
1179 $param = (string)fix_utf8($param);
1180 $tags = explode(',', $param);
1181 $result = array();
1182 foreach ($tags as $tag) {
1183 $res = clean_param($tag, PARAM_TAG);
1184 if ($res !== '') {
1185 $result[] = $res;
1188 if ($result) {
1189 return implode(',', $result);
1190 } else {
1191 return '';
1194 case PARAM_CAPABILITY:
1195 if (get_capability_info($param)) {
1196 return $param;
1197 } else {
1198 return '';
1201 case PARAM_PERMISSION:
1202 $param = (int)$param;
1203 if (in_array($param, array(CAP_INHERIT, CAP_ALLOW, CAP_PREVENT, CAP_PROHIBIT))) {
1204 return $param;
1205 } else {
1206 return CAP_INHERIT;
1209 case PARAM_AUTH:
1210 $param = clean_param($param, PARAM_PLUGIN);
1211 if (empty($param)) {
1212 return '';
1213 } else if (exists_auth_plugin($param)) {
1214 return $param;
1215 } else {
1216 return '';
1219 case PARAM_LANG:
1220 $param = clean_param($param, PARAM_SAFEDIR);
1221 if (get_string_manager()->translation_exists($param)) {
1222 return $param;
1223 } else {
1224 // Specified language is not installed or param malformed.
1225 return '';
1228 case PARAM_THEME:
1229 $param = clean_param($param, PARAM_PLUGIN);
1230 if (empty($param)) {
1231 return '';
1232 } else if (file_exists("$CFG->dirroot/theme/$param/config.php")) {
1233 return $param;
1234 } else if (!empty($CFG->themedir) and file_exists("$CFG->themedir/$param/config.php")) {
1235 return $param;
1236 } else {
1237 // Specified theme is not installed.
1238 return '';
1241 case PARAM_USERNAME:
1242 $param = (string)fix_utf8($param);
1243 $param = trim($param);
1244 // Convert uppercase to lowercase MDL-16919.
1245 $param = core_text::strtolower($param);
1246 if (empty($CFG->extendedusernamechars)) {
1247 $param = str_replace(" " , "", $param);
1248 // Regular expression, eliminate all chars EXCEPT:
1249 // alphanum, dash (-), underscore (_), at sign (@) and period (.) characters.
1250 $param = preg_replace('/[^-\.@_a-z0-9]/', '', $param);
1252 return $param;
1254 case PARAM_EMAIL:
1255 $param = fix_utf8($param);
1256 if (validate_email($param ?? '')) {
1257 return $param;
1258 } else {
1259 return '';
1262 case PARAM_STRINGID:
1263 if (preg_match('|^[a-zA-Z][a-zA-Z0-9\.:/_-]*$|', (string)$param)) {
1264 return $param;
1265 } else {
1266 return '';
1269 case PARAM_TIMEZONE:
1270 // Can be int, float(with .5 or .0) or string seperated by '/' and can have '-_'.
1271 $param = (string)fix_utf8($param);
1272 $timezonepattern = '/^(([+-]?(0?[0-9](\.[5|0])?|1[0-3](\.0)?|1[0-2]\.5))|(99)|[[:alnum:]]+(\/?[[:alpha:]_-])+)$/';
1273 if (preg_match($timezonepattern, $param)) {
1274 return $param;
1275 } else {
1276 return '';
1279 default:
1280 // Doh! throw error, switched parameters in optional_param or another serious problem.
1281 throw new \moodle_exception("unknownparamtype", '', '', $type);
1286 * Whether the PARAM_* type is compatible in RTL.
1288 * Being compatible with RTL means that the data they contain can flow
1289 * from right-to-left or left-to-right without compromising the user experience.
1291 * Take URLs for example, they are not RTL compatible as they should always
1292 * flow from the left to the right. This also applies to numbers, email addresses,
1293 * configuration snippets, base64 strings, etc...
1295 * This function tries to best guess which parameters can contain localised strings.
1297 * @param string $paramtype Constant PARAM_*.
1298 * @return bool
1300 function is_rtl_compatible($paramtype) {
1301 return $paramtype == PARAM_TEXT || $paramtype == PARAM_NOTAGS;
1305 * Makes sure the data is using valid utf8, invalid characters are discarded.
1307 * Note: this function is not intended for full objects with methods and private properties.
1309 * @param mixed $value
1310 * @return mixed with proper utf-8 encoding
1312 function fix_utf8($value) {
1313 if (is_null($value) or $value === '') {
1314 return $value;
1316 } else if (is_string($value)) {
1317 if ((string)(int)$value === $value) {
1318 // Shortcut.
1319 return $value;
1322 // Remove null bytes or invalid Unicode sequences from value.
1323 $value = str_replace(["\0", "\xef\xbf\xbe", "\xef\xbf\xbf"], '', $value);
1325 // Note: this duplicates min_fix_utf8() intentionally.
1326 static $buggyiconv = null;
1327 if ($buggyiconv === null) {
1328 $buggyiconv = (!function_exists('iconv') or @iconv('UTF-8', 'UTF-8//IGNORE', '100'.chr(130).'€') !== '100€');
1331 if ($buggyiconv) {
1332 if (function_exists('mb_convert_encoding')) {
1333 $subst = mb_substitute_character();
1334 mb_substitute_character('none');
1335 $result = mb_convert_encoding($value, 'utf-8', 'utf-8');
1336 mb_substitute_character($subst);
1338 } else {
1339 // Warn admins on admin/index.php page.
1340 $result = $value;
1343 } else {
1344 $result = @iconv('UTF-8', 'UTF-8//IGNORE', $value);
1347 return $result;
1349 } else if (is_array($value)) {
1350 foreach ($value as $k => $v) {
1351 $value[$k] = fix_utf8($v);
1353 return $value;
1355 } else if (is_object($value)) {
1356 // Do not modify original.
1357 $value = clone($value);
1358 foreach ($value as $k => $v) {
1359 $value->$k = fix_utf8($v);
1361 return $value;
1363 } else {
1364 // This is some other type, no utf-8 here.
1365 return $value;
1370 * Return true if given value is integer or string with integer value
1372 * @param mixed $value String or Int
1373 * @return bool true if number, false if not
1375 function is_number($value) {
1376 if (is_int($value)) {
1377 return true;
1378 } else if (is_string($value)) {
1379 return ((string)(int)$value) === $value;
1380 } else {
1381 return false;
1386 * Returns host part from url.
1388 * @param string $url full url
1389 * @return string host, null if not found
1391 function get_host_from_url($url) {
1392 preg_match('|^[a-z]+://([a-zA-Z0-9-.]+)|i', $url, $matches);
1393 if ($matches) {
1394 return $matches[1];
1396 return null;
1400 * Tests whether anything was returned by text editor
1402 * This function is useful for testing whether something you got back from
1403 * the HTML editor actually contains anything. Sometimes the HTML editor
1404 * appear to be empty, but actually you get back a <br> tag or something.
1406 * @param string $string a string containing HTML.
1407 * @return boolean does the string contain any actual content - that is text,
1408 * images, objects, etc.
1410 function html_is_blank($string) {
1411 return trim(strip_tags((string)$string, '<img><object><applet><input><select><textarea><hr>')) == '';
1415 * Set a key in global configuration
1417 * Set a key/value pair in both this session's {@link $CFG} global variable
1418 * and in the 'config' database table for future sessions.
1420 * Can also be used to update keys for plugin-scoped configs in config_plugin table.
1421 * In that case it doesn't affect $CFG.
1423 * A NULL value will delete the entry.
1425 * NOTE: this function is called from lib/db/upgrade.php
1427 * @param string $name the key to set
1428 * @param string $value the value to set (without magic quotes)
1429 * @param string $plugin (optional) the plugin scope, default null
1430 * @return bool true or exception
1432 function set_config($name, $value, $plugin = null) {
1433 global $CFG, $DB;
1435 // Redirect to appropriate handler when value is null.
1436 if ($value === null) {
1437 return unset_config($name, $plugin);
1440 // Set variables determining conditions and where to store the new config.
1441 // Plugin config goes to {config_plugins}, core config goes to {config}.
1442 $iscore = empty($plugin);
1443 if ($iscore) {
1444 // If it's for core config.
1445 $table = 'config';
1446 $conditions = ['name' => $name];
1447 $invalidatecachekey = 'core';
1448 } else {
1449 // If it's a plugin.
1450 $table = 'config_plugins';
1451 $conditions = ['name' => $name, 'plugin' => $plugin];
1452 $invalidatecachekey = $plugin;
1455 // DB handling - checks for existing config, updating or inserting only if necessary.
1456 $invalidatecache = true;
1457 $inserted = false;
1458 $record = $DB->get_record($table, $conditions, 'id, value');
1459 if ($record === false) {
1460 // Inserts a new config record.
1461 $config = new stdClass();
1462 $config->name = $name;
1463 $config->value = $value;
1464 if (!$iscore) {
1465 $config->plugin = $plugin;
1467 $inserted = $DB->insert_record($table, $config, false);
1468 } else if ($invalidatecache = ($record->value !== $value)) {
1469 // Record exists - Check and only set new value if it has changed.
1470 $DB->set_field($table, 'value', $value, ['id' => $record->id]);
1473 if ($iscore && !isset($CFG->config_php_settings[$name])) {
1474 // So it's defined for this invocation at least.
1475 // Settings from db are always strings.
1476 $CFG->$name = (string) $value;
1479 // When setting config during a Behat test (in the CLI script, not in the web browser
1480 // requests), remember which ones are set so that we can clear them later.
1481 if ($iscore && $inserted && defined('BEHAT_TEST')) {
1482 $CFG->behat_cli_added_config[$name] = true;
1485 // Update siteidentifier cache, if required.
1486 if ($iscore && $name === 'siteidentifier') {
1487 cache_helper::update_site_identifier($value);
1490 // Invalidate cache, if required.
1491 if ($invalidatecache) {
1492 cache_helper::invalidate_by_definition('core', 'config', [], $invalidatecachekey);
1495 return true;
1499 * Get configuration values from the global config table
1500 * or the config_plugins table.
1502 * If called with one parameter, it will load all the config
1503 * variables for one plugin, and return them as an object.
1505 * If called with 2 parameters it will return a string single
1506 * value or false if the value is not found.
1508 * NOTE: this function is called from lib/db/upgrade.php
1510 * @param string $plugin full component name
1511 * @param string $name default null
1512 * @return mixed hash-like object or single value, return false no config found
1513 * @throws dml_exception
1515 function get_config($plugin, $name = null) {
1516 global $CFG, $DB;
1518 if ($plugin === 'moodle' || $plugin === 'core' || empty($plugin)) {
1519 $forced =& $CFG->config_php_settings;
1520 $iscore = true;
1521 $plugin = 'core';
1522 } else {
1523 if (array_key_exists($plugin, $CFG->forced_plugin_settings)) {
1524 $forced =& $CFG->forced_plugin_settings[$plugin];
1525 } else {
1526 $forced = array();
1528 $iscore = false;
1531 if (!isset($CFG->siteidentifier)) {
1532 try {
1533 // This may throw an exception during installation, which is how we detect the
1534 // need to install the database. For more details see {@see initialise_cfg()}.
1535 $CFG->siteidentifier = $DB->get_field('config', 'value', array('name' => 'siteidentifier'));
1536 } catch (dml_exception $ex) {
1537 // Set siteidentifier to false. We don't want to trip this continually.
1538 $siteidentifier = false;
1539 throw $ex;
1543 if (!empty($name)) {
1544 if (array_key_exists($name, $forced)) {
1545 return (string)$forced[$name];
1546 } else if ($name === 'siteidentifier' && $plugin == 'core') {
1547 return $CFG->siteidentifier;
1551 $cache = cache::make('core', 'config');
1552 $result = $cache->get($plugin);
1553 if ($result === false) {
1554 // The user is after a recordset.
1555 if (!$iscore) {
1556 $result = $DB->get_records_menu('config_plugins', array('plugin' => $plugin), '', 'name,value');
1557 } else {
1558 // This part is not really used any more, but anyway...
1559 $result = $DB->get_records_menu('config', array(), '', 'name,value');;
1561 $cache->set($plugin, $result);
1564 if (!empty($name)) {
1565 if (array_key_exists($name, $result)) {
1566 return $result[$name];
1568 return false;
1571 if ($plugin === 'core') {
1572 $result['siteidentifier'] = $CFG->siteidentifier;
1575 foreach ($forced as $key => $value) {
1576 if (is_null($value) or is_array($value) or is_object($value)) {
1577 // We do not want any extra mess here, just real settings that could be saved in db.
1578 unset($result[$key]);
1579 } else {
1580 // Convert to string as if it went through the DB.
1581 $result[$key] = (string)$value;
1585 return (object)$result;
1589 * Removes a key from global configuration.
1591 * NOTE: this function is called from lib/db/upgrade.php
1593 * @param string $name the key to set
1594 * @param string $plugin (optional) the plugin scope
1595 * @return boolean whether the operation succeeded.
1597 function unset_config($name, $plugin=null) {
1598 global $CFG, $DB;
1600 if (empty($plugin)) {
1601 unset($CFG->$name);
1602 $DB->delete_records('config', array('name' => $name));
1603 cache_helper::invalidate_by_definition('core', 'config', array(), 'core');
1604 } else {
1605 $DB->delete_records('config_plugins', array('name' => $name, 'plugin' => $plugin));
1606 cache_helper::invalidate_by_definition('core', 'config', array(), $plugin);
1609 return true;
1613 * Remove all the config variables for a given plugin.
1615 * NOTE: this function is called from lib/db/upgrade.php
1617 * @param string $plugin a plugin, for example 'quiz' or 'qtype_multichoice';
1618 * @return boolean whether the operation succeeded.
1620 function unset_all_config_for_plugin($plugin) {
1621 global $DB;
1622 // Delete from the obvious config_plugins first.
1623 $DB->delete_records('config_plugins', array('plugin' => $plugin));
1624 // Next delete any suspect settings from config.
1625 $like = $DB->sql_like('name', '?', true, true, false, '|');
1626 $params = array($DB->sql_like_escape($plugin.'_', '|') . '%');
1627 $DB->delete_records_select('config', $like, $params);
1628 // Finally clear both the plugin cache and the core cache (suspect settings now removed from core).
1629 cache_helper::invalidate_by_definition('core', 'config', array(), array('core', $plugin));
1631 return true;
1635 * Use this function to get a list of users from a config setting of type admin_setting_users_with_capability.
1637 * All users are verified if they still have the necessary capability.
1639 * @param string $value the value of the config setting.
1640 * @param string $capability the capability - must match the one passed to the admin_setting_users_with_capability constructor.
1641 * @param bool $includeadmins include administrators.
1642 * @return array of user objects.
1644 function get_users_from_config($value, $capability, $includeadmins = true) {
1645 if (empty($value) or $value === '$@NONE@$') {
1646 return array();
1649 // We have to make sure that users still have the necessary capability,
1650 // it should be faster to fetch them all first and then test if they are present
1651 // instead of validating them one-by-one.
1652 $users = get_users_by_capability(context_system::instance(), $capability);
1653 if ($includeadmins) {
1654 $admins = get_admins();
1655 foreach ($admins as $admin) {
1656 $users[$admin->id] = $admin;
1660 if ($value === '$@ALL@$') {
1661 return $users;
1664 $result = array(); // Result in correct order.
1665 $allowed = explode(',', $value);
1666 foreach ($allowed as $uid) {
1667 if (isset($users[$uid])) {
1668 $user = $users[$uid];
1669 $result[$user->id] = $user;
1673 return $result;
1678 * Invalidates browser caches and cached data in temp.
1680 * @return void
1682 function purge_all_caches() {
1683 purge_caches();
1687 * Selectively invalidate different types of cache.
1689 * Purges the cache areas specified. By default, this will purge all caches but can selectively purge specific
1690 * areas alone or in combination.
1692 * @param bool[] $options Specific parts of the cache to purge. Valid options are:
1693 * 'muc' Purge MUC caches?
1694 * 'theme' Purge theme cache?
1695 * 'lang' Purge language string cache?
1696 * 'js' Purge javascript cache?
1697 * 'filter' Purge text filter cache?
1698 * 'other' Purge all other caches?
1700 function purge_caches($options = []) {
1701 $defaults = array_fill_keys(['muc', 'theme', 'lang', 'js', 'template', 'filter', 'other'], false);
1702 if (empty(array_filter($options))) {
1703 $options = array_fill_keys(array_keys($defaults), true); // Set all options to true.
1704 } else {
1705 $options = array_merge($defaults, array_intersect_key($options, $defaults)); // Override defaults with specified options.
1707 if ($options['muc']) {
1708 cache_helper::purge_all();
1710 if ($options['theme']) {
1711 theme_reset_all_caches();
1713 if ($options['lang']) {
1714 get_string_manager()->reset_caches();
1716 if ($options['js']) {
1717 js_reset_all_caches();
1719 if ($options['template']) {
1720 template_reset_all_caches();
1722 if ($options['filter']) {
1723 reset_text_filters_cache();
1725 if ($options['other']) {
1726 purge_other_caches();
1731 * Purge all non-MUC caches not otherwise purged in purge_caches.
1733 * IMPORTANT - If you are adding anything here to do with the cache directory you should also have a look at
1734 * {@link phpunit_util::reset_dataroot()}
1736 function purge_other_caches() {
1737 global $DB, $CFG;
1738 if (class_exists('core_plugin_manager')) {
1739 core_plugin_manager::reset_caches();
1742 // Bump up cacherev field for all courses.
1743 try {
1744 increment_revision_number('course', 'cacherev', '');
1745 } catch (moodle_exception $e) {
1746 // Ignore exception since this function is also called before upgrade script when field course.cacherev does not exist yet.
1749 $DB->reset_caches();
1751 // Purge all other caches: rss, simplepie, etc.
1752 clearstatcache();
1753 remove_dir($CFG->cachedir.'', true);
1755 // Make sure cache dir is writable, throws exception if not.
1756 make_cache_directory('');
1758 // This is the only place where we purge local caches, we are only adding files there.
1759 // The $CFG->localcachedirpurged flag forces local directories to be purged on cluster nodes.
1760 remove_dir($CFG->localcachedir, true);
1761 set_config('localcachedirpurged', time());
1762 make_localcache_directory('', true);
1763 \core\task\manager::clear_static_caches();
1767 * Get volatile flags
1769 * @param string $type
1770 * @param int $changedsince default null
1771 * @return array records array
1773 function get_cache_flags($type, $changedsince = null) {
1774 global $DB;
1776 $params = array('type' => $type, 'expiry' => time());
1777 $sqlwhere = "flagtype = :type AND expiry >= :expiry";
1778 if ($changedsince !== null) {
1779 $params['changedsince'] = $changedsince;
1780 $sqlwhere .= " AND timemodified > :changedsince";
1782 $cf = array();
1783 if ($flags = $DB->get_records_select('cache_flags', $sqlwhere, $params, '', 'name,value')) {
1784 foreach ($flags as $flag) {
1785 $cf[$flag->name] = $flag->value;
1788 return $cf;
1792 * Get volatile flags
1794 * @param string $type
1795 * @param string $name
1796 * @param int $changedsince default null
1797 * @return string|false The cache flag value or false
1799 function get_cache_flag($type, $name, $changedsince=null) {
1800 global $DB;
1802 $params = array('type' => $type, 'name' => $name, 'expiry' => time());
1804 $sqlwhere = "flagtype = :type AND name = :name AND expiry >= :expiry";
1805 if ($changedsince !== null) {
1806 $params['changedsince'] = $changedsince;
1807 $sqlwhere .= " AND timemodified > :changedsince";
1810 return $DB->get_field_select('cache_flags', 'value', $sqlwhere, $params);
1814 * Set a volatile flag
1816 * @param string $type the "type" namespace for the key
1817 * @param string $name the key to set
1818 * @param string $value the value to set (without magic quotes) - null will remove the flag
1819 * @param int $expiry (optional) epoch indicating expiry - defaults to now()+ 24hs
1820 * @return bool Always returns true
1822 function set_cache_flag($type, $name, $value, $expiry = null) {
1823 global $DB;
1825 $timemodified = time();
1826 if ($expiry === null || $expiry < $timemodified) {
1827 $expiry = $timemodified + 24 * 60 * 60;
1828 } else {
1829 $expiry = (int)$expiry;
1832 if ($value === null) {
1833 unset_cache_flag($type, $name);
1834 return true;
1837 if ($f = $DB->get_record('cache_flags', array('name' => $name, 'flagtype' => $type), '*', IGNORE_MULTIPLE)) {
1838 // This is a potential problem in DEBUG_DEVELOPER.
1839 if ($f->value == $value and $f->expiry == $expiry and $f->timemodified == $timemodified) {
1840 return true; // No need to update.
1842 $f->value = $value;
1843 $f->expiry = $expiry;
1844 $f->timemodified = $timemodified;
1845 $DB->update_record('cache_flags', $f);
1846 } else {
1847 $f = new stdClass();
1848 $f->flagtype = $type;
1849 $f->name = $name;
1850 $f->value = $value;
1851 $f->expiry = $expiry;
1852 $f->timemodified = $timemodified;
1853 $DB->insert_record('cache_flags', $f);
1855 return true;
1859 * Removes a single volatile flag
1861 * @param string $type the "type" namespace for the key
1862 * @param string $name the key to set
1863 * @return bool
1865 function unset_cache_flag($type, $name) {
1866 global $DB;
1867 $DB->delete_records('cache_flags', array('name' => $name, 'flagtype' => $type));
1868 return true;
1872 * Garbage-collect volatile flags
1874 * @return bool Always returns true
1876 function gc_cache_flags() {
1877 global $DB;
1878 $DB->delete_records_select('cache_flags', 'expiry < ?', array(time()));
1879 return true;
1882 // USER PREFERENCE API.
1885 * Refresh user preference cache. This is used most often for $USER
1886 * object that is stored in session, but it also helps with performance in cron script.
1888 * Preferences for each user are loaded on first use on every page, then again after the timeout expires.
1890 * @package core
1891 * @category preference
1892 * @access public
1893 * @param stdClass $user User object. Preferences are preloaded into 'preference' property
1894 * @param int $cachelifetime Cache life time on the current page (in seconds)
1895 * @throws coding_exception
1896 * @return null
1898 function check_user_preferences_loaded(stdClass $user, $cachelifetime = 120) {
1899 global $DB;
1900 // Static cache, we need to check on each page load, not only every 2 minutes.
1901 static $loadedusers = array();
1903 if (!isset($user->id)) {
1904 throw new coding_exception('Invalid $user parameter in check_user_preferences_loaded() call, missing id field');
1907 if (empty($user->id) or isguestuser($user->id)) {
1908 // No permanent storage for not-logged-in users and guest.
1909 if (!isset($user->preference)) {
1910 $user->preference = array();
1912 return;
1915 $timenow = time();
1917 if (isset($loadedusers[$user->id]) and isset($user->preference) and isset($user->preference['_lastloaded'])) {
1918 // Already loaded at least once on this page. Are we up to date?
1919 if ($user->preference['_lastloaded'] + $cachelifetime > $timenow) {
1920 // No need to reload - we are on the same page and we loaded prefs just a moment ago.
1921 return;
1923 } else if (!get_cache_flag('userpreferenceschanged', $user->id, $user->preference['_lastloaded'])) {
1924 // No change since the lastcheck on this page.
1925 $user->preference['_lastloaded'] = $timenow;
1926 return;
1930 // OK, so we have to reload all preferences.
1931 $loadedusers[$user->id] = true;
1932 $user->preference = $DB->get_records_menu('user_preferences', array('userid' => $user->id), '', 'name,value'); // All values.
1933 $user->preference['_lastloaded'] = $timenow;
1937 * Called from set/unset_user_preferences, so that the prefs can be correctly reloaded in different sessions.
1939 * NOTE: internal function, do not call from other code.
1941 * @package core
1942 * @access private
1943 * @param integer $userid the user whose prefs were changed.
1945 function mark_user_preferences_changed($userid) {
1946 global $CFG;
1948 if (empty($userid) or isguestuser($userid)) {
1949 // No cache flags for guest and not-logged-in users.
1950 return;
1953 set_cache_flag('userpreferenceschanged', $userid, 1, time() + $CFG->sessiontimeout);
1957 * Sets a preference for the specified user.
1959 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1961 * When additional validation/permission check is needed it is better to use {@see useredit_update_user_preference()}
1963 * @package core
1964 * @category preference
1965 * @access public
1966 * @param string $name The key to set as preference for the specified user
1967 * @param string $value The value to set for the $name key in the specified user's
1968 * record, null means delete current value.
1969 * @param stdClass|int|null $user A moodle user object or id, null means current user
1970 * @throws coding_exception
1971 * @return bool Always true or exception
1973 function set_user_preference($name, $value, $user = null) {
1974 global $USER, $DB;
1976 if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
1977 throw new coding_exception('Invalid preference name in set_user_preference() call');
1980 if (is_null($value)) {
1981 // Null means delete current.
1982 return unset_user_preference($name, $user);
1983 } else if (is_object($value)) {
1984 throw new coding_exception('Invalid value in set_user_preference() call, objects are not allowed');
1985 } else if (is_array($value)) {
1986 throw new coding_exception('Invalid value in set_user_preference() call, arrays are not allowed');
1988 // Value column maximum length is 1333 characters.
1989 $value = (string)$value;
1990 if (core_text::strlen($value) > 1333) {
1991 throw new coding_exception('Invalid value in set_user_preference() call, value is is too long for the value column');
1994 if (is_null($user)) {
1995 $user = $USER;
1996 } else if (isset($user->id)) {
1997 // It is a valid object.
1998 } else if (is_numeric($user)) {
1999 $user = (object)array('id' => (int)$user);
2000 } else {
2001 throw new coding_exception('Invalid $user parameter in set_user_preference() call');
2004 check_user_preferences_loaded($user);
2006 if (empty($user->id) or isguestuser($user->id)) {
2007 // No permanent storage for not-logged-in users and guest.
2008 $user->preference[$name] = $value;
2009 return true;
2012 if ($preference = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => $name))) {
2013 if ($preference->value === $value and isset($user->preference[$name]) and $user->preference[$name] === $value) {
2014 // Preference already set to this value.
2015 return true;
2017 $DB->set_field('user_preferences', 'value', $value, array('id' => $preference->id));
2019 } else {
2020 $preference = new stdClass();
2021 $preference->userid = $user->id;
2022 $preference->name = $name;
2023 $preference->value = $value;
2024 $DB->insert_record('user_preferences', $preference);
2027 // Update value in cache.
2028 $user->preference[$name] = $value;
2029 // Update the $USER in case where we've not a direct reference to $USER.
2030 if ($user !== $USER && $user->id == $USER->id) {
2031 $USER->preference[$name] = $value;
2034 // Set reload flag for other sessions.
2035 mark_user_preferences_changed($user->id);
2037 return true;
2041 * Sets a whole array of preferences for the current user
2043 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
2045 * @package core
2046 * @category preference
2047 * @access public
2048 * @param array $prefarray An array of key/value pairs to be set
2049 * @param stdClass|int|null $user A moodle user object or id, null means current user
2050 * @return bool Always true or exception
2052 function set_user_preferences(array $prefarray, $user = null) {
2053 foreach ($prefarray as $name => $value) {
2054 set_user_preference($name, $value, $user);
2056 return true;
2060 * Unsets a preference completely by deleting it from the database
2062 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
2064 * @package core
2065 * @category preference
2066 * @access public
2067 * @param string $name The key to unset as preference for the specified user
2068 * @param stdClass|int|null $user A moodle user object or id, null means current user
2069 * @throws coding_exception
2070 * @return bool Always true or exception
2072 function unset_user_preference($name, $user = null) {
2073 global $USER, $DB;
2075 if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
2076 throw new coding_exception('Invalid preference name in unset_user_preference() call');
2079 if (is_null($user)) {
2080 $user = $USER;
2081 } else if (isset($user->id)) {
2082 // It is a valid object.
2083 } else if (is_numeric($user)) {
2084 $user = (object)array('id' => (int)$user);
2085 } else {
2086 throw new coding_exception('Invalid $user parameter in unset_user_preference() call');
2089 check_user_preferences_loaded($user);
2091 if (empty($user->id) or isguestuser($user->id)) {
2092 // No permanent storage for not-logged-in user and guest.
2093 unset($user->preference[$name]);
2094 return true;
2097 // Delete from DB.
2098 $DB->delete_records('user_preferences', array('userid' => $user->id, 'name' => $name));
2100 // Delete the preference from cache.
2101 unset($user->preference[$name]);
2102 // Update the $USER in case where we've not a direct reference to $USER.
2103 if ($user !== $USER && $user->id == $USER->id) {
2104 unset($USER->preference[$name]);
2107 // Set reload flag for other sessions.
2108 mark_user_preferences_changed($user->id);
2110 return true;
2114 * Used to fetch user preference(s)
2116 * If no arguments are supplied this function will return
2117 * all of the current user preferences as an array.
2119 * If a name is specified then this function
2120 * attempts to return that particular preference value. If
2121 * none is found, then the optional value $default is returned,
2122 * otherwise null.
2124 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
2126 * @package core
2127 * @category preference
2128 * @access public
2129 * @param string $name Name of the key to use in finding a preference value
2130 * @param mixed|null $default Value to be returned if the $name key is not set in the user preferences
2131 * @param stdClass|int|null $user A moodle user object or id, null means current user
2132 * @throws coding_exception
2133 * @return string|mixed|null A string containing the value of a single preference. An
2134 * array with all of the preferences or null
2136 function get_user_preferences($name = null, $default = null, $user = null) {
2137 global $USER;
2139 if (is_null($name)) {
2140 // All prefs.
2141 } else if (is_numeric($name) or $name === '_lastloaded') {
2142 throw new coding_exception('Invalid preference name in get_user_preferences() call');
2145 if (is_null($user)) {
2146 $user = $USER;
2147 } else if (isset($user->id)) {
2148 // Is a valid object.
2149 } else if (is_numeric($user)) {
2150 if ($USER->id == $user) {
2151 $user = $USER;
2152 } else {
2153 $user = (object)array('id' => (int)$user);
2155 } else {
2156 throw new coding_exception('Invalid $user parameter in get_user_preferences() call');
2159 check_user_preferences_loaded($user);
2161 if (empty($name)) {
2162 // All values.
2163 return $user->preference;
2164 } else if (isset($user->preference[$name])) {
2165 // The single string value.
2166 return $user->preference[$name];
2167 } else {
2168 // Default value (null if not specified).
2169 return $default;
2173 // FUNCTIONS FOR HANDLING TIME.
2176 * Given Gregorian date parts in user time produce a GMT timestamp.
2178 * @package core
2179 * @category time
2180 * @param int $year The year part to create timestamp of
2181 * @param int $month The month part to create timestamp of
2182 * @param int $day The day part to create timestamp of
2183 * @param int $hour The hour part to create timestamp of
2184 * @param int $minute The minute part to create timestamp of
2185 * @param int $second The second part to create timestamp of
2186 * @param int|float|string $timezone Timezone modifier, used to calculate GMT time offset.
2187 * if 99 then default user's timezone is used {@link http://docs.moodle.org/dev/Time_API#Timezone}
2188 * @param bool $applydst Toggle Daylight Saving Time, default true, will be
2189 * applied only if timezone is 99 or string.
2190 * @return int GMT timestamp
2192 function make_timestamp($year, $month=1, $day=1, $hour=0, $minute=0, $second=0, $timezone=99, $applydst=true) {
2193 $date = new DateTime('now', core_date::get_user_timezone_object($timezone));
2194 $date->setDate((int)$year, (int)$month, (int)$day);
2195 $date->setTime((int)$hour, (int)$minute, (int)$second);
2197 $time = $date->getTimestamp();
2199 if ($time === false) {
2200 throw new coding_exception('getTimestamp() returned false, please ensure you have passed correct values.'.
2201 ' This can fail if year is more than 2038 and OS is 32 bit windows');
2204 // Moodle BC DST stuff.
2205 if (!$applydst) {
2206 $time += dst_offset_on($time, $timezone);
2209 return $time;
2214 * Format a date/time (seconds) as weeks, days, hours etc as needed
2216 * Given an amount of time in seconds, returns string
2217 * formatted nicely as years, days, hours etc as needed
2219 * @package core
2220 * @category time
2221 * @uses MINSECS
2222 * @uses HOURSECS
2223 * @uses DAYSECS
2224 * @uses YEARSECS
2225 * @param int $totalsecs Time in seconds
2226 * @param stdClass $str Should be a time object
2227 * @return string A nicely formatted date/time string
2229 function format_time($totalsecs, $str = null) {
2231 $totalsecs = abs($totalsecs);
2233 if (!$str) {
2234 // Create the str structure the slow way.
2235 $str = new stdClass();
2236 $str->day = get_string('day');
2237 $str->days = get_string('days');
2238 $str->hour = get_string('hour');
2239 $str->hours = get_string('hours');
2240 $str->min = get_string('min');
2241 $str->mins = get_string('mins');
2242 $str->sec = get_string('sec');
2243 $str->secs = get_string('secs');
2244 $str->year = get_string('year');
2245 $str->years = get_string('years');
2248 $years = floor($totalsecs/YEARSECS);
2249 $remainder = $totalsecs - ($years*YEARSECS);
2250 $days = floor($remainder/DAYSECS);
2251 $remainder = $totalsecs - ($days*DAYSECS);
2252 $hours = floor($remainder/HOURSECS);
2253 $remainder = $remainder - ($hours*HOURSECS);
2254 $mins = floor($remainder/MINSECS);
2255 $secs = $remainder - ($mins*MINSECS);
2257 $ss = ($secs == 1) ? $str->sec : $str->secs;
2258 $sm = ($mins == 1) ? $str->min : $str->mins;
2259 $sh = ($hours == 1) ? $str->hour : $str->hours;
2260 $sd = ($days == 1) ? $str->day : $str->days;
2261 $sy = ($years == 1) ? $str->year : $str->years;
2263 $oyears = '';
2264 $odays = '';
2265 $ohours = '';
2266 $omins = '';
2267 $osecs = '';
2269 if ($years) {
2270 $oyears = $years .' '. $sy;
2272 if ($days) {
2273 $odays = $days .' '. $sd;
2275 if ($hours) {
2276 $ohours = $hours .' '. $sh;
2278 if ($mins) {
2279 $omins = $mins .' '. $sm;
2281 if ($secs) {
2282 $osecs = $secs .' '. $ss;
2285 if ($years) {
2286 return trim($oyears .' '. $odays);
2288 if ($days) {
2289 return trim($odays .' '. $ohours);
2291 if ($hours) {
2292 return trim($ohours .' '. $omins);
2294 if ($mins) {
2295 return trim($omins .' '. $osecs);
2297 if ($secs) {
2298 return $osecs;
2300 return get_string('now');
2304 * Returns a formatted string that represents a date in user time.
2306 * @package core
2307 * @category time
2308 * @param int $date the timestamp in UTC, as obtained from the database.
2309 * @param string $format strftime format. You should probably get this using
2310 * get_string('strftime...', 'langconfig');
2311 * @param int|float|string $timezone by default, uses the user's time zone. if numeric and
2312 * not 99 then daylight saving will not be added.
2313 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2314 * @param bool $fixday If true (default) then the leading zero from %d is removed.
2315 * If false then the leading zero is maintained.
2316 * @param bool $fixhour If true (default) then the leading zero from %I is removed.
2317 * @return string the formatted date/time.
2319 function userdate($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) {
2320 $calendartype = \core_calendar\type_factory::get_calendar_instance();
2321 return $calendartype->timestamp_to_date_string($date, $format, $timezone, $fixday, $fixhour);
2325 * Returns a html "time" tag with both the exact user date with timezone information
2326 * as a datetime attribute in the W3C format, and the user readable date and time as text.
2328 * @package core
2329 * @category time
2330 * @param int $date the timestamp in UTC, as obtained from the database.
2331 * @param string $format strftime format. You should probably get this using
2332 * get_string('strftime...', 'langconfig');
2333 * @param int|float|string $timezone by default, uses the user's time zone. if numeric and
2334 * not 99 then daylight saving will not be added.
2335 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2336 * @param bool $fixday If true (default) then the leading zero from %d is removed.
2337 * If false then the leading zero is maintained.
2338 * @param bool $fixhour If true (default) then the leading zero from %I is removed.
2339 * @return string the formatted date/time.
2341 function userdate_htmltime($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) {
2342 $userdatestr = userdate($date, $format, $timezone, $fixday, $fixhour);
2343 if (CLI_SCRIPT && !PHPUNIT_TEST) {
2344 return $userdatestr;
2346 $machinedate = new DateTime();
2347 $machinedate->setTimestamp(intval($date));
2348 $machinedate->setTimezone(core_date::get_user_timezone_object());
2350 return html_writer::tag('time', $userdatestr, ['datetime' => $machinedate->format(DateTime::W3C)]);
2354 * Returns a formatted date ensuring it is UTF-8.
2356 * If we are running under Windows convert to Windows encoding and then back to UTF-8
2357 * (because it's impossible to specify UTF-8 to fetch locale info in Win32).
2359 * @param int $date the timestamp - since Moodle 2.9 this is a real UTC timestamp
2360 * @param string $format strftime format.
2361 * @param int|float|string $tz the user timezone
2362 * @return string the formatted date/time.
2363 * @since Moodle 2.3.3
2365 function date_format_string($date, $format, $tz = 99) {
2367 date_default_timezone_set(core_date::get_user_timezone($tz));
2369 if (date('A', 0) === date('A', HOURSECS * 18)) {
2370 $datearray = getdate($date);
2371 $format = str_replace([
2372 '%P',
2373 '%p',
2374 ], [
2375 $datearray['hours'] < 12 ? get_string('am', 'langconfig') : get_string('pm', 'langconfig'),
2376 $datearray['hours'] < 12 ? get_string('amcaps', 'langconfig') : get_string('pmcaps', 'langconfig'),
2377 ], $format);
2380 $datestring = core_date::strftime($format, $date);
2381 core_date::set_default_server_timezone();
2383 return $datestring;
2387 * Given a $time timestamp in GMT (seconds since epoch),
2388 * returns an array that represents the Gregorian date in user time
2390 * @package core
2391 * @category time
2392 * @param int $time Timestamp in GMT
2393 * @param float|int|string $timezone user timezone
2394 * @return array An array that represents the date in user time
2396 function usergetdate($time, $timezone=99) {
2397 if ($time === null) {
2398 // PHP8 and PHP7 return different results when getdate(null) is called.
2399 // Display warning and cast to 0 to make sure the usergetdate() behaves consistently on all versions of PHP.
2400 // In the future versions of Moodle we may consider adding a strict typehint.
2401 debugging('usergetdate() expects parameter $time to be int, null given', DEBUG_DEVELOPER);
2402 $time = 0;
2405 date_default_timezone_set(core_date::get_user_timezone($timezone));
2406 $result = getdate($time);
2407 core_date::set_default_server_timezone();
2409 return $result;
2413 * Given a GMT timestamp (seconds since epoch), offsets it by
2414 * the timezone. eg 3pm in India is 3pm GMT - 7 * 3600 seconds
2416 * NOTE: this function does not include DST properly,
2417 * you should use the PHP date stuff instead!
2419 * @package core
2420 * @category time
2421 * @param int $date Timestamp in GMT
2422 * @param float|int|string $timezone user timezone
2423 * @return int
2425 function usertime($date, $timezone=99) {
2426 $userdate = new DateTime('@' . $date);
2427 $userdate->setTimezone(core_date::get_user_timezone_object($timezone));
2428 $dst = dst_offset_on($date, $timezone);
2430 return $date - $userdate->getOffset() + $dst;
2434 * Get a formatted string representation of an interval between two unix timestamps.
2436 * E.g.
2437 * $intervalstring = get_time_interval_string(12345600, 12345660);
2438 * Will produce the string:
2439 * '0d 0h 1m'
2441 * @param int $time1 unix timestamp
2442 * @param int $time2 unix timestamp
2443 * @param string $format string (can be lang string) containing format chars: https://www.php.net/manual/en/dateinterval.format.php.
2444 * @return string the formatted string describing the time difference, e.g. '10d 11h 45m'.
2446 function get_time_interval_string(int $time1, int $time2, string $format = ''): string {
2447 $dtdate = new DateTime();
2448 $dtdate->setTimeStamp($time1);
2449 $dtdate2 = new DateTime();
2450 $dtdate2->setTimeStamp($time2);
2451 $interval = $dtdate2->diff($dtdate);
2452 $format = empty($format) ? get_string('dateintervaldayshoursmins', 'langconfig') : $format;
2453 return $interval->format($format);
2457 * Given a time, return the GMT timestamp of the most recent midnight
2458 * for the current user.
2460 * @package core
2461 * @category time
2462 * @param int $date Timestamp in GMT
2463 * @param float|int|string $timezone user timezone
2464 * @return int Returns a GMT timestamp
2466 function usergetmidnight($date, $timezone=99) {
2468 $userdate = usergetdate($date, $timezone);
2470 // Time of midnight of this user's day, in GMT.
2471 return make_timestamp($userdate['year'], $userdate['mon'], $userdate['mday'], 0, 0, 0, $timezone);
2476 * Returns a string that prints the user's timezone
2478 * @package core
2479 * @category time
2480 * @param float|int|string $timezone user timezone
2481 * @return string
2483 function usertimezone($timezone=99) {
2484 $tz = core_date::get_user_timezone($timezone);
2485 return core_date::get_localised_timezone($tz);
2489 * Returns a float or a string which denotes the user's timezone
2490 * 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)
2491 * means that for this timezone there are also DST rules to be taken into account
2492 * Checks various settings and picks the most dominant of those which have a value
2494 * @package core
2495 * @category time
2496 * @param float|int|string $tz timezone to calculate GMT time offset before
2497 * calculating user timezone, 99 is default user timezone
2498 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2499 * @return float|string
2501 function get_user_timezone($tz = 99) {
2502 global $USER, $CFG;
2504 $timezones = array(
2505 $tz,
2506 isset($CFG->forcetimezone) ? $CFG->forcetimezone : 99,
2507 isset($USER->timezone) ? $USER->timezone : 99,
2508 isset($CFG->timezone) ? $CFG->timezone : 99,
2511 $tz = 99;
2513 // Loop while $tz is, empty but not zero, or 99, and there is another timezone is the array.
2514 foreach ($timezones as $nextvalue) {
2515 if ((empty($tz) && !is_numeric($tz)) || $tz == 99) {
2516 $tz = $nextvalue;
2519 return is_numeric($tz) ? (float) $tz : $tz;
2523 * Calculates the Daylight Saving Offset for a given date/time (timestamp)
2524 * - Note: Daylight saving only works for string timezones and not for float.
2526 * @package core
2527 * @category time
2528 * @param int $time must NOT be compensated at all, it has to be a pure timestamp
2529 * @param int|float|string $strtimezone user timezone
2530 * @return int
2532 function dst_offset_on($time, $strtimezone = null) {
2533 $tz = core_date::get_user_timezone($strtimezone);
2534 $date = new DateTime('@' . $time);
2535 $date->setTimezone(new DateTimeZone($tz));
2536 if ($date->format('I') == '1') {
2537 if ($tz === 'Australia/Lord_Howe') {
2538 return 1800;
2540 return 3600;
2542 return 0;
2546 * Calculates when the day appears in specific month
2548 * @package core
2549 * @category time
2550 * @param int $startday starting day of the month
2551 * @param int $weekday The day when week starts (normally taken from user preferences)
2552 * @param int $month The month whose day is sought
2553 * @param int $year The year of the month whose day is sought
2554 * @return int
2556 function find_day_in_month($startday, $weekday, $month, $year) {
2557 $calendartype = \core_calendar\type_factory::get_calendar_instance();
2559 $daysinmonth = days_in_month($month, $year);
2560 $daysinweek = count($calendartype->get_weekdays());
2562 if ($weekday == -1) {
2563 // Don't care about weekday, so return:
2564 // abs($startday) if $startday != -1
2565 // $daysinmonth otherwise.
2566 return ($startday == -1) ? $daysinmonth : abs($startday);
2569 // From now on we 're looking for a specific weekday.
2570 // Give "end of month" its actual value, since we know it.
2571 if ($startday == -1) {
2572 $startday = -1 * $daysinmonth;
2575 // Starting from day $startday, the sign is the direction.
2576 if ($startday < 1) {
2577 $startday = abs($startday);
2578 $lastmonthweekday = dayofweek($daysinmonth, $month, $year);
2580 // This is the last such weekday of the month.
2581 $lastinmonth = $daysinmonth + $weekday - $lastmonthweekday;
2582 if ($lastinmonth > $daysinmonth) {
2583 $lastinmonth -= $daysinweek;
2586 // Find the first such weekday <= $startday.
2587 while ($lastinmonth > $startday) {
2588 $lastinmonth -= $daysinweek;
2591 return $lastinmonth;
2592 } else {
2593 $indexweekday = dayofweek($startday, $month, $year);
2595 $diff = $weekday - $indexweekday;
2596 if ($diff < 0) {
2597 $diff += $daysinweek;
2600 // This is the first such weekday of the month equal to or after $startday.
2601 $firstfromindex = $startday + $diff;
2603 return $firstfromindex;
2608 * Calculate the number of days in a given month
2610 * @package core
2611 * @category time
2612 * @param int $month The month whose day count is sought
2613 * @param int $year The year of the month whose day count is sought
2614 * @return int
2616 function days_in_month($month, $year) {
2617 $calendartype = \core_calendar\type_factory::get_calendar_instance();
2618 return $calendartype->get_num_days_in_month($year, $month);
2622 * Calculate the position in the week of a specific calendar day
2624 * @package core
2625 * @category time
2626 * @param int $day The day of the date whose position in the week is sought
2627 * @param int $month The month of the date whose position in the week is sought
2628 * @param int $year The year of the date whose position in the week is sought
2629 * @return int
2631 function dayofweek($day, $month, $year) {
2632 $calendartype = \core_calendar\type_factory::get_calendar_instance();
2633 return $calendartype->get_weekday($year, $month, $day);
2636 // USER AUTHENTICATION AND LOGIN.
2639 * Returns full login url.
2641 * Any form submissions for authentication to this URL must include username,
2642 * password as well as a logintoken generated by \core\session\manager::get_login_token().
2644 * @return string login url
2646 function get_login_url() {
2647 global $CFG;
2649 return "$CFG->wwwroot/login/index.php";
2653 * This function checks that the current user is logged in and has the
2654 * required privileges
2656 * This function checks that the current user is logged in, and optionally
2657 * whether they are allowed to be in a particular course and view a particular
2658 * course module.
2659 * If they are not logged in, then it redirects them to the site login unless
2660 * $autologinguest is set and {@link $CFG}->autologinguests is set to 1 in which
2661 * case they are automatically logged in as guests.
2662 * If $courseid is given and the user is not enrolled in that course then the
2663 * user is redirected to the course enrolment page.
2664 * If $cm is given and the course module is hidden and the user is not a teacher
2665 * in the course then the user is redirected to the course home page.
2667 * When $cm parameter specified, this function sets page layout to 'module'.
2668 * You need to change it manually later if some other layout needed.
2670 * @package core_access
2671 * @category access
2673 * @param mixed $courseorid id of the course or course object
2674 * @param bool $autologinguest default true
2675 * @param object $cm course module object
2676 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
2677 * true. Used to avoid (=false) some scripts (file.php...) to set that variable,
2678 * in order to keep redirects working properly. MDL-14495
2679 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
2680 * @return mixed Void, exit, and die depending on path
2681 * @throws coding_exception
2682 * @throws require_login_exception
2683 * @throws moodle_exception
2685 function require_login($courseorid = null, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) {
2686 global $CFG, $SESSION, $USER, $PAGE, $SITE, $DB, $OUTPUT;
2688 // Must not redirect when byteserving already started.
2689 if (!empty($_SERVER['HTTP_RANGE'])) {
2690 $preventredirect = true;
2693 if (AJAX_SCRIPT) {
2694 // We cannot redirect for AJAX scripts either.
2695 $preventredirect = true;
2698 // Setup global $COURSE, themes, language and locale.
2699 if (!empty($courseorid)) {
2700 if (is_object($courseorid)) {
2701 $course = $courseorid;
2702 } else if ($courseorid == SITEID) {
2703 $course = clone($SITE);
2704 } else {
2705 $course = $DB->get_record('course', array('id' => $courseorid), '*', MUST_EXIST);
2707 if ($cm) {
2708 if ($cm->course != $course->id) {
2709 throw new coding_exception('course and cm parameters in require_login() call do not match!!');
2711 // Make sure we have a $cm from get_fast_modinfo as this contains activity access details.
2712 if (!($cm instanceof cm_info)) {
2713 // Note: nearly all pages call get_fast_modinfo anyway and it does not make any
2714 // db queries so this is not really a performance concern, however it is obviously
2715 // better if you use get_fast_modinfo to get the cm before calling this.
2716 $modinfo = get_fast_modinfo($course);
2717 $cm = $modinfo->get_cm($cm->id);
2720 } else {
2721 // Do not touch global $COURSE via $PAGE->set_course(),
2722 // the reasons is we need to be able to call require_login() at any time!!
2723 $course = $SITE;
2724 if ($cm) {
2725 throw new coding_exception('cm parameter in require_login() requires valid course parameter!');
2729 // If this is an AJAX request and $setwantsurltome is true then we need to override it and set it to false.
2730 // Otherwise the AJAX request URL will be set to $SESSION->wantsurl and events such as self enrolment in the future
2731 // risk leading the user back to the AJAX request URL.
2732 if ($setwantsurltome && defined('AJAX_SCRIPT') && AJAX_SCRIPT) {
2733 $setwantsurltome = false;
2736 // Redirect to the login page if session has expired, only with dbsessions enabled (MDL-35029) to maintain current behaviour.
2737 if ((!isloggedin() or isguestuser()) && !empty($SESSION->has_timed_out) && !empty($CFG->dbsessions)) {
2738 if ($preventredirect) {
2739 throw new require_login_session_timeout_exception();
2740 } else {
2741 if ($setwantsurltome) {
2742 $SESSION->wantsurl = qualified_me();
2744 redirect(get_login_url());
2748 // If the user is not even logged in yet then make sure they are.
2749 if (!isloggedin()) {
2750 if ($autologinguest && !empty($CFG->autologinguests)) {
2751 if (!$guest = get_complete_user_data('id', $CFG->siteguest)) {
2752 // Misconfigured site guest, just redirect to login page.
2753 redirect(get_login_url());
2754 exit; // Never reached.
2756 $lang = isset($SESSION->lang) ? $SESSION->lang : $CFG->lang;
2757 complete_user_login($guest);
2758 $USER->autologinguest = true;
2759 $SESSION->lang = $lang;
2760 } else {
2761 // NOTE: $USER->site check was obsoleted by session test cookie, $USER->confirmed test is in login/index.php.
2762 if ($preventredirect) {
2763 throw new require_login_exception('You are not logged in');
2766 if ($setwantsurltome) {
2767 $SESSION->wantsurl = qualified_me();
2770 // Give auth plugins an opportunity to authenticate or redirect to an external login page
2771 $authsequence = get_enabled_auth_plugins(); // Auths, in sequence.
2772 foreach($authsequence as $authname) {
2773 $authplugin = get_auth_plugin($authname);
2774 $authplugin->pre_loginpage_hook();
2775 if (isloggedin()) {
2776 if ($cm) {
2777 $modinfo = get_fast_modinfo($course);
2778 $cm = $modinfo->get_cm($cm->id);
2780 set_access_log_user();
2781 break;
2785 // If we're still not logged in then go to the login page
2786 if (!isloggedin()) {
2787 redirect(get_login_url());
2788 exit; // Never reached.
2793 // Loginas as redirection if needed.
2794 if ($course->id != SITEID and \core\session\manager::is_loggedinas()) {
2795 if ($USER->loginascontext->contextlevel == CONTEXT_COURSE) {
2796 if ($USER->loginascontext->instanceid != $course->id) {
2797 throw new \moodle_exception('loginasonecourse', '',
2798 $CFG->wwwroot.'/course/view.php?id='.$USER->loginascontext->instanceid);
2803 // Check whether the user should be changing password (but only if it is REALLY them).
2804 if (get_user_preferences('auth_forcepasswordchange') && !\core\session\manager::is_loggedinas()) {
2805 $userauth = get_auth_plugin($USER->auth);
2806 if ($userauth->can_change_password() and !$preventredirect) {
2807 if ($setwantsurltome) {
2808 $SESSION->wantsurl = qualified_me();
2810 if ($changeurl = $userauth->change_password_url()) {
2811 // Use plugin custom url.
2812 redirect($changeurl);
2813 } else {
2814 // Use moodle internal method.
2815 redirect($CFG->wwwroot .'/login/change_password.php');
2817 } else if ($userauth->can_change_password()) {
2818 throw new moodle_exception('forcepasswordchangenotice');
2819 } else {
2820 throw new moodle_exception('nopasswordchangeforced', 'auth');
2824 // Check that the user account is properly set up. If we can't redirect to
2825 // edit their profile and this is not a WS request, perform just the lax check.
2826 // It will allow them to use filepicker on the profile edit page.
2828 if ($preventredirect && !WS_SERVER) {
2829 $usernotfullysetup = user_not_fully_set_up($USER, false);
2830 } else {
2831 $usernotfullysetup = user_not_fully_set_up($USER, true);
2834 if ($usernotfullysetup) {
2835 if ($preventredirect) {
2836 throw new moodle_exception('usernotfullysetup');
2838 if ($setwantsurltome) {
2839 $SESSION->wantsurl = qualified_me();
2841 redirect($CFG->wwwroot .'/user/edit.php?id='. $USER->id .'&amp;course='. SITEID);
2844 // Make sure the USER has a sesskey set up. Used for CSRF protection.
2845 sesskey();
2847 if (\core\session\manager::is_loggedinas()) {
2848 // During a "logged in as" session we should force all content to be cleaned because the
2849 // logged in user will be viewing potentially malicious user generated content.
2850 // See MDL-63786 for more details.
2851 $CFG->forceclean = true;
2854 $afterlogins = get_plugins_with_function('after_require_login', 'lib.php');
2856 // Do not bother admins with any formalities, except for activities pending deletion.
2857 if (is_siteadmin() && !($cm && $cm->deletioninprogress)) {
2858 // Set the global $COURSE.
2859 if ($cm) {
2860 $PAGE->set_cm($cm, $course);
2861 $PAGE->set_pagelayout('incourse');
2862 } else if (!empty($courseorid)) {
2863 $PAGE->set_course($course);
2865 // Set accesstime or the user will appear offline which messes up messaging.
2866 // Do not update access time for webservice or ajax requests.
2867 if (!WS_SERVER && !AJAX_SCRIPT) {
2868 user_accesstime_log($course->id);
2871 foreach ($afterlogins as $plugintype => $plugins) {
2872 foreach ($plugins as $pluginfunction) {
2873 $pluginfunction($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2876 return;
2879 // Scripts have a chance to declare that $USER->policyagreed should not be checked.
2880 // This is mostly for places where users are actually accepting the policies, to avoid the redirect loop.
2881 if (!defined('NO_SITEPOLICY_CHECK')) {
2882 define('NO_SITEPOLICY_CHECK', false);
2885 // Check that the user has agreed to a site policy if there is one - do not test in case of admins.
2886 // Do not test if the script explicitly asked for skipping the site policies check.
2887 // Or if the user auth type is webservice.
2888 if (!$USER->policyagreed && !is_siteadmin() && !NO_SITEPOLICY_CHECK && $USER->auth !== 'webservice') {
2889 $manager = new \core_privacy\local\sitepolicy\manager();
2890 if ($policyurl = $manager->get_redirect_url(isguestuser())) {
2891 if ($preventredirect) {
2892 throw new moodle_exception('sitepolicynotagreed', 'error', '', $policyurl->out());
2894 if ($setwantsurltome) {
2895 $SESSION->wantsurl = qualified_me();
2897 redirect($policyurl);
2901 // Fetch the system context, the course context, and prefetch its child contexts.
2902 $sysctx = context_system::instance();
2903 $coursecontext = context_course::instance($course->id, MUST_EXIST);
2904 if ($cm) {
2905 $cmcontext = context_module::instance($cm->id, MUST_EXIST);
2906 } else {
2907 $cmcontext = null;
2910 // If the site is currently under maintenance, then print a message.
2911 if (!empty($CFG->maintenance_enabled) and !has_capability('moodle/site:maintenanceaccess', $sysctx)) {
2912 if ($preventredirect) {
2913 throw new require_login_exception('Maintenance in progress');
2915 $PAGE->set_context(null);
2916 print_maintenance_message();
2919 // Make sure the course itself is not hidden.
2920 if ($course->id == SITEID) {
2921 // Frontpage can not be hidden.
2922 } else {
2923 if (is_role_switched($course->id)) {
2924 // When switching roles ignore the hidden flag - user had to be in course to do the switch.
2925 } else {
2926 if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
2927 // Originally there was also test of parent category visibility, BUT is was very slow in complex queries
2928 // involving "my courses" now it is also possible to simply hide all courses user is not enrolled in :-).
2929 if ($preventredirect) {
2930 throw new require_login_exception('Course is hidden');
2932 $PAGE->set_context(null);
2933 // We need to override the navigation URL as the course won't have been added to the navigation and thus
2934 // the navigation will mess up when trying to find it.
2935 navigation_node::override_active_url(new moodle_url('/'));
2936 notice(get_string('coursehidden'), $CFG->wwwroot .'/');
2941 // Is the user enrolled?
2942 if ($course->id == SITEID) {
2943 // Everybody is enrolled on the frontpage.
2944 } else {
2945 if (\core\session\manager::is_loggedinas()) {
2946 // Make sure the REAL person can access this course first.
2947 $realuser = \core\session\manager::get_realuser();
2948 if (!is_enrolled($coursecontext, $realuser->id, '', true) and
2949 !is_viewing($coursecontext, $realuser->id) and !is_siteadmin($realuser->id)) {
2950 if ($preventredirect) {
2951 throw new require_login_exception('Invalid course login-as access');
2953 $PAGE->set_context(null);
2954 echo $OUTPUT->header();
2955 notice(get_string('studentnotallowed', '', fullname($USER, true)), $CFG->wwwroot .'/');
2959 $access = false;
2961 if (is_role_switched($course->id)) {
2962 // Ok, user had to be inside this course before the switch.
2963 $access = true;
2965 } else if (is_viewing($coursecontext, $USER)) {
2966 // Ok, no need to mess with enrol.
2967 $access = true;
2969 } else {
2970 if (isset($USER->enrol['enrolled'][$course->id])) {
2971 if ($USER->enrol['enrolled'][$course->id] > time()) {
2972 $access = true;
2973 if (isset($USER->enrol['tempguest'][$course->id])) {
2974 unset($USER->enrol['tempguest'][$course->id]);
2975 remove_temp_course_roles($coursecontext);
2977 } else {
2978 // Expired.
2979 unset($USER->enrol['enrolled'][$course->id]);
2982 if (isset($USER->enrol['tempguest'][$course->id])) {
2983 if ($USER->enrol['tempguest'][$course->id] == 0) {
2984 $access = true;
2985 } else if ($USER->enrol['tempguest'][$course->id] > time()) {
2986 $access = true;
2987 } else {
2988 // Expired.
2989 unset($USER->enrol['tempguest'][$course->id]);
2990 remove_temp_course_roles($coursecontext);
2994 if (!$access) {
2995 // Cache not ok.
2996 $until = enrol_get_enrolment_end($coursecontext->instanceid, $USER->id);
2997 if ($until !== false) {
2998 // Active participants may always access, a timestamp in the future, 0 (always) or false.
2999 if ($until == 0) {
3000 $until = ENROL_MAX_TIMESTAMP;
3002 $USER->enrol['enrolled'][$course->id] = $until;
3003 $access = true;
3005 } else if (core_course_category::can_view_course_info($course)) {
3006 $params = array('courseid' => $course->id, 'status' => ENROL_INSTANCE_ENABLED);
3007 $instances = $DB->get_records('enrol', $params, 'sortorder, id ASC');
3008 $enrols = enrol_get_plugins(true);
3009 // First ask all enabled enrol instances in course if they want to auto enrol user.
3010 foreach ($instances as $instance) {
3011 if (!isset($enrols[$instance->enrol])) {
3012 continue;
3014 // Get a duration for the enrolment, a timestamp in the future, 0 (always) or false.
3015 $until = $enrols[$instance->enrol]->try_autoenrol($instance);
3016 if ($until !== false) {
3017 if ($until == 0) {
3018 $until = ENROL_MAX_TIMESTAMP;
3020 $USER->enrol['enrolled'][$course->id] = $until;
3021 $access = true;
3022 break;
3025 // If not enrolled yet try to gain temporary guest access.
3026 if (!$access) {
3027 foreach ($instances as $instance) {
3028 if (!isset($enrols[$instance->enrol])) {
3029 continue;
3031 // Get a duration for the guest access, a timestamp in the future or false.
3032 $until = $enrols[$instance->enrol]->try_guestaccess($instance);
3033 if ($until !== false and $until > time()) {
3034 $USER->enrol['tempguest'][$course->id] = $until;
3035 $access = true;
3036 break;
3040 } else {
3041 // User is not enrolled and is not allowed to browse courses here.
3042 if ($preventredirect) {
3043 throw new require_login_exception('Course is not available');
3045 $PAGE->set_context(null);
3046 // We need to override the navigation URL as the course won't have been added to the navigation and thus
3047 // the navigation will mess up when trying to find it.
3048 navigation_node::override_active_url(new moodle_url('/'));
3049 notice(get_string('coursehidden'), $CFG->wwwroot .'/');
3054 if (!$access) {
3055 if ($preventredirect) {
3056 throw new require_login_exception('Not enrolled');
3058 if ($setwantsurltome) {
3059 $SESSION->wantsurl = qualified_me();
3061 redirect($CFG->wwwroot .'/enrol/index.php?id='. $course->id);
3065 // Check whether the activity has been scheduled for deletion. If so, then deny access, even for admins.
3066 if ($cm && $cm->deletioninprogress) {
3067 if ($preventredirect) {
3068 throw new moodle_exception('activityisscheduledfordeletion');
3070 require_once($CFG->dirroot . '/course/lib.php');
3071 redirect(course_get_url($course), get_string('activityisscheduledfordeletion', 'error'));
3074 // Check visibility of activity to current user; includes visible flag, conditional availability, etc.
3075 if ($cm && !$cm->uservisible) {
3076 if ($preventredirect) {
3077 throw new require_login_exception('Activity is hidden');
3079 // Get the error message that activity is not available and why (if explanation can be shown to the user).
3080 $PAGE->set_course($course);
3081 $renderer = $PAGE->get_renderer('course');
3082 $message = $renderer->course_section_cm_unavailable_error_message($cm);
3083 redirect(course_get_url($course), $message, null, \core\output\notification::NOTIFY_ERROR);
3086 // Set the global $COURSE.
3087 if ($cm) {
3088 $PAGE->set_cm($cm, $course);
3089 $PAGE->set_pagelayout('incourse');
3090 } else if (!empty($courseorid)) {
3091 $PAGE->set_course($course);
3094 foreach ($afterlogins as $plugintype => $plugins) {
3095 foreach ($plugins as $pluginfunction) {
3096 $pluginfunction($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3100 // Finally access granted, update lastaccess times.
3101 // Do not update access time for webservice or ajax requests.
3102 if (!WS_SERVER && !AJAX_SCRIPT) {
3103 user_accesstime_log($course->id);
3108 * A convenience function for where we must be logged in as admin
3109 * @return void
3111 function require_admin() {
3112 require_login(null, false);
3113 require_capability('moodle/site:config', context_system::instance());
3117 * This function just makes sure a user is logged out.
3119 * @package core_access
3120 * @category access
3122 function require_logout() {
3123 global $USER, $DB;
3125 if (!isloggedin()) {
3126 // This should not happen often, no need for hooks or events here.
3127 \core\session\manager::terminate_current();
3128 return;
3131 // Execute hooks before action.
3132 $authplugins = array();
3133 $authsequence = get_enabled_auth_plugins();
3134 foreach ($authsequence as $authname) {
3135 $authplugins[$authname] = get_auth_plugin($authname);
3136 $authplugins[$authname]->prelogout_hook();
3139 // Store info that gets removed during logout.
3140 $sid = session_id();
3141 $event = \core\event\user_loggedout::create(
3142 array(
3143 'userid' => $USER->id,
3144 'objectid' => $USER->id,
3145 'other' => array('sessionid' => $sid),
3148 if ($session = $DB->get_record('sessions', array('sid'=>$sid))) {
3149 $event->add_record_snapshot('sessions', $session);
3152 // Clone of $USER object to be used by auth plugins.
3153 $user = fullclone($USER);
3155 // Delete session record and drop $_SESSION content.
3156 \core\session\manager::terminate_current();
3158 // Trigger event AFTER action.
3159 $event->trigger();
3161 // Hook to execute auth plugins redirection after event trigger.
3162 foreach ($authplugins as $authplugin) {
3163 $authplugin->postlogout_hook($user);
3168 * Weaker version of require_login()
3170 * This is a weaker version of {@link require_login()} which only requires login
3171 * when called from within a course rather than the site page, unless
3172 * the forcelogin option is turned on.
3173 * @see require_login()
3175 * @package core_access
3176 * @category access
3178 * @param mixed $courseorid The course object or id in question
3179 * @param bool $autologinguest Allow autologin guests if that is wanted
3180 * @param object $cm Course activity module if known
3181 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
3182 * true. Used to avoid (=false) some scripts (file.php...) to set that variable,
3183 * in order to keep redirects working properly. MDL-14495
3184 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
3185 * @return void
3186 * @throws coding_exception
3188 function require_course_login($courseorid, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) {
3189 global $CFG, $PAGE, $SITE;
3190 $issite = ((is_object($courseorid) and $courseorid->id == SITEID)
3191 or (!is_object($courseorid) and $courseorid == SITEID));
3192 if ($issite && !empty($cm) && !($cm instanceof cm_info)) {
3193 // Note: nearly all pages call get_fast_modinfo anyway and it does not make any
3194 // db queries so this is not really a performance concern, however it is obviously
3195 // better if you use get_fast_modinfo to get the cm before calling this.
3196 if (is_object($courseorid)) {
3197 $course = $courseorid;
3198 } else {
3199 $course = clone($SITE);
3201 $modinfo = get_fast_modinfo($course);
3202 $cm = $modinfo->get_cm($cm->id);
3204 if (!empty($CFG->forcelogin)) {
3205 // Login required for both SITE and courses.
3206 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3208 } else if ($issite && !empty($cm) and !$cm->uservisible) {
3209 // Always login for hidden activities.
3210 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3212 } else if (isloggedin() && !isguestuser()) {
3213 // User is already logged in. Make sure the login is complete (user is fully setup, policies agreed).
3214 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3216 } else if ($issite) {
3217 // Login for SITE not required.
3218 // We still need to instatiate PAGE vars properly so that things that rely on it like navigation function correctly.
3219 if (!empty($courseorid)) {
3220 if (is_object($courseorid)) {
3221 $course = $courseorid;
3222 } else {
3223 $course = clone $SITE;
3225 if ($cm) {
3226 if ($cm->course != $course->id) {
3227 throw new coding_exception('course and cm parameters in require_course_login() call do not match!!');
3229 $PAGE->set_cm($cm, $course);
3230 $PAGE->set_pagelayout('incourse');
3231 } else {
3232 $PAGE->set_course($course);
3234 } else {
3235 // If $PAGE->course, and hence $PAGE->context, have not already been set up properly, set them up now.
3236 $PAGE->set_course($PAGE->course);
3238 // Do not update access time for webservice or ajax requests.
3239 if (!WS_SERVER && !AJAX_SCRIPT) {
3240 user_accesstime_log(SITEID);
3242 return;
3244 } else {
3245 // Course login always required.
3246 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3251 * Validates a user key, checking if the key exists, is not expired and the remote ip is correct.
3253 * @param string $keyvalue the key value
3254 * @param string $script unique script identifier
3255 * @param int $instance instance id
3256 * @return stdClass the key entry in the user_private_key table
3257 * @since Moodle 3.2
3258 * @throws moodle_exception
3260 function validate_user_key($keyvalue, $script, $instance) {
3261 global $DB;
3263 if (!$key = $DB->get_record('user_private_key', array('script' => $script, 'value' => $keyvalue, 'instance' => $instance))) {
3264 throw new \moodle_exception('invalidkey');
3267 if (!empty($key->validuntil) and $key->validuntil < time()) {
3268 throw new \moodle_exception('expiredkey');
3271 if ($key->iprestriction) {
3272 $remoteaddr = getremoteaddr(null);
3273 if (empty($remoteaddr) or !address_in_subnet($remoteaddr, $key->iprestriction)) {
3274 throw new \moodle_exception('ipmismatch');
3277 return $key;
3281 * Require key login. Function terminates with error if key not found or incorrect.
3283 * @uses NO_MOODLE_COOKIES
3284 * @uses PARAM_ALPHANUM
3285 * @param string $script unique script identifier
3286 * @param int $instance optional instance id
3287 * @param string $keyvalue The key. If not supplied, this will be fetched from the current session.
3288 * @return int Instance ID
3290 function require_user_key_login($script, $instance = null, $keyvalue = null) {
3291 global $DB;
3293 if (!NO_MOODLE_COOKIES) {
3294 throw new \moodle_exception('sessioncookiesdisable');
3297 // Extra safety.
3298 \core\session\manager::write_close();
3300 if (null === $keyvalue) {
3301 $keyvalue = required_param('key', PARAM_ALPHANUM);
3304 $key = validate_user_key($keyvalue, $script, $instance);
3306 if (!$user = $DB->get_record('user', array('id' => $key->userid))) {
3307 throw new \moodle_exception('invaliduserid');
3310 core_user::require_active_user($user, true, true);
3312 // Emulate normal session.
3313 enrol_check_plugins($user, false);
3314 \core\session\manager::set_user($user);
3316 // Note we are not using normal login.
3317 if (!defined('USER_KEY_LOGIN')) {
3318 define('USER_KEY_LOGIN', true);
3321 // Return instance id - it might be empty.
3322 return $key->instance;
3326 * Creates a new private user access key.
3328 * @param string $script unique target identifier
3329 * @param int $userid
3330 * @param int $instance optional instance id
3331 * @param string $iprestriction optional ip restricted access
3332 * @param int $validuntil key valid only until given data
3333 * @return string access key value
3335 function create_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
3336 global $DB;
3338 $key = new stdClass();
3339 $key->script = $script;
3340 $key->userid = $userid;
3341 $key->instance = $instance;
3342 $key->iprestriction = $iprestriction;
3343 $key->validuntil = $validuntil;
3344 $key->timecreated = time();
3346 // Something long and unique.
3347 $key->value = md5($userid.'_'.time().random_string(40));
3348 while ($DB->record_exists('user_private_key', array('value' => $key->value))) {
3349 // Must be unique.
3350 $key->value = md5($userid.'_'.time().random_string(40));
3352 $DB->insert_record('user_private_key', $key);
3353 return $key->value;
3357 * Delete the user's new private user access keys for a particular script.
3359 * @param string $script unique target identifier
3360 * @param int $userid
3361 * @return void
3363 function delete_user_key($script, $userid) {
3364 global $DB;
3365 $DB->delete_records('user_private_key', array('script' => $script, 'userid' => $userid));
3369 * Gets a private user access key (and creates one if one doesn't exist).
3371 * @param string $script unique target identifier
3372 * @param int $userid
3373 * @param int $instance optional instance id
3374 * @param string $iprestriction optional ip restricted access
3375 * @param int $validuntil key valid only until given date
3376 * @return string access key value
3378 function get_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
3379 global $DB;
3381 if ($key = $DB->get_record('user_private_key', array('script' => $script, 'userid' => $userid,
3382 'instance' => $instance, 'iprestriction' => $iprestriction,
3383 'validuntil' => $validuntil))) {
3384 return $key->value;
3385 } else {
3386 return create_user_key($script, $userid, $instance, $iprestriction, $validuntil);
3392 * Modify the user table by setting the currently logged in user's last login to now.
3394 * @return bool Always returns true
3396 function update_user_login_times() {
3397 global $USER, $DB, $SESSION;
3399 if (isguestuser()) {
3400 // Do not update guest access times/ips for performance.
3401 return true;
3404 if (defined('USER_KEY_LOGIN') && USER_KEY_LOGIN === true) {
3405 // Do not update user login time when using user key login.
3406 return true;
3409 $now = time();
3411 $user = new stdClass();
3412 $user->id = $USER->id;
3414 // Make sure all users that logged in have some firstaccess.
3415 if ($USER->firstaccess == 0) {
3416 $USER->firstaccess = $user->firstaccess = $now;
3419 // Store the previous current as lastlogin.
3420 $USER->lastlogin = $user->lastlogin = $USER->currentlogin;
3422 $USER->currentlogin = $user->currentlogin = $now;
3424 // Function user_accesstime_log() may not update immediately, better do it here.
3425 $USER->lastaccess = $user->lastaccess = $now;
3426 $SESSION->userpreviousip = $USER->lastip;
3427 $USER->lastip = $user->lastip = getremoteaddr();
3429 // Note: do not call user_update_user() here because this is part of the login process,
3430 // the login event means that these fields were updated.
3431 $DB->update_record('user', $user);
3432 return true;
3436 * Determines if a user has completed setting up their account.
3438 * The lax mode (with $strict = false) has been introduced for special cases
3439 * only where we want to skip certain checks intentionally. This is valid in
3440 * certain mnet or ajax scenarios when the user cannot / should not be
3441 * redirected to edit their profile. In most cases, you should perform the
3442 * strict check.
3444 * @param stdClass $user A {@link $USER} object to test for the existence of a valid name and email
3445 * @param bool $strict Be more strict and assert id and custom profile fields set, too
3446 * @return bool
3448 function user_not_fully_set_up($user, $strict = true) {
3449 global $CFG, $SESSION, $USER;
3450 require_once($CFG->dirroot.'/user/profile/lib.php');
3452 // If the user is setup then store this in the session to avoid re-checking.
3453 // Some edge cases are when the users email starts to bounce or the
3454 // configuration for custom fields has changed while they are logged in so
3455 // we re-check this fully every hour for the rare cases it has changed.
3456 if (isset($USER->id) && isset($user->id) && $USER->id === $user->id &&
3457 isset($SESSION->fullysetupstrict) && (time() - $SESSION->fullysetupstrict) < HOURSECS) {
3458 return false;
3461 if (isguestuser($user)) {
3462 return false;
3465 if (empty($user->firstname) or empty($user->lastname) or empty($user->email) or over_bounce_threshold($user)) {
3466 return true;
3469 if ($strict) {
3470 if (empty($user->id)) {
3471 // Strict mode can be used with existing accounts only.
3472 return true;
3474 if (!profile_has_required_custom_fields_set($user->id)) {
3475 return true;
3477 if (isset($USER->id) && isset($user->id) && $USER->id === $user->id) {
3478 $SESSION->fullysetupstrict = time();
3482 return false;
3486 * Check whether the user has exceeded the bounce threshold
3488 * @param stdClass $user A {@link $USER} object
3489 * @return bool true => User has exceeded bounce threshold
3491 function over_bounce_threshold($user) {
3492 global $CFG, $DB;
3494 if (empty($CFG->handlebounces)) {
3495 return false;
3498 if (empty($user->id)) {
3499 // No real (DB) user, nothing to do here.
3500 return false;
3503 // Set sensible defaults.
3504 if (empty($CFG->minbounces)) {
3505 $CFG->minbounces = 10;
3507 if (empty($CFG->bounceratio)) {
3508 $CFG->bounceratio = .20;
3510 $bouncecount = 0;
3511 $sendcount = 0;
3512 if ($bounce = $DB->get_record('user_preferences', array ('userid' => $user->id, 'name' => 'email_bounce_count'))) {
3513 $bouncecount = $bounce->value;
3515 if ($send = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) {
3516 $sendcount = $send->value;
3518 return ($bouncecount >= $CFG->minbounces && $bouncecount/$sendcount >= $CFG->bounceratio);
3522 * Used to increment or reset email sent count
3524 * @param stdClass $user object containing an id
3525 * @param bool $reset will reset the count to 0
3526 * @return void
3528 function set_send_count($user, $reset=false) {
3529 global $DB;
3531 if (empty($user->id)) {
3532 // No real (DB) user, nothing to do here.
3533 return;
3536 if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) {
3537 $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
3538 $DB->update_record('user_preferences', $pref);
3539 } else if (!empty($reset)) {
3540 // If it's not there and we're resetting, don't bother. Make a new one.
3541 $pref = new stdClass();
3542 $pref->name = 'email_send_count';
3543 $pref->value = 1;
3544 $pref->userid = $user->id;
3545 $DB->insert_record('user_preferences', $pref, false);
3550 * Increment or reset user's email bounce count
3552 * @param stdClass $user object containing an id
3553 * @param bool $reset will reset the count to 0
3555 function set_bounce_count($user, $reset=false) {
3556 global $DB;
3558 if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_bounce_count'))) {
3559 $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
3560 $DB->update_record('user_preferences', $pref);
3561 } else if (!empty($reset)) {
3562 // If it's not there and we're resetting, don't bother. Make a new one.
3563 $pref = new stdClass();
3564 $pref->name = 'email_bounce_count';
3565 $pref->value = 1;
3566 $pref->userid = $user->id;
3567 $DB->insert_record('user_preferences', $pref, false);
3572 * Determines if the logged in user is currently moving an activity
3574 * @param int $courseid The id of the course being tested
3575 * @return bool
3577 function ismoving($courseid) {
3578 global $USER;
3580 if (!empty($USER->activitycopy)) {
3581 return ($USER->activitycopycourse == $courseid);
3583 return false;
3587 * Returns a persons full name
3589 * Given an object containing all of the users name values, this function returns a string with the full name of the person.
3590 * The result may depend on system settings or language. 'override' will force the alternativefullnameformat to be used. In
3591 * English, fullname as well as alternativefullnameformat is set to 'firstname lastname' by default. But you could have
3592 * fullname set to 'firstname lastname' and alternativefullnameformat set to 'firstname middlename alternatename lastname'.
3594 * @param stdClass $user A {@link $USER} object to get full name of.
3595 * @param bool $override If true then the alternativefullnameformat format rather than fullnamedisplay format will be used.
3596 * @return string
3598 function fullname($user, $override=false) {
3599 // Note: We do not intend to deprecate this function any time soon as it is too widely used at this time.
3600 // Uses of it should be updated to use the new API and pass updated arguments.
3602 // Return an empty string if there is no user.
3603 if (empty($user)) {
3604 return '';
3607 $options = ['override' => $override];
3608 return core_user::get_fullname($user, null, $options);
3612 * Reduces lines of duplicated code for getting user name fields.
3614 * See also {@link user_picture::unalias()}
3616 * @param object $addtoobject Object to add user name fields to.
3617 * @param object $secondobject Object that contains user name field information.
3618 * @param string $prefix prefix to be added to all fields (including $additionalfields) e.g. authorfirstname.
3619 * @param array $additionalfields Additional fields to be matched with data in the second object.
3620 * The key can be set to the user table field name.
3621 * @return object User name fields.
3623 function username_load_fields_from_object($addtoobject, $secondobject, $prefix = null, $additionalfields = null) {
3624 $fields = [];
3625 foreach (\core_user\fields::get_name_fields() as $field) {
3626 $fields[$field] = $prefix . $field;
3628 if ($additionalfields) {
3629 // Additional fields can specify their own 'alias' such as 'id' => 'userid'. This checks to see if
3630 // the key is a number and then sets the key to the array value.
3631 foreach ($additionalfields as $key => $value) {
3632 if (is_numeric($key)) {
3633 $additionalfields[$value] = $prefix . $value;
3634 unset($additionalfields[$key]);
3635 } else {
3636 $additionalfields[$key] = $prefix . $value;
3639 $fields = array_merge($fields, $additionalfields);
3641 foreach ($fields as $key => $field) {
3642 // Important that we have all of the user name fields present in the object that we are sending back.
3643 $addtoobject->$key = '';
3644 if (isset($secondobject->$field)) {
3645 $addtoobject->$key = $secondobject->$field;
3648 return $addtoobject;
3652 * Returns an array of values in order of occurance in a provided string.
3653 * The key in the result is the character postion in the string.
3655 * @param array $values Values to be found in the string format
3656 * @param string $stringformat The string which may contain values being searched for.
3657 * @return array An array of values in order according to placement in the string format.
3659 function order_in_string($values, $stringformat) {
3660 $valuearray = array();
3661 foreach ($values as $value) {
3662 $pattern = "/$value\b/";
3663 // Using preg_match as strpos() may match values that are similar e.g. firstname and firstnamephonetic.
3664 if (preg_match($pattern, $stringformat)) {
3665 $replacement = "thing";
3666 // Replace the value with something more unique to ensure we get the right position when using strpos().
3667 $newformat = preg_replace($pattern, $replacement, $stringformat);
3668 $position = strpos($newformat, $replacement);
3669 $valuearray[$position] = $value;
3672 ksort($valuearray);
3673 return $valuearray;
3677 * Returns whether a given authentication plugin exists.
3679 * @param string $auth Form of authentication to check for. Defaults to the global setting in {@link $CFG}.
3680 * @return boolean Whether the plugin is available.
3682 function exists_auth_plugin($auth) {
3683 global $CFG;
3685 if (file_exists("{$CFG->dirroot}/auth/$auth/auth.php")) {
3686 return is_readable("{$CFG->dirroot}/auth/$auth/auth.php");
3688 return false;
3692 * Checks if a given plugin is in the list of enabled authentication plugins.
3694 * @param string $auth Authentication plugin.
3695 * @return boolean Whether the plugin is enabled.
3697 function is_enabled_auth($auth) {
3698 if (empty($auth)) {
3699 return false;
3702 $enabled = get_enabled_auth_plugins();
3704 return in_array($auth, $enabled);
3708 * Returns an authentication plugin instance.
3710 * @param string $auth name of authentication plugin
3711 * @return auth_plugin_base An instance of the required authentication plugin.
3713 function get_auth_plugin($auth) {
3714 global $CFG;
3716 // Check the plugin exists first.
3717 if (! exists_auth_plugin($auth)) {
3718 throw new \moodle_exception('authpluginnotfound', 'debug', '', $auth);
3721 // Return auth plugin instance.
3722 require_once("{$CFG->dirroot}/auth/$auth/auth.php");
3723 $class = "auth_plugin_$auth";
3724 return new $class;
3728 * Returns array of active auth plugins.
3730 * @param bool $fix fix $CFG->auth if needed. Only set if logged in as admin.
3731 * @return array
3733 function get_enabled_auth_plugins($fix=false) {
3734 global $CFG;
3736 $default = array('manual', 'nologin');
3738 if (empty($CFG->auth)) {
3739 $auths = array();
3740 } else {
3741 $auths = explode(',', $CFG->auth);
3744 $auths = array_unique($auths);
3745 $oldauthconfig = implode(',', $auths);
3746 foreach ($auths as $k => $authname) {
3747 if (in_array($authname, $default)) {
3748 // The manual and nologin plugin never need to be stored.
3749 unset($auths[$k]);
3750 } else if (!exists_auth_plugin($authname)) {
3751 debugging(get_string('authpluginnotfound', 'debug', $authname));
3752 unset($auths[$k]);
3756 // Ideally only explicit interaction from a human admin should trigger a
3757 // change in auth config, see MDL-70424 for details.
3758 if ($fix) {
3759 $newconfig = implode(',', $auths);
3760 if (!isset($CFG->auth) or $newconfig != $CFG->auth) {
3761 add_to_config_log('auth', $oldauthconfig, $newconfig, 'core');
3762 set_config('auth', $newconfig);
3766 return (array_merge($default, $auths));
3770 * Returns true if an internal authentication method is being used.
3771 * if method not specified then, global default is assumed
3773 * @param string $auth Form of authentication required
3774 * @return bool
3776 function is_internal_auth($auth) {
3777 // Throws error if bad $auth.
3778 $authplugin = get_auth_plugin($auth);
3779 return $authplugin->is_internal();
3783 * Returns true if the user is a 'restored' one.
3785 * Used in the login process to inform the user and allow him/her to reset the password
3787 * @param string $username username to be checked
3788 * @return bool
3790 function is_restored_user($username) {
3791 global $CFG, $DB;
3793 return $DB->record_exists('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id, 'password' => 'restored'));
3797 * Returns an array of user fields
3799 * @return array User field/column names
3801 function get_user_fieldnames() {
3802 global $DB;
3804 $fieldarray = $DB->get_columns('user');
3805 unset($fieldarray['id']);
3806 $fieldarray = array_keys($fieldarray);
3808 return $fieldarray;
3812 * Returns the string of the language for the new user.
3814 * @return string language for the new user
3816 function get_newuser_language() {
3817 global $CFG, $SESSION;
3818 return (!empty($CFG->autolangusercreation) && !empty($SESSION->lang)) ? $SESSION->lang : $CFG->lang;
3822 * Creates a bare-bones user record
3824 * @todo Outline auth types and provide code example
3826 * @param string $username New user's username to add to record
3827 * @param string $password New user's password to add to record
3828 * @param string $auth Form of authentication required
3829 * @return stdClass A complete user object
3831 function create_user_record($username, $password, $auth = 'manual') {
3832 global $CFG, $DB, $SESSION;
3833 require_once($CFG->dirroot.'/user/profile/lib.php');
3834 require_once($CFG->dirroot.'/user/lib.php');
3836 // Just in case check text case.
3837 $username = trim(core_text::strtolower($username));
3839 $authplugin = get_auth_plugin($auth);
3840 $customfields = $authplugin->get_custom_user_profile_fields();
3841 $newuser = new stdClass();
3842 if ($newinfo = $authplugin->get_userinfo($username)) {
3843 $newinfo = truncate_userinfo($newinfo);
3844 foreach ($newinfo as $key => $value) {
3845 if (in_array($key, $authplugin->userfields) || (in_array($key, $customfields))) {
3846 $newuser->$key = $value;
3851 if (!empty($newuser->email)) {
3852 if (email_is_not_allowed($newuser->email)) {
3853 unset($newuser->email);
3857 $newuser->auth = $auth;
3858 $newuser->username = $username;
3860 // Fix for MDL-8480
3861 // user CFG lang for user if $newuser->lang is empty
3862 // or $user->lang is not an installed language.
3863 if (empty($newuser->lang) || !get_string_manager()->translation_exists($newuser->lang)) {
3864 $newuser->lang = get_newuser_language();
3866 $newuser->confirmed = 1;
3867 $newuser->lastip = getremoteaddr();
3868 $newuser->timecreated = time();
3869 $newuser->timemodified = $newuser->timecreated;
3870 $newuser->mnethostid = $CFG->mnet_localhost_id;
3872 $newuser->id = user_create_user($newuser, false, false);
3874 // Save user profile data.
3875 profile_save_data($newuser);
3877 $user = get_complete_user_data('id', $newuser->id);
3878 if (!empty($CFG->{'auth_'.$newuser->auth.'_forcechangepassword'})) {
3879 set_user_preference('auth_forcepasswordchange', 1, $user);
3881 // Set the password.
3882 update_internal_user_password($user, $password);
3884 // Trigger event.
3885 \core\event\user_created::create_from_userid($newuser->id)->trigger();
3887 return $user;
3891 * Will update a local user record from an external source (MNET users can not be updated using this method!).
3893 * @param string $username user's username to update the record
3894 * @return stdClass A complete user object
3896 function update_user_record($username) {
3897 global $DB, $CFG;
3898 // Just in case check text case.
3899 $username = trim(core_text::strtolower($username));
3901 $oldinfo = $DB->get_record('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id), '*', MUST_EXIST);
3902 return update_user_record_by_id($oldinfo->id);
3906 * Will update a local user record from an external source (MNET users can not be updated using this method!).
3908 * @param int $id user id
3909 * @return stdClass A complete user object
3911 function update_user_record_by_id($id) {
3912 global $DB, $CFG;
3913 require_once($CFG->dirroot."/user/profile/lib.php");
3914 require_once($CFG->dirroot.'/user/lib.php');
3916 $params = array('mnethostid' => $CFG->mnet_localhost_id, 'id' => $id, 'deleted' => 0);
3917 $oldinfo = $DB->get_record('user', $params, '*', MUST_EXIST);
3919 $newuser = array();
3920 $userauth = get_auth_plugin($oldinfo->auth);
3922 if ($newinfo = $userauth->get_userinfo($oldinfo->username)) {
3923 $newinfo = truncate_userinfo($newinfo);
3924 $customfields = $userauth->get_custom_user_profile_fields();
3926 foreach ($newinfo as $key => $value) {
3927 $iscustom = in_array($key, $customfields);
3928 if (!$iscustom) {
3929 $key = strtolower($key);
3931 if ((!property_exists($oldinfo, $key) && !$iscustom) or $key === 'username' or $key === 'id'
3932 or $key === 'auth' or $key === 'mnethostid' or $key === 'deleted') {
3933 // Unknown or must not be changed.
3934 continue;
3936 if (empty($userauth->config->{'field_updatelocal_' . $key}) || empty($userauth->config->{'field_lock_' . $key})) {
3937 continue;
3939 $confval = $userauth->config->{'field_updatelocal_' . $key};
3940 $lockval = $userauth->config->{'field_lock_' . $key};
3941 if ($confval === 'onlogin') {
3942 // MDL-4207 Don't overwrite modified user profile values with
3943 // empty LDAP values when 'unlocked if empty' is set. The purpose
3944 // of the setting 'unlocked if empty' is to allow the user to fill
3945 // in a value for the selected field _if LDAP is giving
3946 // nothing_ for this field. Thus it makes sense to let this value
3947 // stand in until LDAP is giving a value for this field.
3948 if (!(empty($value) && $lockval === 'unlockedifempty')) {
3949 if ($iscustom || (in_array($key, $userauth->userfields) &&
3950 ((string)$oldinfo->$key !== (string)$value))) {
3951 $newuser[$key] = (string)$value;
3956 if ($newuser) {
3957 $newuser['id'] = $oldinfo->id;
3958 $newuser['timemodified'] = time();
3959 user_update_user((object) $newuser, false, false);
3961 // Save user profile data.
3962 profile_save_data((object) $newuser);
3964 // Trigger event.
3965 \core\event\user_updated::create_from_userid($newuser['id'])->trigger();
3969 return get_complete_user_data('id', $oldinfo->id);
3973 * Will truncate userinfo as it comes from auth_get_userinfo (from external auth) which may have large fields.
3975 * @param array $info Array of user properties to truncate if needed
3976 * @return array The now truncated information that was passed in
3978 function truncate_userinfo(array $info) {
3979 // Define the limits.
3980 $limit = array(
3981 'username' => 100,
3982 'idnumber' => 255,
3983 'firstname' => 100,
3984 'lastname' => 100,
3985 'email' => 100,
3986 'phone1' => 20,
3987 'phone2' => 20,
3988 'institution' => 255,
3989 'department' => 255,
3990 'address' => 255,
3991 'city' => 120,
3992 'country' => 2,
3995 // Apply where needed.
3996 foreach (array_keys($info) as $key) {
3997 if (!empty($limit[$key])) {
3998 $info[$key] = trim(core_text::substr($info[$key], 0, $limit[$key]));
4002 return $info;
4006 * Marks user deleted in internal user database and notifies the auth plugin.
4007 * Also unenrols user from all roles and does other cleanup.
4009 * Any plugin that needs to purge user data should register the 'user_deleted' event.
4011 * @param stdClass $user full user object before delete
4012 * @return boolean success
4013 * @throws coding_exception if invalid $user parameter detected
4015 function delete_user(stdClass $user) {
4016 global $CFG, $DB, $SESSION;
4017 require_once($CFG->libdir.'/grouplib.php');
4018 require_once($CFG->libdir.'/gradelib.php');
4019 require_once($CFG->dirroot.'/message/lib.php');
4020 require_once($CFG->dirroot.'/user/lib.php');
4022 // Make sure nobody sends bogus record type as parameter.
4023 if (!property_exists($user, 'id') or !property_exists($user, 'username')) {
4024 throw new coding_exception('Invalid $user parameter in delete_user() detected');
4027 // Better not trust the parameter and fetch the latest info this will be very expensive anyway.
4028 if (!$user = $DB->get_record('user', array('id' => $user->id))) {
4029 debugging('Attempt to delete unknown user account.');
4030 return false;
4033 // There must be always exactly one guest record, originally the guest account was identified by username only,
4034 // now we use $CFG->siteguest for performance reasons.
4035 if ($user->username === 'guest' or isguestuser($user)) {
4036 debugging('Guest user account can not be deleted.');
4037 return false;
4040 // Admin can be theoretically from different auth plugin, but we want to prevent deletion of internal accoutns only,
4041 // if anything goes wrong ppl may force somebody to be admin via config.php setting $CFG->siteadmins.
4042 if ($user->auth === 'manual' and is_siteadmin($user)) {
4043 debugging('Local administrator accounts can not be deleted.');
4044 return false;
4047 // Allow plugins to use this user object before we completely delete it.
4048 if ($pluginsfunction = get_plugins_with_function('pre_user_delete')) {
4049 foreach ($pluginsfunction as $plugintype => $plugins) {
4050 foreach ($plugins as $pluginfunction) {
4051 $pluginfunction($user);
4056 // Keep user record before updating it, as we have to pass this to user_deleted event.
4057 $olduser = clone $user;
4059 // Keep a copy of user context, we need it for event.
4060 $usercontext = context_user::instance($user->id);
4062 // Remove user from communication rooms immediately.
4063 if (core_communication\api::is_available()) {
4064 foreach (enrol_get_users_courses($user->id) as $course) {
4065 $communication = \core_communication\processor::load_by_instance(
4066 'core_course',
4067 'coursecommunication',
4068 $course->id
4070 $communication->get_room_user_provider()->remove_members_from_room([$user->id]);
4071 $communication->delete_instance_user_mapping([$user->id]);
4075 // Delete all grades - backup is kept in grade_grades_history table.
4076 grade_user_delete($user->id);
4078 // TODO: remove from cohorts using standard API here.
4080 // Remove user tags.
4081 core_tag_tag::remove_all_item_tags('core', 'user', $user->id);
4083 // Unconditionally unenrol from all courses.
4084 enrol_user_delete($user);
4086 // Unenrol from all roles in all contexts.
4087 // This might be slow but it is really needed - modules might do some extra cleanup!
4088 role_unassign_all(array('userid' => $user->id));
4090 // Notify the competency subsystem.
4091 \core_competency\api::hook_user_deleted($user->id);
4093 // Now do a brute force cleanup.
4095 // Delete all user events and subscription events.
4096 $DB->delete_records_select('event', 'userid = :userid AND subscriptionid IS NOT NULL', ['userid' => $user->id]);
4098 // Now, delete all calendar subscription from the user.
4099 $DB->delete_records('event_subscriptions', ['userid' => $user->id]);
4101 // Remove from all cohorts.
4102 $DB->delete_records('cohort_members', array('userid' => $user->id));
4104 // Remove from all groups.
4105 $DB->delete_records('groups_members', array('userid' => $user->id));
4107 // Brute force unenrol from all courses.
4108 $DB->delete_records('user_enrolments', array('userid' => $user->id));
4110 // Purge user preferences.
4111 $DB->delete_records('user_preferences', array('userid' => $user->id));
4113 // Purge user extra profile info.
4114 $DB->delete_records('user_info_data', array('userid' => $user->id));
4116 // Purge log of previous password hashes.
4117 $DB->delete_records('user_password_history', array('userid' => $user->id));
4119 // Last course access not necessary either.
4120 $DB->delete_records('user_lastaccess', array('userid' => $user->id));
4121 // Remove all user tokens.
4122 $DB->delete_records('external_tokens', array('userid' => $user->id));
4124 // Unauthorise the user for all services.
4125 $DB->delete_records('external_services_users', array('userid' => $user->id));
4127 // Remove users private keys.
4128 $DB->delete_records('user_private_key', array('userid' => $user->id));
4130 // Remove users customised pages.
4131 $DB->delete_records('my_pages', array('userid' => $user->id, 'private' => 1));
4133 // Remove user's oauth2 refresh tokens, if present.
4134 $DB->delete_records('oauth2_refresh_token', array('userid' => $user->id));
4136 // Delete user from $SESSION->bulk_users.
4137 if (isset($SESSION->bulk_users[$user->id])) {
4138 unset($SESSION->bulk_users[$user->id]);
4141 // Force logout - may fail if file based sessions used, sorry.
4142 \core\session\manager::kill_user_sessions($user->id);
4144 // Generate username from email address, or a fake email.
4145 $delemail = !empty($user->email) ? $user->email : $user->username . '.' . $user->id . '@unknownemail.invalid';
4147 $deltime = time();
4148 $deltimelength = core_text::strlen((string) $deltime);
4150 // Max username length is 100 chars. Select up to limit - (length of current time + 1 [period character]) from users email.
4151 $delname = clean_param($delemail, PARAM_USERNAME);
4152 $delname = core_text::substr($delname, 0, 100 - ($deltimelength + 1)) . ".{$deltime}";
4154 // Workaround for bulk deletes of users with the same email address.
4155 while ($DB->record_exists('user', array('username' => $delname))) { // No need to use mnethostid here.
4156 $delname++;
4159 // Mark internal user record as "deleted".
4160 $updateuser = new stdClass();
4161 $updateuser->id = $user->id;
4162 $updateuser->deleted = 1;
4163 $updateuser->username = $delname; // Remember it just in case.
4164 $updateuser->email = md5($user->username);// Store hash of username, useful importing/restoring users.
4165 $updateuser->idnumber = ''; // Clear this field to free it up.
4166 $updateuser->picture = 0;
4167 $updateuser->timemodified = $deltime;
4169 // Don't trigger update event, as user is being deleted.
4170 user_update_user($updateuser, false, false);
4172 // Delete all content associated with the user context, but not the context itself.
4173 $usercontext->delete_content();
4175 // Delete any search data.
4176 \core_search\manager::context_deleted($usercontext);
4178 // Any plugin that needs to cleanup should register this event.
4179 // Trigger event.
4180 $event = \core\event\user_deleted::create(
4181 array(
4182 'objectid' => $user->id,
4183 'relateduserid' => $user->id,
4184 'context' => $usercontext,
4185 'other' => array(
4186 'username' => $user->username,
4187 'email' => $user->email,
4188 'idnumber' => $user->idnumber,
4189 'picture' => $user->picture,
4190 'mnethostid' => $user->mnethostid
4194 $event->add_record_snapshot('user', $olduser);
4195 $event->trigger();
4197 // We will update the user's timemodified, as it will be passed to the user_deleted event, which
4198 // should know about this updated property persisted to the user's table.
4199 $user->timemodified = $updateuser->timemodified;
4201 // Notify auth plugin - do not block the delete even when plugin fails.
4202 $authplugin = get_auth_plugin($user->auth);
4203 $authplugin->user_delete($user);
4205 return true;
4209 * Retrieve the guest user object.
4211 * @return stdClass A {@link $USER} object
4213 function guest_user() {
4214 global $CFG, $DB;
4216 if ($newuser = $DB->get_record('user', array('id' => $CFG->siteguest))) {
4217 $newuser->confirmed = 1;
4218 $newuser->lang = get_newuser_language();
4219 $newuser->lastip = getremoteaddr();
4222 return $newuser;
4226 * Authenticates a user against the chosen authentication mechanism
4228 * Given a username and password, this function looks them
4229 * up using the currently selected authentication mechanism,
4230 * and if the authentication is successful, it returns a
4231 * valid $user object from the 'user' table.
4233 * Uses auth_ functions from the currently active auth module
4235 * After authenticate_user_login() returns success, you will need to
4236 * log that the user has logged in, and call complete_user_login() to set
4237 * the session up.
4239 * Note: this function works only with non-mnet accounts!
4241 * @param string $username User's username (or also email if $CFG->authloginviaemail enabled)
4242 * @param string $password User's password
4243 * @param bool $ignorelockout useful when guessing is prevented by other mechanism such as captcha or SSO
4244 * @param int $failurereason login failure reason, can be used in renderers (it may disclose if account exists)
4245 * @param mixed logintoken If this is set to a string it is validated against the login token for the session.
4246 * @return stdClass|false A {@link $USER} object or false if error
4248 function authenticate_user_login($username, $password, $ignorelockout=false, &$failurereason=null, $logintoken=false) {
4249 global $CFG, $DB, $PAGE;
4250 require_once("$CFG->libdir/authlib.php");
4252 if ($user = get_complete_user_data('username', $username, $CFG->mnet_localhost_id)) {
4253 // we have found the user
4255 } else if (!empty($CFG->authloginviaemail)) {
4256 if ($email = clean_param($username, PARAM_EMAIL)) {
4257 $select = "mnethostid = :mnethostid AND LOWER(email) = LOWER(:email) AND deleted = 0";
4258 $params = array('mnethostid' => $CFG->mnet_localhost_id, 'email' => $email);
4259 $users = $DB->get_records_select('user', $select, $params, 'id', 'id', 0, 2);
4260 if (count($users) === 1) {
4261 // Use email for login only if unique.
4262 $user = reset($users);
4263 $user = get_complete_user_data('id', $user->id);
4264 $username = $user->username;
4266 unset($users);
4270 // Make sure this request came from the login form.
4271 if (!\core\session\manager::validate_login_token($logintoken)) {
4272 $failurereason = AUTH_LOGIN_FAILED;
4274 // Trigger login failed event (specifying the ID of the found user, if available).
4275 \core\event\user_login_failed::create([
4276 'userid' => ($user->id ?? 0),
4277 'other' => [
4278 'username' => $username,
4279 'reason' => $failurereason,
4281 ])->trigger();
4283 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Invalid Login Token: $username ".$_SERVER['HTTP_USER_AGENT']);
4284 return false;
4287 $authsenabled = get_enabled_auth_plugins();
4289 if ($user) {
4290 // Use manual if auth not set.
4291 $auth = empty($user->auth) ? 'manual' : $user->auth;
4293 if (in_array($user->auth, $authsenabled)) {
4294 $authplugin = get_auth_plugin($user->auth);
4295 $authplugin->pre_user_login_hook($user);
4298 if (!empty($user->suspended)) {
4299 $failurereason = AUTH_LOGIN_SUSPENDED;
4301 // Trigger login failed event.
4302 $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4303 'other' => array('username' => $username, 'reason' => $failurereason)));
4304 $event->trigger();
4305 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Suspended Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4306 return false;
4308 if ($auth=='nologin' or !is_enabled_auth($auth)) {
4309 // Legacy way to suspend user.
4310 $failurereason = AUTH_LOGIN_SUSPENDED;
4312 // Trigger login failed event.
4313 $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4314 'other' => array('username' => $username, 'reason' => $failurereason)));
4315 $event->trigger();
4316 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Disabled Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4317 return false;
4319 $auths = array($auth);
4321 } else {
4322 // Check if there's a deleted record (cheaply), this should not happen because we mangle usernames in delete_user().
4323 if ($DB->get_field('user', 'id', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id, 'deleted' => 1))) {
4324 $failurereason = AUTH_LOGIN_NOUSER;
4326 // Trigger login failed event.
4327 $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4328 'reason' => $failurereason)));
4329 $event->trigger();
4330 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Deleted Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4331 return false;
4334 // User does not exist.
4335 $auths = $authsenabled;
4336 $user = new stdClass();
4337 $user->id = 0;
4340 if ($ignorelockout) {
4341 // Some other mechanism protects against brute force password guessing, for example login form might include reCAPTCHA
4342 // or this function is called from a SSO script.
4343 } else if ($user->id) {
4344 // Verify login lockout after other ways that may prevent user login.
4345 if (login_is_lockedout($user)) {
4346 $failurereason = AUTH_LOGIN_LOCKOUT;
4348 // Trigger login failed event.
4349 $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4350 'other' => array('username' => $username, 'reason' => $failurereason)));
4351 $event->trigger();
4353 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Login lockout: $username ".$_SERVER['HTTP_USER_AGENT']);
4354 return false;
4356 } else {
4357 // We can not lockout non-existing accounts.
4360 foreach ($auths as $auth) {
4361 $authplugin = get_auth_plugin($auth);
4363 // On auth fail fall through to the next plugin.
4364 if (!$authplugin->user_login($username, $password)) {
4365 continue;
4368 // Before performing login actions, check if user still passes password policy, if admin setting is enabled.
4369 if (!empty($CFG->passwordpolicycheckonlogin)) {
4370 $errmsg = '';
4371 $passed = check_password_policy($password, $errmsg, $user);
4372 if (!$passed) {
4373 // First trigger event for failure.
4374 $failedevent = \core\event\user_password_policy_failed::create_from_user($user);
4375 $failedevent->trigger();
4377 // If able to change password, set flag and move on.
4378 if ($authplugin->can_change_password()) {
4379 // Check if we are on internal change password page, or service is external, don't show notification.
4380 $internalchangeurl = new moodle_url('/login/change_password.php');
4381 if (!($PAGE->has_set_url() && $internalchangeurl->compare($PAGE->url)) && $authplugin->is_internal()) {
4382 \core\notification::error(get_string('passwordpolicynomatch', '', $errmsg));
4384 set_user_preference('auth_forcepasswordchange', 1, $user);
4385 } else if ($authplugin->can_reset_password()) {
4386 // Else force a reset if possible.
4387 \core\notification::error(get_string('forcepasswordresetnotice', '', $errmsg));
4388 redirect(new moodle_url('/login/forgot_password.php'));
4389 } else {
4390 $notifymsg = get_string('forcepasswordresetfailurenotice', '', $errmsg);
4391 // If support page is set, add link for help.
4392 if (!empty($CFG->supportpage)) {
4393 $link = \html_writer::link($CFG->supportpage, $CFG->supportpage);
4394 $link = \html_writer::tag('p', $link);
4395 $notifymsg .= $link;
4398 // If no change or reset is possible, add a notification for user.
4399 \core\notification::error($notifymsg);
4404 // Successful authentication.
4405 if ($user->id) {
4406 // User already exists in database.
4407 if (empty($user->auth)) {
4408 // For some reason auth isn't set yet.
4409 $DB->set_field('user', 'auth', $auth, array('id' => $user->id));
4410 $user->auth = $auth;
4413 // If the existing hash is using an out-of-date algorithm (or the legacy md5 algorithm), then we should update to
4414 // the current hash algorithm while we have access to the user's password.
4415 update_internal_user_password($user, $password);
4417 if ($authplugin->is_synchronised_with_external()) {
4418 // Update user record from external DB.
4419 $user = update_user_record_by_id($user->id);
4421 } else {
4422 // The user is authenticated but user creation may be disabled.
4423 if (!empty($CFG->authpreventaccountcreation)) {
4424 $failurereason = AUTH_LOGIN_UNAUTHORISED;
4426 // Trigger login failed event.
4427 $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4428 'reason' => $failurereason)));
4429 $event->trigger();
4431 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Unknown user, can not create new accounts: $username ".
4432 $_SERVER['HTTP_USER_AGENT']);
4433 return false;
4434 } else {
4435 $user = create_user_record($username, $password, $auth);
4439 $authplugin->sync_roles($user);
4441 foreach ($authsenabled as $hau) {
4442 $hauth = get_auth_plugin($hau);
4443 $hauth->user_authenticated_hook($user, $username, $password);
4446 if (empty($user->id)) {
4447 $failurereason = AUTH_LOGIN_NOUSER;
4448 // Trigger login failed event.
4449 $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4450 'reason' => $failurereason)));
4451 $event->trigger();
4452 return false;
4455 if (!empty($user->suspended)) {
4456 // Just in case some auth plugin suspended account.
4457 $failurereason = AUTH_LOGIN_SUSPENDED;
4458 // Trigger login failed event.
4459 $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4460 'other' => array('username' => $username, 'reason' => $failurereason)));
4461 $event->trigger();
4462 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Suspended Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4463 return false;
4466 login_attempt_valid($user);
4467 $failurereason = AUTH_LOGIN_OK;
4468 return $user;
4471 // Failed if all the plugins have failed.
4472 if (debugging('', DEBUG_ALL)) {
4473 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Failed Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4476 if ($user->id) {
4477 login_attempt_failed($user);
4478 $failurereason = AUTH_LOGIN_FAILED;
4479 // Trigger login failed event.
4480 $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4481 'other' => array('username' => $username, 'reason' => $failurereason)));
4482 $event->trigger();
4483 } else {
4484 $failurereason = AUTH_LOGIN_NOUSER;
4485 // Trigger login failed event.
4486 $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4487 'reason' => $failurereason)));
4488 $event->trigger();
4491 return false;
4495 * Call to complete the user login process after authenticate_user_login()
4496 * has succeeded. It will setup the $USER variable and other required bits
4497 * and pieces.
4499 * NOTE:
4500 * - It will NOT log anything -- up to the caller to decide what to log.
4501 * - this function does not set any cookies any more!
4503 * @param stdClass $user
4504 * @param array $extrauserinfo
4505 * @return stdClass A {@link $USER} object - BC only, do not use
4507 function complete_user_login($user, array $extrauserinfo = []) {
4508 global $CFG, $DB, $USER, $SESSION;
4510 \core\session\manager::login_user($user);
4512 // Reload preferences from DB.
4513 unset($USER->preference);
4514 check_user_preferences_loaded($USER);
4516 // Update login times.
4517 update_user_login_times();
4519 // Extra session prefs init.
4520 set_login_session_preferences();
4522 // Trigger login event.
4523 $event = \core\event\user_loggedin::create(
4524 array(
4525 'userid' => $USER->id,
4526 'objectid' => $USER->id,
4527 'other' => [
4528 'username' => $USER->username,
4529 'extrauserinfo' => $extrauserinfo
4533 $event->trigger();
4535 // Check if the user is using a new browser or session (a new MoodleSession cookie is set in that case).
4536 // If the user is accessing from the same IP, ignore everything (most of the time will be a new session in the same browser).
4537 // Skip Web Service requests, CLI scripts, AJAX scripts, and request from the mobile app itself.
4538 $loginip = getremoteaddr();
4539 $isnewip = isset($SESSION->userpreviousip) && $SESSION->userpreviousip != $loginip;
4540 $isvalidenv = (!WS_SERVER && !CLI_SCRIPT && !NO_MOODLE_COOKIES) || PHPUNIT_TEST;
4542 if (!empty($SESSION->isnewsessioncookie) && $isnewip && $isvalidenv && !\core_useragent::is_moodle_app()) {
4544 $logintime = time();
4545 $ismoodleapp = false;
4546 $useragent = \core_useragent::get_user_agent_string();
4548 // Schedule adhoc task to sent a login notification to the user.
4549 $task = new \core\task\send_login_notifications();
4550 $task->set_userid($USER->id);
4551 $task->set_custom_data(compact('ismoodleapp', 'useragent', 'loginip', 'logintime'));
4552 $task->set_component('core');
4553 \core\task\manager::queue_adhoc_task($task);
4556 // Queue migrating the messaging data, if we need to.
4557 if (!get_user_preferences('core_message_migrate_data', false, $USER->id)) {
4558 // Check if there are any legacy messages to migrate.
4559 if (\core_message\helper::legacy_messages_exist($USER->id)) {
4560 \core_message\task\migrate_message_data::queue_task($USER->id);
4561 } else {
4562 set_user_preference('core_message_migrate_data', true, $USER->id);
4566 if (isguestuser()) {
4567 // No need to continue when user is THE guest.
4568 return $USER;
4571 if (CLI_SCRIPT) {
4572 // We can redirect to password change URL only in browser.
4573 return $USER;
4576 // Select password change url.
4577 $userauth = get_auth_plugin($USER->auth);
4579 // Check whether the user should be changing password.
4580 if (get_user_preferences('auth_forcepasswordchange', false)) {
4581 if ($userauth->can_change_password()) {
4582 if ($changeurl = $userauth->change_password_url()) {
4583 redirect($changeurl);
4584 } else {
4585 require_once($CFG->dirroot . '/login/lib.php');
4586 $SESSION->wantsurl = core_login_get_return_url();
4587 redirect($CFG->wwwroot.'/login/change_password.php');
4589 } else {
4590 throw new \moodle_exception('nopasswordchangeforced', 'auth');
4593 return $USER;
4597 * Check a password hash to see if it was hashed using the legacy hash algorithm (md5).
4599 * @param string $password String to check.
4600 * @return boolean True if the $password matches the format of an md5 sum.
4602 function password_is_legacy_hash($password) {
4603 return (bool) preg_match('/^[0-9a-f]{32}$/', $password);
4607 * Compare password against hash stored in user object to determine if it is valid.
4609 * If necessary it also updates the stored hash to the current format.
4611 * @param stdClass $user (Password property may be updated).
4612 * @param string $password Plain text password.
4613 * @return bool True if password is valid.
4615 function validate_internal_user_password($user, $password) {
4616 global $CFG;
4618 if ($user->password === AUTH_PASSWORD_NOT_CACHED) {
4619 // Internal password is not used at all, it can not validate.
4620 return false;
4623 // If hash isn't a legacy (md5) hash, validate using the library function.
4624 if (!password_is_legacy_hash($user->password)) {
4625 return password_verify($password, $user->password);
4628 // Otherwise we need to check for a legacy (md5) hash instead. If the hash
4629 // is valid we can then update it to the new algorithm.
4631 $sitesalt = isset($CFG->passwordsaltmain) ? $CFG->passwordsaltmain : '';
4632 $validated = false;
4634 if ($user->password === md5($password.$sitesalt)
4635 or $user->password === md5($password)
4636 or $user->password === md5(addslashes($password).$sitesalt)
4637 or $user->password === md5(addslashes($password))) {
4638 // Note: we are intentionally using the addslashes() here because we
4639 // need to accept old password hashes of passwords with magic quotes.
4640 $validated = true;
4642 } else {
4643 for ($i=1; $i<=20; $i++) { // 20 alternative salts should be enough, right?
4644 $alt = 'passwordsaltalt'.$i;
4645 if (!empty($CFG->$alt)) {
4646 if ($user->password === md5($password.$CFG->$alt) or $user->password === md5(addslashes($password).$CFG->$alt)) {
4647 $validated = true;
4648 break;
4654 if ($validated) {
4655 // If the password matches the existing md5 hash, update to the
4656 // current hash algorithm while we have access to the user's password.
4657 update_internal_user_password($user, $password);
4660 return $validated;
4664 * Calculate hash for a plain text password.
4666 * @param string $password Plain text password to be hashed.
4667 * @param bool $fasthash If true, use a low cost factor when generating the hash
4668 * This is much faster to generate but makes the hash
4669 * less secure. It is used when lots of hashes need to
4670 * be generated quickly.
4671 * @return string The hashed password.
4673 * @throws moodle_exception If a problem occurs while generating the hash.
4675 function hash_internal_user_password($password, $fasthash = false) {
4676 global $CFG;
4678 // Set the cost factor to 4 for fast hashing, otherwise use default cost.
4679 $options = ($fasthash) ? array('cost' => 4) : array();
4681 $generatedhash = password_hash($password, PASSWORD_DEFAULT, $options);
4683 if ($generatedhash === false || $generatedhash === null) {
4684 throw new moodle_exception('Failed to generate password hash.');
4687 return $generatedhash;
4691 * Update password hash in user object (if necessary).
4693 * The password is updated if:
4694 * 1. The password has changed (the hash of $user->password is different
4695 * to the hash of $password).
4696 * 2. The existing hash is using an out-of-date algorithm (or the legacy
4697 * md5 algorithm).
4699 * Updating the password will modify the $user object and the database
4700 * record to use the current hashing algorithm.
4701 * It will remove Web Services user tokens too.
4703 * @param stdClass $user User object (password property may be updated).
4704 * @param string $password Plain text password.
4705 * @param bool $fasthash If true, use a low cost factor when generating the hash
4706 * This is much faster to generate but makes the hash
4707 * less secure. It is used when lots of hashes need to
4708 * be generated quickly.
4709 * @return bool Always returns true.
4711 function update_internal_user_password($user, $password, $fasthash = false) {
4712 global $CFG, $DB;
4714 // Figure out what the hashed password should be.
4715 if (!isset($user->auth)) {
4716 debugging('User record in update_internal_user_password() must include field auth',
4717 DEBUG_DEVELOPER);
4718 $user->auth = $DB->get_field('user', 'auth', array('id' => $user->id));
4720 $authplugin = get_auth_plugin($user->auth);
4721 if ($authplugin->prevent_local_passwords()) {
4722 $hashedpassword = AUTH_PASSWORD_NOT_CACHED;
4723 } else {
4724 $hashedpassword = hash_internal_user_password($password, $fasthash);
4727 $algorithmchanged = false;
4729 if ($hashedpassword === AUTH_PASSWORD_NOT_CACHED) {
4730 // Password is not cached, update it if not set to AUTH_PASSWORD_NOT_CACHED.
4731 $passwordchanged = ($user->password !== $hashedpassword);
4733 } else if (isset($user->password)) {
4734 // If verification fails then it means the password has changed.
4735 $passwordchanged = !password_verify($password, $user->password);
4736 $algorithmchanged = password_needs_rehash($user->password, PASSWORD_DEFAULT);
4737 } else {
4738 // While creating new user, password in unset in $user object, to avoid
4739 // saving it with user_create()
4740 $passwordchanged = true;
4743 if ($passwordchanged || $algorithmchanged) {
4744 $DB->set_field('user', 'password', $hashedpassword, array('id' => $user->id));
4745 $user->password = $hashedpassword;
4747 // Trigger event.
4748 $user = $DB->get_record('user', array('id' => $user->id));
4749 \core\event\user_password_updated::create_from_user($user)->trigger();
4751 // Remove WS user tokens.
4752 if (!empty($CFG->passwordchangetokendeletion)) {
4753 require_once($CFG->dirroot.'/webservice/lib.php');
4754 webservice::delete_user_ws_tokens($user->id);
4758 return true;
4762 * Get a complete user record, which includes all the info in the user record.
4764 * Intended for setting as $USER session variable
4766 * @param string $field The user field to be checked for a given value.
4767 * @param string $value The value to match for $field.
4768 * @param int $mnethostid
4769 * @param bool $throwexception If true, it will throw an exception when there's no record found or when there are multiple records
4770 * found. Otherwise, it will just return false.
4771 * @return mixed False, or A {@link $USER} object.
4773 function get_complete_user_data($field, $value, $mnethostid = null, $throwexception = false) {
4774 global $CFG, $DB;
4776 if (!$field || !$value) {
4777 return false;
4780 // Change the field to lowercase.
4781 $field = core_text::strtolower($field);
4783 // List of case insensitive fields.
4784 $caseinsensitivefields = ['email'];
4786 // Username input is forced to lowercase and should be case sensitive.
4787 if ($field == 'username') {
4788 $value = core_text::strtolower($value);
4791 // Build the WHERE clause for an SQL query.
4792 $params = array('fieldval' => $value);
4794 // Do a case-insensitive query, if necessary. These are generally very expensive. The performance can be improved on some DBs
4795 // such as MySQL by pre-filtering users with accent-insensitive subselect.
4796 if (in_array($field, $caseinsensitivefields)) {
4797 $fieldselect = $DB->sql_equal($field, ':fieldval', false);
4798 $idsubselect = $DB->sql_equal($field, ':fieldval2', false, false);
4799 $params['fieldval2'] = $value;
4800 } else {
4801 $fieldselect = "$field = :fieldval";
4802 $idsubselect = '';
4804 $constraints = "$fieldselect AND deleted <> 1";
4806 // If we are loading user data based on anything other than id,
4807 // we must also restrict our search based on mnet host.
4808 if ($field != 'id') {
4809 if (empty($mnethostid)) {
4810 // If empty, we restrict to local users.
4811 $mnethostid = $CFG->mnet_localhost_id;
4814 if (!empty($mnethostid)) {
4815 $params['mnethostid'] = $mnethostid;
4816 $constraints .= " AND mnethostid = :mnethostid";
4819 if ($idsubselect) {
4820 $constraints .= " AND id IN (SELECT id FROM {user} WHERE {$idsubselect})";
4823 // Get all the basic user data.
4824 try {
4825 // Make sure that there's only a single record that matches our query.
4826 // For example, when fetching by email, multiple records might match the query as there's no guarantee that email addresses
4827 // are unique. Therefore we can't reliably tell whether the user profile data that we're fetching is the correct one.
4828 $user = $DB->get_record_select('user', $constraints, $params, '*', MUST_EXIST);
4829 } catch (dml_exception $exception) {
4830 if ($throwexception) {
4831 throw $exception;
4832 } else {
4833 // Return false when no records or multiple records were found.
4834 return false;
4838 // Get various settings and preferences.
4840 // Preload preference cache.
4841 check_user_preferences_loaded($user);
4843 // Load course enrolment related stuff.
4844 $user->lastcourseaccess = array(); // During last session.
4845 $user->currentcourseaccess = array(); // During current session.
4846 if ($lastaccesses = $DB->get_records('user_lastaccess', array('userid' => $user->id))) {
4847 foreach ($lastaccesses as $lastaccess) {
4848 $user->lastcourseaccess[$lastaccess->courseid] = $lastaccess->timeaccess;
4852 // Add cohort theme.
4853 if (!empty($CFG->allowcohortthemes)) {
4854 require_once($CFG->dirroot . '/cohort/lib.php');
4855 if ($cohorttheme = cohort_get_user_cohort_theme($user->id)) {
4856 $user->cohorttheme = $cohorttheme;
4860 // Add the custom profile fields to the user record.
4861 $user->profile = array();
4862 if (!isguestuser($user)) {
4863 require_once($CFG->dirroot.'/user/profile/lib.php');
4864 profile_load_custom_fields($user);
4867 // Rewrite some variables if necessary.
4868 if (!empty($user->description)) {
4869 // No need to cart all of it around.
4870 $user->description = true;
4872 if (isguestuser($user)) {
4873 // Guest language always same as site.
4874 $user->lang = get_newuser_language();
4875 // Name always in current language.
4876 $user->firstname = get_string('guestuser');
4877 $user->lastname = ' ';
4880 return $user;
4884 * Validate a password against the configured password policy
4886 * @param string $password the password to be checked against the password policy
4887 * @param string $errmsg the error message to display when the password doesn't comply with the policy.
4888 * @param stdClass $user the user object to perform password validation against. Defaults to null if not provided.
4890 * @return bool true if the password is valid according to the policy. false otherwise.
4892 function check_password_policy($password, &$errmsg, $user = null) {
4893 global $CFG;
4895 if (!empty($CFG->passwordpolicy)) {
4896 $errmsg = '';
4897 if (core_text::strlen($password) < $CFG->minpasswordlength) {
4898 $errmsg .= '<div>'. get_string('errorminpasswordlength', 'auth', $CFG->minpasswordlength) .'</div>';
4900 if (preg_match_all('/[[:digit:]]/u', $password, $matches) < $CFG->minpassworddigits) {
4901 $errmsg .= '<div>'. get_string('errorminpassworddigits', 'auth', $CFG->minpassworddigits) .'</div>';
4903 if (preg_match_all('/[[:lower:]]/u', $password, $matches) < $CFG->minpasswordlower) {
4904 $errmsg .= '<div>'. get_string('errorminpasswordlower', 'auth', $CFG->minpasswordlower) .'</div>';
4906 if (preg_match_all('/[[:upper:]]/u', $password, $matches) < $CFG->minpasswordupper) {
4907 $errmsg .= '<div>'. get_string('errorminpasswordupper', 'auth', $CFG->minpasswordupper) .'</div>';
4909 if (preg_match_all('/[^[:upper:][:lower:][:digit:]]/u', $password, $matches) < $CFG->minpasswordnonalphanum) {
4910 $errmsg .= '<div>'. get_string('errorminpasswordnonalphanum', 'auth', $CFG->minpasswordnonalphanum) .'</div>';
4912 if (!check_consecutive_identical_characters($password, $CFG->maxconsecutiveidentchars)) {
4913 $errmsg .= '<div>'. get_string('errormaxconsecutiveidentchars', 'auth', $CFG->maxconsecutiveidentchars) .'</div>';
4916 // Fire any additional password policy functions from plugins.
4917 // Plugin functions should output an error message string or empty string for success.
4918 $pluginsfunction = get_plugins_with_function('check_password_policy');
4919 foreach ($pluginsfunction as $plugintype => $plugins) {
4920 foreach ($plugins as $pluginfunction) {
4921 $pluginerr = $pluginfunction($password, $user);
4922 if ($pluginerr) {
4923 $errmsg .= '<div>'. $pluginerr .'</div>';
4929 if ($errmsg == '') {
4930 return true;
4931 } else {
4932 return false;
4938 * When logging in, this function is run to set certain preferences for the current SESSION.
4940 function set_login_session_preferences() {
4941 global $SESSION;
4943 $SESSION->justloggedin = true;
4945 unset($SESSION->lang);
4946 unset($SESSION->forcelang);
4947 unset($SESSION->load_navigation_admin);
4952 * Delete a course, including all related data from the database, and any associated files.
4954 * @param mixed $courseorid The id of the course or course object to delete.
4955 * @param bool $showfeedback Whether to display notifications of each action the function performs.
4956 * @return bool true if all the removals succeeded. false if there were any failures. If this
4957 * method returns false, some of the removals will probably have succeeded, and others
4958 * failed, but you have no way of knowing which.
4960 function delete_course($courseorid, $showfeedback = true) {
4961 global $DB, $CFG;
4963 if (is_object($courseorid)) {
4964 $courseid = $courseorid->id;
4965 $course = $courseorid;
4966 } else {
4967 $courseid = $courseorid;
4968 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
4969 return false;
4972 $context = context_course::instance($courseid);
4974 // Frontpage course can not be deleted!!
4975 if ($courseid == SITEID) {
4976 return false;
4979 // Allow plugins to use this course before we completely delete it.
4980 if ($pluginsfunction = get_plugins_with_function('pre_course_delete')) {
4981 foreach ($pluginsfunction as $plugintype => $plugins) {
4982 foreach ($plugins as $pluginfunction) {
4983 $pluginfunction($course);
4988 // Tell the search manager we are about to delete a course. This prevents us sending updates
4989 // for each individual context being deleted.
4990 \core_search\manager::course_deleting_start($courseid);
4992 $handler = core_course\customfield\course_handler::create();
4993 $handler->delete_instance($courseid);
4995 // Make the course completely empty.
4996 remove_course_contents($courseid, $showfeedback);
4998 // Delete the course and related context instance.
4999 context_helper::delete_instance(CONTEXT_COURSE, $courseid);
5001 // Communication provider delete associated information.
5002 $communication = \core_communication\api::load_by_instance(
5003 'core_course',
5004 'coursecommunication',
5005 $course->id
5008 // Update communication room membership of enrolled users.
5009 require_once($CFG->libdir . '/enrollib.php');
5010 $courseusers = enrol_get_course_users($courseid);
5011 $enrolledusers = [];
5013 foreach ($courseusers as $user) {
5014 $enrolledusers[] = $user->id;
5017 $communication->remove_members_from_room($enrolledusers);
5019 $communication->delete_room();
5021 $DB->delete_records("course", array("id" => $courseid));
5022 $DB->delete_records("course_format_options", array("courseid" => $courseid));
5024 // Reset all course related caches here.
5025 core_courseformat\base::reset_course_cache($courseid);
5027 // Tell search that we have deleted the course so it can delete course data from the index.
5028 \core_search\manager::course_deleting_finish($courseid);
5030 // Trigger a course deleted event.
5031 $event = \core\event\course_deleted::create(array(
5032 'objectid' => $course->id,
5033 'context' => $context,
5034 'other' => array(
5035 'shortname' => $course->shortname,
5036 'fullname' => $course->fullname,
5037 'idnumber' => $course->idnumber
5040 $event->add_record_snapshot('course', $course);
5041 $event->trigger();
5043 return true;
5047 * Clear a course out completely, deleting all content but don't delete the course itself.
5049 * This function does not verify any permissions.
5051 * Please note this function also deletes all user enrolments,
5052 * enrolment instances and role assignments by default.
5054 * $options:
5055 * - 'keep_roles_and_enrolments' - false by default
5056 * - 'keep_groups_and_groupings' - false by default
5058 * @param int $courseid The id of the course that is being deleted
5059 * @param bool $showfeedback Whether to display notifications of each action the function performs.
5060 * @param array $options extra options
5061 * @return bool true if all the removals succeeded. false if there were any failures. If this
5062 * method returns false, some of the removals will probably have succeeded, and others
5063 * failed, but you have no way of knowing which.
5065 function remove_course_contents($courseid, $showfeedback = true, array $options = null) {
5066 global $CFG, $DB, $OUTPUT;
5068 require_once($CFG->libdir.'/badgeslib.php');
5069 require_once($CFG->libdir.'/completionlib.php');
5070 require_once($CFG->libdir.'/questionlib.php');
5071 require_once($CFG->libdir.'/gradelib.php');
5072 require_once($CFG->dirroot.'/group/lib.php');
5073 require_once($CFG->dirroot.'/comment/lib.php');
5074 require_once($CFG->dirroot.'/rating/lib.php');
5075 require_once($CFG->dirroot.'/notes/lib.php');
5077 // Handle course badges.
5078 badges_handle_course_deletion($courseid);
5080 // NOTE: these concatenated strings are suboptimal, but it is just extra info...
5081 $strdeleted = get_string('deleted').' - ';
5083 // Some crazy wishlist of stuff we should skip during purging of course content.
5084 $options = (array)$options;
5086 $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
5087 $coursecontext = context_course::instance($courseid);
5088 $fs = get_file_storage();
5090 // Delete course completion information, this has to be done before grades and enrols.
5091 $cc = new completion_info($course);
5092 $cc->clear_criteria();
5093 if ($showfeedback) {
5094 echo $OUTPUT->notification($strdeleted.get_string('completion', 'completion'), 'notifysuccess');
5097 // Remove all data from gradebook - this needs to be done before course modules
5098 // because while deleting this information, the system may need to reference
5099 // the course modules that own the grades.
5100 remove_course_grades($courseid, $showfeedback);
5101 remove_grade_letters($coursecontext, $showfeedback);
5103 // Delete course blocks in any all child contexts,
5104 // they may depend on modules so delete them first.
5105 $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2.
5106 foreach ($childcontexts as $childcontext) {
5107 blocks_delete_all_for_context($childcontext->id);
5109 unset($childcontexts);
5110 blocks_delete_all_for_context($coursecontext->id);
5111 if ($showfeedback) {
5112 echo $OUTPUT->notification($strdeleted.get_string('type_block_plural', 'plugin'), 'notifysuccess');
5115 $DB->set_field('course_modules', 'deletioninprogress', '1', ['course' => $courseid]);
5116 rebuild_course_cache($courseid, true);
5118 // Get the list of all modules that are properly installed.
5119 $allmodules = $DB->get_records_menu('modules', array(), '', 'name, id');
5121 // Delete every instance of every module,
5122 // this has to be done before deleting of course level stuff.
5123 $locations = core_component::get_plugin_list('mod');
5124 foreach ($locations as $modname => $moddir) {
5125 if ($modname === 'NEWMODULE') {
5126 continue;
5128 if (array_key_exists($modname, $allmodules)) {
5129 $sql = "SELECT cm.*, m.id AS modinstance, m.name, '$modname' AS modname
5130 FROM {".$modname."} m
5131 LEFT JOIN {course_modules} cm ON cm.instance = m.id AND cm.module = :moduleid
5132 WHERE m.course = :courseid";
5133 $instances = $DB->get_records_sql($sql, array('courseid' => $course->id,
5134 'modulename' => $modname, 'moduleid' => $allmodules[$modname]));
5136 include_once("$moddir/lib.php"); // Shows php warning only if plugin defective.
5137 $moddelete = $modname .'_delete_instance'; // Delete everything connected to an instance.
5139 if ($instances) {
5140 foreach ($instances as $cm) {
5141 if ($cm->id) {
5142 // Delete activity context questions and question categories.
5143 question_delete_activity($cm);
5144 // Notify the competency subsystem.
5145 \core_competency\api::hook_course_module_deleted($cm);
5147 // Delete all tag instances associated with the instance of this module.
5148 core_tag_tag::delete_instances("mod_{$modname}", null, context_module::instance($cm->id)->id);
5149 core_tag_tag::remove_all_item_tags('core', 'course_modules', $cm->id);
5151 if (function_exists($moddelete)) {
5152 // This purges all module data in related tables, extra user prefs, settings, etc.
5153 $moddelete($cm->modinstance);
5154 } else {
5155 // NOTE: we should not allow installation of modules with missing delete support!
5156 debugging("Defective module '$modname' detected when deleting course contents: missing function $moddelete()!");
5157 $DB->delete_records($modname, array('id' => $cm->modinstance));
5160 if ($cm->id) {
5161 // Delete cm and its context - orphaned contexts are purged in cron in case of any race condition.
5162 context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
5163 $DB->delete_records('course_modules_completion', ['coursemoduleid' => $cm->id]);
5164 $DB->delete_records('course_modules_viewed', ['coursemoduleid' => $cm->id]);
5165 $DB->delete_records('course_modules', array('id' => $cm->id));
5166 rebuild_course_cache($cm->course, true);
5170 if ($instances and $showfeedback) {
5171 echo $OUTPUT->notification($strdeleted.get_string('pluginname', $modname), 'notifysuccess');
5173 } else {
5174 // Ooops, this module is not properly installed, force-delete it in the next block.
5178 // We have tried to delete everything the nice way - now let's force-delete any remaining module data.
5180 // Delete completion defaults.
5181 $DB->delete_records("course_completion_defaults", array("course" => $courseid));
5183 // Remove all data from availability and completion tables that is associated
5184 // with course-modules belonging to this course. Note this is done even if the
5185 // features are not enabled now, in case they were enabled previously.
5186 $DB->delete_records_subquery('course_modules_completion', 'coursemoduleid', 'id',
5187 'SELECT id from {course_modules} WHERE course = ?', [$courseid]);
5188 $DB->delete_records_subquery('course_modules_viewed', 'coursemoduleid', 'id',
5189 'SELECT id from {course_modules} WHERE course = ?', [$courseid]);
5191 // Remove course-module data that has not been removed in modules' _delete_instance callbacks.
5192 $cms = $DB->get_records('course_modules', array('course' => $course->id));
5193 $allmodulesbyid = array_flip($allmodules);
5194 foreach ($cms as $cm) {
5195 if (array_key_exists($cm->module, $allmodulesbyid)) {
5196 try {
5197 $DB->delete_records($allmodulesbyid[$cm->module], array('id' => $cm->instance));
5198 } catch (Exception $e) {
5199 // Ignore weird or missing table problems.
5202 context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
5203 $DB->delete_records('course_modules', array('id' => $cm->id));
5204 rebuild_course_cache($cm->course, true);
5207 if ($showfeedback) {
5208 echo $OUTPUT->notification($strdeleted.get_string('type_mod_plural', 'plugin'), 'notifysuccess');
5211 // Delete questions and question categories.
5212 question_delete_course($course);
5213 if ($showfeedback) {
5214 echo $OUTPUT->notification($strdeleted.get_string('questions', 'question'), 'notifysuccess');
5217 // Delete content bank contents.
5218 $cb = new \core_contentbank\contentbank();
5219 $cbdeleted = $cb->delete_contents($coursecontext);
5220 if ($showfeedback && $cbdeleted) {
5221 echo $OUTPUT->notification($strdeleted.get_string('contentbank', 'contentbank'), 'notifysuccess');
5224 // Make sure there are no subcontexts left - all valid blocks and modules should be already gone.
5225 $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2.
5226 foreach ($childcontexts as $childcontext) {
5227 $childcontext->delete();
5229 unset($childcontexts);
5231 // Remove roles and enrolments by default.
5232 if (empty($options['keep_roles_and_enrolments'])) {
5233 // This hack is used in restore when deleting contents of existing course.
5234 // During restore, we should remove only enrolment related data that the user performing the restore has a
5235 // permission to remove.
5236 $userid = $options['userid'] ?? null;
5237 enrol_course_delete($course, $userid);
5238 role_unassign_all(array('contextid' => $coursecontext->id, 'component' => ''), true);
5239 if ($showfeedback) {
5240 echo $OUTPUT->notification($strdeleted.get_string('type_enrol_plural', 'plugin'), 'notifysuccess');
5244 // Delete any groups, removing members and grouping/course links first.
5245 if (empty($options['keep_groups_and_groupings'])) {
5246 groups_delete_groupings($course->id, $showfeedback);
5247 groups_delete_groups($course->id, $showfeedback);
5250 // Filters be gone!
5251 filter_delete_all_for_context($coursecontext->id);
5253 // Notes, you shall not pass!
5254 note_delete_all($course->id);
5256 // Die comments!
5257 comment::delete_comments($coursecontext->id);
5259 // Ratings are history too.
5260 $delopt = new stdclass();
5261 $delopt->contextid = $coursecontext->id;
5262 $rm = new rating_manager();
5263 $rm->delete_ratings($delopt);
5265 // Delete course tags.
5266 core_tag_tag::remove_all_item_tags('core', 'course', $course->id);
5268 // Give the course format the opportunity to remove its obscure data.
5269 $format = course_get_format($course);
5270 $format->delete_format_data();
5272 // Notify the competency subsystem.
5273 \core_competency\api::hook_course_deleted($course);
5275 // Delete calendar events.
5276 $DB->delete_records('event', array('courseid' => $course->id));
5277 $fs->delete_area_files($coursecontext->id, 'calendar');
5279 // Delete all related records in other core tables that may have a courseid
5280 // This array stores the tables that need to be cleared, as
5281 // table_name => column_name that contains the course id.
5282 $tablestoclear = array(
5283 'backup_courses' => 'courseid', // Scheduled backup stuff.
5284 'user_lastaccess' => 'courseid', // User access info.
5286 foreach ($tablestoclear as $table => $col) {
5287 $DB->delete_records($table, array($col => $course->id));
5290 // Delete all course backup files.
5291 $fs->delete_area_files($coursecontext->id, 'backup');
5293 // Cleanup course record - remove links to deleted stuff.
5294 $oldcourse = new stdClass();
5295 $oldcourse->id = $course->id;
5296 $oldcourse->summary = '';
5297 $oldcourse->cacherev = 0;
5298 $oldcourse->legacyfiles = 0;
5299 if (!empty($options['keep_groups_and_groupings'])) {
5300 $oldcourse->defaultgroupingid = 0;
5302 $DB->update_record('course', $oldcourse);
5304 // Delete course sections.
5305 $DB->delete_records('course_sections', array('course' => $course->id));
5307 // Delete legacy, section and any other course files.
5308 $fs->delete_area_files($coursecontext->id, 'course'); // Files from summary and section.
5310 // Delete all remaining stuff linked to context such as files, comments, ratings, etc.
5311 if (empty($options['keep_roles_and_enrolments']) and empty($options['keep_groups_and_groupings'])) {
5312 // Easy, do not delete the context itself...
5313 $coursecontext->delete_content();
5314 } else {
5315 // Hack alert!!!!
5316 // We can not drop all context stuff because it would bork enrolments and roles,
5317 // there might be also files used by enrol plugins...
5320 // Delete legacy files - just in case some files are still left there after conversion to new file api,
5321 // also some non-standard unsupported plugins may try to store something there.
5322 fulldelete($CFG->dataroot.'/'.$course->id);
5324 // Delete from cache to reduce the cache size especially makes sense in case of bulk course deletion.
5325 course_modinfo::purge_course_cache($courseid);
5327 // Trigger a course content deleted event.
5328 $event = \core\event\course_content_deleted::create(array(
5329 'objectid' => $course->id,
5330 'context' => $coursecontext,
5331 'other' => array('shortname' => $course->shortname,
5332 'fullname' => $course->fullname,
5333 'options' => $options) // Passing this for legacy reasons.
5335 $event->add_record_snapshot('course', $course);
5336 $event->trigger();
5338 return true;
5342 * Change dates in module - used from course reset.
5344 * @param string $modname forum, assignment, etc
5345 * @param array $fields array of date fields from mod table
5346 * @param int $timeshift time difference
5347 * @param int $courseid
5348 * @param int $modid (Optional) passed if specific mod instance in course needs to be updated.
5349 * @return bool success
5351 function shift_course_mod_dates($modname, $fields, $timeshift, $courseid, $modid = 0) {
5352 global $CFG, $DB;
5353 include_once($CFG->dirroot.'/mod/'.$modname.'/lib.php');
5355 $return = true;
5356 $params = array($timeshift, $courseid);
5357 foreach ($fields as $field) {
5358 $updatesql = "UPDATE {".$modname."}
5359 SET $field = $field + ?
5360 WHERE course=? AND $field<>0";
5361 if ($modid) {
5362 $updatesql .= ' AND id=?';
5363 $params[] = $modid;
5365 $return = $DB->execute($updatesql, $params) && $return;
5368 return $return;
5372 * This function will empty a course of user data.
5373 * It will retain the activities and the structure of the course.
5375 * @param object $data an object containing all the settings including courseid (without magic quotes)
5376 * @return array status array of array component, item, error
5378 function reset_course_userdata($data) {
5379 global $CFG, $DB;
5380 require_once($CFG->libdir.'/gradelib.php');
5381 require_once($CFG->libdir.'/completionlib.php');
5382 require_once($CFG->dirroot.'/completion/criteria/completion_criteria_date.php');
5383 require_once($CFG->dirroot.'/group/lib.php');
5385 $data->courseid = $data->id;
5386 $context = context_course::instance($data->courseid);
5388 $eventparams = array(
5389 'context' => $context,
5390 'courseid' => $data->id,
5391 'other' => array(
5392 'reset_options' => (array) $data
5395 $event = \core\event\course_reset_started::create($eventparams);
5396 $event->trigger();
5398 // Calculate the time shift of dates.
5399 if (!empty($data->reset_start_date)) {
5400 // Time part of course startdate should be zero.
5401 $data->timeshift = $data->reset_start_date - usergetmidnight($data->reset_start_date_old);
5402 } else {
5403 $data->timeshift = 0;
5406 // Result array: component, item, error.
5407 $status = array();
5409 // Start the resetting.
5410 $componentstr = get_string('general');
5412 // Move the course start time.
5413 if (!empty($data->reset_start_date) and $data->timeshift) {
5414 // Change course start data.
5415 $DB->set_field('course', 'startdate', $data->reset_start_date, array('id' => $data->courseid));
5416 // Update all course and group events - do not move activity events.
5417 $updatesql = "UPDATE {event}
5418 SET timestart = timestart + ?
5419 WHERE courseid=? AND instance=0";
5420 $DB->execute($updatesql, array($data->timeshift, $data->courseid));
5422 // Update any date activity restrictions.
5423 if ($CFG->enableavailability) {
5424 \availability_date\condition::update_all_dates($data->courseid, $data->timeshift);
5427 // Update completion expected dates.
5428 if ($CFG->enablecompletion) {
5429 $modinfo = get_fast_modinfo($data->courseid);
5430 $changed = false;
5431 foreach ($modinfo->get_cms() as $cm) {
5432 if ($cm->completion && !empty($cm->completionexpected)) {
5433 $DB->set_field('course_modules', 'completionexpected', $cm->completionexpected + $data->timeshift,
5434 array('id' => $cm->id));
5435 $changed = true;
5439 // Clear course cache if changes made.
5440 if ($changed) {
5441 rebuild_course_cache($data->courseid, true);
5444 // Update course date completion criteria.
5445 \completion_criteria_date::update_date($data->courseid, $data->timeshift);
5448 $status[] = array('component' => $componentstr, 'item' => get_string('datechanged'), 'error' => false);
5451 if (!empty($data->reset_end_date)) {
5452 // If the user set a end date value respect it.
5453 $DB->set_field('course', 'enddate', $data->reset_end_date, array('id' => $data->courseid));
5454 } else if ($data->timeshift > 0 && $data->reset_end_date_old) {
5455 // If there is a time shift apply it to the end date as well.
5456 $enddate = $data->reset_end_date_old + $data->timeshift;
5457 $DB->set_field('course', 'enddate', $enddate, array('id' => $data->courseid));
5460 if (!empty($data->reset_events)) {
5461 $DB->delete_records('event', array('courseid' => $data->courseid));
5462 $status[] = array('component' => $componentstr, 'item' => get_string('deleteevents', 'calendar'), 'error' => false);
5465 if (!empty($data->reset_notes)) {
5466 require_once($CFG->dirroot.'/notes/lib.php');
5467 note_delete_all($data->courseid);
5468 $status[] = array('component' => $componentstr, 'item' => get_string('deletenotes', 'notes'), 'error' => false);
5471 if (!empty($data->delete_blog_associations)) {
5472 require_once($CFG->dirroot.'/blog/lib.php');
5473 blog_remove_associations_for_course($data->courseid);
5474 $status[] = array('component' => $componentstr, 'item' => get_string('deleteblogassociations', 'blog'), 'error' => false);
5477 if (!empty($data->reset_completion)) {
5478 // Delete course and activity completion information.
5479 $course = $DB->get_record('course', array('id' => $data->courseid));
5480 $cc = new completion_info($course);
5481 $cc->delete_all_completion_data();
5482 $status[] = array('component' => $componentstr,
5483 'item' => get_string('deletecompletiondata', 'completion'), 'error' => false);
5486 if (!empty($data->reset_competency_ratings)) {
5487 \core_competency\api::hook_course_reset_competency_ratings($data->courseid);
5488 $status[] = array('component' => $componentstr,
5489 'item' => get_string('deletecompetencyratings', 'core_competency'), 'error' => false);
5492 $componentstr = get_string('roles');
5494 if (!empty($data->reset_roles_overrides)) {
5495 $children = $context->get_child_contexts();
5496 foreach ($children as $child) {
5497 $child->delete_capabilities();
5499 $context->delete_capabilities();
5500 $status[] = array('component' => $componentstr, 'item' => get_string('deletecourseoverrides', 'role'), 'error' => false);
5503 if (!empty($data->reset_roles_local)) {
5504 $children = $context->get_child_contexts();
5505 foreach ($children as $child) {
5506 role_unassign_all(array('contextid' => $child->id));
5508 $status[] = array('component' => $componentstr, 'item' => get_string('deletelocalroles', 'role'), 'error' => false);
5511 // First unenrol users - this cleans some of related user data too, such as forum subscriptions, tracking, etc.
5512 $data->unenrolled = array();
5513 if (!empty($data->unenrol_users)) {
5514 $plugins = enrol_get_plugins(true);
5515 $instances = enrol_get_instances($data->courseid, true);
5516 foreach ($instances as $key => $instance) {
5517 if (!isset($plugins[$instance->enrol])) {
5518 unset($instances[$key]);
5519 continue;
5523 $usersroles = enrol_get_course_users_roles($data->courseid);
5524 foreach ($data->unenrol_users as $withroleid) {
5525 if ($withroleid) {
5526 $sql = "SELECT ue.*
5527 FROM {user_enrolments} ue
5528 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5529 JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5530 JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.roleid = :roleid AND ra.userid = ue.userid)";
5531 $params = array('courseid' => $data->courseid, 'roleid' => $withroleid, 'courselevel' => CONTEXT_COURSE);
5533 } else {
5534 // Without any role assigned at course context.
5535 $sql = "SELECT ue.*
5536 FROM {user_enrolments} ue
5537 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5538 JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5539 LEFT JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.userid = ue.userid)
5540 WHERE ra.id IS null";
5541 $params = array('courseid' => $data->courseid, 'courselevel' => CONTEXT_COURSE);
5544 $rs = $DB->get_recordset_sql($sql, $params);
5545 foreach ($rs as $ue) {
5546 if (!isset($instances[$ue->enrolid])) {
5547 continue;
5549 $instance = $instances[$ue->enrolid];
5550 $plugin = $plugins[$instance->enrol];
5551 if (!$plugin->allow_unenrol($instance) and !$plugin->allow_unenrol_user($instance, $ue)) {
5552 continue;
5555 if ($withroleid && count($usersroles[$ue->userid]) > 1) {
5556 // If we don't remove all roles and user has more than one role, just remove this role.
5557 role_unassign($withroleid, $ue->userid, $context->id);
5559 unset($usersroles[$ue->userid][$withroleid]);
5560 } else {
5561 // If we remove all roles or user has only one role, unenrol user from course.
5562 $plugin->unenrol_user($instance, $ue->userid);
5564 $data->unenrolled[$ue->userid] = $ue->userid;
5566 $rs->close();
5569 if (!empty($data->unenrolled)) {
5570 $status[] = array(
5571 'component' => $componentstr,
5572 'item' => get_string('unenrol', 'enrol').' ('.count($data->unenrolled).')',
5573 'error' => false
5577 $componentstr = get_string('groups');
5579 // Remove all group members.
5580 if (!empty($data->reset_groups_members)) {
5581 groups_delete_group_members($data->courseid);
5582 $status[] = array('component' => $componentstr, 'item' => get_string('removegroupsmembers', 'group'), 'error' => false);
5585 // Remove all groups.
5586 if (!empty($data->reset_groups_remove)) {
5587 groups_delete_groups($data->courseid, false);
5588 $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroups', 'group'), 'error' => false);
5591 // Remove all grouping members.
5592 if (!empty($data->reset_groupings_members)) {
5593 groups_delete_groupings_groups($data->courseid, false);
5594 $status[] = array('component' => $componentstr, 'item' => get_string('removegroupingsmembers', 'group'), 'error' => false);
5597 // Remove all groupings.
5598 if (!empty($data->reset_groupings_remove)) {
5599 groups_delete_groupings($data->courseid, false);
5600 $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroupings', 'group'), 'error' => false);
5603 // Look in every instance of every module for data to delete.
5604 $unsupportedmods = array();
5605 if ($allmods = $DB->get_records('modules') ) {
5606 foreach ($allmods as $mod) {
5607 $modname = $mod->name;
5608 $modfile = $CFG->dirroot.'/mod/'. $modname.'/lib.php';
5609 $moddeleteuserdata = $modname.'_reset_userdata'; // Function to delete user data.
5610 if (file_exists($modfile)) {
5611 if (!$DB->count_records($modname, array('course' => $data->courseid))) {
5612 continue; // Skip mods with no instances.
5614 include_once($modfile);
5615 if (function_exists($moddeleteuserdata)) {
5616 $modstatus = $moddeleteuserdata($data);
5617 if (is_array($modstatus)) {
5618 $status = array_merge($status, $modstatus);
5619 } else {
5620 debugging('Module '.$modname.' returned incorrect staus - must be an array!');
5622 } else {
5623 $unsupportedmods[] = $mod;
5625 } else {
5626 debugging('Missing lib.php in '.$modname.' module!');
5628 // Update calendar events for all modules.
5629 course_module_bulk_update_calendar_events($modname, $data->courseid);
5631 // Purge the course cache after resetting course start date. MDL-76936
5632 if ($data->timeshift) {
5633 course_modinfo::purge_course_cache($data->courseid);
5637 // Mention unsupported mods.
5638 if (!empty($unsupportedmods)) {
5639 foreach ($unsupportedmods as $mod) {
5640 $status[] = array(
5641 'component' => get_string('modulenameplural', $mod->name),
5642 'item' => '',
5643 'error' => get_string('resetnotimplemented')
5648 $componentstr = get_string('gradebook', 'grades');
5649 // Reset gradebook,.
5650 if (!empty($data->reset_gradebook_items)) {
5651 remove_course_grades($data->courseid, false);
5652 grade_grab_course_grades($data->courseid);
5653 grade_regrade_final_grades($data->courseid);
5654 $status[] = array('component' => $componentstr, 'item' => get_string('removeallcourseitems', 'grades'), 'error' => false);
5656 } else if (!empty($data->reset_gradebook_grades)) {
5657 grade_course_reset($data->courseid);
5658 $status[] = array('component' => $componentstr, 'item' => get_string('removeallcoursegrades', 'grades'), 'error' => false);
5660 // Reset comments.
5661 if (!empty($data->reset_comments)) {
5662 require_once($CFG->dirroot.'/comment/lib.php');
5663 comment::reset_course_page_comments($context);
5666 $event = \core\event\course_reset_ended::create($eventparams);
5667 $event->trigger();
5669 return $status;
5673 * Generate an email processing address.
5675 * @param int $modid
5676 * @param string $modargs
5677 * @return string Returns email processing address
5679 function generate_email_processing_address($modid, $modargs) {
5680 global $CFG;
5682 $header = $CFG->mailprefix . substr(base64_encode(pack('C', $modid)), 0, 2).$modargs;
5683 return $header . substr(md5($header.get_site_identifier()), 0, 16).'@'.$CFG->maildomain;
5689 * @todo Finish documenting this function
5691 * @param string $modargs
5692 * @param string $body Currently unused
5694 function moodle_process_email($modargs, $body) {
5695 global $DB;
5697 // The first char should be an unencoded letter. We'll take this as an action.
5698 switch ($modargs[0]) {
5699 case 'B': { // Bounce.
5700 list(, $userid) = unpack('V', base64_decode(substr($modargs, 1, 8)));
5701 if ($user = $DB->get_record("user", array('id' => $userid), "id,email")) {
5702 // Check the half md5 of their email.
5703 $md5check = substr(md5($user->email), 0, 16);
5704 if ($md5check == substr($modargs, -16)) {
5705 set_bounce_count($user);
5707 // Else maybe they've already changed it?
5710 break;
5711 // Maybe more later?
5715 // CORRESPONDENCE.
5718 * Get mailer instance, enable buffering, flush buffer or disable buffering.
5720 * @param string $action 'get', 'buffer', 'close' or 'flush'
5721 * @return moodle_phpmailer|null mailer instance if 'get' used or nothing
5723 function get_mailer($action='get') {
5724 global $CFG;
5726 /** @var moodle_phpmailer $mailer */
5727 static $mailer = null;
5728 static $counter = 0;
5730 if (!isset($CFG->smtpmaxbulk)) {
5731 $CFG->smtpmaxbulk = 1;
5734 if ($action == 'get') {
5735 $prevkeepalive = false;
5737 if (isset($mailer) and $mailer->Mailer == 'smtp') {
5738 if ($counter < $CFG->smtpmaxbulk and !$mailer->isError()) {
5739 $counter++;
5740 // Reset the mailer.
5741 $mailer->Priority = 3;
5742 $mailer->CharSet = 'UTF-8'; // Our default.
5743 $mailer->ContentType = "text/plain";
5744 $mailer->Encoding = "8bit";
5745 $mailer->From = "root@localhost";
5746 $mailer->FromName = "Root User";
5747 $mailer->Sender = "";
5748 $mailer->Subject = "";
5749 $mailer->Body = "";
5750 $mailer->AltBody = "";
5751 $mailer->ConfirmReadingTo = "";
5753 $mailer->clearAllRecipients();
5754 $mailer->clearReplyTos();
5755 $mailer->clearAttachments();
5756 $mailer->clearCustomHeaders();
5757 return $mailer;
5760 $prevkeepalive = $mailer->SMTPKeepAlive;
5761 get_mailer('flush');
5764 require_once($CFG->libdir.'/phpmailer/moodle_phpmailer.php');
5765 $mailer = new moodle_phpmailer();
5767 $counter = 1;
5769 if ($CFG->smtphosts == 'qmail') {
5770 // Use Qmail system.
5771 $mailer->isQmail();
5773 } else if (empty($CFG->smtphosts)) {
5774 // Use PHP mail() = sendmail.
5775 $mailer->isMail();
5777 } else {
5778 // Use SMTP directly.
5779 $mailer->isSMTP();
5780 if (!empty($CFG->debugsmtp) && (!empty($CFG->debugdeveloper))) {
5781 $mailer->SMTPDebug = 3;
5783 // Specify main and backup servers.
5784 $mailer->Host = $CFG->smtphosts;
5785 // Specify secure connection protocol.
5786 $mailer->SMTPSecure = $CFG->smtpsecure;
5787 // Use previous keepalive.
5788 $mailer->SMTPKeepAlive = $prevkeepalive;
5790 if ($CFG->smtpuser) {
5791 // Use SMTP authentication.
5792 $mailer->SMTPAuth = true;
5793 $mailer->Username = $CFG->smtpuser;
5794 $mailer->Password = $CFG->smtppass;
5798 return $mailer;
5801 $nothing = null;
5803 // Keep smtp session open after sending.
5804 if ($action == 'buffer') {
5805 if (!empty($CFG->smtpmaxbulk)) {
5806 get_mailer('flush');
5807 $m = get_mailer();
5808 if ($m->Mailer == 'smtp') {
5809 $m->SMTPKeepAlive = true;
5812 return $nothing;
5815 // Close smtp session, but continue buffering.
5816 if ($action == 'flush') {
5817 if (isset($mailer) and $mailer->Mailer == 'smtp') {
5818 if (!empty($mailer->SMTPDebug)) {
5819 echo '<pre>'."\n";
5821 $mailer->SmtpClose();
5822 if (!empty($mailer->SMTPDebug)) {
5823 echo '</pre>';
5826 return $nothing;
5829 // Close smtp session, do not buffer anymore.
5830 if ($action == 'close') {
5831 if (isset($mailer) and $mailer->Mailer == 'smtp') {
5832 get_mailer('flush');
5833 $mailer->SMTPKeepAlive = false;
5835 $mailer = null; // Better force new instance.
5836 return $nothing;
5841 * A helper function to test for email diversion
5843 * @param string $email
5844 * @return bool Returns true if the email should be diverted
5846 function email_should_be_diverted($email) {
5847 global $CFG;
5849 if (empty($CFG->divertallemailsto)) {
5850 return false;
5853 if (empty($CFG->divertallemailsexcept)) {
5854 return true;
5857 $patterns = array_map('trim', preg_split("/[\s,]+/", $CFG->divertallemailsexcept, -1, PREG_SPLIT_NO_EMPTY));
5858 foreach ($patterns as $pattern) {
5859 if (preg_match("/{$pattern}/i", $email)) {
5860 return false;
5864 return true;
5868 * Generate a unique email Message-ID using the moodle domain and install path
5870 * @param string $localpart An optional unique message id prefix.
5871 * @return string The formatted ID ready for appending to the email headers.
5873 function generate_email_messageid($localpart = null) {
5874 global $CFG;
5876 $urlinfo = parse_url($CFG->wwwroot);
5877 $base = '@' . $urlinfo['host'];
5879 // If multiple moodles are on the same domain we want to tell them
5880 // apart so we add the install path to the local part. This means
5881 // that the id local part should never contain a / character so
5882 // we can correctly parse the id to reassemble the wwwroot.
5883 if (isset($urlinfo['path'])) {
5884 $base = $urlinfo['path'] . $base;
5887 if (empty($localpart)) {
5888 $localpart = uniqid('', true);
5891 // Because we may have an option /installpath suffix to the local part
5892 // of the id we need to escape any / chars which are in the $localpart.
5893 $localpart = str_replace('/', '%2F', $localpart);
5895 return '<' . $localpart . $base . '>';
5899 * Send an email to a specified user
5901 * @param stdClass $user A {@link $USER} object
5902 * @param stdClass $from A {@link $USER} object
5903 * @param string $subject plain text subject line of the email
5904 * @param string $messagetext plain text version of the message
5905 * @param string $messagehtml complete html version of the message (optional)
5906 * @param string $attachment a file on the filesystem, either relative to $CFG->dataroot or a full path to a file in one of
5907 * the following directories: $CFG->cachedir, $CFG->dataroot, $CFG->dirroot, $CFG->localcachedir, $CFG->tempdir
5908 * @param string $attachname the name of the file (extension indicates MIME)
5909 * @param bool $usetrueaddress determines whether $from email address should
5910 * be sent out. Will be overruled by user profile setting for maildisplay
5911 * @param string $replyto Email address to reply to
5912 * @param string $replytoname Name of reply to recipient
5913 * @param int $wordwrapwidth custom word wrap width, default 79
5914 * @return bool Returns true if mail was sent OK and false if there was an error.
5916 function email_to_user($user, $from, $subject, $messagetext, $messagehtml = '', $attachment = '', $attachname = '',
5917 $usetrueaddress = true, $replyto = '', $replytoname = '', $wordwrapwidth = 79) {
5919 global $CFG, $PAGE, $SITE;
5921 if (empty($user) or empty($user->id)) {
5922 debugging('Can not send email to null user', DEBUG_DEVELOPER);
5923 return false;
5926 if (empty($user->email)) {
5927 debugging('Can not send email to user without email: '.$user->id, DEBUG_DEVELOPER);
5928 return false;
5931 if (!empty($user->deleted)) {
5932 debugging('Can not send email to deleted user: '.$user->id, DEBUG_DEVELOPER);
5933 return false;
5936 if (defined('BEHAT_SITE_RUNNING')) {
5937 // Fake email sending in behat.
5938 return true;
5941 if (!empty($CFG->noemailever)) {
5942 // Hidden setting for development sites, set in config.php if needed.
5943 debugging('Not sending email due to $CFG->noemailever config setting', DEBUG_NORMAL);
5944 return true;
5947 if (email_should_be_diverted($user->email)) {
5948 $subject = "[DIVERTED {$user->email}] $subject";
5949 $user = clone($user);
5950 $user->email = $CFG->divertallemailsto;
5953 // Skip mail to suspended users.
5954 if ((isset($user->auth) && $user->auth=='nologin') or (isset($user->suspended) && $user->suspended)) {
5955 return true;
5958 if (!validate_email($user->email)) {
5959 // We can not send emails to invalid addresses - it might create security issue or confuse the mailer.
5960 debugging("email_to_user: User $user->id (".fullname($user).") email ($user->email) is invalid! Not sending.");
5961 return false;
5964 if (over_bounce_threshold($user)) {
5965 debugging("email_to_user: User $user->id (".fullname($user).") is over bounce threshold! Not sending.");
5966 return false;
5969 // TLD .invalid is specifically reserved for invalid domain names.
5970 // For More information, see {@link http://tools.ietf.org/html/rfc2606#section-2}.
5971 if (substr($user->email, -8) == '.invalid') {
5972 debugging("email_to_user: User $user->id (".fullname($user).") email domain ($user->email) is invalid! Not sending.");
5973 return true; // This is not an error.
5976 // If the user is a remote mnet user, parse the email text for URL to the
5977 // wwwroot and modify the url to direct the user's browser to login at their
5978 // home site (identity provider - idp) before hitting the link itself.
5979 if (is_mnet_remote_user($user)) {
5980 require_once($CFG->dirroot.'/mnet/lib.php');
5982 $jumpurl = mnet_get_idp_jump_url($user);
5983 $callback = partial('mnet_sso_apply_indirection', $jumpurl);
5985 $messagetext = preg_replace_callback("%($CFG->wwwroot[^[:space:]]*)%",
5986 $callback,
5987 $messagetext);
5988 $messagehtml = preg_replace_callback("%href=[\"'`]($CFG->wwwroot[\w_:\?=#&@/;.~-]*)[\"'`]%",
5989 $callback,
5990 $messagehtml);
5992 $mail = get_mailer();
5994 if (!empty($mail->SMTPDebug)) {
5995 echo '<pre>' . "\n";
5998 $temprecipients = array();
5999 $tempreplyto = array();
6001 // Make sure that we fall back onto some reasonable no-reply address.
6002 $noreplyaddressdefault = 'noreply@' . get_host_from_url($CFG->wwwroot);
6003 $noreplyaddress = empty($CFG->noreplyaddress) ? $noreplyaddressdefault : $CFG->noreplyaddress;
6005 if (!validate_email($noreplyaddress)) {
6006 debugging('email_to_user: Invalid noreply-email '.s($noreplyaddress));
6007 $noreplyaddress = $noreplyaddressdefault;
6010 // Make up an email address for handling bounces.
6011 if (!empty($CFG->handlebounces)) {
6012 $modargs = 'B'.base64_encode(pack('V', $user->id)).substr(md5($user->email), 0, 16);
6013 $mail->Sender = generate_email_processing_address(0, $modargs);
6014 } else {
6015 $mail->Sender = $noreplyaddress;
6018 // Make sure that the explicit replyto is valid, fall back to the implicit one.
6019 if (!empty($replyto) && !validate_email($replyto)) {
6020 debugging('email_to_user: Invalid replyto-email '.s($replyto));
6021 $replyto = $noreplyaddress;
6024 if (is_string($from)) { // So we can pass whatever we want if there is need.
6025 $mail->From = $noreplyaddress;
6026 $mail->FromName = $from;
6027 // Check if using the true address is true, and the email is in the list of allowed domains for sending email,
6028 // and that the senders email setting is either displayed to everyone, or display to only other users that are enrolled
6029 // in a course with the sender.
6030 } else if ($usetrueaddress && can_send_from_real_email_address($from, $user)) {
6031 if (!validate_email($from->email)) {
6032 debugging('email_to_user: Invalid from-email '.s($from->email).' - not sending');
6033 // Better not to use $noreplyaddress in this case.
6034 return false;
6036 $mail->From = $from->email;
6037 $fromdetails = new stdClass();
6038 $fromdetails->name = fullname($from);
6039 $fromdetails->url = preg_replace('#^https?://#', '', $CFG->wwwroot);
6040 $fromdetails->siteshortname = format_string($SITE->shortname);
6041 $fromstring = $fromdetails->name;
6042 if ($CFG->emailfromvia == EMAIL_VIA_ALWAYS) {
6043 $fromstring = get_string('emailvia', 'core', $fromdetails);
6045 $mail->FromName = $fromstring;
6046 if (empty($replyto)) {
6047 $tempreplyto[] = array($from->email, fullname($from));
6049 } else {
6050 $mail->From = $noreplyaddress;
6051 $fromdetails = new stdClass();
6052 $fromdetails->name = fullname($from);
6053 $fromdetails->url = preg_replace('#^https?://#', '', $CFG->wwwroot);
6054 $fromdetails->siteshortname = format_string($SITE->shortname);
6055 $fromstring = $fromdetails->name;
6056 if ($CFG->emailfromvia != EMAIL_VIA_NEVER) {
6057 $fromstring = get_string('emailvia', 'core', $fromdetails);
6059 $mail->FromName = $fromstring;
6060 if (empty($replyto)) {
6061 $tempreplyto[] = array($noreplyaddress, get_string('noreplyname'));
6065 if (!empty($replyto)) {
6066 $tempreplyto[] = array($replyto, $replytoname);
6069 $temprecipients[] = array($user->email, fullname($user));
6071 // Set word wrap.
6072 $mail->WordWrap = $wordwrapwidth;
6074 if (!empty($from->customheaders)) {
6075 // Add custom headers.
6076 if (is_array($from->customheaders)) {
6077 foreach ($from->customheaders as $customheader) {
6078 $mail->addCustomHeader($customheader);
6080 } else {
6081 $mail->addCustomHeader($from->customheaders);
6085 // If the X-PHP-Originating-Script email header is on then also add an additional
6086 // header with details of where exactly in moodle the email was triggered from,
6087 // either a call to message_send() or to email_to_user().
6088 if (ini_get('mail.add_x_header')) {
6090 $stack = debug_backtrace(false);
6091 $origin = $stack[0];
6093 foreach ($stack as $depth => $call) {
6094 if ($call['function'] == 'message_send') {
6095 $origin = $call;
6099 $originheader = $CFG->wwwroot . ' => ' . gethostname() . ':'
6100 . str_replace($CFG->dirroot . '/', '', $origin['file']) . ':' . $origin['line'];
6101 $mail->addCustomHeader('X-Moodle-Originating-Script: ' . $originheader);
6104 if (!empty($CFG->emailheaders)) {
6105 $headers = array_map('trim', explode("\n", $CFG->emailheaders));
6106 foreach ($headers as $header) {
6107 if (!empty($header)) {
6108 $mail->addCustomHeader($header);
6113 if (!empty($from->priority)) {
6114 $mail->Priority = $from->priority;
6117 $renderer = $PAGE->get_renderer('core');
6118 $context = array(
6119 'sitefullname' => $SITE->fullname,
6120 'siteshortname' => $SITE->shortname,
6121 'sitewwwroot' => $CFG->wwwroot,
6122 'subject' => $subject,
6123 'prefix' => $CFG->emailsubjectprefix,
6124 'to' => $user->email,
6125 'toname' => fullname($user),
6126 'from' => $mail->From,
6127 'fromname' => $mail->FromName,
6129 if (!empty($tempreplyto[0])) {
6130 $context['replyto'] = $tempreplyto[0][0];
6131 $context['replytoname'] = $tempreplyto[0][1];
6133 if ($user->id > 0) {
6134 $context['touserid'] = $user->id;
6135 $context['tousername'] = $user->username;
6138 if (!empty($user->mailformat) && $user->mailformat == 1) {
6139 // Only process html templates if the user preferences allow html email.
6141 if (!$messagehtml) {
6142 // If no html has been given, BUT there is an html wrapping template then
6143 // auto convert the text to html and then wrap it.
6144 $messagehtml = trim(text_to_html($messagetext));
6146 $context['body'] = $messagehtml;
6147 $messagehtml = $renderer->render_from_template('core/email_html', $context);
6150 $context['body'] = html_to_text(nl2br($messagetext));
6151 $mail->Subject = $renderer->render_from_template('core/email_subject', $context);
6152 $mail->FromName = $renderer->render_from_template('core/email_fromname', $context);
6153 $messagetext = $renderer->render_from_template('core/email_text', $context);
6155 // Autogenerate a MessageID if it's missing.
6156 if (empty($mail->MessageID)) {
6157 $mail->MessageID = generate_email_messageid();
6160 if ($messagehtml && !empty($user->mailformat) && $user->mailformat == 1) {
6161 // Don't ever send HTML to users who don't want it.
6162 $mail->isHTML(true);
6163 $mail->Encoding = 'quoted-printable';
6164 $mail->Body = $messagehtml;
6165 $mail->AltBody = "\n$messagetext\n";
6166 } else {
6167 $mail->IsHTML(false);
6168 $mail->Body = "\n$messagetext\n";
6171 if ($attachment && $attachname) {
6172 if (preg_match( "~\\.\\.~" , $attachment )) {
6173 // Security check for ".." in dir path.
6174 $supportuser = core_user::get_support_user();
6175 $temprecipients[] = array($supportuser->email, fullname($supportuser, true));
6176 $mail->addStringAttachment('Error in attachment. User attempted to attach a filename with a unsafe name.', 'error.txt', '8bit', 'text/plain');
6177 } else {
6178 require_once($CFG->libdir.'/filelib.php');
6179 $mimetype = mimeinfo('type', $attachname);
6181 // Before doing the comparison, make sure that the paths are correct (Windows uses slashes in the other direction).
6182 // The absolute (real) path is also fetched to ensure that comparisons to allowed paths are compared equally.
6183 $attachpath = str_replace('\\', '/', realpath($attachment));
6185 // Build an array of all filepaths from which attachments can be added (normalised slashes, absolute/real path).
6186 $allowedpaths = array_map(function(string $path): string {
6187 return str_replace('\\', '/', realpath($path));
6188 }, [
6189 $CFG->cachedir,
6190 $CFG->dataroot,
6191 $CFG->dirroot,
6192 $CFG->localcachedir,
6193 $CFG->tempdir,
6194 $CFG->localrequestdir,
6197 // Set addpath to true.
6198 $addpath = true;
6200 // Check if attachment includes one of the allowed paths.
6201 foreach (array_filter($allowedpaths) as $allowedpath) {
6202 // Set addpath to false if the attachment includes one of the allowed paths.
6203 if (strpos($attachpath, $allowedpath) === 0) {
6204 $addpath = false;
6205 break;
6209 // If the attachment is a full path to a file in the multiple allowed paths, use it as is,
6210 // otherwise assume it is a relative path from the dataroot (for backwards compatibility reasons).
6211 if ($addpath == true) {
6212 $attachment = $CFG->dataroot . '/' . $attachment;
6215 $mail->addAttachment($attachment, $attachname, 'base64', $mimetype);
6219 // Check if the email should be sent in an other charset then the default UTF-8.
6220 if ((!empty($CFG->sitemailcharset) || !empty($CFG->allowusermailcharset))) {
6222 // Use the defined site mail charset or eventually the one preferred by the recipient.
6223 $charset = $CFG->sitemailcharset;
6224 if (!empty($CFG->allowusermailcharset)) {
6225 if ($useremailcharset = get_user_preferences('mailcharset', '0', $user->id)) {
6226 $charset = $useremailcharset;
6230 // Convert all the necessary strings if the charset is supported.
6231 $charsets = get_list_of_charsets();
6232 unset($charsets['UTF-8']);
6233 if (in_array($charset, $charsets)) {
6234 $mail->CharSet = $charset;
6235 $mail->FromName = core_text::convert($mail->FromName, 'utf-8', strtolower($charset));
6236 $mail->Subject = core_text::convert($mail->Subject, 'utf-8', strtolower($charset));
6237 $mail->Body = core_text::convert($mail->Body, 'utf-8', strtolower($charset));
6238 $mail->AltBody = core_text::convert($mail->AltBody, 'utf-8', strtolower($charset));
6240 foreach ($temprecipients as $key => $values) {
6241 $temprecipients[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset));
6243 foreach ($tempreplyto as $key => $values) {
6244 $tempreplyto[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset));
6249 foreach ($temprecipients as $values) {
6250 $mail->addAddress($values[0], $values[1]);
6252 foreach ($tempreplyto as $values) {
6253 $mail->addReplyTo($values[0], $values[1]);
6256 if (!empty($CFG->emaildkimselector)) {
6257 $domain = substr(strrchr($mail->From, "@"), 1);
6258 $pempath = "{$CFG->dataroot}/dkim/{$domain}/{$CFG->emaildkimselector}.private";
6259 if (file_exists($pempath)) {
6260 $mail->DKIM_domain = $domain;
6261 $mail->DKIM_private = $pempath;
6262 $mail->DKIM_selector = $CFG->emaildkimselector;
6263 $mail->DKIM_identity = $mail->From;
6264 } else {
6265 debugging("Email DKIM selector chosen due to {$mail->From} but no certificate found at $pempath", DEBUG_DEVELOPER);
6269 if ($mail->send()) {
6270 set_send_count($user);
6271 if (!empty($mail->SMTPDebug)) {
6272 echo '</pre>';
6274 return true;
6275 } else {
6276 // Trigger event for failing to send email.
6277 $event = \core\event\email_failed::create(array(
6278 'context' => context_system::instance(),
6279 'userid' => $from->id,
6280 'relateduserid' => $user->id,
6281 'other' => array(
6282 'subject' => $subject,
6283 'message' => $messagetext,
6284 'errorinfo' => $mail->ErrorInfo
6287 $event->trigger();
6288 if (CLI_SCRIPT) {
6289 mtrace('Error: lib/moodlelib.php email_to_user(): '.$mail->ErrorInfo);
6291 if (!empty($mail->SMTPDebug)) {
6292 echo '</pre>';
6294 return false;
6299 * Check to see if a user's real email address should be used for the "From" field.
6301 * @param object $from The user object for the user we are sending the email from.
6302 * @param object $user The user object that we are sending the email to.
6303 * @param array $unused No longer used.
6304 * @return bool Returns true if we can use the from user's email adress in the "From" field.
6306 function can_send_from_real_email_address($from, $user, $unused = null) {
6307 global $CFG;
6308 if (!isset($CFG->allowedemaildomains) || empty(trim($CFG->allowedemaildomains))) {
6309 return false;
6311 $alloweddomains = array_map('trim', explode("\n", $CFG->allowedemaildomains));
6312 // Email is in the list of allowed domains for sending email,
6313 // and the senders email setting is either displayed to everyone, or display to only other users that are enrolled
6314 // in a course with the sender.
6315 if (\core\ip_utils::is_domain_in_allowed_list(substr($from->email, strpos($from->email, '@') + 1), $alloweddomains)
6316 && ($from->maildisplay == core_user::MAILDISPLAY_EVERYONE
6317 || ($from->maildisplay == core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY
6318 && enrol_get_shared_courses($user, $from, false, true)))) {
6319 return true;
6321 return false;
6325 * Generate a signoff for emails based on support settings
6327 * @return string
6329 function generate_email_signoff() {
6330 global $CFG, $OUTPUT;
6332 $signoff = "\n";
6333 if (!empty($CFG->supportname)) {
6334 $signoff .= $CFG->supportname."\n";
6337 $supportemail = $OUTPUT->supportemail(['class' => 'font-weight-bold']);
6339 if ($supportemail) {
6340 $signoff .= "\n" . $supportemail . "\n";
6343 return $signoff;
6347 * Sets specified user's password and send the new password to the user via email.
6349 * @param stdClass $user A {@link $USER} object
6350 * @param bool $fasthash If true, use a low cost factor when generating the hash for speed.
6351 * @return bool|string Returns "true" if mail was sent OK and "false" if there was an error
6353 function setnew_password_and_mail($user, $fasthash = false) {
6354 global $CFG, $DB;
6356 // We try to send the mail in language the user understands,
6357 // unfortunately the filter_string() does not support alternative langs yet
6358 // so multilang will not work properly for site->fullname.
6359 $lang = empty($user->lang) ? get_newuser_language() : $user->lang;
6361 $site = get_site();
6363 $supportuser = core_user::get_support_user();
6365 $newpassword = generate_password();
6367 update_internal_user_password($user, $newpassword, $fasthash);
6369 $a = new stdClass();
6370 $a->firstname = fullname($user, true);
6371 $a->sitename = format_string($site->fullname);
6372 $a->username = $user->username;
6373 $a->newpassword = $newpassword;
6374 $a->link = $CFG->wwwroot .'/login/?lang='.$lang;
6375 $a->signoff = generate_email_signoff();
6377 $message = (string)new lang_string('newusernewpasswordtext', '', $a, $lang);
6379 $subject = format_string($site->fullname) .': '. (string)new lang_string('newusernewpasswordsubj', '', $a, $lang);
6381 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6382 return email_to_user($user, $supportuser, $subject, $message);
6387 * Resets specified user's password and send the new password to the user via email.
6389 * @param stdClass $user A {@link $USER} object
6390 * @return bool Returns true if mail was sent OK and false if there was an error.
6392 function reset_password_and_mail($user) {
6393 global $CFG;
6395 $site = get_site();
6396 $supportuser = core_user::get_support_user();
6398 $userauth = get_auth_plugin($user->auth);
6399 if (!$userauth->can_reset_password() or !is_enabled_auth($user->auth)) {
6400 trigger_error("Attempt to reset user password for user $user->username with Auth $user->auth.");
6401 return false;
6404 $newpassword = generate_password();
6406 if (!$userauth->user_update_password($user, $newpassword)) {
6407 throw new \moodle_exception("cannotsetpassword");
6410 $a = new stdClass();
6411 $a->firstname = $user->firstname;
6412 $a->lastname = $user->lastname;
6413 $a->sitename = format_string($site->fullname);
6414 $a->username = $user->username;
6415 $a->newpassword = $newpassword;
6416 $a->link = $CFG->wwwroot .'/login/change_password.php';
6417 $a->signoff = generate_email_signoff();
6419 $message = get_string('newpasswordtext', '', $a);
6421 $subject = format_string($site->fullname) .': '. get_string('changedpassword');
6423 unset_user_preference('create_password', $user); // Prevent cron from generating the password.
6425 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6426 return email_to_user($user, $supportuser, $subject, $message);
6430 * Send email to specified user with confirmation text and activation link.
6432 * @param stdClass $user A {@link $USER} object
6433 * @param string $confirmationurl user confirmation URL
6434 * @return bool Returns true if mail was sent OK and false if there was an error.
6436 function send_confirmation_email($user, $confirmationurl = null) {
6437 global $CFG;
6439 $site = get_site();
6440 $supportuser = core_user::get_support_user();
6442 $data = new stdClass();
6443 $data->sitename = format_string($site->fullname);
6444 $data->admin = generate_email_signoff();
6446 $subject = get_string('emailconfirmationsubject', '', format_string($site->fullname));
6448 if (empty($confirmationurl)) {
6449 $confirmationurl = '/login/confirm.php';
6452 $confirmationurl = new moodle_url($confirmationurl);
6453 // Remove data parameter just in case it was included in the confirmation so we can add it manually later.
6454 $confirmationurl->remove_params('data');
6455 $confirmationpath = $confirmationurl->out(false);
6457 // We need to custom encode the username to include trailing dots in the link.
6458 // Because of this custom encoding we can't use moodle_url directly.
6459 // Determine if a query string is present in the confirmation url.
6460 $hasquerystring = strpos($confirmationpath, '?') !== false;
6461 // Perform normal url encoding of the username first.
6462 $username = urlencode($user->username);
6463 // Prevent problems with trailing dots not being included as part of link in some mail clients.
6464 $username = str_replace('.', '%2E', $username);
6466 $data->link = $confirmationpath . ( $hasquerystring ? '&' : '?') . 'data='. $user->secret .'/'. $username;
6468 $message = get_string('emailconfirmation', '', $data);
6469 $messagehtml = text_to_html(get_string('emailconfirmation', '', $data), false, false, true);
6471 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6472 return email_to_user($user, $supportuser, $subject, $message, $messagehtml);
6476 * Sends a password change confirmation email.
6478 * @param stdClass $user A {@link $USER} object
6479 * @param stdClass $resetrecord An object tracking metadata regarding password reset request
6480 * @return bool Returns true if mail was sent OK and false if there was an error.
6482 function send_password_change_confirmation_email($user, $resetrecord) {
6483 global $CFG;
6485 $site = get_site();
6486 $supportuser = core_user::get_support_user();
6487 $pwresetmins = isset($CFG->pwresettime) ? floor($CFG->pwresettime / MINSECS) : 30;
6489 $data = new stdClass();
6490 $data->firstname = $user->firstname;
6491 $data->lastname = $user->lastname;
6492 $data->username = $user->username;
6493 $data->sitename = format_string($site->fullname);
6494 $data->link = $CFG->wwwroot .'/login/forgot_password.php?token='. $resetrecord->token;
6495 $data->admin = generate_email_signoff();
6496 $data->resetminutes = $pwresetmins;
6498 $message = get_string('emailresetconfirmation', '', $data);
6499 $subject = get_string('emailresetconfirmationsubject', '', format_string($site->fullname));
6501 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6502 return email_to_user($user, $supportuser, $subject, $message);
6507 * Sends an email containing information on how to change your password.
6509 * @param stdClass $user A {@link $USER} object
6510 * @return bool Returns true if mail was sent OK and false if there was an error.
6512 function send_password_change_info($user) {
6513 $site = get_site();
6514 $supportuser = core_user::get_support_user();
6516 $data = new stdClass();
6517 $data->firstname = $user->firstname;
6518 $data->lastname = $user->lastname;
6519 $data->username = $user->username;
6520 $data->sitename = format_string($site->fullname);
6521 $data->admin = generate_email_signoff();
6523 if (!is_enabled_auth($user->auth)) {
6524 $message = get_string('emailpasswordchangeinfodisabled', '', $data);
6525 $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
6526 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6527 return email_to_user($user, $supportuser, $subject, $message);
6530 $userauth = get_auth_plugin($user->auth);
6531 ['subject' => $subject, 'message' => $message] = $userauth->get_password_change_info($user);
6533 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6534 return email_to_user($user, $supportuser, $subject, $message);
6538 * Check that an email is allowed. It returns an error message if there was a problem.
6540 * @param string $email Content of email
6541 * @return string|false
6543 function email_is_not_allowed($email) {
6544 global $CFG;
6546 // Comparing lowercase domains.
6547 $email = strtolower($email);
6548 if (!empty($CFG->allowemailaddresses)) {
6549 $allowed = explode(' ', strtolower($CFG->allowemailaddresses));
6550 foreach ($allowed as $allowedpattern) {
6551 $allowedpattern = trim($allowedpattern);
6552 if (!$allowedpattern) {
6553 continue;
6555 if (strpos($allowedpattern, '.') === 0) {
6556 if (strpos(strrev($email), strrev($allowedpattern)) === 0) {
6557 // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com".
6558 return false;
6561 } else if (strpos(strrev($email), strrev('@'.$allowedpattern)) === 0) {
6562 return false;
6565 return get_string('emailonlyallowed', '', $CFG->allowemailaddresses);
6567 } else if (!empty($CFG->denyemailaddresses)) {
6568 $denied = explode(' ', strtolower($CFG->denyemailaddresses));
6569 foreach ($denied as $deniedpattern) {
6570 $deniedpattern = trim($deniedpattern);
6571 if (!$deniedpattern) {
6572 continue;
6574 if (strpos($deniedpattern, '.') === 0) {
6575 if (strpos(strrev($email), strrev($deniedpattern)) === 0) {
6576 // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com".
6577 return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
6580 } else if (strpos(strrev($email), strrev('@'.$deniedpattern)) === 0) {
6581 return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
6586 return false;
6589 // FILE HANDLING.
6592 * Returns local file storage instance
6594 * @return file_storage
6596 function get_file_storage($reset = false) {
6597 global $CFG;
6599 static $fs = null;
6601 if ($reset) {
6602 $fs = null;
6603 return;
6606 if ($fs) {
6607 return $fs;
6610 require_once("$CFG->libdir/filelib.php");
6612 $fs = new file_storage();
6614 return $fs;
6618 * Returns local file storage instance
6620 * @return file_browser
6622 function get_file_browser() {
6623 global $CFG;
6625 static $fb = null;
6627 if ($fb) {
6628 return $fb;
6631 require_once("$CFG->libdir/filelib.php");
6633 $fb = new file_browser();
6635 return $fb;
6639 * Returns file packer
6641 * @param string $mimetype default application/zip
6642 * @return file_packer
6644 function get_file_packer($mimetype='application/zip') {
6645 global $CFG;
6647 static $fp = array();
6649 if (isset($fp[$mimetype])) {
6650 return $fp[$mimetype];
6653 switch ($mimetype) {
6654 case 'application/zip':
6655 case 'application/vnd.moodle.profiling':
6656 $classname = 'zip_packer';
6657 break;
6659 case 'application/x-gzip' :
6660 $classname = 'tgz_packer';
6661 break;
6663 case 'application/vnd.moodle.backup':
6664 $classname = 'mbz_packer';
6665 break;
6667 default:
6668 return false;
6671 require_once("$CFG->libdir/filestorage/$classname.php");
6672 $fp[$mimetype] = new $classname();
6674 return $fp[$mimetype];
6678 * Returns current name of file on disk if it exists.
6680 * @param string $newfile File to be verified
6681 * @return string Current name of file on disk if true
6683 function valid_uploaded_file($newfile) {
6684 if (empty($newfile)) {
6685 return '';
6687 if (is_uploaded_file($newfile['tmp_name']) and $newfile['size'] > 0) {
6688 return $newfile['tmp_name'];
6689 } else {
6690 return '';
6695 * Returns the maximum size for uploading files.
6697 * There are seven possible upload limits:
6698 * 1. in Apache using LimitRequestBody (no way of checking or changing this)
6699 * 2. in php.ini for 'upload_max_filesize' (can not be changed inside PHP)
6700 * 3. in .htaccess for 'upload_max_filesize' (can not be changed inside PHP)
6701 * 4. in php.ini for 'post_max_size' (can not be changed inside PHP)
6702 * 5. by the Moodle admin in $CFG->maxbytes
6703 * 6. by the teacher in the current course $course->maxbytes
6704 * 7. by the teacher for the current module, eg $assignment->maxbytes
6706 * These last two are passed to this function as arguments (in bytes).
6707 * Anything defined as 0 is ignored.
6708 * The smallest of all the non-zero numbers is returned.
6710 * @todo Finish documenting this function
6712 * @param int $sitebytes Set maximum size
6713 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6714 * @param int $modulebytes Current module ->maxbytes (in bytes)
6715 * @param bool $unused This parameter has been deprecated and is not used any more.
6716 * @return int The maximum size for uploading files.
6718 function get_max_upload_file_size($sitebytes=0, $coursebytes=0, $modulebytes=0, $unused = false) {
6720 if (! $filesize = ini_get('upload_max_filesize')) {
6721 $filesize = '5M';
6723 $minimumsize = get_real_size($filesize);
6725 if ($postsize = ini_get('post_max_size')) {
6726 $postsize = get_real_size($postsize);
6727 if ($postsize < $minimumsize) {
6728 $minimumsize = $postsize;
6732 if (($sitebytes > 0) and ($sitebytes < $minimumsize)) {
6733 $minimumsize = $sitebytes;
6736 if (($coursebytes > 0) and ($coursebytes < $minimumsize)) {
6737 $minimumsize = $coursebytes;
6740 if (($modulebytes > 0) and ($modulebytes < $minimumsize)) {
6741 $minimumsize = $modulebytes;
6744 return $minimumsize;
6748 * Returns the maximum size for uploading files for the current user
6750 * This function takes in account {@link get_max_upload_file_size()} the user's capabilities
6752 * @param context $context The context in which to check user capabilities
6753 * @param int $sitebytes Set maximum size
6754 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6755 * @param int $modulebytes Current module ->maxbytes (in bytes)
6756 * @param stdClass $user The user
6757 * @param bool $unused This parameter has been deprecated and is not used any more.
6758 * @return int The maximum size for uploading files.
6760 function get_user_max_upload_file_size($context, $sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $user = null,
6761 $unused = false) {
6762 global $USER;
6764 if (empty($user)) {
6765 $user = $USER;
6768 if (has_capability('moodle/course:ignorefilesizelimits', $context, $user)) {
6769 return USER_CAN_IGNORE_FILE_SIZE_LIMITS;
6772 return get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes);
6776 * Returns an array of possible sizes in local language
6778 * Related to {@link get_max_upload_file_size()} - this function returns an
6779 * array of possible sizes in an array, translated to the
6780 * local language.
6782 * The list of options will go up to the minimum of $sitebytes, $coursebytes or $modulebytes.
6784 * If $coursebytes or $sitebytes is not 0, an option will be included for "Course/Site upload limit (X)"
6785 * with the value set to 0. This option will be the first in the list.
6787 * @uses SORT_NUMERIC
6788 * @param int $sitebytes Set maximum size
6789 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6790 * @param int $modulebytes Current module ->maxbytes (in bytes)
6791 * @param int|array $custombytes custom upload size/s which will be added to list,
6792 * Only value/s smaller then maxsize will be added to list.
6793 * @return array
6795 function get_max_upload_sizes($sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $custombytes = null) {
6796 global $CFG;
6798 if (!$maxsize = get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes)) {
6799 return array();
6802 if ($sitebytes == 0) {
6803 // Will get the minimum of upload_max_filesize or post_max_size.
6804 $sitebytes = get_max_upload_file_size();
6807 $filesize = array();
6808 $sizelist = array(10240, 51200, 102400, 512000, 1048576, 2097152,
6809 5242880, 10485760, 20971520, 52428800, 104857600,
6810 262144000, 524288000, 786432000, 1073741824,
6811 2147483648, 4294967296, 8589934592);
6813 // If custombytes is given and is valid then add it to the list.
6814 if (is_number($custombytes) and $custombytes > 0) {
6815 $custombytes = (int)$custombytes;
6816 if (!in_array($custombytes, $sizelist)) {
6817 $sizelist[] = $custombytes;
6819 } else if (is_array($custombytes)) {
6820 $sizelist = array_unique(array_merge($sizelist, $custombytes));
6823 // Allow maxbytes to be selected if it falls outside the above boundaries.
6824 if (isset($CFG->maxbytes) && !in_array(get_real_size($CFG->maxbytes), $sizelist)) {
6825 // Note: get_real_size() is used in order to prevent problems with invalid values.
6826 $sizelist[] = get_real_size($CFG->maxbytes);
6829 foreach ($sizelist as $sizebytes) {
6830 if ($sizebytes < $maxsize && $sizebytes > 0) {
6831 $filesize[(string)intval($sizebytes)] = display_size($sizebytes, 0);
6835 $limitlevel = '';
6836 $displaysize = '';
6837 if ($modulebytes &&
6838 (($modulebytes < $coursebytes || $coursebytes == 0) &&
6839 ($modulebytes < $sitebytes || $sitebytes == 0))) {
6840 $limitlevel = get_string('activity', 'core');
6841 $displaysize = display_size($modulebytes, 0);
6842 $filesize[$modulebytes] = $displaysize; // Make sure the limit is also included in the list.
6844 } else if ($coursebytes && ($coursebytes < $sitebytes || $sitebytes == 0)) {
6845 $limitlevel = get_string('course', 'core');
6846 $displaysize = display_size($coursebytes, 0);
6847 $filesize[$coursebytes] = $displaysize; // Make sure the limit is also included in the list.
6849 } else if ($sitebytes) {
6850 $limitlevel = get_string('site', 'core');
6851 $displaysize = display_size($sitebytes, 0);
6852 $filesize[$sitebytes] = $displaysize; // Make sure the limit is also included in the list.
6855 krsort($filesize, SORT_NUMERIC);
6856 if ($limitlevel) {
6857 $params = (object) array('contextname' => $limitlevel, 'displaysize' => $displaysize);
6858 $filesize = array('0' => get_string('uploadlimitwithsize', 'core', $params)) + $filesize;
6861 return $filesize;
6865 * Returns an array with all the filenames in all subdirectories, relative to the given rootdir.
6867 * If excludefiles is defined, then that file/directory is ignored
6868 * If getdirs is true, then (sub)directories are included in the output
6869 * If getfiles is true, then files are included in the output
6870 * (at least one of these must be true!)
6872 * @todo Finish documenting this function. Add examples of $excludefile usage.
6874 * @param string $rootdir A given root directory to start from
6875 * @param string|array $excludefiles If defined then the specified file/directory is ignored
6876 * @param bool $descend If true then subdirectories are recursed as well
6877 * @param bool $getdirs If true then (sub)directories are included in the output
6878 * @param bool $getfiles If true then files are included in the output
6879 * @return array An array with all the filenames in all subdirectories, relative to the given rootdir
6881 function get_directory_list($rootdir, $excludefiles='', $descend=true, $getdirs=false, $getfiles=true) {
6883 $dirs = array();
6885 if (!$getdirs and !$getfiles) { // Nothing to show.
6886 return $dirs;
6889 if (!is_dir($rootdir)) { // Must be a directory.
6890 return $dirs;
6893 if (!$dir = opendir($rootdir)) { // Can't open it for some reason.
6894 return $dirs;
6897 if (!is_array($excludefiles)) {
6898 $excludefiles = array($excludefiles);
6901 while (false !== ($file = readdir($dir))) {
6902 $firstchar = substr($file, 0, 1);
6903 if ($firstchar == '.' or $file == 'CVS' or in_array($file, $excludefiles)) {
6904 continue;
6906 $fullfile = $rootdir .'/'. $file;
6907 if (filetype($fullfile) == 'dir') {
6908 if ($getdirs) {
6909 $dirs[] = $file;
6911 if ($descend) {
6912 $subdirs = get_directory_list($fullfile, $excludefiles, $descend, $getdirs, $getfiles);
6913 foreach ($subdirs as $subdir) {
6914 $dirs[] = $file .'/'. $subdir;
6917 } else if ($getfiles) {
6918 $dirs[] = $file;
6921 closedir($dir);
6923 asort($dirs);
6925 return $dirs;
6930 * Adds up all the files in a directory and works out the size.
6932 * @param string $rootdir The directory to start from
6933 * @param string $excludefile A file to exclude when summing directory size
6934 * @return int The summed size of all files and subfiles within the root directory
6936 function get_directory_size($rootdir, $excludefile='') {
6937 global $CFG;
6939 // Do it this way if we can, it's much faster.
6940 if (!empty($CFG->pathtodu) && is_executable(trim($CFG->pathtodu))) {
6941 $command = trim($CFG->pathtodu).' -sk '.escapeshellarg($rootdir);
6942 $output = null;
6943 $return = null;
6944 exec($command, $output, $return);
6945 if (is_array($output)) {
6946 // We told it to return k.
6947 return get_real_size(intval($output[0]).'k');
6951 if (!is_dir($rootdir)) {
6952 // Must be a directory.
6953 return 0;
6956 if (!$dir = @opendir($rootdir)) {
6957 // Can't open it for some reason.
6958 return 0;
6961 $size = 0;
6963 while (false !== ($file = readdir($dir))) {
6964 $firstchar = substr($file, 0, 1);
6965 if ($firstchar == '.' or $file == 'CVS' or $file == $excludefile) {
6966 continue;
6968 $fullfile = $rootdir .'/'. $file;
6969 if (filetype($fullfile) == 'dir') {
6970 $size += get_directory_size($fullfile, $excludefile);
6971 } else {
6972 $size += filesize($fullfile);
6975 closedir($dir);
6977 return $size;
6981 * Converts bytes into display form
6983 * @param int $size The size to convert to human readable form
6984 * @param int $decimalplaces If specified, uses fixed number of decimal places
6985 * @param string $fixedunits If specified, uses fixed units (e.g. 'KB')
6986 * @return string Display version of size
6988 function display_size($size, int $decimalplaces = 1, string $fixedunits = ''): string {
6990 static $units;
6992 if ($size === USER_CAN_IGNORE_FILE_SIZE_LIMITS) {
6993 return get_string('unlimited');
6996 if (empty($units)) {
6997 $units[] = get_string('sizeb');
6998 $units[] = get_string('sizekb');
6999 $units[] = get_string('sizemb');
7000 $units[] = get_string('sizegb');
7001 $units[] = get_string('sizetb');
7002 $units[] = get_string('sizepb');
7005 switch ($fixedunits) {
7006 case 'PB' :
7007 $magnitude = 5;
7008 break;
7009 case 'TB' :
7010 $magnitude = 4;
7011 break;
7012 case 'GB' :
7013 $magnitude = 3;
7014 break;
7015 case 'MB' :
7016 $magnitude = 2;
7017 break;
7018 case 'KB' :
7019 $magnitude = 1;
7020 break;
7021 case 'B' :
7022 $magnitude = 0;
7023 break;
7024 case '':
7025 $magnitude = floor(log($size, 1024));
7026 $magnitude = max(0, min(5, $magnitude));
7027 break;
7028 default:
7029 throw new coding_exception('Unknown fixed units value: ' . $fixedunits);
7032 // Special case for magnitude 0 (bytes) - never use decimal places.
7033 $nbsp = "\xc2\xa0";
7034 if ($magnitude === 0) {
7035 return round($size) . $nbsp . $units[$magnitude];
7038 // Convert to specified units.
7039 $sizeinunit = $size / 1024 ** $magnitude;
7041 // Fixed decimal places.
7042 return sprintf('%.' . $decimalplaces . 'f', $sizeinunit) . $nbsp . $units[$magnitude];
7046 * Cleans a given filename by removing suspicious or troublesome characters
7048 * @see clean_param()
7049 * @param string $string file name
7050 * @return string cleaned file name
7052 function clean_filename($string) {
7053 return clean_param($string, PARAM_FILE);
7056 // STRING TRANSLATION.
7059 * Returns the code for the current language
7061 * @category string
7062 * @return string
7064 function current_language() {
7065 global $CFG, $PAGE, $SESSION, $USER;
7067 if (!empty($SESSION->forcelang)) {
7068 // Allows overriding course-forced language (useful for admins to check
7069 // issues in courses whose language they don't understand).
7070 // Also used by some code to temporarily get language-related information in a
7071 // specific language (see force_current_language()).
7072 $return = $SESSION->forcelang;
7074 } else if (!empty($PAGE->cm->lang)) {
7075 // Activity language, if set.
7076 $return = $PAGE->cm->lang;
7078 } else if (!empty($PAGE->course->id) && $PAGE->course->id != SITEID && !empty($PAGE->course->lang)) {
7079 // Course language can override all other settings for this page.
7080 $return = $PAGE->course->lang;
7082 } else if (!empty($SESSION->lang)) {
7083 // Session language can override other settings.
7084 $return = $SESSION->lang;
7086 } else if (!empty($USER->lang)) {
7087 $return = $USER->lang;
7089 } else if (isset($CFG->lang)) {
7090 $return = $CFG->lang;
7092 } else {
7093 $return = 'en';
7096 // Just in case this slipped in from somewhere by accident.
7097 $return = str_replace('_utf8', '', $return);
7099 return $return;
7103 * Fix the current language to the given language code.
7105 * @param string $lang The language code to use.
7106 * @return void
7108 function fix_current_language(string $lang): void {
7109 global $CFG, $COURSE, $SESSION, $USER;
7111 if (!get_string_manager()->translation_exists($lang)) {
7112 throw new coding_exception("The language pack for $lang is not available");
7115 $fixglobal = '';
7116 $fixlang = 'lang';
7117 if (!empty($SESSION->forcelang)) {
7118 $fixglobal = $SESSION;
7119 $fixlang = 'forcelang';
7120 } else if (!empty($COURSE->id) && $COURSE->id != SITEID && !empty($COURSE->lang)) {
7121 $fixglobal = $COURSE;
7122 } else if (!empty($SESSION->lang)) {
7123 $fixglobal = $SESSION;
7124 } else if (!empty($USER->lang)) {
7125 $fixglobal = $USER;
7126 } else if (isset($CFG->lang)) {
7127 set_config('lang', $lang);
7130 if ($fixglobal) {
7131 $fixglobal->$fixlang = $lang;
7136 * Returns parent language of current active language if defined
7138 * @category string
7139 * @param string $lang null means current language
7140 * @return string
7142 function get_parent_language($lang=null) {
7144 $parentlang = get_string_manager()->get_string('parentlanguage', 'langconfig', null, $lang);
7146 if ($parentlang === 'en') {
7147 $parentlang = '';
7150 return $parentlang;
7154 * Force the current language to get strings and dates localised in the given language.
7156 * After calling this function, all strings will be provided in the given language
7157 * until this function is called again, or equivalent code is run.
7159 * @param string $language
7160 * @return string previous $SESSION->forcelang value
7162 function force_current_language($language) {
7163 global $SESSION;
7164 $sessionforcelang = isset($SESSION->forcelang) ? $SESSION->forcelang : '';
7165 if ($language !== $sessionforcelang) {
7166 // Setting forcelang to null or an empty string disables its effect.
7167 if (empty($language) || get_string_manager()->translation_exists($language, false)) {
7168 $SESSION->forcelang = $language;
7169 moodle_setlocale();
7172 return $sessionforcelang;
7176 * Returns current string_manager instance.
7178 * The param $forcereload is needed for CLI installer only where the string_manager instance
7179 * must be replaced during the install.php script life time.
7181 * @category string
7182 * @param bool $forcereload shall the singleton be released and new instance created instead?
7183 * @return core_string_manager
7185 function get_string_manager($forcereload=false) {
7186 global $CFG;
7188 static $singleton = null;
7190 if ($forcereload) {
7191 $singleton = null;
7193 if ($singleton === null) {
7194 if (empty($CFG->early_install_lang)) {
7196 $transaliases = array();
7197 if (empty($CFG->langlist)) {
7198 $translist = array();
7199 } else {
7200 $translist = explode(',', $CFG->langlist);
7201 $translist = array_map('trim', $translist);
7202 // Each language in the $CFG->langlist can has an "alias" that would substitute the default language name.
7203 foreach ($translist as $i => $value) {
7204 $parts = preg_split('/\s*\|\s*/', $value, 2);
7205 if (count($parts) == 2) {
7206 $transaliases[$parts[0]] = $parts[1];
7207 $translist[$i] = $parts[0];
7212 if (!empty($CFG->config_php_settings['customstringmanager'])) {
7213 $classname = $CFG->config_php_settings['customstringmanager'];
7215 if (class_exists($classname)) {
7216 $implements = class_implements($classname);
7218 if (isset($implements['core_string_manager'])) {
7219 $singleton = new $classname($CFG->langotherroot, $CFG->langlocalroot, $translist, $transaliases);
7220 return $singleton;
7222 } else {
7223 debugging('Unable to instantiate custom string manager: class '.$classname.
7224 ' does not implement the core_string_manager interface.');
7227 } else {
7228 debugging('Unable to instantiate custom string manager: class '.$classname.' can not be found.');
7232 $singleton = new core_string_manager_standard($CFG->langotherroot, $CFG->langlocalroot, $translist, $transaliases);
7234 } else {
7235 $singleton = new core_string_manager_install();
7239 return $singleton;
7243 * Returns a localized string.
7245 * Returns the translated string specified by $identifier as
7246 * for $module. Uses the same format files as STphp.
7247 * $a is an object, string or number that can be used
7248 * within translation strings
7250 * eg 'hello {$a->firstname} {$a->lastname}'
7251 * or 'hello {$a}'
7253 * If you would like to directly echo the localized string use
7254 * the function {@link print_string()}
7256 * Example usage of this function involves finding the string you would
7257 * like a local equivalent of and using its identifier and module information
7258 * to retrieve it.<br/>
7259 * If you open moodle/lang/en/moodle.php and look near line 278
7260 * you will find a string to prompt a user for their word for 'course'
7261 * <code>
7262 * $string['course'] = 'Course';
7263 * </code>
7264 * So if you want to display the string 'Course'
7265 * in any language that supports it on your site
7266 * you just need to use the identifier 'course'
7267 * <code>
7268 * $mystring = '<strong>'. get_string('course') .'</strong>';
7269 * or
7270 * </code>
7271 * If the string you want is in another file you'd take a slightly
7272 * different approach. Looking in moodle/lang/en/calendar.php you find
7273 * around line 75:
7274 * <code>
7275 * $string['typecourse'] = 'Course event';
7276 * </code>
7277 * If you want to display the string "Course event" in any language
7278 * supported you would use the identifier 'typecourse' and the module 'calendar'
7279 * (because it is in the file calendar.php):
7280 * <code>
7281 * $mystring = '<h1>'. get_string('typecourse', 'calendar') .'</h1>';
7282 * </code>
7284 * As a last resort, should the identifier fail to map to a string
7285 * the returned string will be [[ $identifier ]]
7287 * In Moodle 2.3 there is a new argument to this function $lazyload.
7288 * Setting $lazyload to true causes get_string to return a lang_string object
7289 * rather than the string itself. The fetching of the string is then put off until
7290 * the string object is first used. The object can be used by calling it's out
7291 * method or by casting the object to a string, either directly e.g.
7292 * (string)$stringobject
7293 * or indirectly by using the string within another string or echoing it out e.g.
7294 * echo $stringobject
7295 * return "<p>{$stringobject}</p>";
7296 * It is worth noting that using $lazyload and attempting to use the string as an
7297 * array key will cause a fatal error as objects cannot be used as array keys.
7298 * But you should never do that anyway!
7299 * For more information {@link lang_string}
7301 * @category string
7302 * @param string $identifier The key identifier for the localized string
7303 * @param string $component The module where the key identifier is stored,
7304 * usually expressed as the filename in the language pack without the
7305 * .php on the end but can also be written as mod/forum or grade/export/xls.
7306 * If none is specified then moodle.php is used.
7307 * @param string|object|array|int $a An object, string or number that can be used
7308 * within translation strings
7309 * @param bool $lazyload If set to true a string object is returned instead of
7310 * the string itself. The string then isn't calculated until it is first used.
7311 * @return string The localized string.
7312 * @throws coding_exception
7314 function get_string($identifier, $component = '', $a = null, $lazyload = false) {
7315 global $CFG;
7317 // If the lazy load argument has been supplied return a lang_string object
7318 // instead.
7319 // We need to make sure it is true (and a bool) as you will see below there
7320 // used to be a forth argument at one point.
7321 if ($lazyload === true) {
7322 return new lang_string($identifier, $component, $a);
7325 if ($CFG->debugdeveloper && clean_param($identifier, PARAM_STRINGID) === '') {
7326 throw new coding_exception('Invalid string identifier. The identifier cannot be empty. Please fix your get_string() call.', DEBUG_DEVELOPER);
7329 // There is now a forth argument again, this time it is a boolean however so
7330 // we can still check for the old extralocations parameter.
7331 if (!is_bool($lazyload) && !empty($lazyload)) {
7332 debugging('extralocations parameter in get_string() is not supported any more, please use standard lang locations only.');
7335 if (strpos((string)$component, '/') !== false) {
7336 debugging('The module name you passed to get_string is the deprecated format ' .
7337 'like mod/mymod or block/myblock. The correct form looks like mymod, or block_myblock.' , DEBUG_DEVELOPER);
7338 $componentpath = explode('/', $component);
7340 switch ($componentpath[0]) {
7341 case 'mod':
7342 $component = $componentpath[1];
7343 break;
7344 case 'blocks':
7345 case 'block':
7346 $component = 'block_'.$componentpath[1];
7347 break;
7348 case 'enrol':
7349 $component = 'enrol_'.$componentpath[1];
7350 break;
7351 case 'format':
7352 $component = 'format_'.$componentpath[1];
7353 break;
7354 case 'grade':
7355 $component = 'grade'.$componentpath[1].'_'.$componentpath[2];
7356 break;
7360 $result = get_string_manager()->get_string($identifier, $component, $a);
7362 // Debugging feature lets you display string identifier and component.
7363 if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
7364 $result .= ' {' . $identifier . '/' . $component . '}';
7366 return $result;
7370 * Converts an array of strings to their localized value.
7372 * @param array $array An array of strings
7373 * @param string $component The language module that these strings can be found in.
7374 * @return stdClass translated strings.
7376 function get_strings($array, $component = '') {
7377 $string = new stdClass;
7378 foreach ($array as $item) {
7379 $string->$item = get_string($item, $component);
7381 return $string;
7385 * Prints out a translated string.
7387 * Prints out a translated string using the return value from the {@link get_string()} function.
7389 * Example usage of this function when the string is in the moodle.php file:<br/>
7390 * <code>
7391 * echo '<strong>';
7392 * print_string('course');
7393 * echo '</strong>';
7394 * </code>
7396 * Example usage of this function when the string is not in the moodle.php file:<br/>
7397 * <code>
7398 * echo '<h1>';
7399 * print_string('typecourse', 'calendar');
7400 * echo '</h1>';
7401 * </code>
7403 * @category string
7404 * @param string $identifier The key identifier for the localized string
7405 * @param string $component The module where the key identifier is stored. If none is specified then moodle.php is used.
7406 * @param string|object|array $a An object, string or number that can be used within translation strings
7408 function print_string($identifier, $component = '', $a = null) {
7409 echo get_string($identifier, $component, $a);
7413 * Returns a list of charset codes
7415 * Returns a list of charset codes. It's hardcoded, so they should be added manually
7416 * (checking that such charset is supported by the texlib library!)
7418 * @return array And associative array with contents in the form of charset => charset
7420 function get_list_of_charsets() {
7422 $charsets = array(
7423 'EUC-JP' => 'EUC-JP',
7424 'ISO-2022-JP'=> 'ISO-2022-JP',
7425 'ISO-8859-1' => 'ISO-8859-1',
7426 'SHIFT-JIS' => 'SHIFT-JIS',
7427 'GB2312' => 'GB2312',
7428 'GB18030' => 'GB18030', // GB18030 not supported by typo and mbstring.
7429 'UTF-8' => 'UTF-8');
7431 asort($charsets);
7433 return $charsets;
7437 * Returns a list of valid and compatible themes
7439 * @return array
7441 function get_list_of_themes() {
7442 global $CFG;
7444 $themes = array();
7446 if (!empty($CFG->themelist)) { // Use admin's list of themes.
7447 $themelist = explode(',', $CFG->themelist);
7448 } else {
7449 $themelist = array_keys(core_component::get_plugin_list("theme"));
7452 foreach ($themelist as $key => $themename) {
7453 $theme = theme_config::load($themename);
7454 $themes[$themename] = $theme;
7457 core_collator::asort_objects_by_method($themes, 'get_theme_name');
7459 return $themes;
7463 * Factory function for emoticon_manager
7465 * @return emoticon_manager singleton
7467 function get_emoticon_manager() {
7468 static $singleton = null;
7470 if (is_null($singleton)) {
7471 $singleton = new emoticon_manager();
7474 return $singleton;
7478 * Provides core support for plugins that have to deal with emoticons (like HTML editor or emoticon filter).
7480 * Whenever this manager mentiones 'emoticon object', the following data
7481 * structure is expected: stdClass with properties text, imagename, imagecomponent,
7482 * altidentifier and altcomponent
7484 * @see admin_setting_emoticons
7486 * @copyright 2010 David Mudrak
7487 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
7489 class emoticon_manager {
7492 * Returns the currently enabled emoticons
7494 * @param boolean $selectable - If true, only return emoticons that should be selectable from a list.
7495 * @return array of emoticon objects
7497 public function get_emoticons($selectable = false) {
7498 global $CFG;
7499 $notselectable = ['martin', 'egg'];
7501 if (empty($CFG->emoticons)) {
7502 return array();
7505 $emoticons = $this->decode_stored_config($CFG->emoticons);
7507 if (!is_array($emoticons)) {
7508 // Something is wrong with the format of stored setting.
7509 debugging('Invalid format of emoticons setting, please resave the emoticons settings form', DEBUG_NORMAL);
7510 return array();
7512 if ($selectable) {
7513 foreach ($emoticons as $index => $emote) {
7514 if (in_array($emote->altidentifier, $notselectable)) {
7515 // Skip this one.
7516 unset($emoticons[$index]);
7521 return $emoticons;
7525 * Converts emoticon object into renderable pix_emoticon object
7527 * @param stdClass $emoticon emoticon object
7528 * @param array $attributes explicit HTML attributes to set
7529 * @return pix_emoticon
7531 public function prepare_renderable_emoticon(stdClass $emoticon, array $attributes = array()) {
7532 $stringmanager = get_string_manager();
7533 if ($stringmanager->string_exists($emoticon->altidentifier, $emoticon->altcomponent)) {
7534 $alt = get_string($emoticon->altidentifier, $emoticon->altcomponent);
7535 } else {
7536 $alt = s($emoticon->text);
7538 return new pix_emoticon($emoticon->imagename, $alt, $emoticon->imagecomponent, $attributes);
7542 * Encodes the array of emoticon objects into a string storable in config table
7544 * @see self::decode_stored_config()
7545 * @param array $emoticons array of emtocion objects
7546 * @return string
7548 public function encode_stored_config(array $emoticons) {
7549 return json_encode($emoticons);
7553 * Decodes the string into an array of emoticon objects
7555 * @see self::encode_stored_config()
7556 * @param string $encoded
7557 * @return array|null
7559 public function decode_stored_config($encoded) {
7560 $decoded = json_decode($encoded);
7561 if (!is_array($decoded)) {
7562 return null;
7564 return $decoded;
7568 * Returns default set of emoticons supported by Moodle
7570 * @return array of sdtClasses
7572 public function default_emoticons() {
7573 return array(
7574 $this->prepare_emoticon_object(":-)", 's/smiley', 'smiley'),
7575 $this->prepare_emoticon_object(":)", 's/smiley', 'smiley'),
7576 $this->prepare_emoticon_object(":-D", 's/biggrin', 'biggrin'),
7577 $this->prepare_emoticon_object(";-)", 's/wink', 'wink'),
7578 $this->prepare_emoticon_object(":-/", 's/mixed', 'mixed'),
7579 $this->prepare_emoticon_object("V-.", 's/thoughtful', 'thoughtful'),
7580 $this->prepare_emoticon_object(":-P", 's/tongueout', 'tongueout'),
7581 $this->prepare_emoticon_object(":-p", 's/tongueout', 'tongueout'),
7582 $this->prepare_emoticon_object("B-)", 's/cool', 'cool'),
7583 $this->prepare_emoticon_object("^-)", 's/approve', 'approve'),
7584 $this->prepare_emoticon_object("8-)", 's/wideeyes', 'wideeyes'),
7585 $this->prepare_emoticon_object(":o)", 's/clown', 'clown'),
7586 $this->prepare_emoticon_object(":-(", 's/sad', 'sad'),
7587 $this->prepare_emoticon_object(":(", 's/sad', 'sad'),
7588 $this->prepare_emoticon_object("8-.", 's/shy', 'shy'),
7589 $this->prepare_emoticon_object(":-I", 's/blush', 'blush'),
7590 $this->prepare_emoticon_object(":-X", 's/kiss', 'kiss'),
7591 $this->prepare_emoticon_object("8-o", 's/surprise', 'surprise'),
7592 $this->prepare_emoticon_object("P-|", 's/blackeye', 'blackeye'),
7593 $this->prepare_emoticon_object("8-[", 's/angry', 'angry'),
7594 $this->prepare_emoticon_object("(grr)", 's/angry', 'angry'),
7595 $this->prepare_emoticon_object("xx-P", 's/dead', 'dead'),
7596 $this->prepare_emoticon_object("|-.", 's/sleepy', 'sleepy'),
7597 $this->prepare_emoticon_object("}-]", 's/evil', 'evil'),
7598 $this->prepare_emoticon_object("(h)", 's/heart', 'heart'),
7599 $this->prepare_emoticon_object("(heart)", 's/heart', 'heart'),
7600 $this->prepare_emoticon_object("(y)", 's/yes', 'yes', 'core'),
7601 $this->prepare_emoticon_object("(n)", 's/no', 'no', 'core'),
7602 $this->prepare_emoticon_object("(martin)", 's/martin', 'martin'),
7603 $this->prepare_emoticon_object("( )", 's/egg', 'egg'),
7608 * Helper method preparing the stdClass with the emoticon properties
7610 * @param string|array $text or array of strings
7611 * @param string $imagename to be used by {@link pix_emoticon}
7612 * @param string $altidentifier alternative string identifier, null for no alt
7613 * @param string $altcomponent where the alternative string is defined
7614 * @param string $imagecomponent to be used by {@link pix_emoticon}
7615 * @return stdClass
7617 protected function prepare_emoticon_object($text, $imagename, $altidentifier = null,
7618 $altcomponent = 'core_pix', $imagecomponent = 'core') {
7619 return (object)array(
7620 'text' => $text,
7621 'imagename' => $imagename,
7622 'imagecomponent' => $imagecomponent,
7623 'altidentifier' => $altidentifier,
7624 'altcomponent' => $altcomponent,
7629 // ENCRYPTION.
7632 * rc4encrypt
7634 * @param string $data Data to encrypt.
7635 * @return string The now encrypted data.
7637 function rc4encrypt($data) {
7638 return endecrypt(get_site_identifier(), $data, '');
7642 * rc4decrypt
7644 * @param string $data Data to decrypt.
7645 * @return string The now decrypted data.
7647 function rc4decrypt($data) {
7648 return endecrypt(get_site_identifier(), $data, 'de');
7652 * Based on a class by Mukul Sabharwal [mukulsabharwal @ yahoo.com]
7654 * @todo Finish documenting this function
7656 * @param string $pwd The password to use when encrypting or decrypting
7657 * @param string $data The data to be decrypted/encrypted
7658 * @param string $case Either 'de' for decrypt or '' for encrypt
7659 * @return string
7661 function endecrypt ($pwd, $data, $case) {
7663 if ($case == 'de') {
7664 $data = urldecode($data);
7667 $key[] = '';
7668 $box[] = '';
7669 $pwdlength = strlen($pwd);
7671 for ($i = 0; $i <= 255; $i++) {
7672 $key[$i] = ord(substr($pwd, ($i % $pwdlength), 1));
7673 $box[$i] = $i;
7676 $x = 0;
7678 for ($i = 0; $i <= 255; $i++) {
7679 $x = ($x + $box[$i] + $key[$i]) % 256;
7680 $tempswap = $box[$i];
7681 $box[$i] = $box[$x];
7682 $box[$x] = $tempswap;
7685 $cipher = '';
7687 $a = 0;
7688 $j = 0;
7690 for ($i = 0; $i < strlen($data); $i++) {
7691 $a = ($a + 1) % 256;
7692 $j = ($j + $box[$a]) % 256;
7693 $temp = $box[$a];
7694 $box[$a] = $box[$j];
7695 $box[$j] = $temp;
7696 $k = $box[(($box[$a] + $box[$j]) % 256)];
7697 $cipherby = ord(substr($data, $i, 1)) ^ $k;
7698 $cipher .= chr($cipherby);
7701 if ($case == 'de') {
7702 $cipher = urldecode(urlencode($cipher));
7703 } else {
7704 $cipher = urlencode($cipher);
7707 return $cipher;
7710 // ENVIRONMENT CHECKING.
7713 * This method validates a plug name. It is much faster than calling clean_param.
7715 * @param string $name a string that might be a plugin name.
7716 * @return bool if this string is a valid plugin name.
7718 function is_valid_plugin_name($name) {
7719 // This does not work for 'mod', bad luck, use any other type.
7720 return core_component::is_valid_plugin_name('tool', $name);
7724 * Get a list of all the plugins of a given type that define a certain API function
7725 * in a certain file. The plugin component names and function names are returned.
7727 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
7728 * @param string $function the part of the name of the function after the
7729 * frankenstyle prefix. e.g 'hook' if you are looking for functions with
7730 * names like report_courselist_hook.
7731 * @param string $file the name of file within the plugin that defines the
7732 * function. Defaults to lib.php.
7733 * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
7734 * and the function names as values (e.g. 'report_courselist_hook', 'forum_hook').
7736 function get_plugin_list_with_function($plugintype, $function, $file = 'lib.php') {
7737 global $CFG;
7739 // We don't include here as all plugin types files would be included.
7740 $plugins = get_plugins_with_function($function, $file, false);
7742 if (empty($plugins[$plugintype])) {
7743 return array();
7746 $allplugins = core_component::get_plugin_list($plugintype);
7748 // Reformat the array and include the files.
7749 $pluginfunctions = array();
7750 foreach ($plugins[$plugintype] as $pluginname => $functionname) {
7752 // Check that it has not been removed and the file is still available.
7753 if (!empty($allplugins[$pluginname])) {
7755 $filepath = $allplugins[$pluginname] . DIRECTORY_SEPARATOR . $file;
7756 if (file_exists($filepath)) {
7757 include_once($filepath);
7759 // Now that the file is loaded, we must verify the function still exists.
7760 if (function_exists($functionname)) {
7761 $pluginfunctions[$plugintype . '_' . $pluginname] = $functionname;
7762 } else {
7763 // Invalidate the cache for next run.
7764 \cache_helper::invalidate_by_definition('core', 'plugin_functions');
7770 return $pluginfunctions;
7774 * Get a list of all the plugins that define a certain API function in a certain file.
7776 * @param string $function the part of the name of the function after the
7777 * frankenstyle prefix. e.g 'hook' if you are looking for functions with
7778 * names like report_courselist_hook.
7779 * @param string $file the name of file within the plugin that defines the
7780 * function. Defaults to lib.php.
7781 * @param bool $include Whether to include the files that contain the functions or not.
7782 * @param bool $migratedtohook if true this is a deprecated lib.php callback, if hook callback is present then do nothing
7783 * @return array with [plugintype][plugin] = functionname
7785 function get_plugins_with_function($function, $file = 'lib.php', $include = true, bool $migratedtohook = false) {
7786 global $CFG;
7788 if (during_initial_install() || isset($CFG->upgraderunning)) {
7789 // API functions _must not_ be called during an installation or upgrade.
7790 return [];
7793 $plugincallback = $function;
7794 $filtermigrated = function($plugincallback, $pluginfunctions): array {
7795 foreach ($pluginfunctions as $plugintype => $plugins) {
7796 foreach ($plugins as $plugin => $unusedfunction) {
7797 $component = $plugintype . '_' . $plugin;
7798 if (\core\hook\manager::get_instance()->is_deprecated_plugin_callback($plugincallback)) {
7799 if (\core\hook\manager::get_instance()->is_deprecating_hook_present($component, $plugincallback)) {
7800 // Ignore the old callback, it is there only for older Moodle versions.
7801 unset($pluginfunctions[$plugintype][$plugin]);
7802 } else {
7803 debugging("Callback $plugincallback in $component component should be migrated to new hook callback",
7804 DEBUG_DEVELOPER);
7809 return $pluginfunctions;
7812 $cache = \cache::make('core', 'plugin_functions');
7814 // Including both although I doubt that we will find two functions definitions with the same name.
7815 // Clean the filename as cache_helper::hash_key only allows a-zA-Z0-9_.
7816 $pluginfunctions = false;
7817 if (!empty($CFG->allversionshash)) {
7818 $key = $CFG->allversionshash . '_' . $function . '_' . clean_param($file, PARAM_ALPHA);
7819 $pluginfunctions = $cache->get($key);
7821 $dirty = false;
7823 // Use the plugin manager to check that plugins are currently installed.
7824 $pluginmanager = \core_plugin_manager::instance();
7826 if ($pluginfunctions !== false) {
7828 // Checking that the files are still available.
7829 foreach ($pluginfunctions as $plugintype => $plugins) {
7831 $allplugins = \core_component::get_plugin_list($plugintype);
7832 $installedplugins = $pluginmanager->get_installed_plugins($plugintype);
7833 foreach ($plugins as $plugin => $function) {
7834 if (!isset($installedplugins[$plugin])) {
7835 // Plugin code is still present on disk but it is not installed.
7836 $dirty = true;
7837 break 2;
7840 // Cache might be out of sync with the codebase, skip the plugin if it is not available.
7841 if (empty($allplugins[$plugin])) {
7842 $dirty = true;
7843 break 2;
7846 $fileexists = file_exists($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file);
7847 if ($include && $fileexists) {
7848 // Include the files if it was requested.
7849 include_once($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file);
7850 } else if (!$fileexists) {
7851 // If the file is not available any more it should not be returned.
7852 $dirty = true;
7853 break 2;
7856 // Check if the function still exists in the file.
7857 if ($include && !function_exists($function)) {
7858 $dirty = true;
7859 break 2;
7864 // If the cache is dirty, we should fall through and let it rebuild.
7865 if (!$dirty) {
7866 if ($migratedtohook && $file === 'lib.php') {
7867 $pluginfunctions = $filtermigrated($plugincallback, $pluginfunctions);
7869 return $pluginfunctions;
7873 $pluginfunctions = array();
7875 // To fill the cached. Also, everything should continue working with cache disabled.
7876 $plugintypes = \core_component::get_plugin_types();
7877 foreach ($plugintypes as $plugintype => $unused) {
7879 // We need to include files here.
7880 $pluginswithfile = \core_component::get_plugin_list_with_file($plugintype, $file, true);
7881 $installedplugins = $pluginmanager->get_installed_plugins($plugintype);
7882 foreach ($pluginswithfile as $plugin => $notused) {
7884 if (!isset($installedplugins[$plugin])) {
7885 continue;
7888 $fullfunction = $plugintype . '_' . $plugin . '_' . $function;
7890 $pluginfunction = false;
7891 if (function_exists($fullfunction)) {
7892 // Function exists with standard name. Store, indexed by frankenstyle name of plugin.
7893 $pluginfunction = $fullfunction;
7895 } else if ($plugintype === 'mod') {
7896 // For modules, we also allow plugin without full frankenstyle but just starting with the module name.
7897 $shortfunction = $plugin . '_' . $function;
7898 if (function_exists($shortfunction)) {
7899 $pluginfunction = $shortfunction;
7903 if ($pluginfunction) {
7904 if (empty($pluginfunctions[$plugintype])) {
7905 $pluginfunctions[$plugintype] = array();
7907 $pluginfunctions[$plugintype][$plugin] = $pluginfunction;
7912 if (!empty($CFG->allversionshash)) {
7913 $cache->set($key, $pluginfunctions);
7916 if ($migratedtohook && $file === 'lib.php') {
7917 $pluginfunctions = $filtermigrated($plugincallback, $pluginfunctions);
7920 return $pluginfunctions;
7925 * Lists plugin-like directories within specified directory
7927 * This function was originally used for standard Moodle plugins, please use
7928 * new core_component::get_plugin_list() now.
7930 * This function is used for general directory listing and backwards compatility.
7932 * @param string $directory relative directory from root
7933 * @param string $exclude dir name to exclude from the list (defaults to none)
7934 * @param string $basedir full path to the base dir where $plugin resides (defaults to $CFG->dirroot)
7935 * @return array Sorted array of directory names found under the requested parameters
7937 function get_list_of_plugins($directory='mod', $exclude='', $basedir='') {
7938 global $CFG;
7940 $plugins = array();
7942 if (empty($basedir)) {
7943 $basedir = $CFG->dirroot .'/'. $directory;
7945 } else {
7946 $basedir = $basedir .'/'. $directory;
7949 if ($CFG->debugdeveloper and empty($exclude)) {
7950 // Make sure devs do not use this to list normal plugins,
7951 // this is intended for general directories that are not plugins!
7953 $subtypes = core_component::get_plugin_types();
7954 if (in_array($basedir, $subtypes)) {
7955 debugging('get_list_of_plugins() should not be used to list real plugins, use core_component::get_plugin_list() instead!', DEBUG_DEVELOPER);
7957 unset($subtypes);
7960 $ignorelist = array_flip(array_filter([
7961 'CVS',
7962 '_vti_cnf',
7963 'amd',
7964 'classes',
7965 'simpletest',
7966 'tests',
7967 'templates',
7968 'yui',
7969 $exclude,
7970 ]));
7972 if (file_exists($basedir) && filetype($basedir) == 'dir') {
7973 if (!$dirhandle = opendir($basedir)) {
7974 debugging("Directory permission error for plugin ({$directory}). Directory exists but cannot be read.", DEBUG_DEVELOPER);
7975 return array();
7977 while (false !== ($dir = readdir($dirhandle))) {
7978 if (strpos($dir, '.') === 0) {
7979 // Ignore directories starting with .
7980 // These are treated as hidden directories.
7981 continue;
7983 if (array_key_exists($dir, $ignorelist)) {
7984 // This directory features on the ignore list.
7985 continue;
7987 if (filetype($basedir .'/'. $dir) != 'dir') {
7988 continue;
7990 $plugins[] = $dir;
7992 closedir($dirhandle);
7994 if ($plugins) {
7995 asort($plugins);
7997 return $plugins;
8001 * Invoke plugin's callback functions
8003 * @param string $type plugin type e.g. 'mod'
8004 * @param string $name plugin name
8005 * @param string $feature feature name
8006 * @param string $action feature's action
8007 * @param array $params parameters of callback function, should be an array
8008 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
8009 * @param bool $migratedtohook if true this is a deprecated callback, if hook callback is present then do nothing
8010 * @return mixed
8012 * @todo Decide about to deprecate and drop plugin_callback() - MDL-30743
8014 function plugin_callback($type, $name, $feature, $action, $params = null, $default = null, bool $migratedtohook = false) {
8015 return component_callback($type . '_' . $name, $feature . '_' . $action, (array) $params, $default, $migratedtohook);
8019 * Invoke component's callback functions
8021 * @param string $component frankenstyle component name, e.g. 'mod_quiz'
8022 * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
8023 * @param array $params parameters of callback function
8024 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
8025 * @param bool $migratedtohook if true this is a deprecated callback, if hook callback is present then do nothing
8026 * @return mixed
8028 function component_callback($component, $function, array $params = array(), $default = null, bool $migratedtohook = false) {
8030 $functionname = component_callback_exists($component, $function);
8032 if ($params && (array_keys($params) !== range(0, count($params) - 1))) {
8033 // PHP 8 allows to have associative arrays in the call_user_func_array() parameters but
8034 // PHP 7 does not. Using associative arrays can result in different behavior in different PHP versions.
8035 // See https://php.watch/versions/8.0/named-parameters#named-params-call_user_func_array
8036 // This check can be removed when minimum PHP version for Moodle is raised to 8.
8037 debugging('Parameters array can not be an associative array while Moodle supports both PHP 7 and PHP 8.',
8038 DEBUG_DEVELOPER);
8039 $params = array_values($params);
8042 if ($functionname) {
8043 if ($migratedtohook) {
8044 if (\core\hook\manager::get_instance()->is_deprecated_plugin_callback($function)) {
8045 if (\core\hook\manager::get_instance()->is_deprecating_hook_present($component, $function)) {
8046 // Do not call the old lib.php callback,
8047 // it is there for compatibility with older Moodle versions only.
8048 return null;
8049 } else {
8050 debugging("Callback $function in $component component should be migrated to new hook callback",
8051 DEBUG_DEVELOPER);
8056 // Function exists, so just return function result.
8057 $ret = call_user_func_array($functionname, $params);
8058 if (is_null($ret)) {
8059 return $default;
8060 } else {
8061 return $ret;
8064 return $default;
8068 * Determine if a component callback exists and return the function name to call. Note that this
8069 * function will include the required library files so that the functioname returned can be
8070 * called directly.
8072 * @param string $component frankenstyle component name, e.g. 'mod_quiz'
8073 * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
8074 * @return mixed Complete function name to call if the callback exists or false if it doesn't.
8075 * @throws coding_exception if invalid component specfied
8077 function component_callback_exists($component, $function) {
8078 global $CFG; // This is needed for the inclusions.
8080 $cleancomponent = clean_param($component, PARAM_COMPONENT);
8081 if (empty($cleancomponent)) {
8082 throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
8084 $component = $cleancomponent;
8086 list($type, $name) = core_component::normalize_component($component);
8087 $component = $type . '_' . $name;
8089 $oldfunction = $name.'_'.$function;
8090 $function = $component.'_'.$function;
8092 $dir = core_component::get_component_directory($component);
8093 if (empty($dir)) {
8094 throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
8097 // Load library and look for function.
8098 if (file_exists($dir.'/lib.php')) {
8099 require_once($dir.'/lib.php');
8102 if (!function_exists($function) and function_exists($oldfunction)) {
8103 if ($type !== 'mod' and $type !== 'core') {
8104 debugging("Please use new function name $function instead of legacy $oldfunction", DEBUG_DEVELOPER);
8106 $function = $oldfunction;
8109 if (function_exists($function)) {
8110 return $function;
8112 return false;
8116 * Call the specified callback method on the provided class.
8118 * If the callback returns null, then the default value is returned instead.
8119 * If the class does not exist, then the default value is returned.
8121 * @param string $classname The name of the class to call upon.
8122 * @param string $methodname The name of the staticically defined method on the class.
8123 * @param array $params The arguments to pass into the method.
8124 * @param mixed $default The default value.
8125 * @return mixed The return value.
8127 function component_class_callback($classname, $methodname, array $params, $default = null) {
8128 if (!class_exists($classname)) {
8129 return $default;
8132 if (!method_exists($classname, $methodname)) {
8133 return $default;
8136 $fullfunction = $classname . '::' . $methodname;
8137 $result = call_user_func_array($fullfunction, $params);
8139 if (null === $result) {
8140 return $default;
8141 } else {
8142 return $result;
8147 * Checks whether a plugin supports a specified feature.
8149 * @param string $type Plugin type e.g. 'mod'
8150 * @param string $name Plugin name e.g. 'forum'
8151 * @param string $feature Feature code (FEATURE_xx constant)
8152 * @param mixed $default default value if feature support unknown
8153 * @return mixed Feature result (false if not supported, null if feature is unknown,
8154 * otherwise usually true but may have other feature-specific value such as array)
8155 * @throws coding_exception
8157 function plugin_supports($type, $name, $feature, $default = null) {
8158 global $CFG;
8160 if ($type === 'mod' and $name === 'NEWMODULE') {
8161 // Somebody forgot to rename the module template.
8162 return false;
8165 $component = clean_param($type . '_' . $name, PARAM_COMPONENT);
8166 if (empty($component)) {
8167 throw new coding_exception('Invalid component used in plugin_supports():' . $type . '_' . $name);
8170 $function = null;
8172 if ($type === 'mod') {
8173 // We need this special case because we support subplugins in modules,
8174 // otherwise it would end up in infinite loop.
8175 if (file_exists("$CFG->dirroot/mod/$name/lib.php")) {
8176 include_once("$CFG->dirroot/mod/$name/lib.php");
8177 $function = $component.'_supports';
8178 if (!function_exists($function)) {
8179 // Legacy non-frankenstyle function name.
8180 $function = $name.'_supports';
8184 } else {
8185 if (!$path = core_component::get_plugin_directory($type, $name)) {
8186 // Non existent plugin type.
8187 return false;
8189 if (file_exists("$path/lib.php")) {
8190 include_once("$path/lib.php");
8191 $function = $component.'_supports';
8195 if ($function and function_exists($function)) {
8196 $supports = $function($feature);
8197 if (is_null($supports)) {
8198 // Plugin does not know - use default.
8199 return $default;
8200 } else {
8201 return $supports;
8205 // Plugin does not care, so use default.
8206 return $default;
8210 * Returns true if the current version of PHP is greater that the specified one.
8212 * @todo Check PHP version being required here is it too low?
8214 * @param string $version The version of php being tested.
8215 * @return bool
8217 function check_php_version($version='5.2.4') {
8218 return (version_compare(phpversion(), $version) >= 0);
8222 * Determine if moodle installation requires update.
8224 * Checks version numbers of main code and all plugins to see
8225 * if there are any mismatches.
8227 * @param bool $checkupgradeflag check the outagelessupgrade flag to see if an upgrade is running.
8228 * @return bool
8230 function moodle_needs_upgrading($checkupgradeflag = true) {
8231 global $CFG, $DB;
8233 // Say no if there is already an upgrade running.
8234 if ($checkupgradeflag) {
8235 $lock = $DB->get_field('config', 'value', ['name' => 'outagelessupgrade']);
8236 $currentprocessrunningupgrade = (defined('CLI_UPGRADE_RUNNING') && CLI_UPGRADE_RUNNING);
8237 // If we ARE locked, but this PHP process is NOT the process running the upgrade,
8238 // We should always return false.
8239 // This means the upgrade is running from CLI somewhere, or about to.
8240 if (!empty($lock) && !$currentprocessrunningupgrade) {
8241 return false;
8245 if (empty($CFG->version)) {
8246 return true;
8249 // There is no need to purge plugininfo caches here because
8250 // these caches are not used during upgrade and they are purged after
8251 // every upgrade.
8253 if (empty($CFG->allversionshash)) {
8254 return true;
8257 $hash = core_component::get_all_versions_hash();
8259 return ($hash !== $CFG->allversionshash);
8263 * Returns the major version of this site
8265 * Moodle version numbers consist of three numbers separated by a dot, for
8266 * example 1.9.11 or 2.0.2. The first two numbers, like 1.9 or 2.0, represent so
8267 * called major version. This function extracts the major version from either
8268 * $CFG->release (default) or eventually from the $release variable defined in
8269 * the main version.php.
8271 * @param bool $fromdisk should the version if source code files be used
8272 * @return string|false the major version like '2.3', false if could not be determined
8274 function moodle_major_version($fromdisk = false) {
8275 global $CFG;
8277 if ($fromdisk) {
8278 $release = null;
8279 require($CFG->dirroot.'/version.php');
8280 if (empty($release)) {
8281 return false;
8284 } else {
8285 if (empty($CFG->release)) {
8286 return false;
8288 $release = $CFG->release;
8291 if (preg_match('/^[0-9]+\.[0-9]+/', $release, $matches)) {
8292 return $matches[0];
8293 } else {
8294 return false;
8298 // MISCELLANEOUS.
8301 * Gets the system locale
8303 * @return string Retuns the current locale.
8305 function moodle_getlocale() {
8306 global $CFG;
8308 // Fetch the correct locale based on ostype.
8309 if ($CFG->ostype == 'WINDOWS') {
8310 $stringtofetch = 'localewin';
8311 } else {
8312 $stringtofetch = 'locale';
8315 if (!empty($CFG->locale)) { // Override locale for all language packs.
8316 return $CFG->locale;
8319 return get_string($stringtofetch, 'langconfig');
8323 * Sets the system locale
8325 * @category string
8326 * @param string $locale Can be used to force a locale
8328 function moodle_setlocale($locale='') {
8329 global $CFG;
8331 static $currentlocale = ''; // Last locale caching.
8333 $oldlocale = $currentlocale;
8335 // The priority is the same as in get_string() - parameter, config, course, session, user, global language.
8336 if (!empty($locale)) {
8337 $currentlocale = $locale;
8338 } else {
8339 $currentlocale = moodle_getlocale();
8342 // Do nothing if locale already set up.
8343 if ($oldlocale == $currentlocale) {
8344 return;
8347 // Due to some strange BUG we cannot set the LC_TIME directly, so we fetch current values,
8348 // set LC_ALL and then set values again. Just wondering why we cannot set LC_ALL only??? - stronk7
8349 // Some day, numeric, monetary and other categories should be set too, I think. :-/.
8351 // Get current values.
8352 $monetary= setlocale (LC_MONETARY, 0);
8353 $numeric = setlocale (LC_NUMERIC, 0);
8354 $ctype = setlocale (LC_CTYPE, 0);
8355 if ($CFG->ostype != 'WINDOWS') {
8356 $messages= setlocale (LC_MESSAGES, 0);
8358 // Set locale to all.
8359 $result = setlocale (LC_ALL, $currentlocale);
8360 // If setting of locale fails try the other utf8 or utf-8 variant,
8361 // some operating systems support both (Debian), others just one (OSX).
8362 if ($result === false) {
8363 if (stripos($currentlocale, '.UTF-8') !== false) {
8364 $newlocale = str_ireplace('.UTF-8', '.UTF8', $currentlocale);
8365 setlocale (LC_ALL, $newlocale);
8366 } else if (stripos($currentlocale, '.UTF8') !== false) {
8367 $newlocale = str_ireplace('.UTF8', '.UTF-8', $currentlocale);
8368 setlocale (LC_ALL, $newlocale);
8371 // Set old values.
8372 setlocale (LC_MONETARY, $monetary);
8373 setlocale (LC_NUMERIC, $numeric);
8374 if ($CFG->ostype != 'WINDOWS') {
8375 setlocale (LC_MESSAGES, $messages);
8377 if ($currentlocale == 'tr_TR' or $currentlocale == 'tr_TR.UTF-8') {
8378 // To workaround a well-known PHP problem with Turkish letter Ii.
8379 setlocale (LC_CTYPE, $ctype);
8384 * Count words in a string.
8386 * Words are defined as things between whitespace.
8388 * @category string
8389 * @param string $string The text to be searched for words. May be HTML.
8390 * @param int|null $format
8391 * @return int The count of words in the specified string
8393 function count_words($string, $format = null) {
8394 // Before stripping tags, add a space after the close tag of anything that is not obviously inline.
8395 // Also, br is a special case because it definitely delimits a word, but has no close tag.
8396 $string = preg_replace('~
8397 ( # Capture the tag we match.
8398 </ # Start of close tag.
8399 (?! # Do not match any of these specific close tag names.
8400 a> | b> | del> | em> | i> |
8401 ins> | s> | small> | span> |
8402 strong> | sub> | sup> | u>
8404 \w+ # But, apart from those execptions, match any tag name.
8405 > # End of close tag.
8407 <br> | <br\s*/> # Special cases that are not close tags.
8409 ~x', '$1 ', $string); // Add a space after the close tag.
8410 if ($format !== null && $format != FORMAT_PLAIN) {
8411 // Match the usual text cleaning before display.
8412 // Ideally we should apply multilang filter only here, other filters might add extra text.
8413 $string = format_text($string, $format, ['filter' => false, 'noclean' => false, 'para' => false]);
8415 // Now remove HTML tags.
8416 $string = strip_tags($string);
8417 // Decode HTML entities.
8418 $string = html_entity_decode($string, ENT_COMPAT);
8420 // Now, the word count is the number of blocks of characters separated
8421 // by any sort of space. That seems to be the definition used by all other systems.
8422 // To be precise about what is considered to separate words:
8423 // * Anything that Unicode considers a 'Separator'
8424 // * Anything that Unicode considers a 'Control character'
8425 // * An em- or en- dash.
8426 return count(preg_split('~[\p{Z}\p{Cc}—–]+~u', $string, -1, PREG_SPLIT_NO_EMPTY));
8430 * Count letters in a string.
8432 * Letters are defined as chars not in tags and different from whitespace.
8434 * @category string
8435 * @param string $string The text to be searched for letters. May be HTML.
8436 * @param int|null $format
8437 * @return int The count of letters in the specified text.
8439 function count_letters($string, $format = null) {
8440 if ($format !== null && $format != FORMAT_PLAIN) {
8441 // Match the usual text cleaning before display.
8442 // Ideally we should apply multilang filter only here, other filters might add extra text.
8443 $string = format_text($string, $format, ['filter' => false, 'noclean' => false, 'para' => false]);
8445 $string = strip_tags($string); // Tags are out now.
8446 $string = html_entity_decode($string, ENT_COMPAT);
8447 $string = preg_replace('/[[:space:]]*/', '', $string); // Whitespace are out now.
8449 return core_text::strlen($string);
8453 * Generate and return a random string of the specified length.
8455 * @param int $length The length of the string to be created.
8456 * @return string
8458 function random_string($length=15) {
8459 $randombytes = random_bytes_emulate($length);
8460 $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
8461 $pool .= 'abcdefghijklmnopqrstuvwxyz';
8462 $pool .= '0123456789';
8463 $poollen = strlen($pool);
8464 $string = '';
8465 for ($i = 0; $i < $length; $i++) {
8466 $rand = ord($randombytes[$i]);
8467 $string .= substr($pool, ($rand%($poollen)), 1);
8469 return $string;
8473 * Generate a complex random string (useful for md5 salts)
8475 * This function is based on the above {@link random_string()} however it uses a
8476 * larger pool of characters and generates a string between 24 and 32 characters
8478 * @param int $length Optional if set generates a string to exactly this length
8479 * @return string
8481 function complex_random_string($length=null) {
8482 $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
8483 $pool .= '`~!@#%^&*()_+-=[];,./<>?:{} ';
8484 $poollen = strlen($pool);
8485 if ($length===null) {
8486 $length = floor(rand(24, 32));
8488 $randombytes = random_bytes_emulate($length);
8489 $string = '';
8490 for ($i = 0; $i < $length; $i++) {
8491 $rand = ord($randombytes[$i]);
8492 $string .= $pool[($rand%$poollen)];
8494 return $string;
8498 * Try to generates cryptographically secure pseudo-random bytes.
8500 * Note this is achieved by fallbacking between:
8501 * - PHP 7 random_bytes().
8502 * - OpenSSL openssl_random_pseudo_bytes().
8503 * - In house random generator getting its entropy from various, hard to guess, pseudo-random sources.
8505 * @param int $length requested length in bytes
8506 * @return string binary data
8508 function random_bytes_emulate($length) {
8509 global $CFG;
8510 if ($length <= 0) {
8511 debugging('Invalid random bytes length', DEBUG_DEVELOPER);
8512 return '';
8514 if (function_exists('random_bytes')) {
8515 // Use PHP 7 goodness.
8516 $hash = @random_bytes($length);
8517 if ($hash !== false) {
8518 return $hash;
8521 if (function_exists('openssl_random_pseudo_bytes')) {
8522 // If you have the openssl extension enabled.
8523 $hash = openssl_random_pseudo_bytes($length);
8524 if ($hash !== false) {
8525 return $hash;
8529 // Bad luck, there is no reliable random generator, let's just slowly hash some unique stuff that is hard to guess.
8530 $staticdata = serialize($CFG) . serialize($_SERVER);
8531 $hash = '';
8532 do {
8533 $hash .= sha1($staticdata . microtime(true) . uniqid('', true), true);
8534 } while (strlen($hash) < $length);
8536 return substr($hash, 0, $length);
8540 * Given some text (which may contain HTML) and an ideal length,
8541 * this function truncates the text neatly on a word boundary if possible
8543 * @category string
8544 * @param string $text text to be shortened
8545 * @param int $ideal ideal string length
8546 * @param boolean $exact if false, $text will not be cut mid-word
8547 * @param string $ending The string to append if the passed string is truncated
8548 * @return string $truncate shortened string
8550 function shorten_text($text, $ideal=30, $exact = false, $ending='...') {
8551 // If the plain text is shorter than the maximum length, return the whole text.
8552 if (core_text::strlen(preg_replace('/<.*?>/', '', $text)) <= $ideal) {
8553 return $text;
8556 // Splits on HTML tags. Each open/close/empty tag will be the first thing
8557 // and only tag in its 'line'.
8558 preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER);
8560 $totallength = core_text::strlen($ending);
8561 $truncate = '';
8563 // This array stores information about open and close tags and their position
8564 // in the truncated string. Each item in the array is an object with fields
8565 // ->open (true if open), ->tag (tag name in lower case), and ->pos
8566 // (byte position in truncated text).
8567 $tagdetails = array();
8569 foreach ($lines as $linematchings) {
8570 // If there is any html-tag in this line, handle it and add it (uncounted) to the output.
8571 if (!empty($linematchings[1])) {
8572 // If it's an "empty element" with or without xhtml-conform closing slash (f.e. <br/>).
8573 if (!preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $linematchings[1])) {
8574 if (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $linematchings[1], $tagmatchings)) {
8575 // Record closing tag.
8576 $tagdetails[] = (object) array(
8577 'open' => false,
8578 'tag' => core_text::strtolower($tagmatchings[1]),
8579 'pos' => core_text::strlen($truncate),
8582 } else if (preg_match('/^<\s*([^\s>!]+).*?>$/s', $linematchings[1], $tagmatchings)) {
8583 // Record opening tag.
8584 $tagdetails[] = (object) array(
8585 'open' => true,
8586 'tag' => core_text::strtolower($tagmatchings[1]),
8587 'pos' => core_text::strlen($truncate),
8589 } else if (preg_match('/^<!--\[if\s.*?\]>$/s', $linematchings[1], $tagmatchings)) {
8590 $tagdetails[] = (object) array(
8591 'open' => true,
8592 'tag' => core_text::strtolower('if'),
8593 'pos' => core_text::strlen($truncate),
8595 } else if (preg_match('/^<!--<!\[endif\]-->$/s', $linematchings[1], $tagmatchings)) {
8596 $tagdetails[] = (object) array(
8597 'open' => false,
8598 'tag' => core_text::strtolower('if'),
8599 'pos' => core_text::strlen($truncate),
8603 // Add html-tag to $truncate'd text.
8604 $truncate .= $linematchings[1];
8607 // Calculate the length of the plain text part of the line; handle entities as one character.
8608 $contentlength = core_text::strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', ' ', $linematchings[2]));
8609 if ($totallength + $contentlength > $ideal) {
8610 // The number of characters which are left.
8611 $left = $ideal - $totallength;
8612 $entitieslength = 0;
8613 // Search for html entities.
8614 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)) {
8615 // Calculate the real length of all entities in the legal range.
8616 foreach ($entities[0] as $entity) {
8617 if ($entity[1]+1-$entitieslength <= $left) {
8618 $left--;
8619 $entitieslength += core_text::strlen($entity[0]);
8620 } else {
8621 // No more characters left.
8622 break;
8626 $breakpos = $left + $entitieslength;
8628 // If the words shouldn't be cut in the middle...
8629 if (!$exact) {
8630 // Search the last occurence of a space.
8631 for (; $breakpos > 0; $breakpos--) {
8632 if ($char = core_text::substr($linematchings[2], $breakpos, 1)) {
8633 if ($char === '.' or $char === ' ') {
8634 $breakpos += 1;
8635 break;
8636 } else if (strlen($char) > 2) {
8637 // Chinese/Japanese/Korean text can be truncated at any UTF-8 character boundary.
8638 $breakpos += 1;
8639 break;
8644 if ($breakpos == 0) {
8645 // This deals with the test_shorten_text_no_spaces case.
8646 $breakpos = $left + $entitieslength;
8647 } else if ($breakpos > $left + $entitieslength) {
8648 // This deals with the previous for loop breaking on the first char.
8649 $breakpos = $left + $entitieslength;
8652 $truncate .= core_text::substr($linematchings[2], 0, $breakpos);
8653 // Maximum length is reached, so get off the loop.
8654 break;
8655 } else {
8656 $truncate .= $linematchings[2];
8657 $totallength += $contentlength;
8660 // If the maximum length is reached, get off the loop.
8661 if ($totallength >= $ideal) {
8662 break;
8666 // Add the defined ending to the text.
8667 $truncate .= $ending;
8669 // Now calculate the list of open html tags based on the truncate position.
8670 $opentags = array();
8671 foreach ($tagdetails as $taginfo) {
8672 if ($taginfo->open) {
8673 // Add tag to the beginning of $opentags list.
8674 array_unshift($opentags, $taginfo->tag);
8675 } else {
8676 // Can have multiple exact same open tags, close the last one.
8677 $pos = array_search($taginfo->tag, array_reverse($opentags, true));
8678 if ($pos !== false) {
8679 unset($opentags[$pos]);
8684 // Close all unclosed html-tags.
8685 foreach ($opentags as $tag) {
8686 if ($tag === 'if') {
8687 $truncate .= '<!--<![endif]-->';
8688 } else {
8689 $truncate .= '</' . $tag . '>';
8693 return $truncate;
8697 * Shortens a given filename by removing characters positioned after the ideal string length.
8698 * When the filename is too long, the file cannot be created on the filesystem due to exceeding max byte size.
8699 * Limiting the filename to a certain size (considering multibyte characters) will prevent this.
8701 * @param string $filename file name
8702 * @param int $length ideal string length
8703 * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness.
8704 * @return string $shortened shortened file name
8706 function shorten_filename($filename, $length = MAX_FILENAME_SIZE, $includehash = false) {
8707 $shortened = $filename;
8708 // Extract a part of the filename if it's char size exceeds the ideal string length.
8709 if (core_text::strlen($filename) > $length) {
8710 // Exclude extension if present in filename.
8711 $mimetypes = get_mimetypes_array();
8712 $extension = pathinfo($filename, PATHINFO_EXTENSION);
8713 if ($extension && !empty($mimetypes[$extension])) {
8714 $basename = pathinfo($filename, PATHINFO_FILENAME);
8715 $hash = empty($includehash) ? '' : ' - ' . substr(sha1($basename), 0, 10);
8716 $shortened = core_text::substr($basename, 0, $length - strlen($hash)) . $hash;
8717 $shortened .= '.' . $extension;
8718 } else {
8719 $hash = empty($includehash) ? '' : ' - ' . substr(sha1($filename), 0, 10);
8720 $shortened = core_text::substr($filename, 0, $length - strlen($hash)) . $hash;
8723 return $shortened;
8727 * Shortens a given array of filenames by removing characters positioned after the ideal string length.
8729 * @param array $path The paths to reduce the length.
8730 * @param int $length Ideal string length
8731 * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness.
8732 * @return array $result Shortened paths in array.
8734 function shorten_filenames(array $path, $length = MAX_FILENAME_SIZE, $includehash = false) {
8735 $result = null;
8737 $result = array_reduce($path, function($carry, $singlepath) use ($length, $includehash) {
8738 $carry[] = shorten_filename($singlepath, $length, $includehash);
8739 return $carry;
8740 }, []);
8742 return $result;
8746 * Given dates in seconds, how many weeks is the date from startdate
8747 * The first week is 1, the second 2 etc ...
8749 * @param int $startdate Timestamp for the start date
8750 * @param int $thedate Timestamp for the end date
8751 * @return string
8753 function getweek ($startdate, $thedate) {
8754 if ($thedate < $startdate) {
8755 return 0;
8758 return floor(($thedate - $startdate) / WEEKSECS) + 1;
8762 * Returns a randomly generated password of length $maxlen. inspired by
8764 * {@link http://www.phpbuilder.com/columns/jesus19990502.php3} and
8765 * {@link http://es2.php.net/manual/en/function.str-shuffle.php#73254}
8767 * @param int $maxlen The maximum size of the password being generated.
8768 * @return string
8770 function generate_password($maxlen=10) {
8771 global $CFG;
8773 if (empty($CFG->passwordpolicy)) {
8774 $fillers = PASSWORD_DIGITS;
8775 $wordlist = file($CFG->wordlist);
8776 $word1 = trim($wordlist[rand(0, count($wordlist) - 1)]);
8777 $word2 = trim($wordlist[rand(0, count($wordlist) - 1)]);
8778 $filler1 = $fillers[rand(0, strlen($fillers) - 1)];
8779 $password = $word1 . $filler1 . $word2;
8780 } else {
8781 $minlen = !empty($CFG->minpasswordlength) ? $CFG->minpasswordlength : 0;
8782 $digits = $CFG->minpassworddigits;
8783 $lower = $CFG->minpasswordlower;
8784 $upper = $CFG->minpasswordupper;
8785 $nonalphanum = $CFG->minpasswordnonalphanum;
8786 $total = $lower + $upper + $digits + $nonalphanum;
8787 // Var minlength should be the greater one of the two ( $minlen and $total ).
8788 $minlen = $minlen < $total ? $total : $minlen;
8789 // Var maxlen can never be smaller than minlen.
8790 $maxlen = $minlen > $maxlen ? $minlen : $maxlen;
8791 $additional = $maxlen - $total;
8793 // Make sure we have enough characters to fulfill
8794 // complexity requirements.
8795 $passworddigits = PASSWORD_DIGITS;
8796 while ($digits > strlen($passworddigits)) {
8797 $passworddigits .= PASSWORD_DIGITS;
8799 $passwordlower = PASSWORD_LOWER;
8800 while ($lower > strlen($passwordlower)) {
8801 $passwordlower .= PASSWORD_LOWER;
8803 $passwordupper = PASSWORD_UPPER;
8804 while ($upper > strlen($passwordupper)) {
8805 $passwordupper .= PASSWORD_UPPER;
8807 $passwordnonalphanum = PASSWORD_NONALPHANUM;
8808 while ($nonalphanum > strlen($passwordnonalphanum)) {
8809 $passwordnonalphanum .= PASSWORD_NONALPHANUM;
8812 // Now mix and shuffle it all.
8813 $password = str_shuffle (substr(str_shuffle ($passwordlower), 0, $lower) .
8814 substr(str_shuffle ($passwordupper), 0, $upper) .
8815 substr(str_shuffle ($passworddigits), 0, $digits) .
8816 substr(str_shuffle ($passwordnonalphanum), 0 , $nonalphanum) .
8817 substr(str_shuffle ($passwordlower .
8818 $passwordupper .
8819 $passworddigits .
8820 $passwordnonalphanum), 0 , $additional));
8823 return substr ($password, 0, $maxlen);
8827 * Given a float, prints it nicely.
8828 * Localized floats must not be used in calculations!
8830 * The stripzeros feature is intended for making numbers look nicer in small
8831 * areas where it is not necessary to indicate the degree of accuracy by showing
8832 * ending zeros. If you turn it on with $decimalpoints set to 3, for example,
8833 * then it will display '5.4' instead of '5.400' or '5' instead of '5.000'.
8835 * @param float $float The float to print
8836 * @param int $decimalpoints The number of decimal places to print. -1 is a special value for auto detect (full precision).
8837 * @param bool $localized use localized decimal separator
8838 * @param bool $stripzeros If true, removes final zeros after decimal point. It will be ignored and the trailing zeros after
8839 * the decimal point are always striped if $decimalpoints is -1.
8840 * @return string locale float
8842 function format_float($float, $decimalpoints=1, $localized=true, $stripzeros=false) {
8843 if (is_null($float)) {
8844 return '';
8846 if ($localized) {
8847 $separator = get_string('decsep', 'langconfig');
8848 } else {
8849 $separator = '.';
8851 if ($decimalpoints == -1) {
8852 // The following counts the number of decimals.
8853 // It is safe as both floatval() and round() functions have same behaviour when non-numeric values are provided.
8854 $floatval = floatval($float);
8855 for ($decimalpoints = 0; $floatval != round($float, $decimalpoints); $decimalpoints++);
8858 $result = number_format($float, $decimalpoints, $separator, '');
8859 if ($stripzeros && $decimalpoints > 0) {
8860 // Remove zeros and final dot if not needed.
8861 // However, only do this if there is a decimal point!
8862 $result = preg_replace('~(' . preg_quote($separator, '~') . ')?0+$~', '', $result);
8864 return $result;
8868 * Converts locale specific floating point/comma number back to standard PHP float value
8869 * Do NOT try to do any math operations before this conversion on any user submitted floats!
8871 * @param string $localefloat locale aware float representation
8872 * @param bool $strict If true, then check the input and return false if it is not a valid number.
8873 * @return mixed float|bool - false or the parsed float.
8875 function unformat_float($localefloat, $strict = false) {
8876 $localefloat = trim((string)$localefloat);
8878 if ($localefloat == '') {
8879 return null;
8882 $localefloat = str_replace(' ', '', $localefloat); // No spaces - those might be used as thousand separators.
8883 $localefloat = str_replace(get_string('decsep', 'langconfig'), '.', $localefloat);
8885 if ($strict && !is_numeric($localefloat)) {
8886 return false;
8889 return (float)$localefloat;
8893 * Given a simple array, this shuffles it up just like shuffle()
8894 * Unlike PHP's shuffle() this function works on any machine.
8896 * @param array $array The array to be rearranged
8897 * @return array
8899 function swapshuffle($array) {
8901 $last = count($array) - 1;
8902 for ($i = 0; $i <= $last; $i++) {
8903 $from = rand(0, $last);
8904 $curr = $array[$i];
8905 $array[$i] = $array[$from];
8906 $array[$from] = $curr;
8908 return $array;
8912 * Like {@link swapshuffle()}, but works on associative arrays
8914 * @param array $array The associative array to be rearranged
8915 * @return array
8917 function swapshuffle_assoc($array) {
8919 $newarray = array();
8920 $newkeys = swapshuffle(array_keys($array));
8922 foreach ($newkeys as $newkey) {
8923 $newarray[$newkey] = $array[$newkey];
8925 return $newarray;
8929 * Given an arbitrary array, and a number of draws,
8930 * this function returns an array with that amount
8931 * of items. The indexes are retained.
8933 * @todo Finish documenting this function
8935 * @param array $array
8936 * @param int $draws
8937 * @return array
8939 function draw_rand_array($array, $draws) {
8941 $return = array();
8943 $last = count($array);
8945 if ($draws > $last) {
8946 $draws = $last;
8949 while ($draws > 0) {
8950 $last--;
8952 $keys = array_keys($array);
8953 $rand = rand(0, $last);
8955 $return[$keys[$rand]] = $array[$keys[$rand]];
8956 unset($array[$keys[$rand]]);
8958 $draws--;
8961 return $return;
8965 * Calculate the difference between two microtimes
8967 * @param string $a The first Microtime
8968 * @param string $b The second Microtime
8969 * @return string
8971 function microtime_diff($a, $b) {
8972 list($adec, $asec) = explode(' ', $a);
8973 list($bdec, $bsec) = explode(' ', $b);
8974 return $bsec - $asec + $bdec - $adec;
8978 * Given a list (eg a,b,c,d,e) this function returns
8979 * an array of 1->a, 2->b, 3->c etc
8981 * @param string $list The string to explode into array bits
8982 * @param string $separator The separator used within the list string
8983 * @return array The now assembled array
8985 function make_menu_from_list($list, $separator=',') {
8987 $array = array_reverse(explode($separator, $list), true);
8988 foreach ($array as $key => $item) {
8989 $outarray[$key+1] = trim($item);
8991 return $outarray;
8995 * Creates an array that represents all the current grades that
8996 * can be chosen using the given grading type.
8998 * Negative numbers
8999 * are scales, zero is no grade, and positive numbers are maximum
9000 * grades.
9002 * @todo Finish documenting this function or better deprecated this completely!
9004 * @param int $gradingtype
9005 * @return array
9007 function make_grades_menu($gradingtype) {
9008 global $DB;
9010 $grades = array();
9011 if ($gradingtype < 0) {
9012 if ($scale = $DB->get_record('scale', array('id'=> (-$gradingtype)))) {
9013 return make_menu_from_list($scale->scale);
9015 } else if ($gradingtype > 0) {
9016 for ($i=$gradingtype; $i>=0; $i--) {
9017 $grades[$i] = $i .' / '. $gradingtype;
9019 return $grades;
9021 return $grades;
9025 * make_unique_id_code
9027 * @todo Finish documenting this function
9029 * @uses $_SERVER
9030 * @param string $extra Extra string to append to the end of the code
9031 * @return string
9033 function make_unique_id_code($extra = '') {
9035 $hostname = 'unknownhost';
9036 if (!empty($_SERVER['HTTP_HOST'])) {
9037 $hostname = $_SERVER['HTTP_HOST'];
9038 } else if (!empty($_ENV['HTTP_HOST'])) {
9039 $hostname = $_ENV['HTTP_HOST'];
9040 } else if (!empty($_SERVER['SERVER_NAME'])) {
9041 $hostname = $_SERVER['SERVER_NAME'];
9042 } else if (!empty($_ENV['SERVER_NAME'])) {
9043 $hostname = $_ENV['SERVER_NAME'];
9046 $date = gmdate("ymdHis");
9048 $random = random_string(6);
9050 if ($extra) {
9051 return $hostname .'+'. $date .'+'. $random .'+'. $extra;
9052 } else {
9053 return $hostname .'+'. $date .'+'. $random;
9059 * Function to check the passed address is within the passed subnet
9061 * The parameter is a comma separated string of subnet definitions.
9062 * Subnet strings can be in one of three formats:
9063 * 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn (number of bits in net mask)
9064 * 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)
9065 * 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx. (incomplete address, a bit non-technical ;-)
9066 * Code for type 1 modified from user posted comments by mediator at
9067 * {@link http://au.php.net/manual/en/function.ip2long.php}
9069 * @param string $addr The address you are checking
9070 * @param string $subnetstr The string of subnet addresses
9071 * @param bool $checkallzeros The state to whether check for 0.0.0.0
9072 * @return bool
9074 function address_in_subnet($addr, $subnetstr, $checkallzeros = false) {
9076 if ($addr == '0.0.0.0' && !$checkallzeros) {
9077 return false;
9079 $subnets = explode(',', $subnetstr);
9080 $found = false;
9081 $addr = trim($addr);
9082 $addr = cleanremoteaddr($addr, false); // Normalise.
9083 if ($addr === null) {
9084 return false;
9086 $addrparts = explode(':', $addr);
9088 $ipv6 = strpos($addr, ':');
9090 foreach ($subnets as $subnet) {
9091 $subnet = trim($subnet);
9092 if ($subnet === '') {
9093 continue;
9096 if (strpos($subnet, '/') !== false) {
9097 // 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn.
9098 list($ip, $mask) = explode('/', $subnet);
9099 $mask = trim($mask);
9100 if (!is_number($mask)) {
9101 continue; // Incorect mask number, eh?
9103 $ip = cleanremoteaddr($ip, false); // Normalise.
9104 if ($ip === null) {
9105 continue;
9107 if (strpos($ip, ':') !== false) {
9108 // IPv6.
9109 if (!$ipv6) {
9110 continue;
9112 if ($mask > 128 or $mask < 0) {
9113 continue; // Nonsense.
9115 if ($mask == 0) {
9116 return true; // Any address.
9118 if ($mask == 128) {
9119 if ($ip === $addr) {
9120 return true;
9122 continue;
9124 $ipparts = explode(':', $ip);
9125 $modulo = $mask % 16;
9126 $ipnet = array_slice($ipparts, 0, ($mask-$modulo)/16);
9127 $addrnet = array_slice($addrparts, 0, ($mask-$modulo)/16);
9128 if (implode(':', $ipnet) === implode(':', $addrnet)) {
9129 if ($modulo == 0) {
9130 return true;
9132 $pos = ($mask-$modulo)/16;
9133 $ipnet = hexdec($ipparts[$pos]);
9134 $addrnet = hexdec($addrparts[$pos]);
9135 $mask = 0xffff << (16 - $modulo);
9136 if (($addrnet & $mask) == ($ipnet & $mask)) {
9137 return true;
9141 } else {
9142 // IPv4.
9143 if ($ipv6) {
9144 continue;
9146 if ($mask > 32 or $mask < 0) {
9147 continue; // Nonsense.
9149 if ($mask == 0) {
9150 return true;
9152 if ($mask == 32) {
9153 if ($ip === $addr) {
9154 return true;
9156 continue;
9158 $mask = 0xffffffff << (32 - $mask);
9159 if (((ip2long($addr) & $mask) == (ip2long($ip) & $mask))) {
9160 return true;
9164 } else if (strpos($subnet, '-') !== false) {
9165 // 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.
9166 $parts = explode('-', $subnet);
9167 if (count($parts) != 2) {
9168 continue;
9171 if (strpos($subnet, ':') !== false) {
9172 // IPv6.
9173 if (!$ipv6) {
9174 continue;
9176 $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise.
9177 if ($ipstart === null) {
9178 continue;
9180 $ipparts = explode(':', $ipstart);
9181 $start = hexdec(array_pop($ipparts));
9182 $ipparts[] = trim($parts[1]);
9183 $ipend = cleanremoteaddr(implode(':', $ipparts), false); // Normalise.
9184 if ($ipend === null) {
9185 continue;
9187 $ipparts[7] = '';
9188 $ipnet = implode(':', $ipparts);
9189 if (strpos($addr, $ipnet) !== 0) {
9190 continue;
9192 $ipparts = explode(':', $ipend);
9193 $end = hexdec($ipparts[7]);
9195 $addrend = hexdec($addrparts[7]);
9197 if (($addrend >= $start) and ($addrend <= $end)) {
9198 return true;
9201 } else {
9202 // IPv4.
9203 if ($ipv6) {
9204 continue;
9206 $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise.
9207 if ($ipstart === null) {
9208 continue;
9210 $ipparts = explode('.', $ipstart);
9211 $ipparts[3] = trim($parts[1]);
9212 $ipend = cleanremoteaddr(implode('.', $ipparts), false); // Normalise.
9213 if ($ipend === null) {
9214 continue;
9217 if ((ip2long($addr) >= ip2long($ipstart)) and (ip2long($addr) <= ip2long($ipend))) {
9218 return true;
9222 } else {
9223 // 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx.
9224 if (strpos($subnet, ':') !== false) {
9225 // IPv6.
9226 if (!$ipv6) {
9227 continue;
9229 $parts = explode(':', $subnet);
9230 $count = count($parts);
9231 if ($parts[$count-1] === '') {
9232 unset($parts[$count-1]); // Trim trailing :'s.
9233 $count--;
9234 $subnet = implode('.', $parts);
9236 $isip = cleanremoteaddr($subnet, false); // Normalise.
9237 if ($isip !== null) {
9238 if ($isip === $addr) {
9239 return true;
9241 continue;
9242 } else if ($count > 8) {
9243 continue;
9245 $zeros = array_fill(0, 8-$count, '0');
9246 $subnet = $subnet.':'.implode(':', $zeros).'/'.($count*16);
9247 if (address_in_subnet($addr, $subnet)) {
9248 return true;
9251 } else {
9252 // IPv4.
9253 if ($ipv6) {
9254 continue;
9256 $parts = explode('.', $subnet);
9257 $count = count($parts);
9258 if ($parts[$count-1] === '') {
9259 unset($parts[$count-1]); // Trim trailing .
9260 $count--;
9261 $subnet = implode('.', $parts);
9263 if ($count == 4) {
9264 $subnet = cleanremoteaddr($subnet, false); // Normalise.
9265 if ($subnet === $addr) {
9266 return true;
9268 continue;
9269 } else if ($count > 4) {
9270 continue;
9272 $zeros = array_fill(0, 4-$count, '0');
9273 $subnet = $subnet.'.'.implode('.', $zeros).'/'.($count*8);
9274 if (address_in_subnet($addr, $subnet)) {
9275 return true;
9281 return false;
9285 * For outputting debugging info
9287 * @param string $string The string to write
9288 * @param string $eol The end of line char(s) to use
9289 * @param string $sleep Period to make the application sleep
9290 * This ensures any messages have time to display before redirect
9292 function mtrace($string, $eol="\n", $sleep=0) {
9293 global $CFG;
9295 if (isset($CFG->mtrace_wrapper) && function_exists($CFG->mtrace_wrapper)) {
9296 $fn = $CFG->mtrace_wrapper;
9297 $fn($string, $eol);
9298 return;
9299 } else if (defined('STDOUT') && !PHPUNIT_TEST && !defined('BEHAT_TEST')) {
9300 // We must explicitly call the add_line function here.
9301 // Uses of fwrite to STDOUT are not picked up by ob_start.
9302 if ($output = \core\task\logmanager::add_line("{$string}{$eol}")) {
9303 fwrite(STDOUT, $output);
9305 } else {
9306 echo $string . $eol;
9309 // Flush again.
9310 flush();
9312 // Delay to keep message on user's screen in case of subsequent redirect.
9313 if ($sleep) {
9314 sleep($sleep);
9319 * Helper to {@see mtrace()} an exception or throwable, including all relevant information.
9321 * @param Throwable $e the error to ouptput.
9323 function mtrace_exception(Throwable $e): void {
9324 $info = get_exception_info($e);
9326 $message = $info->message;
9327 if ($info->debuginfo) {
9328 $message .= "\n\n" . $info->debuginfo;
9330 if ($info->backtrace) {
9331 $message .= "\n\n" . format_backtrace($info->backtrace, true);
9334 mtrace($message);
9338 * Replace 1 or more slashes or backslashes to 1 slash
9340 * @param string $path The path to strip
9341 * @return string the path with double slashes removed
9343 function cleardoubleslashes ($path) {
9344 return preg_replace('/(\/|\\\){1,}/', '/', $path);
9348 * Is the current ip in a given list?
9350 * @param string $list
9351 * @return bool
9353 function remoteip_in_list($list) {
9354 $clientip = getremoteaddr(null);
9356 if (!$clientip) {
9357 // Ensure access on cli.
9358 return true;
9360 return \core\ip_utils::is_ip_in_subnet_list($clientip, $list);
9364 * Returns most reliable client address
9366 * @param string $default If an address can't be determined, then return this
9367 * @return string The remote IP address
9369 function getremoteaddr($default='0.0.0.0') {
9370 global $CFG;
9372 if (!isset($CFG->getremoteaddrconf)) {
9373 // This will happen, for example, before just after the upgrade, as the
9374 // user is redirected to the admin screen.
9375 $variablestoskip = GETREMOTEADDR_SKIP_DEFAULT;
9376 } else {
9377 $variablestoskip = $CFG->getremoteaddrconf;
9379 if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_CLIENT_IP)) {
9380 if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
9381 $address = cleanremoteaddr($_SERVER['HTTP_CLIENT_IP']);
9382 return $address ? $address : $default;
9385 if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR)) {
9386 if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
9387 $forwardedaddresses = explode(",", $_SERVER['HTTP_X_FORWARDED_FOR']);
9389 $forwardedaddresses = array_filter($forwardedaddresses, function($ip) {
9390 global $CFG;
9391 return !\core\ip_utils::is_ip_in_subnet_list($ip, $CFG->reverseproxyignore ?? '', ',');
9394 // Multiple proxies can append values to this header including an
9395 // untrusted original request header so we must only trust the last ip.
9396 $address = end($forwardedaddresses);
9398 if (substr_count($address, ":") > 1) {
9399 // Remove port and brackets from IPv6.
9400 if (preg_match("/\[(.*)\]:/", $address, $matches)) {
9401 $address = $matches[1];
9403 } else {
9404 // Remove port from IPv4.
9405 if (substr_count($address, ":") == 1) {
9406 $parts = explode(":", $address);
9407 $address = $parts[0];
9411 $address = cleanremoteaddr($address);
9412 return $address ? $address : $default;
9415 if (!empty($_SERVER['REMOTE_ADDR'])) {
9416 $address = cleanremoteaddr($_SERVER['REMOTE_ADDR']);
9417 return $address ? $address : $default;
9418 } else {
9419 return $default;
9424 * Cleans an ip address. Internal addresses are now allowed.
9425 * (Originally local addresses were not allowed.)
9427 * @param string $addr IPv4 or IPv6 address
9428 * @param bool $compress use IPv6 address compression
9429 * @return string normalised ip address string, null if error
9431 function cleanremoteaddr($addr, $compress=false) {
9432 $addr = trim($addr);
9434 if (strpos($addr, ':') !== false) {
9435 // Can be only IPv6.
9436 $parts = explode(':', $addr);
9437 $count = count($parts);
9439 if (strpos($parts[$count-1], '.') !== false) {
9440 // Legacy ipv4 notation.
9441 $last = array_pop($parts);
9442 $ipv4 = cleanremoteaddr($last, true);
9443 if ($ipv4 === null) {
9444 return null;
9446 $bits = explode('.', $ipv4);
9447 $parts[] = dechex($bits[0]).dechex($bits[1]);
9448 $parts[] = dechex($bits[2]).dechex($bits[3]);
9449 $count = count($parts);
9450 $addr = implode(':', $parts);
9453 if ($count < 3 or $count > 8) {
9454 return null; // Severly malformed.
9457 if ($count != 8) {
9458 if (strpos($addr, '::') === false) {
9459 return null; // Malformed.
9461 // Uncompress.
9462 $insertat = array_search('', $parts, true);
9463 $missing = array_fill(0, 1 + 8 - $count, '0');
9464 array_splice($parts, $insertat, 1, $missing);
9465 foreach ($parts as $key => $part) {
9466 if ($part === '') {
9467 $parts[$key] = '0';
9472 $adr = implode(':', $parts);
9473 if (!preg_match('/^([0-9a-f]{1,4})(:[0-9a-f]{1,4})*$/i', $adr)) {
9474 return null; // Incorrect format - sorry.
9477 // Normalise 0s and case.
9478 $parts = array_map('hexdec', $parts);
9479 $parts = array_map('dechex', $parts);
9481 $result = implode(':', $parts);
9483 if (!$compress) {
9484 return $result;
9487 if ($result === '0:0:0:0:0:0:0:0') {
9488 return '::'; // All addresses.
9491 $compressed = preg_replace('/(:0)+:0$/', '::', $result, 1);
9492 if ($compressed !== $result) {
9493 return $compressed;
9496 $compressed = preg_replace('/^(0:){2,7}/', '::', $result, 1);
9497 if ($compressed !== $result) {
9498 return $compressed;
9501 $compressed = preg_replace('/(:0){2,6}:/', '::', $result, 1);
9502 if ($compressed !== $result) {
9503 return $compressed;
9506 return $result;
9509 // First get all things that look like IPv4 addresses.
9510 $parts = array();
9511 if (!preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $addr, $parts)) {
9512 return null;
9514 unset($parts[0]);
9516 foreach ($parts as $key => $match) {
9517 if ($match > 255) {
9518 return null;
9520 $parts[$key] = (int)$match; // Normalise 0s.
9523 return implode('.', $parts);
9528 * Is IP address a public address?
9530 * @param string $ip The ip to check
9531 * @return bool true if the ip is public
9533 function ip_is_public($ip) {
9534 return (bool) filter_var($ip, FILTER_VALIDATE_IP, (FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE));
9538 * This function will make a complete copy of anything it's given,
9539 * regardless of whether it's an object or not.
9541 * @param mixed $thing Something you want cloned
9542 * @return mixed What ever it is you passed it
9544 function fullclone($thing) {
9545 return unserialize(serialize($thing));
9549 * Used to make sure that $min <= $value <= $max
9551 * Make sure that value is between min, and max
9553 * @param int $min The minimum value
9554 * @param int $value The value to check
9555 * @param int $max The maximum value
9556 * @return int
9558 function bounded_number($min, $value, $max) {
9559 if ($value < $min) {
9560 return $min;
9562 if ($value > $max) {
9563 return $max;
9565 return $value;
9569 * Check if there is a nested array within the passed array
9571 * @param array $array
9572 * @return bool true if there is a nested array false otherwise
9574 function array_is_nested($array) {
9575 foreach ($array as $value) {
9576 if (is_array($value)) {
9577 return true;
9580 return false;
9584 * get_performance_info() pairs up with init_performance_info()
9585 * loaded in setup.php. Returns an array with 'html' and 'txt'
9586 * values ready for use, and each of the individual stats provided
9587 * separately as well.
9589 * @return array
9591 function get_performance_info() {
9592 global $CFG, $PERF, $DB, $PAGE;
9594 $info = array();
9595 $info['txt'] = me() . ' '; // Holds log-friendly representation.
9597 $info['html'] = '';
9598 if (!empty($CFG->themedesignermode)) {
9599 // Attempt to avoid devs debugging peformance issues, when its caused by css building and so on.
9600 $info['html'] .= '<p><strong>Warning: Theme designer mode is enabled.</strong></p>';
9602 $info['html'] .= '<ul class="list-unstyled row mx-md-0">'; // Holds userfriendly HTML representation.
9604 $info['realtime'] = microtime_diff($PERF->starttime, microtime());
9606 $info['html'] .= '<li class="timeused col-sm-4">'.$info['realtime'].' secs</li> ';
9607 $info['txt'] .= 'time: '.$info['realtime'].'s ';
9609 // GET/POST (or NULL if $_SERVER['REQUEST_METHOD'] is undefined) is useful for txt logged information.
9610 $info['txt'] .= 'method: ' . ($_SERVER['REQUEST_METHOD'] ?? "NULL") . ' ';
9612 if (function_exists('memory_get_usage')) {
9613 $info['memory_total'] = memory_get_usage();
9614 $info['memory_growth'] = memory_get_usage() - $PERF->startmemory;
9615 $info['html'] .= '<li class="memoryused col-sm-4">RAM: '.display_size($info['memory_total']).'</li> ';
9616 $info['txt'] .= 'memory_total: '.$info['memory_total'].'B (' . display_size($info['memory_total']).') memory_growth: '.
9617 $info['memory_growth'].'B ('.display_size($info['memory_growth']).') ';
9620 if (function_exists('memory_get_peak_usage')) {
9621 $info['memory_peak'] = memory_get_peak_usage();
9622 $info['html'] .= '<li class="memoryused col-sm-4">RAM peak: '.display_size($info['memory_peak']).'</li> ';
9623 $info['txt'] .= 'memory_peak: '.$info['memory_peak'].'B (' . display_size($info['memory_peak']).') ';
9626 $info['html'] .= '</ul><ul class="list-unstyled row mx-md-0">';
9627 $inc = get_included_files();
9628 $info['includecount'] = count($inc);
9629 $info['html'] .= '<li class="included col-sm-4">Included '.$info['includecount'].' files</li> ';
9630 $info['txt'] .= 'includecount: '.$info['includecount'].' ';
9632 if (!empty($CFG->early_install_lang) or empty($PAGE)) {
9633 // We can not track more performance before installation or before PAGE init, sorry.
9634 return $info;
9637 $filtermanager = filter_manager::instance();
9638 if (method_exists($filtermanager, 'get_performance_summary')) {
9639 list($filterinfo, $nicenames) = $filtermanager->get_performance_summary();
9640 $info = array_merge($filterinfo, $info);
9641 foreach ($filterinfo as $key => $value) {
9642 $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> ";
9643 $info['txt'] .= "$key: $value ";
9647 $stringmanager = get_string_manager();
9648 if (method_exists($stringmanager, 'get_performance_summary')) {
9649 list($filterinfo, $nicenames) = $stringmanager->get_performance_summary();
9650 $info = array_merge($filterinfo, $info);
9651 foreach ($filterinfo as $key => $value) {
9652 $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> ";
9653 $info['txt'] .= "$key: $value ";
9657 $info['dbqueries'] = $DB->perf_get_reads().'/'.$DB->perf_get_writes();
9658 $info['html'] .= '<li class="dbqueries col-sm-4">DB reads/writes: '.$info['dbqueries'].'</li> ';
9659 $info['txt'] .= 'db reads/writes: '.$info['dbqueries'].' ';
9661 if ($DB->want_read_slave()) {
9662 $info['dbreads_slave'] = $DB->perf_get_reads_slave();
9663 $info['html'] .= '<li class="dbqueries col-sm-4">DB reads from slave: '.$info['dbreads_slave'].'</li> ';
9664 $info['txt'] .= 'db reads from slave: '.$info['dbreads_slave'].' ';
9667 $info['dbtime'] = round($DB->perf_get_queries_time(), 5);
9668 $info['html'] .= '<li class="dbtime col-sm-4">DB queries time: '.$info['dbtime'].' secs</li> ';
9669 $info['txt'] .= 'db queries time: ' . $info['dbtime'] . 's ';
9671 if (function_exists('posix_times')) {
9672 $ptimes = posix_times();
9673 if (is_array($ptimes)) {
9674 foreach ($ptimes as $key => $val) {
9675 $info[$key] = $ptimes[$key] - $PERF->startposixtimes[$key];
9677 $info['html'] .= "<li class=\"posixtimes col-sm-4\">ticks: $info[ticks] user: $info[utime]";
9678 $info['html'] .= "sys: $info[stime] cuser: $info[cutime] csys: $info[cstime]</li> ";
9679 $info['txt'] .= "ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime] ";
9683 // Grab the load average for the last minute.
9684 // /proc will only work under some linux configurations
9685 // while uptime is there under MacOSX/Darwin and other unices.
9686 if (is_readable('/proc/loadavg') && $loadavg = @file('/proc/loadavg')) {
9687 list($serverload) = explode(' ', $loadavg[0]);
9688 unset($loadavg);
9689 } else if ( function_exists('is_executable') && is_executable('/usr/bin/uptime') && $loadavg = `/usr/bin/uptime` ) {
9690 if (preg_match('/load averages?: (\d+[\.,:]\d+)/', $loadavg, $matches)) {
9691 $serverload = $matches[1];
9692 } else {
9693 trigger_error('Could not parse uptime output!');
9696 if (!empty($serverload)) {
9697 $info['serverload'] = $serverload;
9698 $info['html'] .= '<li class="serverload col-sm-4">Load average: '.$info['serverload'].'</li> ';
9699 $info['txt'] .= "serverload: {$info['serverload']} ";
9702 // Display size of session if session started.
9703 if ($si = \core\session\manager::get_performance_info()) {
9704 $info['sessionsize'] = $si['size'];
9705 $info['html'] .= "<li class=\"serverload col-sm-4\">" . $si['html'] . "</li>";
9706 $info['txt'] .= $si['txt'];
9709 // Display time waiting for session if applicable.
9710 if (!empty($PERF->sessionlock['wait'])) {
9711 $sessionwait = number_format($PERF->sessionlock['wait'], 3) . ' secs';
9712 $info['html'] .= html_writer::tag('li', 'Session wait: ' . $sessionwait, [
9713 'class' => 'sessionwait col-sm-4'
9715 $info['txt'] .= 'sessionwait: ' . $sessionwait . ' ';
9718 $info['html'] .= '</ul>';
9719 $html = '';
9720 if ($stats = cache_helper::get_stats()) {
9722 $table = new html_table();
9723 $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered';
9724 $table->head = ['Mode', 'Cache item', 'Static', 'H', 'M', get_string('mappingprimary', 'cache'), 'H', 'M', 'S', 'I/O'];
9725 $table->data = [];
9726 $table->align = ['left', 'left', 'left', 'right', 'right', 'left', 'right', 'right', 'right', 'right'];
9728 $text = 'Caches used (hits/misses/sets): ';
9729 $hits = 0;
9730 $misses = 0;
9731 $sets = 0;
9732 $maxstores = 0;
9734 // We want to align static caches into their own column.
9735 $hasstatic = false;
9736 foreach ($stats as $definition => $details) {
9737 $numstores = count($details['stores']);
9738 $first = key($details['stores']);
9739 if ($first !== cache_store::STATIC_ACCEL) {
9740 $numstores++; // Add a blank space for the missing static store.
9742 $maxstores = max($maxstores, $numstores);
9745 $storec = 0;
9747 while ($storec++ < ($maxstores - 2)) {
9748 if ($storec == ($maxstores - 2)) {
9749 $table->head[] = get_string('mappingfinal', 'cache');
9750 } else {
9751 $table->head[] = "Store $storec";
9753 $table->align[] = 'left';
9754 $table->align[] = 'right';
9755 $table->align[] = 'right';
9756 $table->align[] = 'right';
9757 $table->align[] = 'right';
9758 $table->head[] = 'H';
9759 $table->head[] = 'M';
9760 $table->head[] = 'S';
9761 $table->head[] = 'I/O';
9764 ksort($stats);
9766 foreach ($stats as $definition => $details) {
9767 switch ($details['mode']) {
9768 case cache_store::MODE_APPLICATION:
9769 $modeclass = 'application';
9770 $mode = ' <span title="application cache">App</span>';
9771 break;
9772 case cache_store::MODE_SESSION:
9773 $modeclass = 'session';
9774 $mode = ' <span title="session cache">Ses</span>';
9775 break;
9776 case cache_store::MODE_REQUEST:
9777 $modeclass = 'request';
9778 $mode = ' <span title="request cache">Req</span>';
9779 break;
9781 $row = [$mode, $definition];
9783 $text .= "$definition {";
9785 $storec = 0;
9786 foreach ($details['stores'] as $store => $data) {
9788 if ($storec == 0 && $store !== cache_store::STATIC_ACCEL) {
9789 $row[] = '';
9790 $row[] = '';
9791 $row[] = '';
9792 $storec++;
9795 $hits += $data['hits'];
9796 $misses += $data['misses'];
9797 $sets += $data['sets'];
9798 if ($data['hits'] == 0 and $data['misses'] > 0) {
9799 $cachestoreclass = 'nohits bg-danger';
9800 } else if ($data['hits'] < $data['misses']) {
9801 $cachestoreclass = 'lowhits bg-warning text-dark';
9802 } else {
9803 $cachestoreclass = 'hihits';
9805 $text .= "$store($data[hits]/$data[misses]/$data[sets]) ";
9806 $cell = new html_table_cell($store);
9807 $cell->attributes = ['class' => $cachestoreclass];
9808 $row[] = $cell;
9809 $cell = new html_table_cell($data['hits']);
9810 $cell->attributes = ['class' => $cachestoreclass];
9811 $row[] = $cell;
9812 $cell = new html_table_cell($data['misses']);
9813 $cell->attributes = ['class' => $cachestoreclass];
9814 $row[] = $cell;
9816 if ($store !== cache_store::STATIC_ACCEL) {
9817 // The static cache is never set.
9818 $cell = new html_table_cell($data['sets']);
9819 $cell->attributes = ['class' => $cachestoreclass];
9820 $row[] = $cell;
9822 if ($data['hits'] || $data['sets']) {
9823 if ($data['iobytes'] === cache_store::IO_BYTES_NOT_SUPPORTED) {
9824 $size = '-';
9825 } else {
9826 $size = display_size($data['iobytes'], 1, 'KB');
9827 if ($data['iobytes'] >= 10 * 1024) {
9828 $cachestoreclass = ' bg-warning text-dark';
9831 } else {
9832 $size = '';
9834 $cell = new html_table_cell($size);
9835 $cell->attributes = ['class' => $cachestoreclass];
9836 $row[] = $cell;
9838 $storec++;
9840 while ($storec++ < $maxstores) {
9841 $row[] = '';
9842 $row[] = '';
9843 $row[] = '';
9844 $row[] = '';
9845 $row[] = '';
9847 $text .= '} ';
9849 $table->data[] = $row;
9852 $html .= html_writer::table($table);
9854 // Now lets also show sub totals for each cache store.
9855 $storetotals = [];
9856 $storetotal = ['hits' => 0, 'misses' => 0, 'sets' => 0, 'iobytes' => 0];
9857 foreach ($stats as $definition => $details) {
9858 foreach ($details['stores'] as $store => $data) {
9859 if (!array_key_exists($store, $storetotals)) {
9860 $storetotals[$store] = ['hits' => 0, 'misses' => 0, 'sets' => 0, 'iobytes' => 0];
9862 $storetotals[$store]['class'] = $data['class'];
9863 $storetotals[$store]['hits'] += $data['hits'];
9864 $storetotals[$store]['misses'] += $data['misses'];
9865 $storetotals[$store]['sets'] += $data['sets'];
9866 $storetotal['hits'] += $data['hits'];
9867 $storetotal['misses'] += $data['misses'];
9868 $storetotal['sets'] += $data['sets'];
9869 if ($data['iobytes'] !== cache_store::IO_BYTES_NOT_SUPPORTED) {
9870 $storetotals[$store]['iobytes'] += $data['iobytes'];
9871 $storetotal['iobytes'] += $data['iobytes'];
9876 $table = new html_table();
9877 $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered';
9878 $table->head = [get_string('storename', 'cache'), get_string('type_cachestore', 'plugin'), 'H', 'M', 'S', 'I/O'];
9879 $table->data = [];
9880 $table->align = ['left', 'left', 'right', 'right', 'right', 'right'];
9882 ksort($storetotals);
9884 foreach ($storetotals as $store => $data) {
9885 $row = [];
9886 if ($data['hits'] == 0 and $data['misses'] > 0) {
9887 $cachestoreclass = 'nohits bg-danger';
9888 } else if ($data['hits'] < $data['misses']) {
9889 $cachestoreclass = 'lowhits bg-warning text-dark';
9890 } else {
9891 $cachestoreclass = 'hihits';
9893 $cell = new html_table_cell($store);
9894 $cell->attributes = ['class' => $cachestoreclass];
9895 $row[] = $cell;
9896 $cell = new html_table_cell($data['class']);
9897 $cell->attributes = ['class' => $cachestoreclass];
9898 $row[] = $cell;
9899 $cell = new html_table_cell($data['hits']);
9900 $cell->attributes = ['class' => $cachestoreclass];
9901 $row[] = $cell;
9902 $cell = new html_table_cell($data['misses']);
9903 $cell->attributes = ['class' => $cachestoreclass];
9904 $row[] = $cell;
9905 $cell = new html_table_cell($data['sets']);
9906 $cell->attributes = ['class' => $cachestoreclass];
9907 $row[] = $cell;
9908 if ($data['hits'] || $data['sets']) {
9909 if ($data['iobytes']) {
9910 $size = display_size($data['iobytes'], 1, 'KB');
9911 } else {
9912 $size = '-';
9914 } else {
9915 $size = '';
9917 $cell = new html_table_cell($size);
9918 $cell->attributes = ['class' => $cachestoreclass];
9919 $row[] = $cell;
9920 $table->data[] = $row;
9922 if (!empty($storetotal['iobytes'])) {
9923 $size = display_size($storetotal['iobytes'], 1, 'KB');
9924 } else if (!empty($storetotal['hits']) || !empty($storetotal['sets'])) {
9925 $size = '-';
9926 } else {
9927 $size = '';
9929 $row = [
9930 get_string('total'),
9932 $storetotal['hits'],
9933 $storetotal['misses'],
9934 $storetotal['sets'],
9935 $size,
9937 $table->data[] = $row;
9939 $html .= html_writer::table($table);
9941 $info['cachesused'] = "$hits / $misses / $sets";
9942 $info['html'] .= $html;
9943 $info['txt'] .= $text.'. ';
9944 } else {
9945 $info['cachesused'] = '0 / 0 / 0';
9946 $info['html'] .= '<div class="cachesused">Caches used (hits/misses/sets): 0/0/0</div>';
9947 $info['txt'] .= 'Caches used (hits/misses/sets): 0/0/0 ';
9950 // Display lock information if any.
9951 if (!empty($PERF->locks)) {
9952 $table = new html_table();
9953 $table->attributes['class'] = 'locktimings table table-dark table-sm w-auto table-bordered';
9954 $table->head = ['Lock', 'Waited (s)', 'Obtained', 'Held for (s)'];
9955 $table->align = ['left', 'right', 'center', 'right'];
9956 $table->data = [];
9957 $text = 'Locks (waited/obtained/held):';
9958 foreach ($PERF->locks as $locktiming) {
9959 $row = [];
9960 $row[] = s($locktiming->type . '/' . $locktiming->resource);
9961 $text .= ' ' . $locktiming->type . '/' . $locktiming->resource . ' (';
9963 // The time we had to wait to get the lock.
9964 $roundedtime = number_format($locktiming->wait, 1);
9965 $cell = new html_table_cell($roundedtime);
9966 if ($locktiming->wait > 0.5) {
9967 $cell->attributes = ['class' => 'bg-warning text-dark'];
9969 $row[] = $cell;
9970 $text .= $roundedtime . '/';
9972 // Show a tick or cross for success.
9973 $row[] = $locktiming->success ? '&#x2713;' : '&#x274c;';
9974 $text .= ($locktiming->success ? 'y' : 'n') . '/';
9976 // If applicable, show how long we held the lock before releasing it.
9977 if (property_exists($locktiming, 'held')) {
9978 $roundedtime = number_format($locktiming->held, 1);
9979 $cell = new html_table_cell($roundedtime);
9980 if ($locktiming->held > 0.5) {
9981 $cell->attributes = ['class' => 'bg-warning text-dark'];
9983 $row[] = $cell;
9984 $text .= $roundedtime;
9985 } else {
9986 $row[] = '-';
9987 $text .= '-';
9989 $text .= ')';
9991 $table->data[] = $row;
9993 $info['html'] .= html_writer::table($table);
9994 $info['txt'] .= $text . '. ';
9997 $info['html'] = '<div class="performanceinfo siteinfo container-fluid px-md-0 overflow-auto pt-3">'.$info['html'].'</div>';
9998 return $info;
10002 * Renames a file or directory to a unique name within the same directory.
10004 * This function is designed to avoid any potential race conditions, and select an unused name.
10006 * @param string $filepath Original filepath
10007 * @param string $prefix Prefix to use for the temporary name
10008 * @return string|bool New file path or false if failed
10009 * @since Moodle 3.10
10011 function rename_to_unused_name(string $filepath, string $prefix = '_temp_') {
10012 $dir = dirname($filepath);
10013 $basename = $dir . '/' . $prefix;
10014 $limit = 0;
10015 while ($limit < 100) {
10016 // Select a new name based on a random number.
10017 $newfilepath = $basename . md5(mt_rand());
10019 // Attempt a rename to that new name.
10020 if (@rename($filepath, $newfilepath)) {
10021 return $newfilepath;
10024 // The first time, do some sanity checks, maybe it is failing for a good reason and there
10025 // is no point trying 100 times if so.
10026 if ($limit === 0 && (!file_exists($filepath) || !is_writable($dir))) {
10027 return false;
10029 $limit++;
10031 return false;
10035 * Delete directory or only its content
10037 * @param string $dir directory path
10038 * @param bool $contentonly
10039 * @return bool success, true also if dir does not exist
10041 function remove_dir($dir, $contentonly=false) {
10042 if (!is_dir($dir)) {
10043 // Nothing to do.
10044 return true;
10047 if (!$contentonly) {
10048 // Start by renaming the directory; this will guarantee that other processes don't write to it
10049 // while it is in the process of being deleted.
10050 $tempdir = rename_to_unused_name($dir);
10051 if ($tempdir) {
10052 // If the rename was successful then delete the $tempdir instead.
10053 $dir = $tempdir;
10055 // If the rename fails, we will continue through and attempt to delete the directory
10056 // without renaming it since that is likely to at least delete most of the files.
10059 if (!$handle = opendir($dir)) {
10060 return false;
10062 $result = true;
10063 while (false!==($item = readdir($handle))) {
10064 if ($item != '.' && $item != '..') {
10065 if (is_dir($dir.'/'.$item)) {
10066 $result = remove_dir($dir.'/'.$item) && $result;
10067 } else {
10068 $result = unlink($dir.'/'.$item) && $result;
10072 closedir($handle);
10073 if ($contentonly) {
10074 clearstatcache(); // Make sure file stat cache is properly invalidated.
10075 return $result;
10077 $result = rmdir($dir); // If anything left the result will be false, no need for && $result.
10078 clearstatcache(); // Make sure file stat cache is properly invalidated.
10079 return $result;
10083 * Detect if an object or a class contains a given property
10084 * will take an actual object or the name of a class
10086 * @param mixed $obj Name of class or real object to test
10087 * @param string $property name of property to find
10088 * @return bool true if property exists
10090 function object_property_exists( $obj, $property ) {
10091 if (is_string( $obj )) {
10092 $properties = get_class_vars( $obj );
10093 } else {
10094 $properties = get_object_vars( $obj );
10096 return array_key_exists( $property, $properties );
10100 * Converts an object into an associative array
10102 * This function converts an object into an associative array by iterating
10103 * over its public properties. Because this function uses the foreach
10104 * construct, Iterators are respected. It works recursively on arrays of objects.
10105 * Arrays and simple values are returned as is.
10107 * If class has magic properties, it can implement IteratorAggregate
10108 * and return all available properties in getIterator()
10110 * @param mixed $var
10111 * @return array
10113 function convert_to_array($var) {
10114 $result = array();
10116 // Loop over elements/properties.
10117 foreach ($var as $key => $value) {
10118 // Recursively convert objects.
10119 if (is_object($value) || is_array($value)) {
10120 $result[$key] = convert_to_array($value);
10121 } else {
10122 // Simple values are untouched.
10123 $result[$key] = $value;
10126 return $result;
10130 * Detect a custom script replacement in the data directory that will
10131 * replace an existing moodle script
10133 * @return string|bool full path name if a custom script exists, false if no custom script exists
10135 function custom_script_path() {
10136 global $CFG, $SCRIPT;
10138 if ($SCRIPT === null) {
10139 // Probably some weird external script.
10140 return false;
10143 $scriptpath = $CFG->customscripts . $SCRIPT;
10145 // Check the custom script exists.
10146 if (file_exists($scriptpath) and is_file($scriptpath)) {
10147 return $scriptpath;
10148 } else {
10149 return false;
10154 * Returns whether or not the user object is a remote MNET user. This function
10155 * is in moodlelib because it does not rely on loading any of the MNET code.
10157 * @param object $user A valid user object
10158 * @return bool True if the user is from a remote Moodle.
10160 function is_mnet_remote_user($user) {
10161 global $CFG;
10163 if (!isset($CFG->mnet_localhost_id)) {
10164 include_once($CFG->dirroot . '/mnet/lib.php');
10165 $env = new mnet_environment();
10166 $env->init();
10167 unset($env);
10170 return (!empty($user->mnethostid) && $user->mnethostid != $CFG->mnet_localhost_id);
10174 * This function will search for browser prefereed languages, setting Moodle
10175 * to use the best one available if $SESSION->lang is undefined
10177 function setup_lang_from_browser() {
10178 global $CFG, $SESSION, $USER;
10180 if (!empty($SESSION->lang) or !empty($USER->lang) or empty($CFG->autolang)) {
10181 // Lang is defined in session or user profile, nothing to do.
10182 return;
10185 if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { // There isn't list of browser langs, nothing to do.
10186 return;
10189 // Extract and clean langs from headers.
10190 $rawlangs = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
10191 $rawlangs = str_replace('-', '_', $rawlangs); // We are using underscores.
10192 $rawlangs = explode(',', $rawlangs); // Convert to array.
10193 $langs = array();
10195 $order = 1.0;
10196 foreach ($rawlangs as $lang) {
10197 if (strpos($lang, ';') === false) {
10198 $langs[(string)$order] = $lang;
10199 $order = $order-0.01;
10200 } else {
10201 $parts = explode(';', $lang);
10202 $pos = strpos($parts[1], '=');
10203 $langs[substr($parts[1], $pos+1)] = $parts[0];
10206 krsort($langs, SORT_NUMERIC);
10208 // Look for such langs under standard locations.
10209 foreach ($langs as $lang) {
10210 // Clean it properly for include.
10211 $lang = strtolower(clean_param($lang, PARAM_SAFEDIR));
10212 if (get_string_manager()->translation_exists($lang, false)) {
10213 // Lang exists, set it in session.
10214 $SESSION->lang = $lang;
10215 // We have finished. Go out.
10216 break;
10219 return;
10223 * Check if $url matches anything in proxybypass list
10225 * Any errors just result in the proxy being used (least bad)
10227 * @param string $url url to check
10228 * @return boolean true if we should bypass the proxy
10230 function is_proxybypass( $url ) {
10231 global $CFG;
10233 // Sanity check.
10234 if (empty($CFG->proxyhost) or empty($CFG->proxybypass)) {
10235 return false;
10238 // Get the host part out of the url.
10239 if (!$host = parse_url( $url, PHP_URL_HOST )) {
10240 return false;
10243 // Get the possible bypass hosts into an array.
10244 $matches = explode( ',', $CFG->proxybypass );
10246 // Check for a match.
10247 // (IPs need to match the left hand side and hosts the right of the url,
10248 // but we can recklessly check both as there can't be a false +ve).
10249 foreach ($matches as $match) {
10250 $match = trim($match);
10252 // Try for IP match (Left side).
10253 $lhs = substr($host, 0, strlen($match));
10254 if (strcasecmp($match, $lhs)==0) {
10255 return true;
10258 // Try for host match (Right side).
10259 $rhs = substr($host, -strlen($match));
10260 if (strcasecmp($match, $rhs)==0) {
10261 return true;
10265 // Nothing matched.
10266 return false;
10270 * Check if the passed navigation is of the new style
10272 * @param mixed $navigation
10273 * @return bool true for yes false for no
10275 function is_newnav($navigation) {
10276 if (is_array($navigation) && !empty($navigation['newnav'])) {
10277 return true;
10278 } else {
10279 return false;
10284 * Checks whether the given variable name is defined as a variable within the given object.
10286 * This will NOT work with stdClass objects, which have no class variables.
10288 * @param string $var The variable name
10289 * @param object $object The object to check
10290 * @return boolean
10292 function in_object_vars($var, $object) {
10293 $classvars = get_class_vars(get_class($object));
10294 $classvars = array_keys($classvars);
10295 return in_array($var, $classvars);
10299 * Returns an array without repeated objects.
10300 * This function is similar to array_unique, but for arrays that have objects as values
10302 * @param array $array
10303 * @param bool $keepkeyassoc
10304 * @return array
10306 function object_array_unique($array, $keepkeyassoc = true) {
10307 $duplicatekeys = array();
10308 $tmp = array();
10310 foreach ($array as $key => $val) {
10311 // Convert objects to arrays, in_array() does not support objects.
10312 if (is_object($val)) {
10313 $val = (array)$val;
10316 if (!in_array($val, $tmp)) {
10317 $tmp[] = $val;
10318 } else {
10319 $duplicatekeys[] = $key;
10323 foreach ($duplicatekeys as $key) {
10324 unset($array[$key]);
10327 return $keepkeyassoc ? $array : array_values($array);
10331 * Is a userid the primary administrator?
10333 * @param int $userid int id of user to check
10334 * @return boolean
10336 function is_primary_admin($userid) {
10337 $primaryadmin = get_admin();
10339 if ($userid == $primaryadmin->id) {
10340 return true;
10341 } else {
10342 return false;
10347 * Returns the site identifier
10349 * @return string $CFG->siteidentifier, first making sure it is properly initialised.
10351 function get_site_identifier() {
10352 global $CFG;
10353 // Check to see if it is missing. If so, initialise it.
10354 if (empty($CFG->siteidentifier)) {
10355 set_config('siteidentifier', random_string(32) . $_SERVER['HTTP_HOST']);
10357 // Return it.
10358 return $CFG->siteidentifier;
10362 * Check whether the given password has no more than the specified
10363 * number of consecutive identical characters.
10365 * @param string $password password to be checked against the password policy
10366 * @param integer $maxchars maximum number of consecutive identical characters
10367 * @return bool
10369 function check_consecutive_identical_characters($password, $maxchars) {
10371 if ($maxchars < 1) {
10372 return true; // Zero 0 is to disable this check.
10374 if (strlen($password) <= $maxchars) {
10375 return true; // Too short to fail this test.
10378 $previouschar = '';
10379 $consecutivecount = 1;
10380 foreach (str_split($password) as $char) {
10381 if ($char != $previouschar) {
10382 $consecutivecount = 1;
10383 } else {
10384 $consecutivecount++;
10385 if ($consecutivecount > $maxchars) {
10386 return false; // Check failed already.
10390 $previouschar = $char;
10393 return true;
10397 * Helper function to do partial function binding.
10398 * so we can use it for preg_replace_callback, for example
10399 * this works with php functions, user functions, static methods and class methods
10400 * it returns you a callback that you can pass on like so:
10402 * $callback = partial('somefunction', $arg1, $arg2);
10403 * or
10404 * $callback = partial(array('someclass', 'somestaticmethod'), $arg1, $arg2);
10405 * or even
10406 * $obj = new someclass();
10407 * $callback = partial(array($obj, 'somemethod'), $arg1, $arg2);
10409 * and then the arguments that are passed through at calltime are appended to the argument list.
10411 * @param mixed $function a php callback
10412 * @param mixed $arg1,... $argv arguments to partially bind with
10413 * @return array Array callback
10415 function partial() {
10416 if (!class_exists('partial')) {
10418 * Used to manage function binding.
10419 * @copyright 2009 Penny Leach
10420 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
10422 class partial{
10423 /** @var array */
10424 public $values = array();
10425 /** @var string The function to call as a callback. */
10426 public $func;
10428 * Constructor
10429 * @param string $func
10430 * @param array $args
10432 public function __construct($func, $args) {
10433 $this->values = $args;
10434 $this->func = $func;
10437 * Calls the callback function.
10438 * @return mixed
10440 public function method() {
10441 $args = func_get_args();
10442 return call_user_func_array($this->func, array_merge($this->values, $args));
10446 $args = func_get_args();
10447 $func = array_shift($args);
10448 $p = new partial($func, $args);
10449 return array($p, 'method');
10453 * helper function to load up and initialise the mnet environment
10454 * this must be called before you use mnet functions.
10456 * @return mnet_environment the equivalent of old $MNET global
10458 function get_mnet_environment() {
10459 global $CFG;
10460 require_once($CFG->dirroot . '/mnet/lib.php');
10461 static $instance = null;
10462 if (empty($instance)) {
10463 $instance = new mnet_environment();
10464 $instance->init();
10466 return $instance;
10470 * during xmlrpc server code execution, any code wishing to access
10471 * information about the remote peer must use this to get it.
10473 * @return mnet_remote_client the equivalent of old $MNETREMOTE_CLIENT global
10475 function get_mnet_remote_client() {
10476 if (!defined('MNET_SERVER')) {
10477 debugging(get_string('notinxmlrpcserver', 'mnet'));
10478 return false;
10480 global $MNET_REMOTE_CLIENT;
10481 if (isset($MNET_REMOTE_CLIENT)) {
10482 return $MNET_REMOTE_CLIENT;
10484 return false;
10488 * during the xmlrpc server code execution, this will be called
10489 * to setup the object returned by {@link get_mnet_remote_client}
10491 * @param mnet_remote_client $client the client to set up
10492 * @throws moodle_exception
10494 function set_mnet_remote_client($client) {
10495 if (!defined('MNET_SERVER')) {
10496 throw new moodle_exception('notinxmlrpcserver', 'mnet');
10498 global $MNET_REMOTE_CLIENT;
10499 $MNET_REMOTE_CLIENT = $client;
10503 * return the jump url for a given remote user
10504 * this is used for rewriting forum post links in emails, etc
10506 * @param stdclass $user the user to get the idp url for
10508 function mnet_get_idp_jump_url($user) {
10509 global $CFG;
10511 static $mnetjumps = array();
10512 if (!array_key_exists($user->mnethostid, $mnetjumps)) {
10513 $idp = mnet_get_peer_host($user->mnethostid);
10514 $idpjumppath = mnet_get_app_jumppath($idp->applicationid);
10515 $mnetjumps[$user->mnethostid] = $idp->wwwroot . $idpjumppath . '?hostwwwroot=' . $CFG->wwwroot . '&wantsurl=';
10517 return $mnetjumps[$user->mnethostid];
10521 * Gets the homepage to use for the current user
10523 * @return int One of HOMEPAGE_*
10525 function get_home_page() {
10526 global $CFG;
10528 if (isloggedin() && !isguestuser() && !empty($CFG->defaulthomepage)) {
10529 // If dashboard is disabled, home will be set to default page.
10530 $defaultpage = get_default_home_page();
10531 if ($CFG->defaulthomepage == HOMEPAGE_MY) {
10532 if (!empty($CFG->enabledashboard)) {
10533 return HOMEPAGE_MY;
10534 } else {
10535 return $defaultpage;
10537 } else if ($CFG->defaulthomepage == HOMEPAGE_MYCOURSES) {
10538 return HOMEPAGE_MYCOURSES;
10539 } else {
10540 $userhomepage = (int) get_user_preferences('user_home_page_preference', $defaultpage);
10541 if (empty($CFG->enabledashboard) && $userhomepage == HOMEPAGE_MY) {
10542 // If the user was using the dashboard but it's disabled, return the default home page.
10543 $userhomepage = $defaultpage;
10545 return $userhomepage;
10548 return HOMEPAGE_SITE;
10552 * Returns the default home page to display if current one is not defined or can't be applied.
10553 * The default behaviour is to return Dashboard if it's enabled or My courses page if it isn't.
10555 * @return int The default home page.
10557 function get_default_home_page(): int {
10558 global $CFG;
10560 return !empty($CFG->enabledashboard) ? HOMEPAGE_MY : HOMEPAGE_MYCOURSES;
10564 * Gets the name of a course to be displayed when showing a list of courses.
10565 * By default this is just $course->fullname but user can configure it. The
10566 * result of this function should be passed through print_string.
10567 * @param stdClass|core_course_list_element $course Moodle course object
10568 * @return string Display name of course (either fullname or short + fullname)
10570 function get_course_display_name_for_list($course) {
10571 global $CFG;
10572 if (!empty($CFG->courselistshortnames)) {
10573 if (!($course instanceof stdClass)) {
10574 $course = (object)convert_to_array($course);
10576 return get_string('courseextendednamedisplay', '', $course);
10577 } else {
10578 return $course->fullname;
10583 * Safe analogue of unserialize() that can only parse arrays
10585 * Arrays may contain only integers or strings as both keys and values. Nested arrays are allowed.
10586 * Note: If any string (key or value) has semicolon (;) as part of the string parsing will fail.
10587 * This is a simple method to substitute unnecessary unserialize() in code and not intended to cover all possible cases.
10589 * @param string $expression
10590 * @return array|bool either parsed array or false if parsing was impossible.
10592 function unserialize_array($expression) {
10593 $subs = [];
10594 // Find nested arrays, parse them and store in $subs , substitute with special string.
10595 while (preg_match('/([\^;\}])(a:\d+:\{[^\{\}]*\})/', $expression, $matches) && strlen($matches[2]) < strlen($expression)) {
10596 $key = '--SUB' . count($subs) . '--';
10597 $subs[$key] = unserialize_array($matches[2]);
10598 if ($subs[$key] === false) {
10599 return false;
10601 $expression = str_replace($matches[2], $key . ';', $expression);
10604 // Check the expression is an array.
10605 if (!preg_match('/^a:(\d+):\{([^\}]*)\}$/', $expression, $matches1)) {
10606 return false;
10608 // Get the size and elements of an array (key;value;key;value;....).
10609 $parts = explode(';', $matches1[2]);
10610 $size = intval($matches1[1]);
10611 if (count($parts) < $size * 2 + 1) {
10612 return false;
10614 // Analyze each part and make sure it is an integer or string or a substitute.
10615 $value = [];
10616 for ($i = 0; $i < $size * 2; $i++) {
10617 if (preg_match('/^i:(\d+)$/', $parts[$i], $matches2)) {
10618 $parts[$i] = (int)$matches2[1];
10619 } else if (preg_match('/^s:(\d+):"(.*)"$/', $parts[$i], $matches3) && strlen($matches3[2]) == (int)$matches3[1]) {
10620 $parts[$i] = $matches3[2];
10621 } else if (preg_match('/^--SUB\d+--$/', $parts[$i])) {
10622 $parts[$i] = $subs[$parts[$i]];
10623 } else {
10624 return false;
10627 // Combine keys and values.
10628 for ($i = 0; $i < $size * 2; $i += 2) {
10629 $value[$parts[$i]] = $parts[$i+1];
10631 return $value;
10635 * Safe method for unserializing given input that is expected to contain only a serialized instance of an stdClass object
10637 * If any class type other than stdClass is included in the input string, it will not be instantiated and will be cast to an
10638 * stdClass object. The initial cast to array, then back to object is to ensure we are always returning the correct type,
10639 * otherwise we would return an instances of {@see __PHP_Incomplete_class} for malformed strings
10641 * @param string $input
10642 * @return stdClass
10644 function unserialize_object(string $input): stdClass {
10645 $instance = (array) unserialize($input, ['allowed_classes' => [stdClass::class]]);
10646 return (object) $instance;
10650 * The lang_string class
10652 * This special class is used to create an object representation of a string request.
10653 * It is special because processing doesn't occur until the object is first used.
10654 * The class was created especially to aid performance in areas where strings were
10655 * required to be generated but were not necessarily used.
10656 * As an example the admin tree when generated uses over 1500 strings, of which
10657 * normally only 1/3 are ever actually printed at any time.
10658 * The performance advantage is achieved by not actually processing strings that
10659 * arn't being used, as such reducing the processing required for the page.
10661 * How to use the lang_string class?
10662 * There are two methods of using the lang_string class, first through the
10663 * forth argument of the get_string function, and secondly directly.
10664 * The following are examples of both.
10665 * 1. Through get_string calls e.g.
10666 * $string = get_string($identifier, $component, $a, true);
10667 * $string = get_string('yes', 'moodle', null, true);
10668 * 2. Direct instantiation
10669 * $string = new lang_string($identifier, $component, $a, $lang);
10670 * $string = new lang_string('yes');
10672 * How do I use a lang_string object?
10673 * The lang_string object makes use of a magic __toString method so that you
10674 * are able to use the object exactly as you would use a string in most cases.
10675 * This means you are able to collect it into a variable and then directly
10676 * echo it, or concatenate it into another string, or similar.
10677 * The other thing you can do is manually get the string by calling the
10678 * lang_strings out method e.g.
10679 * $string = new lang_string('yes');
10680 * $string->out();
10681 * Also worth noting is that the out method can take one argument, $lang which
10682 * allows the developer to change the language on the fly.
10684 * When should I use a lang_string object?
10685 * The lang_string object is designed to be used in any situation where a
10686 * string may not be needed, but needs to be generated.
10687 * The admin tree is a good example of where lang_string objects should be
10688 * used.
10689 * A more practical example would be any class that requries strings that may
10690 * not be printed (after all classes get renderer by renderers and who knows
10691 * what they will do ;))
10693 * When should I not use a lang_string object?
10694 * Don't use lang_strings when you are going to use a string immediately.
10695 * There is no need as it will be processed immediately and there will be no
10696 * advantage, and in fact perhaps a negative hit as a class has to be
10697 * instantiated for a lang_string object, however get_string won't require
10698 * that.
10700 * Limitations:
10701 * 1. You cannot use a lang_string object as an array offset. Doing so will
10702 * result in PHP throwing an error. (You can use it as an object property!)
10704 * @package core
10705 * @category string
10706 * @copyright 2011 Sam Hemelryk
10707 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
10709 class lang_string {
10711 /** @var string The strings identifier */
10712 protected $identifier;
10713 /** @var string The strings component. Default '' */
10714 protected $component = '';
10715 /** @var array|stdClass Any arguments required for the string. Default null */
10716 protected $a = null;
10717 /** @var string The language to use when processing the string. Default null */
10718 protected $lang = null;
10720 /** @var string The processed string (once processed) */
10721 protected $string = null;
10724 * A special boolean. If set to true then the object has been woken up and
10725 * cannot be regenerated. If this is set then $this->string MUST be used.
10726 * @var bool
10728 protected $forcedstring = false;
10731 * Constructs a lang_string object
10733 * This function should do as little processing as possible to ensure the best
10734 * performance for strings that won't be used.
10736 * @param string $identifier The strings identifier
10737 * @param string $component The strings component
10738 * @param stdClass|array|mixed $a Any arguments the string requires
10739 * @param string $lang The language to use when processing the string.
10740 * @throws coding_exception
10742 public function __construct($identifier, $component = '', $a = null, $lang = null) {
10743 if (empty($component)) {
10744 $component = 'moodle';
10747 $this->identifier = $identifier;
10748 $this->component = $component;
10749 $this->lang = $lang;
10751 // We MUST duplicate $a to ensure that it if it changes by reference those
10752 // changes are not carried across.
10753 // To do this we always ensure $a or its properties/values are strings
10754 // and that any properties/values that arn't convertable are forgotten.
10755 if ($a !== null) {
10756 if (is_scalar($a)) {
10757 $this->a = $a;
10758 } else if ($a instanceof lang_string) {
10759 $this->a = $a->out();
10760 } else if (is_object($a) or is_array($a)) {
10761 $a = (array)$a;
10762 $this->a = array();
10763 foreach ($a as $key => $value) {
10764 // Make sure conversion errors don't get displayed (results in '').
10765 if (is_array($value)) {
10766 $this->a[$key] = '';
10767 } else if (is_object($value)) {
10768 if (method_exists($value, '__toString')) {
10769 $this->a[$key] = $value->__toString();
10770 } else {
10771 $this->a[$key] = '';
10773 } else {
10774 $this->a[$key] = (string)$value;
10780 if (debugging(false, DEBUG_DEVELOPER)) {
10781 if (clean_param($this->identifier, PARAM_STRINGID) == '') {
10782 throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please check your string definition');
10784 if (!empty($this->component) && clean_param($this->component, PARAM_COMPONENT) == '') {
10785 throw new coding_exception('Invalid string compontent. Please check your string definition');
10787 if (!get_string_manager()->string_exists($this->identifier, $this->component)) {
10788 debugging('String does not exist. Please check your string definition for '.$this->identifier.'/'.$this->component, DEBUG_DEVELOPER);
10794 * Processes the string.
10796 * This function actually processes the string, stores it in the string property
10797 * and then returns it.
10798 * You will notice that this function is VERY similar to the get_string method.
10799 * That is because it is pretty much doing the same thing.
10800 * However as this function is an upgrade it isn't as tolerant to backwards
10801 * compatibility.
10803 * @return string
10804 * @throws coding_exception
10806 protected function get_string() {
10807 global $CFG;
10809 // Check if we need to process the string.
10810 if ($this->string === null) {
10811 // Check the quality of the identifier.
10812 if ($CFG->debugdeveloper && clean_param($this->identifier, PARAM_STRINGID) === '') {
10813 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);
10816 // Process the string.
10817 $this->string = get_string_manager()->get_string($this->identifier, $this->component, $this->a, $this->lang);
10818 // Debugging feature lets you display string identifier and component.
10819 if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
10820 $this->string .= ' {' . $this->identifier . '/' . $this->component . '}';
10823 // Return the string.
10824 return $this->string;
10828 * Returns the string
10830 * @param string $lang The langauge to use when processing the string
10831 * @return string
10833 public function out($lang = null) {
10834 if ($lang !== null && $lang != $this->lang && ($this->lang == null && $lang != current_language())) {
10835 if ($this->forcedstring) {
10836 debugging('lang_string objects that have been used cannot be printed in another language. ('.$this->lang.' used)', DEBUG_DEVELOPER);
10837 return $this->get_string();
10839 $translatedstring = new lang_string($this->identifier, $this->component, $this->a, $lang);
10840 return $translatedstring->out();
10842 return $this->get_string();
10846 * Magic __toString method for printing a string
10848 * @return string
10850 public function __toString() {
10851 return $this->get_string();
10855 * Magic __set_state method used for var_export
10857 * @param array $array
10858 * @return self
10860 public static function __set_state(array $array): self {
10861 $tmp = new lang_string($array['identifier'], $array['component'], $array['a'], $array['lang']);
10862 $tmp->string = $array['string'];
10863 $tmp->forcedstring = $array['forcedstring'];
10864 return $tmp;
10868 * Prepares the lang_string for sleep and stores only the forcedstring and
10869 * string properties... the string cannot be regenerated so we need to ensure
10870 * it is generated for this.
10872 * @return string
10874 public function __sleep() {
10875 $this->get_string();
10876 $this->forcedstring = true;
10877 return array('forcedstring', 'string', 'lang');
10881 * Returns the identifier.
10883 * @return string
10885 public function get_identifier() {
10886 return $this->identifier;
10890 * Returns the component.
10892 * @return string
10894 public function get_component() {
10895 return $this->component;
10900 * Get human readable name describing the given callable.
10902 * This performs syntax check only to see if the given param looks like a valid function, method or closure.
10903 * It does not check if the callable actually exists.
10905 * @param callable|string|array $callable
10906 * @return string|bool Human readable name of callable, or false if not a valid callable.
10908 function get_callable_name($callable) {
10910 if (!is_callable($callable, true, $name)) {
10911 return false;
10913 } else {
10914 return $name;
10919 * Tries to guess if $CFG->wwwroot is publicly accessible or not.
10920 * Never put your faith on this function and rely on its accuracy as there might be false positives.
10921 * It just performs some simple checks, and mainly is used for places where we want to hide some options
10922 * such as site registration when $CFG->wwwroot is not publicly accessible.
10923 * Good thing is there is no false negative.
10924 * Note that it's possible to force the result of this check by specifying $CFG->site_is_public in config.php
10926 * @return bool
10928 function site_is_public() {
10929 global $CFG;
10931 // Return early if site admin has forced this setting.
10932 if (isset($CFG->site_is_public)) {
10933 return (bool)$CFG->site_is_public;
10936 $host = parse_url($CFG->wwwroot, PHP_URL_HOST);
10938 if ($host === 'localhost' || preg_match('|^127\.\d+\.\d+\.\d+$|', $host)) {
10939 $ispublic = false;
10940 } else if (\core\ip_utils::is_ip_address($host) && !ip_is_public($host)) {
10941 $ispublic = false;
10942 } else if (($address = \core\ip_utils::get_ip_address($host)) && !ip_is_public($address)) {
10943 $ispublic = false;
10944 } else {
10945 $ispublic = true;
10948 return $ispublic;