2 // This file is part of Moodle - http://moodle.org/
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
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
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.
37 * Time constant - the number of seconds in a year
39 define('YEARSECS', 31536000);
42 * Time constant - the number of seconds in a week
44 define('WEEKSECS', 604800);
47 * Time constant - the number of seconds in a day
49 define('DAYSECS', 86400);
52 * Time constant - the number of seconds in an hour
54 define('HOURSECS', 3600);
57 * Time constant - the number of seconds in a minute
59 define('MINSECS', 60);
62 * Time constant - the number of minutes in a day
64 define('DAYMINS', 1440);
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.
75 * PARAM_ALPHA - contains only English ascii letters [a-zA-Z].
77 define('PARAM_ALPHA', 'alpha');
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');
86 * PARAM_ALPHANUM - expected numbers 0-9 and English ascii letters [a-zA-Z] only.
88 define('PARAM_ALPHANUM', 'alphanum');
91 * PARAM_ALPHANUMEXT - expected numbers 0-9, letters (English ascii letters [a-zA-Z]) and _- only.
93 define('PARAM_ALPHANUMEXT', 'alphanumext');
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');
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);
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);
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', '.,;:!?_-+/*@#&$');
401 * Required password pepper entropy.
403 define ('PEPPER_ENTROPY', 112);
405 // Feature constants.
406 // Used for plugin_supports() to report features that are, or are not, supported by a module.
408 /** True if module can provide a grade */
409 define('FEATURE_GRADE_HAS_GRADE', 'grade_has_grade');
410 /** True if module supports outcomes */
411 define('FEATURE_GRADE_OUTCOMES', 'outcomes');
412 /** True if module supports advanced grading methods */
413 define('FEATURE_ADVANCED_GRADING', 'grade_advanced_grading');
414 /** True if module controls the grade visibility over the gradebook */
415 define('FEATURE_CONTROLS_GRADE_VISIBILITY', 'controlsgradevisbility');
416 /** True if module supports plagiarism plugins */
417 define('FEATURE_PLAGIARISM', 'plagiarism');
419 /** True if module has code to track whether somebody viewed it */
420 define('FEATURE_COMPLETION_TRACKS_VIEWS', 'completion_tracks_views');
421 /** True if module has custom completion rules */
422 define('FEATURE_COMPLETION_HAS_RULES', 'completion_has_rules');
424 /** True if module has no 'view' page (like label) */
425 define('FEATURE_NO_VIEW_LINK', 'viewlink');
426 /** True (which is default) if the module wants support for setting the ID number for grade calculation purposes. */
427 define('FEATURE_IDNUMBER', 'idnumber');
428 /** True if module supports groups */
429 define('FEATURE_GROUPS', 'groups');
430 /** True if module supports groupings */
431 define('FEATURE_GROUPINGS', 'groupings');
433 * True if module supports groupmembersonly (which no longer exists)
434 * @deprecated Since Moodle 2.8
436 define('FEATURE_GROUPMEMBERSONLY', 'groupmembersonly');
438 /** Type of module */
439 define('FEATURE_MOD_ARCHETYPE', 'mod_archetype');
440 /** True if module supports intro editor */
441 define('FEATURE_MOD_INTRO', 'mod_intro');
442 /** True if module has default completion */
443 define('FEATURE_MODEDIT_DEFAULT_COMPLETION', 'modedit_default_completion');
445 define('FEATURE_COMMENT', 'comment');
447 define('FEATURE_RATE', 'rate');
448 /** True if module supports backup/restore of moodle2 format */
449 define('FEATURE_BACKUP_MOODLE2', 'backup_moodle2');
451 /** True if module can show description on course main page */
452 define('FEATURE_SHOW_DESCRIPTION', 'showdescription');
454 /** True if module uses the question bank */
455 define('FEATURE_USES_QUESTIONS', 'usesquestions');
458 * Maximum filename char size
460 define('MAX_FILENAME_SIZE', 100);
462 /** Unspecified module archetype */
463 define('MOD_ARCHETYPE_OTHER', 0);
464 /** Resource-like type module */
465 define('MOD_ARCHETYPE_RESOURCE', 1);
466 /** Assignment module archetype */
467 define('MOD_ARCHETYPE_ASSIGNMENT', 2);
468 /** System (not user-addable) module archetype */
469 define('MOD_ARCHETYPE_SYSTEM', 3);
471 /** Type of module */
472 define('FEATURE_MOD_PURPOSE', 'mod_purpose');
473 /** Module purpose administration */
474 define('MOD_PURPOSE_ADMINISTRATION', 'administration');
475 /** Module purpose assessment */
476 define('MOD_PURPOSE_ASSESSMENT', 'assessment');
477 /** Module purpose communication */
478 define('MOD_PURPOSE_COLLABORATION', 'collaboration');
479 /** Module purpose communication */
480 define('MOD_PURPOSE_COMMUNICATION', 'communication');
481 /** Module purpose content */
482 define('MOD_PURPOSE_CONTENT', 'content');
483 /** Module purpose interface */
484 define('MOD_PURPOSE_INTERFACE', 'interface');
485 /** Module purpose other */
486 define('MOD_PURPOSE_OTHER', 'other');
489 * Security token used for allowing access
490 * from external application such as web services.
491 * Scripts do not use any session, performance is relatively
492 * low because we need to load access info in each request.
493 * Scripts are executed in parallel.
495 define('EXTERNAL_TOKEN_PERMANENT', 0);
498 * Security token used for allowing access
499 * of embedded applications, the code is executed in the
500 * active user session. Token is invalidated after user logs out.
501 * Scripts are executed serially - normal session locking is used.
503 define('EXTERNAL_TOKEN_EMBEDDED', 1);
506 * The home page should be the site home
508 define('HOMEPAGE_SITE', 0);
510 * The home page should be the users my page
512 define('HOMEPAGE_MY', 1);
514 * The home page can be chosen by the user
516 define('HOMEPAGE_USER', 2);
518 * The home page should be the users my courses page
520 define('HOMEPAGE_MYCOURSES', 3);
523 * URL of the Moodle sites registration portal.
525 defined('HUB_MOODLEORGHUBURL') ||
define('HUB_MOODLEORGHUBURL', 'https://stats.moodle.org');
528 * URL of the statistic server public key.
530 defined('HUB_STATSPUBLICKEY') ||
define('HUB_STATSPUBLICKEY', 'https://moodle.org/static/statspubkey.pem');
533 * Moodle mobile app service name
535 define('MOODLE_OFFICIAL_MOBILE_SERVICE', 'moodle_mobile_app');
538 * Indicates the user has the capabilities required to ignore activity and course file size restrictions
540 define('USER_CAN_IGNORE_FILE_SIZE_LIMITS', -1);
543 * Course display settings: display all sections on one page.
545 define('COURSE_DISPLAY_SINGLEPAGE', 0);
547 * Course display settings: split pages into a page per section.
549 define('COURSE_DISPLAY_MULTIPAGE', 1);
552 * Authentication constant: String used in password field when password is not stored.
554 define('AUTH_PASSWORD_NOT_CACHED', 'not cached');
557 * Email from header to never include via information.
559 define('EMAIL_VIA_NEVER', 0);
562 * Email from header to always include via information.
564 define('EMAIL_VIA_ALWAYS', 1);
567 * Email from header to only include via information if the address is no-reply.
569 define('EMAIL_VIA_NO_REPLY_ONLY', 2);
572 * Contact site support form/link disabled.
574 define('CONTACT_SUPPORT_DISABLED', 0);
577 * Contact site support form/link only available to authenticated users.
579 define('CONTACT_SUPPORT_AUTHENTICATED', 1);
582 * Contact site support form/link available to anyone visiting the site.
584 define('CONTACT_SUPPORT_ANYONE', 2);
587 * Maximum number of characters for password.
589 define('MAX_PASSWORD_CHARACTERS', 128);
591 // PARAMETER HANDLING.
594 * Returns a particular value for the named variable, taken from
595 * POST or GET. If the parameter doesn't exist then an error is
596 * thrown because we require this variable.
598 * This function should be used to initialise all required values
599 * in a script that are based on parameters. Usually it will be
601 * $id = required_param('id', PARAM_INT);
603 * Please note the $type parameter is now required and the value can not be array.
605 * @param string $parname the name of the page parameter we want
606 * @param string $type expected type of parameter
608 * @throws coding_exception
610 function required_param($parname, $type) {
611 if (func_num_args() != 2 or empty($parname) or empty($type)) {
612 throw new coding_exception('required_param() requires $parname and $type to be specified (parameter: '.$parname.')');
614 // POST has precedence.
615 if (isset($_POST[$parname])) {
616 $param = $_POST[$parname];
617 } else if (isset($_GET[$parname])) {
618 $param = $_GET[$parname];
620 throw new \
moodle_exception('missingparam', '', '', $parname);
623 if (is_array($param)) {
624 debugging('Invalid array parameter detected in required_param(): '.$parname);
625 // TODO: switch to fatal error in Moodle 2.3.
626 return required_param_array($parname, $type);
629 return clean_param($param, $type);
633 * Returns a particular array value for the named variable, taken from
634 * POST or GET. If the parameter doesn't exist then an error is
635 * thrown because we require this variable.
637 * This function should be used to initialise all required values
638 * in a script that are based on parameters. Usually it will be
640 * $ids = required_param_array('ids', PARAM_INT);
642 * Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
644 * @param string $parname the name of the page parameter we want
645 * @param string $type expected type of parameter
647 * @throws coding_exception
649 function required_param_array($parname, $type) {
650 if (func_num_args() != 2 or empty($parname) or empty($type)) {
651 throw new coding_exception('required_param_array() requires $parname and $type to be specified (parameter: '.$parname.')');
653 // POST has precedence.
654 if (isset($_POST[$parname])) {
655 $param = $_POST[$parname];
656 } else if (isset($_GET[$parname])) {
657 $param = $_GET[$parname];
659 throw new \
moodle_exception('missingparam', '', '', $parname);
661 if (!is_array($param)) {
662 throw new \
moodle_exception('missingparam', '', '', $parname);
666 foreach ($param as $key => $value) {
667 if (!preg_match('/^[a-z0-9_-]+$/i', $key)) {
668 debugging('Invalid key name in required_param_array() detected: '.$key.', parameter: '.$parname);
671 $result[$key] = clean_param($value, $type);
678 * Returns a particular value for the named variable, taken from
679 * POST or GET, otherwise returning a given default.
681 * This function should be used to initialise all optional values
682 * in a script that are based on parameters. Usually it will be
684 * $name = optional_param('name', 'Fred', PARAM_TEXT);
686 * Please note the $type parameter is now required and the value can not be array.
688 * @param string $parname the name of the page parameter we want
689 * @param mixed $default the default value to return if nothing is found
690 * @param string $type expected type of parameter
692 * @throws coding_exception
694 function optional_param($parname, $default, $type) {
695 if (func_num_args() != 3 or empty($parname) or empty($type)) {
696 throw new coding_exception('optional_param requires $parname, $default + $type to be specified (parameter: '.$parname.')');
699 // POST has precedence.
700 if (isset($_POST[$parname])) {
701 $param = $_POST[$parname];
702 } else if (isset($_GET[$parname])) {
703 $param = $_GET[$parname];
708 if (is_array($param)) {
709 debugging('Invalid array parameter detected in required_param(): '.$parname);
710 // TODO: switch to $default in Moodle 2.3.
711 return optional_param_array($parname, $default, $type);
714 return clean_param($param, $type);
718 * Returns a particular array value for the named variable, taken from
719 * POST or GET, otherwise returning a given default.
721 * This function should be used to initialise all optional values
722 * in a script that are based on parameters. Usually it will be
724 * $ids = optional_param('id', array(), PARAM_INT);
726 * Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
728 * @param string $parname the name of the page parameter we want
729 * @param mixed $default the default value to return if nothing is found
730 * @param string $type expected type of parameter
732 * @throws coding_exception
734 function optional_param_array($parname, $default, $type) {
735 if (func_num_args() != 3 or empty($parname) or empty($type)) {
736 throw new coding_exception('optional_param_array requires $parname, $default + $type to be specified (parameter: '.$parname.')');
739 // POST has precedence.
740 if (isset($_POST[$parname])) {
741 $param = $_POST[$parname];
742 } else if (isset($_GET[$parname])) {
743 $param = $_GET[$parname];
747 if (!is_array($param)) {
748 debugging('optional_param_array() expects array parameters only: '.$parname);
753 foreach ($param as $key => $value) {
754 if (!preg_match('/^[a-z0-9_-]+$/i', $key)) {
755 debugging('Invalid key name in optional_param_array() detected: '.$key.', parameter: '.$parname);
758 $result[$key] = clean_param($value, $type);
765 * Strict validation of parameter values, the values are only converted
766 * to requested PHP type. Internally it is using clean_param, the values
767 * before and after cleaning must be equal - otherwise
768 * an invalid_parameter_exception is thrown.
769 * Objects and classes are not accepted.
771 * @param mixed $param
772 * @param string $type PARAM_ constant
773 * @param bool $allownull are nulls valid value?
774 * @param string $debuginfo optional debug information
775 * @return mixed the $param value converted to PHP type
776 * @throws invalid_parameter_exception if $param is not of given type
778 function validate_param($param, $type, $allownull=NULL_NOT_ALLOWED
, $debuginfo='') {
779 if (is_null($param)) {
780 if ($allownull == NULL_ALLOWED
) {
783 throw new invalid_parameter_exception($debuginfo);
786 if (is_array($param) or is_object($param)) {
787 throw new invalid_parameter_exception($debuginfo);
790 $cleaned = clean_param($param, $type);
792 if ($type == PARAM_FLOAT
) {
793 // Do not detect precision loss here.
794 if (is_float($param) or is_int($param)) {
796 } else if (!is_numeric($param) or !preg_match('/^[\+-]?[0-9]*\.?[0-9]*(e[-+]?[0-9]+)?$/i', (string)$param)) {
797 throw new invalid_parameter_exception($debuginfo);
799 } else if ((string)$param !== (string)$cleaned) {
800 // Conversion to string is usually lossless.
801 throw new invalid_parameter_exception($debuginfo);
808 * Makes sure array contains only the allowed types, this function does not validate array key names!
811 * $options = clean_param($options, PARAM_INT);
814 * @param array|null $param the variable array we are cleaning
815 * @param string $type expected format of param after cleaning.
816 * @param bool $recursive clean recursive arrays
818 * @throws coding_exception
820 function clean_param_array(?
array $param, $type, $recursive = false) {
821 // Convert null to empty array.
822 $param = (array)$param;
823 foreach ($param as $key => $value) {
824 if (is_array($value)) {
826 $param[$key] = clean_param_array($value, $type, true);
828 throw new coding_exception('clean_param_array can not process multidimensional arrays when $recursive is false.');
831 $param[$key] = clean_param($value, $type);
838 * Used by {@link optional_param()} and {@link required_param()} to
839 * clean the variables and/or cast to specific types, based on
842 * $course->format = clean_param($course->format, PARAM_ALPHA);
843 * $selectedgradeitem = clean_param($selectedgradeitem, PARAM_INT);
846 * @param mixed $param the variable we are cleaning
847 * @param string $type expected format of param after cleaning.
849 * @throws coding_exception
851 function clean_param($param, $type) {
854 if (is_array($param)) {
855 throw new coding_exception('clean_param() can not process arrays, please use clean_param_array() instead.');
856 } else if (is_object($param)) {
857 if (method_exists($param, '__toString')) {
858 $param = $param->__toString();
860 throw new coding_exception('clean_param() can not process objects, please use clean_param_array() instead.');
866 // No cleaning at all.
867 $param = fix_utf8($param);
870 case PARAM_RAW_TRIMMED
:
871 // No cleaning, but strip leading and trailing whitespace.
872 $param = (string)fix_utf8($param);
876 // General HTML cleaning, try to use more specific type if possible this is deprecated!
877 // Please use more specific type instead.
878 if (is_numeric($param)) {
881 $param = fix_utf8($param);
882 // Sweep for scripts, etc.
883 return clean_text($param);
885 case PARAM_CLEANHTML
:
886 // Clean html fragment.
887 $param = (string)fix_utf8($param);
888 // Sweep for scripts, etc.
889 $param = clean_text($param, FORMAT_HTML
);
893 // Convert to integer.
898 return (float)$param;
900 case PARAM_LOCALISEDFLOAT
:
902 return unformat_float($param, true);
905 // Remove everything not `a-z`.
906 return preg_replace('/[^a-zA-Z]/i', '', (string)$param);
909 // Remove everything not `a-zA-Z_-` (originally allowed "/" too).
910 return preg_replace('/[^a-zA-Z_-]/i', '', (string)$param);
913 // Remove everything not `a-zA-Z0-9`.
914 return preg_replace('/[^A-Za-z0-9]/i', '', (string)$param);
916 case PARAM_ALPHANUMEXT
:
917 // Remove everything not `a-zA-Z0-9_-`.
918 return preg_replace('/[^A-Za-z0-9_-]/i', '', (string)$param);
921 // Remove everything not `0-9,`.
922 return preg_replace('/[^0-9,]/i', '', (string)$param);
925 // Convert to 1 or 0.
926 $tempstr = strtolower((string)$param);
927 if ($tempstr === 'on' or $tempstr === 'yes' or $tempstr === 'true') {
929 } else if ($tempstr === 'off' or $tempstr === 'no' or $tempstr === 'false') {
932 $param = empty($param) ?
0 : 1;
938 $param = fix_utf8($param);
939 return strip_tags((string)$param);
942 // Leave only tags needed for multilang.
943 $param = fix_utf8($param);
944 // If the multilang syntax is not correct we strip all tags because it would break xhtml strict which is required
945 // for accessibility standards please note this cleaning does not strip unbalanced '>' for BC compatibility reasons.
947 if (strpos((string)$param, '</lang>') !== false) {
948 // Old and future mutilang syntax.
949 $param = strip_tags($param, '<lang>');
950 if (!preg_match_all('/<.*>/suU', $param, $matches)) {
954 foreach ($matches[0] as $match) {
955 if ($match === '</lang>') {
963 if (!preg_match('/^<lang lang="[a-zA-Z0-9_-]+"\s*>$/u', $match)) {
974 } else if (strpos((string)$param, '</span>') !== false) {
975 // Current problematic multilang syntax.
976 $param = strip_tags($param, '<span>');
977 if (!preg_match_all('/<.*>/suU', $param, $matches)) {
981 foreach ($matches[0] as $match) {
982 if ($match === '</span>') {
990 if (!preg_match('/^<span(\s+lang="[a-zA-Z0-9_-]+"|\s+class="multilang"){2}\s*>$/u', $match)) {
1002 // Easy, just strip all tags, if we ever want to fix orphaned '&' we have to do that in format_string().
1003 return strip_tags((string)$param);
1005 case PARAM_COMPONENT
:
1006 // We do not want any guessing here, either the name is correct or not
1007 // please note only normalised component names are accepted.
1008 $param = (string)$param;
1009 if (!preg_match('/^[a-z][a-z0-9]*(_[a-z][a-z0-9_]*)?[a-z0-9]+$/', $param)) {
1012 if (strpos($param, '__') !== false) {
1015 if (strpos($param, 'mod_') === 0) {
1016 // Module names must not contain underscores because we need to differentiate them from invalid plugin types.
1017 if (substr_count($param, '_') != 1) {
1025 // We do not want any guessing here, either the name is correct or not.
1026 if (!is_valid_plugin_name($param)) {
1032 // Remove everything not a-zA-Z0-9_- .
1033 return preg_replace('/[^a-zA-Z0-9_-]/i', '', (string)$param);
1035 case PARAM_SAFEPATH
:
1036 // Remove everything not a-zA-Z0-9/_- .
1037 return preg_replace('/[^a-zA-Z0-9\/_-]/i', '', (string)$param);
1040 // Strip all suspicious characters from filename.
1041 $param = (string)fix_utf8($param);
1042 $param = preg_replace('~[[:cntrl:]]|[&<>"`\|\':\\\\/]~u', '', $param);
1043 if ($param === '.' ||
$param === '..') {
1049 // Strip all suspicious characters from file path.
1050 $param = (string)fix_utf8($param);
1051 $param = str_replace('\\', '/', $param);
1053 // Explode the path and clean each element using the PARAM_FILE rules.
1054 $breadcrumb = explode('/', $param);
1055 foreach ($breadcrumb as $key => $crumb) {
1056 if ($crumb === '.' && $key === 0) {
1057 // Special condition to allow for relative current path such as ./currentdirfile.txt.
1059 $crumb = clean_param($crumb, PARAM_FILE
);
1061 $breadcrumb[$key] = $crumb;
1063 $param = implode('/', $breadcrumb);
1065 // Remove multiple current path (./././) and multiple slashes (///).
1066 $param = preg_replace('~//+~', '/', $param);
1067 $param = preg_replace('~/(\./)+~', '/', $param);
1071 // Allow FQDN or IPv4 dotted quad.
1072 $param = preg_replace('/[^\.\d\w-]/', '', (string)$param );
1073 // Match ipv4 dotted quad.
1074 if (preg_match('/(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/', $param, $match)) {
1075 // Confirm values are ok.
1076 if ( $match[0] > 255
1079 ||
$match[4] > 255 ) {
1080 // Hmmm, what kind of dotted quad is this?
1083 } else if ( preg_match('/^[\w\d\.-]+$/', $param) // Dots, hyphens, numbers.
1084 && !preg_match('/^[\.-]/', $param) // No leading dots/hyphens.
1085 && !preg_match('/[\.-]$/', $param) // No trailing dots/hyphens.
1087 // All is ok - $param is respected.
1096 $param = (string)fix_utf8($param);
1097 include_once($CFG->dirroot
. '/lib/validateurlsyntax.php');
1098 if (!empty($param) && validateUrlSyntax($param, 's?H?S?F?E-u-P-a?I?p?f?q?r?')) {
1099 // All is ok, param is respected.
1106 case PARAM_LOCALURL
:
1107 // Allow http absolute, root relative and relative URLs within wwwroot.
1108 $param = clean_param($param, PARAM_URL
);
1109 if (!empty($param)) {
1111 if ($param === $CFG->wwwroot
) {
1113 } else if (preg_match(':^/:', $param)) {
1114 // Root-relative, ok!
1115 } else if (preg_match('/^' . preg_quote($CFG->wwwroot
. '/', '/') . '/i', $param)) {
1116 // Absolute, and matches our wwwroot.
1119 // Relative - let's make sure there are no tricks.
1120 if (validateUrlSyntax('/' . $param, 's-u-P-a-p-f+q?r?') && !preg_match('/javascript:/i', $param)) {
1130 $param = trim((string)$param);
1131 // PEM formatted strings may contain letters/numbers and the symbols:
1135 // , surrounded by BEGIN and END CERTIFICATE prefix and suffixes.
1136 if (preg_match('/^-----BEGIN CERTIFICATE-----([\s\w\/\+=]+)-----END CERTIFICATE-----$/', trim($param), $matches)) {
1137 list($wholething, $body) = $matches;
1138 unset($wholething, $matches);
1139 $b64 = clean_param($body, PARAM_BASE64
);
1141 return "-----BEGIN CERTIFICATE-----\n$b64\n-----END CERTIFICATE-----\n";
1149 if (!empty($param)) {
1150 // PEM formatted strings may contain letters/numbers and the symbols
1154 if (0 >= preg_match('/^([\s\w\/\+=]+)$/', trim($param))) {
1157 $lines = preg_split('/[\s]+/', $param, -1, PREG_SPLIT_NO_EMPTY
);
1158 // Each line of base64 encoded data must be 64 characters in length, except for the last line which may be less
1159 // than (or equal to) 64 characters long.
1160 for ($i=0, $j=count($lines); $i < $j; $i++
) {
1162 if (64 < strlen($lines[$i])) {
1168 if (64 != strlen($lines[$i])) {
1172 return implode("\n", $lines);
1178 $param = (string)fix_utf8($param);
1179 // Please note it is not safe to use the tag name directly anywhere,
1180 // it must be processed with s(), urlencode() before embedding anywhere.
1181 // Remove some nasties.
1182 $param = preg_replace('~[[:cntrl:]]|[<>`]~u', '', $param);
1183 // Convert many whitespace chars into one.
1184 $param = preg_replace('/\s+/u', ' ', $param);
1185 $param = core_text
::substr(trim($param), 0, TAG_MAX_LENGTH
);
1189 $param = (string)fix_utf8($param);
1190 $tags = explode(',', $param);
1192 foreach ($tags as $tag) {
1193 $res = clean_param($tag, PARAM_TAG
);
1199 return implode(',', $result);
1204 case PARAM_CAPABILITY
:
1205 if (get_capability_info($param)) {
1211 case PARAM_PERMISSION
:
1212 $param = (int)$param;
1213 if (in_array($param, array(CAP_INHERIT
, CAP_ALLOW
, CAP_PREVENT
, CAP_PROHIBIT
))) {
1220 $param = clean_param($param, PARAM_PLUGIN
);
1221 if (empty($param)) {
1223 } else if (exists_auth_plugin($param)) {
1230 $param = clean_param($param, PARAM_SAFEDIR
);
1231 if (get_string_manager()->translation_exists($param)) {
1234 // Specified language is not installed or param malformed.
1239 $param = clean_param($param, PARAM_PLUGIN
);
1240 if (empty($param)) {
1242 } else if (file_exists("$CFG->dirroot/theme/$param/config.php")) {
1244 } else if (!empty($CFG->themedir
) and file_exists("$CFG->themedir/$param/config.php")) {
1247 // Specified theme is not installed.
1251 case PARAM_USERNAME
:
1252 $param = (string)fix_utf8($param);
1253 $param = trim($param);
1254 // Convert uppercase to lowercase MDL-16919.
1255 $param = core_text
::strtolower($param);
1256 if (empty($CFG->extendedusernamechars
)) {
1257 $param = str_replace(" " , "", $param);
1258 // Regular expression, eliminate all chars EXCEPT:
1259 // alphanum, dash (-), underscore (_), at sign (@) and period (.) characters.
1260 $param = preg_replace('/[^-\.@_a-z0-9]/', '', $param);
1265 $param = fix_utf8($param);
1266 if (validate_email($param ??
'')) {
1272 case PARAM_STRINGID
:
1273 if (preg_match('|^[a-zA-Z][a-zA-Z0-9\.:/_-]*$|', (string)$param)) {
1279 case PARAM_TIMEZONE
:
1280 // Can be int, float(with .5 or .0) or string seperated by '/' and can have '-_'.
1281 $param = (string)fix_utf8($param);
1282 $timezonepattern = '/^(([+-]?(0?[0-9](\.[5|0])?|1[0-3](\.0)?|1[0-2]\.5))|(99)|[[:alnum:]]+(\/?[[:alpha:]_-])+)$/';
1283 if (preg_match($timezonepattern, $param)) {
1290 // Doh! throw error, switched parameters in optional_param or another serious problem.
1291 throw new \
moodle_exception("unknownparamtype", '', '', $type);
1296 * Whether the PARAM_* type is compatible in RTL.
1298 * Being compatible with RTL means that the data they contain can flow
1299 * from right-to-left or left-to-right without compromising the user experience.
1301 * Take URLs for example, they are not RTL compatible as they should always
1302 * flow from the left to the right. This also applies to numbers, email addresses,
1303 * configuration snippets, base64 strings, etc...
1305 * This function tries to best guess which parameters can contain localised strings.
1307 * @param string $paramtype Constant PARAM_*.
1310 function is_rtl_compatible($paramtype) {
1311 return $paramtype == PARAM_TEXT ||
$paramtype == PARAM_NOTAGS
;
1315 * Makes sure the data is using valid utf8, invalid characters are discarded.
1317 * Note: this function is not intended for full objects with methods and private properties.
1319 * @param mixed $value
1320 * @return mixed with proper utf-8 encoding
1322 function fix_utf8($value) {
1323 if (is_null($value) or $value === '') {
1326 } else if (is_string($value)) {
1327 if ((string)(int)$value === $value) {
1332 // Remove null bytes or invalid Unicode sequences from value.
1333 $value = str_replace(["\0", "\xef\xbf\xbe", "\xef\xbf\xbf"], '', $value);
1335 // Note: this duplicates min_fix_utf8() intentionally.
1336 static $buggyiconv = null;
1337 if ($buggyiconv === null) {
1338 $buggyiconv = (!function_exists('iconv') or @iconv
('UTF-8', 'UTF-8//IGNORE', '100'.chr(130).'€') !== '100€');
1342 if (function_exists('mb_convert_encoding')) {
1343 $subst = mb_substitute_character();
1344 mb_substitute_character('none');
1345 $result = mb_convert_encoding($value, 'utf-8', 'utf-8');
1346 mb_substitute_character($subst);
1349 // Warn admins on admin/index.php page.
1354 $result = @iconv
('UTF-8', 'UTF-8//IGNORE', $value);
1359 } else if (is_array($value)) {
1360 foreach ($value as $k => $v) {
1361 $value[$k] = fix_utf8($v);
1365 } else if (is_object($value)) {
1366 // Do not modify original.
1367 $value = clone($value);
1368 foreach ($value as $k => $v) {
1369 $value->$k = fix_utf8($v);
1374 // This is some other type, no utf-8 here.
1380 * Return true if given value is integer or string with integer value
1382 * @param mixed $value String or Int
1383 * @return bool true if number, false if not
1385 function is_number($value) {
1386 if (is_int($value)) {
1388 } else if (is_string($value)) {
1389 return ((string)(int)$value) === $value;
1396 * Returns host part from url.
1398 * @param string $url full url
1399 * @return string host, null if not found
1401 function get_host_from_url($url) {
1402 preg_match('|^[a-z]+://([a-zA-Z0-9-.]+)|i', $url, $matches);
1410 * Tests whether anything was returned by text editor
1412 * This function is useful for testing whether something you got back from
1413 * the HTML editor actually contains anything. Sometimes the HTML editor
1414 * appear to be empty, but actually you get back a <br> tag or something.
1416 * @param string $string a string containing HTML.
1417 * @return boolean does the string contain any actual content - that is text,
1418 * images, objects, etc.
1420 function html_is_blank($string) {
1421 return trim(strip_tags((string)$string, '<img><object><applet><input><select><textarea><hr>')) == '';
1425 * Set a key in global configuration
1427 * Set a key/value pair in both this session's {@link $CFG} global variable
1428 * and in the 'config' database table for future sessions.
1430 * Can also be used to update keys for plugin-scoped configs in config_plugin table.
1431 * In that case it doesn't affect $CFG.
1433 * A NULL value will delete the entry.
1435 * NOTE: this function is called from lib/db/upgrade.php
1437 * @param string $name the key to set
1438 * @param string $value the value to set (without magic quotes)
1439 * @param string $plugin (optional) the plugin scope, default null
1440 * @return bool true or exception
1442 function set_config($name, $value, $plugin = null) {
1445 // Redirect to appropriate handler when value is null.
1446 if ($value === null) {
1447 return unset_config($name, $plugin);
1450 // Set variables determining conditions and where to store the new config.
1451 // Plugin config goes to {config_plugins}, core config goes to {config}.
1452 $iscore = empty($plugin);
1454 // If it's for core config.
1456 $conditions = ['name' => $name];
1457 $invalidatecachekey = 'core';
1459 // If it's a plugin.
1460 $table = 'config_plugins';
1461 $conditions = ['name' => $name, 'plugin' => $plugin];
1462 $invalidatecachekey = $plugin;
1465 // DB handling - checks for existing config, updating or inserting only if necessary.
1466 $invalidatecache = true;
1468 $record = $DB->get_record($table, $conditions, 'id, value');
1469 if ($record === false) {
1470 // Inserts a new config record.
1471 $config = new stdClass();
1472 $config->name
= $name;
1473 $config->value
= $value;
1475 $config->plugin
= $plugin;
1477 $inserted = $DB->insert_record($table, $config, false);
1478 } else if ($invalidatecache = ($record->value
!== $value)) {
1479 // Record exists - Check and only set new value if it has changed.
1480 $DB->set_field($table, 'value', $value, ['id' => $record->id
]);
1483 if ($iscore && !isset($CFG->config_php_settings
[$name])) {
1484 // So it's defined for this invocation at least.
1485 // Settings from db are always strings.
1486 $CFG->$name = (string) $value;
1489 // When setting config during a Behat test (in the CLI script, not in the web browser
1490 // requests), remember which ones are set so that we can clear them later.
1491 if ($iscore && $inserted && defined('BEHAT_TEST')) {
1492 $CFG->behat_cli_added_config
[$name] = true;
1495 // Update siteidentifier cache, if required.
1496 if ($iscore && $name === 'siteidentifier') {
1497 cache_helper
::update_site_identifier($value);
1500 // Invalidate cache, if required.
1501 if ($invalidatecache) {
1502 cache_helper
::invalidate_by_definition('core', 'config', [], $invalidatecachekey);
1509 * Get configuration values from the global config table
1510 * or the config_plugins table.
1512 * If called with one parameter, it will load all the config
1513 * variables for one plugin, and return them as an object.
1515 * If called with 2 parameters it will return a string single
1516 * value or false if the value is not found.
1518 * NOTE: this function is called from lib/db/upgrade.php
1520 * @param string $plugin full component name
1521 * @param string $name default null
1522 * @return mixed hash-like object or single value, return false no config found
1523 * @throws dml_exception
1525 function get_config($plugin, $name = null) {
1528 if ($plugin === 'moodle' ||
$plugin === 'core' ||
empty($plugin)) {
1529 $forced =& $CFG->config_php_settings
;
1533 if (array_key_exists($plugin, $CFG->forced_plugin_settings
)) {
1534 $forced =& $CFG->forced_plugin_settings
[$plugin];
1541 if (!isset($CFG->siteidentifier
)) {
1543 // This may throw an exception during installation, which is how we detect the
1544 // need to install the database. For more details see {@see initialise_cfg()}.
1545 $CFG->siteidentifier
= $DB->get_field('config', 'value', array('name' => 'siteidentifier'));
1546 } catch (dml_exception
$ex) {
1547 // Set siteidentifier to false. We don't want to trip this continually.
1548 $siteidentifier = false;
1553 if (!empty($name)) {
1554 if (array_key_exists($name, $forced)) {
1555 return (string)$forced[$name];
1556 } else if ($name === 'siteidentifier' && $plugin == 'core') {
1557 return $CFG->siteidentifier
;
1561 $cache = cache
::make('core', 'config');
1562 $result = $cache->get($plugin);
1563 if ($result === false) {
1564 // The user is after a recordset.
1566 $result = $DB->get_records_menu('config_plugins', array('plugin' => $plugin), '', 'name,value');
1568 // This part is not really used any more, but anyway...
1569 $result = $DB->get_records_menu('config', array(), '', 'name,value');;
1571 $cache->set($plugin, $result);
1574 if (!empty($name)) {
1575 if (array_key_exists($name, $result)) {
1576 return $result[$name];
1581 if ($plugin === 'core') {
1582 $result['siteidentifier'] = $CFG->siteidentifier
;
1585 foreach ($forced as $key => $value) {
1586 if (is_null($value) or is_array($value) or is_object($value)) {
1587 // We do not want any extra mess here, just real settings that could be saved in db.
1588 unset($result[$key]);
1590 // Convert to string as if it went through the DB.
1591 $result[$key] = (string)$value;
1595 return (object)$result;
1599 * Removes a key from global configuration.
1601 * NOTE: this function is called from lib/db/upgrade.php
1603 * @param string $name the key to set
1604 * @param string $plugin (optional) the plugin scope
1605 * @return boolean whether the operation succeeded.
1607 function unset_config($name, $plugin=null) {
1610 if (empty($plugin)) {
1612 $DB->delete_records('config', array('name' => $name));
1613 cache_helper
::invalidate_by_definition('core', 'config', array(), 'core');
1615 $DB->delete_records('config_plugins', array('name' => $name, 'plugin' => $plugin));
1616 cache_helper
::invalidate_by_definition('core', 'config', array(), $plugin);
1623 * Remove all the config variables for a given plugin.
1625 * NOTE: this function is called from lib/db/upgrade.php
1627 * @param string $plugin a plugin, for example 'quiz' or 'qtype_multichoice';
1628 * @return boolean whether the operation succeeded.
1630 function unset_all_config_for_plugin($plugin) {
1632 // Delete from the obvious config_plugins first.
1633 $DB->delete_records('config_plugins', array('plugin' => $plugin));
1634 // Next delete any suspect settings from config.
1635 $like = $DB->sql_like('name', '?', true, true, false, '|');
1636 $params = array($DB->sql_like_escape($plugin.'_', '|') . '%');
1637 $DB->delete_records_select('config', $like, $params);
1638 // Finally clear both the plugin cache and the core cache (suspect settings now removed from core).
1639 cache_helper
::invalidate_by_definition('core', 'config', array(), array('core', $plugin));
1645 * Use this function to get a list of users from a config setting of type admin_setting_users_with_capability.
1647 * All users are verified if they still have the necessary capability.
1649 * @param string $value the value of the config setting.
1650 * @param string $capability the capability - must match the one passed to the admin_setting_users_with_capability constructor.
1651 * @param bool $includeadmins include administrators.
1652 * @return array of user objects.
1654 function get_users_from_config($value, $capability, $includeadmins = true) {
1655 if (empty($value) or $value === '$@NONE@$') {
1659 // We have to make sure that users still have the necessary capability,
1660 // it should be faster to fetch them all first and then test if they are present
1661 // instead of validating them one-by-one.
1662 $users = get_users_by_capability(context_system
::instance(), $capability);
1663 if ($includeadmins) {
1664 $admins = get_admins();
1665 foreach ($admins as $admin) {
1666 $users[$admin->id
] = $admin;
1670 if ($value === '$@ALL@$') {
1674 $result = array(); // Result in correct order.
1675 $allowed = explode(',', $value);
1676 foreach ($allowed as $uid) {
1677 if (isset($users[$uid])) {
1678 $user = $users[$uid];
1679 $result[$user->id
] = $user;
1688 * Invalidates browser caches and cached data in temp.
1692 function purge_all_caches() {
1697 * Selectively invalidate different types of cache.
1699 * Purges the cache areas specified. By default, this will purge all caches but can selectively purge specific
1700 * areas alone or in combination.
1702 * @param bool[] $options Specific parts of the cache to purge. Valid options are:
1703 * 'muc' Purge MUC caches?
1704 * 'theme' Purge theme cache?
1705 * 'lang' Purge language string cache?
1706 * 'js' Purge javascript cache?
1707 * 'filter' Purge text filter cache?
1708 * 'other' Purge all other caches?
1710 function purge_caches($options = []) {
1711 $defaults = array_fill_keys(['muc', 'theme', 'lang', 'js', 'template', 'filter', 'other'], false);
1712 if (empty(array_filter($options))) {
1713 $options = array_fill_keys(array_keys($defaults), true); // Set all options to true.
1715 $options = array_merge($defaults, array_intersect_key($options, $defaults)); // Override defaults with specified options.
1717 if ($options['muc']) {
1718 cache_helper
::purge_all();
1720 if ($options['theme']) {
1721 theme_reset_all_caches();
1723 if ($options['lang']) {
1724 get_string_manager()->reset_caches();
1726 if ($options['js']) {
1727 js_reset_all_caches();
1729 if ($options['template']) {
1730 template_reset_all_caches();
1732 if ($options['filter']) {
1733 reset_text_filters_cache();
1735 if ($options['other']) {
1736 purge_other_caches();
1741 * Purge all non-MUC caches not otherwise purged in purge_caches.
1743 * IMPORTANT - If you are adding anything here to do with the cache directory you should also have a look at
1744 * {@link phpunit_util::reset_dataroot()}
1746 function purge_other_caches() {
1748 if (class_exists('core_plugin_manager')) {
1749 core_plugin_manager
::reset_caches();
1752 // Bump up cacherev field for all courses.
1754 increment_revision_number('course', 'cacherev', '');
1755 } catch (moodle_exception
$e) {
1756 // Ignore exception since this function is also called before upgrade script when field course.cacherev does not exist yet.
1759 $DB->reset_caches();
1761 // Purge all other caches: rss, simplepie, etc.
1763 remove_dir($CFG->cachedir
.'', true);
1765 // Make sure cache dir is writable, throws exception if not.
1766 make_cache_directory('');
1768 // This is the only place where we purge local caches, we are only adding files there.
1769 // The $CFG->localcachedirpurged flag forces local directories to be purged on cluster nodes.
1770 remove_dir($CFG->localcachedir
, true);
1771 set_config('localcachedirpurged', time());
1772 make_localcache_directory('', true);
1773 \core\task\manager
::clear_static_caches();
1777 * Get volatile flags
1779 * @param string $type
1780 * @param int $changedsince default null
1781 * @return array records array
1783 function get_cache_flags($type, $changedsince = null) {
1786 $params = array('type' => $type, 'expiry' => time());
1787 $sqlwhere = "flagtype = :type AND expiry >= :expiry";
1788 if ($changedsince !== null) {
1789 $params['changedsince'] = $changedsince;
1790 $sqlwhere .= " AND timemodified > :changedsince";
1793 if ($flags = $DB->get_records_select('cache_flags', $sqlwhere, $params, '', 'name,value')) {
1794 foreach ($flags as $flag) {
1795 $cf[$flag->name
] = $flag->value
;
1802 * Get volatile flags
1804 * @param string $type
1805 * @param string $name
1806 * @param int $changedsince default null
1807 * @return string|false The cache flag value or false
1809 function get_cache_flag($type, $name, $changedsince=null) {
1812 $params = array('type' => $type, 'name' => $name, 'expiry' => time());
1814 $sqlwhere = "flagtype = :type AND name = :name AND expiry >= :expiry";
1815 if ($changedsince !== null) {
1816 $params['changedsince'] = $changedsince;
1817 $sqlwhere .= " AND timemodified > :changedsince";
1820 return $DB->get_field_select('cache_flags', 'value', $sqlwhere, $params);
1824 * Set a volatile flag
1826 * @param string $type the "type" namespace for the key
1827 * @param string $name the key to set
1828 * @param string $value the value to set (without magic quotes) - null will remove the flag
1829 * @param int $expiry (optional) epoch indicating expiry - defaults to now()+ 24hs
1830 * @return bool Always returns true
1832 function set_cache_flag($type, $name, $value, $expiry = null) {
1835 $timemodified = time();
1836 if ($expiry === null ||
$expiry < $timemodified) {
1837 $expiry = $timemodified +
24 * 60 * 60;
1839 $expiry = (int)$expiry;
1842 if ($value === null) {
1843 unset_cache_flag($type, $name);
1847 if ($f = $DB->get_record('cache_flags', array('name' => $name, 'flagtype' => $type), '*', IGNORE_MULTIPLE
)) {
1848 // This is a potential problem in DEBUG_DEVELOPER.
1849 if ($f->value
== $value and $f->expiry
== $expiry and $f->timemodified
== $timemodified) {
1850 return true; // No need to update.
1853 $f->expiry
= $expiry;
1854 $f->timemodified
= $timemodified;
1855 $DB->update_record('cache_flags', $f);
1857 $f = new stdClass();
1858 $f->flagtype
= $type;
1861 $f->expiry
= $expiry;
1862 $f->timemodified
= $timemodified;
1863 $DB->insert_record('cache_flags', $f);
1869 * Removes a single volatile flag
1871 * @param string $type the "type" namespace for the key
1872 * @param string $name the key to set
1875 function unset_cache_flag($type, $name) {
1877 $DB->delete_records('cache_flags', array('name' => $name, 'flagtype' => $type));
1882 * Garbage-collect volatile flags
1884 * @return bool Always returns true
1886 function gc_cache_flags() {
1888 $DB->delete_records_select('cache_flags', 'expiry < ?', array(time()));
1892 // USER PREFERENCE API.
1895 * Refresh user preference cache. This is used most often for $USER
1896 * object that is stored in session, but it also helps with performance in cron script.
1898 * Preferences for each user are loaded on first use on every page, then again after the timeout expires.
1901 * @category preference
1903 * @param stdClass $user User object. Preferences are preloaded into 'preference' property
1904 * @param int $cachelifetime Cache life time on the current page (in seconds)
1905 * @throws coding_exception
1908 function check_user_preferences_loaded(stdClass
$user, $cachelifetime = 120) {
1910 // Static cache, we need to check on each page load, not only every 2 minutes.
1911 static $loadedusers = array();
1913 if (!isset($user->id
)) {
1914 throw new coding_exception('Invalid $user parameter in check_user_preferences_loaded() call, missing id field');
1917 if (empty($user->id
) or isguestuser($user->id
)) {
1918 // No permanent storage for not-logged-in users and guest.
1919 if (!isset($user->preference
)) {
1920 $user->preference
= array();
1927 if (isset($loadedusers[$user->id
]) and isset($user->preference
) and isset($user->preference
['_lastloaded'])) {
1928 // Already loaded at least once on this page. Are we up to date?
1929 if ($user->preference
['_lastloaded'] +
$cachelifetime > $timenow) {
1930 // No need to reload - we are on the same page and we loaded prefs just a moment ago.
1933 } else if (!get_cache_flag('userpreferenceschanged', $user->id
, $user->preference
['_lastloaded'])) {
1934 // No change since the lastcheck on this page.
1935 $user->preference
['_lastloaded'] = $timenow;
1940 // OK, so we have to reload all preferences.
1941 $loadedusers[$user->id
] = true;
1942 $user->preference
= $DB->get_records_menu('user_preferences', array('userid' => $user->id
), '', 'name,value'); // All values.
1943 $user->preference
['_lastloaded'] = $timenow;
1947 * Called from set/unset_user_preferences, so that the prefs can be correctly reloaded in different sessions.
1949 * NOTE: internal function, do not call from other code.
1953 * @param integer $userid the user whose prefs were changed.
1955 function mark_user_preferences_changed($userid) {
1958 if (empty($userid) or isguestuser($userid)) {
1959 // No cache flags for guest and not-logged-in users.
1963 set_cache_flag('userpreferenceschanged', $userid, 1, time() +
$CFG->sessiontimeout
);
1967 * Sets a preference for the specified user.
1969 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1971 * When additional validation/permission check is needed it is better to use {@see useredit_update_user_preference()}
1974 * @category preference
1976 * @param string $name The key to set as preference for the specified user
1977 * @param string $value The value to set for the $name key in the specified user's
1978 * record, null means delete current value.
1979 * @param stdClass|int|null $user A moodle user object or id, null means current user
1980 * @throws coding_exception
1981 * @return bool Always true or exception
1983 function set_user_preference($name, $value, $user = null) {
1986 if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
1987 throw new coding_exception('Invalid preference name in set_user_preference() call');
1990 if (is_null($value)) {
1991 // Null means delete current.
1992 return unset_user_preference($name, $user);
1993 } else if (is_object($value)) {
1994 throw new coding_exception('Invalid value in set_user_preference() call, objects are not allowed');
1995 } else if (is_array($value)) {
1996 throw new coding_exception('Invalid value in set_user_preference() call, arrays are not allowed');
1998 // Value column maximum length is 1333 characters.
1999 $value = (string)$value;
2000 if (core_text
::strlen($value) > 1333) {
2001 throw new coding_exception('Invalid value in set_user_preference() call, value is is too long for the value column');
2004 if (is_null($user)) {
2006 } else if (isset($user->id
)) {
2007 // It is a valid object.
2008 } else if (is_numeric($user)) {
2009 $user = (object)array('id' => (int)$user);
2011 throw new coding_exception('Invalid $user parameter in set_user_preference() call');
2014 check_user_preferences_loaded($user);
2016 if (empty($user->id
) or isguestuser($user->id
)) {
2017 // No permanent storage for not-logged-in users and guest.
2018 $user->preference
[$name] = $value;
2022 if ($preference = $DB->get_record('user_preferences', array('userid' => $user->id
, 'name' => $name))) {
2023 if ($preference->value
=== $value and isset($user->preference
[$name]) and $user->preference
[$name] === $value) {
2024 // Preference already set to this value.
2027 $DB->set_field('user_preferences', 'value', $value, array('id' => $preference->id
));
2030 $preference = new stdClass();
2031 $preference->userid
= $user->id
;
2032 $preference->name
= $name;
2033 $preference->value
= $value;
2034 $DB->insert_record('user_preferences', $preference);
2037 // Update value in cache.
2038 $user->preference
[$name] = $value;
2039 // Update the $USER in case where we've not a direct reference to $USER.
2040 if ($user !== $USER && $user->id
== $USER->id
) {
2041 $USER->preference
[$name] = $value;
2044 // Set reload flag for other sessions.
2045 mark_user_preferences_changed($user->id
);
2051 * Sets a whole array of preferences for the current user
2053 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
2056 * @category preference
2058 * @param array $prefarray An array of key/value pairs to be set
2059 * @param stdClass|int|null $user A moodle user object or id, null means current user
2060 * @return bool Always true or exception
2062 function set_user_preferences(array $prefarray, $user = null) {
2063 foreach ($prefarray as $name => $value) {
2064 set_user_preference($name, $value, $user);
2070 * Unsets a preference completely by deleting it from the database
2072 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
2075 * @category preference
2077 * @param string $name The key to unset as preference for the specified user
2078 * @param stdClass|int|null $user A moodle user object or id, null means current user
2079 * @throws coding_exception
2080 * @return bool Always true or exception
2082 function unset_user_preference($name, $user = null) {
2085 if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
2086 throw new coding_exception('Invalid preference name in unset_user_preference() call');
2089 if (is_null($user)) {
2091 } else if (isset($user->id
)) {
2092 // It is a valid object.
2093 } else if (is_numeric($user)) {
2094 $user = (object)array('id' => (int)$user);
2096 throw new coding_exception('Invalid $user parameter in unset_user_preference() call');
2099 check_user_preferences_loaded($user);
2101 if (empty($user->id
) or isguestuser($user->id
)) {
2102 // No permanent storage for not-logged-in user and guest.
2103 unset($user->preference
[$name]);
2108 $DB->delete_records('user_preferences', array('userid' => $user->id
, 'name' => $name));
2110 // Delete the preference from cache.
2111 unset($user->preference
[$name]);
2112 // Update the $USER in case where we've not a direct reference to $USER.
2113 if ($user !== $USER && $user->id
== $USER->id
) {
2114 unset($USER->preference
[$name]);
2117 // Set reload flag for other sessions.
2118 mark_user_preferences_changed($user->id
);
2124 * Used to fetch user preference(s)
2126 * If no arguments are supplied this function will return
2127 * all of the current user preferences as an array.
2129 * If a name is specified then this function
2130 * attempts to return that particular preference value. If
2131 * none is found, then the optional value $default is returned,
2134 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
2137 * @category preference
2139 * @param string $name Name of the key to use in finding a preference value
2140 * @param mixed|null $default Value to be returned if the $name key is not set in the user preferences
2141 * @param stdClass|int|null $user A moodle user object or id, null means current user
2142 * @throws coding_exception
2143 * @return string|mixed|null A string containing the value of a single preference. An
2144 * array with all of the preferences or null
2146 function get_user_preferences($name = null, $default = null, $user = null) {
2149 if (is_null($name)) {
2151 } else if (is_numeric($name) or $name === '_lastloaded') {
2152 throw new coding_exception('Invalid preference name in get_user_preferences() call');
2155 if (is_null($user)) {
2157 } else if (isset($user->id
)) {
2158 // Is a valid object.
2159 } else if (is_numeric($user)) {
2160 if ($USER->id
== $user) {
2163 $user = (object)array('id' => (int)$user);
2166 throw new coding_exception('Invalid $user parameter in get_user_preferences() call');
2169 check_user_preferences_loaded($user);
2173 return $user->preference
;
2174 } else if (isset($user->preference
[$name])) {
2175 // The single string value.
2176 return $user->preference
[$name];
2178 // Default value (null if not specified).
2183 // FUNCTIONS FOR HANDLING TIME.
2186 * Given Gregorian date parts in user time produce a GMT timestamp.
2190 * @param int $year The year part to create timestamp of
2191 * @param int $month The month part to create timestamp of
2192 * @param int $day The day part to create timestamp of
2193 * @param int $hour The hour part to create timestamp of
2194 * @param int $minute The minute part to create timestamp of
2195 * @param int $second The second part to create timestamp of
2196 * @param int|float|string $timezone Timezone modifier, used to calculate GMT time offset.
2197 * if 99 then default user's timezone is used {@link http://docs.moodle.org/dev/Time_API#Timezone}
2198 * @param bool $applydst Toggle Daylight Saving Time, default true, will be
2199 * applied only if timezone is 99 or string.
2200 * @return int GMT timestamp
2202 function make_timestamp($year, $month=1, $day=1, $hour=0, $minute=0, $second=0, $timezone=99, $applydst=true) {
2203 $date = new DateTime('now', core_date
::get_user_timezone_object($timezone));
2204 $date->setDate((int)$year, (int)$month, (int)$day);
2205 $date->setTime((int)$hour, (int)$minute, (int)$second);
2207 $time = $date->getTimestamp();
2209 if ($time === false) {
2210 throw new coding_exception('getTimestamp() returned false, please ensure you have passed correct values.'.
2211 ' This can fail if year is more than 2038 and OS is 32 bit windows');
2214 // Moodle BC DST stuff.
2216 $time +
= dst_offset_on($time, $timezone);
2224 * Format a date/time (seconds) as weeks, days, hours etc as needed
2226 * Given an amount of time in seconds, returns string
2227 * formatted nicely as years, days, hours etc as needed
2235 * @param int $totalsecs Time in seconds
2236 * @param stdClass $str Should be a time object
2237 * @return string A nicely formatted date/time string
2239 function format_time($totalsecs, $str = null) {
2241 $totalsecs = abs($totalsecs);
2244 // Create the str structure the slow way.
2245 $str = new stdClass();
2246 $str->day
= get_string('day');
2247 $str->days
= get_string('days');
2248 $str->hour
= get_string('hour');
2249 $str->hours
= get_string('hours');
2250 $str->min
= get_string('min');
2251 $str->mins
= get_string('mins');
2252 $str->sec
= get_string('sec');
2253 $str->secs
= get_string('secs');
2254 $str->year
= get_string('year');
2255 $str->years
= get_string('years');
2258 $years = floor($totalsecs/YEARSECS
);
2259 $remainder = $totalsecs - ($years*YEARSECS
);
2260 $days = floor($remainder/DAYSECS
);
2261 $remainder = $totalsecs - ($days*DAYSECS
);
2262 $hours = floor($remainder/HOURSECS
);
2263 $remainder = $remainder - ($hours*HOURSECS
);
2264 $mins = floor($remainder/MINSECS
);
2265 $secs = $remainder - ($mins*MINSECS
);
2267 $ss = ($secs == 1) ?
$str->sec
: $str->secs
;
2268 $sm = ($mins == 1) ?
$str->min
: $str->mins
;
2269 $sh = ($hours == 1) ?
$str->hour
: $str->hours
;
2270 $sd = ($days == 1) ?
$str->day
: $str->days
;
2271 $sy = ($years == 1) ?
$str->year
: $str->years
;
2280 $oyears = $years .' '. $sy;
2283 $odays = $days .' '. $sd;
2286 $ohours = $hours .' '. $sh;
2289 $omins = $mins .' '. $sm;
2292 $osecs = $secs .' '. $ss;
2296 return trim($oyears .' '. $odays);
2299 return trim($odays .' '. $ohours);
2302 return trim($ohours .' '. $omins);
2305 return trim($omins .' '. $osecs);
2310 return get_string('now');
2314 * Returns a formatted string that represents a date in user time.
2318 * @param int $date the timestamp in UTC, as obtained from the database.
2319 * @param string $format strftime format. You should probably get this using
2320 * get_string('strftime...', 'langconfig');
2321 * @param int|float|string $timezone by default, uses the user's time zone. if numeric and
2322 * not 99 then daylight saving will not be added.
2323 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2324 * @param bool $fixday If true (default) then the leading zero from %d is removed.
2325 * If false then the leading zero is maintained.
2326 * @param bool $fixhour If true (default) then the leading zero from %I is removed.
2327 * @return string the formatted date/time.
2329 function userdate($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) {
2330 $calendartype = \core_calendar\type_factory
::get_calendar_instance();
2331 return $calendartype->timestamp_to_date_string($date, $format, $timezone, $fixday, $fixhour);
2335 * Returns a html "time" tag with both the exact user date with timezone information
2336 * as a datetime attribute in the W3C format, and the user readable date and time as text.
2340 * @param int $date the timestamp in UTC, as obtained from the database.
2341 * @param string $format strftime format. You should probably get this using
2342 * get_string('strftime...', 'langconfig');
2343 * @param int|float|string $timezone by default, uses the user's time zone. if numeric and
2344 * not 99 then daylight saving will not be added.
2345 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2346 * @param bool $fixday If true (default) then the leading zero from %d is removed.
2347 * If false then the leading zero is maintained.
2348 * @param bool $fixhour If true (default) then the leading zero from %I is removed.
2349 * @return string the formatted date/time.
2351 function userdate_htmltime($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) {
2352 $userdatestr = userdate($date, $format, $timezone, $fixday, $fixhour);
2353 if (CLI_SCRIPT
&& !PHPUNIT_TEST
) {
2354 return $userdatestr;
2356 $machinedate = new DateTime();
2357 $machinedate->setTimestamp(intval($date));
2358 $machinedate->setTimezone(core_date
::get_user_timezone_object());
2360 return html_writer
::tag('time', $userdatestr, ['datetime' => $machinedate->format(DateTime
::W3C
)]);
2364 * Returns a formatted date ensuring it is UTF-8.
2366 * If we are running under Windows convert to Windows encoding and then back to UTF-8
2367 * (because it's impossible to specify UTF-8 to fetch locale info in Win32).
2369 * @param int $date the timestamp - since Moodle 2.9 this is a real UTC timestamp
2370 * @param string $format strftime format.
2371 * @param int|float|string $tz the user timezone
2372 * @return string the formatted date/time.
2373 * @since Moodle 2.3.3
2375 function date_format_string($date, $format, $tz = 99) {
2377 date_default_timezone_set(core_date
::get_user_timezone($tz));
2379 if (date('A', 0) === date('A', HOURSECS
* 18)) {
2380 $datearray = getdate($date);
2381 $format = str_replace([
2385 $datearray['hours'] < 12 ?
get_string('am', 'langconfig') : get_string('pm', 'langconfig'),
2386 $datearray['hours'] < 12 ?
get_string('amcaps', 'langconfig') : get_string('pmcaps', 'langconfig'),
2390 $datestring = core_date
::strftime($format, $date);
2391 core_date
::set_default_server_timezone();
2397 * Given a $time timestamp in GMT (seconds since epoch),
2398 * returns an array that represents the Gregorian date in user time
2402 * @param int $time Timestamp in GMT
2403 * @param float|int|string $timezone user timezone
2404 * @return array An array that represents the date in user time
2406 function usergetdate($time, $timezone=99) {
2407 if ($time === null) {
2408 // PHP8 and PHP7 return different results when getdate(null) is called.
2409 // Display warning and cast to 0 to make sure the usergetdate() behaves consistently on all versions of PHP.
2410 // In the future versions of Moodle we may consider adding a strict typehint.
2411 debugging('usergetdate() expects parameter $time to be int, null given', DEBUG_DEVELOPER
);
2415 date_default_timezone_set(core_date
::get_user_timezone($timezone));
2416 $result = getdate($time);
2417 core_date
::set_default_server_timezone();
2423 * Given a GMT timestamp (seconds since epoch), offsets it by
2424 * the timezone. eg 3pm in India is 3pm GMT - 7 * 3600 seconds
2426 * NOTE: this function does not include DST properly,
2427 * you should use the PHP date stuff instead!
2431 * @param int $date Timestamp in GMT
2432 * @param float|int|string $timezone user timezone
2435 function usertime($date, $timezone=99) {
2436 $userdate = new DateTime('@' . $date);
2437 $userdate->setTimezone(core_date
::get_user_timezone_object($timezone));
2438 $dst = dst_offset_on($date, $timezone);
2440 return $date - $userdate->getOffset() +
$dst;
2444 * Get a formatted string representation of an interval between two unix timestamps.
2447 * $intervalstring = get_time_interval_string(12345600, 12345660);
2448 * Will produce the string:
2451 * @param int $time1 unix timestamp
2452 * @param int $time2 unix timestamp
2453 * @param string $format string (can be lang string) containing format chars: https://www.php.net/manual/en/dateinterval.format.php.
2454 * @param bool $dropzeroes If format is not provided and this is set to true, do not include zero time units.
2455 * e.g. a duration of 3 days and 2 hours will be displayed as '3d 2h' instead of '3d 2h 0s'
2456 * @param bool $fullformat If format is not provided and this is set to true, display time units in full format.
2457 * e.g. instead of showing "3d", "3 days" will be returned.
2458 * @return string the formatted string describing the time difference, e.g. '10d 11h 45m'.
2460 function get_time_interval_string(int $time1, int $time2, string $format = '',
2461 bool $dropzeroes = false, bool $fullformat = false): string {
2462 $dtdate = new DateTime();
2463 $dtdate->setTimeStamp($time1);
2464 $dtdate2 = new DateTime();
2465 $dtdate2->setTimeStamp($time2);
2466 $interval = $dtdate2->diff($dtdate);
2468 if (empty(trim($format))) {
2469 // Default to this key.
2470 $formatkey = 'dateintervaldayhrmin';
2482 foreach ($units as $key => $unit) {
2483 if (empty($interval->$key)) {
2486 $formatunits[] = $unit;
2488 if (!empty($formatunits)) {
2489 $formatkey = 'dateinterval' . implode("", $formatunits);
2494 $formatkey .= 'full';
2496 $format = get_string($formatkey, 'langconfig');
2498 return $interval->format($format);
2502 * Given a time, return the GMT timestamp of the most recent midnight
2503 * for the current user.
2507 * @param int $date Timestamp in GMT
2508 * @param float|int|string $timezone user timezone
2509 * @return int Returns a GMT timestamp
2511 function usergetmidnight($date, $timezone=99) {
2513 $userdate = usergetdate($date, $timezone);
2515 // Time of midnight of this user's day, in GMT.
2516 return make_timestamp($userdate['year'], $userdate['mon'], $userdate['mday'], 0, 0, 0, $timezone);
2521 * Returns a string that prints the user's timezone
2525 * @param float|int|string $timezone user timezone
2528 function usertimezone($timezone=99) {
2529 $tz = core_date
::get_user_timezone($timezone);
2530 return core_date
::get_localised_timezone($tz);
2534 * Returns a float or a string which denotes the user's timezone
2535 * 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)
2536 * means that for this timezone there are also DST rules to be taken into account
2537 * Checks various settings and picks the most dominant of those which have a value
2541 * @param float|int|string $tz timezone to calculate GMT time offset before
2542 * calculating user timezone, 99 is default user timezone
2543 * {@link http://docs.moodle.org/dev/Time_API#Timezone}
2544 * @return float|string
2546 function get_user_timezone($tz = 99) {
2551 isset($CFG->forcetimezone
) ?
$CFG->forcetimezone
: 99,
2552 isset($USER->timezone
) ?
$USER->timezone
: 99,
2553 isset($CFG->timezone
) ?
$CFG->timezone
: 99,
2558 // Loop while $tz is, empty but not zero, or 99, and there is another timezone is the array.
2559 foreach ($timezones as $nextvalue) {
2560 if ((empty($tz) && !is_numeric($tz)) ||
$tz == 99) {
2564 return is_numeric($tz) ?
(float) $tz : $tz;
2568 * Calculates the Daylight Saving Offset for a given date/time (timestamp)
2569 * - Note: Daylight saving only works for string timezones and not for float.
2573 * @param int $time must NOT be compensated at all, it has to be a pure timestamp
2574 * @param int|float|string $strtimezone user timezone
2577 function dst_offset_on($time, $strtimezone = null) {
2578 $tz = core_date
::get_user_timezone($strtimezone);
2579 $date = new DateTime('@' . $time);
2580 $date->setTimezone(new DateTimeZone($tz));
2581 if ($date->format('I') == '1') {
2582 if ($tz === 'Australia/Lord_Howe') {
2591 * Calculates when the day appears in specific month
2595 * @param int $startday starting day of the month
2596 * @param int $weekday The day when week starts (normally taken from user preferences)
2597 * @param int $month The month whose day is sought
2598 * @param int $year The year of the month whose day is sought
2601 function find_day_in_month($startday, $weekday, $month, $year) {
2602 $calendartype = \core_calendar\type_factory
::get_calendar_instance();
2604 $daysinmonth = days_in_month($month, $year);
2605 $daysinweek = count($calendartype->get_weekdays());
2607 if ($weekday == -1) {
2608 // Don't care about weekday, so return:
2609 // abs($startday) if $startday != -1
2610 // $daysinmonth otherwise.
2611 return ($startday == -1) ?
$daysinmonth : abs($startday);
2614 // From now on we 're looking for a specific weekday.
2615 // Give "end of month" its actual value, since we know it.
2616 if ($startday == -1) {
2617 $startday = -1 * $daysinmonth;
2620 // Starting from day $startday, the sign is the direction.
2621 if ($startday < 1) {
2622 $startday = abs($startday);
2623 $lastmonthweekday = dayofweek($daysinmonth, $month, $year);
2625 // This is the last such weekday of the month.
2626 $lastinmonth = $daysinmonth +
$weekday - $lastmonthweekday;
2627 if ($lastinmonth > $daysinmonth) {
2628 $lastinmonth -= $daysinweek;
2631 // Find the first such weekday <= $startday.
2632 while ($lastinmonth > $startday) {
2633 $lastinmonth -= $daysinweek;
2636 return $lastinmonth;
2638 $indexweekday = dayofweek($startday, $month, $year);
2640 $diff = $weekday - $indexweekday;
2642 $diff +
= $daysinweek;
2645 // This is the first such weekday of the month equal to or after $startday.
2646 $firstfromindex = $startday +
$diff;
2648 return $firstfromindex;
2653 * Calculate the number of days in a given month
2657 * @param int $month The month whose day count is sought
2658 * @param int $year The year of the month whose day count is sought
2661 function days_in_month($month, $year) {
2662 $calendartype = \core_calendar\type_factory
::get_calendar_instance();
2663 return $calendartype->get_num_days_in_month($year, $month);
2667 * Calculate the position in the week of a specific calendar day
2671 * @param int $day The day of the date whose position in the week is sought
2672 * @param int $month The month of the date whose position in the week is sought
2673 * @param int $year The year of the date whose position in the week is sought
2676 function dayofweek($day, $month, $year) {
2677 $calendartype = \core_calendar\type_factory
::get_calendar_instance();
2678 return $calendartype->get_weekday($year, $month, $day);
2681 // USER AUTHENTICATION AND LOGIN.
2684 * Returns full login url.
2686 * Any form submissions for authentication to this URL must include username,
2687 * password as well as a logintoken generated by \core\session\manager::get_login_token().
2689 * @return string login url
2691 function get_login_url() {
2694 return "$CFG->wwwroot/login/index.php";
2698 * This function checks that the current user is logged in and has the
2699 * required privileges
2701 * This function checks that the current user is logged in, and optionally
2702 * whether they are allowed to be in a particular course and view a particular
2704 * If they are not logged in, then it redirects them to the site login unless
2705 * $autologinguest is set and {@link $CFG}->autologinguests is set to 1 in which
2706 * case they are automatically logged in as guests.
2707 * If $courseid is given and the user is not enrolled in that course then the
2708 * user is redirected to the course enrolment page.
2709 * If $cm is given and the course module is hidden and the user is not a teacher
2710 * in the course then the user is redirected to the course home page.
2712 * When $cm parameter specified, this function sets page layout to 'module'.
2713 * You need to change it manually later if some other layout needed.
2715 * @package core_access
2718 * @param mixed $courseorid id of the course or course object
2719 * @param bool $autologinguest default true
2720 * @param object $cm course module object
2721 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
2722 * true. Used to avoid (=false) some scripts (file.php...) to set that variable,
2723 * in order to keep redirects working properly. MDL-14495
2724 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
2725 * @return mixed Void, exit, and die depending on path
2726 * @throws coding_exception
2727 * @throws require_login_exception
2728 * @throws moodle_exception
2730 function require_login($courseorid = null, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) {
2731 global $CFG, $SESSION, $USER, $PAGE, $SITE, $DB, $OUTPUT;
2733 // Must not redirect when byteserving already started.
2734 if (!empty($_SERVER['HTTP_RANGE'])) {
2735 $preventredirect = true;
2739 // We cannot redirect for AJAX scripts either.
2740 $preventredirect = true;
2743 // Setup global $COURSE, themes, language and locale.
2744 if (!empty($courseorid)) {
2745 if (is_object($courseorid)) {
2746 $course = $courseorid;
2747 } else if ($courseorid == SITEID
) {
2748 $course = clone($SITE);
2750 $course = $DB->get_record('course', array('id' => $courseorid), '*', MUST_EXIST
);
2753 if ($cm->course
!= $course->id
) {
2754 throw new coding_exception('course and cm parameters in require_login() call do not match!!');
2756 // Make sure we have a $cm from get_fast_modinfo as this contains activity access details.
2757 if (!($cm instanceof cm_info
)) {
2758 // Note: nearly all pages call get_fast_modinfo anyway and it does not make any
2759 // db queries so this is not really a performance concern, however it is obviously
2760 // better if you use get_fast_modinfo to get the cm before calling this.
2761 $modinfo = get_fast_modinfo($course);
2762 $cm = $modinfo->get_cm($cm->id
);
2766 // Do not touch global $COURSE via $PAGE->set_course(),
2767 // the reasons is we need to be able to call require_login() at any time!!
2770 throw new coding_exception('cm parameter in require_login() requires valid course parameter!');
2774 // If this is an AJAX request and $setwantsurltome is true then we need to override it and set it to false.
2775 // Otherwise the AJAX request URL will be set to $SESSION->wantsurl and events such as self enrolment in the future
2776 // risk leading the user back to the AJAX request URL.
2777 if ($setwantsurltome && defined('AJAX_SCRIPT') && AJAX_SCRIPT
) {
2778 $setwantsurltome = false;
2781 // Redirect to the login page if session has expired, only with dbsessions enabled (MDL-35029) to maintain current behaviour.
2782 if ((!isloggedin() or isguestuser()) && !empty($SESSION->has_timed_out
) && !empty($CFG->dbsessions
)) {
2783 if ($preventredirect) {
2784 throw new require_login_session_timeout_exception();
2786 if ($setwantsurltome) {
2787 $SESSION->wantsurl
= qualified_me();
2789 redirect(get_login_url());
2793 // If the user is not even logged in yet then make sure they are.
2794 if (!isloggedin()) {
2795 if ($autologinguest && !empty($CFG->autologinguests
)) {
2796 if (!$guest = get_complete_user_data('id', $CFG->siteguest
)) {
2797 // Misconfigured site guest, just redirect to login page.
2798 redirect(get_login_url());
2799 exit; // Never reached.
2801 $lang = isset($SESSION->lang
) ?
$SESSION->lang
: $CFG->lang
;
2802 complete_user_login($guest);
2803 $USER->autologinguest
= true;
2804 $SESSION->lang
= $lang;
2806 // NOTE: $USER->site check was obsoleted by session test cookie, $USER->confirmed test is in login/index.php.
2807 if ($preventredirect) {
2808 throw new require_login_exception('You are not logged in');
2811 if ($setwantsurltome) {
2812 $SESSION->wantsurl
= qualified_me();
2815 // Give auth plugins an opportunity to authenticate or redirect to an external login page
2816 $authsequence = get_enabled_auth_plugins(); // Auths, in sequence.
2817 foreach($authsequence as $authname) {
2818 $authplugin = get_auth_plugin($authname);
2819 $authplugin->pre_loginpage_hook();
2822 $modinfo = get_fast_modinfo($course);
2823 $cm = $modinfo->get_cm($cm->id
);
2825 set_access_log_user();
2830 // If we're still not logged in then go to the login page
2831 if (!isloggedin()) {
2832 redirect(get_login_url());
2833 exit; // Never reached.
2838 // Loginas as redirection if needed.
2839 if ($course->id
!= SITEID
and \core\session\manager
::is_loggedinas()) {
2840 if ($USER->loginascontext
->contextlevel
== CONTEXT_COURSE
) {
2841 if ($USER->loginascontext
->instanceid
!= $course->id
) {
2842 throw new \
moodle_exception('loginasonecourse', '',
2843 $CFG->wwwroot
.'/course/view.php?id='.$USER->loginascontext
->instanceid
);
2848 // Check whether the user should be changing password (but only if it is REALLY them).
2849 if (get_user_preferences('auth_forcepasswordchange') && !\core\session\manager
::is_loggedinas()) {
2850 $userauth = get_auth_plugin($USER->auth
);
2851 if ($userauth->can_change_password() and !$preventredirect) {
2852 if ($setwantsurltome) {
2853 $SESSION->wantsurl
= qualified_me();
2855 if ($changeurl = $userauth->change_password_url()) {
2856 // Use plugin custom url.
2857 redirect($changeurl);
2859 // Use moodle internal method.
2860 redirect($CFG->wwwroot
.'/login/change_password.php');
2862 } else if ($userauth->can_change_password()) {
2863 throw new moodle_exception('forcepasswordchangenotice');
2865 throw new moodle_exception('nopasswordchangeforced', 'auth');
2869 // Check that the user account is properly set up. If we can't redirect to
2870 // edit their profile and this is not a WS request, perform just the lax check.
2871 // It will allow them to use filepicker on the profile edit page.
2873 if ($preventredirect && !WS_SERVER
) {
2874 $usernotfullysetup = user_not_fully_set_up($USER, false);
2876 $usernotfullysetup = user_not_fully_set_up($USER, true);
2879 if ($usernotfullysetup) {
2880 if ($preventredirect) {
2881 throw new moodle_exception('usernotfullysetup');
2883 if ($setwantsurltome) {
2884 $SESSION->wantsurl
= qualified_me();
2886 redirect($CFG->wwwroot
.'/user/edit.php?id='. $USER->id
.'&course='. SITEID
);
2889 // Make sure the USER has a sesskey set up. Used for CSRF protection.
2892 if (\core\session\manager
::is_loggedinas()) {
2893 // During a "logged in as" session we should force all content to be cleaned because the
2894 // logged in user will be viewing potentially malicious user generated content.
2895 // See MDL-63786 for more details.
2896 $CFG->forceclean
= true;
2899 $afterlogins = get_plugins_with_function('after_require_login', 'lib.php');
2901 // Do not bother admins with any formalities, except for activities pending deletion.
2902 if (is_siteadmin() && !($cm && $cm->deletioninprogress
)) {
2903 // Set the global $COURSE.
2905 $PAGE->set_cm($cm, $course);
2906 $PAGE->set_pagelayout('incourse');
2907 } else if (!empty($courseorid)) {
2908 $PAGE->set_course($course);
2910 // Set accesstime or the user will appear offline which messes up messaging.
2911 // Do not update access time for webservice or ajax requests.
2912 if (!WS_SERVER
&& !AJAX_SCRIPT
) {
2913 user_accesstime_log($course->id
);
2916 foreach ($afterlogins as $plugintype => $plugins) {
2917 foreach ($plugins as $pluginfunction) {
2918 $pluginfunction($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2924 // Scripts have a chance to declare that $USER->policyagreed should not be checked.
2925 // This is mostly for places where users are actually accepting the policies, to avoid the redirect loop.
2926 if (!defined('NO_SITEPOLICY_CHECK')) {
2927 define('NO_SITEPOLICY_CHECK', false);
2930 // Check that the user has agreed to a site policy if there is one - do not test in case of admins.
2931 // Do not test if the script explicitly asked for skipping the site policies check.
2932 // Or if the user auth type is webservice.
2933 if (!$USER->policyagreed
&& !is_siteadmin() && !NO_SITEPOLICY_CHECK
&& $USER->auth
!== 'webservice') {
2934 $manager = new \core_privacy\local\sitepolicy\
manager();
2935 if ($policyurl = $manager->get_redirect_url(isguestuser())) {
2936 if ($preventredirect) {
2937 throw new moodle_exception('sitepolicynotagreed', 'error', '', $policyurl->out());
2939 if ($setwantsurltome) {
2940 $SESSION->wantsurl
= qualified_me();
2942 redirect($policyurl);
2946 // Fetch the system context, the course context, and prefetch its child contexts.
2947 $sysctx = context_system
::instance();
2948 $coursecontext = context_course
::instance($course->id
, MUST_EXIST
);
2950 $cmcontext = context_module
::instance($cm->id
, MUST_EXIST
);
2955 // If the site is currently under maintenance, then print a message.
2956 if (!empty($CFG->maintenance_enabled
) and !has_capability('moodle/site:maintenanceaccess', $sysctx)) {
2957 if ($preventredirect) {
2958 throw new require_login_exception('Maintenance in progress');
2960 $PAGE->set_context(null);
2961 print_maintenance_message();
2964 // Make sure the course itself is not hidden.
2965 if ($course->id
== SITEID
) {
2966 // Frontpage can not be hidden.
2968 if (is_role_switched($course->id
)) {
2969 // When switching roles ignore the hidden flag - user had to be in course to do the switch.
2971 if (!$course->visible
and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
2972 // Originally there was also test of parent category visibility, BUT is was very slow in complex queries
2973 // involving "my courses" now it is also possible to simply hide all courses user is not enrolled in :-).
2974 if ($preventredirect) {
2975 throw new require_login_exception('Course is hidden');
2977 $PAGE->set_context(null);
2978 // We need to override the navigation URL as the course won't have been added to the navigation and thus
2979 // the navigation will mess up when trying to find it.
2980 navigation_node
::override_active_url(new moodle_url('/'));
2981 notice(get_string('coursehidden'), $CFG->wwwroot
.'/');
2986 // Is the user enrolled?
2987 if ($course->id
== SITEID
) {
2988 // Everybody is enrolled on the frontpage.
2990 if (\core\session\manager
::is_loggedinas()) {
2991 // Make sure the REAL person can access this course first.
2992 $realuser = \core\session\manager
::get_realuser();
2993 if (!is_enrolled($coursecontext, $realuser->id
, '', true) and
2994 !is_viewing($coursecontext, $realuser->id
) and !is_siteadmin($realuser->id
)) {
2995 if ($preventredirect) {
2996 throw new require_login_exception('Invalid course login-as access');
2998 $PAGE->set_context(null);
2999 echo $OUTPUT->header();
3000 notice(get_string('studentnotallowed', '', fullname($USER, true)), $CFG->wwwroot
.'/');
3006 if (is_role_switched($course->id
)) {
3007 // Ok, user had to be inside this course before the switch.
3010 } else if (is_viewing($coursecontext, $USER)) {
3011 // Ok, no need to mess with enrol.
3015 if (isset($USER->enrol
['enrolled'][$course->id
])) {
3016 if ($USER->enrol
['enrolled'][$course->id
] > time()) {
3018 if (isset($USER->enrol
['tempguest'][$course->id
])) {
3019 unset($USER->enrol
['tempguest'][$course->id
]);
3020 remove_temp_course_roles($coursecontext);
3024 unset($USER->enrol
['enrolled'][$course->id
]);
3027 if (isset($USER->enrol
['tempguest'][$course->id
])) {
3028 if ($USER->enrol
['tempguest'][$course->id
] == 0) {
3030 } else if ($USER->enrol
['tempguest'][$course->id
] > time()) {
3034 unset($USER->enrol
['tempguest'][$course->id
]);
3035 remove_temp_course_roles($coursecontext);
3041 $until = enrol_get_enrolment_end($coursecontext->instanceid
, $USER->id
);
3042 if ($until !== false) {
3043 // Active participants may always access, a timestamp in the future, 0 (always) or false.
3045 $until = ENROL_MAX_TIMESTAMP
;
3047 $USER->enrol
['enrolled'][$course->id
] = $until;
3050 } else if (core_course_category
::can_view_course_info($course)) {
3051 $params = array('courseid' => $course->id
, 'status' => ENROL_INSTANCE_ENABLED
);
3052 $instances = $DB->get_records('enrol', $params, 'sortorder, id ASC');
3053 $enrols = enrol_get_plugins(true);
3054 // First ask all enabled enrol instances in course if they want to auto enrol user.
3055 foreach ($instances as $instance) {
3056 if (!isset($enrols[$instance->enrol
])) {
3059 // Get a duration for the enrolment, a timestamp in the future, 0 (always) or false.
3060 $until = $enrols[$instance->enrol
]->try_autoenrol($instance);
3061 if ($until !== false) {
3063 $until = ENROL_MAX_TIMESTAMP
;
3065 $USER->enrol
['enrolled'][$course->id
] = $until;
3070 // If not enrolled yet try to gain temporary guest access.
3072 foreach ($instances as $instance) {
3073 if (!isset($enrols[$instance->enrol
])) {
3076 // Get a duration for the guest access, a timestamp in the future or false.
3077 $until = $enrols[$instance->enrol
]->try_guestaccess($instance);
3078 if ($until !== false and $until > time()) {
3079 $USER->enrol
['tempguest'][$course->id
] = $until;
3086 // User is not enrolled and is not allowed to browse courses here.
3087 if ($preventredirect) {
3088 throw new require_login_exception('Course is not available');
3090 $PAGE->set_context(null);
3091 // We need to override the navigation URL as the course won't have been added to the navigation and thus
3092 // the navigation will mess up when trying to find it.
3093 navigation_node
::override_active_url(new moodle_url('/'));
3094 notice(get_string('coursehidden'), $CFG->wwwroot
.'/');
3100 if ($preventredirect) {
3101 throw new require_login_exception('Not enrolled');
3103 if ($setwantsurltome) {
3104 $SESSION->wantsurl
= qualified_me();
3106 redirect($CFG->wwwroot
.'/enrol/index.php?id='. $course->id
);
3110 // Check whether the activity has been scheduled for deletion. If so, then deny access, even for admins.
3111 if ($cm && $cm->deletioninprogress
) {
3112 if ($preventredirect) {
3113 throw new moodle_exception('activityisscheduledfordeletion');
3115 require_once($CFG->dirroot
. '/course/lib.php');
3116 redirect(course_get_url($course), get_string('activityisscheduledfordeletion', 'error'));
3119 // Check visibility of activity to current user; includes visible flag, conditional availability, etc.
3120 if ($cm && !$cm->uservisible
) {
3121 if ($preventredirect) {
3122 throw new require_login_exception('Activity is hidden');
3124 // Get the error message that activity is not available and why (if explanation can be shown to the user).
3125 $PAGE->set_course($course);
3126 $renderer = $PAGE->get_renderer('course');
3127 $message = $renderer->course_section_cm_unavailable_error_message($cm);
3128 redirect(course_get_url($course), $message, null, \core\output\notification
::NOTIFY_ERROR
);
3131 // Set the global $COURSE.
3133 $PAGE->set_cm($cm, $course);
3134 $PAGE->set_pagelayout('incourse');
3135 } else if (!empty($courseorid)) {
3136 $PAGE->set_course($course);
3139 foreach ($afterlogins as $plugintype => $plugins) {
3140 foreach ($plugins as $pluginfunction) {
3141 $pluginfunction($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3145 // Finally access granted, update lastaccess times.
3146 // Do not update access time for webservice or ajax requests.
3147 if (!WS_SERVER
&& !AJAX_SCRIPT
) {
3148 user_accesstime_log($course->id
);
3153 * A convenience function for where we must be logged in as admin
3156 function require_admin() {
3157 require_login(null, false);
3158 require_capability('moodle/site:config', context_system
::instance());
3162 * This function just makes sure a user is logged out.
3164 * @package core_access
3167 function require_logout() {
3170 if (!isloggedin()) {
3171 // This should not happen often, no need for hooks or events here.
3172 \core\session\manager
::terminate_current();
3176 // Execute hooks before action.
3177 $authplugins = array();
3178 $authsequence = get_enabled_auth_plugins();
3179 foreach ($authsequence as $authname) {
3180 $authplugins[$authname] = get_auth_plugin($authname);
3181 $authplugins[$authname]->prelogout_hook();
3184 // Store info that gets removed during logout.
3185 $sid = session_id();
3186 $event = \core\event\user_loggedout
::create(
3188 'userid' => $USER->id
,
3189 'objectid' => $USER->id
,
3190 'other' => array('sessionid' => $sid),
3193 if ($session = $DB->get_record('sessions', array('sid'=>$sid))) {
3194 $event->add_record_snapshot('sessions', $session);
3197 // Clone of $USER object to be used by auth plugins.
3198 $user = fullclone($USER);
3200 // Delete session record and drop $_SESSION content.
3201 \core\session\manager
::terminate_current();
3203 // Trigger event AFTER action.
3206 // Hook to execute auth plugins redirection after event trigger.
3207 foreach ($authplugins as $authplugin) {
3208 $authplugin->postlogout_hook($user);
3213 * Weaker version of require_login()
3215 * This is a weaker version of {@link require_login()} which only requires login
3216 * when called from within a course rather than the site page, unless
3217 * the forcelogin option is turned on.
3218 * @see require_login()
3220 * @package core_access
3223 * @param mixed $courseorid The course object or id in question
3224 * @param bool $autologinguest Allow autologin guests if that is wanted
3225 * @param object $cm Course activity module if known
3226 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
3227 * true. Used to avoid (=false) some scripts (file.php...) to set that variable,
3228 * in order to keep redirects working properly. MDL-14495
3229 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
3231 * @throws coding_exception
3233 function require_course_login($courseorid, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) {
3234 global $CFG, $PAGE, $SITE;
3235 $issite = ((is_object($courseorid) and $courseorid->id
== SITEID
)
3236 or (!is_object($courseorid) and $courseorid == SITEID
));
3237 if ($issite && !empty($cm) && !($cm instanceof cm_info
)) {
3238 // Note: nearly all pages call get_fast_modinfo anyway and it does not make any
3239 // db queries so this is not really a performance concern, however it is obviously
3240 // better if you use get_fast_modinfo to get the cm before calling this.
3241 if (is_object($courseorid)) {
3242 $course = $courseorid;
3244 $course = clone($SITE);
3246 $modinfo = get_fast_modinfo($course);
3247 $cm = $modinfo->get_cm($cm->id
);
3249 if (!empty($CFG->forcelogin
)) {
3250 // Login required for both SITE and courses.
3251 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3253 } else if ($issite && !empty($cm) and !$cm->uservisible
) {
3254 // Always login for hidden activities.
3255 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3257 } else if (isloggedin() && !isguestuser()) {
3258 // User is already logged in. Make sure the login is complete (user is fully setup, policies agreed).
3259 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3261 } else if ($issite) {
3262 // Login for SITE not required.
3263 // We still need to instatiate PAGE vars properly so that things that rely on it like navigation function correctly.
3264 if (!empty($courseorid)) {
3265 if (is_object($courseorid)) {
3266 $course = $courseorid;
3268 $course = clone $SITE;
3271 if ($cm->course
!= $course->id
) {
3272 throw new coding_exception('course and cm parameters in require_course_login() call do not match!!');
3274 $PAGE->set_cm($cm, $course);
3275 $PAGE->set_pagelayout('incourse');
3277 $PAGE->set_course($course);
3280 // If $PAGE->course, and hence $PAGE->context, have not already been set up properly, set them up now.
3281 $PAGE->set_course($PAGE->course
);
3283 // Do not update access time for webservice or ajax requests.
3284 if (!WS_SERVER
&& !AJAX_SCRIPT
) {
3285 user_accesstime_log(SITEID
);
3290 // Course login always required.
3291 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
3296 * Validates a user key, checking if the key exists, is not expired and the remote ip is correct.
3298 * @param string $keyvalue the key value
3299 * @param string $script unique script identifier
3300 * @param int $instance instance id
3301 * @return stdClass the key entry in the user_private_key table
3303 * @throws moodle_exception
3305 function validate_user_key($keyvalue, $script, $instance) {
3308 if (!$key = $DB->get_record('user_private_key', array('script' => $script, 'value' => $keyvalue, 'instance' => $instance))) {
3309 throw new \
moodle_exception('invalidkey');
3312 if (!empty($key->validuntil
) and $key->validuntil
< time()) {
3313 throw new \
moodle_exception('expiredkey');
3316 if ($key->iprestriction
) {
3317 $remoteaddr = getremoteaddr(null);
3318 if (empty($remoteaddr) or !address_in_subnet($remoteaddr, $key->iprestriction
)) {
3319 throw new \
moodle_exception('ipmismatch');
3326 * Require key login. Function terminates with error if key not found or incorrect.
3328 * @uses NO_MOODLE_COOKIES
3329 * @uses PARAM_ALPHANUM
3330 * @param string $script unique script identifier
3331 * @param int $instance optional instance id
3332 * @param string $keyvalue The key. If not supplied, this will be fetched from the current session.
3333 * @return int Instance ID
3335 function require_user_key_login($script, $instance = null, $keyvalue = null) {
3338 if (!NO_MOODLE_COOKIES
) {
3339 throw new \
moodle_exception('sessioncookiesdisable');
3343 \core\session\manager
::write_close();
3345 if (null === $keyvalue) {
3346 $keyvalue = required_param('key', PARAM_ALPHANUM
);
3349 $key = validate_user_key($keyvalue, $script, $instance);
3351 if (!$user = $DB->get_record('user', array('id' => $key->userid
))) {
3352 throw new \
moodle_exception('invaliduserid');
3355 core_user
::require_active_user($user, true, true);
3357 // Emulate normal session.
3358 enrol_check_plugins($user, false);
3359 \core\session\manager
::set_user($user);
3361 // Note we are not using normal login.
3362 if (!defined('USER_KEY_LOGIN')) {
3363 define('USER_KEY_LOGIN', true);
3366 // Return instance id - it might be empty.
3367 return $key->instance
;
3371 * Creates a new private user access key.
3373 * @param string $script unique target identifier
3374 * @param int $userid
3375 * @param int $instance optional instance id
3376 * @param string $iprestriction optional ip restricted access
3377 * @param int $validuntil key valid only until given data
3378 * @return string access key value
3380 function create_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
3383 $key = new stdClass();
3384 $key->script
= $script;
3385 $key->userid
= $userid;
3386 $key->instance
= $instance;
3387 $key->iprestriction
= $iprestriction;
3388 $key->validuntil
= $validuntil;
3389 $key->timecreated
= time();
3391 // Something long and unique.
3392 $key->value
= md5($userid.'_'.time().random_string(40));
3393 while ($DB->record_exists('user_private_key', array('value' => $key->value
))) {
3395 $key->value
= md5($userid.'_'.time().random_string(40));
3397 $DB->insert_record('user_private_key', $key);
3402 * Delete the user's new private user access keys for a particular script.
3404 * @param string $script unique target identifier
3405 * @param int $userid
3408 function delete_user_key($script, $userid) {
3410 $DB->delete_records('user_private_key', array('script' => $script, 'userid' => $userid));
3414 * Gets a private user access key (and creates one if one doesn't exist).
3416 * @param string $script unique target identifier
3417 * @param int $userid
3418 * @param int $instance optional instance id
3419 * @param string $iprestriction optional ip restricted access
3420 * @param int $validuntil key valid only until given date
3421 * @return string access key value
3423 function get_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
3426 if ($key = $DB->get_record('user_private_key', array('script' => $script, 'userid' => $userid,
3427 'instance' => $instance, 'iprestriction' => $iprestriction,
3428 'validuntil' => $validuntil))) {
3431 return create_user_key($script, $userid, $instance, $iprestriction, $validuntil);
3437 * Modify the user table by setting the currently logged in user's last login to now.
3439 * @return bool Always returns true
3441 function update_user_login_times() {
3442 global $USER, $DB, $SESSION;
3444 if (isguestuser()) {
3445 // Do not update guest access times/ips for performance.
3449 if (defined('USER_KEY_LOGIN') && USER_KEY_LOGIN
=== true) {
3450 // Do not update user login time when using user key login.
3456 $user = new stdClass();
3457 $user->id
= $USER->id
;
3459 // Make sure all users that logged in have some firstaccess.
3460 if ($USER->firstaccess
== 0) {
3461 $USER->firstaccess
= $user->firstaccess
= $now;
3464 // Store the previous current as lastlogin.
3465 $USER->lastlogin
= $user->lastlogin
= $USER->currentlogin
;
3467 $USER->currentlogin
= $user->currentlogin
= $now;
3469 // Function user_accesstime_log() may not update immediately, better do it here.
3470 $USER->lastaccess
= $user->lastaccess
= $now;
3471 $SESSION->userpreviousip
= $USER->lastip
;
3472 $USER->lastip
= $user->lastip
= getremoteaddr();
3474 // Note: do not call user_update_user() here because this is part of the login process,
3475 // the login event means that these fields were updated.
3476 $DB->update_record('user', $user);
3481 * Determines if a user has completed setting up their account.
3483 * The lax mode (with $strict = false) has been introduced for special cases
3484 * only where we want to skip certain checks intentionally. This is valid in
3485 * certain mnet or ajax scenarios when the user cannot / should not be
3486 * redirected to edit their profile. In most cases, you should perform the
3489 * @param stdClass $user A {@link $USER} object to test for the existence of a valid name and email
3490 * @param bool $strict Be more strict and assert id and custom profile fields set, too
3493 function user_not_fully_set_up($user, $strict = true) {
3494 global $CFG, $SESSION, $USER;
3495 require_once($CFG->dirroot
.'/user/profile/lib.php');
3497 // If the user is setup then store this in the session to avoid re-checking.
3498 // Some edge cases are when the users email starts to bounce or the
3499 // configuration for custom fields has changed while they are logged in so
3500 // we re-check this fully every hour for the rare cases it has changed.
3501 if (isset($USER->id
) && isset($user->id
) && $USER->id
=== $user->id
&&
3502 isset($SESSION->fullysetupstrict
) && (time() - $SESSION->fullysetupstrict
) < HOURSECS
) {
3506 if (isguestuser($user)) {
3510 if (empty($user->firstname
) or empty($user->lastname
) or empty($user->email
) or over_bounce_threshold($user)) {
3515 if (empty($user->id
)) {
3516 // Strict mode can be used with existing accounts only.
3519 if (!profile_has_required_custom_fields_set($user->id
)) {
3522 if (isset($USER->id
) && isset($user->id
) && $USER->id
=== $user->id
) {
3523 $SESSION->fullysetupstrict
= time();
3531 * Check whether the user has exceeded the bounce threshold
3533 * @param stdClass $user A {@link $USER} object
3534 * @return bool true => User has exceeded bounce threshold
3536 function over_bounce_threshold($user) {
3539 if (empty($CFG->handlebounces
)) {
3543 if (empty($user->id
)) {
3544 // No real (DB) user, nothing to do here.
3548 // Set sensible defaults.
3549 if (empty($CFG->minbounces
)) {
3550 $CFG->minbounces
= 10;
3552 if (empty($CFG->bounceratio
)) {
3553 $CFG->bounceratio
= .20;
3557 if ($bounce = $DB->get_record('user_preferences', array ('userid' => $user->id
, 'name' => 'email_bounce_count'))) {
3558 $bouncecount = $bounce->value
;
3560 if ($send = $DB->get_record('user_preferences', array('userid' => $user->id
, 'name' => 'email_send_count'))) {
3561 $sendcount = $send->value
;
3563 return ($bouncecount >= $CFG->minbounces
&& $bouncecount/$sendcount >= $CFG->bounceratio
);
3567 * Used to increment or reset email sent count
3569 * @param stdClass $user object containing an id
3570 * @param bool $reset will reset the count to 0
3573 function set_send_count($user, $reset=false) {
3576 if (empty($user->id
)) {
3577 // No real (DB) user, nothing to do here.
3581 if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id
, 'name' => 'email_send_count'))) {
3582 $pref->value
= (!empty($reset)) ?
0 : $pref->value+
1;
3583 $DB->update_record('user_preferences', $pref);
3584 } else if (!empty($reset)) {
3585 // If it's not there and we're resetting, don't bother. Make a new one.
3586 $pref = new stdClass();
3587 $pref->name
= 'email_send_count';
3589 $pref->userid
= $user->id
;
3590 $DB->insert_record('user_preferences', $pref, false);
3595 * Increment or reset user's email bounce count
3597 * @param stdClass $user object containing an id
3598 * @param bool $reset will reset the count to 0
3600 function set_bounce_count($user, $reset=false) {
3603 if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id
, 'name' => 'email_bounce_count'))) {
3604 $pref->value
= (!empty($reset)) ?
0 : $pref->value+
1;
3605 $DB->update_record('user_preferences', $pref);
3606 } else if (!empty($reset)) {
3607 // If it's not there and we're resetting, don't bother. Make a new one.
3608 $pref = new stdClass();
3609 $pref->name
= 'email_bounce_count';
3611 $pref->userid
= $user->id
;
3612 $DB->insert_record('user_preferences', $pref, false);
3617 * Determines if the logged in user is currently moving an activity
3619 * @param int $courseid The id of the course being tested
3622 function ismoving($courseid) {
3625 if (!empty($USER->activitycopy
)) {
3626 return ($USER->activitycopycourse
== $courseid);
3632 * Returns a persons full name
3634 * Given an object containing all of the users name values, this function returns a string with the full name of the person.
3635 * The result may depend on system settings or language. 'override' will force the alternativefullnameformat to be used. In
3636 * English, fullname as well as alternativefullnameformat is set to 'firstname lastname' by default. But you could have
3637 * fullname set to 'firstname lastname' and alternativefullnameformat set to 'firstname middlename alternatename lastname'.
3639 * @param stdClass $user A {@link $USER} object to get full name of.
3640 * @param bool $override If true then the alternativefullnameformat format rather than fullnamedisplay format will be used.
3643 function fullname($user, $override=false) {
3644 // Note: We do not intend to deprecate this function any time soon as it is too widely used at this time.
3645 // Uses of it should be updated to use the new API and pass updated arguments.
3647 // Return an empty string if there is no user.
3652 $options = ['override' => $override];
3653 return core_user
::get_fullname($user, null, $options);
3657 * Reduces lines of duplicated code for getting user name fields.
3659 * See also {@link user_picture::unalias()}
3661 * @param object $addtoobject Object to add user name fields to.
3662 * @param object $secondobject Object that contains user name field information.
3663 * @param string $prefix prefix to be added to all fields (including $additionalfields) e.g. authorfirstname.
3664 * @param array $additionalfields Additional fields to be matched with data in the second object.
3665 * The key can be set to the user table field name.
3666 * @return object User name fields.
3668 function username_load_fields_from_object($addtoobject, $secondobject, $prefix = null, $additionalfields = null) {
3670 foreach (\core_user\fields
::get_name_fields() as $field) {
3671 $fields[$field] = $prefix . $field;
3673 if ($additionalfields) {
3674 // Additional fields can specify their own 'alias' such as 'id' => 'userid'. This checks to see if
3675 // the key is a number and then sets the key to the array value.
3676 foreach ($additionalfields as $key => $value) {
3677 if (is_numeric($key)) {
3678 $additionalfields[$value] = $prefix . $value;
3679 unset($additionalfields[$key]);
3681 $additionalfields[$key] = $prefix . $value;
3684 $fields = array_merge($fields, $additionalfields);
3686 foreach ($fields as $key => $field) {
3687 // Important that we have all of the user name fields present in the object that we are sending back.
3688 $addtoobject->$key = '';
3689 if (isset($secondobject->$field)) {
3690 $addtoobject->$key = $secondobject->$field;
3693 return $addtoobject;
3697 * Returns an array of values in order of occurance in a provided string.
3698 * The key in the result is the character postion in the string.
3700 * @param array $values Values to be found in the string format
3701 * @param string $stringformat The string which may contain values being searched for.
3702 * @return array An array of values in order according to placement in the string format.
3704 function order_in_string($values, $stringformat) {
3705 $valuearray = array();
3706 foreach ($values as $value) {
3707 $pattern = "/$value\b/";
3708 // Using preg_match as strpos() may match values that are similar e.g. firstname and firstnamephonetic.
3709 if (preg_match($pattern, $stringformat)) {
3710 $replacement = "thing";
3711 // Replace the value with something more unique to ensure we get the right position when using strpos().
3712 $newformat = preg_replace($pattern, $replacement, $stringformat);
3713 $position = strpos($newformat, $replacement);
3714 $valuearray[$position] = $value;
3722 * Returns whether a given authentication plugin exists.
3724 * @param string $auth Form of authentication to check for. Defaults to the global setting in {@link $CFG}.
3725 * @return boolean Whether the plugin is available.
3727 function exists_auth_plugin($auth) {
3730 if (file_exists("{$CFG->dirroot}/auth/$auth/auth.php")) {
3731 return is_readable("{$CFG->dirroot}/auth/$auth/auth.php");
3737 * Checks if a given plugin is in the list of enabled authentication plugins.
3739 * @param string $auth Authentication plugin.
3740 * @return boolean Whether the plugin is enabled.
3742 function is_enabled_auth($auth) {
3747 $enabled = get_enabled_auth_plugins();
3749 return in_array($auth, $enabled);
3753 * Returns an authentication plugin instance.
3755 * @param string $auth name of authentication plugin
3756 * @return auth_plugin_base An instance of the required authentication plugin.
3758 function get_auth_plugin($auth) {
3761 // Check the plugin exists first.
3762 if (! exists_auth_plugin($auth)) {
3763 throw new \
moodle_exception('authpluginnotfound', 'debug', '', $auth);
3766 // Return auth plugin instance.
3767 require_once("{$CFG->dirroot}/auth/$auth/auth.php");
3768 $class = "auth_plugin_$auth";
3773 * Returns array of active auth plugins.
3775 * @param bool $fix fix $CFG->auth if needed. Only set if logged in as admin.
3778 function get_enabled_auth_plugins($fix=false) {
3781 $default = array('manual', 'nologin');
3783 if (empty($CFG->auth
)) {
3786 $auths = explode(',', $CFG->auth
);
3789 $auths = array_unique($auths);
3790 $oldauthconfig = implode(',', $auths);
3791 foreach ($auths as $k => $authname) {
3792 if (in_array($authname, $default)) {
3793 // The manual and nologin plugin never need to be stored.
3795 } else if (!exists_auth_plugin($authname)) {
3796 debugging(get_string('authpluginnotfound', 'debug', $authname));
3801 // Ideally only explicit interaction from a human admin should trigger a
3802 // change in auth config, see MDL-70424 for details.
3804 $newconfig = implode(',', $auths);
3805 if (!isset($CFG->auth
) or $newconfig != $CFG->auth
) {
3806 add_to_config_log('auth', $oldauthconfig, $newconfig, 'core');
3807 set_config('auth', $newconfig);
3811 return (array_merge($default, $auths));
3815 * Returns true if an internal authentication method is being used.
3816 * if method not specified then, global default is assumed
3818 * @param string $auth Form of authentication required
3821 function is_internal_auth($auth) {
3822 // Throws error if bad $auth.
3823 $authplugin = get_auth_plugin($auth);
3824 return $authplugin->is_internal();
3828 * Returns true if the user is a 'restored' one.
3830 * Used in the login process to inform the user and allow him/her to reset the password
3832 * @param string $username username to be checked
3835 function is_restored_user($username) {
3838 return $DB->record_exists('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id
, 'password' => 'restored'));
3842 * Returns an array of user fields
3844 * @return array User field/column names
3846 function get_user_fieldnames() {
3849 $fieldarray = $DB->get_columns('user');
3850 unset($fieldarray['id']);
3851 $fieldarray = array_keys($fieldarray);
3857 * Returns the string of the language for the new user.
3859 * @return string language for the new user
3861 function get_newuser_language() {
3862 global $CFG, $SESSION;
3863 return (!empty($CFG->autolangusercreation
) && !empty($SESSION->lang
)) ?
$SESSION->lang
: $CFG->lang
;
3867 * Creates a bare-bones user record
3869 * @todo Outline auth types and provide code example
3871 * @param string $username New user's username to add to record
3872 * @param string $password New user's password to add to record
3873 * @param string $auth Form of authentication required
3874 * @return stdClass A complete user object
3876 function create_user_record($username, $password, $auth = 'manual') {
3877 global $CFG, $DB, $SESSION;
3878 require_once($CFG->dirroot
.'/user/profile/lib.php');
3879 require_once($CFG->dirroot
.'/user/lib.php');
3881 // Just in case check text case.
3882 $username = trim(core_text
::strtolower($username));
3884 $authplugin = get_auth_plugin($auth);
3885 $customfields = $authplugin->get_custom_user_profile_fields();
3886 $newuser = new stdClass();
3887 if ($newinfo = $authplugin->get_userinfo($username)) {
3888 $newinfo = truncate_userinfo($newinfo);
3889 foreach ($newinfo as $key => $value) {
3890 if (in_array($key, $authplugin->userfields
) ||
(in_array($key, $customfields))) {
3891 $newuser->$key = $value;
3896 if (!empty($newuser->email
)) {
3897 if (email_is_not_allowed($newuser->email
)) {
3898 unset($newuser->email
);
3902 $newuser->auth
= $auth;
3903 $newuser->username
= $username;
3906 // user CFG lang for user if $newuser->lang is empty
3907 // or $user->lang is not an installed language.
3908 if (empty($newuser->lang
) ||
!get_string_manager()->translation_exists($newuser->lang
)) {
3909 $newuser->lang
= get_newuser_language();
3911 $newuser->confirmed
= 1;
3912 $newuser->lastip
= getremoteaddr();
3913 $newuser->timecreated
= time();
3914 $newuser->timemodified
= $newuser->timecreated
;
3915 $newuser->mnethostid
= $CFG->mnet_localhost_id
;
3917 $newuser->id
= user_create_user($newuser, false, false);
3919 // Save user profile data.
3920 profile_save_data($newuser);
3922 $user = get_complete_user_data('id', $newuser->id
);
3923 if (!empty($CFG->{'auth_'.$newuser->auth
.'_forcechangepassword'})) {
3924 set_user_preference('auth_forcepasswordchange', 1, $user);
3926 // Set the password.
3927 update_internal_user_password($user, $password);
3930 \core\event\user_created
::create_from_userid($newuser->id
)->trigger();
3936 * Will update a local user record from an external source (MNET users can not be updated using this method!).
3938 * @param string $username user's username to update the record
3939 * @return stdClass A complete user object
3941 function update_user_record($username) {
3943 // Just in case check text case.
3944 $username = trim(core_text
::strtolower($username));
3946 $oldinfo = $DB->get_record('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id
), '*', MUST_EXIST
);
3947 return update_user_record_by_id($oldinfo->id
);
3951 * Will update a local user record from an external source (MNET users can not be updated using this method!).
3953 * @param int $id user id
3954 * @return stdClass A complete user object
3956 function update_user_record_by_id($id) {
3958 require_once($CFG->dirroot
."/user/profile/lib.php");
3959 require_once($CFG->dirroot
.'/user/lib.php');
3961 $params = array('mnethostid' => $CFG->mnet_localhost_id
, 'id' => $id, 'deleted' => 0);
3962 $oldinfo = $DB->get_record('user', $params, '*', MUST_EXIST
);
3965 $userauth = get_auth_plugin($oldinfo->auth
);
3967 if ($newinfo = $userauth->get_userinfo($oldinfo->username
)) {
3968 $newinfo = truncate_userinfo($newinfo);
3969 $customfields = $userauth->get_custom_user_profile_fields();
3971 foreach ($newinfo as $key => $value) {
3972 $iscustom = in_array($key, $customfields);
3974 $key = strtolower($key);
3976 if ((!property_exists($oldinfo, $key) && !$iscustom) or $key === 'username' or $key === 'id'
3977 or $key === 'auth' or $key === 'mnethostid' or $key === 'deleted') {
3978 // Unknown or must not be changed.
3981 if (empty($userauth->config
->{'field_updatelocal_' . $key}) ||
empty($userauth->config
->{'field_lock_' . $key})) {
3984 $confval = $userauth->config
->{'field_updatelocal_' . $key};
3985 $lockval = $userauth->config
->{'field_lock_' . $key};
3986 if ($confval === 'onlogin') {
3987 // MDL-4207 Don't overwrite modified user profile values with
3988 // empty LDAP values when 'unlocked if empty' is set. The purpose
3989 // of the setting 'unlocked if empty' is to allow the user to fill
3990 // in a value for the selected field _if LDAP is giving
3991 // nothing_ for this field. Thus it makes sense to let this value
3992 // stand in until LDAP is giving a value for this field.
3993 if (!(empty($value) && $lockval === 'unlockedifempty')) {
3994 if ($iscustom ||
(in_array($key, $userauth->userfields
) &&
3995 ((string)$oldinfo->$key !== (string)$value))) {
3996 $newuser[$key] = (string)$value;
4002 $newuser['id'] = $oldinfo->id
;
4003 $newuser['timemodified'] = time();
4004 user_update_user((object) $newuser, false, false);
4006 // Save user profile data.
4007 profile_save_data((object) $newuser);
4010 \core\event\user_updated
::create_from_userid($newuser['id'])->trigger();
4014 return get_complete_user_data('id', $oldinfo->id
);
4018 * Will truncate userinfo as it comes from auth_get_userinfo (from external auth) which may have large fields.
4020 * @param array $info Array of user properties to truncate if needed
4021 * @return array The now truncated information that was passed in
4023 function truncate_userinfo(array $info) {
4024 // Define the limits.
4033 'institution' => 255,
4034 'department' => 255,
4040 // Apply where needed.
4041 foreach (array_keys($info) as $key) {
4042 if (!empty($limit[$key])) {
4043 $info[$key] = trim(core_text
::substr($info[$key], 0, $limit[$key]));
4051 * Marks user deleted in internal user database and notifies the auth plugin.
4052 * Also unenrols user from all roles and does other cleanup.
4054 * Any plugin that needs to purge user data should register the 'user_deleted' event.
4056 * @param stdClass $user full user object before delete
4057 * @return boolean success
4058 * @throws coding_exception if invalid $user parameter detected
4060 function delete_user(stdClass
$user) {
4061 global $CFG, $DB, $SESSION;
4062 require_once($CFG->libdir
.'/grouplib.php');
4063 require_once($CFG->libdir
.'/gradelib.php');
4064 require_once($CFG->dirroot
.'/message/lib.php');
4065 require_once($CFG->dirroot
.'/user/lib.php');
4067 // Make sure nobody sends bogus record type as parameter.
4068 if (!property_exists($user, 'id') or !property_exists($user, 'username')) {
4069 throw new coding_exception('Invalid $user parameter in delete_user() detected');
4072 // Better not trust the parameter and fetch the latest info this will be very expensive anyway.
4073 if (!$user = $DB->get_record('user', array('id' => $user->id
))) {
4074 debugging('Attempt to delete unknown user account.');
4078 // There must be always exactly one guest record, originally the guest account was identified by username only,
4079 // now we use $CFG->siteguest for performance reasons.
4080 if ($user->username
=== 'guest' or isguestuser($user)) {
4081 debugging('Guest user account can not be deleted.');
4085 // Admin can be theoretically from different auth plugin, but we want to prevent deletion of internal accoutns only,
4086 // if anything goes wrong ppl may force somebody to be admin via config.php setting $CFG->siteadmins.
4087 if ($user->auth
=== 'manual' and is_siteadmin($user)) {
4088 debugging('Local administrator accounts can not be deleted.');
4092 // Allow plugins to use this user object before we completely delete it.
4093 if ($pluginsfunction = get_plugins_with_function('pre_user_delete')) {
4094 foreach ($pluginsfunction as $plugintype => $plugins) {
4095 foreach ($plugins as $pluginfunction) {
4096 $pluginfunction($user);
4101 // Keep user record before updating it, as we have to pass this to user_deleted event.
4102 $olduser = clone $user;
4104 // Keep a copy of user context, we need it for event.
4105 $usercontext = context_user
::instance($user->id
);
4107 // Remove user from communication rooms immediately.
4108 if (core_communication\api
::is_available()) {
4109 foreach (enrol_get_users_courses($user->id
) as $course) {
4110 $communication = \core_communication\processor
::load_by_instance(
4112 'coursecommunication',
4115 $communication->get_room_user_provider()->remove_members_from_room([$user->id
]);
4116 $communication->delete_instance_user_mapping([$user->id
]);
4120 // Delete all grades - backup is kept in grade_grades_history table.
4121 grade_user_delete($user->id
);
4123 // TODO: remove from cohorts using standard API here.
4125 // Remove user tags.
4126 core_tag_tag
::remove_all_item_tags('core', 'user', $user->id
);
4128 // Unconditionally unenrol from all courses.
4129 enrol_user_delete($user);
4131 // Unenrol from all roles in all contexts.
4132 // This might be slow but it is really needed - modules might do some extra cleanup!
4133 role_unassign_all(array('userid' => $user->id
));
4135 // Notify the competency subsystem.
4136 \core_competency\api
::hook_user_deleted($user->id
);
4138 // Now do a brute force cleanup.
4140 // Delete all user events and subscription events.
4141 $DB->delete_records_select('event', 'userid = :userid AND subscriptionid IS NOT NULL', ['userid' => $user->id
]);
4143 // Now, delete all calendar subscription from the user.
4144 $DB->delete_records('event_subscriptions', ['userid' => $user->id
]);
4146 // Remove from all cohorts.
4147 $DB->delete_records('cohort_members', array('userid' => $user->id
));
4149 // Remove from all groups.
4150 $DB->delete_records('groups_members', array('userid' => $user->id
));
4152 // Brute force unenrol from all courses.
4153 $DB->delete_records('user_enrolments', array('userid' => $user->id
));
4155 // Purge user preferences.
4156 $DB->delete_records('user_preferences', array('userid' => $user->id
));
4158 // Purge user extra profile info.
4159 $DB->delete_records('user_info_data', array('userid' => $user->id
));
4161 // Purge log of previous password hashes.
4162 $DB->delete_records('user_password_history', array('userid' => $user->id
));
4164 // Last course access not necessary either.
4165 $DB->delete_records('user_lastaccess', array('userid' => $user->id
));
4166 // Remove all user tokens.
4167 $DB->delete_records('external_tokens', array('userid' => $user->id
));
4169 // Unauthorise the user for all services.
4170 $DB->delete_records('external_services_users', array('userid' => $user->id
));
4172 // Remove users private keys.
4173 $DB->delete_records('user_private_key', array('userid' => $user->id
));
4175 // Remove users customised pages.
4176 $DB->delete_records('my_pages', array('userid' => $user->id
, 'private' => 1));
4178 // Remove user's oauth2 refresh tokens, if present.
4179 $DB->delete_records('oauth2_refresh_token', array('userid' => $user->id
));
4181 // Delete user from $SESSION->bulk_users.
4182 if (isset($SESSION->bulk_users
[$user->id
])) {
4183 unset($SESSION->bulk_users
[$user->id
]);
4186 // Force logout - may fail if file based sessions used, sorry.
4187 \core\session\manager
::kill_user_sessions($user->id
);
4189 // Generate username from email address, or a fake email.
4190 $delemail = !empty($user->email
) ?
$user->email
: $user->username
. '.' . $user->id
. '@unknownemail.invalid';
4193 $deltimelength = core_text
::strlen((string) $deltime);
4195 // Max username length is 100 chars. Select up to limit - (length of current time + 1 [period character]) from users email.
4196 $delname = clean_param($delemail, PARAM_USERNAME
);
4197 $delname = core_text
::substr($delname, 0, 100 - ($deltimelength +
1)) . ".{$deltime}";
4199 // Workaround for bulk deletes of users with the same email address.
4200 while ($DB->record_exists('user', array('username' => $delname))) { // No need to use mnethostid here.
4204 // Mark internal user record as "deleted".
4205 $updateuser = new stdClass();
4206 $updateuser->id
= $user->id
;
4207 $updateuser->deleted
= 1;
4208 $updateuser->username
= $delname; // Remember it just in case.
4209 $updateuser->email
= md5($user->username
);// Store hash of username, useful importing/restoring users.
4210 $updateuser->idnumber
= ''; // Clear this field to free it up.
4211 $updateuser->picture
= 0;
4212 $updateuser->timemodified
= $deltime;
4214 // Don't trigger update event, as user is being deleted.
4215 user_update_user($updateuser, false, false);
4217 // Delete all content associated with the user context, but not the context itself.
4218 $usercontext->delete_content();
4220 // Delete any search data.
4221 \core_search\manager
::context_deleted($usercontext);
4223 // Any plugin that needs to cleanup should register this event.
4225 $event = \core\event\user_deleted
::create(
4227 'objectid' => $user->id
,
4228 'relateduserid' => $user->id
,
4229 'context' => $usercontext,
4231 'username' => $user->username
,
4232 'email' => $user->email
,
4233 'idnumber' => $user->idnumber
,
4234 'picture' => $user->picture
,
4235 'mnethostid' => $user->mnethostid
4239 $event->add_record_snapshot('user', $olduser);
4242 // We will update the user's timemodified, as it will be passed to the user_deleted event, which
4243 // should know about this updated property persisted to the user's table.
4244 $user->timemodified
= $updateuser->timemodified
;
4246 // Notify auth plugin - do not block the delete even when plugin fails.
4247 $authplugin = get_auth_plugin($user->auth
);
4248 $authplugin->user_delete($user);
4254 * Retrieve the guest user object.
4256 * @return stdClass A {@link $USER} object
4258 function guest_user() {
4261 if ($newuser = $DB->get_record('user', array('id' => $CFG->siteguest
))) {
4262 $newuser->confirmed
= 1;
4263 $newuser->lang
= get_newuser_language();
4264 $newuser->lastip
= getremoteaddr();
4271 * Authenticates a user against the chosen authentication mechanism
4273 * Given a username and password, this function looks them
4274 * up using the currently selected authentication mechanism,
4275 * and if the authentication is successful, it returns a
4276 * valid $user object from the 'user' table.
4278 * Uses auth_ functions from the currently active auth module
4280 * After authenticate_user_login() returns success, you will need to
4281 * log that the user has logged in, and call complete_user_login() to set
4284 * Note: this function works only with non-mnet accounts!
4286 * @param string $username User's username (or also email if $CFG->authloginviaemail enabled)
4287 * @param string $password User's password
4288 * @param bool $ignorelockout useful when guessing is prevented by other mechanism such as captcha or SSO
4289 * @param int $failurereason login failure reason, can be used in renderers (it may disclose if account exists)
4290 * @param string|bool $logintoken If this is set to a string it is validated against the login token for the session.
4291 * @param string|bool $loginrecaptcha If this is set to a string it is validated against Google reCaptcha.
4292 * @return stdClass|false A {@link $USER} object or false if error
4294 function authenticate_user_login(
4297 $ignorelockout = false,
4298 &$failurereason = null,
4299 $logintoken = false,
4300 string|
bool $loginrecaptcha = false,
4302 global $CFG, $DB, $PAGE, $SESSION;
4303 require_once("$CFG->libdir/authlib.php");
4305 if ($user = get_complete_user_data('username', $username, $CFG->mnet_localhost_id
)) {
4306 // we have found the user
4308 } else if (!empty($CFG->authloginviaemail
)) {
4309 if ($email = clean_param($username, PARAM_EMAIL
)) {
4310 $select = "mnethostid = :mnethostid AND LOWER(email) = LOWER(:email) AND deleted = 0";
4311 $params = array('mnethostid' => $CFG->mnet_localhost_id
, 'email' => $email);
4312 $users = $DB->get_records_select('user', $select, $params, 'id', 'id', 0, 2);
4313 if (count($users) === 1) {
4314 // Use email for login only if unique.
4315 $user = reset($users);
4316 $user = get_complete_user_data('id', $user->id
);
4317 $username = $user->username
;
4323 // Make sure this request came from the login form.
4324 if (!\core\session\manager
::validate_login_token($logintoken)) {
4325 $failurereason = AUTH_LOGIN_FAILED
;
4327 // Trigger login failed event (specifying the ID of the found user, if available).
4328 \core\event\user_login_failed
::create([
4329 'userid' => ($user->id ??
0),
4331 'username' => $username,
4332 'reason' => $failurereason,
4336 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Invalid Login Token: $username ".$_SERVER['HTTP_USER_AGENT']);
4341 if (login_captcha_enabled() && !validate_login_captcha($loginrecaptcha)) {
4342 $failurereason = AUTH_LOGIN_FAILED_RECAPTCHA
;
4343 // Trigger login failed event (specifying the ID of the found user, if available).
4344 \core\event\user_login_failed
::create([
4345 'userid' => ($user->id ??
0),
4347 'username' => $username,
4348 'reason' => $failurereason,
4354 $authsenabled = get_enabled_auth_plugins();
4357 // Use manual if auth not set.
4358 $auth = empty($user->auth
) ?
'manual' : $user->auth
;
4360 if (in_array($user->auth
, $authsenabled)) {
4361 $authplugin = get_auth_plugin($user->auth
);
4362 $authplugin->pre_user_login_hook($user);
4365 if (!empty($user->suspended
)) {
4366 $failurereason = AUTH_LOGIN_SUSPENDED
;
4368 // Trigger login failed event.
4369 $event = \core\event\user_login_failed
::create(array('userid' => $user->id
,
4370 'other' => array('username' => $username, 'reason' => $failurereason)));
4372 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Suspended Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4375 if ($auth=='nologin' or !is_enabled_auth($auth)) {
4376 // Legacy way to suspend user.
4377 $failurereason = AUTH_LOGIN_SUSPENDED
;
4379 // Trigger login failed event.
4380 $event = \core\event\user_login_failed
::create(array('userid' => $user->id
,
4381 'other' => array('username' => $username, 'reason' => $failurereason)));
4383 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Disabled Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4386 $auths = array($auth);
4389 // Check if there's a deleted record (cheaply), this should not happen because we mangle usernames in delete_user().
4390 if ($DB->get_field('user', 'id', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id
, 'deleted' => 1))) {
4391 $failurereason = AUTH_LOGIN_NOUSER
;
4393 // Trigger login failed event.
4394 $event = \core\event\user_login_failed
::create(array('other' => array('username' => $username,
4395 'reason' => $failurereason)));
4397 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Deleted Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4401 // User does not exist.
4402 $auths = $authsenabled;
4403 $user = new stdClass();
4407 if ($ignorelockout) {
4408 // Some other mechanism protects against brute force password guessing, for example login form might include reCAPTCHA
4409 // or this function is called from a SSO script.
4410 } else if ($user->id
) {
4411 // Verify login lockout after other ways that may prevent user login.
4412 if (login_is_lockedout($user)) {
4413 $failurereason = AUTH_LOGIN_LOCKOUT
;
4415 // Trigger login failed event.
4416 $event = \core\event\user_login_failed
::create(array('userid' => $user->id
,
4417 'other' => array('username' => $username, 'reason' => $failurereason)));
4420 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Login lockout: $username ".$_SERVER['HTTP_USER_AGENT']);
4421 $SESSION->loginerrormsg
= get_string('accountlocked', 'admin');
4426 // We can not lockout non-existing accounts.
4429 foreach ($auths as $auth) {
4430 $authplugin = get_auth_plugin($auth);
4432 // On auth fail fall through to the next plugin.
4433 if (!$authplugin->user_login($username, $password)) {
4437 // Before performing login actions, check if user still passes password policy, if admin setting is enabled.
4438 if (!empty($CFG->passwordpolicycheckonlogin
)) {
4440 $passed = check_password_policy($password, $errmsg, $user);
4442 // First trigger event for failure.
4443 $failedevent = \core\event\user_password_policy_failed
::create_from_user($user);
4444 $failedevent->trigger();
4446 // If able to change password, set flag and move on.
4447 if ($authplugin->can_change_password()) {
4448 // Check if we are on internal change password page, or service is external, don't show notification.
4449 $internalchangeurl = new moodle_url('/login/change_password.php');
4450 if (!($PAGE->has_set_url() && $internalchangeurl->compare($PAGE->url
)) && $authplugin->is_internal()) {
4451 \core\notification
::error(get_string('passwordpolicynomatch', '', $errmsg));
4453 set_user_preference('auth_forcepasswordchange', 1, $user);
4454 } else if ($authplugin->can_reset_password()) {
4455 // Else force a reset if possible.
4456 \core\notification
::error(get_string('forcepasswordresetnotice', '', $errmsg));
4457 redirect(new moodle_url('/login/forgot_password.php'));
4459 $notifymsg = get_string('forcepasswordresetfailurenotice', '', $errmsg);
4460 // If support page is set, add link for help.
4461 if (!empty($CFG->supportpage
)) {
4462 $link = \html_writer
::link($CFG->supportpage
, $CFG->supportpage
);
4463 $link = \html_writer
::tag('p', $link);
4464 $notifymsg .= $link;
4467 // If no change or reset is possible, add a notification for user.
4468 \core\notification
::error($notifymsg);
4473 // Successful authentication.
4475 // User already exists in database.
4476 if (empty($user->auth
)) {
4477 // For some reason auth isn't set yet.
4478 $DB->set_field('user', 'auth', $auth, array('id' => $user->id
));
4479 $user->auth
= $auth;
4482 // If the existing hash is using an out-of-date algorithm (or the legacy md5 algorithm), then we should update to
4483 // the current hash algorithm while we have access to the user's password.
4484 update_internal_user_password($user, $password);
4486 if ($authplugin->is_synchronised_with_external()) {
4487 // Update user record from external DB.
4488 $user = update_user_record_by_id($user->id
);
4491 // The user is authenticated but user creation may be disabled.
4492 if (!empty($CFG->authpreventaccountcreation
)) {
4493 $failurereason = AUTH_LOGIN_UNAUTHORISED
;
4495 // Trigger login failed event.
4496 $event = \core\event\user_login_failed
::create(array('other' => array('username' => $username,
4497 'reason' => $failurereason)));
4500 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Unknown user, can not create new accounts: $username ".
4501 $_SERVER['HTTP_USER_AGENT']);
4504 $user = create_user_record($username, $password, $auth);
4508 $authplugin->sync_roles($user);
4510 foreach ($authsenabled as $hau) {
4511 $hauth = get_auth_plugin($hau);
4512 $hauth->user_authenticated_hook($user, $username, $password);
4515 if (empty($user->id
)) {
4516 $failurereason = AUTH_LOGIN_NOUSER
;
4517 // Trigger login failed event.
4518 $event = \core\event\user_login_failed
::create(array('other' => array('username' => $username,
4519 'reason' => $failurereason)));
4524 if (!empty($user->suspended
)) {
4525 // Just in case some auth plugin suspended account.
4526 $failurereason = AUTH_LOGIN_SUSPENDED
;
4527 // Trigger login failed event.
4528 $event = \core\event\user_login_failed
::create(array('userid' => $user->id
,
4529 'other' => array('username' => $username, 'reason' => $failurereason)));
4531 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Suspended Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4535 login_attempt_valid($user);
4536 $failurereason = AUTH_LOGIN_OK
;
4540 // Failed if all the plugins have failed.
4541 if (debugging('', DEBUG_ALL
)) {
4542 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Failed Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4546 login_attempt_failed($user);
4547 $failurereason = AUTH_LOGIN_FAILED
;
4548 // Trigger login failed event.
4549 $event = \core\event\user_login_failed
::create(array('userid' => $user->id
,
4550 'other' => array('username' => $username, 'reason' => $failurereason)));
4553 $failurereason = AUTH_LOGIN_NOUSER
;
4554 // Trigger login failed event.
4555 $event = \core\event\user_login_failed
::create(array('other' => array('username' => $username,
4556 'reason' => $failurereason)));
4564 * Call to complete the user login process after authenticate_user_login()
4565 * has succeeded. It will setup the $USER variable and other required bits
4569 * - It will NOT log anything -- up to the caller to decide what to log.
4570 * - this function does not set any cookies any more!
4572 * @param stdClass $user
4573 * @param array $extrauserinfo
4574 * @return stdClass A {@link $USER} object - BC only, do not use
4576 function complete_user_login($user, array $extrauserinfo = []) {
4577 global $CFG, $DB, $USER, $SESSION;
4579 \core\session\manager
::login_user($user);
4581 // Reload preferences from DB.
4582 unset($USER->preference
);
4583 check_user_preferences_loaded($USER);
4585 // Update login times.
4586 update_user_login_times();
4588 // Extra session prefs init.
4589 set_login_session_preferences();
4591 // Trigger login event.
4592 $event = \core\event\user_loggedin
::create(
4594 'userid' => $USER->id
,
4595 'objectid' => $USER->id
,
4597 'username' => $USER->username
,
4598 'extrauserinfo' => $extrauserinfo
4604 // Check if the user is using a new browser or session (a new MoodleSession cookie is set in that case).
4605 // If the user is accessing from the same IP, ignore everything (most of the time will be a new session in the same browser).
4606 // Skip Web Service requests, CLI scripts, AJAX scripts, and request from the mobile app itself.
4607 $loginip = getremoteaddr();
4608 $isnewip = isset($SESSION->userpreviousip
) && $SESSION->userpreviousip
!= $loginip;
4609 $isvalidenv = (!WS_SERVER
&& !CLI_SCRIPT
&& !NO_MOODLE_COOKIES
) || PHPUNIT_TEST
;
4611 if (!empty($SESSION->isnewsessioncookie
) && $isnewip && $isvalidenv && !\core_useragent
::is_moodle_app()) {
4613 $logintime = time();
4614 $ismoodleapp = false;
4615 $useragent = \core_useragent
::get_user_agent_string();
4617 // Schedule adhoc task to sent a login notification to the user.
4618 $task = new \core\task\
send_login_notifications();
4619 $task->set_userid($USER->id
);
4620 $task->set_custom_data(compact('ismoodleapp', 'useragent', 'loginip', 'logintime'));
4621 $task->set_component('core');
4622 \core\task\manager
::queue_adhoc_task($task);
4625 // Queue migrating the messaging data, if we need to.
4626 if (!get_user_preferences('core_message_migrate_data', false, $USER->id
)) {
4627 // Check if there are any legacy messages to migrate.
4628 if (\core_message\helper
::legacy_messages_exist($USER->id
)) {
4629 \core_message\task\migrate_message_data
::queue_task($USER->id
);
4631 set_user_preference('core_message_migrate_data', true, $USER->id
);
4635 if (isguestuser()) {
4636 // No need to continue when user is THE guest.
4641 // We can redirect to password change URL only in browser.
4645 // Select password change url.
4646 $userauth = get_auth_plugin($USER->auth
);
4648 // Check whether the user should be changing password.
4649 if (get_user_preferences('auth_forcepasswordchange', false)) {
4650 if ($userauth->can_change_password()) {
4651 if ($changeurl = $userauth->change_password_url()) {
4652 redirect($changeurl);
4654 require_once($CFG->dirroot
. '/login/lib.php');
4655 $SESSION->wantsurl
= core_login_get_return_url();
4656 redirect($CFG->wwwroot
.'/login/change_password.php');
4659 throw new \
moodle_exception('nopasswordchangeforced', 'auth');
4666 * Check a password hash to see if it was hashed using the legacy hash algorithm (bcrypt).
4668 * @param string $password String to check.
4669 * @return bool True if the $password matches the format of a bcrypt hash.
4671 function password_is_legacy_hash(#[\SensitiveParameter] string $password): bool {
4672 return (bool) preg_match('/^\$2y\$[\d]{2}\$[A-Za-z0-9\.\/]{53}$/', $password);
4676 * Calculate the Shannon entropy of a string.
4678 * @param string $pepper The pepper to calculate the entropy of.
4679 * @return float The Shannon entropy of the string.
4681 function calculate_entropy(#[\SensitiveParameter] string $pepper): float {
4682 // Initialize entropy.
4685 // Calculate the length of the string.
4686 $size = strlen($pepper);
4688 // For each unique character in the string.
4689 foreach (count_chars($pepper, 1) as $v) {
4690 // Calculate the probability of the character.
4693 // Add the character's contribution to the total entropy.
4694 // This uses the formula for the entropy of a discrete random variable.
4695 $h -= $p * log($p) / log(2);
4698 // Instead of returning the average entropy per symbol (Shannon entropy),
4699 // we multiply by the length of the string to get total entropy.
4704 * Get the available password peppers.
4705 * The latest pepper is checked for minimum entropy as part of this function.
4706 * We only calculate the entropy of the most recent pepper,
4707 * because passwords are always updated to the latest pepper,
4708 * and in the past we may have enforced a lower minimum entropy.
4709 * Also, we allow the latest pepper to be empty, to allow admins to migrate off peppers.
4711 * @return array The password peppers.
4712 * @throws coding_exception If the entropy of the password pepper is less than the recommended minimum.
4714 function get_password_peppers(): array {
4717 // Get all available peppers.
4718 if (isset($CFG->passwordpeppers
) && is_array($CFG->passwordpeppers
)) {
4719 // Sort the array in descending order of keys (numerical).
4720 $peppers = $CFG->passwordpeppers
;
4721 krsort($peppers, SORT_NUMERIC
);
4723 $peppers = []; // Set an empty array if no peppers are found.
4726 // Check if the entropy of the most recent pepper is less than the minimum.
4727 // Also, we allow the most recent pepper to be empty, to allow admins to migrate off peppers.
4728 $lastpepper = reset($peppers);
4729 if (!empty($peppers) && $lastpepper !== '' && calculate_entropy($lastpepper) < PEPPER_ENTROPY
) {
4730 throw new coding_exception(
4731 'password pepper below minimum',
4732 'The entropy of the password pepper is less than the recommended minimum.');
4738 * Compare password against hash stored in user object to determine if it is valid.
4740 * If necessary it also updates the stored hash to the current format.
4742 * @param stdClass $user (Password property may be updated).
4743 * @param string $password Plain text password.
4744 * @return bool True if password is valid.
4746 function validate_internal_user_password(stdClass
$user, #[\SensitiveParameter] string $password): bool {
4748 if (exceeds_password_length($password)) {
4749 // Password cannot be more than MAX_PASSWORD_CHARACTERS characters.
4753 if ($user->password
=== AUTH_PASSWORD_NOT_CACHED
) {
4754 // Internal password is not used at all, it can not validate.
4758 $peppers = get_password_peppers(); // Get the array of available peppers.
4759 $islegacy = password_is_legacy_hash($user->password
); // Check if the password is a legacy bcrypt hash.
4761 // If the password is a legacy hash, no peppers were used, so verify and update directly.
4762 if ($islegacy && password_verify($password, $user->password
)) {
4763 update_internal_user_password($user, $password);
4767 // If the password is not a legacy hash, iterate through the peppers.
4768 $latestpepper = reset($peppers);
4769 // Add an empty pepper to the beginning of the array. To make it easier to check if the password matches without any pepper.
4770 $peppers = [-1 => ''] +
$peppers;
4771 foreach ($peppers as $pepper) {
4772 $pepperedpassword = $password . $pepper;
4774 // If the peppered password is correct, update (if necessary) and return true.
4775 if (password_verify($pepperedpassword, $user->password
)) {
4776 // If the pepper used is not the latest one, update the password.
4777 if ($pepper !== $latestpepper) {
4778 update_internal_user_password($user, $password);
4784 // If no peppered password was correct, the password is wrong.
4789 * Calculate hash for a plain text password.
4791 * @param string $password Plain text password to be hashed.
4792 * @param bool $fasthash If true, use a low number of rounds when generating the hash
4793 * This is faster to generate but makes the hash less secure.
4794 * It is used when lots of hashes need to be generated quickly.
4795 * @param int $pepperlength Lenght of the peppers
4796 * @return string The hashed password.
4798 * @throws moodle_exception If a problem occurs while generating the hash.
4800 function hash_internal_user_password(#[\SensitiveParameter] string $password, $fasthash = false, $pepperlength = 0): string {
4801 if (exceeds_password_length($password, $pepperlength)) {
4802 // Password cannot be more than MAX_PASSWORD_CHARACTERS.
4803 throw new \
moodle_exception(get_string("passwordexceeded", 'error', MAX_PASSWORD_CHARACTERS
));
4806 // Set the cost factor to 5000 for fast hashing, otherwise use default cost.
4807 $rounds = $fasthash ?
5000 : 10000;
4809 // First generate a cryptographically suitable salt.
4810 $randombytes = random_bytes(16);
4811 $salt = substr(strtr(base64_encode($randombytes), '+', '.'), 0, 16);
4813 // Now construct the password string with the salt and number of rounds.
4814 // The password string is in the format $algorithm$rounds$salt$hash. ($6 is the SHA512 algorithm).
4815 $generatedhash = crypt($password, implode('$', [
4817 // The SHA512 Algorithm
4824 if ($generatedhash === false ||
$generatedhash === null) {
4825 throw new moodle_exception('Failed to generate password hash.');
4828 return $generatedhash;
4832 * Update password hash in user object (if necessary).
4834 * The password is updated if:
4835 * 1. The password has changed (the hash of $user->password is different
4836 * to the hash of $password).
4837 * 2. The existing hash is using an out-of-date algorithm (or the legacy
4840 * The password is peppered with the latest pepper before hashing,
4841 * if peppers are available.
4842 * Updating the password will modify the $user object and the database
4843 * record to use the current hashing algorithm.
4844 * It will remove Web Services user tokens too.
4846 * @param stdClass $user User object (password property may be updated).
4847 * @param string $password Plain text password.
4848 * @param bool $fasthash If true, use a low cost factor when generating the hash
4849 * This is much faster to generate but makes the hash
4850 * less secure. It is used when lots of hashes need to
4851 * be generated quickly.
4852 * @return bool Always returns true.
4854 function update_internal_user_password(
4856 #[\SensitiveParameter] string $password,
4857 bool $fasthash = false
4861 // Add the latest password pepper to the password before further processing.
4862 $peppers = get_password_peppers();
4863 if (!empty($peppers)) {
4864 $password = $password . reset($peppers);
4867 // Figure out what the hashed password should be.
4868 if (!isset($user->auth
)) {
4869 debugging('User record in update_internal_user_password() must include field auth',
4871 $user->auth
= $DB->get_field('user', 'auth', array('id' => $user->id
));
4873 $authplugin = get_auth_plugin($user->auth
);
4874 if ($authplugin->prevent_local_passwords()) {
4875 $hashedpassword = AUTH_PASSWORD_NOT_CACHED
;
4877 $hashedpassword = hash_internal_user_password($password, $fasthash);
4880 $algorithmchanged = false;
4882 if ($hashedpassword === AUTH_PASSWORD_NOT_CACHED
) {
4883 // Password is not cached, update it if not set to AUTH_PASSWORD_NOT_CACHED.
4884 $passwordchanged = ($user->password
!== $hashedpassword);
4886 } else if (isset($user->password
)) {
4887 // If verification fails then it means the password has changed.
4888 $passwordchanged = !password_verify($password, $user->password
);
4889 $algorithmchanged = password_is_legacy_hash($user->password
);
4891 // While creating new user, password in unset in $user object, to avoid
4892 // saving it with user_create()
4893 $passwordchanged = true;
4896 if ($passwordchanged ||
$algorithmchanged) {
4897 $DB->set_field('user', 'password', $hashedpassword, array('id' => $user->id
));
4898 $user->password
= $hashedpassword;
4901 $user = $DB->get_record('user', array('id' => $user->id
));
4902 \core\event\user_password_updated
::create_from_user($user)->trigger();
4904 // Remove WS user tokens.
4905 if (!empty($CFG->passwordchangetokendeletion
)) {
4906 require_once($CFG->dirroot
.'/webservice/lib.php');
4907 webservice
::delete_user_ws_tokens($user->id
);
4915 * Get a complete user record, which includes all the info in the user record.
4917 * Intended for setting as $USER session variable
4919 * @param string $field The user field to be checked for a given value.
4920 * @param string $value The value to match for $field.
4921 * @param int $mnethostid
4922 * @param bool $throwexception If true, it will throw an exception when there's no record found or when there are multiple records
4923 * found. Otherwise, it will just return false.
4924 * @return mixed False, or A {@link $USER} object.
4926 function get_complete_user_data($field, $value, $mnethostid = null, $throwexception = false) {
4929 if (!$field ||
!$value) {
4933 // Change the field to lowercase.
4934 $field = core_text
::strtolower($field);
4936 // List of case insensitive fields.
4937 $caseinsensitivefields = ['email'];
4939 // Username input is forced to lowercase and should be case sensitive.
4940 if ($field == 'username') {
4941 $value = core_text
::strtolower($value);
4944 // Build the WHERE clause for an SQL query.
4945 $params = array('fieldval' => $value);
4947 // Do a case-insensitive query, if necessary. These are generally very expensive. The performance can be improved on some DBs
4948 // such as MySQL by pre-filtering users with accent-insensitive subselect.
4949 if (in_array($field, $caseinsensitivefields)) {
4950 $fieldselect = $DB->sql_equal($field, ':fieldval', false);
4951 $idsubselect = $DB->sql_equal($field, ':fieldval2', false, false);
4952 $params['fieldval2'] = $value;
4954 $fieldselect = "$field = :fieldval";
4957 $constraints = "$fieldselect AND deleted <> 1";
4959 // If we are loading user data based on anything other than id,
4960 // we must also restrict our search based on mnet host.
4961 if ($field != 'id') {
4962 if (empty($mnethostid)) {
4963 // If empty, we restrict to local users.
4964 $mnethostid = $CFG->mnet_localhost_id
;
4967 if (!empty($mnethostid)) {
4968 $params['mnethostid'] = $mnethostid;
4969 $constraints .= " AND mnethostid = :mnethostid";
4973 $constraints .= " AND id IN (SELECT id FROM {user} WHERE {$idsubselect})";
4976 // Get all the basic user data.
4978 // Make sure that there's only a single record that matches our query.
4979 // For example, when fetching by email, multiple records might match the query as there's no guarantee that email addresses
4980 // are unique. Therefore we can't reliably tell whether the user profile data that we're fetching is the correct one.
4981 $user = $DB->get_record_select('user', $constraints, $params, '*', MUST_EXIST
);
4982 } catch (dml_exception
$exception) {
4983 if ($throwexception) {
4986 // Return false when no records or multiple records were found.
4991 // Get various settings and preferences.
4993 // Preload preference cache.
4994 check_user_preferences_loaded($user);
4996 // Load course enrolment related stuff.
4997 $user->lastcourseaccess
= array(); // During last session.
4998 $user->currentcourseaccess
= array(); // During current session.
4999 if ($lastaccesses = $DB->get_records('user_lastaccess', array('userid' => $user->id
))) {
5000 foreach ($lastaccesses as $lastaccess) {
5001 $user->lastcourseaccess
[$lastaccess->courseid
] = $lastaccess->timeaccess
;
5005 // Add cohort theme.
5006 if (!empty($CFG->allowcohortthemes
)) {
5007 require_once($CFG->dirroot
. '/cohort/lib.php');
5008 if ($cohorttheme = cohort_get_user_cohort_theme($user->id
)) {
5009 $user->cohorttheme
= $cohorttheme;
5013 // Add the custom profile fields to the user record.
5014 $user->profile
= array();
5015 if (!isguestuser($user)) {
5016 require_once($CFG->dirroot
.'/user/profile/lib.php');
5017 profile_load_custom_fields($user);
5020 // Rewrite some variables if necessary.
5021 if (!empty($user->description
)) {
5022 // No need to cart all of it around.
5023 $user->description
= true;
5025 if (isguestuser($user)) {
5026 // Guest language always same as site.
5027 $user->lang
= get_newuser_language();
5028 // Name always in current language.
5029 $user->firstname
= get_string('guestuser');
5030 $user->lastname
= ' ';
5037 * Validate a password against the configured password policy
5039 * @param string $password the password to be checked against the password policy
5040 * @param string $errmsg the error message to display when the password doesn't comply with the policy.
5041 * @param stdClass $user the user object to perform password validation against. Defaults to null if not provided.
5043 * @return bool true if the password is valid according to the policy. false otherwise.
5045 function check_password_policy($password, &$errmsg, $user = null) {
5048 if (!empty($CFG->passwordpolicy
)) {
5050 if (core_text
::strlen($password) < $CFG->minpasswordlength
) {
5051 $errmsg .= '<div>'. get_string('errorminpasswordlength', 'auth', $CFG->minpasswordlength
) .'</div>';
5053 if (preg_match_all('/[[:digit:]]/u', $password, $matches) < $CFG->minpassworddigits
) {
5054 $errmsg .= '<div>'. get_string('errorminpassworddigits', 'auth', $CFG->minpassworddigits
) .'</div>';
5056 if (preg_match_all('/[[:lower:]]/u', $password, $matches) < $CFG->minpasswordlower
) {
5057 $errmsg .= '<div>'. get_string('errorminpasswordlower', 'auth', $CFG->minpasswordlower
) .'</div>';
5059 if (preg_match_all('/[[:upper:]]/u', $password, $matches) < $CFG->minpasswordupper
) {
5060 $errmsg .= '<div>'. get_string('errorminpasswordupper', 'auth', $CFG->minpasswordupper
) .'</div>';
5062 if (preg_match_all('/[^[:upper:][:lower:][:digit:]]/u', $password, $matches) < $CFG->minpasswordnonalphanum
) {
5063 $errmsg .= '<div>'. get_string('errorminpasswordnonalphanum', 'auth', $CFG->minpasswordnonalphanum
) .'</div>';
5065 if (!check_consecutive_identical_characters($password, $CFG->maxconsecutiveidentchars
)) {
5066 $errmsg .= '<div>'. get_string('errormaxconsecutiveidentchars', 'auth', $CFG->maxconsecutiveidentchars
) .'</div>';
5069 // Fire any additional password policy functions from plugins.
5070 // Plugin functions should output an error message string or empty string for success.
5071 $pluginsfunction = get_plugins_with_function('check_password_policy');
5072 foreach ($pluginsfunction as $plugintype => $plugins) {
5073 foreach ($plugins as $pluginfunction) {
5074 $pluginerr = $pluginfunction($password, $user);
5076 $errmsg .= '<div>'. $pluginerr .'</div>';
5082 if ($errmsg == '') {
5091 * When logging in, this function is run to set certain preferences for the current SESSION.
5093 function set_login_session_preferences() {
5096 $SESSION->justloggedin
= true;
5098 unset($SESSION->lang
);
5099 unset($SESSION->forcelang
);
5100 unset($SESSION->load_navigation_admin
);
5105 * Delete a course, including all related data from the database, and any associated files.
5107 * @param mixed $courseorid The id of the course or course object to delete.
5108 * @param bool $showfeedback Whether to display notifications of each action the function performs.
5109 * @return bool true if all the removals succeeded. false if there were any failures. If this
5110 * method returns false, some of the removals will probably have succeeded, and others
5111 * failed, but you have no way of knowing which.
5113 function delete_course($courseorid, $showfeedback = true) {
5116 if (is_object($courseorid)) {
5117 $courseid = $courseorid->id
;
5118 $course = $courseorid;
5120 $courseid = $courseorid;
5121 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
5125 $context = context_course
::instance($courseid);
5127 // Frontpage course can not be deleted!!
5128 if ($courseid == SITEID
) {
5132 // Allow plugins to use this course before we completely delete it.
5133 if ($pluginsfunction = get_plugins_with_function('pre_course_delete')) {
5134 foreach ($pluginsfunction as $plugintype => $plugins) {
5135 foreach ($plugins as $pluginfunction) {
5136 $pluginfunction($course);
5141 // Tell the search manager we are about to delete a course. This prevents us sending updates
5142 // for each individual context being deleted.
5143 \core_search\manager
::course_deleting_start($courseid);
5145 $handler = core_course\customfield\course_handler
::create();
5146 $handler->delete_instance($courseid);
5148 // Make the course completely empty.
5149 remove_course_contents($courseid, $showfeedback);
5151 // Delete the course and related context instance.
5152 context_helper
::delete_instance(CONTEXT_COURSE
, $courseid);
5154 // Communication provider delete associated information.
5155 $communication = \core_communication\api
::load_by_instance(
5157 'coursecommunication',
5161 // Update communication room membership of enrolled users.
5162 require_once($CFG->libdir
. '/enrollib.php');
5163 $courseusers = enrol_get_course_users($courseid);
5164 $enrolledusers = [];
5166 foreach ($courseusers as $user) {
5167 $enrolledusers[] = $user->id
;
5170 $communication->remove_members_from_room($enrolledusers);
5172 $communication->delete_room();
5174 $DB->delete_records("course", array("id" => $courseid));
5175 $DB->delete_records("course_format_options", array("courseid" => $courseid));
5177 // Reset all course related caches here.
5178 core_courseformat\base
::reset_course_cache($courseid);
5180 // Tell search that we have deleted the course so it can delete course data from the index.
5181 \core_search\manager
::course_deleting_finish($courseid);
5183 // Trigger a course deleted event.
5184 $event = \core\event\course_deleted
::create(array(
5185 'objectid' => $course->id
,
5186 'context' => $context,
5188 'shortname' => $course->shortname
,
5189 'fullname' => $course->fullname
,
5190 'idnumber' => $course->idnumber
5193 $event->add_record_snapshot('course', $course);
5200 * Clear a course out completely, deleting all content but don't delete the course itself.
5202 * This function does not verify any permissions.
5204 * Please note this function also deletes all user enrolments,
5205 * enrolment instances and role assignments by default.
5208 * - 'keep_roles_and_enrolments' - false by default
5209 * - 'keep_groups_and_groupings' - false by default
5211 * @param int $courseid The id of the course that is being deleted
5212 * @param bool $showfeedback Whether to display notifications of each action the function performs.
5213 * @param array $options extra options
5214 * @return bool true if all the removals succeeded. false if there were any failures. If this
5215 * method returns false, some of the removals will probably have succeeded, and others
5216 * failed, but you have no way of knowing which.
5218 function remove_course_contents($courseid, $showfeedback = true, array $options = null) {
5219 global $CFG, $DB, $OUTPUT;
5221 require_once($CFG->libdir
.'/badgeslib.php');
5222 require_once($CFG->libdir
.'/completionlib.php');
5223 require_once($CFG->libdir
.'/questionlib.php');
5224 require_once($CFG->libdir
.'/gradelib.php');
5225 require_once($CFG->dirroot
.'/group/lib.php');
5226 require_once($CFG->dirroot
.'/comment/lib.php');
5227 require_once($CFG->dirroot
.'/rating/lib.php');
5228 require_once($CFG->dirroot
.'/notes/lib.php');
5230 // Handle course badges.
5231 badges_handle_course_deletion($courseid);
5233 // NOTE: these concatenated strings are suboptimal, but it is just extra info...
5234 $strdeleted = get_string('deleted').' - ';
5236 // Some crazy wishlist of stuff we should skip during purging of course content.
5237 $options = (array)$options;
5239 $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST
);
5240 $coursecontext = context_course
::instance($courseid);
5241 $fs = get_file_storage();
5243 // Delete course completion information, this has to be done before grades and enrols.
5244 $cc = new completion_info($course);
5245 $cc->clear_criteria();
5246 if ($showfeedback) {
5247 echo $OUTPUT->notification($strdeleted.get_string('completion', 'completion'), 'notifysuccess');
5250 // Remove all data from gradebook - this needs to be done before course modules
5251 // because while deleting this information, the system may need to reference
5252 // the course modules that own the grades.
5253 remove_course_grades($courseid, $showfeedback);
5254 remove_grade_letters($coursecontext, $showfeedback);
5256 // Delete course blocks in any all child contexts,
5257 // they may depend on modules so delete them first.
5258 $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2.
5259 foreach ($childcontexts as $childcontext) {
5260 blocks_delete_all_for_context($childcontext->id
);
5262 unset($childcontexts);
5263 blocks_delete_all_for_context($coursecontext->id
);
5264 if ($showfeedback) {
5265 echo $OUTPUT->notification($strdeleted.get_string('type_block_plural', 'plugin'), 'notifysuccess');
5268 $DB->set_field('course_modules', 'deletioninprogress', '1', ['course' => $courseid]);
5269 rebuild_course_cache($courseid, true);
5271 // Get the list of all modules that are properly installed.
5272 $allmodules = $DB->get_records_menu('modules', array(), '', 'name, id');
5274 // Delete every instance of every module,
5275 // this has to be done before deleting of course level stuff.
5276 $locations = core_component
::get_plugin_list('mod');
5277 foreach ($locations as $modname => $moddir) {
5278 if ($modname === 'NEWMODULE') {
5281 if (array_key_exists($modname, $allmodules)) {
5282 $sql = "SELECT cm.*, m.id AS modinstance, m.name, '$modname' AS modname
5283 FROM {".$modname."} m
5284 LEFT JOIN {course_modules} cm ON cm.instance = m.id AND cm.module = :moduleid
5285 WHERE m.course = :courseid";
5286 $instances = $DB->get_records_sql($sql, array('courseid' => $course->id
,
5287 'modulename' => $modname, 'moduleid' => $allmodules[$modname]));
5289 include_once("$moddir/lib.php"); // Shows php warning only if plugin defective.
5290 $moddelete = $modname .'_delete_instance'; // Delete everything connected to an instance.
5293 foreach ($instances as $cm) {
5295 // Delete activity context questions and question categories.
5296 question_delete_activity($cm);
5297 // Notify the competency subsystem.
5298 \core_competency\api
::hook_course_module_deleted($cm);
5300 // Delete all tag instances associated with the instance of this module.
5301 core_tag_tag
::delete_instances("mod_{$modname}", null, context_module
::instance($cm->id
)->id
);
5302 core_tag_tag
::remove_all_item_tags('core', 'course_modules', $cm->id
);
5304 if (function_exists($moddelete)) {
5305 // This purges all module data in related tables, extra user prefs, settings, etc.
5306 $moddelete($cm->modinstance
);
5308 // NOTE: we should not allow installation of modules with missing delete support!
5309 debugging("Defective module '$modname' detected when deleting course contents: missing function $moddelete()!");
5310 $DB->delete_records($modname, array('id' => $cm->modinstance
));
5314 // Delete cm and its context - orphaned contexts are purged in cron in case of any race condition.
5315 context_helper
::delete_instance(CONTEXT_MODULE
, $cm->id
);
5316 $DB->delete_records('course_modules_completion', ['coursemoduleid' => $cm->id
]);
5317 $DB->delete_records('course_modules_viewed', ['coursemoduleid' => $cm->id
]);
5318 $DB->delete_records('course_modules', array('id' => $cm->id
));
5319 rebuild_course_cache($cm->course
, true);
5323 if ($instances and $showfeedback) {
5324 echo $OUTPUT->notification($strdeleted.get_string('pluginname', $modname), 'notifysuccess');
5327 // Ooops, this module is not properly installed, force-delete it in the next block.
5331 // We have tried to delete everything the nice way - now let's force-delete any remaining module data.
5333 // Delete completion defaults.
5334 $DB->delete_records("course_completion_defaults", array("course" => $courseid));
5336 // Remove all data from availability and completion tables that is associated
5337 // with course-modules belonging to this course. Note this is done even if the
5338 // features are not enabled now, in case they were enabled previously.
5339 $DB->delete_records_subquery('course_modules_completion', 'coursemoduleid', 'id',
5340 'SELECT id from {course_modules} WHERE course = ?', [$courseid]);
5341 $DB->delete_records_subquery('course_modules_viewed', 'coursemoduleid', 'id',
5342 'SELECT id from {course_modules} WHERE course = ?', [$courseid]);
5344 // Remove course-module data that has not been removed in modules' _delete_instance callbacks.
5345 $cms = $DB->get_records('course_modules', array('course' => $course->id
));
5346 $allmodulesbyid = array_flip($allmodules);
5347 foreach ($cms as $cm) {
5348 if (array_key_exists($cm->module
, $allmodulesbyid)) {
5350 $DB->delete_records($allmodulesbyid[$cm->module
], array('id' => $cm->instance
));
5351 } catch (Exception
$e) {
5352 // Ignore weird or missing table problems.
5355 context_helper
::delete_instance(CONTEXT_MODULE
, $cm->id
);
5356 $DB->delete_records('course_modules', array('id' => $cm->id
));
5357 rebuild_course_cache($cm->course
, true);
5360 if ($showfeedback) {
5361 echo $OUTPUT->notification($strdeleted.get_string('type_mod_plural', 'plugin'), 'notifysuccess');
5364 // Delete questions and question categories.
5365 question_delete_course($course);
5366 if ($showfeedback) {
5367 echo $OUTPUT->notification($strdeleted.get_string('questions', 'question'), 'notifysuccess');
5370 // Delete content bank contents.
5371 $cb = new \core_contentbank\
contentbank();
5372 $cbdeleted = $cb->delete_contents($coursecontext);
5373 if ($showfeedback && $cbdeleted) {
5374 echo $OUTPUT->notification($strdeleted.get_string('contentbank', 'contentbank'), 'notifysuccess');
5377 // Make sure there are no subcontexts left - all valid blocks and modules should be already gone.
5378 $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2.
5379 foreach ($childcontexts as $childcontext) {
5380 $childcontext->delete();
5382 unset($childcontexts);
5384 // Remove roles and enrolments by default.
5385 if (empty($options['keep_roles_and_enrolments'])) {
5386 // This hack is used in restore when deleting contents of existing course.
5387 // During restore, we should remove only enrolment related data that the user performing the restore has a
5388 // permission to remove.
5389 $userid = $options['userid'] ??
null;
5390 enrol_course_delete($course, $userid);
5391 role_unassign_all(array('contextid' => $coursecontext->id
, 'component' => ''), true);
5392 if ($showfeedback) {
5393 echo $OUTPUT->notification($strdeleted.get_string('type_enrol_plural', 'plugin'), 'notifysuccess');
5397 // Delete any groups, removing members and grouping/course links first.
5398 if (empty($options['keep_groups_and_groupings'])) {
5399 groups_delete_groupings($course->id
, $showfeedback);
5400 groups_delete_groups($course->id
, $showfeedback);
5404 filter_delete_all_for_context($coursecontext->id
);
5406 // Notes, you shall not pass!
5407 note_delete_all($course->id
);
5410 comment
::delete_comments($coursecontext->id
);
5412 // Ratings are history too.
5413 $delopt = new stdclass();
5414 $delopt->contextid
= $coursecontext->id
;
5415 $rm = new rating_manager();
5416 $rm->delete_ratings($delopt);
5418 // Delete course tags.
5419 core_tag_tag
::remove_all_item_tags('core', 'course', $course->id
);
5421 // Give the course format the opportunity to remove its obscure data.
5422 $format = course_get_format($course);
5423 $format->delete_format_data();
5425 // Notify the competency subsystem.
5426 \core_competency\api
::hook_course_deleted($course);
5428 // Delete calendar events.
5429 $DB->delete_records('event', array('courseid' => $course->id
));
5430 $fs->delete_area_files($coursecontext->id
, 'calendar');
5432 // Delete all related records in other core tables that may have a courseid
5433 // This array stores the tables that need to be cleared, as
5434 // table_name => column_name that contains the course id.
5435 $tablestoclear = array(
5436 'backup_courses' => 'courseid', // Scheduled backup stuff.
5437 'user_lastaccess' => 'courseid', // User access info.
5439 foreach ($tablestoclear as $table => $col) {
5440 $DB->delete_records($table, array($col => $course->id
));
5443 // Delete all course backup files.
5444 $fs->delete_area_files($coursecontext->id
, 'backup');
5446 // Cleanup course record - remove links to deleted stuff.
5447 $oldcourse = new stdClass();
5448 $oldcourse->id
= $course->id
;
5449 $oldcourse->summary
= '';
5450 $oldcourse->cacherev
= 0;
5451 $oldcourse->legacyfiles
= 0;
5452 if (!empty($options['keep_groups_and_groupings'])) {
5453 $oldcourse->defaultgroupingid
= 0;
5455 $DB->update_record('course', $oldcourse);
5457 // Delete course sections.
5458 $DB->delete_records('course_sections', array('course' => $course->id
));
5460 // Delete legacy, section and any other course files.
5461 $fs->delete_area_files($coursecontext->id
, 'course'); // Files from summary and section.
5463 // Delete all remaining stuff linked to context such as files, comments, ratings, etc.
5464 if (empty($options['keep_roles_and_enrolments']) and empty($options['keep_groups_and_groupings'])) {
5465 // Easy, do not delete the context itself...
5466 $coursecontext->delete_content();
5469 // We can not drop all context stuff because it would bork enrolments and roles,
5470 // there might be also files used by enrol plugins...
5473 // Delete legacy files - just in case some files are still left there after conversion to new file api,
5474 // also some non-standard unsupported plugins may try to store something there.
5475 fulldelete($CFG->dataroot
.'/'.$course->id
);
5477 // Delete from cache to reduce the cache size especially makes sense in case of bulk course deletion.
5478 course_modinfo
::purge_course_cache($courseid);
5480 // Trigger a course content deleted event.
5481 $event = \core\event\course_content_deleted
::create(array(
5482 'objectid' => $course->id
,
5483 'context' => $coursecontext,
5484 'other' => array('shortname' => $course->shortname
,
5485 'fullname' => $course->fullname
,
5486 'options' => $options) // Passing this for legacy reasons.
5488 $event->add_record_snapshot('course', $course);
5495 * Change dates in module - used from course reset.
5497 * @param string $modname forum, assignment, etc
5498 * @param array $fields array of date fields from mod table
5499 * @param int $timeshift time difference
5500 * @param int $courseid
5501 * @param int $modid (Optional) passed if specific mod instance in course needs to be updated.
5502 * @return bool success
5504 function shift_course_mod_dates($modname, $fields, $timeshift, $courseid, $modid = 0) {
5506 include_once($CFG->dirroot
.'/mod/'.$modname.'/lib.php');
5509 $params = array($timeshift, $courseid);
5510 foreach ($fields as $field) {
5511 $updatesql = "UPDATE {".$modname."}
5512 SET $field = $field + ?
5513 WHERE course=? AND $field<>0";
5515 $updatesql .= ' AND id=?';
5518 $return = $DB->execute($updatesql, $params) && $return;
5525 * This function will empty a course of user data.
5526 * It will retain the activities and the structure of the course.
5528 * @param object $data an object containing all the settings including courseid (without magic quotes)
5529 * @return array status array of array component, item, error
5531 function reset_course_userdata($data) {
5533 require_once($CFG->libdir
.'/gradelib.php');
5534 require_once($CFG->libdir
.'/completionlib.php');
5535 require_once($CFG->dirroot
.'/completion/criteria/completion_criteria_date.php');
5536 require_once($CFG->dirroot
.'/group/lib.php');
5538 $data->courseid
= $data->id
;
5539 $context = context_course
::instance($data->courseid
);
5541 $eventparams = array(
5542 'context' => $context,
5543 'courseid' => $data->id
,
5545 'reset_options' => (array) $data
5548 $event = \core\event\course_reset_started
::create($eventparams);
5551 // Calculate the time shift of dates.
5552 if (!empty($data->reset_start_date
)) {
5553 // Time part of course startdate should be zero.
5554 $data->timeshift
= $data->reset_start_date
- usergetmidnight($data->reset_start_date_old
);
5556 $data->timeshift
= 0;
5559 // Result array: component, item, error.
5562 // Start the resetting.
5563 $componentstr = get_string('general');
5565 // Move the course start time.
5566 if (!empty($data->reset_start_date
) and $data->timeshift
) {
5567 // Change course start data.
5568 $DB->set_field('course', 'startdate', $data->reset_start_date
, array('id' => $data->courseid
));
5569 // Update all course and group events - do not move activity events.
5570 $updatesql = "UPDATE {event}
5571 SET timestart = timestart + ?
5572 WHERE courseid=? AND instance=0";
5573 $DB->execute($updatesql, array($data->timeshift
, $data->courseid
));
5575 // Update any date activity restrictions.
5576 if ($CFG->enableavailability
) {
5577 \availability_date\condition
::update_all_dates($data->courseid
, $data->timeshift
);
5580 // Update completion expected dates.
5581 if ($CFG->enablecompletion
) {
5582 $modinfo = get_fast_modinfo($data->courseid
);
5584 foreach ($modinfo->get_cms() as $cm) {
5585 if ($cm->completion
&& !empty($cm->completionexpected
)) {
5586 $DB->set_field('course_modules', 'completionexpected', $cm->completionexpected +
$data->timeshift
,
5587 array('id' => $cm->id
));
5592 // Clear course cache if changes made.
5594 rebuild_course_cache($data->courseid
, true);
5597 // Update course date completion criteria.
5598 \completion_criteria_date
::update_date($data->courseid
, $data->timeshift
);
5601 $status[] = array('component' => $componentstr, 'item' => get_string('datechanged'), 'error' => false);
5604 if (!empty($data->reset_end_date
)) {
5605 // If the user set a end date value respect it.
5606 $DB->set_field('course', 'enddate', $data->reset_end_date
, array('id' => $data->courseid
));
5607 } else if ($data->timeshift
> 0 && $data->reset_end_date_old
) {
5608 // If there is a time shift apply it to the end date as well.
5609 $enddate = $data->reset_end_date_old +
$data->timeshift
;
5610 $DB->set_field('course', 'enddate', $enddate, array('id' => $data->courseid
));
5613 if (!empty($data->reset_events
)) {
5614 $DB->delete_records('event', array('courseid' => $data->courseid
));
5615 $status[] = array('component' => $componentstr, 'item' => get_string('deleteevents', 'calendar'), 'error' => false);
5618 if (!empty($data->reset_notes
)) {
5619 require_once($CFG->dirroot
.'/notes/lib.php');
5620 note_delete_all($data->courseid
);
5621 $status[] = array('component' => $componentstr, 'item' => get_string('deletenotes', 'notes'), 'error' => false);
5624 if (!empty($data->delete_blog_associations
)) {
5625 require_once($CFG->dirroot
.'/blog/lib.php');
5626 blog_remove_associations_for_course($data->courseid
);
5627 $status[] = array('component' => $componentstr, 'item' => get_string('deleteblogassociations', 'blog'), 'error' => false);
5630 if (!empty($data->reset_completion
)) {
5631 // Delete course and activity completion information.
5632 $course = $DB->get_record('course', array('id' => $data->courseid
));
5633 $cc = new completion_info($course);
5634 $cc->delete_all_completion_data();
5635 $status[] = array('component' => $componentstr,
5636 'item' => get_string('deletecompletiondata', 'completion'), 'error' => false);
5639 if (!empty($data->reset_competency_ratings
)) {
5640 \core_competency\api
::hook_course_reset_competency_ratings($data->courseid
);
5641 $status[] = array('component' => $componentstr,
5642 'item' => get_string('deletecompetencyratings', 'core_competency'), 'error' => false);
5645 $componentstr = get_string('roles');
5647 if (!empty($data->reset_roles_overrides
)) {
5648 $children = $context->get_child_contexts();
5649 foreach ($children as $child) {
5650 $child->delete_capabilities();
5652 $context->delete_capabilities();
5653 $status[] = array('component' => $componentstr, 'item' => get_string('deletecourseoverrides', 'role'), 'error' => false);
5656 if (!empty($data->reset_roles_local
)) {
5657 $children = $context->get_child_contexts();
5658 foreach ($children as $child) {
5659 role_unassign_all(array('contextid' => $child->id
));
5661 $status[] = array('component' => $componentstr, 'item' => get_string('deletelocalroles', 'role'), 'error' => false);
5664 // First unenrol users - this cleans some of related user data too, such as forum subscriptions, tracking, etc.
5665 $data->unenrolled
= array();
5666 if (!empty($data->unenrol_users
)) {
5667 $plugins = enrol_get_plugins(true);
5668 $instances = enrol_get_instances($data->courseid
, true);
5669 foreach ($instances as $key => $instance) {
5670 if (!isset($plugins[$instance->enrol
])) {
5671 unset($instances[$key]);
5676 $usersroles = enrol_get_course_users_roles($data->courseid
);
5677 foreach ($data->unenrol_users
as $withroleid) {
5680 FROM {user_enrolments} ue
5681 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5682 JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5683 JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.roleid = :roleid AND ra.userid = ue.userid)";
5684 $params = array('courseid' => $data->courseid
, 'roleid' => $withroleid, 'courselevel' => CONTEXT_COURSE
);
5687 // Without any role assigned at course context.
5689 FROM {user_enrolments} ue
5690 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5691 JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5692 LEFT JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.userid = ue.userid)
5693 WHERE ra.id IS null";
5694 $params = array('courseid' => $data->courseid
, 'courselevel' => CONTEXT_COURSE
);
5697 $rs = $DB->get_recordset_sql($sql, $params);
5698 foreach ($rs as $ue) {
5699 if (!isset($instances[$ue->enrolid
])) {
5702 $instance = $instances[$ue->enrolid
];
5703 $plugin = $plugins[$instance->enrol
];
5704 if (!$plugin->allow_unenrol($instance) and !$plugin->allow_unenrol_user($instance, $ue)) {
5708 if ($withroleid && count($usersroles[$ue->userid
]) > 1) {
5709 // If we don't remove all roles and user has more than one role, just remove this role.
5710 role_unassign($withroleid, $ue->userid
, $context->id
);
5712 unset($usersroles[$ue->userid
][$withroleid]);
5714 // If we remove all roles or user has only one role, unenrol user from course.
5715 $plugin->unenrol_user($instance, $ue->userid
);
5717 $data->unenrolled
[$ue->userid
] = $ue->userid
;
5722 if (!empty($data->unenrolled
)) {
5724 'component' => $componentstr,
5725 'item' => get_string('unenrol', 'enrol').' ('.count($data->unenrolled
).')',
5730 $componentstr = get_string('groups');
5732 // Remove all group members.
5733 if (!empty($data->reset_groups_members
)) {
5734 groups_delete_group_members($data->courseid
);
5735 $status[] = array('component' => $componentstr, 'item' => get_string('removegroupsmembers', 'group'), 'error' => false);
5738 // Remove all groups.
5739 if (!empty($data->reset_groups_remove
)) {
5740 groups_delete_groups($data->courseid
, false);
5741 $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroups', 'group'), 'error' => false);
5744 // Remove all grouping members.
5745 if (!empty($data->reset_groupings_members
)) {
5746 groups_delete_groupings_groups($data->courseid
, false);
5747 $status[] = array('component' => $componentstr, 'item' => get_string('removegroupingsmembers', 'group'), 'error' => false);
5750 // Remove all groupings.
5751 if (!empty($data->reset_groupings_remove
)) {
5752 groups_delete_groupings($data->courseid
, false);
5753 $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroupings', 'group'), 'error' => false);
5756 // Look in every instance of every module for data to delete.
5757 $unsupportedmods = array();
5758 if ($allmods = $DB->get_records('modules') ) {
5759 foreach ($allmods as $mod) {
5760 $modname = $mod->name
;
5761 $modfile = $CFG->dirroot
.'/mod/'. $modname.'/lib.php';
5762 $moddeleteuserdata = $modname.'_reset_userdata'; // Function to delete user data.
5763 if (file_exists($modfile)) {
5764 if (!$DB->count_records($modname, array('course' => $data->courseid
))) {
5765 continue; // Skip mods with no instances.
5767 include_once($modfile);
5768 if (function_exists($moddeleteuserdata)) {
5769 $modstatus = $moddeleteuserdata($data);
5770 if (is_array($modstatus)) {
5771 $status = array_merge($status, $modstatus);
5773 debugging('Module '.$modname.' returned incorrect staus - must be an array!');
5776 $unsupportedmods[] = $mod;
5779 debugging('Missing lib.php in '.$modname.' module!');
5781 // Update calendar events for all modules.
5782 course_module_bulk_update_calendar_events($modname, $data->courseid
);
5784 // Purge the course cache after resetting course start date. MDL-76936
5785 if ($data->timeshift
) {
5786 course_modinfo
::purge_course_cache($data->courseid
);
5790 // Mention unsupported mods.
5791 if (!empty($unsupportedmods)) {
5792 foreach ($unsupportedmods as $mod) {
5794 'component' => get_string('modulenameplural', $mod->name
),
5796 'error' => get_string('resetnotimplemented')
5801 $componentstr = get_string('gradebook', 'grades');
5802 // Reset gradebook,.
5803 if (!empty($data->reset_gradebook_items
)) {
5804 remove_course_grades($data->courseid
, false);
5805 grade_grab_course_grades($data->courseid
);
5806 grade_regrade_final_grades($data->courseid
);
5807 $status[] = array('component' => $componentstr, 'item' => get_string('removeallcourseitems', 'grades'), 'error' => false);
5809 } else if (!empty($data->reset_gradebook_grades
)) {
5810 grade_course_reset($data->courseid
);
5811 $status[] = array('component' => $componentstr, 'item' => get_string('removeallcoursegrades', 'grades'), 'error' => false);
5814 if (!empty($data->reset_comments
)) {
5815 require_once($CFG->dirroot
.'/comment/lib.php');
5816 comment
::reset_course_page_comments($context);
5819 $event = \core\event\course_reset_ended
::create($eventparams);
5826 * Generate an email processing address.
5829 * @param string $modargs
5830 * @return string Returns email processing address
5832 function generate_email_processing_address($modid, $modargs) {
5835 $header = $CFG->mailprefix
. substr(base64_encode(pack('C', $modid)), 0, 2).$modargs;
5836 return $header . substr(md5($header.get_site_identifier()), 0, 16).'@'.$CFG->maildomain
;
5842 * @todo Finish documenting this function
5844 * @param string $modargs
5845 * @param string $body Currently unused
5847 function moodle_process_email($modargs, $body) {
5850 // The first char should be an unencoded letter. We'll take this as an action.
5851 switch ($modargs[0]) {
5852 case 'B': { // Bounce.
5853 list(, $userid) = unpack('V', base64_decode(substr($modargs, 1, 8)));
5854 if ($user = $DB->get_record("user", array('id' => $userid), "id,email")) {
5855 // Check the half md5 of their email.
5856 $md5check = substr(md5($user->email
), 0, 16);
5857 if ($md5check == substr($modargs, -16)) {
5858 set_bounce_count($user);
5860 // Else maybe they've already changed it?
5864 // Maybe more later?
5871 * Get mailer instance, enable buffering, flush buffer or disable buffering.
5873 * @param string $action 'get', 'buffer', 'close' or 'flush'
5874 * @return moodle_phpmailer|null mailer instance if 'get' used or nothing
5876 function get_mailer($action='get') {
5879 /** @var moodle_phpmailer $mailer */
5880 static $mailer = null;
5881 static $counter = 0;
5883 if (!isset($CFG->smtpmaxbulk
)) {
5884 $CFG->smtpmaxbulk
= 1;
5887 if ($action == 'get') {
5888 $prevkeepalive = false;
5890 if (isset($mailer) and $mailer->Mailer
== 'smtp') {
5891 if ($counter < $CFG->smtpmaxbulk
and !$mailer->isError()) {
5893 // Reset the mailer.
5894 $mailer->Priority
= 3;
5895 $mailer->CharSet
= 'UTF-8'; // Our default.
5896 $mailer->ContentType
= "text/plain";
5897 $mailer->Encoding
= "8bit";
5898 $mailer->From
= "root@localhost";
5899 $mailer->FromName
= "Root User";
5900 $mailer->Sender
= "";
5901 $mailer->Subject
= "";
5903 $mailer->AltBody
= "";
5904 $mailer->ConfirmReadingTo
= "";
5906 $mailer->clearAllRecipients();
5907 $mailer->clearReplyTos();
5908 $mailer->clearAttachments();
5909 $mailer->clearCustomHeaders();
5913 $prevkeepalive = $mailer->SMTPKeepAlive
;
5914 get_mailer('flush');
5917 require_once($CFG->libdir
.'/phpmailer/moodle_phpmailer.php');
5918 $mailer = new moodle_phpmailer();
5922 if ($CFG->smtphosts
== 'qmail') {
5923 // Use Qmail system.
5926 } else if (empty($CFG->smtphosts
)) {
5927 // Use PHP mail() = sendmail.
5931 // Use SMTP directly.
5933 if (!empty($CFG->debugsmtp
) && (!empty($CFG->debugdeveloper
))) {
5934 $mailer->SMTPDebug
= 3;
5936 // Specify main and backup servers.
5937 $mailer->Host
= $CFG->smtphosts
;
5938 // Specify secure connection protocol.
5939 $mailer->SMTPSecure
= $CFG->smtpsecure
;
5940 // Use previous keepalive.
5941 $mailer->SMTPKeepAlive
= $prevkeepalive;
5943 if ($CFG->smtpuser
) {
5944 // Use SMTP authentication.
5945 $mailer->SMTPAuth
= true;
5946 $mailer->Username
= $CFG->smtpuser
;
5947 $mailer->Password
= $CFG->smtppass
;
5956 // Keep smtp session open after sending.
5957 if ($action == 'buffer') {
5958 if (!empty($CFG->smtpmaxbulk
)) {
5959 get_mailer('flush');
5961 if ($m->Mailer
== 'smtp') {
5962 $m->SMTPKeepAlive
= true;
5968 // Close smtp session, but continue buffering.
5969 if ($action == 'flush') {
5970 if (isset($mailer) and $mailer->Mailer
== 'smtp') {
5971 if (!empty($mailer->SMTPDebug
)) {
5974 $mailer->SmtpClose();
5975 if (!empty($mailer->SMTPDebug
)) {
5982 // Close smtp session, do not buffer anymore.
5983 if ($action == 'close') {
5984 if (isset($mailer) and $mailer->Mailer
== 'smtp') {
5985 get_mailer('flush');
5986 $mailer->SMTPKeepAlive
= false;
5988 $mailer = null; // Better force new instance.
5994 * A helper function to test for email diversion
5996 * @param string $email
5997 * @return bool Returns true if the email should be diverted
5999 function email_should_be_diverted($email) {
6002 if (empty($CFG->divertallemailsto
)) {
6006 if (empty($CFG->divertallemailsexcept
)) {
6010 $patterns = array_map('trim', preg_split("/[\s,]+/", $CFG->divertallemailsexcept
, -1, PREG_SPLIT_NO_EMPTY
));
6011 foreach ($patterns as $pattern) {
6012 if (preg_match("/{$pattern}/i", $email)) {
6021 * Generate a unique email Message-ID using the moodle domain and install path
6023 * @param string $localpart An optional unique message id prefix.
6024 * @return string The formatted ID ready for appending to the email headers.
6026 function generate_email_messageid($localpart = null) {
6029 $urlinfo = parse_url($CFG->wwwroot
);
6030 $base = '@' . $urlinfo['host'];
6032 // If multiple moodles are on the same domain we want to tell them
6033 // apart so we add the install path to the local part. This means
6034 // that the id local part should never contain a / character so
6035 // we can correctly parse the id to reassemble the wwwroot.
6036 if (isset($urlinfo['path'])) {
6037 $base = $urlinfo['path'] . $base;
6040 if (empty($localpart)) {
6041 $localpart = uniqid('', true);
6044 // Because we may have an option /installpath suffix to the local part
6045 // of the id we need to escape any / chars which are in the $localpart.
6046 $localpart = str_replace('/', '%2F', $localpart);
6048 return '<' . $localpart . $base . '>';
6052 * Send an email to a specified user
6054 * @param stdClass $user A {@link $USER} object
6055 * @param stdClass $from A {@link $USER} object
6056 * @param string $subject plain text subject line of the email
6057 * @param string $messagetext plain text version of the message
6058 * @param string $messagehtml complete html version of the message (optional)
6059 * @param string $attachment a file on the filesystem, either relative to $CFG->dataroot or a full path to a file in one of
6060 * the following directories: $CFG->cachedir, $CFG->dataroot, $CFG->dirroot, $CFG->localcachedir, $CFG->tempdir
6061 * @param string $attachname the name of the file (extension indicates MIME)
6062 * @param bool $usetrueaddress determines whether $from email address should
6063 * be sent out. Will be overruled by user profile setting for maildisplay
6064 * @param string $replyto Email address to reply to
6065 * @param string $replytoname Name of reply to recipient
6066 * @param int $wordwrapwidth custom word wrap width, default 79
6067 * @return bool Returns true if mail was sent OK and false if there was an error.
6069 function email_to_user($user, $from, $subject, $messagetext, $messagehtml = '', $attachment = '', $attachname = '',
6070 $usetrueaddress = true, $replyto = '', $replytoname = '', $wordwrapwidth = 79) {
6072 global $CFG, $PAGE, $SITE;
6074 if (empty($user) or empty($user->id
)) {
6075 debugging('Can not send email to null user', DEBUG_DEVELOPER
);
6079 if (empty($user->email
)) {
6080 debugging('Can not send email to user without email: '.$user->id
, DEBUG_DEVELOPER
);
6084 if (!empty($user->deleted
)) {
6085 debugging('Can not send email to deleted user: '.$user->id
, DEBUG_DEVELOPER
);
6089 if (defined('BEHAT_SITE_RUNNING')) {
6090 // Fake email sending in behat.
6094 if (!empty($CFG->noemailever
)) {
6095 // Hidden setting for development sites, set in config.php if needed.
6096 debugging('Not sending email due to $CFG->noemailever config setting', DEBUG_NORMAL
);
6100 if (email_should_be_diverted($user->email
)) {
6101 $subject = "[DIVERTED {$user->email}] $subject";
6102 $user = clone($user);
6103 $user->email
= $CFG->divertallemailsto
;
6106 // Skip mail to suspended users.
6107 if ((isset($user->auth
) && $user->auth
=='nologin') or (isset($user->suspended
) && $user->suspended
)) {
6111 if (!validate_email($user->email
)) {
6112 // We can not send emails to invalid addresses - it might create security issue or confuse the mailer.
6113 debugging("email_to_user: User $user->id (".fullname($user).") email ($user->email) is invalid! Not sending.");
6117 if (over_bounce_threshold($user)) {
6118 debugging("email_to_user: User $user->id (".fullname($user).") is over bounce threshold! Not sending.");
6122 // TLD .invalid is specifically reserved for invalid domain names.
6123 // For More information, see {@link http://tools.ietf.org/html/rfc2606#section-2}.
6124 if (substr($user->email
, -8) == '.invalid') {
6125 debugging("email_to_user: User $user->id (".fullname($user).") email domain ($user->email) is invalid! Not sending.");
6126 return true; // This is not an error.
6129 // If the user is a remote mnet user, parse the email text for URL to the
6130 // wwwroot and modify the url to direct the user's browser to login at their
6131 // home site (identity provider - idp) before hitting the link itself.
6132 if (is_mnet_remote_user($user)) {
6133 require_once($CFG->dirroot
.'/mnet/lib.php');
6135 $jumpurl = mnet_get_idp_jump_url($user);
6136 $callback = partial('mnet_sso_apply_indirection', $jumpurl);
6138 $messagetext = preg_replace_callback("%($CFG->wwwroot[^[:space:]]*)%",
6141 $messagehtml = preg_replace_callback("%href=[\"'`]($CFG->wwwroot[\w_:\?=#&@/;.~-]*)[\"'`]%",
6145 $mail = get_mailer();
6147 if (!empty($mail->SMTPDebug
)) {
6148 echo '<pre>' . "\n";
6151 $temprecipients = array();
6152 $tempreplyto = array();
6154 // Make sure that we fall back onto some reasonable no-reply address.
6155 $noreplyaddressdefault = 'noreply@' . get_host_from_url($CFG->wwwroot
);
6156 $noreplyaddress = empty($CFG->noreplyaddress
) ?
$noreplyaddressdefault : $CFG->noreplyaddress
;
6158 if (!validate_email($noreplyaddress)) {
6159 debugging('email_to_user: Invalid noreply-email '.s($noreplyaddress));
6160 $noreplyaddress = $noreplyaddressdefault;
6163 // Make up an email address for handling bounces.
6164 if (!empty($CFG->handlebounces
)) {
6165 $modargs = 'B'.base64_encode(pack('V', $user->id
)).substr(md5($user->email
), 0, 16);
6166 $mail->Sender
= generate_email_processing_address(0, $modargs);
6168 $mail->Sender
= $noreplyaddress;
6171 // Make sure that the explicit replyto is valid, fall back to the implicit one.
6172 if (!empty($replyto) && !validate_email($replyto)) {
6173 debugging('email_to_user: Invalid replyto-email '.s($replyto));
6174 $replyto = $noreplyaddress;
6177 if (is_string($from)) { // So we can pass whatever we want if there is need.
6178 $mail->From
= $noreplyaddress;
6179 $mail->FromName
= $from;
6180 // Check if using the true address is true, and the email is in the list of allowed domains for sending email,
6181 // and that the senders email setting is either displayed to everyone, or display to only other users that are enrolled
6182 // in a course with the sender.
6183 } else if ($usetrueaddress && can_send_from_real_email_address($from, $user)) {
6184 if (!validate_email($from->email
)) {
6185 debugging('email_to_user: Invalid from-email '.s($from->email
).' - not sending');
6186 // Better not to use $noreplyaddress in this case.
6189 $mail->From
= $from->email
;
6190 $fromdetails = new stdClass();
6191 $fromdetails->name
= fullname($from);
6192 $fromdetails->url
= preg_replace('#^https?://#', '', $CFG->wwwroot
);
6193 $fromdetails->siteshortname
= format_string($SITE->shortname
);
6194 $fromstring = $fromdetails->name
;
6195 if ($CFG->emailfromvia
== EMAIL_VIA_ALWAYS
) {
6196 $fromstring = get_string('emailvia', 'core', $fromdetails);
6198 $mail->FromName
= $fromstring;
6199 if (empty($replyto)) {
6200 $tempreplyto[] = array($from->email
, fullname($from));
6203 $mail->From
= $noreplyaddress;
6204 $fromdetails = new stdClass();
6205 $fromdetails->name
= fullname($from);
6206 $fromdetails->url
= preg_replace('#^https?://#', '', $CFG->wwwroot
);
6207 $fromdetails->siteshortname
= format_string($SITE->shortname
);
6208 $fromstring = $fromdetails->name
;
6209 if ($CFG->emailfromvia
!= EMAIL_VIA_NEVER
) {
6210 $fromstring = get_string('emailvia', 'core', $fromdetails);
6212 $mail->FromName
= $fromstring;
6213 if (empty($replyto)) {
6214 $tempreplyto[] = array($noreplyaddress, get_string('noreplyname'));
6218 if (!empty($replyto)) {
6219 $tempreplyto[] = array($replyto, $replytoname);
6222 $temprecipients[] = array($user->email
, fullname($user));
6225 $mail->WordWrap
= $wordwrapwidth;
6227 if (!empty($from->customheaders
)) {
6228 // Add custom headers.
6229 if (is_array($from->customheaders
)) {
6230 foreach ($from->customheaders
as $customheader) {
6231 $mail->addCustomHeader($customheader);
6234 $mail->addCustomHeader($from->customheaders
);
6238 // If the X-PHP-Originating-Script email header is on then also add an additional
6239 // header with details of where exactly in moodle the email was triggered from,
6240 // either a call to message_send() or to email_to_user().
6241 if (ini_get('mail.add_x_header')) {
6243 $stack = debug_backtrace(false);
6244 $origin = $stack[0];
6246 foreach ($stack as $depth => $call) {
6247 if ($call['function'] == 'message_send') {
6252 $originheader = $CFG->wwwroot
. ' => ' . gethostname() . ':'
6253 . str_replace($CFG->dirroot
. '/', '', $origin['file']) . ':' . $origin['line'];
6254 $mail->addCustomHeader('X-Moodle-Originating-Script: ' . $originheader);
6257 if (!empty($CFG->emailheaders
)) {
6258 $headers = array_map('trim', explode("\n", $CFG->emailheaders
));
6259 foreach ($headers as $header) {
6260 if (!empty($header)) {
6261 $mail->addCustomHeader($header);
6266 if (!empty($from->priority
)) {
6267 $mail->Priority
= $from->priority
;
6270 $renderer = $PAGE->get_renderer('core');
6272 'sitefullname' => $SITE->fullname
,
6273 'siteshortname' => $SITE->shortname
,
6274 'sitewwwroot' => $CFG->wwwroot
,
6275 'subject' => $subject,
6276 'prefix' => $CFG->emailsubjectprefix
,
6277 'to' => $user->email
,
6278 'toname' => fullname($user),
6279 'from' => $mail->From
,
6280 'fromname' => $mail->FromName
,
6282 if (!empty($tempreplyto[0])) {
6283 $context['replyto'] = $tempreplyto[0][0];
6284 $context['replytoname'] = $tempreplyto[0][1];
6286 if ($user->id
> 0) {
6287 $context['touserid'] = $user->id
;
6288 $context['tousername'] = $user->username
;
6291 if (!empty($user->mailformat
) && $user->mailformat
== 1) {
6292 // Only process html templates if the user preferences allow html email.
6294 if (!$messagehtml) {
6295 // If no html has been given, BUT there is an html wrapping template then
6296 // auto convert the text to html and then wrap it.
6297 $messagehtml = trim(text_to_html($messagetext));
6299 $context['body'] = $messagehtml;
6300 $messagehtml = $renderer->render_from_template('core/email_html', $context);
6303 $context['body'] = html_to_text(nl2br($messagetext));
6304 $mail->Subject
= $renderer->render_from_template('core/email_subject', $context);
6305 $mail->FromName
= $renderer->render_from_template('core/email_fromname', $context);
6306 $messagetext = $renderer->render_from_template('core/email_text', $context);
6308 // Autogenerate a MessageID if it's missing.
6309 if (empty($mail->MessageID
)) {
6310 $mail->MessageID
= generate_email_messageid();
6313 if ($messagehtml && !empty($user->mailformat
) && $user->mailformat
== 1) {
6314 // Don't ever send HTML to users who don't want it.
6315 $mail->isHTML(true);
6316 $mail->Encoding
= 'quoted-printable';
6317 $mail->Body
= $messagehtml;
6318 $mail->AltBody
= "\n$messagetext\n";
6320 $mail->IsHTML(false);
6321 $mail->Body
= "\n$messagetext\n";
6324 if ($attachment && $attachname) {
6325 if (preg_match( "~\\.\\.~" , $attachment )) {
6326 // Security check for ".." in dir path.
6327 $supportuser = core_user
::get_support_user();
6328 $temprecipients[] = array($supportuser->email
, fullname($supportuser, true));
6329 $mail->addStringAttachment('Error in attachment. User attempted to attach a filename with a unsafe name.', 'error.txt', '8bit', 'text/plain');
6331 require_once($CFG->libdir
.'/filelib.php');
6332 $mimetype = mimeinfo('type', $attachname);
6334 // Before doing the comparison, make sure that the paths are correct (Windows uses slashes in the other direction).
6335 // The absolute (real) path is also fetched to ensure that comparisons to allowed paths are compared equally.
6336 $attachpath = str_replace('\\', '/', realpath($attachment));
6338 // Build an array of all filepaths from which attachments can be added (normalised slashes, absolute/real path).
6339 $allowedpaths = array_map(function(string $path): string {
6340 return str_replace('\\', '/', realpath($path));
6345 $CFG->localcachedir
,
6347 $CFG->localrequestdir
,
6350 // Set addpath to true.
6353 // Check if attachment includes one of the allowed paths.
6354 foreach (array_filter($allowedpaths) as $allowedpath) {
6355 // Set addpath to false if the attachment includes one of the allowed paths.
6356 if (strpos($attachpath, $allowedpath) === 0) {
6362 // If the attachment is a full path to a file in the multiple allowed paths, use it as is,
6363 // otherwise assume it is a relative path from the dataroot (for backwards compatibility reasons).
6364 if ($addpath == true) {
6365 $attachment = $CFG->dataroot
. '/' . $attachment;
6368 $mail->addAttachment($attachment, $attachname, 'base64', $mimetype);
6372 // Check if the email should be sent in an other charset then the default UTF-8.
6373 if ((!empty($CFG->sitemailcharset
) ||
!empty($CFG->allowusermailcharset
))) {
6375 // Use the defined site mail charset or eventually the one preferred by the recipient.
6376 $charset = $CFG->sitemailcharset
;
6377 if (!empty($CFG->allowusermailcharset
)) {
6378 if ($useremailcharset = get_user_preferences('mailcharset', '0', $user->id
)) {
6379 $charset = $useremailcharset;
6383 // Convert all the necessary strings if the charset is supported.
6384 $charsets = get_list_of_charsets();
6385 unset($charsets['UTF-8']);
6386 if (in_array($charset, $charsets)) {
6387 $mail->CharSet
= $charset;
6388 $mail->FromName
= core_text
::convert($mail->FromName
, 'utf-8', strtolower($charset));
6389 $mail->Subject
= core_text
::convert($mail->Subject
, 'utf-8', strtolower($charset));
6390 $mail->Body
= core_text
::convert($mail->Body
, 'utf-8', strtolower($charset));
6391 $mail->AltBody
= core_text
::convert($mail->AltBody
, 'utf-8', strtolower($charset));
6393 foreach ($temprecipients as $key => $values) {
6394 $temprecipients[$key][1] = core_text
::convert($values[1], 'utf-8', strtolower($charset));
6396 foreach ($tempreplyto as $key => $values) {
6397 $tempreplyto[$key][1] = core_text
::convert($values[1], 'utf-8', strtolower($charset));
6402 foreach ($temprecipients as $values) {
6403 $mail->addAddress($values[0], $values[1]);
6405 foreach ($tempreplyto as $values) {
6406 $mail->addReplyTo($values[0], $values[1]);
6409 if (!empty($CFG->emaildkimselector
)) {
6410 $domain = substr(strrchr($mail->From
, "@"), 1);
6411 $pempath = "{$CFG->dataroot}/dkim/{$domain}/{$CFG->emaildkimselector}.private";
6412 if (file_exists($pempath)) {
6413 $mail->DKIM_domain
= $domain;
6414 $mail->DKIM_private
= $pempath;
6415 $mail->DKIM_selector
= $CFG->emaildkimselector
;
6416 $mail->DKIM_identity
= $mail->From
;
6418 debugging("Email DKIM selector chosen due to {$mail->From} but no certificate found at $pempath", DEBUG_DEVELOPER
);
6422 if ($mail->send()) {
6423 set_send_count($user);
6424 if (!empty($mail->SMTPDebug
)) {
6429 // Trigger event for failing to send email.
6430 $event = \core\event\email_failed
::create(array(
6431 'context' => context_system
::instance(),
6432 'userid' => $from->id
,
6433 'relateduserid' => $user->id
,
6435 'subject' => $subject,
6436 'message' => $messagetext,
6437 'errorinfo' => $mail->ErrorInfo
6442 mtrace('Error: lib/moodlelib.php email_to_user(): '.$mail->ErrorInfo
);
6444 if (!empty($mail->SMTPDebug
)) {
6452 * Check to see if a user's real email address should be used for the "From" field.
6454 * @param object $from The user object for the user we are sending the email from.
6455 * @param object $user The user object that we are sending the email to.
6456 * @param array $unused No longer used.
6457 * @return bool Returns true if we can use the from user's email adress in the "From" field.
6459 function can_send_from_real_email_address($from, $user, $unused = null) {
6461 if (!isset($CFG->allowedemaildomains
) ||
empty(trim($CFG->allowedemaildomains
))) {
6464 $alloweddomains = array_map('trim', explode("\n", $CFG->allowedemaildomains
));
6465 // Email is in the list of allowed domains for sending email,
6466 // and the senders email setting is either displayed to everyone, or display to only other users that are enrolled
6467 // in a course with the sender.
6468 if (\core\ip_utils
::is_domain_in_allowed_list(substr($from->email
, strpos($from->email
, '@') +
1), $alloweddomains)
6469 && ($from->maildisplay
== core_user
::MAILDISPLAY_EVERYONE
6470 ||
($from->maildisplay
== core_user
::MAILDISPLAY_COURSE_MEMBERS_ONLY
6471 && enrol_get_shared_courses($user, $from, false, true)))) {
6478 * Generate a signoff for emails based on support settings
6482 function generate_email_signoff() {
6483 global $CFG, $OUTPUT;
6486 if (!empty($CFG->supportname
)) {
6487 $signoff .= $CFG->supportname
."\n";
6490 $supportemail = $OUTPUT->supportemail(['class' => 'font-weight-bold']);
6492 if ($supportemail) {
6493 $signoff .= "\n" . $supportemail . "\n";
6500 * Sets specified user's password and send the new password to the user via email.
6502 * @param stdClass $user A {@link $USER} object
6503 * @param bool $fasthash If true, use a low cost factor when generating the hash for speed.
6504 * @return bool|string Returns "true" if mail was sent OK and "false" if there was an error
6506 function setnew_password_and_mail($user, $fasthash = false) {
6509 // We try to send the mail in language the user understands,
6510 // unfortunately the filter_string() does not support alternative langs yet
6511 // so multilang will not work properly for site->fullname.
6512 $lang = empty($user->lang
) ?
get_newuser_language() : $user->lang
;
6516 $supportuser = core_user
::get_support_user();
6518 $newpassword = generate_password();
6520 update_internal_user_password($user, $newpassword, $fasthash);
6522 $a = new stdClass();
6523 $a->firstname
= fullname($user, true);
6524 $a->sitename
= format_string($site->fullname
);
6525 $a->username
= $user->username
;
6526 $a->newpassword
= $newpassword;
6527 $a->link
= $CFG->wwwroot
.'/login/?lang='.$lang;
6528 $a->signoff
= generate_email_signoff();
6530 $message = (string)new lang_string('newusernewpasswordtext', '', $a, $lang);
6532 $subject = format_string($site->fullname
) .': '. (string)new lang_string('newusernewpasswordsubj', '', $a, $lang);
6534 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6535 return email_to_user($user, $supportuser, $subject, $message);
6540 * Resets specified user's password and send the new password to the user via email.
6542 * @param stdClass $user A {@link $USER} object
6543 * @return bool Returns true if mail was sent OK and false if there was an error.
6545 function reset_password_and_mail($user) {
6549 $supportuser = core_user
::get_support_user();
6551 $userauth = get_auth_plugin($user->auth
);
6552 if (!$userauth->can_reset_password() or !is_enabled_auth($user->auth
)) {
6553 trigger_error("Attempt to reset user password for user $user->username with Auth $user->auth.");
6557 $newpassword = generate_password();
6559 if (!$userauth->user_update_password($user, $newpassword)) {
6560 throw new \
moodle_exception("cannotsetpassword");
6563 $a = new stdClass();
6564 $a->firstname
= $user->firstname
;
6565 $a->lastname
= $user->lastname
;
6566 $a->sitename
= format_string($site->fullname
);
6567 $a->username
= $user->username
;
6568 $a->newpassword
= $newpassword;
6569 $a->link
= $CFG->wwwroot
.'/login/change_password.php';
6570 $a->signoff
= generate_email_signoff();
6572 $message = get_string('newpasswordtext', '', $a);
6574 $subject = format_string($site->fullname
) .': '. get_string('changedpassword');
6576 unset_user_preference('create_password', $user); // Prevent cron from generating the password.
6578 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6579 return email_to_user($user, $supportuser, $subject, $message);
6583 * Send email to specified user with confirmation text and activation link.
6585 * @param stdClass $user A {@link $USER} object
6586 * @param string $confirmationurl user confirmation URL
6587 * @return bool Returns true if mail was sent OK and false if there was an error.
6589 function send_confirmation_email($user, $confirmationurl = null) {
6593 $supportuser = core_user
::get_support_user();
6595 $data = new stdClass();
6596 $data->sitename
= format_string($site->fullname
);
6597 $data->admin
= generate_email_signoff();
6599 $subject = get_string('emailconfirmationsubject', '', format_string($site->fullname
));
6601 if (empty($confirmationurl)) {
6602 $confirmationurl = '/login/confirm.php';
6605 $confirmationurl = new moodle_url($confirmationurl);
6606 // Remove data parameter just in case it was included in the confirmation so we can add it manually later.
6607 $confirmationurl->remove_params('data');
6608 $confirmationpath = $confirmationurl->out(false);
6610 // We need to custom encode the username to include trailing dots in the link.
6611 // Because of this custom encoding we can't use moodle_url directly.
6612 // Determine if a query string is present in the confirmation url.
6613 $hasquerystring = strpos($confirmationpath, '?') !== false;
6614 // Perform normal url encoding of the username first.
6615 $username = urlencode($user->username
);
6616 // Prevent problems with trailing dots not being included as part of link in some mail clients.
6617 $username = str_replace('.', '%2E', $username);
6619 $data->link
= $confirmationpath . ( $hasquerystring ?
'&' : '?') . 'data='. $user->secret
.'/'. $username;
6621 $message = get_string('emailconfirmation', '', $data);
6622 $messagehtml = text_to_html(get_string('emailconfirmation', '', $data), false, false, true);
6624 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6625 return email_to_user($user, $supportuser, $subject, $message, $messagehtml);
6629 * Sends a password change confirmation email.
6631 * @param stdClass $user A {@link $USER} object
6632 * @param stdClass $resetrecord An object tracking metadata regarding password reset request
6633 * @return bool Returns true if mail was sent OK and false if there was an error.
6635 function send_password_change_confirmation_email($user, $resetrecord) {
6639 $supportuser = core_user
::get_support_user();
6640 $pwresetmins = isset($CFG->pwresettime
) ?
floor($CFG->pwresettime
/ MINSECS
) : 30;
6642 $data = new stdClass();
6643 $data->firstname
= $user->firstname
;
6644 $data->lastname
= $user->lastname
;
6645 $data->username
= $user->username
;
6646 $data->sitename
= format_string($site->fullname
);
6647 $data->link
= $CFG->wwwroot
.'/login/forgot_password.php?token='. $resetrecord->token
;
6648 $data->admin
= generate_email_signoff();
6649 $data->resetminutes
= $pwresetmins;
6651 $message = get_string('emailresetconfirmation', '', $data);
6652 $subject = get_string('emailresetconfirmationsubject', '', format_string($site->fullname
));
6654 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6655 return email_to_user($user, $supportuser, $subject, $message);
6660 * Sends an email containing information on how to change your password.
6662 * @param stdClass $user A {@link $USER} object
6663 * @return bool Returns true if mail was sent OK and false if there was an error.
6665 function send_password_change_info($user) {
6667 $supportuser = core_user
::get_support_user();
6669 $data = new stdClass();
6670 $data->firstname
= $user->firstname
;
6671 $data->lastname
= $user->lastname
;
6672 $data->username
= $user->username
;
6673 $data->sitename
= format_string($site->fullname
);
6674 $data->admin
= generate_email_signoff();
6676 if (!is_enabled_auth($user->auth
)) {
6677 $message = get_string('emailpasswordchangeinfodisabled', '', $data);
6678 $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname
));
6679 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6680 return email_to_user($user, $supportuser, $subject, $message);
6683 $userauth = get_auth_plugin($user->auth
);
6684 ['subject' => $subject, 'message' => $message] = $userauth->get_password_change_info($user);
6686 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6687 return email_to_user($user, $supportuser, $subject, $message);
6691 * Check that an email is allowed. It returns an error message if there was a problem.
6693 * @param string $email Content of email
6694 * @return string|false
6696 function email_is_not_allowed($email) {
6699 // Comparing lowercase domains.
6700 $email = strtolower($email);
6701 if (!empty($CFG->allowemailaddresses
)) {
6702 $allowed = explode(' ', strtolower($CFG->allowemailaddresses
));
6703 foreach ($allowed as $allowedpattern) {
6704 $allowedpattern = trim($allowedpattern);
6705 if (!$allowedpattern) {
6708 if (strpos($allowedpattern, '.') === 0) {
6709 if (strpos(strrev($email), strrev($allowedpattern)) === 0) {
6710 // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com".
6714 } else if (strpos(strrev($email), strrev('@'.$allowedpattern)) === 0) {
6718 return get_string('emailonlyallowed', '', $CFG->allowemailaddresses
);
6720 } else if (!empty($CFG->denyemailaddresses
)) {
6721 $denied = explode(' ', strtolower($CFG->denyemailaddresses
));
6722 foreach ($denied as $deniedpattern) {
6723 $deniedpattern = trim($deniedpattern);
6724 if (!$deniedpattern) {
6727 if (strpos($deniedpattern, '.') === 0) {
6728 if (strpos(strrev($email), strrev($deniedpattern)) === 0) {
6729 // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com".
6730 return get_string('emailnotallowed', '', $CFG->denyemailaddresses
);
6733 } else if (strpos(strrev($email), strrev('@'.$deniedpattern)) === 0) {
6734 return get_string('emailnotallowed', '', $CFG->denyemailaddresses
);
6745 * Returns local file storage instance
6747 * @return file_storage
6749 function get_file_storage($reset = false) {
6763 require_once("$CFG->libdir/filelib.php");
6765 $fs = new file_storage();
6771 * Returns local file storage instance
6773 * @return file_browser
6775 function get_file_browser() {
6784 require_once("$CFG->libdir/filelib.php");
6786 $fb = new file_browser();
6792 * Returns file packer
6794 * @param string $mimetype default application/zip
6795 * @return file_packer
6797 function get_file_packer($mimetype='application/zip') {
6800 static $fp = array();
6802 if (isset($fp[$mimetype])) {
6803 return $fp[$mimetype];
6806 switch ($mimetype) {
6807 case 'application/zip':
6808 case 'application/vnd.moodle.profiling':
6809 $classname = 'zip_packer';
6812 case 'application/x-gzip' :
6813 $classname = 'tgz_packer';
6816 case 'application/vnd.moodle.backup':
6817 $classname = 'mbz_packer';
6824 require_once("$CFG->libdir/filestorage/$classname.php");
6825 $fp[$mimetype] = new $classname();
6827 return $fp[$mimetype];
6831 * Returns current name of file on disk if it exists.
6833 * @param string $newfile File to be verified
6834 * @return string Current name of file on disk if true
6836 function valid_uploaded_file($newfile) {
6837 if (empty($newfile)) {
6840 if (is_uploaded_file($newfile['tmp_name']) and $newfile['size'] > 0) {
6841 return $newfile['tmp_name'];
6848 * Returns the maximum size for uploading files.
6850 * There are seven possible upload limits:
6851 * 1. in Apache using LimitRequestBody (no way of checking or changing this)
6852 * 2. in php.ini for 'upload_max_filesize' (can not be changed inside PHP)
6853 * 3. in .htaccess for 'upload_max_filesize' (can not be changed inside PHP)
6854 * 4. in php.ini for 'post_max_size' (can not be changed inside PHP)
6855 * 5. by the Moodle admin in $CFG->maxbytes
6856 * 6. by the teacher in the current course $course->maxbytes
6857 * 7. by the teacher for the current module, eg $assignment->maxbytes
6859 * These last two are passed to this function as arguments (in bytes).
6860 * Anything defined as 0 is ignored.
6861 * The smallest of all the non-zero numbers is returned.
6863 * @todo Finish documenting this function
6865 * @param int $sitebytes Set maximum size
6866 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6867 * @param int $modulebytes Current module ->maxbytes (in bytes)
6868 * @param bool $unused This parameter has been deprecated and is not used any more.
6869 * @return int The maximum size for uploading files.
6871 function get_max_upload_file_size($sitebytes=0, $coursebytes=0, $modulebytes=0, $unused = false) {
6873 if (! $filesize = ini_get('upload_max_filesize')) {
6876 $minimumsize = get_real_size($filesize);
6878 if ($postsize = ini_get('post_max_size')) {
6879 $postsize = get_real_size($postsize);
6880 if ($postsize < $minimumsize) {
6881 $minimumsize = $postsize;
6885 if (($sitebytes > 0) and ($sitebytes < $minimumsize)) {
6886 $minimumsize = $sitebytes;
6889 if (($coursebytes > 0) and ($coursebytes < $minimumsize)) {
6890 $minimumsize = $coursebytes;
6893 if (($modulebytes > 0) and ($modulebytes < $minimumsize)) {
6894 $minimumsize = $modulebytes;
6897 return $minimumsize;
6901 * Returns the maximum size for uploading files for the current user
6903 * This function takes in account {@link get_max_upload_file_size()} the user's capabilities
6905 * @param context $context The context in which to check user capabilities
6906 * @param int $sitebytes Set maximum size
6907 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6908 * @param int $modulebytes Current module ->maxbytes (in bytes)
6909 * @param stdClass $user The user
6910 * @param bool $unused This parameter has been deprecated and is not used any more.
6911 * @return int The maximum size for uploading files.
6913 function get_user_max_upload_file_size($context, $sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $user = null,
6921 if (has_capability('moodle/course:ignorefilesizelimits', $context, $user)) {
6922 return USER_CAN_IGNORE_FILE_SIZE_LIMITS
;
6925 return get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes);
6929 * Returns an array of possible sizes in local language
6931 * Related to {@link get_max_upload_file_size()} - this function returns an
6932 * array of possible sizes in an array, translated to the
6935 * The list of options will go up to the minimum of $sitebytes, $coursebytes or $modulebytes.
6937 * If $coursebytes or $sitebytes is not 0, an option will be included for "Course/Site upload limit (X)"
6938 * with the value set to 0. This option will be the first in the list.
6940 * @uses SORT_NUMERIC
6941 * @param int $sitebytes Set maximum size
6942 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6943 * @param int $modulebytes Current module ->maxbytes (in bytes)
6944 * @param int|array $custombytes custom upload size/s which will be added to list,
6945 * Only value/s smaller then maxsize will be added to list.
6948 function get_max_upload_sizes($sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $custombytes = null) {
6951 if (!$maxsize = get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes)) {
6955 if ($sitebytes == 0) {
6956 // Will get the minimum of upload_max_filesize or post_max_size.
6957 $sitebytes = get_max_upload_file_size();
6960 $filesize = array();
6961 $sizelist = array(10240, 51200, 102400, 512000, 1048576, 2097152,
6962 5242880, 10485760, 20971520, 52428800, 104857600,
6963 262144000, 524288000, 786432000, 1073741824,
6964 2147483648, 4294967296, 8589934592);
6966 // If custombytes is given and is valid then add it to the list.
6967 if (is_number($custombytes) and $custombytes > 0) {
6968 $custombytes = (int)$custombytes;
6969 if (!in_array($custombytes, $sizelist)) {
6970 $sizelist[] = $custombytes;
6972 } else if (is_array($custombytes)) {
6973 $sizelist = array_unique(array_merge($sizelist, $custombytes));
6976 // Allow maxbytes to be selected if it falls outside the above boundaries.
6977 if (isset($CFG->maxbytes
) && !in_array(get_real_size($CFG->maxbytes
), $sizelist)) {
6978 // Note: get_real_size() is used in order to prevent problems with invalid values.
6979 $sizelist[] = get_real_size($CFG->maxbytes
);
6982 foreach ($sizelist as $sizebytes) {
6983 if ($sizebytes < $maxsize && $sizebytes > 0) {
6984 $filesize[(string)intval($sizebytes)] = display_size($sizebytes, 0);
6991 (($modulebytes < $coursebytes ||
$coursebytes == 0) &&
6992 ($modulebytes < $sitebytes ||
$sitebytes == 0))) {
6993 $limitlevel = get_string('activity', 'core');
6994 $displaysize = display_size($modulebytes, 0);
6995 $filesize[$modulebytes] = $displaysize; // Make sure the limit is also included in the list.
6997 } else if ($coursebytes && ($coursebytes < $sitebytes ||
$sitebytes == 0)) {
6998 $limitlevel = get_string('course', 'core');
6999 $displaysize = display_size($coursebytes, 0);
7000 $filesize[$coursebytes] = $displaysize; // Make sure the limit is also included in the list.
7002 } else if ($sitebytes) {
7003 $limitlevel = get_string('site', 'core');
7004 $displaysize = display_size($sitebytes, 0);
7005 $filesize[$sitebytes] = $displaysize; // Make sure the limit is also included in the list.
7008 krsort($filesize, SORT_NUMERIC
);
7010 $params = (object) array('contextname' => $limitlevel, 'displaysize' => $displaysize);
7011 $filesize = array('0' => get_string('uploadlimitwithsize', 'core', $params)) +
$filesize;
7018 * Returns an array with all the filenames in all subdirectories, relative to the given rootdir.
7020 * If excludefiles is defined, then that file/directory is ignored
7021 * If getdirs is true, then (sub)directories are included in the output
7022 * If getfiles is true, then files are included in the output
7023 * (at least one of these must be true!)
7025 * @todo Finish documenting this function. Add examples of $excludefile usage.
7027 * @param string $rootdir A given root directory to start from
7028 * @param string|array $excludefiles If defined then the specified file/directory is ignored
7029 * @param bool $descend If true then subdirectories are recursed as well
7030 * @param bool $getdirs If true then (sub)directories are included in the output
7031 * @param bool $getfiles If true then files are included in the output
7032 * @return array An array with all the filenames in all subdirectories, relative to the given rootdir
7034 function get_directory_list($rootdir, $excludefiles='', $descend=true, $getdirs=false, $getfiles=true) {
7038 if (!$getdirs and !$getfiles) { // Nothing to show.
7042 if (!is_dir($rootdir)) { // Must be a directory.
7046 if (!$dir = opendir($rootdir)) { // Can't open it for some reason.
7050 if (!is_array($excludefiles)) {
7051 $excludefiles = array($excludefiles);
7054 while (false !== ($file = readdir($dir))) {
7055 $firstchar = substr($file, 0, 1);
7056 if ($firstchar == '.' or $file == 'CVS' or in_array($file, $excludefiles)) {
7059 $fullfile = $rootdir .'/'. $file;
7060 if (filetype($fullfile) == 'dir') {
7065 $subdirs = get_directory_list($fullfile, $excludefiles, $descend, $getdirs, $getfiles);
7066 foreach ($subdirs as $subdir) {
7067 $dirs[] = $file .'/'. $subdir;
7070 } else if ($getfiles) {
7083 * Adds up all the files in a directory and works out the size.
7085 * @param string $rootdir The directory to start from
7086 * @param string $excludefile A file to exclude when summing directory size
7087 * @return int The summed size of all files and subfiles within the root directory
7089 function get_directory_size($rootdir, $excludefile='') {
7092 // Do it this way if we can, it's much faster.
7093 if (!empty($CFG->pathtodu
) && is_executable(trim($CFG->pathtodu
))) {
7094 $command = trim($CFG->pathtodu
).' -sk '.escapeshellarg($rootdir);
7097 exec($command, $output, $return);
7098 if (is_array($output)) {
7099 // We told it to return k.
7100 return get_real_size(intval($output[0]).'k');
7104 if (!is_dir($rootdir)) {
7105 // Must be a directory.
7109 if (!$dir = @opendir
($rootdir)) {
7110 // Can't open it for some reason.
7116 while (false !== ($file = readdir($dir))) {
7117 $firstchar = substr($file, 0, 1);
7118 if ($firstchar == '.' or $file == 'CVS' or $file == $excludefile) {
7121 $fullfile = $rootdir .'/'. $file;
7122 if (filetype($fullfile) == 'dir') {
7123 $size +
= get_directory_size($fullfile, $excludefile);
7125 $size +
= filesize($fullfile);
7134 * Converts bytes into display form
7136 * @param int $size The size to convert to human readable form
7137 * @param int $decimalplaces If specified, uses fixed number of decimal places
7138 * @param string $fixedunits If specified, uses fixed units (e.g. 'KB')
7139 * @return string Display version of size
7141 function display_size($size, int $decimalplaces = 1, string $fixedunits = ''): string {
7145 if ($size === USER_CAN_IGNORE_FILE_SIZE_LIMITS
) {
7146 return get_string('unlimited');
7149 if (empty($units)) {
7150 $units[] = get_string('sizeb');
7151 $units[] = get_string('sizekb');
7152 $units[] = get_string('sizemb');
7153 $units[] = get_string('sizegb');
7154 $units[] = get_string('sizetb');
7155 $units[] = get_string('sizepb');
7158 switch ($fixedunits) {
7178 $magnitude = floor(log($size, 1024));
7179 $magnitude = max(0, min(5, $magnitude));
7182 throw new coding_exception('Unknown fixed units value: ' . $fixedunits);
7185 // Special case for magnitude 0 (bytes) - never use decimal places.
7187 if ($magnitude === 0) {
7188 return round($size) . $nbsp . $units[$magnitude];
7191 // Convert to specified units.
7192 $sizeinunit = $size / 1024 ** $magnitude;
7194 // Fixed decimal places.
7195 return sprintf('%.' . $decimalplaces . 'f', $sizeinunit) . $nbsp . $units[$magnitude];
7199 * Cleans a given filename by removing suspicious or troublesome characters
7201 * @see clean_param()
7202 * @param string $string file name
7203 * @return string cleaned file name
7205 function clean_filename($string) {
7206 return clean_param($string, PARAM_FILE
);
7209 // STRING TRANSLATION.
7212 * Returns the code for the current language
7217 function current_language() {
7218 global $CFG, $PAGE, $SESSION, $USER;
7220 if (!empty($SESSION->forcelang
)) {
7221 // Allows overriding course-forced language (useful for admins to check
7222 // issues in courses whose language they don't understand).
7223 // Also used by some code to temporarily get language-related information in a
7224 // specific language (see force_current_language()).
7225 $return = $SESSION->forcelang
;
7227 } else if (!empty($PAGE->cm
->lang
)) {
7228 // Activity language, if set.
7229 $return = $PAGE->cm
->lang
;
7231 } else if (!empty($PAGE->course
->id
) && $PAGE->course
->id
!= SITEID
&& !empty($PAGE->course
->lang
)) {
7232 // Course language can override all other settings for this page.
7233 $return = $PAGE->course
->lang
;
7235 } else if (!empty($SESSION->lang
)) {
7236 // Session language can override other settings.
7237 $return = $SESSION->lang
;
7239 } else if (!empty($USER->lang
)) {
7240 $return = $USER->lang
;
7242 } else if (isset($CFG->lang
)) {
7243 $return = $CFG->lang
;
7249 // Just in case this slipped in from somewhere by accident.
7250 $return = str_replace('_utf8', '', $return);
7256 * Fix the current language to the given language code.
7258 * @param string $lang The language code to use.
7261 function fix_current_language(string $lang): void
{
7262 global $CFG, $COURSE, $SESSION, $USER;
7264 if (!get_string_manager()->translation_exists($lang)) {
7265 throw new coding_exception("The language pack for $lang is not available");
7270 if (!empty($SESSION->forcelang
)) {
7271 $fixglobal = $SESSION;
7272 $fixlang = 'forcelang';
7273 } else if (!empty($COURSE->id
) && $COURSE->id
!= SITEID
&& !empty($COURSE->lang
)) {
7274 $fixglobal = $COURSE;
7275 } else if (!empty($SESSION->lang
)) {
7276 $fixglobal = $SESSION;
7277 } else if (!empty($USER->lang
)) {
7279 } else if (isset($CFG->lang
)) {
7280 set_config('lang', $lang);
7284 $fixglobal->$fixlang = $lang;
7289 * Returns parent language of current active language if defined
7292 * @param string $lang null means current language
7295 function get_parent_language($lang=null) {
7297 $parentlang = get_string_manager()->get_string('parentlanguage', 'langconfig', null, $lang);
7299 if ($parentlang === 'en') {
7307 * Force the current language to get strings and dates localised in the given language.
7309 * After calling this function, all strings will be provided in the given language
7310 * until this function is called again, or equivalent code is run.
7312 * @param string $language
7313 * @return string previous $SESSION->forcelang value
7315 function force_current_language($language) {
7317 $sessionforcelang = isset($SESSION->forcelang
) ?
$SESSION->forcelang
: '';
7318 if ($language !== $sessionforcelang) {
7319 // Setting forcelang to null or an empty string disables its effect.
7320 if (empty($language) ||
get_string_manager()->translation_exists($language, false)) {
7321 $SESSION->forcelang
= $language;
7325 return $sessionforcelang;
7329 * Returns current string_manager instance.
7331 * The param $forcereload is needed for CLI installer only where the string_manager instance
7332 * must be replaced during the install.php script life time.
7335 * @param bool $forcereload shall the singleton be released and new instance created instead?
7336 * @return core_string_manager
7338 function get_string_manager($forcereload=false) {
7341 static $singleton = null;
7346 if ($singleton === null) {
7347 if (empty($CFG->early_install_lang
)) {
7349 $transaliases = array();
7350 if (empty($CFG->langlist
)) {
7351 $translist = array();
7353 $translist = explode(',', $CFG->langlist
);
7354 $translist = array_map('trim', $translist);
7355 // Each language in the $CFG->langlist can has an "alias" that would substitute the default language name.
7356 foreach ($translist as $i => $value) {
7357 $parts = preg_split('/\s*\|\s*/', $value, 2);
7358 if (count($parts) == 2) {
7359 $transaliases[$parts[0]] = $parts[1];
7360 $translist[$i] = $parts[0];
7365 if (!empty($CFG->config_php_settings
['customstringmanager'])) {
7366 $classname = $CFG->config_php_settings
['customstringmanager'];
7368 if (class_exists($classname)) {
7369 $implements = class_implements($classname);
7371 if (isset($implements['core_string_manager'])) {
7372 $singleton = new $classname($CFG->langotherroot
, $CFG->langlocalroot
, $translist, $transaliases);
7376 debugging('Unable to instantiate custom string manager: class '.$classname.
7377 ' does not implement the core_string_manager interface.');
7381 debugging('Unable to instantiate custom string manager: class '.$classname.' can not be found.');
7385 $singleton = new core_string_manager_standard($CFG->langotherroot
, $CFG->langlocalroot
, $translist, $transaliases);
7388 $singleton = new core_string_manager_install();
7396 * Returns a localized string.
7398 * Returns the translated string specified by $identifier as
7399 * for $module. Uses the same format files as STphp.
7400 * $a is an object, string or number that can be used
7401 * within translation strings
7403 * eg 'hello {$a->firstname} {$a->lastname}'
7406 * If you would like to directly echo the localized string use
7407 * the function {@link print_string()}
7409 * Example usage of this function involves finding the string you would
7410 * like a local equivalent of and using its identifier and module information
7411 * to retrieve it.<br/>
7412 * If you open moodle/lang/en/moodle.php and look near line 278
7413 * you will find a string to prompt a user for their word for 'course'
7415 * $string['course'] = 'Course';
7417 * So if you want to display the string 'Course'
7418 * in any language that supports it on your site
7419 * you just need to use the identifier 'course'
7421 * $mystring = '<strong>'. get_string('course') .'</strong>';
7424 * If the string you want is in another file you'd take a slightly
7425 * different approach. Looking in moodle/lang/en/calendar.php you find
7428 * $string['typecourse'] = 'Course event';
7430 * If you want to display the string "Course event" in any language
7431 * supported you would use the identifier 'typecourse' and the module 'calendar'
7432 * (because it is in the file calendar.php):
7434 * $mystring = '<h1>'. get_string('typecourse', 'calendar') .'</h1>';
7437 * As a last resort, should the identifier fail to map to a string
7438 * the returned string will be [[ $identifier ]]
7440 * In Moodle 2.3 there is a new argument to this function $lazyload.
7441 * Setting $lazyload to true causes get_string to return a lang_string object
7442 * rather than the string itself. The fetching of the string is then put off until
7443 * the string object is first used. The object can be used by calling it's out
7444 * method or by casting the object to a string, either directly e.g.
7445 * (string)$stringobject
7446 * or indirectly by using the string within another string or echoing it out e.g.
7447 * echo $stringobject
7448 * return "<p>{$stringobject}</p>";
7449 * It is worth noting that using $lazyload and attempting to use the string as an
7450 * array key will cause a fatal error as objects cannot be used as array keys.
7451 * But you should never do that anyway!
7452 * For more information {@link lang_string}
7455 * @param string $identifier The key identifier for the localized string
7456 * @param string $component The module where the key identifier is stored,
7457 * usually expressed as the filename in the language pack without the
7458 * .php on the end but can also be written as mod/forum or grade/export/xls.
7459 * If none is specified then moodle.php is used.
7460 * @param string|object|array|int $a An object, string or number that can be used
7461 * within translation strings
7462 * @param bool $lazyload If set to true a string object is returned instead of
7463 * the string itself. The string then isn't calculated until it is first used.
7464 * @return string The localized string.
7465 * @throws coding_exception
7467 function get_string($identifier, $component = '', $a = null, $lazyload = false) {
7470 // If the lazy load argument has been supplied return a lang_string object
7472 // We need to make sure it is true (and a bool) as you will see below there
7473 // used to be a forth argument at one point.
7474 if ($lazyload === true) {
7475 return new lang_string($identifier, $component, $a);
7478 if ($CFG->debugdeveloper
&& clean_param($identifier, PARAM_STRINGID
) === '') {
7479 throw new coding_exception('Invalid string identifier. The identifier cannot be empty. Please fix your get_string() call.', DEBUG_DEVELOPER
);
7482 // There is now a forth argument again, this time it is a boolean however so
7483 // we can still check for the old extralocations parameter.
7484 if (!is_bool($lazyload) && !empty($lazyload)) {
7485 debugging('extralocations parameter in get_string() is not supported any more, please use standard lang locations only.');
7488 if (strpos((string)$component, '/') !== false) {
7489 debugging('The module name you passed to get_string is the deprecated format ' .
7490 'like mod/mymod or block/myblock. The correct form looks like mymod, or block_myblock.' , DEBUG_DEVELOPER
);
7491 $componentpath = explode('/', $component);
7493 switch ($componentpath[0]) {
7495 $component = $componentpath[1];
7499 $component = 'block_'.$componentpath[1];
7502 $component = 'enrol_'.$componentpath[1];
7505 $component = 'format_'.$componentpath[1];
7508 $component = 'grade'.$componentpath[1].'_'.$componentpath[2];
7513 $result = get_string_manager()->get_string($identifier, $component, $a);
7515 // Debugging feature lets you display string identifier and component.
7516 if (isset($CFG->debugstringids
) && $CFG->debugstringids
&& optional_param('strings', 0, PARAM_INT
)) {
7517 $result .= ' {' . $identifier . '/' . $component . '}';
7523 * Converts an array of strings to their localized value.
7525 * @param array $array An array of strings
7526 * @param string $component The language module that these strings can be found in.
7527 * @return stdClass translated strings.
7529 function get_strings($array, $component = '') {
7530 $string = new stdClass
;
7531 foreach ($array as $item) {
7532 $string->$item = get_string($item, $component);
7538 * Prints out a translated string.
7540 * Prints out a translated string using the return value from the {@link get_string()} function.
7542 * Example usage of this function when the string is in the moodle.php file:<br/>
7545 * print_string('course');
7549 * Example usage of this function when the string is not in the moodle.php file:<br/>
7552 * print_string('typecourse', 'calendar');
7557 * @param string $identifier The key identifier for the localized string
7558 * @param string $component The module where the key identifier is stored. If none is specified then moodle.php is used.
7559 * @param string|object|array $a An object, string or number that can be used within translation strings
7561 function print_string($identifier, $component = '', $a = null) {
7562 echo get_string($identifier, $component, $a);
7566 * Returns a list of charset codes
7568 * Returns a list of charset codes. It's hardcoded, so they should be added manually
7569 * (checking that such charset is supported by the texlib library!)
7571 * @return array And associative array with contents in the form of charset => charset
7573 function get_list_of_charsets() {
7576 'EUC-JP' => 'EUC-JP',
7577 'ISO-2022-JP'=> 'ISO-2022-JP',
7578 'ISO-8859-1' => 'ISO-8859-1',
7579 'SHIFT-JIS' => 'SHIFT-JIS',
7580 'GB2312' => 'GB2312',
7581 'GB18030' => 'GB18030', // GB18030 not supported by typo and mbstring.
7582 'UTF-8' => 'UTF-8');
7590 * Returns a list of valid and compatible themes
7594 function get_list_of_themes() {
7599 if (!empty($CFG->themelist
)) { // Use admin's list of themes.
7600 $themelist = explode(',', $CFG->themelist
);
7602 $themelist = array_keys(core_component
::get_plugin_list("theme"));
7605 foreach ($themelist as $key => $themename) {
7606 $theme = theme_config
::load($themename);
7607 $themes[$themename] = $theme;
7610 core_collator
::asort_objects_by_method($themes, 'get_theme_name');
7616 * Factory function for emoticon_manager
7618 * @return emoticon_manager singleton
7620 function get_emoticon_manager() {
7621 static $singleton = null;
7623 if (is_null($singleton)) {
7624 $singleton = new emoticon_manager();
7631 * Provides core support for plugins that have to deal with emoticons (like HTML editor or emoticon filter).
7633 * Whenever this manager mentiones 'emoticon object', the following data
7634 * structure is expected: stdClass with properties text, imagename, imagecomponent,
7635 * altidentifier and altcomponent
7637 * @see admin_setting_emoticons
7639 * @copyright 2010 David Mudrak
7640 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
7642 class emoticon_manager
{
7645 * Returns the currently enabled emoticons
7647 * @param boolean $selectable - If true, only return emoticons that should be selectable from a list.
7648 * @return array of emoticon objects
7650 public function get_emoticons($selectable = false) {
7652 $notselectable = ['martin', 'egg'];
7654 if (empty($CFG->emoticons
)) {
7658 $emoticons = $this->decode_stored_config($CFG->emoticons
);
7660 if (!is_array($emoticons)) {
7661 // Something is wrong with the format of stored setting.
7662 debugging('Invalid format of emoticons setting, please resave the emoticons settings form', DEBUG_NORMAL
);
7666 foreach ($emoticons as $index => $emote) {
7667 if (in_array($emote->altidentifier
, $notselectable)) {
7669 unset($emoticons[$index]);
7678 * Converts emoticon object into renderable pix_emoticon object
7680 * @param stdClass $emoticon emoticon object
7681 * @param array $attributes explicit HTML attributes to set
7682 * @return pix_emoticon
7684 public function prepare_renderable_emoticon(stdClass
$emoticon, array $attributes = array()) {
7685 $stringmanager = get_string_manager();
7686 if ($stringmanager->string_exists($emoticon->altidentifier
, $emoticon->altcomponent
)) {
7687 $alt = get_string($emoticon->altidentifier
, $emoticon->altcomponent
);
7689 $alt = s($emoticon->text
);
7691 return new pix_emoticon($emoticon->imagename
, $alt, $emoticon->imagecomponent
, $attributes);
7695 * Encodes the array of emoticon objects into a string storable in config table
7697 * @see self::decode_stored_config()
7698 * @param array $emoticons array of emtocion objects
7701 public function encode_stored_config(array $emoticons) {
7702 return json_encode($emoticons);
7706 * Decodes the string into an array of emoticon objects
7708 * @see self::encode_stored_config()
7709 * @param string $encoded
7710 * @return array|null
7712 public function decode_stored_config($encoded) {
7713 $decoded = json_decode($encoded);
7714 if (!is_array($decoded)) {
7721 * Returns default set of emoticons supported by Moodle
7723 * @return array of sdtClasses
7725 public function default_emoticons() {
7727 $this->prepare_emoticon_object(":-)", 's/smiley', 'smiley'),
7728 $this->prepare_emoticon_object(":)", 's/smiley', 'smiley'),
7729 $this->prepare_emoticon_object(":-D", 's/biggrin', 'biggrin'),
7730 $this->prepare_emoticon_object(";-)", 's/wink', 'wink'),
7731 $this->prepare_emoticon_object(":-/", 's/mixed', 'mixed'),
7732 $this->prepare_emoticon_object("V-.", 's/thoughtful', 'thoughtful'),
7733 $this->prepare_emoticon_object(":-P", 's/tongueout', 'tongueout'),
7734 $this->prepare_emoticon_object(":-p", 's/tongueout', 'tongueout'),
7735 $this->prepare_emoticon_object("B-)", 's/cool', 'cool'),
7736 $this->prepare_emoticon_object("^-)", 's/approve', 'approve'),
7737 $this->prepare_emoticon_object("8-)", 's/wideeyes', 'wideeyes'),
7738 $this->prepare_emoticon_object(":o)", 's/clown', 'clown'),
7739 $this->prepare_emoticon_object(":-(", 's/sad', 'sad'),
7740 $this->prepare_emoticon_object(":(", 's/sad', 'sad'),
7741 $this->prepare_emoticon_object("8-.", 's/shy', 'shy'),
7742 $this->prepare_emoticon_object(":-I", 's/blush', 'blush'),
7743 $this->prepare_emoticon_object(":-X", 's/kiss', 'kiss'),
7744 $this->prepare_emoticon_object("8-o", 's/surprise', 'surprise'),
7745 $this->prepare_emoticon_object("P-|", 's/blackeye', 'blackeye'),
7746 $this->prepare_emoticon_object("8-[", 's/angry', 'angry'),
7747 $this->prepare_emoticon_object("(grr)", 's/angry', 'angry'),
7748 $this->prepare_emoticon_object("xx-P", 's/dead', 'dead'),
7749 $this->prepare_emoticon_object("|-.", 's/sleepy', 'sleepy'),
7750 $this->prepare_emoticon_object("}-]", 's/evil', 'evil'),
7751 $this->prepare_emoticon_object("(h)", 's/heart', 'heart'),
7752 $this->prepare_emoticon_object("(heart)", 's/heart', 'heart'),
7753 $this->prepare_emoticon_object("(y)", 's/yes', 'yes', 'core'),
7754 $this->prepare_emoticon_object("(n)", 's/no', 'no', 'core'),
7755 $this->prepare_emoticon_object("(martin)", 's/martin', 'martin'),
7756 $this->prepare_emoticon_object("( )", 's/egg', 'egg'),
7761 * Helper method preparing the stdClass with the emoticon properties
7763 * @param string|array $text or array of strings
7764 * @param string $imagename to be used by {@link pix_emoticon}
7765 * @param string $altidentifier alternative string identifier, null for no alt
7766 * @param string $altcomponent where the alternative string is defined
7767 * @param string $imagecomponent to be used by {@link pix_emoticon}
7770 protected function prepare_emoticon_object($text, $imagename, $altidentifier = null,
7771 $altcomponent = 'core_pix', $imagecomponent = 'core') {
7772 return (object)array(
7774 'imagename' => $imagename,
7775 'imagecomponent' => $imagecomponent,
7776 'altidentifier' => $altidentifier,
7777 'altcomponent' => $altcomponent,
7787 * @param string $data Data to encrypt.
7788 * @return string The now encrypted data.
7790 function rc4encrypt($data) {
7791 return endecrypt(get_site_identifier(), $data, '');
7797 * @param string $data Data to decrypt.
7798 * @return string The now decrypted data.
7800 function rc4decrypt($data) {
7801 return endecrypt(get_site_identifier(), $data, 'de');
7805 * Based on a class by Mukul Sabharwal [mukulsabharwal @ yahoo.com]
7807 * @todo Finish documenting this function
7809 * @param string $pwd The password to use when encrypting or decrypting
7810 * @param string $data The data to be decrypted/encrypted
7811 * @param string $case Either 'de' for decrypt or '' for encrypt
7814 function endecrypt ($pwd, $data, $case) {
7816 if ($case == 'de') {
7817 $data = urldecode($data);
7822 $pwdlength = strlen($pwd);
7824 for ($i = 0; $i <= 255; $i++
) {
7825 $key[$i] = ord(substr($pwd, ($i %
$pwdlength), 1));
7831 for ($i = 0; $i <= 255; $i++
) {
7832 $x = ($x +
$box[$i] +
$key[$i]) %
256;
7833 $tempswap = $box[$i];
7834 $box[$i] = $box[$x];
7835 $box[$x] = $tempswap;
7843 for ($i = 0; $i < strlen($data); $i++
) {
7844 $a = ($a +
1) %
256;
7845 $j = ($j +
$box[$a]) %
256;
7847 $box[$a] = $box[$j];
7849 $k = $box[(($box[$a] +
$box[$j]) %
256)];
7850 $cipherby = ord(substr($data, $i, 1)) ^
$k;
7851 $cipher .= chr($cipherby);
7854 if ($case == 'de') {
7855 $cipher = urldecode(urlencode($cipher));
7857 $cipher = urlencode($cipher);
7863 // ENVIRONMENT CHECKING.
7866 * This method validates a plug name. It is much faster than calling clean_param.
7868 * @param string $name a string that might be a plugin name.
7869 * @return bool if this string is a valid plugin name.
7871 function is_valid_plugin_name($name) {
7872 // This does not work for 'mod', bad luck, use any other type.
7873 return core_component
::is_valid_plugin_name('tool', $name);
7877 * Get a list of all the plugins of a given type that define a certain API function
7878 * in a certain file. The plugin component names and function names are returned.
7880 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
7881 * @param string $function the part of the name of the function after the
7882 * frankenstyle prefix. e.g 'hook' if you are looking for functions with
7883 * names like report_courselist_hook.
7884 * @param string $file the name of file within the plugin that defines the
7885 * function. Defaults to lib.php.
7886 * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
7887 * and the function names as values (e.g. 'report_courselist_hook', 'forum_hook').
7889 function get_plugin_list_with_function($plugintype, $function, $file = 'lib.php') {
7892 // We don't include here as all plugin types files would be included.
7893 $plugins = get_plugins_with_function($function, $file, false);
7895 if (empty($plugins[$plugintype])) {
7899 $allplugins = core_component
::get_plugin_list($plugintype);
7901 // Reformat the array and include the files.
7902 $pluginfunctions = array();
7903 foreach ($plugins[$plugintype] as $pluginname => $functionname) {
7905 // Check that it has not been removed and the file is still available.
7906 if (!empty($allplugins[$pluginname])) {
7908 $filepath = $allplugins[$pluginname] . DIRECTORY_SEPARATOR
. $file;
7909 if (file_exists($filepath)) {
7910 include_once($filepath);
7912 // Now that the file is loaded, we must verify the function still exists.
7913 if (function_exists($functionname)) {
7914 $pluginfunctions[$plugintype . '_' . $pluginname] = $functionname;
7916 // Invalidate the cache for next run.
7917 \cache_helper
::invalidate_by_definition('core', 'plugin_functions');
7923 return $pluginfunctions;
7927 * Get a list of all the plugins that define a certain API function in a certain file.
7929 * @param string $function the part of the name of the function after the
7930 * frankenstyle prefix. e.g 'hook' if you are looking for functions with
7931 * names like report_courselist_hook.
7932 * @param string $file the name of file within the plugin that defines the
7933 * function. Defaults to lib.php.
7934 * @param bool $include Whether to include the files that contain the functions or not.
7935 * @param bool $migratedtohook if true this is a deprecated lib.php callback, if hook callback is present then do nothing
7936 * @return array with [plugintype][plugin] = functionname
7938 function get_plugins_with_function($function, $file = 'lib.php', $include = true, bool $migratedtohook = false) {
7941 if (during_initial_install() ||
isset($CFG->upgraderunning
)) {
7942 // API functions _must not_ be called during an installation or upgrade.
7946 $plugincallback = $function;
7947 $filtermigrated = function($plugincallback, $pluginfunctions): array {
7948 foreach ($pluginfunctions as $plugintype => $plugins) {
7949 foreach ($plugins as $plugin => $unusedfunction) {
7950 $component = $plugintype . '_' . $plugin;
7951 if (\core\hook\manager
::get_instance()->is_deprecated_plugin_callback($plugincallback)) {
7952 if (\core\hook\manager
::get_instance()->is_deprecating_hook_present($component, $plugincallback)) {
7953 // Ignore the old callback, it is there only for older Moodle versions.
7954 unset($pluginfunctions[$plugintype][$plugin]);
7956 debugging("Callback $plugincallback in $component component should be migrated to new hook callback",
7962 return $pluginfunctions;
7965 $cache = \cache
::make('core', 'plugin_functions');
7967 // Including both although I doubt that we will find two functions definitions with the same name.
7968 // Clean the filename as cache_helper::hash_key only allows a-zA-Z0-9_.
7969 $pluginfunctions = false;
7970 if (!empty($CFG->allversionshash
)) {
7971 $key = $CFG->allversionshash
. '_' . $function . '_' . clean_param($file, PARAM_ALPHA
);
7972 $pluginfunctions = $cache->get($key);
7976 // Use the plugin manager to check that plugins are currently installed.
7977 $pluginmanager = \core_plugin_manager
::instance();
7979 if ($pluginfunctions !== false) {
7981 // Checking that the files are still available.
7982 foreach ($pluginfunctions as $plugintype => $plugins) {
7984 $allplugins = \core_component
::get_plugin_list($plugintype);
7985 $installedplugins = $pluginmanager->get_installed_plugins($plugintype);
7986 foreach ($plugins as $plugin => $function) {
7987 if (!isset($installedplugins[$plugin])) {
7988 // Plugin code is still present on disk but it is not installed.
7993 // Cache might be out of sync with the codebase, skip the plugin if it is not available.
7994 if (empty($allplugins[$plugin])) {
7999 $fileexists = file_exists($allplugins[$plugin] . DIRECTORY_SEPARATOR
. $file);
8000 if ($include && $fileexists) {
8001 // Include the files if it was requested.
8002 include_once($allplugins[$plugin] . DIRECTORY_SEPARATOR
. $file);
8003 } else if (!$fileexists) {
8004 // If the file is not available any more it should not be returned.
8009 // Check if the function still exists in the file.
8010 if ($include && !function_exists($function)) {
8017 // If the cache is dirty, we should fall through and let it rebuild.
8019 if ($migratedtohook && $file === 'lib.php') {
8020 $pluginfunctions = $filtermigrated($plugincallback, $pluginfunctions);
8022 return $pluginfunctions;
8026 $pluginfunctions = array();
8028 // To fill the cached. Also, everything should continue working with cache disabled.
8029 $plugintypes = \core_component
::get_plugin_types();
8030 foreach ($plugintypes as $plugintype => $unused) {
8032 // We need to include files here.
8033 $pluginswithfile = \core_component
::get_plugin_list_with_file($plugintype, $file, true);
8034 $installedplugins = $pluginmanager->get_installed_plugins($plugintype);
8035 foreach ($pluginswithfile as $plugin => $notused) {
8037 if (!isset($installedplugins[$plugin])) {
8041 $fullfunction = $plugintype . '_' . $plugin . '_' . $function;
8043 $pluginfunction = false;
8044 if (function_exists($fullfunction)) {
8045 // Function exists with standard name. Store, indexed by frankenstyle name of plugin.
8046 $pluginfunction = $fullfunction;
8048 } else if ($plugintype === 'mod') {
8049 // For modules, we also allow plugin without full frankenstyle but just starting with the module name.
8050 $shortfunction = $plugin . '_' . $function;
8051 if (function_exists($shortfunction)) {
8052 $pluginfunction = $shortfunction;
8056 if ($pluginfunction) {
8057 if (empty($pluginfunctions[$plugintype])) {
8058 $pluginfunctions[$plugintype] = array();
8060 $pluginfunctions[$plugintype][$plugin] = $pluginfunction;
8065 if (!empty($CFG->allversionshash
)) {
8066 $cache->set($key, $pluginfunctions);
8069 if ($migratedtohook && $file === 'lib.php') {
8070 $pluginfunctions = $filtermigrated($plugincallback, $pluginfunctions);
8073 return $pluginfunctions;
8078 * Lists plugin-like directories within specified directory
8080 * This function was originally used for standard Moodle plugins, please use
8081 * new core_component::get_plugin_list() now.
8083 * This function is used for general directory listing and backwards compatility.
8085 * @param string $directory relative directory from root
8086 * @param string $exclude dir name to exclude from the list (defaults to none)
8087 * @param string $basedir full path to the base dir where $plugin resides (defaults to $CFG->dirroot)
8088 * @return array Sorted array of directory names found under the requested parameters
8090 function get_list_of_plugins($directory='mod', $exclude='', $basedir='') {
8095 if (empty($basedir)) {
8096 $basedir = $CFG->dirroot
.'/'. $directory;
8099 $basedir = $basedir .'/'. $directory;
8102 if ($CFG->debugdeveloper
and empty($exclude)) {
8103 // Make sure devs do not use this to list normal plugins,
8104 // this is intended for general directories that are not plugins!
8106 $subtypes = core_component
::get_plugin_types();
8107 if (in_array($basedir, $subtypes)) {
8108 debugging('get_list_of_plugins() should not be used to list real plugins, use core_component::get_plugin_list() instead!', DEBUG_DEVELOPER
);
8113 $ignorelist = array_flip(array_filter([
8125 if (file_exists($basedir) && filetype($basedir) == 'dir') {
8126 if (!$dirhandle = opendir($basedir)) {
8127 debugging("Directory permission error for plugin ({$directory}). Directory exists but cannot be read.", DEBUG_DEVELOPER
);
8130 while (false !== ($dir = readdir($dirhandle))) {
8131 if (strpos($dir, '.') === 0) {
8132 // Ignore directories starting with .
8133 // These are treated as hidden directories.
8136 if (array_key_exists($dir, $ignorelist)) {
8137 // This directory features on the ignore list.
8140 if (filetype($basedir .'/'. $dir) != 'dir') {
8145 closedir($dirhandle);
8154 * Invoke plugin's callback functions
8156 * @param string $type plugin type e.g. 'mod'
8157 * @param string $name plugin name
8158 * @param string $feature feature name
8159 * @param string $action feature's action
8160 * @param array $params parameters of callback function, should be an array
8161 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
8162 * @param bool $migratedtohook if true this is a deprecated callback, if hook callback is present then do nothing
8165 * @todo Decide about to deprecate and drop plugin_callback() - MDL-30743
8167 function plugin_callback($type, $name, $feature, $action, $params = null, $default = null, bool $migratedtohook = false) {
8168 return component_callback($type . '_' . $name, $feature . '_' . $action, (array) $params, $default, $migratedtohook);
8172 * Invoke component's callback functions
8174 * @param string $component frankenstyle component name, e.g. 'mod_quiz'
8175 * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
8176 * @param array $params parameters of callback function
8177 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
8178 * @param bool $migratedtohook if true this is a deprecated callback, if hook callback is present then do nothing
8181 function component_callback($component, $function, array $params = array(), $default = null, bool $migratedtohook = false) {
8183 $functionname = component_callback_exists($component, $function);
8185 if ($params && (array_keys($params) !== range(0, count($params) - 1))) {
8186 // PHP 8 allows to have associative arrays in the call_user_func_array() parameters but
8187 // PHP 7 does not. Using associative arrays can result in different behavior in different PHP versions.
8188 // See https://php.watch/versions/8.0/named-parameters#named-params-call_user_func_array
8189 // This check can be removed when minimum PHP version for Moodle is raised to 8.
8190 debugging('Parameters array can not be an associative array while Moodle supports both PHP 7 and PHP 8.',
8192 $params = array_values($params);
8195 if ($functionname) {
8196 if ($migratedtohook) {
8197 if (\core\hook\manager
::get_instance()->is_deprecated_plugin_callback($function)) {
8198 if (\core\hook\manager
::get_instance()->is_deprecating_hook_present($component, $function)) {
8199 // Do not call the old lib.php callback,
8200 // it is there for compatibility with older Moodle versions only.
8203 debugging("Callback $function in $component component should be migrated to new hook callback",
8209 // Function exists, so just return function result.
8210 $ret = call_user_func_array($functionname, $params);
8211 if (is_null($ret)) {
8221 * Determine if a component callback exists and return the function name to call. Note that this
8222 * function will include the required library files so that the functioname returned can be
8225 * @param string $component frankenstyle component name, e.g. 'mod_quiz'
8226 * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
8227 * @return mixed Complete function name to call if the callback exists or false if it doesn't.
8228 * @throws coding_exception if invalid component specfied
8230 function component_callback_exists($component, $function) {
8231 global $CFG; // This is needed for the inclusions.
8233 $cleancomponent = clean_param($component, PARAM_COMPONENT
);
8234 if (empty($cleancomponent)) {
8235 throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
8237 $component = $cleancomponent;
8239 list($type, $name) = core_component
::normalize_component($component);
8240 $component = $type . '_' . $name;
8242 $oldfunction = $name.'_'.$function;
8243 $function = $component.'_'.$function;
8245 $dir = core_component
::get_component_directory($component);
8247 throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
8250 // Load library and look for function.
8251 if (file_exists($dir.'/lib.php')) {
8252 require_once($dir.'/lib.php');
8255 if (!function_exists($function) and function_exists($oldfunction)) {
8256 if ($type !== 'mod' and $type !== 'core') {
8257 debugging("Please use new function name $function instead of legacy $oldfunction", DEBUG_DEVELOPER
);
8259 $function = $oldfunction;
8262 if (function_exists($function)) {
8269 * Call the specified callback method on the provided class.
8271 * If the callback returns null, then the default value is returned instead.
8272 * If the class does not exist, then the default value is returned.
8274 * @param string $classname The name of the class to call upon.
8275 * @param string $methodname The name of the staticically defined method on the class.
8276 * @param array $params The arguments to pass into the method.
8277 * @param mixed $default The default value.
8278 * @return mixed The return value.
8280 function component_class_callback($classname, $methodname, array $params, $default = null) {
8281 if (!class_exists($classname)) {
8285 if (!method_exists($classname, $methodname)) {
8289 $fullfunction = $classname . '::' . $methodname;
8290 $result = call_user_func_array($fullfunction, $params);
8292 if (null === $result) {
8300 * Checks whether a plugin supports a specified feature.
8302 * @param string $type Plugin type e.g. 'mod'
8303 * @param string $name Plugin name e.g. 'forum'
8304 * @param string $feature Feature code (FEATURE_xx constant)
8305 * @param mixed $default default value if feature support unknown
8306 * @return mixed Feature result (false if not supported, null if feature is unknown,
8307 * otherwise usually true but may have other feature-specific value such as array)
8308 * @throws coding_exception
8310 function plugin_supports($type, $name, $feature, $default = null) {
8313 if ($type === 'mod' and $name === 'NEWMODULE') {
8314 // Somebody forgot to rename the module template.
8318 $component = clean_param($type . '_' . $name, PARAM_COMPONENT
);
8319 if (empty($component)) {
8320 throw new coding_exception('Invalid component used in plugin_supports():' . $type . '_' . $name);
8325 if ($type === 'mod') {
8326 // We need this special case because we support subplugins in modules,
8327 // otherwise it would end up in infinite loop.
8328 if (file_exists("$CFG->dirroot/mod/$name/lib.php")) {
8329 include_once("$CFG->dirroot/mod/$name/lib.php");
8330 $function = $component.'_supports';
8331 if (!function_exists($function)) {
8332 // Legacy non-frankenstyle function name.
8333 $function = $name.'_supports';
8338 if (!$path = core_component
::get_plugin_directory($type, $name)) {
8339 // Non existent plugin type.
8342 if (file_exists("$path/lib.php")) {
8343 include_once("$path/lib.php");
8344 $function = $component.'_supports';
8348 if ($function and function_exists($function)) {
8349 $supports = $function($feature);
8350 if (is_null($supports)) {
8351 // Plugin does not know - use default.
8358 // Plugin does not care, so use default.
8363 * Returns true if the current version of PHP is greater that the specified one.
8365 * @todo Check PHP version being required here is it too low?
8367 * @param string $version The version of php being tested.
8370 function check_php_version($version='5.2.4') {
8371 return (version_compare(phpversion(), $version) >= 0);
8375 * Determine if moodle installation requires update.
8377 * Checks version numbers of main code and all plugins to see
8378 * if there are any mismatches.
8380 * @param bool $checkupgradeflag check the outagelessupgrade flag to see if an upgrade is running.
8383 function moodle_needs_upgrading($checkupgradeflag = true) {
8386 // Say no if there is already an upgrade running.
8387 if ($checkupgradeflag) {
8388 $lock = $DB->get_field('config', 'value', ['name' => 'outagelessupgrade']);
8389 $currentprocessrunningupgrade = (defined('CLI_UPGRADE_RUNNING') && CLI_UPGRADE_RUNNING
);
8390 // If we ARE locked, but this PHP process is NOT the process running the upgrade,
8391 // We should always return false.
8392 // This means the upgrade is running from CLI somewhere, or about to.
8393 if (!empty($lock) && !$currentprocessrunningupgrade) {
8398 if (empty($CFG->version
)) {
8402 // There is no need to purge plugininfo caches here because
8403 // these caches are not used during upgrade and they are purged after
8406 if (empty($CFG->allversionshash
)) {
8410 $hash = core_component
::get_all_versions_hash();
8412 return ($hash !== $CFG->allversionshash
);
8416 * Returns the major version of this site
8418 * Moodle version numbers consist of three numbers separated by a dot, for
8419 * example 1.9.11 or 2.0.2. The first two numbers, like 1.9 or 2.0, represent so
8420 * called major version. This function extracts the major version from either
8421 * $CFG->release (default) or eventually from the $release variable defined in
8422 * the main version.php.
8424 * @param bool $fromdisk should the version if source code files be used
8425 * @return string|false the major version like '2.3', false if could not be determined
8427 function moodle_major_version($fromdisk = false) {
8432 require($CFG->dirroot
.'/version.php');
8433 if (empty($release)) {
8438 if (empty($CFG->release
)) {
8441 $release = $CFG->release
;
8444 if (preg_match('/^[0-9]+\.[0-9]+/', $release, $matches)) {
8454 * Gets the system locale
8456 * @return string Retuns the current locale.
8458 function moodle_getlocale() {
8461 // Fetch the correct locale based on ostype.
8462 if ($CFG->ostype
== 'WINDOWS') {
8463 $stringtofetch = 'localewin';
8465 $stringtofetch = 'locale';
8468 if (!empty($CFG->locale
)) { // Override locale for all language packs.
8469 return $CFG->locale
;
8472 return get_string($stringtofetch, 'langconfig');
8476 * Sets the system locale
8479 * @param string $locale Can be used to force a locale
8481 function moodle_setlocale($locale='') {
8484 static $currentlocale = ''; // Last locale caching.
8486 $oldlocale = $currentlocale;
8488 // The priority is the same as in get_string() - parameter, config, course, session, user, global language.
8489 if (!empty($locale)) {
8490 $currentlocale = $locale;
8492 $currentlocale = moodle_getlocale();
8495 // Do nothing if locale already set up.
8496 if ($oldlocale == $currentlocale) {
8500 // Due to some strange BUG we cannot set the LC_TIME directly, so we fetch current values,
8501 // set LC_ALL and then set values again. Just wondering why we cannot set LC_ALL only??? - stronk7
8502 // Some day, numeric, monetary and other categories should be set too, I think. :-/.
8504 // Get current values.
8505 $monetary= setlocale (LC_MONETARY
, 0);
8506 $numeric = setlocale (LC_NUMERIC
, 0);
8507 $ctype = setlocale (LC_CTYPE
, 0);
8508 if ($CFG->ostype
!= 'WINDOWS') {
8509 $messages= setlocale (LC_MESSAGES
, 0);
8511 // Set locale to all.
8512 $result = setlocale (LC_ALL
, $currentlocale);
8513 // If setting of locale fails try the other utf8 or utf-8 variant,
8514 // some operating systems support both (Debian), others just one (OSX).
8515 if ($result === false) {
8516 if (stripos($currentlocale, '.UTF-8') !== false) {
8517 $newlocale = str_ireplace('.UTF-8', '.UTF8', $currentlocale);
8518 setlocale (LC_ALL
, $newlocale);
8519 } else if (stripos($currentlocale, '.UTF8') !== false) {
8520 $newlocale = str_ireplace('.UTF8', '.UTF-8', $currentlocale);
8521 setlocale (LC_ALL
, $newlocale);
8525 setlocale (LC_MONETARY
, $monetary);
8526 setlocale (LC_NUMERIC
, $numeric);
8527 if ($CFG->ostype
!= 'WINDOWS') {
8528 setlocale (LC_MESSAGES
, $messages);
8530 if ($currentlocale == 'tr_TR' or $currentlocale == 'tr_TR.UTF-8') {
8531 // To workaround a well-known PHP problem with Turkish letter Ii.
8532 setlocale (LC_CTYPE
, $ctype);
8537 * Count words in a string.
8539 * Words are defined as things between whitespace.
8542 * @param string $string The text to be searched for words. May be HTML.
8543 * @param int|null $format
8544 * @return int The count of words in the specified string
8546 function count_words($string, $format = null) {
8547 // Before stripping tags, add a space after the close tag of anything that is not obviously inline.
8548 // Also, br is a special case because it definitely delimits a word, but has no close tag.
8549 $string = preg_replace('~
8550 ( # Capture the tag we match.
8551 </ # Start of close tag.
8552 (?! # Do not match any of these specific close tag names.
8553 a> | b> | del> | em> | i> |
8554 ins> | s> | small> | span> |
8555 strong> | sub> | sup> | u>
8557 \w+ # But, apart from those execptions, match any tag name.
8558 > # End of close tag.
8560 <br> | <br\s*/> # Special cases that are not close tags.
8562 ~x', '$1 ', $string); // Add a space after the close tag.
8563 if ($format !== null && $format != FORMAT_PLAIN
) {
8564 // Match the usual text cleaning before display.
8565 // Ideally we should apply multilang filter only here, other filters might add extra text.
8566 $string = format_text($string, $format, ['filter' => false, 'noclean' => false, 'para' => false]);
8568 // Now remove HTML tags.
8569 $string = strip_tags($string);
8570 // Decode HTML entities.
8571 $string = html_entity_decode($string, ENT_COMPAT
);
8573 // Now, the word count is the number of blocks of characters separated
8574 // by any sort of space. That seems to be the definition used by all other systems.
8575 // To be precise about what is considered to separate words:
8576 // * Anything that Unicode considers a 'Separator'
8577 // * Anything that Unicode considers a 'Control character'
8578 // * An em- or en- dash.
8579 return count(preg_split('~[\p{Z}\p{Cc}—–]+~u', $string, -1, PREG_SPLIT_NO_EMPTY
));
8583 * Count letters in a string.
8585 * Letters are defined as chars not in tags and different from whitespace.
8588 * @param string $string The text to be searched for letters. May be HTML.
8589 * @param int|null $format
8590 * @return int The count of letters in the specified text.
8592 function count_letters($string, $format = null) {
8593 if ($format !== null && $format != FORMAT_PLAIN
) {
8594 // Match the usual text cleaning before display.
8595 // Ideally we should apply multilang filter only here, other filters might add extra text.
8596 $string = format_text($string, $format, ['filter' => false, 'noclean' => false, 'para' => false]);
8598 $string = strip_tags($string); // Tags are out now.
8599 $string = html_entity_decode($string, ENT_COMPAT
);
8600 $string = preg_replace('/[[:space:]]*/', '', $string); // Whitespace are out now.
8602 return core_text
::strlen($string);
8606 * Generate and return a random string of the specified length.
8608 * @param int $length The length of the string to be created.
8611 function random_string($length=15) {
8612 $randombytes = random_bytes($length);
8613 $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
8614 $pool .= 'abcdefghijklmnopqrstuvwxyz';
8615 $pool .= '0123456789';
8616 $poollen = strlen($pool);
8618 for ($i = 0; $i < $length; $i++
) {
8619 $rand = ord($randombytes[$i]);
8620 $string .= substr($pool, ($rand%
($poollen)), 1);
8626 * Generate a complex random string (useful for md5 salts)
8628 * This function is based on the above {@link random_string()} however it uses a
8629 * larger pool of characters and generates a string between 24 and 32 characters
8631 * @param int $length Optional if set generates a string to exactly this length
8634 function complex_random_string($length=null) {
8635 $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
8636 $pool .= '`~!@#%^&*()_+-=[];,./<>?:{} ';
8637 $poollen = strlen($pool);
8638 if ($length===null) {
8639 $length = floor(rand(24, 32));
8641 $randombytes = random_bytes($length);
8643 for ($i = 0; $i < $length; $i++
) {
8644 $rand = ord($randombytes[$i]);
8645 $string .= $pool[($rand%
$poollen)];
8651 * Given some text (which may contain HTML) and an ideal length,
8652 * this function truncates the text neatly on a word boundary if possible
8655 * @param string $text text to be shortened
8656 * @param int $ideal ideal string length
8657 * @param boolean $exact if false, $text will not be cut mid-word
8658 * @param string $ending The string to append if the passed string is truncated
8659 * @return string $truncate shortened string
8661 function shorten_text($text, $ideal=30, $exact = false, $ending='...') {
8662 // If the plain text is shorter than the maximum length, return the whole text.
8663 if (core_text
::strlen(preg_replace('/<.*?>/', '', $text)) <= $ideal) {
8667 // Splits on HTML tags. Each open/close/empty tag will be the first thing
8668 // and only tag in its 'line'.
8669 preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER
);
8671 $totallength = core_text
::strlen($ending);
8674 // This array stores information about open and close tags and their position
8675 // in the truncated string. Each item in the array is an object with fields
8676 // ->open (true if open), ->tag (tag name in lower case), and ->pos
8677 // (byte position in truncated text).
8678 $tagdetails = array();
8680 foreach ($lines as $linematchings) {
8681 // If there is any html-tag in this line, handle it and add it (uncounted) to the output.
8682 if (!empty($linematchings[1])) {
8683 // If it's an "empty element" with or without xhtml-conform closing slash (f.e. <br/>).
8684 if (!preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $linematchings[1])) {
8685 if (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $linematchings[1], $tagmatchings)) {
8686 // Record closing tag.
8687 $tagdetails[] = (object) array(
8689 'tag' => core_text
::strtolower($tagmatchings[1]),
8690 'pos' => core_text
::strlen($truncate),
8693 } else if (preg_match('/^<\s*([^\s>!]+).*?>$/s', $linematchings[1], $tagmatchings)) {
8694 // Record opening tag.
8695 $tagdetails[] = (object) array(
8697 'tag' => core_text
::strtolower($tagmatchings[1]),
8698 'pos' => core_text
::strlen($truncate),
8700 } else if (preg_match('/^<!--\[if\s.*?\]>$/s', $linematchings[1], $tagmatchings)) {
8701 $tagdetails[] = (object) array(
8703 'tag' => core_text
::strtolower('if'),
8704 'pos' => core_text
::strlen($truncate),
8706 } else if (preg_match('/^<!--<!\[endif\]-->$/s', $linematchings[1], $tagmatchings)) {
8707 $tagdetails[] = (object) array(
8709 'tag' => core_text
::strtolower('if'),
8710 'pos' => core_text
::strlen($truncate),
8714 // Add html-tag to $truncate'd text.
8715 $truncate .= $linematchings[1];
8718 // Calculate the length of the plain text part of the line; handle entities as one character.
8719 $contentlength = core_text
::strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', ' ', $linematchings[2]));
8720 if ($totallength +
$contentlength > $ideal) {
8721 // The number of characters which are left.
8722 $left = $ideal - $totallength;
8723 $entitieslength = 0;
8724 // Search for html entities.
8725 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
)) {
8726 // Calculate the real length of all entities in the legal range.
8727 foreach ($entities[0] as $entity) {
8728 if ($entity[1]+
1-$entitieslength <= $left) {
8730 $entitieslength +
= core_text
::strlen($entity[0]);
8732 // No more characters left.
8737 $breakpos = $left +
$entitieslength;
8739 // If the words shouldn't be cut in the middle...
8741 // Search the last occurence of a space.
8742 for (; $breakpos > 0; $breakpos--) {
8743 if ($char = core_text
::substr($linematchings[2], $breakpos, 1)) {
8744 if ($char === '.' or $char === ' ') {
8747 } else if (strlen($char) > 2) {
8748 // Chinese/Japanese/Korean text can be truncated at any UTF-8 character boundary.
8755 if ($breakpos == 0) {
8756 // This deals with the test_shorten_text_no_spaces case.
8757 $breakpos = $left +
$entitieslength;
8758 } else if ($breakpos > $left +
$entitieslength) {
8759 // This deals with the previous for loop breaking on the first char.
8760 $breakpos = $left +
$entitieslength;
8763 $truncate .= core_text
::substr($linematchings[2], 0, $breakpos);
8764 // Maximum length is reached, so get off the loop.
8767 $truncate .= $linematchings[2];
8768 $totallength +
= $contentlength;
8771 // If the maximum length is reached, get off the loop.
8772 if ($totallength >= $ideal) {
8777 // Add the defined ending to the text.
8778 $truncate .= $ending;
8780 // Now calculate the list of open html tags based on the truncate position.
8781 $opentags = array();
8782 foreach ($tagdetails as $taginfo) {
8783 if ($taginfo->open
) {
8784 // Add tag to the beginning of $opentags list.
8785 array_unshift($opentags, $taginfo->tag
);
8787 // Can have multiple exact same open tags, close the last one.
8788 $pos = array_search($taginfo->tag
, array_reverse($opentags, true));
8789 if ($pos !== false) {
8790 unset($opentags[$pos]);
8795 // Close all unclosed html-tags.
8796 foreach ($opentags as $tag) {
8797 if ($tag === 'if') {
8798 $truncate .= '<!--<![endif]-->';
8800 $truncate .= '</' . $tag . '>';
8808 * Shortens a given filename by removing characters positioned after the ideal string length.
8809 * When the filename is too long, the file cannot be created on the filesystem due to exceeding max byte size.
8810 * Limiting the filename to a certain size (considering multibyte characters) will prevent this.
8812 * @param string $filename file name
8813 * @param int $length ideal string length
8814 * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness.
8815 * @return string $shortened shortened file name
8817 function shorten_filename($filename, $length = MAX_FILENAME_SIZE
, $includehash = false) {
8818 $shortened = $filename;
8819 // Extract a part of the filename if it's char size exceeds the ideal string length.
8820 if (core_text
::strlen($filename) > $length) {
8821 // Exclude extension if present in filename.
8822 $mimetypes = get_mimetypes_array();
8823 $extension = pathinfo($filename, PATHINFO_EXTENSION
);
8824 if ($extension && !empty($mimetypes[$extension])) {
8825 $basename = pathinfo($filename, PATHINFO_FILENAME
);
8826 $hash = empty($includehash) ?
'' : ' - ' . substr(sha1($basename), 0, 10);
8827 $shortened = core_text
::substr($basename, 0, $length - strlen($hash)) . $hash;
8828 $shortened .= '.' . $extension;
8830 $hash = empty($includehash) ?
'' : ' - ' . substr(sha1($filename), 0, 10);
8831 $shortened = core_text
::substr($filename, 0, $length - strlen($hash)) . $hash;
8838 * Shortens a given array of filenames by removing characters positioned after the ideal string length.
8840 * @param array $path The paths to reduce the length.
8841 * @param int $length Ideal string length
8842 * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness.
8843 * @return array $result Shortened paths in array.
8845 function shorten_filenames(array $path, $length = MAX_FILENAME_SIZE
, $includehash = false) {
8848 $result = array_reduce($path, function($carry, $singlepath) use ($length, $includehash) {
8849 $carry[] = shorten_filename($singlepath, $length, $includehash);
8857 * Given dates in seconds, how many weeks is the date from startdate
8858 * The first week is 1, the second 2 etc ...
8860 * @param int $startdate Timestamp for the start date
8861 * @param int $thedate Timestamp for the end date
8864 function getweek ($startdate, $thedate) {
8865 if ($thedate < $startdate) {
8869 return floor(($thedate - $startdate) / WEEKSECS
) +
1;
8873 * Returns a randomly generated password of length $maxlen. inspired by
8875 * {@link http://www.phpbuilder.com/columns/jesus19990502.php3} and
8876 * {@link http://es2.php.net/manual/en/function.str-shuffle.php#73254}
8878 * @param int $maxlen The maximum size of the password being generated.
8881 function generate_password($maxlen=10) {
8884 if (empty($CFG->passwordpolicy
)) {
8885 $fillers = PASSWORD_DIGITS
;
8886 $wordlist = file($CFG->wordlist
);
8887 $word1 = trim($wordlist[rand(0, count($wordlist) - 1)]);
8888 $word2 = trim($wordlist[rand(0, count($wordlist) - 1)]);
8889 $filler1 = $fillers[rand(0, strlen($fillers) - 1)];
8890 $password = $word1 . $filler1 . $word2;
8892 $minlen = !empty($CFG->minpasswordlength
) ?
$CFG->minpasswordlength
: 0;
8893 $digits = $CFG->minpassworddigits
;
8894 $lower = $CFG->minpasswordlower
;
8895 $upper = $CFG->minpasswordupper
;
8896 $nonalphanum = $CFG->minpasswordnonalphanum
;
8897 $total = $lower +
$upper +
$digits +
$nonalphanum;
8898 // Var minlength should be the greater one of the two ( $minlen and $total ).
8899 $minlen = $minlen < $total ?
$total : $minlen;
8900 // Var maxlen can never be smaller than minlen.
8901 $maxlen = $minlen > $maxlen ?
$minlen : $maxlen;
8902 $additional = $maxlen - $total;
8904 // Make sure we have enough characters to fulfill
8905 // complexity requirements.
8906 $passworddigits = PASSWORD_DIGITS
;
8907 while ($digits > strlen($passworddigits)) {
8908 $passworddigits .= PASSWORD_DIGITS
;
8910 $passwordlower = PASSWORD_LOWER
;
8911 while ($lower > strlen($passwordlower)) {
8912 $passwordlower .= PASSWORD_LOWER
;
8914 $passwordupper = PASSWORD_UPPER
;
8915 while ($upper > strlen($passwordupper)) {
8916 $passwordupper .= PASSWORD_UPPER
;
8918 $passwordnonalphanum = PASSWORD_NONALPHANUM
;
8919 while ($nonalphanum > strlen($passwordnonalphanum)) {
8920 $passwordnonalphanum .= PASSWORD_NONALPHANUM
;
8923 // Now mix and shuffle it all.
8924 $password = str_shuffle (substr(str_shuffle ($passwordlower), 0, $lower) .
8925 substr(str_shuffle ($passwordupper), 0, $upper) .
8926 substr(str_shuffle ($passworddigits), 0, $digits) .
8927 substr(str_shuffle ($passwordnonalphanum), 0 , $nonalphanum) .
8928 substr(str_shuffle ($passwordlower .
8931 $passwordnonalphanum), 0 , $additional));
8934 return substr ($password, 0, $maxlen);
8938 * Given a float, prints it nicely.
8939 * Localized floats must not be used in calculations!
8941 * The stripzeros feature is intended for making numbers look nicer in small
8942 * areas where it is not necessary to indicate the degree of accuracy by showing
8943 * ending zeros. If you turn it on with $decimalpoints set to 3, for example,
8944 * then it will display '5.4' instead of '5.400' or '5' instead of '5.000'.
8946 * @param float $float The float to print
8947 * @param int $decimalpoints The number of decimal places to print. -1 is a special value for auto detect (full precision).
8948 * @param bool $localized use localized decimal separator
8949 * @param bool $stripzeros If true, removes final zeros after decimal point. It will be ignored and the trailing zeros after
8950 * the decimal point are always striped if $decimalpoints is -1.
8951 * @return string locale float
8953 function format_float($float, $decimalpoints=1, $localized=true, $stripzeros=false) {
8954 if (is_null($float)) {
8958 $separator = get_string('decsep', 'langconfig');
8962 if ($decimalpoints == -1) {
8963 // The following counts the number of decimals.
8964 // It is safe as both floatval() and round() functions have same behaviour when non-numeric values are provided.
8965 $floatval = floatval($float);
8966 for ($decimalpoints = 0; $floatval != round($float, $decimalpoints); $decimalpoints++
);
8969 $result = number_format($float, $decimalpoints, $separator, '');
8970 if ($stripzeros && $decimalpoints > 0) {
8971 // Remove zeros and final dot if not needed.
8972 // However, only do this if there is a decimal point!
8973 $result = preg_replace('~(' . preg_quote($separator, '~') . ')?0+$~', '', $result);
8979 * Converts locale specific floating point/comma number back to standard PHP float value
8980 * Do NOT try to do any math operations before this conversion on any user submitted floats!
8982 * @param string $localefloat locale aware float representation
8983 * @param bool $strict If true, then check the input and return false if it is not a valid number.
8984 * @return mixed float|bool - false or the parsed float.
8986 function unformat_float($localefloat, $strict = false) {
8987 $localefloat = trim((string)$localefloat);
8989 if ($localefloat == '') {
8993 $localefloat = str_replace(' ', '', $localefloat); // No spaces - those might be used as thousand separators.
8994 $localefloat = str_replace(get_string('decsep', 'langconfig'), '.', $localefloat);
8996 if ($strict && !is_numeric($localefloat)) {
9000 return (float)$localefloat;
9004 * Given a simple array, this shuffles it up just like shuffle()
9005 * Unlike PHP's shuffle() this function works on any machine.
9007 * @param array $array The array to be rearranged
9010 function swapshuffle($array) {
9012 $last = count($array) - 1;
9013 for ($i = 0; $i <= $last; $i++
) {
9014 $from = rand(0, $last);
9016 $array[$i] = $array[$from];
9017 $array[$from] = $curr;
9023 * Like {@link swapshuffle()}, but works on associative arrays
9025 * @param array $array The associative array to be rearranged
9028 function swapshuffle_assoc($array) {
9030 $newarray = array();
9031 $newkeys = swapshuffle(array_keys($array));
9033 foreach ($newkeys as $newkey) {
9034 $newarray[$newkey] = $array[$newkey];
9040 * Given an arbitrary array, and a number of draws,
9041 * this function returns an array with that amount
9042 * of items. The indexes are retained.
9044 * @todo Finish documenting this function
9046 * @param array $array
9050 function draw_rand_array($array, $draws) {
9054 $last = count($array);
9056 if ($draws > $last) {
9060 while ($draws > 0) {
9063 $keys = array_keys($array);
9064 $rand = rand(0, $last);
9066 $return[$keys[$rand]] = $array[$keys[$rand]];
9067 unset($array[$keys[$rand]]);
9076 * Calculate the difference between two microtimes
9078 * @param string $a The first Microtime
9079 * @param string $b The second Microtime
9082 function microtime_diff($a, $b) {
9083 list($adec, $asec) = explode(' ', $a);
9084 list($bdec, $bsec) = explode(' ', $b);
9085 return $bsec - $asec +
$bdec - $adec;
9089 * Given a list (eg a,b,c,d,e) this function returns
9090 * an array of 1->a, 2->b, 3->c etc
9092 * @param string $list The string to explode into array bits
9093 * @param string $separator The separator used within the list string
9094 * @return array The now assembled array
9096 function make_menu_from_list($list, $separator=',') {
9098 $array = array_reverse(explode($separator, $list), true);
9099 foreach ($array as $key => $item) {
9100 $outarray[$key+
1] = trim($item);
9106 * Creates an array that represents all the current grades that
9107 * can be chosen using the given grading type.
9110 * are scales, zero is no grade, and positive numbers are maximum
9113 * @todo Finish documenting this function or better deprecated this completely!
9115 * @param int $gradingtype
9118 function make_grades_menu($gradingtype) {
9122 if ($gradingtype < 0) {
9123 if ($scale = $DB->get_record('scale', array('id'=> (-$gradingtype)))) {
9124 return make_menu_from_list($scale->scale
);
9126 } else if ($gradingtype > 0) {
9127 for ($i=$gradingtype; $i>=0; $i--) {
9128 $grades[$i] = $i .' / '. $gradingtype;
9136 * make_unique_id_code
9138 * @todo Finish documenting this function
9141 * @param string $extra Extra string to append to the end of the code
9144 function make_unique_id_code($extra = '') {
9146 $hostname = 'unknownhost';
9147 if (!empty($_SERVER['HTTP_HOST'])) {
9148 $hostname = $_SERVER['HTTP_HOST'];
9149 } else if (!empty($_ENV['HTTP_HOST'])) {
9150 $hostname = $_ENV['HTTP_HOST'];
9151 } else if (!empty($_SERVER['SERVER_NAME'])) {
9152 $hostname = $_SERVER['SERVER_NAME'];
9153 } else if (!empty($_ENV['SERVER_NAME'])) {
9154 $hostname = $_ENV['SERVER_NAME'];
9157 $date = gmdate("ymdHis");
9159 $random = random_string(6);
9162 return $hostname .'+'. $date .'+'. $random .'+'. $extra;
9164 return $hostname .'+'. $date .'+'. $random;
9170 * Function to check the passed address is within the passed subnet
9172 * The parameter is a comma separated string of subnet definitions.
9173 * Subnet strings can be in one of three formats:
9174 * 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn (number of bits in net mask)
9175 * 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)
9176 * 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx. (incomplete address, a bit non-technical ;-)
9177 * Code for type 1 modified from user posted comments by mediator at
9178 * {@link http://au.php.net/manual/en/function.ip2long.php}
9180 * @param string $addr The address you are checking
9181 * @param string $subnetstr The string of subnet addresses
9182 * @param bool $checkallzeros The state to whether check for 0.0.0.0
9185 function address_in_subnet($addr, $subnetstr, $checkallzeros = false) {
9187 if ($addr == '0.0.0.0' && !$checkallzeros) {
9190 $subnets = explode(',', $subnetstr);
9192 $addr = trim($addr);
9193 $addr = cleanremoteaddr($addr, false); // Normalise.
9194 if ($addr === null) {
9197 $addrparts = explode(':', $addr);
9199 $ipv6 = strpos($addr, ':');
9201 foreach ($subnets as $subnet) {
9202 $subnet = trim($subnet);
9203 if ($subnet === '') {
9207 if (strpos($subnet, '/') !== false) {
9208 // 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn.
9209 list($ip, $mask) = explode('/', $subnet);
9210 $mask = trim($mask);
9211 if (!is_number($mask)) {
9212 continue; // Incorect mask number, eh?
9214 $ip = cleanremoteaddr($ip, false); // Normalise.
9218 if (strpos($ip, ':') !== false) {
9223 if ($mask > 128 or $mask < 0) {
9224 continue; // Nonsense.
9227 return true; // Any address.
9230 if ($ip === $addr) {
9235 $ipparts = explode(':', $ip);
9236 $modulo = $mask %
16;
9237 $ipnet = array_slice($ipparts, 0, ($mask-$modulo)/16);
9238 $addrnet = array_slice($addrparts, 0, ($mask-$modulo)/16);
9239 if (implode(':', $ipnet) === implode(':', $addrnet)) {
9243 $pos = ($mask-$modulo)/16;
9244 $ipnet = hexdec($ipparts[$pos]);
9245 $addrnet = hexdec($addrparts[$pos]);
9246 $mask = 0xffff << (16 - $modulo);
9247 if (($addrnet & $mask) == ($ipnet & $mask)) {
9257 if ($mask > 32 or $mask < 0) {
9258 continue; // Nonsense.
9264 if ($ip === $addr) {
9269 $mask = 0xffffffff << (32 - $mask);
9270 if (((ip2long($addr) & $mask) == (ip2long($ip) & $mask))) {
9275 } else if (strpos($subnet, '-') !== false) {
9276 // 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.
9277 $parts = explode('-', $subnet);
9278 if (count($parts) != 2) {
9282 if (strpos($subnet, ':') !== false) {
9287 $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise.
9288 if ($ipstart === null) {
9291 $ipparts = explode(':', $ipstart);
9292 $start = hexdec(array_pop($ipparts));
9293 $ipparts[] = trim($parts[1]);
9294 $ipend = cleanremoteaddr(implode(':', $ipparts), false); // Normalise.
9295 if ($ipend === null) {
9299 $ipnet = implode(':', $ipparts);
9300 if (strpos($addr, $ipnet) !== 0) {
9303 $ipparts = explode(':', $ipend);
9304 $end = hexdec($ipparts[7]);
9306 $addrend = hexdec($addrparts[7]);
9308 if (($addrend >= $start) and ($addrend <= $end)) {
9317 $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise.
9318 if ($ipstart === null) {
9321 $ipparts = explode('.', $ipstart);
9322 $ipparts[3] = trim($parts[1]);
9323 $ipend = cleanremoteaddr(implode('.', $ipparts), false); // Normalise.
9324 if ($ipend === null) {
9328 if ((ip2long($addr) >= ip2long($ipstart)) and (ip2long($addr) <= ip2long($ipend))) {
9334 // 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx.
9335 if (strpos($subnet, ':') !== false) {
9340 $parts = explode(':', $subnet);
9341 $count = count($parts);
9342 if ($parts[$count-1] === '') {
9343 unset($parts[$count-1]); // Trim trailing :'s.
9345 $subnet = implode('.', $parts);
9347 $isip = cleanremoteaddr($subnet, false); // Normalise.
9348 if ($isip !== null) {
9349 if ($isip === $addr) {
9353 } else if ($count > 8) {
9356 $zeros = array_fill(0, 8-$count, '0');
9357 $subnet = $subnet.':'.implode(':', $zeros).'/'.($count*16);
9358 if (address_in_subnet($addr, $subnet)) {
9367 $parts = explode('.', $subnet);
9368 $count = count($parts);
9369 if ($parts[$count-1] === '') {
9370 unset($parts[$count-1]); // Trim trailing .
9372 $subnet = implode('.', $parts);
9375 $subnet = cleanremoteaddr($subnet, false); // Normalise.
9376 if ($subnet === $addr) {
9380 } else if ($count > 4) {
9383 $zeros = array_fill(0, 4-$count, '0');
9384 $subnet = $subnet.'.'.implode('.', $zeros).'/'.($count*8);
9385 if (address_in_subnet($addr, $subnet)) {
9396 * For outputting debugging info
9398 * @param string $string The string to write
9399 * @param string $eol The end of line char(s) to use
9400 * @param string $sleep Period to make the application sleep
9401 * This ensures any messages have time to display before redirect
9403 function mtrace($string, $eol="\n", $sleep=0) {
9406 if (isset($CFG->mtrace_wrapper
) && function_exists($CFG->mtrace_wrapper
)) {
9407 $fn = $CFG->mtrace_wrapper
;
9410 } else if (defined('STDOUT') && !PHPUNIT_TEST
&& !defined('BEHAT_TEST')) {
9411 // We must explicitly call the add_line function here.
9412 // Uses of fwrite to STDOUT are not picked up by ob_start.
9413 if ($output = \core\task\logmanager
::add_line("{$string}{$eol}")) {
9414 fwrite(STDOUT
, $output);
9417 echo $string . $eol;
9423 // Delay to keep message on user's screen in case of subsequent redirect.
9430 * Helper to {@see mtrace()} an exception or throwable, including all relevant information.
9432 * @param Throwable $e the error to ouptput.
9434 function mtrace_exception(Throwable
$e): void
{
9435 $info = get_exception_info($e);
9437 $message = $info->message
;
9438 if ($info->debuginfo
) {
9439 $message .= "\n\n" . $info->debuginfo
;
9441 if ($info->backtrace
) {
9442 $message .= "\n\n" . format_backtrace($info->backtrace
, true);
9449 * Replace 1 or more slashes or backslashes to 1 slash
9451 * @param string $path The path to strip
9452 * @return string the path with double slashes removed
9454 function cleardoubleslashes ($path) {
9455 return preg_replace('/(\/|\\\){1,}/', '/', $path);
9459 * Is the current ip in a given list?
9461 * @param string $list
9464 function remoteip_in_list($list) {
9465 $clientip = getremoteaddr(null);
9468 // Ensure access on cli.
9471 return \core\ip_utils
::is_ip_in_subnet_list($clientip, $list);
9475 * Returns most reliable client address
9477 * @param string $default If an address can't be determined, then return this
9478 * @return string The remote IP address
9480 function getremoteaddr($default='0.0.0.0') {
9483 if (!isset($CFG->getremoteaddrconf
)) {
9484 // This will happen, for example, before just after the upgrade, as the
9485 // user is redirected to the admin screen.
9486 $variablestoskip = GETREMOTEADDR_SKIP_DEFAULT
;
9488 $variablestoskip = $CFG->getremoteaddrconf
;
9490 if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_CLIENT_IP
)) {
9491 if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
9492 $address = cleanremoteaddr($_SERVER['HTTP_CLIENT_IP']);
9493 return $address ?
$address : $default;
9496 if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR
)) {
9497 if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
9498 $forwardedaddresses = explode(",", $_SERVER['HTTP_X_FORWARDED_FOR']);
9500 $forwardedaddresses = array_filter($forwardedaddresses, function($ip) {
9502 return !\core\ip_utils
::is_ip_in_subnet_list($ip, $CFG->reverseproxyignore ??
'', ',');
9505 // Multiple proxies can append values to this header including an
9506 // untrusted original request header so we must only trust the last ip.
9507 $address = end($forwardedaddresses);
9509 if (substr_count($address, ":") > 1) {
9510 // Remove port and brackets from IPv6.
9511 if (preg_match("/\[(.*)\]:/", $address, $matches)) {
9512 $address = $matches[1];
9515 // Remove port from IPv4.
9516 if (substr_count($address, ":") == 1) {
9517 $parts = explode(":", $address);
9518 $address = $parts[0];
9522 $address = cleanremoteaddr($address);
9523 return $address ?
$address : $default;
9526 if (!empty($_SERVER['REMOTE_ADDR'])) {
9527 $address = cleanremoteaddr($_SERVER['REMOTE_ADDR']);
9528 return $address ?
$address : $default;
9535 * Cleans an ip address. Internal addresses are now allowed.
9536 * (Originally local addresses were not allowed.)
9538 * @param string $addr IPv4 or IPv6 address
9539 * @param bool $compress use IPv6 address compression
9540 * @return string normalised ip address string, null if error
9542 function cleanremoteaddr($addr, $compress=false) {
9543 $addr = trim($addr);
9545 if (strpos($addr, ':') !== false) {
9546 // Can be only IPv6.
9547 $parts = explode(':', $addr);
9548 $count = count($parts);
9550 if (strpos($parts[$count-1], '.') !== false) {
9551 // Legacy ipv4 notation.
9552 $last = array_pop($parts);
9553 $ipv4 = cleanremoteaddr($last, true);
9554 if ($ipv4 === null) {
9557 $bits = explode('.', $ipv4);
9558 $parts[] = dechex($bits[0]).dechex($bits[1]);
9559 $parts[] = dechex($bits[2]).dechex($bits[3]);
9560 $count = count($parts);
9561 $addr = implode(':', $parts);
9564 if ($count < 3 or $count > 8) {
9565 return null; // Severly malformed.
9569 if (strpos($addr, '::') === false) {
9570 return null; // Malformed.
9573 $insertat = array_search('', $parts, true);
9574 $missing = array_fill(0, 1 +
8 - $count, '0');
9575 array_splice($parts, $insertat, 1, $missing);
9576 foreach ($parts as $key => $part) {
9583 $adr = implode(':', $parts);
9584 if (!preg_match('/^([0-9a-f]{1,4})(:[0-9a-f]{1,4})*$/i', $adr)) {
9585 return null; // Incorrect format - sorry.
9588 // Normalise 0s and case.
9589 $parts = array_map('hexdec', $parts);
9590 $parts = array_map('dechex', $parts);
9592 $result = implode(':', $parts);
9598 if ($result === '0:0:0:0:0:0:0:0') {
9599 return '::'; // All addresses.
9602 $compressed = preg_replace('/(:0)+:0$/', '::', $result, 1);
9603 if ($compressed !== $result) {
9607 $compressed = preg_replace('/^(0:){2,7}/', '::', $result, 1);
9608 if ($compressed !== $result) {
9612 $compressed = preg_replace('/(:0){2,6}:/', '::', $result, 1);
9613 if ($compressed !== $result) {
9620 // First get all things that look like IPv4 addresses.
9622 if (!preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $addr, $parts)) {
9627 foreach ($parts as $key => $match) {
9631 $parts[$key] = (int)$match; // Normalise 0s.
9634 return implode('.', $parts);
9639 * Is IP address a public address?
9641 * @param string $ip The ip to check
9642 * @return bool true if the ip is public
9644 function ip_is_public($ip) {
9645 return (bool) filter_var($ip, FILTER_VALIDATE_IP
, (FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
));
9649 * This function will make a complete copy of anything it's given,
9650 * regardless of whether it's an object or not.
9652 * @param mixed $thing Something you want cloned
9653 * @return mixed What ever it is you passed it
9655 function fullclone($thing) {
9656 return unserialize(serialize($thing));
9660 * Used to make sure that $min <= $value <= $max
9662 * Make sure that value is between min, and max
9664 * @param int $min The minimum value
9665 * @param int $value The value to check
9666 * @param int $max The maximum value
9669 function bounded_number($min, $value, $max) {
9670 if ($value < $min) {
9673 if ($value > $max) {
9680 * Check if there is a nested array within the passed array
9682 * @param array $array
9683 * @return bool true if there is a nested array false otherwise
9685 function array_is_nested($array) {
9686 foreach ($array as $value) {
9687 if (is_array($value)) {
9695 * get_performance_info() pairs up with init_performance_info()
9696 * loaded in setup.php. Returns an array with 'html' and 'txt'
9697 * values ready for use, and each of the individual stats provided
9698 * separately as well.
9702 function get_performance_info() {
9703 global $CFG, $PERF, $DB, $PAGE;
9706 $info['txt'] = me() . ' '; // Holds log-friendly representation.
9709 if (!empty($CFG->themedesignermode
)) {
9710 // Attempt to avoid devs debugging peformance issues, when its caused by css building and so on.
9711 $info['html'] .= '<p><strong>Warning: Theme designer mode is enabled.</strong></p>';
9713 $info['html'] .= '<ul class="list-unstyled row mx-md-0">'; // Holds userfriendly HTML representation.
9715 $info['realtime'] = microtime_diff($PERF->starttime
, microtime());
9717 $info['html'] .= '<li class="timeused col-sm-4">'.$info['realtime'].' secs</li> ';
9718 $info['txt'] .= 'time: '.$info['realtime'].'s ';
9720 // GET/POST (or NULL if $_SERVER['REQUEST_METHOD'] is undefined) is useful for txt logged information.
9721 $info['txt'] .= 'method: ' . ($_SERVER['REQUEST_METHOD'] ??
"NULL") . ' ';
9723 if (function_exists('memory_get_usage')) {
9724 $info['memory_total'] = memory_get_usage();
9725 $info['memory_growth'] = memory_get_usage() - $PERF->startmemory
;
9726 $info['html'] .= '<li class="memoryused col-sm-4">RAM: '.display_size($info['memory_total']).'</li> ';
9727 $info['txt'] .= 'memory_total: '.$info['memory_total'].'B (' . display_size($info['memory_total']).') memory_growth: '.
9728 $info['memory_growth'].'B ('.display_size($info['memory_growth']).') ';
9731 if (function_exists('memory_get_peak_usage')) {
9732 $info['memory_peak'] = memory_get_peak_usage();
9733 $info['html'] .= '<li class="memoryused col-sm-4">RAM peak: '.display_size($info['memory_peak']).'</li> ';
9734 $info['txt'] .= 'memory_peak: '.$info['memory_peak'].'B (' . display_size($info['memory_peak']).') ';
9737 $info['html'] .= '</ul><ul class="list-unstyled row mx-md-0">';
9738 $inc = get_included_files();
9739 $info['includecount'] = count($inc);
9740 $info['html'] .= '<li class="included col-sm-4">Included '.$info['includecount'].' files</li> ';
9741 $info['txt'] .= 'includecount: '.$info['includecount'].' ';
9743 if (!empty($CFG->early_install_lang
) or empty($PAGE)) {
9744 // We can not track more performance before installation or before PAGE init, sorry.
9748 $filtermanager = filter_manager
::instance();
9749 if (method_exists($filtermanager, 'get_performance_summary')) {
9750 list($filterinfo, $nicenames) = $filtermanager->get_performance_summary();
9751 $info = array_merge($filterinfo, $info);
9752 foreach ($filterinfo as $key => $value) {
9753 $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> ";
9754 $info['txt'] .= "$key: $value ";
9758 $stringmanager = get_string_manager();
9759 if (method_exists($stringmanager, 'get_performance_summary')) {
9760 list($filterinfo, $nicenames) = $stringmanager->get_performance_summary();
9761 $info = array_merge($filterinfo, $info);
9762 foreach ($filterinfo as $key => $value) {
9763 $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> ";
9764 $info['txt'] .= "$key: $value ";
9768 $info['dbqueries'] = $DB->perf_get_reads().'/'.$DB->perf_get_writes();
9769 $info['html'] .= '<li class="dbqueries col-sm-4">DB reads/writes: '.$info['dbqueries'].'</li> ';
9770 $info['txt'] .= 'db reads/writes: '.$info['dbqueries'].' ';
9772 if ($DB->want_read_slave()) {
9773 $info['dbreads_slave'] = $DB->perf_get_reads_slave();
9774 $info['html'] .= '<li class="dbqueries col-sm-4">DB reads from slave: '.$info['dbreads_slave'].'</li> ';
9775 $info['txt'] .= 'db reads from slave: '.$info['dbreads_slave'].' ';
9778 $info['dbtime'] = round($DB->perf_get_queries_time(), 5);
9779 $info['html'] .= '<li class="dbtime col-sm-4">DB queries time: '.$info['dbtime'].' secs</li> ';
9780 $info['txt'] .= 'db queries time: ' . $info['dbtime'] . 's ';
9782 if (function_exists('posix_times')) {
9783 $ptimes = posix_times();
9784 if (is_array($ptimes)) {
9785 foreach ($ptimes as $key => $val) {
9786 $info[$key] = $ptimes[$key] - $PERF->startposixtimes
[$key];
9788 $info['html'] .= "<li class=\"posixtimes col-sm-4\">ticks: $info[ticks] user: $info[utime]";
9789 $info['html'] .= "sys: $info[stime] cuser: $info[cutime] csys: $info[cstime]</li> ";
9790 $info['txt'] .= "ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime] ";
9794 // Grab the load average for the last minute.
9795 // /proc will only work under some linux configurations
9796 // while uptime is there under MacOSX/Darwin and other unices.
9797 if (is_readable('/proc/loadavg') && $loadavg = @file
('/proc/loadavg')) {
9798 list($serverload) = explode(' ', $loadavg[0]);
9800 } else if ( function_exists('is_executable') && is_executable('/usr/bin/uptime') && $loadavg = `
/usr
/bin
/uptime`
) {
9801 if (preg_match('/load averages?: (\d+[\.,:]\d+)/', $loadavg, $matches)) {
9802 $serverload = $matches[1];
9804 trigger_error('Could not parse uptime output!');
9807 if (!empty($serverload)) {
9808 $info['serverload'] = $serverload;
9809 $info['html'] .= '<li class="serverload col-sm-4">Load average: '.$info['serverload'].'</li> ';
9810 $info['txt'] .= "serverload: {$info['serverload']} ";
9813 // Display size of session if session started.
9814 if ($si = \core\session\manager
::get_performance_info()) {
9815 $info['sessionsize'] = $si['size'];
9816 $info['html'] .= "<li class=\"serverload col-sm-4\">" . $si['html'] . "</li>";
9817 $info['txt'] .= $si['txt'];
9820 // Display time waiting for session if applicable.
9821 if (!empty($PERF->sessionlock
['wait'])) {
9822 $sessionwait = number_format($PERF->sessionlock
['wait'], 3) . ' secs';
9823 $info['html'] .= html_writer
::tag('li', 'Session wait: ' . $sessionwait, [
9824 'class' => 'sessionwait col-sm-4'
9826 $info['txt'] .= 'sessionwait: ' . $sessionwait . ' ';
9829 $info['html'] .= '</ul>';
9831 if ($stats = cache_helper
::get_stats()) {
9833 $table = new html_table();
9834 $table->attributes
['class'] = 'cachesused table table-dark table-sm w-auto table-bordered';
9835 $table->head
= ['Mode', 'Cache item', 'Static', 'H', 'M', get_string('mappingprimary', 'cache'), 'H', 'M', 'S', 'I/O'];
9837 $table->align
= ['left', 'left', 'left', 'right', 'right', 'left', 'right', 'right', 'right', 'right'];
9839 $text = 'Caches used (hits/misses/sets): ';
9845 // We want to align static caches into their own column.
9847 foreach ($stats as $definition => $details) {
9848 $numstores = count($details['stores']);
9849 $first = key($details['stores']);
9850 if ($first !== cache_store
::STATIC_ACCEL
) {
9851 $numstores++
; // Add a blank space for the missing static store.
9853 $maxstores = max($maxstores, $numstores);
9858 while ($storec++
< ($maxstores - 2)) {
9859 if ($storec == ($maxstores - 2)) {
9860 $table->head
[] = get_string('mappingfinal', 'cache');
9862 $table->head
[] = "Store $storec";
9864 $table->align
[] = 'left';
9865 $table->align
[] = 'right';
9866 $table->align
[] = 'right';
9867 $table->align
[] = 'right';
9868 $table->align
[] = 'right';
9869 $table->head
[] = 'H';
9870 $table->head
[] = 'M';
9871 $table->head
[] = 'S';
9872 $table->head
[] = 'I/O';
9877 foreach ($stats as $definition => $details) {
9878 switch ($details['mode']) {
9879 case cache_store
::MODE_APPLICATION
:
9880 $modeclass = 'application';
9881 $mode = ' <span title="application cache">App</span>';
9883 case cache_store
::MODE_SESSION
:
9884 $modeclass = 'session';
9885 $mode = ' <span title="session cache">Ses</span>';
9887 case cache_store
::MODE_REQUEST
:
9888 $modeclass = 'request';
9889 $mode = ' <span title="request cache">Req</span>';
9892 $row = [$mode, $definition];
9894 $text .= "$definition {";
9897 foreach ($details['stores'] as $store => $data) {
9899 if ($storec == 0 && $store !== cache_store
::STATIC_ACCEL
) {
9906 $hits +
= $data['hits'];
9907 $misses +
= $data['misses'];
9908 $sets +
= $data['sets'];
9909 if ($data['hits'] == 0 and $data['misses'] > 0) {
9910 $cachestoreclass = 'nohits bg-danger';
9911 } else if ($data['hits'] < $data['misses']) {
9912 $cachestoreclass = 'lowhits bg-warning text-dark';
9914 $cachestoreclass = 'hihits';
9916 $text .= "$store($data[hits]/$data[misses]/$data[sets]) ";
9917 $cell = new html_table_cell($store);
9918 $cell->attributes
= ['class' => $cachestoreclass];
9920 $cell = new html_table_cell($data['hits']);
9921 $cell->attributes
= ['class' => $cachestoreclass];
9923 $cell = new html_table_cell($data['misses']);
9924 $cell->attributes
= ['class' => $cachestoreclass];
9927 if ($store !== cache_store
::STATIC_ACCEL
) {
9928 // The static cache is never set.
9929 $cell = new html_table_cell($data['sets']);
9930 $cell->attributes
= ['class' => $cachestoreclass];
9933 if ($data['hits'] ||
$data['sets']) {
9934 if ($data['iobytes'] === cache_store
::IO_BYTES_NOT_SUPPORTED
) {
9937 $size = display_size($data['iobytes'], 1, 'KB');
9938 if ($data['iobytes'] >= 10 * 1024) {
9939 $cachestoreclass = ' bg-warning text-dark';
9945 $cell = new html_table_cell($size);
9946 $cell->attributes
= ['class' => $cachestoreclass];
9951 while ($storec++
< $maxstores) {
9960 $table->data
[] = $row;
9963 $html .= html_writer
::table($table);
9965 // Now lets also show sub totals for each cache store.
9967 $storetotal = ['hits' => 0, 'misses' => 0, 'sets' => 0, 'iobytes' => 0];
9968 foreach ($stats as $definition => $details) {
9969 foreach ($details['stores'] as $store => $data) {
9970 if (!array_key_exists($store, $storetotals)) {
9971 $storetotals[$store] = ['hits' => 0, 'misses' => 0, 'sets' => 0, 'iobytes' => 0];
9973 $storetotals[$store]['class'] = $data['class'];
9974 $storetotals[$store]['hits'] +
= $data['hits'];
9975 $storetotals[$store]['misses'] +
= $data['misses'];
9976 $storetotals[$store]['sets'] +
= $data['sets'];
9977 $storetotal['hits'] +
= $data['hits'];
9978 $storetotal['misses'] +
= $data['misses'];
9979 $storetotal['sets'] +
= $data['sets'];
9980 if ($data['iobytes'] !== cache_store
::IO_BYTES_NOT_SUPPORTED
) {
9981 $storetotals[$store]['iobytes'] +
= $data['iobytes'];
9982 $storetotal['iobytes'] +
= $data['iobytes'];
9987 $table = new html_table();
9988 $table->attributes
['class'] = 'cachesused table table-dark table-sm w-auto table-bordered';
9989 $table->head
= [get_string('storename', 'cache'), get_string('type_cachestore', 'plugin'), 'H', 'M', 'S', 'I/O'];
9991 $table->align
= ['left', 'left', 'right', 'right', 'right', 'right'];
9993 ksort($storetotals);
9995 foreach ($storetotals as $store => $data) {
9997 if ($data['hits'] == 0 and $data['misses'] > 0) {
9998 $cachestoreclass = 'nohits bg-danger';
9999 } else if ($data['hits'] < $data['misses']) {
10000 $cachestoreclass = 'lowhits bg-warning text-dark';
10002 $cachestoreclass = 'hihits';
10004 $cell = new html_table_cell($store);
10005 $cell->attributes
= ['class' => $cachestoreclass];
10007 $cell = new html_table_cell($data['class']);
10008 $cell->attributes
= ['class' => $cachestoreclass];
10010 $cell = new html_table_cell($data['hits']);
10011 $cell->attributes
= ['class' => $cachestoreclass];
10013 $cell = new html_table_cell($data['misses']);
10014 $cell->attributes
= ['class' => $cachestoreclass];
10016 $cell = new html_table_cell($data['sets']);
10017 $cell->attributes
= ['class' => $cachestoreclass];
10019 if ($data['hits'] ||
$data['sets']) {
10020 if ($data['iobytes']) {
10021 $size = display_size($data['iobytes'], 1, 'KB');
10028 $cell = new html_table_cell($size);
10029 $cell->attributes
= ['class' => $cachestoreclass];
10031 $table->data
[] = $row;
10033 if (!empty($storetotal['iobytes'])) {
10034 $size = display_size($storetotal['iobytes'], 1, 'KB');
10035 } else if (!empty($storetotal['hits']) ||
!empty($storetotal['sets'])) {
10041 get_string('total'),
10043 $storetotal['hits'],
10044 $storetotal['misses'],
10045 $storetotal['sets'],
10048 $table->data
[] = $row;
10050 $html .= html_writer
::table($table);
10052 $info['cachesused'] = "$hits / $misses / $sets";
10053 $info['html'] .= $html;
10054 $info['txt'] .= $text.'. ';
10056 $info['cachesused'] = '0 / 0 / 0';
10057 $info['html'] .= '<div class="cachesused">Caches used (hits/misses/sets): 0/0/0</div>';
10058 $info['txt'] .= 'Caches used (hits/misses/sets): 0/0/0 ';
10061 // Display lock information if any.
10062 if (!empty($PERF->locks
)) {
10063 $table = new html_table();
10064 $table->attributes
['class'] = 'locktimings table table-dark table-sm w-auto table-bordered';
10065 $table->head
= ['Lock', 'Waited (s)', 'Obtained', 'Held for (s)'];
10066 $table->align
= ['left', 'right', 'center', 'right'];
10068 $text = 'Locks (waited/obtained/held):';
10069 foreach ($PERF->locks
as $locktiming) {
10071 $row[] = s($locktiming->type
. '/' . $locktiming->resource);
10072 $text .= ' ' . $locktiming->type
. '/' . $locktiming->resource . ' (';
10074 // The time we had to wait to get the lock.
10075 $roundedtime = number_format($locktiming->wait
, 1);
10076 $cell = new html_table_cell($roundedtime);
10077 if ($locktiming->wait
> 0.5) {
10078 $cell->attributes
= ['class' => 'bg-warning text-dark'];
10081 $text .= $roundedtime . '/';
10083 // Show a tick or cross for success.
10084 $row[] = $locktiming->success ?
'✓' : '❌';
10085 $text .= ($locktiming->success ?
'y' : 'n') . '/';
10087 // If applicable, show how long we held the lock before releasing it.
10088 if (property_exists($locktiming, 'held')) {
10089 $roundedtime = number_format($locktiming->held
, 1);
10090 $cell = new html_table_cell($roundedtime);
10091 if ($locktiming->held
> 0.5) {
10092 $cell->attributes
= ['class' => 'bg-warning text-dark'];
10095 $text .= $roundedtime;
10102 $table->data
[] = $row;
10104 $info['html'] .= html_writer
::table($table);
10105 $info['txt'] .= $text . '. ';
10108 $info['html'] = '<div class="performanceinfo siteinfo container-fluid px-md-0 overflow-auto pt-3">'.$info['html'].'</div>';
10113 * Renames a file or directory to a unique name within the same directory.
10115 * This function is designed to avoid any potential race conditions, and select an unused name.
10117 * @param string $filepath Original filepath
10118 * @param string $prefix Prefix to use for the temporary name
10119 * @return string|bool New file path or false if failed
10120 * @since Moodle 3.10
10122 function rename_to_unused_name(string $filepath, string $prefix = '_temp_') {
10123 $dir = dirname($filepath);
10124 $basename = $dir . '/' . $prefix;
10126 while ($limit < 100) {
10127 // Select a new name based on a random number.
10128 $newfilepath = $basename . md5(mt_rand());
10130 // Attempt a rename to that new name.
10131 if (@rename
($filepath, $newfilepath)) {
10132 return $newfilepath;
10135 // The first time, do some sanity checks, maybe it is failing for a good reason and there
10136 // is no point trying 100 times if so.
10137 if ($limit === 0 && (!file_exists($filepath) ||
!is_writable($dir))) {
10146 * Delete directory or only its content
10148 * @param string $dir directory path
10149 * @param bool $contentonly
10150 * @return bool success, true also if dir does not exist
10152 function remove_dir($dir, $contentonly=false) {
10153 if (!is_dir($dir)) {
10158 if (!$contentonly) {
10159 // Start by renaming the directory; this will guarantee that other processes don't write to it
10160 // while it is in the process of being deleted.
10161 $tempdir = rename_to_unused_name($dir);
10163 // If the rename was successful then delete the $tempdir instead.
10166 // If the rename fails, we will continue through and attempt to delete the directory
10167 // without renaming it since that is likely to at least delete most of the files.
10170 if (!$handle = opendir($dir)) {
10174 while (false!==($item = readdir($handle))) {
10175 if ($item != '.' && $item != '..') {
10176 if (is_dir($dir.'/'.$item)) {
10177 $result = remove_dir($dir.'/'.$item) && $result;
10179 $result = unlink($dir.'/'.$item) && $result;
10184 if ($contentonly) {
10185 clearstatcache(); // Make sure file stat cache is properly invalidated.
10188 $result = rmdir($dir); // If anything left the result will be false, no need for && $result.
10189 clearstatcache(); // Make sure file stat cache is properly invalidated.
10194 * Detect if an object or a class contains a given property
10195 * will take an actual object or the name of a class
10197 * @param mixed $obj Name of class or real object to test
10198 * @param string $property name of property to find
10199 * @return bool true if property exists
10201 function object_property_exists( $obj, $property ) {
10202 if (is_string( $obj )) {
10203 $properties = get_class_vars( $obj );
10205 $properties = get_object_vars( $obj );
10207 return array_key_exists( $property, $properties );
10211 * Converts an object into an associative array
10213 * This function converts an object into an associative array by iterating
10214 * over its public properties. Because this function uses the foreach
10215 * construct, Iterators are respected. It works recursively on arrays of objects.
10216 * Arrays and simple values are returned as is.
10218 * If class has magic properties, it can implement IteratorAggregate
10219 * and return all available properties in getIterator()
10221 * @param mixed $var
10224 function convert_to_array($var) {
10227 // Loop over elements/properties.
10228 foreach ($var as $key => $value) {
10229 // Recursively convert objects.
10230 if (is_object($value) ||
is_array($value)) {
10231 $result[$key] = convert_to_array($value);
10233 // Simple values are untouched.
10234 $result[$key] = $value;
10241 * Detect a custom script replacement in the data directory that will
10242 * replace an existing moodle script
10244 * @return string|bool full path name if a custom script exists, false if no custom script exists
10246 function custom_script_path() {
10247 global $CFG, $SCRIPT;
10249 if ($SCRIPT === null) {
10250 // Probably some weird external script.
10254 $scriptpath = $CFG->customscripts
. $SCRIPT;
10256 // Check the custom script exists.
10257 if (file_exists($scriptpath) and is_file($scriptpath)) {
10258 return $scriptpath;
10265 * Returns whether or not the user object is a remote MNET user. This function
10266 * is in moodlelib because it does not rely on loading any of the MNET code.
10268 * @param object $user A valid user object
10269 * @return bool True if the user is from a remote Moodle.
10271 function is_mnet_remote_user($user) {
10274 if (!isset($CFG->mnet_localhost_id
)) {
10275 include_once($CFG->dirroot
. '/mnet/lib.php');
10276 $env = new mnet_environment();
10281 return (!empty($user->mnethostid
) && $user->mnethostid
!= $CFG->mnet_localhost_id
);
10285 * This function will search for browser prefereed languages, setting Moodle
10286 * to use the best one available if $SESSION->lang is undefined
10288 function setup_lang_from_browser() {
10289 global $CFG, $SESSION, $USER;
10291 if (!empty($SESSION->lang
) or !empty($USER->lang
) or empty($CFG->autolang
)) {
10292 // Lang is defined in session or user profile, nothing to do.
10296 if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { // There isn't list of browser langs, nothing to do.
10300 // Extract and clean langs from headers.
10301 $rawlangs = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
10302 $rawlangs = str_replace('-', '_', $rawlangs); // We are using underscores.
10303 $rawlangs = explode(',', $rawlangs); // Convert to array.
10307 foreach ($rawlangs as $lang) {
10308 if (strpos($lang, ';') === false) {
10309 $langs[(string)$order] = $lang;
10310 $order = $order-0.01;
10312 $parts = explode(';', $lang);
10313 $pos = strpos($parts[1], '=');
10314 $langs[substr($parts[1], $pos+
1)] = $parts[0];
10317 krsort($langs, SORT_NUMERIC
);
10319 // Look for such langs under standard locations.
10320 foreach ($langs as $lang) {
10321 // Clean it properly for include.
10322 $lang = strtolower(clean_param($lang, PARAM_SAFEDIR
));
10323 if (get_string_manager()->translation_exists($lang, false)) {
10324 // Lang exists, set it in session.
10325 $SESSION->lang
= $lang;
10326 // We have finished. Go out.
10334 * Check if $url matches anything in proxybypass list
10336 * Any errors just result in the proxy being used (least bad)
10338 * @param string $url url to check
10339 * @return boolean true if we should bypass the proxy
10341 function is_proxybypass( $url ) {
10345 if (empty($CFG->proxyhost
) or empty($CFG->proxybypass
)) {
10349 // Get the host part out of the url.
10350 if (!$host = parse_url( $url, PHP_URL_HOST
)) {
10354 // Get the possible bypass hosts into an array.
10355 $matches = explode( ',', $CFG->proxybypass
);
10357 // Check for a exact match on the IP or in the domains.
10358 $isdomaininallowedlist = \core\ip_utils
::is_domain_in_allowed_list($host, $matches);
10359 $isipinsubnetlist = \core\ip_utils
::is_ip_in_subnet_list($host, $CFG->proxybypass
, ',');
10361 if ($isdomaininallowedlist ||
$isipinsubnetlist) {
10365 // Nothing matched.
10370 * Check if the passed navigation is of the new style
10372 * @param mixed $navigation
10373 * @return bool true for yes false for no
10375 function is_newnav($navigation) {
10376 if (is_array($navigation) && !empty($navigation['newnav'])) {
10384 * Checks whether the given variable name is defined as a variable within the given object.
10386 * This will NOT work with stdClass objects, which have no class variables.
10388 * @param string $var The variable name
10389 * @param object $object The object to check
10392 function in_object_vars($var, $object) {
10393 $classvars = get_class_vars(get_class($object));
10394 $classvars = array_keys($classvars);
10395 return in_array($var, $classvars);
10399 * Returns an array without repeated objects.
10400 * This function is similar to array_unique, but for arrays that have objects as values
10402 * @param array $array
10403 * @param bool $keepkeyassoc
10406 function object_array_unique($array, $keepkeyassoc = true) {
10407 $duplicatekeys = array();
10410 foreach ($array as $key => $val) {
10411 // Convert objects to arrays, in_array() does not support objects.
10412 if (is_object($val)) {
10413 $val = (array)$val;
10416 if (!in_array($val, $tmp)) {
10419 $duplicatekeys[] = $key;
10423 foreach ($duplicatekeys as $key) {
10424 unset($array[$key]);
10427 return $keepkeyassoc ?
$array : array_values($array);
10431 * Is a userid the primary administrator?
10433 * @param int $userid int id of user to check
10436 function is_primary_admin($userid) {
10437 $primaryadmin = get_admin();
10439 if ($userid == $primaryadmin->id
) {
10447 * Returns the site identifier
10449 * @return string $CFG->siteidentifier, first making sure it is properly initialised.
10451 function get_site_identifier() {
10453 // Check to see if it is missing. If so, initialise it.
10454 if (empty($CFG->siteidentifier
)) {
10455 set_config('siteidentifier', random_string(32) . $_SERVER['HTTP_HOST']);
10458 return $CFG->siteidentifier
;
10462 * Check whether the given password has no more than the specified
10463 * number of consecutive identical characters.
10465 * @param string $password password to be checked against the password policy
10466 * @param integer $maxchars maximum number of consecutive identical characters
10469 function check_consecutive_identical_characters($password, $maxchars) {
10471 if ($maxchars < 1) {
10472 return true; // Zero 0 is to disable this check.
10474 if (strlen($password) <= $maxchars) {
10475 return true; // Too short to fail this test.
10478 $previouschar = '';
10479 $consecutivecount = 1;
10480 foreach (str_split($password) as $char) {
10481 if ($char != $previouschar) {
10482 $consecutivecount = 1;
10484 $consecutivecount++
;
10485 if ($consecutivecount > $maxchars) {
10486 return false; // Check failed already.
10490 $previouschar = $char;
10497 * Helper function to do partial function binding.
10498 * so we can use it for preg_replace_callback, for example
10499 * this works with php functions, user functions, static methods and class methods
10500 * it returns you a callback that you can pass on like so:
10502 * $callback = partial('somefunction', $arg1, $arg2);
10504 * $callback = partial(array('someclass', 'somestaticmethod'), $arg1, $arg2);
10506 * $obj = new someclass();
10507 * $callback = partial(array($obj, 'somemethod'), $arg1, $arg2);
10509 * and then the arguments that are passed through at calltime are appended to the argument list.
10511 * @param mixed $function a php callback
10512 * @param mixed $arg1,... $argv arguments to partially bind with
10513 * @return array Array callback
10515 function partial() {
10516 if (!class_exists('partial')) {
10518 * Used to manage function binding.
10519 * @copyright 2009 Penny Leach
10520 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
10524 public $values = array();
10525 /** @var string The function to call as a callback. */
10529 * @param string $func
10530 * @param array $args
10532 public function __construct($func, $args) {
10533 $this->values
= $args;
10534 $this->func
= $func;
10537 * Calls the callback function.
10540 public function method() {
10541 $args = func_get_args();
10542 return call_user_func_array($this->func
, array_merge($this->values
, $args));
10546 $args = func_get_args();
10547 $func = array_shift($args);
10548 $p = new partial($func, $args);
10549 return array($p, 'method');
10553 * helper function to load up and initialise the mnet environment
10554 * this must be called before you use mnet functions.
10556 * @return mnet_environment the equivalent of old $MNET global
10558 function get_mnet_environment() {
10560 require_once($CFG->dirroot
. '/mnet/lib.php');
10561 static $instance = null;
10562 if (empty($instance)) {
10563 $instance = new mnet_environment();
10570 * during xmlrpc server code execution, any code wishing to access
10571 * information about the remote peer must use this to get it.
10573 * @return mnet_remote_client the equivalent of old $MNETREMOTE_CLIENT global
10575 function get_mnet_remote_client() {
10576 if (!defined('MNET_SERVER')) {
10577 debugging(get_string('notinxmlrpcserver', 'mnet'));
10580 global $MNET_REMOTE_CLIENT;
10581 if (isset($MNET_REMOTE_CLIENT)) {
10582 return $MNET_REMOTE_CLIENT;
10588 * during the xmlrpc server code execution, this will be called
10589 * to setup the object returned by {@link get_mnet_remote_client}
10591 * @param mnet_remote_client $client the client to set up
10592 * @throws moodle_exception
10594 function set_mnet_remote_client($client) {
10595 if (!defined('MNET_SERVER')) {
10596 throw new moodle_exception('notinxmlrpcserver', 'mnet');
10598 global $MNET_REMOTE_CLIENT;
10599 $MNET_REMOTE_CLIENT = $client;
10603 * return the jump url for a given remote user
10604 * this is used for rewriting forum post links in emails, etc
10606 * @param stdclass $user the user to get the idp url for
10608 function mnet_get_idp_jump_url($user) {
10611 static $mnetjumps = array();
10612 if (!array_key_exists($user->mnethostid
, $mnetjumps)) {
10613 $idp = mnet_get_peer_host($user->mnethostid
);
10614 $idpjumppath = mnet_get_app_jumppath($idp->applicationid
);
10615 $mnetjumps[$user->mnethostid
] = $idp->wwwroot
. $idpjumppath . '?hostwwwroot=' . $CFG->wwwroot
. '&wantsurl=';
10617 return $mnetjumps[$user->mnethostid
];
10621 * Gets the homepage to use for the current user
10623 * @return int One of HOMEPAGE_*
10625 function get_home_page() {
10628 if (isloggedin() && !isguestuser() && !empty($CFG->defaulthomepage
)) {
10629 // If dashboard is disabled, home will be set to default page.
10630 $defaultpage = get_default_home_page();
10631 if ($CFG->defaulthomepage
== HOMEPAGE_MY
) {
10632 if (!empty($CFG->enabledashboard
)) {
10633 return HOMEPAGE_MY
;
10635 return $defaultpage;
10637 } else if ($CFG->defaulthomepage
== HOMEPAGE_MYCOURSES
) {
10638 return HOMEPAGE_MYCOURSES
;
10640 $userhomepage = (int) get_user_preferences('user_home_page_preference', $defaultpage);
10641 if (empty($CFG->enabledashboard
) && $userhomepage == HOMEPAGE_MY
) {
10642 // If the user was using the dashboard but it's disabled, return the default home page.
10643 $userhomepage = $defaultpage;
10645 return $userhomepage;
10648 return HOMEPAGE_SITE
;
10652 * Returns the default home page to display if current one is not defined or can't be applied.
10653 * The default behaviour is to return Dashboard if it's enabled or My courses page if it isn't.
10655 * @return int The default home page.
10657 function get_default_home_page(): int {
10660 return !empty($CFG->enabledashboard
) ? HOMEPAGE_MY
: HOMEPAGE_MYCOURSES
;
10664 * Gets the name of a course to be displayed when showing a list of courses.
10665 * By default this is just $course->fullname but user can configure it. The
10666 * result of this function should be passed through print_string.
10667 * @param stdClass|core_course_list_element $course Moodle course object
10668 * @return string Display name of course (either fullname or short + fullname)
10670 function get_course_display_name_for_list($course) {
10672 if (!empty($CFG->courselistshortnames
)) {
10673 if (!($course instanceof stdClass
)) {
10674 $course = (object)convert_to_array($course);
10676 return get_string('courseextendednamedisplay', '', $course);
10678 return $course->fullname
;
10683 * Safe analogue of unserialize() that can only parse arrays
10685 * Arrays may contain only integers or strings as both keys and values. Nested arrays are allowed.
10687 * @param string $expression
10688 * @return array|bool either parsed array or false if parsing was impossible.
10690 function unserialize_array($expression) {
10692 // Check the expression is an array.
10693 if (!preg_match('/^a:(\d+):/', $expression)) {
10697 $values = (array) unserialize_object($expression);
10699 // Callback that returns true if the given value is an unserialized object, executes recursively.
10700 $invalidvaluecallback = static function($value) use (&$invalidvaluecallback): bool {
10701 if (is_array($value)) {
10702 return (bool) array_filter($value, $invalidvaluecallback);
10704 return ($value instanceof stdClass
) ||
($value instanceof __PHP_Incomplete_Class
);
10707 // Iterate over the result to ensure there are no stray objects.
10708 if (array_filter($values, $invalidvaluecallback)) {
10716 * Safe method for unserializing given input that is expected to contain only a serialized instance of an stdClass object
10718 * If any class type other than stdClass is included in the input string, it will not be instantiated and will be cast to an
10719 * stdClass object. The initial cast to array, then back to object is to ensure we are always returning the correct type,
10720 * otherwise we would return an instances of {@see __PHP_Incomplete_class} for malformed strings
10722 * @param string $input
10725 function unserialize_object(string $input): stdClass
{
10726 $instance = (array) unserialize($input, ['allowed_classes' => [stdClass
::class]]);
10727 return (object) $instance;
10731 * The lang_string class
10733 * This special class is used to create an object representation of a string request.
10734 * It is special because processing doesn't occur until the object is first used.
10735 * The class was created especially to aid performance in areas where strings were
10736 * required to be generated but were not necessarily used.
10737 * As an example the admin tree when generated uses over 1500 strings, of which
10738 * normally only 1/3 are ever actually printed at any time.
10739 * The performance advantage is achieved by not actually processing strings that
10740 * arn't being used, as such reducing the processing required for the page.
10742 * How to use the lang_string class?
10743 * There are two methods of using the lang_string class, first through the
10744 * forth argument of the get_string function, and secondly directly.
10745 * The following are examples of both.
10746 * 1. Through get_string calls e.g.
10747 * $string = get_string($identifier, $component, $a, true);
10748 * $string = get_string('yes', 'moodle', null, true);
10749 * 2. Direct instantiation
10750 * $string = new lang_string($identifier, $component, $a, $lang);
10751 * $string = new lang_string('yes');
10753 * How do I use a lang_string object?
10754 * The lang_string object makes use of a magic __toString method so that you
10755 * are able to use the object exactly as you would use a string in most cases.
10756 * This means you are able to collect it into a variable and then directly
10757 * echo it, or concatenate it into another string, or similar.
10758 * The other thing you can do is manually get the string by calling the
10759 * lang_strings out method e.g.
10760 * $string = new lang_string('yes');
10762 * Also worth noting is that the out method can take one argument, $lang which
10763 * allows the developer to change the language on the fly.
10765 * When should I use a lang_string object?
10766 * The lang_string object is designed to be used in any situation where a
10767 * string may not be needed, but needs to be generated.
10768 * The admin tree is a good example of where lang_string objects should be
10770 * A more practical example would be any class that requries strings that may
10771 * not be printed (after all classes get renderer by renderers and who knows
10772 * what they will do ;))
10774 * When should I not use a lang_string object?
10775 * Don't use lang_strings when you are going to use a string immediately.
10776 * There is no need as it will be processed immediately and there will be no
10777 * advantage, and in fact perhaps a negative hit as a class has to be
10778 * instantiated for a lang_string object, however get_string won't require
10782 * 1. You cannot use a lang_string object as an array offset. Doing so will
10783 * result in PHP throwing an error. (You can use it as an object property!)
10787 * @copyright 2011 Sam Hemelryk
10788 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
10790 class lang_string
{
10792 /** @var string The strings identifier */
10793 protected $identifier;
10794 /** @var string The strings component. Default '' */
10795 protected $component = '';
10796 /** @var array|stdClass Any arguments required for the string. Default null */
10797 protected $a = null;
10798 /** @var string The language to use when processing the string. Default null */
10799 protected $lang = null;
10801 /** @var string The processed string (once processed) */
10802 protected $string = null;
10805 * A special boolean. If set to true then the object has been woken up and
10806 * cannot be regenerated. If this is set then $this->string MUST be used.
10809 protected $forcedstring = false;
10812 * Constructs a lang_string object
10814 * This function should do as little processing as possible to ensure the best
10815 * performance for strings that won't be used.
10817 * @param string $identifier The strings identifier
10818 * @param string $component The strings component
10819 * @param stdClass|array|mixed $a Any arguments the string requires
10820 * @param string $lang The language to use when processing the string.
10821 * @throws coding_exception
10823 public function __construct($identifier, $component = '', $a = null, $lang = null) {
10824 if (empty($component)) {
10825 $component = 'moodle';
10828 $this->identifier
= $identifier;
10829 $this->component
= $component;
10830 $this->lang
= $lang;
10832 // We MUST duplicate $a to ensure that it if it changes by reference those
10833 // changes are not carried across.
10834 // To do this we always ensure $a or its properties/values are strings
10835 // and that any properties/values that arn't convertable are forgotten.
10837 if (is_scalar($a)) {
10839 } else if ($a instanceof lang_string
) {
10840 $this->a
= $a->out();
10841 } else if (is_object($a) or is_array($a)) {
10843 $this->a
= array();
10844 foreach ($a as $key => $value) {
10845 // Make sure conversion errors don't get displayed (results in '').
10846 if (is_array($value)) {
10847 $this->a
[$key] = '';
10848 } else if (is_object($value)) {
10849 if (method_exists($value, '__toString')) {
10850 $this->a
[$key] = $value->__toString();
10852 $this->a
[$key] = '';
10855 $this->a
[$key] = (string)$value;
10861 if (debugging(false, DEBUG_DEVELOPER
)) {
10862 if (clean_param($this->identifier
, PARAM_STRINGID
) == '') {
10863 throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please check your string definition');
10865 if (!empty($this->component
) && clean_param($this->component
, PARAM_COMPONENT
) == '') {
10866 throw new coding_exception('Invalid string compontent. Please check your string definition');
10868 if (!get_string_manager()->string_exists($this->identifier
, $this->component
)) {
10869 debugging('String does not exist. Please check your string definition for '.$this->identifier
.'/'.$this->component
, DEBUG_DEVELOPER
);
10875 * Processes the string.
10877 * This function actually processes the string, stores it in the string property
10878 * and then returns it.
10879 * You will notice that this function is VERY similar to the get_string method.
10880 * That is because it is pretty much doing the same thing.
10881 * However as this function is an upgrade it isn't as tolerant to backwards
10885 * @throws coding_exception
10887 protected function get_string() {
10890 // Check if we need to process the string.
10891 if ($this->string === null) {
10892 // Check the quality of the identifier.
10893 if ($CFG->debugdeveloper
&& clean_param($this->identifier
, PARAM_STRINGID
) === '') {
10894 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
);
10897 // Process the string.
10898 $this->string = get_string_manager()->get_string($this->identifier
, $this->component
, $this->a
, $this->lang
);
10899 // Debugging feature lets you display string identifier and component.
10900 if (isset($CFG->debugstringids
) && $CFG->debugstringids
&& optional_param('strings', 0, PARAM_INT
)) {
10901 $this->string .= ' {' . $this->identifier
. '/' . $this->component
. '}';
10904 // Return the string.
10905 return $this->string;
10909 * Returns the string
10911 * @param string $lang The langauge to use when processing the string
10914 public function out($lang = null) {
10915 if ($lang !== null && $lang != $this->lang
&& ($this->lang
== null && $lang != current_language())) {
10916 if ($this->forcedstring
) {
10917 debugging('lang_string objects that have been used cannot be printed in another language. ('.$this->lang
.' used)', DEBUG_DEVELOPER
);
10918 return $this->get_string();
10920 $translatedstring = new lang_string($this->identifier
, $this->component
, $this->a
, $lang);
10921 return $translatedstring->out();
10923 return $this->get_string();
10927 * Magic __toString method for printing a string
10931 public function __toString() {
10932 return $this->get_string();
10936 * Magic __set_state method used for var_export
10938 * @param array $array
10941 public static function __set_state(array $array): self
{
10942 $tmp = new lang_string($array['identifier'], $array['component'], $array['a'], $array['lang']);
10943 $tmp->string = $array['string'];
10944 $tmp->forcedstring
= $array['forcedstring'];
10949 * Prepares the lang_string for sleep and stores only the forcedstring and
10950 * string properties... the string cannot be regenerated so we need to ensure
10951 * it is generated for this.
10955 public function __sleep() {
10956 $this->get_string();
10957 $this->forcedstring
= true;
10958 return array('forcedstring', 'string', 'lang');
10962 * Returns the identifier.
10966 public function get_identifier() {
10967 return $this->identifier
;
10971 * Returns the component.
10975 public function get_component() {
10976 return $this->component
;
10981 * Get human readable name describing the given callable.
10983 * This performs syntax check only to see if the given param looks like a valid function, method or closure.
10984 * It does not check if the callable actually exists.
10986 * @param callable|string|array $callable
10987 * @return string|bool Human readable name of callable, or false if not a valid callable.
10989 function get_callable_name($callable) {
10991 if (!is_callable($callable, true, $name)) {
11000 * Tries to guess if $CFG->wwwroot is publicly accessible or not.
11001 * Never put your faith on this function and rely on its accuracy as there might be false positives.
11002 * It just performs some simple checks, and mainly is used for places where we want to hide some options
11003 * such as site registration when $CFG->wwwroot is not publicly accessible.
11004 * Good thing is there is no false negative.
11005 * Note that it's possible to force the result of this check by specifying $CFG->site_is_public in config.php
11009 function site_is_public() {
11012 // Return early if site admin has forced this setting.
11013 if (isset($CFG->site_is_public
)) {
11014 return (bool)$CFG->site_is_public
;
11017 $host = parse_url($CFG->wwwroot
, PHP_URL_HOST
);
11019 if ($host === 'localhost' ||
preg_match('|^127\.\d+\.\d+\.\d+$|', $host)) {
11021 } else if (\core\ip_utils
::is_ip_address($host) && !ip_is_public($host)) {
11023 } else if (($address = \core\ip_utils
::get_ip_address($host)) && !ip_is_public($address)) {
11033 * Validates user's password length.
11035 * @param string $password
11036 * @param int $pepperlength The length of the used peppers
11039 function exceeds_password_length(string $password, int $pepperlength = 0): bool {
11040 return (strlen($password) > (MAX_PASSWORD_CHARACTERS +
$pepperlength));