Merge branch 'MDL-81713-main' of https://github.com/junpataleta/moodle
[moodle.git] / lib / moodlelib.php
blob7e0b0d9513441be215b1b3123cd227af5fd95128
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 /**
18 * moodlelib.php - Moodle main library
20 * Main library file of miscellaneous general-purpose Moodle functions.
21 * Other main libraries:
22 * - weblib.php - functions that produce web output
23 * - datalib.php - functions that access the database
25 * @package core
26 * @subpackage lib
27 * @copyright 1999 onwards Martin Dougiamas http://dougiamas.com
28 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
31 use core\di;
32 use core\hook;
34 defined('MOODLE_INTERNAL') || die();
36 // CONSTANTS (Encased in phpdoc proper comments).
38 // Date and time constants.
39 /**
40 * Time constant - the number of seconds in a year
42 define('YEARSECS', 31536000);
44 /**
45 * Time constant - the number of seconds in a week
47 define('WEEKSECS', 604800);
49 /**
50 * Time constant - the number of seconds in a day
52 define('DAYSECS', 86400);
54 /**
55 * Time constant - the number of seconds in an hour
57 define('HOURSECS', 3600);
59 /**
60 * Time constant - the number of seconds in a minute
62 define('MINSECS', 60);
64 /**
65 * Time constant - the number of minutes in a day
67 define('DAYMINS', 1440);
69 /**
70 * Time constant - the number of minutes in an hour
72 define('HOURMINS', 60);
74 // Parameter constants - every call to optional_param(), required_param()
75 // or clean_param() should have a specified type of parameter.
77 // We currently include \core\param manually here to avoid broken upgrades.
78 // This may change after the next LTS release as LTS releases require the previous LTS release.
79 require_once(__DIR__ . '/classes/deprecation.php');
80 require_once(__DIR__ . '/classes/param.php');
82 /**
83 * PARAM_ALPHA - contains only English ascii letters [a-zA-Z].
85 define('PARAM_ALPHA', \core\param::ALPHA->value);
87 /**
88 * PARAM_ALPHAEXT the same contents as PARAM_ALPHA (English ascii letters [a-zA-Z]) plus the chars in quotes: "_-" allowed
89 * NOTE: originally this allowed "/" too, please use PARAM_SAFEPATH if "/" needed
91 define('PARAM_ALPHAEXT', \core\param::ALPHAEXT->value);
93 /**
94 * PARAM_ALPHANUM - expected numbers 0-9 and English ascii letters [a-zA-Z] only.
96 define('PARAM_ALPHANUM', \core\param::ALPHANUM->value);
98 /**
99 * PARAM_ALPHANUMEXT - expected numbers 0-9, letters (English ascii letters [a-zA-Z]) and _- only.
101 define('PARAM_ALPHANUMEXT', \core\param::ALPHANUMEXT->value);
104 * PARAM_AUTH - actually checks to make sure the string is a valid auth plugin
106 define('PARAM_AUTH', \core\param::AUTH->value);
109 * PARAM_BASE64 - Base 64 encoded format
111 define('PARAM_BASE64', \core\param::BASE64->value);
114 * PARAM_BOOL - converts input into 0 or 1, use for switches in forms and urls.
116 define('PARAM_BOOL', \core\param::BOOL->value);
119 * PARAM_CAPABILITY - A capability name, like 'moodle/role:manage'. Actually
120 * checked against the list of capabilities in the database.
122 define('PARAM_CAPABILITY', \core\param::CAPABILITY->value);
125 * PARAM_CLEANHTML - cleans submitted HTML code. Note that you almost never want
126 * to use this. The normal mode of operation is to use PARAM_RAW when receiving
127 * the input (required/optional_param or formslib) and then sanitise the HTML
128 * using format_text on output. This is for the rare cases when you want to
129 * sanitise the HTML on input. This cleaning may also fix xhtml strictness.
131 define('PARAM_CLEANHTML', \core\param::CLEANHTML->value);
134 * PARAM_EMAIL - an email address following the RFC
136 define('PARAM_EMAIL', \core\param::EMAIL->value);
139 * PARAM_FILE - safe file name, all dangerous chars are stripped, protects against XSS, SQL injections and directory traversals
141 define('PARAM_FILE', \core\param::FILE->value);
144 * PARAM_FLOAT - a real/floating point number.
146 * Note that you should not use PARAM_FLOAT for numbers typed in by the user.
147 * It does not work for languages that use , as a decimal separator.
148 * Use PARAM_LOCALISEDFLOAT instead.
150 define('PARAM_FLOAT', \core\param::FLOAT->value);
153 * PARAM_LOCALISEDFLOAT - a localised real/floating point number.
154 * This is preferred over PARAM_FLOAT for numbers typed in by the user.
155 * Cleans localised numbers to computer readable numbers; false for invalid numbers.
157 define('PARAM_LOCALISEDFLOAT', \core\param::LOCALISEDFLOAT->value);
160 * PARAM_HOST - expected fully qualified domain name (FQDN) or an IPv4 dotted quad (IP address)
162 define('PARAM_HOST', \core\param::HOST->value);
165 * PARAM_INT - integers only, use when expecting only numbers.
167 define('PARAM_INT', \core\param::INT->value);
170 * PARAM_LANG - checks to see if the string is a valid installed language in the current site.
172 define('PARAM_LANG', \core\param::LANG->value);
175 * PARAM_LOCALURL - expected properly formatted URL as well as one that refers to the local server itself. (NOT orthogonal to the
176 * others! Implies PARAM_URL!)
178 define('PARAM_LOCALURL', \core\param::LOCALURL->value);
181 * PARAM_NOTAGS - all html tags are stripped from the text. Do not abuse this type.
183 define('PARAM_NOTAGS', \core\param::NOTAGS->value);
186 * PARAM_PATH - safe relative path name, all dangerous chars are stripped, protects against XSS, SQL injections and directory
187 * traversals note: the leading slash is not removed, window drive letter is not allowed
189 define('PARAM_PATH', \core\param::PATH->value);
192 * PARAM_PEM - Privacy Enhanced Mail format
194 define('PARAM_PEM', \core\param::PEM->value);
197 * PARAM_PERMISSION - A permission, one of CAP_INHERIT, CAP_ALLOW, CAP_PREVENT or CAP_PROHIBIT.
199 define('PARAM_PERMISSION', \core\param::PERMISSION->value);
202 * PARAM_RAW specifies a parameter that is not cleaned/processed in any way except the discarding of the invalid utf-8 characters
204 define('PARAM_RAW', \core\param::RAW->value);
207 * PARAM_RAW_TRIMMED like PARAM_RAW but leading and trailing whitespace is stripped.
209 define('PARAM_RAW_TRIMMED', \core\param::RAW_TRIMMED->value);
212 * PARAM_SAFEDIR - safe directory name, suitable for include() and require()
214 define('PARAM_SAFEDIR', \core\param::SAFEDIR->value);
217 * PARAM_SAFEPATH - several PARAM_SAFEDIR joined by "/", suitable for include() and require(), plugin paths
218 * and other references to Moodle code files.
220 * This is NOT intended to be used for absolute paths or any user uploaded files.
222 define('PARAM_SAFEPATH', \core\param::SAFEPATH->value);
225 * PARAM_SEQUENCE - expects a sequence of numbers like 8 to 1,5,6,4,6,8,9. Numbers and comma only.
227 define('PARAM_SEQUENCE', \core\param::SEQUENCE->value);
230 * PARAM_TAG - one tag (interests, blogs, etc.) - mostly international characters and space, <> not supported
232 define('PARAM_TAG', \core\param::TAG->value);
235 * PARAM_TAGLIST - list of tags separated by commas (interests, blogs, etc.)
237 define('PARAM_TAGLIST', \core\param::TAGLIST->value);
240 * PARAM_TEXT - general plain text compatible with multilang filter, no other html tags. Please note '<', or '>' are allowed here.
242 define('PARAM_TEXT', \core\param::TEXT->value);
245 * PARAM_THEME - Checks to see if the string is a valid theme name in the current site
247 define('PARAM_THEME', \core\param::THEME->value);
250 * PARAM_URL - expected properly formatted URL. Please note that domain part is required, http://localhost/ is not accepted but
251 * http://localhost.localdomain/ is ok.
253 define('PARAM_URL', \core\param::URL->value);
256 * PARAM_USERNAME - Clean username to only contains allowed characters. This is to be used ONLY when manually creating user
257 * accounts, do NOT use when syncing with external systems!!
259 define('PARAM_USERNAME', \core\param::USERNAME->value);
262 * PARAM_STRINGID - used to check if the given string is valid string identifier for get_string()
264 define('PARAM_STRINGID', \core\param::STRINGID->value);
266 // DEPRECATED PARAM TYPES OR ALIASES - DO NOT USE FOR NEW CODE.
268 * PARAM_CLEAN - obsoleted, please use a more specific type of parameter.
269 * It was one of the first types, that is why it is abused so much ;-)
270 * @deprecated since 2.0
272 define('PARAM_CLEAN', \core\param::CLEAN->value);
275 * PARAM_INTEGER - deprecated alias for PARAM_INT
276 * @deprecated since 2.0
278 define('PARAM_INTEGER', \core\param::INT->value);
281 * PARAM_NUMBER - deprecated alias of PARAM_FLOAT
282 * @deprecated since 2.0
284 define('PARAM_NUMBER', \core\param::FLOAT->value);
287 * PARAM_ACTION - deprecated alias for PARAM_ALPHANUMEXT, use for various actions in forms and urls
288 * NOTE: originally alias for PARAM_APLHA
289 * @deprecated since 2.0
291 define('PARAM_ACTION', \core\param::ALPHANUMEXT->value);
294 * PARAM_FORMAT - deprecated alias for PARAM_ALPHANUMEXT, use for names of plugins, formats, etc.
295 * NOTE: originally alias for PARAM_APLHA
296 * @deprecated since 2.0
298 define('PARAM_FORMAT', \core\param::ALPHANUMEXT->value);
301 * PARAM_MULTILANG - deprecated alias of PARAM_TEXT.
302 * @deprecated since 2.0
304 define('PARAM_MULTILANG', \core\param::TEXT->value);
307 * PARAM_TIMEZONE - expected timezone. Timezone can be int +-(0-13) or float +-(0.5-12.5) or
308 * string separated by '/' and can have '-' &/ '_' (eg. America/North_Dakota/New_Salem
309 * America/Port-au-Prince)
311 define('PARAM_TIMEZONE', \core\param::TIMEZONE->value);
314 * PARAM_CLEANFILE - deprecated alias of PARAM_FILE; originally was removing regional chars too
315 * @deprecated since 2.0
317 define('PARAM_CLEANFILE', \core\param::CLEANFILE->value);
320 * PARAM_COMPONENT is used for full component names (aka frankenstyle) such as 'mod_forum', 'core_rating', 'auth_ldap'.
321 * Short legacy subsystem names and module names are accepted too ex: 'forum', 'rating', 'user'.
322 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
323 * NOTE: numbers and underscores are strongly discouraged in plugin names!
325 define('PARAM_COMPONENT', \core\param::COMPONENT->value);
328 * PARAM_AREA is a name of area used when addressing files, comments, ratings, etc.
329 * It is usually used together with context id and component.
330 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
332 define('PARAM_AREA', \core\param::AREA->value);
335 * PARAM_PLUGIN is used for plugin names such as 'forum', 'glossary', 'ldap', 'paypal', 'completionstatus'.
336 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
337 * NOTE: numbers and underscores are strongly discouraged in plugin names! Underscores are forbidden in module names.
339 define('PARAM_PLUGIN', \core\param::PLUGIN->value);
342 // Web Services.
345 * VALUE_REQUIRED - if the parameter is not supplied, there is an error
347 define('VALUE_REQUIRED', 1);
350 * VALUE_OPTIONAL - if the parameter is not supplied, then the param has no value
352 define('VALUE_OPTIONAL', 2);
355 * VALUE_DEFAULT - if the parameter is not supplied, then the default value is used
357 define('VALUE_DEFAULT', 0);
360 * NULL_NOT_ALLOWED - the parameter can not be set to null in the database
362 define('NULL_NOT_ALLOWED', false);
365 * NULL_ALLOWED - the parameter can be set to null in the database
367 define('NULL_ALLOWED', true);
369 // Page types.
372 * PAGE_COURSE_VIEW is a definition of a page type. For more information on the page class see moodle/lib/pagelib.php.
374 define('PAGE_COURSE_VIEW', 'course-view');
376 /** Get remote addr constant */
377 define('GETREMOTEADDR_SKIP_HTTP_CLIENT_IP', '1');
378 /** Get remote addr constant */
379 define('GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR', '2');
381 * GETREMOTEADDR_SKIP_DEFAULT defines the default behavior remote IP address validation.
383 define('GETREMOTEADDR_SKIP_DEFAULT', GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR|GETREMOTEADDR_SKIP_HTTP_CLIENT_IP);
385 // Blog access level constant declaration.
386 define ('BLOG_USER_LEVEL', 1);
387 define ('BLOG_GROUP_LEVEL', 2);
388 define ('BLOG_COURSE_LEVEL', 3);
389 define ('BLOG_SITE_LEVEL', 4);
390 define ('BLOG_GLOBAL_LEVEL', 5);
393 // Tag constants.
395 * To prevent problems with multibytes strings,Flag updating in nav not working on the review page. this should not exceed the
396 * length of "varchar(255) / 3 (bytes / utf-8 character) = 85".
397 * TODO: this is not correct, varchar(255) are 255 unicode chars ;-)
399 * @todo define(TAG_MAX_LENGTH) this is not correct, varchar(255) are 255 unicode chars ;-)
401 define('TAG_MAX_LENGTH', 50);
403 // Password policy constants.
404 define ('PASSWORD_LOWER', 'abcdefghijklmnopqrstuvwxyz');
405 define ('PASSWORD_UPPER', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
406 define ('PASSWORD_DIGITS', '0123456789');
407 define ('PASSWORD_NONALPHANUM', '.,;:!?_-+/*@#&$');
410 * Required password pepper entropy.
412 define ('PEPPER_ENTROPY', 112);
414 // Feature constants.
415 // Used for plugin_supports() to report features that are, or are not, supported by a module.
417 /** True if module can provide a grade */
418 define('FEATURE_GRADE_HAS_GRADE', 'grade_has_grade');
419 /** True if module supports outcomes */
420 define('FEATURE_GRADE_OUTCOMES', 'outcomes');
421 /** True if module supports advanced grading methods */
422 define('FEATURE_ADVANCED_GRADING', 'grade_advanced_grading');
423 /** True if module controls the grade visibility over the gradebook */
424 define('FEATURE_CONTROLS_GRADE_VISIBILITY', 'controlsgradevisbility');
425 /** True if module supports plagiarism plugins */
426 define('FEATURE_PLAGIARISM', 'plagiarism');
428 /** True if module has code to track whether somebody viewed it */
429 define('FEATURE_COMPLETION_TRACKS_VIEWS', 'completion_tracks_views');
430 /** True if module has custom completion rules */
431 define('FEATURE_COMPLETION_HAS_RULES', 'completion_has_rules');
433 /** True if module has no 'view' page (like label) */
434 define('FEATURE_NO_VIEW_LINK', 'viewlink');
435 /** True (which is default) if the module wants support for setting the ID number for grade calculation purposes. */
436 define('FEATURE_IDNUMBER', 'idnumber');
437 /** True if module supports groups */
438 define('FEATURE_GROUPS', 'groups');
439 /** True if module supports groupings */
440 define('FEATURE_GROUPINGS', 'groupings');
442 * True if module supports groupmembersonly (which no longer exists)
443 * @deprecated Since Moodle 2.8
445 define('FEATURE_GROUPMEMBERSONLY', 'groupmembersonly');
447 /** Type of module */
448 define('FEATURE_MOD_ARCHETYPE', 'mod_archetype');
449 /** True if module supports intro editor */
450 define('FEATURE_MOD_INTRO', 'mod_intro');
451 /** True if module has default completion */
452 define('FEATURE_MODEDIT_DEFAULT_COMPLETION', 'modedit_default_completion');
454 define('FEATURE_COMMENT', 'comment');
456 define('FEATURE_RATE', 'rate');
457 /** True if module supports backup/restore of moodle2 format */
458 define('FEATURE_BACKUP_MOODLE2', 'backup_moodle2');
460 /** True if module can show description on course main page */
461 define('FEATURE_SHOW_DESCRIPTION', 'showdescription');
463 /** True if module uses the question bank */
464 define('FEATURE_USES_QUESTIONS', 'usesquestions');
467 * Maximum filename char size
469 define('MAX_FILENAME_SIZE', 100);
471 /** Unspecified module archetype */
472 define('MOD_ARCHETYPE_OTHER', 0);
473 /** Resource-like type module */
474 define('MOD_ARCHETYPE_RESOURCE', 1);
475 /** Assignment module archetype */
476 define('MOD_ARCHETYPE_ASSIGNMENT', 2);
477 /** System (not user-addable) module archetype */
478 define('MOD_ARCHETYPE_SYSTEM', 3);
480 /** Type of module */
481 define('FEATURE_MOD_PURPOSE', 'mod_purpose');
482 /** Module purpose administration */
483 define('MOD_PURPOSE_ADMINISTRATION', 'administration');
484 /** Module purpose assessment */
485 define('MOD_PURPOSE_ASSESSMENT', 'assessment');
486 /** Module purpose communication */
487 define('MOD_PURPOSE_COLLABORATION', 'collaboration');
488 /** Module purpose communication */
489 define('MOD_PURPOSE_COMMUNICATION', 'communication');
490 /** Module purpose content */
491 define('MOD_PURPOSE_CONTENT', 'content');
492 /** Module purpose interactive content */
493 define('MOD_PURPOSE_INTERACTIVECONTENT', 'interactivecontent');
494 /** Module purpose other */
495 define('MOD_PURPOSE_OTHER', 'other');
497 * Module purpose interface
498 * @deprecated since Moodle 4.4
499 * @todo MDL-80701 Remove in Moodle 4.8
501 define('MOD_PURPOSE_INTERFACE', 'interface');
504 * Security token used for allowing access
505 * from external application such as web services.
506 * Scripts do not use any session, performance is relatively
507 * low because we need to load access info in each request.
508 * Scripts are executed in parallel.
510 define('EXTERNAL_TOKEN_PERMANENT', 0);
513 * Security token used for allowing access
514 * of embedded applications, the code is executed in the
515 * active user session. Token is invalidated after user logs out.
516 * Scripts are executed serially - normal session locking is used.
518 define('EXTERNAL_TOKEN_EMBEDDED', 1);
521 * The home page should be the site home
523 define('HOMEPAGE_SITE', 0);
525 * The home page should be the users my page
527 define('HOMEPAGE_MY', 1);
529 * The home page can be chosen by the user
531 define('HOMEPAGE_USER', 2);
533 * The home page should be the users my courses page
535 define('HOMEPAGE_MYCOURSES', 3);
538 * URL of the Moodle sites registration portal.
540 defined('HUB_MOODLEORGHUBURL') || define('HUB_MOODLEORGHUBURL', 'https://stats.moodle.org');
543 * URL of main Moodle site for marketing, products and services.
545 defined('MOODLE_PRODUCTURL') || define('MOODLE_PRODUCTURL', 'https://moodle.com');
548 * URL of the statistic server public key.
550 defined('HUB_STATSPUBLICKEY') || define('HUB_STATSPUBLICKEY', 'https://moodle.org/static/statspubkey.pem');
553 * Moodle mobile app service name
555 define('MOODLE_OFFICIAL_MOBILE_SERVICE', 'moodle_mobile_app');
558 * Indicates the user has the capabilities required to ignore activity and course file size restrictions
560 define('USER_CAN_IGNORE_FILE_SIZE_LIMITS', -1);
563 * Course display settings: display all sections on one page.
565 define('COURSE_DISPLAY_SINGLEPAGE', 0);
567 * Course display settings: split pages into a page per section.
569 define('COURSE_DISPLAY_MULTIPAGE', 1);
572 * Authentication constant: String used in password field when password is not stored.
574 define('AUTH_PASSWORD_NOT_CACHED', 'not cached');
577 * Email from header to never include via information.
579 define('EMAIL_VIA_NEVER', 0);
582 * Email from header to always include via information.
584 define('EMAIL_VIA_ALWAYS', 1);
587 * Email from header to only include via information if the address is no-reply.
589 define('EMAIL_VIA_NO_REPLY_ONLY', 2);
592 * Contact site support form/link disabled.
594 define('CONTACT_SUPPORT_DISABLED', 0);
597 * Contact site support form/link only available to authenticated users.
599 define('CONTACT_SUPPORT_AUTHENTICATED', 1);
602 * Contact site support form/link available to anyone visiting the site.
604 define('CONTACT_SUPPORT_ANYONE', 2);
607 * Maximum number of characters for password.
609 define('MAX_PASSWORD_CHARACTERS', 128);
612 * Toggle sensitive feature is disabled. Used for sensitive inputs (passwords, tokens, keys).
614 define('TOGGLE_SENSITIVE_DISABLED', 0);
617 * Toggle sensitive feature is enabled. Used for sensitive inputs (passwords, tokens, keys).
619 define('TOGGLE_SENSITIVE_ENABLED', 1);
622 * Toggle sensitive feature is enabled for small screens only. Used for sensitive inputs (passwords, tokens, keys).
624 define('TOGGLE_SENSITIVE_SMALL_SCREENS_ONLY', 2);
626 // PARAMETER HANDLING.
629 * Returns a particular value for the named variable, taken from
630 * POST or GET. If the parameter doesn't exist then an error is
631 * thrown because we require this variable.
633 * This function should be used to initialise all required values
634 * in a script that are based on parameters. Usually it will be
635 * used like this:
636 * $id = required_param('id', PARAM_INT);
638 * Please note the $type parameter is now required and the value can not be array.
640 * @param string $parname the name of the page parameter we want
641 * @param string $type expected type of parameter
642 * @return mixed
643 * @throws coding_exception
645 function required_param($parname, $type) {
646 return \core\param::from_type($type)->required_param($parname);
650 * Returns a particular array value for the named variable, taken from
651 * POST or GET. If the parameter doesn't exist then an error is
652 * thrown because we require this variable.
654 * This function should be used to initialise all required values
655 * in a script that are based on parameters. Usually it will be
656 * used like this:
657 * $ids = required_param_array('ids', PARAM_INT);
659 * Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
661 * @param string $parname the name of the page parameter we want
662 * @param string $type expected type of parameter
663 * @return array
664 * @throws coding_exception
666 function required_param_array($parname, $type) {
667 return \core\param::from_type($type)->required_param_array($parname);
671 * Returns a particular value for the named variable, taken from
672 * POST or GET, otherwise returning a given default.
674 * This function should be used to initialise all optional values
675 * in a script that are based on parameters. Usually it will be
676 * used like this:
677 * $name = optional_param('name', 'Fred', PARAM_TEXT);
679 * Please note the $type parameter is now required and the value can not be array.
681 * @param string $parname the name of the page parameter we want
682 * @param mixed $default the default value to return if nothing is found
683 * @param string $type expected type of parameter
684 * @return mixed
685 * @throws coding_exception
687 function optional_param($parname, $default, $type) {
688 return \core\param::from_type($type)->optional_param(
689 paramname: $parname,
690 default: $default,
695 * Returns a particular array value for the named variable, taken from
696 * POST or GET, otherwise returning a given default.
698 * This function should be used to initialise all optional values
699 * in a script that are based on parameters. Usually it will be
700 * used like this:
701 * $ids = optional_param('id', array(), PARAM_INT);
703 * Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
705 * @param string $parname the name of the page parameter we want
706 * @param mixed $default the default value to return if nothing is found
707 * @param string $type expected type of parameter
708 * @return array
709 * @throws coding_exception
711 function optional_param_array($parname, $default, $type) {
712 return \core\param::from_type($type)->optional_param_array(
713 paramname: $parname,
714 default: $default,
719 * Strict validation of parameter values, the values are only converted
720 * to requested PHP type. Internally it is using clean_param, the values
721 * before and after cleaning must be equal - otherwise
722 * an invalid_parameter_exception is thrown.
723 * Objects and classes are not accepted.
725 * @param mixed $param
726 * @param string $type PARAM_ constant
727 * @param bool $allownull are nulls valid value?
728 * @param string $debuginfo optional debug information
729 * @return mixed the $param value converted to PHP type
730 * @throws invalid_parameter_exception if $param is not of given type
732 function validate_param($param, $type, $allownull = NULL_NOT_ALLOWED, $debuginfo = '') {
733 return \core\param::from_type($type)->validate_param(
734 param: $param,
735 allownull: $allownull,
736 debuginfo: $debuginfo,
741 * Makes sure array contains only the allowed types, this function does not validate array key names!
743 * <code>
744 * $options = clean_param($options, PARAM_INT);
745 * </code>
747 * @param array|null $param the variable array we are cleaning
748 * @param string $type expected format of param after cleaning.
749 * @param bool $recursive clean recursive arrays
750 * @return array
751 * @throws coding_exception
753 function clean_param_array(?array $param, $type, $recursive = false) {
754 return \core\param::from_type($type)->clean_param_array(
755 param: $param,
756 recursive: $recursive,
761 * Used by {@link optional_param()} and {@link required_param()} to
762 * clean the variables and/or cast to specific types, based on
763 * an options field.
764 * <code>
765 * $course->format = clean_param($course->format, PARAM_ALPHA);
766 * $selectedgradeitem = clean_param($selectedgradeitem, PARAM_INT);
767 * </code>
769 * @param mixed $param the variable we are cleaning
770 * @param string $type expected format of param after cleaning.
771 * @return mixed
772 * @throws coding_exception
774 function clean_param($param, $type) {
775 return \core\param::from_type($type)->clean($param);
779 * Whether the PARAM_* type is compatible in RTL.
781 * Being compatible with RTL means that the data they contain can flow
782 * from right-to-left or left-to-right without compromising the user experience.
784 * Take URLs for example, they are not RTL compatible as they should always
785 * flow from the left to the right. This also applies to numbers, email addresses,
786 * configuration snippets, base64 strings, etc...
788 * This function tries to best guess which parameters can contain localised strings.
790 * @param string $paramtype Constant PARAM_*.
791 * @return bool
793 function is_rtl_compatible($paramtype) {
794 return $paramtype == PARAM_TEXT || $paramtype == PARAM_NOTAGS;
798 * Makes sure the data is using valid utf8, invalid characters are discarded.
800 * Note: this function is not intended for full objects with methods and private properties.
802 * @param mixed $value
803 * @return mixed with proper utf-8 encoding
805 function fix_utf8($value) {
806 if (is_null($value) or $value === '') {
807 return $value;
809 } else if (is_string($value)) {
810 if ((string)(int)$value === $value) {
811 // Shortcut.
812 return $value;
815 // Remove null bytes or invalid Unicode sequences from value.
816 $value = str_replace(["\0", "\xef\xbf\xbe", "\xef\xbf\xbf"], '', $value);
818 // Note: this duplicates min_fix_utf8() intentionally.
819 static $buggyiconv = null;
820 if ($buggyiconv === null) {
821 $buggyiconv = (!function_exists('iconv') or @iconv('UTF-8', 'UTF-8//IGNORE', '100'.chr(130).'€') !== '100€');
824 if ($buggyiconv) {
825 if (function_exists('mb_convert_encoding')) {
826 $subst = mb_substitute_character();
827 mb_substitute_character('none');
828 $result = mb_convert_encoding($value, 'utf-8', 'utf-8');
829 mb_substitute_character($subst);
831 } else {
832 // Warn admins on admin/index.php page.
833 $result = $value;
836 } else {
837 $result = @iconv('UTF-8', 'UTF-8//IGNORE', $value);
840 return $result;
842 } else if (is_array($value)) {
843 foreach ($value as $k => $v) {
844 $value[$k] = fix_utf8($v);
846 return $value;
848 } else if (is_object($value)) {
849 // Do not modify original.
850 $value = clone($value);
851 foreach ($value as $k => $v) {
852 $value->$k = fix_utf8($v);
854 return $value;
856 } else {
857 // This is some other type, no utf-8 here.
858 return $value;
863 * Return true if given value is integer or string with integer value
865 * @param mixed $value String or Int
866 * @return bool true if number, false if not
868 function is_number($value) {
869 if (is_int($value)) {
870 return true;
871 } else if (is_string($value)) {
872 return ((string)(int)$value) === $value;
873 } else {
874 return false;
879 * Returns host part from url.
881 * @param string $url full url
882 * @return string host, null if not found
884 function get_host_from_url($url) {
885 preg_match('|^[a-z]+://([a-zA-Z0-9-.]+)|i', $url, $matches);
886 if ($matches) {
887 return $matches[1];
889 return null;
893 * Tests whether anything was returned by text editor
895 * This function is useful for testing whether something you got back from
896 * the HTML editor actually contains anything. Sometimes the HTML editor
897 * appear to be empty, but actually you get back a <br> tag or something.
899 * @param string $string a string containing HTML.
900 * @return boolean does the string contain any actual content - that is text,
901 * images, objects, etc.
903 function html_is_blank($string) {
904 return trim(strip_tags((string)$string, '<img><object><applet><input><select><textarea><hr>')) == '';
908 * Set a key in global configuration
910 * Set a key/value pair in both this session's {@link $CFG} global variable
911 * and in the 'config' database table for future sessions.
913 * Can also be used to update keys for plugin-scoped configs in config_plugin table.
914 * In that case it doesn't affect $CFG.
916 * A NULL value will delete the entry.
918 * NOTE: this function is called from lib/db/upgrade.php
920 * @param string $name the key to set
921 * @param string|int|bool|null $value the value to set (without magic quotes),
922 * null to unset the value
923 * @param string $plugin (optional) the plugin scope, default null
924 * @return bool true or exception
926 function set_config($name, $value, $plugin = null) {
927 global $CFG, $DB;
929 // Redirect to appropriate handler when value is null.
930 if ($value === null) {
931 return unset_config($name, $plugin);
934 // Set variables determining conditions and where to store the new config.
935 // Plugin config goes to {config_plugins}, core config goes to {config}.
936 $iscore = empty($plugin);
937 if ($iscore) {
938 // If it's for core config.
939 $table = 'config';
940 $conditions = ['name' => $name];
941 $invalidatecachekey = 'core';
942 } else {
943 // If it's a plugin.
944 $table = 'config_plugins';
945 $conditions = ['name' => $name, 'plugin' => $plugin];
946 $invalidatecachekey = $plugin;
949 // DB handling - checks for existing config, updating or inserting only if necessary.
950 $invalidatecache = true;
951 $inserted = false;
952 $record = $DB->get_record($table, $conditions, 'id, value');
953 if ($record === false) {
954 // Inserts a new config record.
955 $config = new stdClass();
956 $config->name = $name;
957 $config->value = $value;
958 if (!$iscore) {
959 $config->plugin = $plugin;
961 $inserted = $DB->insert_record($table, $config, false);
962 } else if ($invalidatecache = ($record->value !== $value)) {
963 // Record exists - Check and only set new value if it has changed.
964 $DB->set_field($table, 'value', $value, ['id' => $record->id]);
967 if ($iscore && !isset($CFG->config_php_settings[$name])) {
968 // So it's defined for this invocation at least.
969 // Settings from db are always strings.
970 $CFG->$name = (string) $value;
973 // When setting config during a Behat test (in the CLI script, not in the web browser
974 // requests), remember which ones are set so that we can clear them later.
975 if ($iscore && $inserted && defined('BEHAT_TEST')) {
976 $CFG->behat_cli_added_config[$name] = true;
979 // Update siteidentifier cache, if required.
980 if ($iscore && $name === 'siteidentifier') {
981 cache_helper::update_site_identifier($value);
984 // Invalidate cache, if required.
985 if ($invalidatecache) {
986 cache_helper::invalidate_by_definition('core', 'config', [], $invalidatecachekey);
989 return true;
993 * Get configuration values from the global config table
994 * or the config_plugins table.
996 * If called with one parameter, it will load all the config
997 * variables for one plugin, and return them as an object.
999 * If called with 2 parameters it will return a string single
1000 * value or false if the value is not found.
1002 * NOTE: this function is called from lib/db/upgrade.php
1004 * @param string $plugin full component name
1005 * @param string $name default null
1006 * @return mixed hash-like object or single value, return false no config found
1007 * @throws dml_exception
1009 function get_config($plugin, $name = null) {
1010 global $CFG, $DB;
1012 if ($plugin === 'moodle' || $plugin === 'core' || empty($plugin)) {
1013 $forced =& $CFG->config_php_settings;
1014 $iscore = true;
1015 $plugin = 'core';
1016 } else {
1017 if (array_key_exists($plugin, $CFG->forced_plugin_settings)) {
1018 $forced =& $CFG->forced_plugin_settings[$plugin];
1019 } else {
1020 $forced = array();
1022 $iscore = false;
1025 if (!isset($CFG->siteidentifier)) {
1026 try {
1027 // This may throw an exception during installation, which is how we detect the
1028 // need to install the database. For more details see {@see initialise_cfg()}.
1029 $CFG->siteidentifier = $DB->get_field('config', 'value', array('name' => 'siteidentifier'));
1030 } catch (dml_exception $ex) {
1031 // Set siteidentifier to false. We don't want to trip this continually.
1032 $siteidentifier = false;
1033 throw $ex;
1037 if (!empty($name)) {
1038 if (array_key_exists($name, $forced)) {
1039 return (string)$forced[$name];
1040 } else if ($name === 'siteidentifier' && $plugin == 'core') {
1041 return $CFG->siteidentifier;
1045 $cache = cache::make('core', 'config');
1046 $result = $cache->get($plugin);
1047 if ($result === false) {
1048 // The user is after a recordset.
1049 if (!$iscore) {
1050 $result = $DB->get_records_menu('config_plugins', array('plugin' => $plugin), '', 'name,value');
1051 } else {
1052 // This part is not really used any more, but anyway...
1053 $result = $DB->get_records_menu('config', array(), '', 'name,value');;
1055 $cache->set($plugin, $result);
1058 if (!empty($name)) {
1059 if (array_key_exists($name, $result)) {
1060 return $result[$name];
1062 return false;
1065 if ($plugin === 'core') {
1066 $result['siteidentifier'] = $CFG->siteidentifier;
1069 foreach ($forced as $key => $value) {
1070 if (is_null($value) or is_array($value) or is_object($value)) {
1071 // We do not want any extra mess here, just real settings that could be saved in db.
1072 unset($result[$key]);
1073 } else {
1074 // Convert to string as if it went through the DB.
1075 $result[$key] = (string)$value;
1079 return (object)$result;
1083 * Removes a key from global configuration.
1085 * NOTE: this function is called from lib/db/upgrade.php
1087 * @param string $name the key to set
1088 * @param string $plugin (optional) the plugin scope
1089 * @return boolean whether the operation succeeded.
1091 function unset_config($name, $plugin=null) {
1092 global $CFG, $DB;
1094 if (empty($plugin)) {
1095 unset($CFG->$name);
1096 $DB->delete_records('config', array('name' => $name));
1097 cache_helper::invalidate_by_definition('core', 'config', array(), 'core');
1098 } else {
1099 $DB->delete_records('config_plugins', array('name' => $name, 'plugin' => $plugin));
1100 cache_helper::invalidate_by_definition('core', 'config', array(), $plugin);
1103 return true;
1107 * Remove all the config variables for a given plugin.
1109 * NOTE: this function is called from lib/db/upgrade.php
1111 * @param string $plugin a plugin, for example 'quiz' or 'qtype_multichoice';
1112 * @return boolean whether the operation succeeded.
1114 function unset_all_config_for_plugin($plugin) {
1115 global $DB;
1116 // Delete from the obvious config_plugins first.
1117 $DB->delete_records('config_plugins', array('plugin' => $plugin));
1118 // Next delete any suspect settings from config.
1119 $like = $DB->sql_like('name', '?', true, true, false, '|');
1120 $params = array($DB->sql_like_escape($plugin.'_', '|') . '%');
1121 $DB->delete_records_select('config', $like, $params);
1122 // Finally clear both the plugin cache and the core cache (suspect settings now removed from core).
1123 cache_helper::invalidate_by_definition('core', 'config', array(), array('core', $plugin));
1125 return true;
1129 * Use this function to get a list of users from a config setting of type admin_setting_users_with_capability.
1131 * All users are verified if they still have the necessary capability.
1133 * @param string $value the value of the config setting.
1134 * @param string $capability the capability - must match the one passed to the admin_setting_users_with_capability constructor.
1135 * @param bool $includeadmins include administrators.
1136 * @return array of user objects.
1138 function get_users_from_config($value, $capability, $includeadmins = true) {
1139 if (empty($value) or $value === '$@NONE@$') {
1140 return array();
1143 // We have to make sure that users still have the necessary capability,
1144 // it should be faster to fetch them all first and then test if they are present
1145 // instead of validating them one-by-one.
1146 $users = get_users_by_capability(context_system::instance(), $capability);
1147 if ($includeadmins) {
1148 $admins = get_admins();
1149 foreach ($admins as $admin) {
1150 $users[$admin->id] = $admin;
1154 if ($value === '$@ALL@$') {
1155 return $users;
1158 $result = array(); // Result in correct order.
1159 $allowed = explode(',', $value);
1160 foreach ($allowed as $uid) {
1161 if (isset($users[$uid])) {
1162 $user = $users[$uid];
1163 $result[$user->id] = $user;
1167 return $result;
1172 * Invalidates browser caches and cached data in temp.
1174 * @return void
1176 function purge_all_caches() {
1177 purge_caches();
1181 * Selectively invalidate different types of cache.
1183 * Purges the cache areas specified. By default, this will purge all caches but can selectively purge specific
1184 * areas alone or in combination.
1186 * @param bool[] $options Specific parts of the cache to purge. Valid options are:
1187 * 'muc' Purge MUC caches?
1188 * 'theme' Purge theme cache?
1189 * 'lang' Purge language string cache?
1190 * 'js' Purge javascript cache?
1191 * 'filter' Purge text filter cache?
1192 * 'other' Purge all other caches?
1194 function purge_caches($options = []) {
1195 $defaults = array_fill_keys(['muc', 'courses', 'theme', 'lang', 'js', 'template', 'filter', 'other'], false);
1196 if (empty(array_filter($options))) {
1197 $options = array_fill_keys(array_keys($defaults), true); // Set all options to true.
1198 } else {
1199 $options = array_merge($defaults, array_intersect_key($options, $defaults)); // Override defaults with specified options.
1201 if ($options['muc']) {
1202 cache_helper::purge_all();
1204 if ($options['courses']) {
1205 if ($options['courses'] === true) {
1206 $courseids = [];
1207 } else {
1208 $courseids = preg_split('/\s*,\s*/', $options['courses'], -1, PREG_SPLIT_NO_EMPTY);
1210 course_modinfo::purge_course_caches($courseids);
1212 if ($options['theme']) {
1213 theme_reset_all_caches();
1215 if ($options['lang']) {
1216 get_string_manager()->reset_caches();
1218 if ($options['js']) {
1219 js_reset_all_caches();
1221 if ($options['template']) {
1222 template_reset_all_caches();
1224 if ($options['filter']) {
1225 reset_text_filters_cache();
1227 if ($options['other']) {
1228 purge_other_caches();
1233 * Purge all non-MUC caches not otherwise purged in purge_caches.
1235 * IMPORTANT - If you are adding anything here to do with the cache directory you should also have a look at
1236 * {@link phpunit_util::reset_dataroot()}
1238 function purge_other_caches() {
1239 global $DB, $CFG;
1240 if (class_exists('core_plugin_manager')) {
1241 core_plugin_manager::reset_caches();
1244 // Bump up cacherev field for all courses.
1245 try {
1246 increment_revision_number('course', 'cacherev', '');
1247 } catch (moodle_exception $e) {
1248 // Ignore exception since this function is also called before upgrade script when field course.cacherev does not exist yet.
1251 $DB->reset_caches();
1253 // Purge all other caches: rss, simplepie, etc.
1254 clearstatcache();
1255 remove_dir($CFG->cachedir.'', true);
1257 // Make sure cache dir is writable, throws exception if not.
1258 make_cache_directory('');
1260 // This is the only place where we purge local caches, we are only adding files there.
1261 // The $CFG->localcachedirpurged flag forces local directories to be purged on cluster nodes.
1262 remove_dir($CFG->localcachedir, true);
1263 set_config('localcachedirpurged', time());
1264 make_localcache_directory('', true);
1265 \core\task\manager::clear_static_caches();
1269 * Get volatile flags
1271 * @param string $type
1272 * @param int $changedsince default null
1273 * @return array records array
1275 function get_cache_flags($type, $changedsince = null) {
1276 global $DB;
1278 $params = array('type' => $type, 'expiry' => time());
1279 $sqlwhere = "flagtype = :type AND expiry >= :expiry";
1280 if ($changedsince !== null) {
1281 $params['changedsince'] = $changedsince;
1282 $sqlwhere .= " AND timemodified > :changedsince";
1284 $cf = array();
1285 if ($flags = $DB->get_records_select('cache_flags', $sqlwhere, $params, '', 'name,value')) {
1286 foreach ($flags as $flag) {
1287 $cf[$flag->name] = $flag->value;
1290 return $cf;
1294 * Get volatile flags
1296 * @param string $type
1297 * @param string $name
1298 * @param int $changedsince default null
1299 * @return string|false The cache flag value or false
1301 function get_cache_flag($type, $name, $changedsince=null) {
1302 global $DB;
1304 $params = array('type' => $type, 'name' => $name, 'expiry' => time());
1306 $sqlwhere = "flagtype = :type AND name = :name AND expiry >= :expiry";
1307 if ($changedsince !== null) {
1308 $params['changedsince'] = $changedsince;
1309 $sqlwhere .= " AND timemodified > :changedsince";
1312 return $DB->get_field_select('cache_flags', 'value', $sqlwhere, $params);
1316 * Set a volatile flag
1318 * @param string $type the "type" namespace for the key
1319 * @param string $name the key to set
1320 * @param string $value the value to set (without magic quotes) - null will remove the flag
1321 * @param int $expiry (optional) epoch indicating expiry - defaults to now()+ 24hs
1322 * @return bool Always returns true
1324 function set_cache_flag($type, $name, $value, $expiry = null) {
1325 global $DB;
1327 $timemodified = time();
1328 if ($expiry === null || $expiry < $timemodified) {
1329 $expiry = $timemodified + 24 * 60 * 60;
1330 } else {
1331 $expiry = (int)$expiry;
1334 if ($value === null) {
1335 unset_cache_flag($type, $name);
1336 return true;
1339 if ($f = $DB->get_record('cache_flags', array('name' => $name, 'flagtype' => $type), '*', IGNORE_MULTIPLE)) {
1340 // This is a potential problem in DEBUG_DEVELOPER.
1341 if ($f->value == $value and $f->expiry == $expiry and $f->timemodified == $timemodified) {
1342 return true; // No need to update.
1344 $f->value = $value;
1345 $f->expiry = $expiry;
1346 $f->timemodified = $timemodified;
1347 $DB->update_record('cache_flags', $f);
1348 } else {
1349 $f = new stdClass();
1350 $f->flagtype = $type;
1351 $f->name = $name;
1352 $f->value = $value;
1353 $f->expiry = $expiry;
1354 $f->timemodified = $timemodified;
1355 $DB->insert_record('cache_flags', $f);
1357 return true;
1361 * Removes a single volatile flag
1363 * @param string $type the "type" namespace for the key
1364 * @param string $name the key to set
1365 * @return bool
1367 function unset_cache_flag($type, $name) {
1368 global $DB;
1369 $DB->delete_records('cache_flags', array('name' => $name, 'flagtype' => $type));
1370 return true;
1374 * Garbage-collect volatile flags
1376 * @return bool Always returns true
1378 function gc_cache_flags() {
1379 global $DB;
1380 $DB->delete_records_select('cache_flags', 'expiry < ?', array(time()));
1381 return true;
1384 // USER PREFERENCE API.
1387 * Refresh user preference cache. This is used most often for $USER
1388 * object that is stored in session, but it also helps with performance in cron script.
1390 * Preferences for each user are loaded on first use on every page, then again after the timeout expires.
1392 * @package core
1393 * @category preference
1394 * @access public
1395 * @param stdClass $user User object. Preferences are preloaded into 'preference' property
1396 * @param int $cachelifetime Cache life time on the current page (in seconds)
1397 * @throws coding_exception
1398 * @return null
1400 function check_user_preferences_loaded(stdClass $user, $cachelifetime = 120) {
1401 global $DB;
1402 // Static cache, we need to check on each page load, not only every 2 minutes.
1403 static $loadedusers = array();
1405 if (!isset($user->id)) {
1406 throw new coding_exception('Invalid $user parameter in check_user_preferences_loaded() call, missing id field');
1409 if (empty($user->id) or isguestuser($user->id)) {
1410 // No permanent storage for not-logged-in users and guest.
1411 if (!isset($user->preference)) {
1412 $user->preference = array();
1414 return;
1417 $timenow = time();
1419 if (isset($loadedusers[$user->id]) and isset($user->preference) and isset($user->preference['_lastloaded'])) {
1420 // Already loaded at least once on this page. Are we up to date?
1421 if ($user->preference['_lastloaded'] + $cachelifetime > $timenow) {
1422 // No need to reload - we are on the same page and we loaded prefs just a moment ago.
1423 return;
1425 } else if (!get_cache_flag('userpreferenceschanged', $user->id, $user->preference['_lastloaded'])) {
1426 // No change since the lastcheck on this page.
1427 $user->preference['_lastloaded'] = $timenow;
1428 return;
1432 // OK, so we have to reload all preferences.
1433 $loadedusers[$user->id] = true;
1434 $user->preference = $DB->get_records_menu('user_preferences', array('userid' => $user->id), '', 'name,value'); // All values.
1435 $user->preference['_lastloaded'] = $timenow;
1439 * Called from set/unset_user_preferences, so that the prefs can be correctly reloaded in different sessions.
1441 * NOTE: internal function, do not call from other code.
1443 * @package core
1444 * @access private
1445 * @param integer $userid the user whose prefs were changed.
1447 function mark_user_preferences_changed($userid) {
1448 global $CFG;
1450 if (empty($userid) or isguestuser($userid)) {
1451 // No cache flags for guest and not-logged-in users.
1452 return;
1455 set_cache_flag('userpreferenceschanged', $userid, 1, time() + $CFG->sessiontimeout);
1459 * Sets a preference for the specified user.
1461 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1463 * When additional validation/permission check is needed it is better to use {@see useredit_update_user_preference()}
1465 * @package core
1466 * @category preference
1467 * @access public
1468 * @param string $name The key to set as preference for the specified user
1469 * @param string|int|bool|null $value The value to set for the $name key in the specified user's
1470 * record, null means delete current value.
1471 * @param stdClass|int|null $user A moodle user object or id, null means current user
1472 * @throws coding_exception
1473 * @return bool Always true or exception
1475 function set_user_preference($name, $value, $user = null) {
1476 global $USER, $DB;
1478 if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
1479 throw new coding_exception('Invalid preference name in set_user_preference() call');
1482 if (is_null($value)) {
1483 // Null means delete current.
1484 return unset_user_preference($name, $user);
1485 } else if (is_object($value)) {
1486 throw new coding_exception('Invalid value in set_user_preference() call, objects are not allowed');
1487 } else if (is_array($value)) {
1488 throw new coding_exception('Invalid value in set_user_preference() call, arrays are not allowed');
1490 // Value column maximum length is 1333 characters.
1491 $value = (string)$value;
1492 if (core_text::strlen($value) > 1333) {
1493 throw new coding_exception('Invalid value in set_user_preference() call, value is is too long for the value column');
1496 if (is_null($user)) {
1497 $user = $USER;
1498 } else if (isset($user->id)) {
1499 // It is a valid object.
1500 } else if (is_numeric($user)) {
1501 $user = (object)array('id' => (int)$user);
1502 } else {
1503 throw new coding_exception('Invalid $user parameter in set_user_preference() call');
1506 check_user_preferences_loaded($user);
1508 if (empty($user->id) or isguestuser($user->id)) {
1509 // No permanent storage for not-logged-in users and guest.
1510 $user->preference[$name] = $value;
1511 return true;
1514 if ($preference = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => $name))) {
1515 if ($preference->value === $value and isset($user->preference[$name]) and $user->preference[$name] === $value) {
1516 // Preference already set to this value.
1517 return true;
1519 $DB->set_field('user_preferences', 'value', $value, array('id' => $preference->id));
1521 } else {
1522 $preference = new stdClass();
1523 $preference->userid = $user->id;
1524 $preference->name = $name;
1525 $preference->value = $value;
1526 $DB->insert_record('user_preferences', $preference);
1529 // Update value in cache.
1530 $user->preference[$name] = $value;
1531 // Update the $USER in case where we've not a direct reference to $USER.
1532 if ($user !== $USER && $user->id == $USER->id) {
1533 $USER->preference[$name] = $value;
1536 // Set reload flag for other sessions.
1537 mark_user_preferences_changed($user->id);
1539 return true;
1543 * Sets a whole array of preferences for the current user
1545 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1547 * @package core
1548 * @category preference
1549 * @access public
1550 * @param array $prefarray An array of key/value pairs to be set
1551 * @param stdClass|int|null $user A moodle user object or id, null means current user
1552 * @return bool Always true or exception
1554 function set_user_preferences(array $prefarray, $user = null) {
1555 foreach ($prefarray as $name => $value) {
1556 set_user_preference($name, $value, $user);
1558 return true;
1562 * Unsets a preference completely by deleting it from the database
1564 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1566 * @package core
1567 * @category preference
1568 * @access public
1569 * @param string $name The key to unset as preference for the specified user
1570 * @param stdClass|int|null $user A moodle user object or id, null means current user
1571 * @throws coding_exception
1572 * @return bool Always true or exception
1574 function unset_user_preference($name, $user = null) {
1575 global $USER, $DB;
1577 if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
1578 throw new coding_exception('Invalid preference name in unset_user_preference() call');
1581 if (is_null($user)) {
1582 $user = $USER;
1583 } else if (isset($user->id)) {
1584 // It is a valid object.
1585 } else if (is_numeric($user)) {
1586 $user = (object)array('id' => (int)$user);
1587 } else {
1588 throw new coding_exception('Invalid $user parameter in unset_user_preference() call');
1591 check_user_preferences_loaded($user);
1593 if (empty($user->id) or isguestuser($user->id)) {
1594 // No permanent storage for not-logged-in user and guest.
1595 unset($user->preference[$name]);
1596 return true;
1599 // Delete from DB.
1600 $DB->delete_records('user_preferences', array('userid' => $user->id, 'name' => $name));
1602 // Delete the preference from cache.
1603 unset($user->preference[$name]);
1604 // Update the $USER in case where we've not a direct reference to $USER.
1605 if ($user !== $USER && $user->id == $USER->id) {
1606 unset($USER->preference[$name]);
1609 // Set reload flag for other sessions.
1610 mark_user_preferences_changed($user->id);
1612 return true;
1616 * Used to fetch user preference(s)
1618 * If no arguments are supplied this function will return
1619 * all of the current user preferences as an array.
1621 * If a name is specified then this function
1622 * attempts to return that particular preference value. If
1623 * none is found, then the optional value $default is returned,
1624 * otherwise null.
1626 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1628 * @package core
1629 * @category preference
1630 * @access public
1631 * @param string $name Name of the key to use in finding a preference value
1632 * @param mixed|null $default Value to be returned if the $name key is not set in the user preferences
1633 * @param stdClass|int|null $user A moodle user object or id, null means current user
1634 * @throws coding_exception
1635 * @return string|mixed|null A string containing the value of a single preference. An
1636 * array with all of the preferences or null
1638 function get_user_preferences($name = null, $default = null, $user = null) {
1639 global $USER;
1641 if (is_null($name)) {
1642 // All prefs.
1643 } else if (is_numeric($name) or $name === '_lastloaded') {
1644 throw new coding_exception('Invalid preference name in get_user_preferences() call');
1647 if (is_null($user)) {
1648 $user = $USER;
1649 } else if (isset($user->id)) {
1650 // Is a valid object.
1651 } else if (is_numeric($user)) {
1652 if ($USER->id == $user) {
1653 $user = $USER;
1654 } else {
1655 $user = (object)array('id' => (int)$user);
1657 } else {
1658 throw new coding_exception('Invalid $user parameter in get_user_preferences() call');
1661 check_user_preferences_loaded($user);
1663 if (empty($name)) {
1664 // All values.
1665 return $user->preference;
1666 } else if (isset($user->preference[$name])) {
1667 // The single string value.
1668 return $user->preference[$name];
1669 } else {
1670 // Default value (null if not specified).
1671 return $default;
1675 // FUNCTIONS FOR HANDLING TIME.
1678 * Given Gregorian date parts in user time produce a GMT timestamp.
1680 * @package core
1681 * @category time
1682 * @param int $year The year part to create timestamp of
1683 * @param int $month The month part to create timestamp of
1684 * @param int $day The day part to create timestamp of
1685 * @param int $hour The hour part to create timestamp of
1686 * @param int $minute The minute part to create timestamp of
1687 * @param int $second The second part to create timestamp of
1688 * @param int|float|string $timezone Timezone modifier, used to calculate GMT time offset.
1689 * if 99 then default user's timezone is used {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
1690 * @param bool $applydst Toggle Daylight Saving Time, default true, will be
1691 * applied only if timezone is 99 or string.
1692 * @return int GMT timestamp
1694 function make_timestamp($year, $month=1, $day=1, $hour=0, $minute=0, $second=0, $timezone=99, $applydst=true) {
1695 $date = new DateTime('now', core_date::get_user_timezone_object($timezone));
1696 $date->setDate((int)$year, (int)$month, (int)$day);
1697 $date->setTime((int)$hour, (int)$minute, (int)$second);
1699 $time = $date->getTimestamp();
1701 if ($time === false) {
1702 throw new coding_exception('getTimestamp() returned false, please ensure you have passed correct values.'.
1703 ' This can fail if year is more than 2038 and OS is 32 bit windows');
1706 // Moodle BC DST stuff.
1707 if (!$applydst) {
1708 $time += dst_offset_on($time, $timezone);
1711 return $time;
1716 * Format a date/time (seconds) as weeks, days, hours etc as needed
1718 * Given an amount of time in seconds, returns string
1719 * formatted nicely as years, days, hours etc as needed
1721 * @package core
1722 * @category time
1723 * @uses MINSECS
1724 * @uses HOURSECS
1725 * @uses DAYSECS
1726 * @uses YEARSECS
1727 * @param int $totalsecs Time in seconds
1728 * @param stdClass $str Should be a time object
1729 * @return string A nicely formatted date/time string
1731 function format_time($totalsecs, $str = null) {
1733 $totalsecs = abs($totalsecs);
1735 if (!$str) {
1736 // Create the str structure the slow way.
1737 $str = new stdClass();
1738 $str->day = get_string('day');
1739 $str->days = get_string('days');
1740 $str->hour = get_string('hour');
1741 $str->hours = get_string('hours');
1742 $str->min = get_string('min');
1743 $str->mins = get_string('mins');
1744 $str->sec = get_string('sec');
1745 $str->secs = get_string('secs');
1746 $str->year = get_string('year');
1747 $str->years = get_string('years');
1750 $years = floor($totalsecs/YEARSECS);
1751 $remainder = $totalsecs - ($years*YEARSECS);
1752 $days = floor($remainder/DAYSECS);
1753 $remainder = $totalsecs - ($days*DAYSECS);
1754 $hours = floor($remainder/HOURSECS);
1755 $remainder = $remainder - ($hours*HOURSECS);
1756 $mins = floor($remainder/MINSECS);
1757 $secs = $remainder - ($mins*MINSECS);
1759 $ss = ($secs == 1) ? $str->sec : $str->secs;
1760 $sm = ($mins == 1) ? $str->min : $str->mins;
1761 $sh = ($hours == 1) ? $str->hour : $str->hours;
1762 $sd = ($days == 1) ? $str->day : $str->days;
1763 $sy = ($years == 1) ? $str->year : $str->years;
1765 $oyears = '';
1766 $odays = '';
1767 $ohours = '';
1768 $omins = '';
1769 $osecs = '';
1771 if ($years) {
1772 $oyears = $years .' '. $sy;
1774 if ($days) {
1775 $odays = $days .' '. $sd;
1777 if ($hours) {
1778 $ohours = $hours .' '. $sh;
1780 if ($mins) {
1781 $omins = $mins .' '. $sm;
1783 if ($secs) {
1784 $osecs = $secs .' '. $ss;
1787 if ($years) {
1788 return trim($oyears .' '. $odays);
1790 if ($days) {
1791 return trim($odays .' '. $ohours);
1793 if ($hours) {
1794 return trim($ohours .' '. $omins);
1796 if ($mins) {
1797 return trim($omins .' '. $osecs);
1799 if ($secs) {
1800 return $osecs;
1802 return get_string('now');
1806 * Returns a formatted string that represents a date in user time.
1808 * @package core
1809 * @category time
1810 * @param int $date the timestamp in UTC, as obtained from the database.
1811 * @param string $format strftime format. You should probably get this using
1812 * get_string('strftime...', 'langconfig');
1813 * @param int|float|string $timezone by default, uses the user's time zone. if numeric and
1814 * not 99 then daylight saving will not be added.
1815 * {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
1816 * @param bool $fixday If true (default) then the leading zero from %d is removed.
1817 * If false then the leading zero is maintained.
1818 * @param bool $fixhour If true (default) then the leading zero from %I is removed.
1819 * @return string the formatted date/time.
1821 function userdate($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) {
1822 $calendartype = \core_calendar\type_factory::get_calendar_instance();
1823 return $calendartype->timestamp_to_date_string($date, $format, $timezone, $fixday, $fixhour);
1827 * Returns a html "time" tag with both the exact user date with timezone information
1828 * as a datetime attribute in the W3C format, and the user readable date and time as text.
1830 * @package core
1831 * @category time
1832 * @param int $date the timestamp in UTC, as obtained from the database.
1833 * @param string $format strftime format. You should probably get this using
1834 * get_string('strftime...', 'langconfig');
1835 * @param int|float|string $timezone by default, uses the user's time zone. if numeric and
1836 * not 99 then daylight saving will not be added.
1837 * {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
1838 * @param bool $fixday If true (default) then the leading zero from %d is removed.
1839 * If false then the leading zero is maintained.
1840 * @param bool $fixhour If true (default) then the leading zero from %I is removed.
1841 * @return string the formatted date/time.
1843 function userdate_htmltime($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) {
1844 $userdatestr = userdate($date, $format, $timezone, $fixday, $fixhour);
1845 if (CLI_SCRIPT && !PHPUNIT_TEST) {
1846 return $userdatestr;
1848 $machinedate = new DateTime();
1849 $machinedate->setTimestamp(intval($date));
1850 $machinedate->setTimezone(core_date::get_user_timezone_object());
1852 return html_writer::tag('time', $userdatestr, ['datetime' => $machinedate->format(DateTime::W3C)]);
1856 * Returns a formatted date ensuring it is UTF-8.
1858 * If we are running under Windows convert to Windows encoding and then back to UTF-8
1859 * (because it's impossible to specify UTF-8 to fetch locale info in Win32).
1861 * @param int $date the timestamp - since Moodle 2.9 this is a real UTC timestamp
1862 * @param string $format strftime format.
1863 * @param int|float|string $tz the user timezone
1864 * @return string the formatted date/time.
1865 * @since Moodle 2.3.3
1867 function date_format_string($date, $format, $tz = 99) {
1869 date_default_timezone_set(core_date::get_user_timezone($tz));
1871 if (date('A', 0) === date('A', HOURSECS * 18)) {
1872 $datearray = getdate($date);
1873 $format = str_replace([
1874 '%P',
1875 '%p',
1876 ], [
1877 $datearray['hours'] < 12 ? get_string('am', 'langconfig') : get_string('pm', 'langconfig'),
1878 $datearray['hours'] < 12 ? get_string('amcaps', 'langconfig') : get_string('pmcaps', 'langconfig'),
1879 ], $format);
1882 $datestring = core_date::strftime($format, $date);
1883 core_date::set_default_server_timezone();
1885 return $datestring;
1889 * Given a $time timestamp in GMT (seconds since epoch),
1890 * returns an array that represents the Gregorian date in user time
1892 * @package core
1893 * @category time
1894 * @param int $time Timestamp in GMT
1895 * @param float|int|string $timezone user timezone
1896 * @return array An array that represents the date in user time
1898 function usergetdate($time, $timezone=99) {
1899 if ($time === null) {
1900 // PHP8 and PHP7 return different results when getdate(null) is called.
1901 // Display warning and cast to 0 to make sure the usergetdate() behaves consistently on all versions of PHP.
1902 // In the future versions of Moodle we may consider adding a strict typehint.
1903 debugging('usergetdate() expects parameter $time to be int, null given', DEBUG_DEVELOPER);
1904 $time = 0;
1907 date_default_timezone_set(core_date::get_user_timezone($timezone));
1908 $result = getdate($time);
1909 core_date::set_default_server_timezone();
1911 return $result;
1915 * Given a GMT timestamp (seconds since epoch), offsets it by
1916 * the timezone. eg 3pm in India is 3pm GMT - 7 * 3600 seconds
1918 * NOTE: this function does not include DST properly,
1919 * you should use the PHP date stuff instead!
1921 * @package core
1922 * @category time
1923 * @param int $date Timestamp in GMT
1924 * @param float|int|string $timezone user timezone
1925 * @return int
1927 function usertime($date, $timezone=99) {
1928 $userdate = new DateTime('@' . $date);
1929 $userdate->setTimezone(core_date::get_user_timezone_object($timezone));
1930 $dst = dst_offset_on($date, $timezone);
1932 return $date - $userdate->getOffset() + $dst;
1936 * Get a formatted string representation of an interval between two unix timestamps.
1938 * E.g.
1939 * $intervalstring = get_time_interval_string(12345600, 12345660);
1940 * Will produce the string:
1941 * '0d 0h 1m'
1943 * @param int $time1 unix timestamp
1944 * @param int $time2 unix timestamp
1945 * @param string $format string (can be lang string) containing format chars: https://www.php.net/manual/en/dateinterval.format.php.
1946 * @param bool $dropzeroes If format is not provided and this is set to true, do not include zero time units.
1947 * e.g. a duration of 3 days and 2 hours will be displayed as '3d 2h' instead of '3d 2h 0s'
1948 * @param bool $fullformat If format is not provided and this is set to true, display time units in full format.
1949 * e.g. instead of showing "3d", "3 days" will be returned.
1950 * @return string the formatted string describing the time difference, e.g. '10d 11h 45m'.
1952 function get_time_interval_string(int $time1, int $time2, string $format = '',
1953 bool $dropzeroes = false, bool $fullformat = false): string {
1954 $dtdate = new DateTime();
1955 $dtdate->setTimeStamp($time1);
1956 $dtdate2 = new DateTime();
1957 $dtdate2->setTimeStamp($time2);
1958 $interval = $dtdate2->diff($dtdate);
1960 if (empty(trim($format))) {
1961 // Default to this key.
1962 $formatkey = 'dateintervaldayhrmin';
1964 if ($dropzeroes) {
1965 $units = [
1966 'y' => 'yr',
1967 'm' => 'mo',
1968 'd' => 'day',
1969 'h' => 'hr',
1970 'i' => 'min',
1971 's' => 'sec',
1973 $formatunits = [];
1974 foreach ($units as $key => $unit) {
1975 if (empty($interval->$key)) {
1976 continue;
1978 $formatunits[] = $unit;
1980 if (!empty($formatunits)) {
1981 $formatkey = 'dateinterval' . implode("", $formatunits);
1985 if ($fullformat) {
1986 $formatkey .= 'full';
1988 $format = get_string($formatkey, 'langconfig');
1990 return $interval->format($format);
1994 * Given a time, return the GMT timestamp of the most recent midnight
1995 * for the current user.
1997 * @package core
1998 * @category time
1999 * @param int $date Timestamp in GMT
2000 * @param float|int|string $timezone user timezone
2001 * @return int Returns a GMT timestamp
2003 function usergetmidnight($date, $timezone=99) {
2005 $userdate = usergetdate($date, $timezone);
2007 // Time of midnight of this user's day, in GMT.
2008 return make_timestamp($userdate['year'], $userdate['mon'], $userdate['mday'], 0, 0, 0, $timezone);
2013 * Returns a string that prints the user's timezone
2015 * @package core
2016 * @category time
2017 * @param float|int|string $timezone user timezone
2018 * @return string
2020 function usertimezone($timezone=99) {
2021 $tz = core_date::get_user_timezone($timezone);
2022 return core_date::get_localised_timezone($tz);
2026 * Returns a float or a string which denotes the user's timezone
2027 * 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)
2028 * means that for this timezone there are also DST rules to be taken into account
2029 * Checks various settings and picks the most dominant of those which have a value
2031 * @package core
2032 * @category time
2033 * @param float|int|string $tz timezone to calculate GMT time offset before
2034 * calculating user timezone, 99 is default user timezone
2035 * {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
2036 * @return float|string
2038 function get_user_timezone($tz = 99) {
2039 global $USER, $CFG;
2041 $timezones = array(
2042 $tz,
2043 isset($CFG->forcetimezone) ? $CFG->forcetimezone : 99,
2044 isset($USER->timezone) ? $USER->timezone : 99,
2045 isset($CFG->timezone) ? $CFG->timezone : 99,
2048 $tz = 99;
2050 // Loop while $tz is, empty but not zero, or 99, and there is another timezone is the array.
2051 foreach ($timezones as $nextvalue) {
2052 if ((empty($tz) && !is_numeric($tz)) || $tz == 99) {
2053 $tz = $nextvalue;
2056 return is_numeric($tz) ? (float) $tz : $tz;
2060 * Calculates the Daylight Saving Offset for a given date/time (timestamp)
2061 * - Note: Daylight saving only works for string timezones and not for float.
2063 * @package core
2064 * @category time
2065 * @param int $time must NOT be compensated at all, it has to be a pure timestamp
2066 * @param int|float|string $strtimezone user timezone
2067 * @return int
2069 function dst_offset_on($time, $strtimezone = null) {
2070 $tz = core_date::get_user_timezone($strtimezone);
2071 $date = new DateTime('@' . $time);
2072 $date->setTimezone(new DateTimeZone($tz));
2073 if ($date->format('I') == '1') {
2074 if ($tz === 'Australia/Lord_Howe') {
2075 return 1800;
2077 return 3600;
2079 return 0;
2083 * Calculates when the day appears in specific month
2085 * @package core
2086 * @category time
2087 * @param int $startday starting day of the month
2088 * @param int $weekday The day when week starts (normally taken from user preferences)
2089 * @param int $month The month whose day is sought
2090 * @param int $year The year of the month whose day is sought
2091 * @return int
2093 function find_day_in_month($startday, $weekday, $month, $year) {
2094 $calendartype = \core_calendar\type_factory::get_calendar_instance();
2096 $daysinmonth = days_in_month($month, $year);
2097 $daysinweek = count($calendartype->get_weekdays());
2099 if ($weekday == -1) {
2100 // Don't care about weekday, so return:
2101 // abs($startday) if $startday != -1
2102 // $daysinmonth otherwise.
2103 return ($startday == -1) ? $daysinmonth : abs($startday);
2106 // From now on we 're looking for a specific weekday.
2107 // Give "end of month" its actual value, since we know it.
2108 if ($startday == -1) {
2109 $startday = -1 * $daysinmonth;
2112 // Starting from day $startday, the sign is the direction.
2113 if ($startday < 1) {
2114 $startday = abs($startday);
2115 $lastmonthweekday = dayofweek($daysinmonth, $month, $year);
2117 // This is the last such weekday of the month.
2118 $lastinmonth = $daysinmonth + $weekday - $lastmonthweekday;
2119 if ($lastinmonth > $daysinmonth) {
2120 $lastinmonth -= $daysinweek;
2123 // Find the first such weekday <= $startday.
2124 while ($lastinmonth > $startday) {
2125 $lastinmonth -= $daysinweek;
2128 return $lastinmonth;
2129 } else {
2130 $indexweekday = dayofweek($startday, $month, $year);
2132 $diff = $weekday - $indexweekday;
2133 if ($diff < 0) {
2134 $diff += $daysinweek;
2137 // This is the first such weekday of the month equal to or after $startday.
2138 $firstfromindex = $startday + $diff;
2140 return $firstfromindex;
2145 * Calculate the number of days in a given month
2147 * @package core
2148 * @category time
2149 * @param int $month The month whose day count is sought
2150 * @param int $year The year of the month whose day count is sought
2151 * @return int
2153 function days_in_month($month, $year) {
2154 $calendartype = \core_calendar\type_factory::get_calendar_instance();
2155 return $calendartype->get_num_days_in_month($year, $month);
2159 * Calculate the position in the week of a specific calendar day
2161 * @package core
2162 * @category time
2163 * @param int $day The day of the date whose position in the week is sought
2164 * @param int $month The month of the date whose position in the week is sought
2165 * @param int $year The year of the date whose position in the week is sought
2166 * @return int
2168 function dayofweek($day, $month, $year) {
2169 $calendartype = \core_calendar\type_factory::get_calendar_instance();
2170 return $calendartype->get_weekday($year, $month, $day);
2173 // USER AUTHENTICATION AND LOGIN.
2176 * Returns full login url.
2178 * Any form submissions for authentication to this URL must include username,
2179 * password as well as a logintoken generated by \core\session\manager::get_login_token().
2181 * @return string login url
2183 function get_login_url() {
2184 global $CFG;
2186 return "$CFG->wwwroot/login/index.php";
2190 * This function checks that the current user is logged in and has the
2191 * required privileges
2193 * This function checks that the current user is logged in, and optionally
2194 * whether they are allowed to be in a particular course and view a particular
2195 * course module.
2196 * If they are not logged in, then it redirects them to the site login unless
2197 * $autologinguest is set and {@link $CFG}->autologinguests is set to 1 in which
2198 * case they are automatically logged in as guests.
2199 * If $courseid is given and the user is not enrolled in that course then the
2200 * user is redirected to the course enrolment page.
2201 * If $cm is given and the course module is hidden and the user is not a teacher
2202 * in the course then the user is redirected to the course home page.
2204 * When $cm parameter specified, this function sets page layout to 'module'.
2205 * You need to change it manually later if some other layout needed.
2207 * @package core_access
2208 * @category access
2210 * @param mixed $courseorid id of the course or course object
2211 * @param bool $autologinguest default true
2212 * @param object $cm course module object
2213 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
2214 * true. Used to avoid (=false) some scripts (file.php...) to set that variable,
2215 * in order to keep redirects working properly. MDL-14495
2216 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
2217 * @return mixed Void, exit, and die depending on path
2218 * @throws coding_exception
2219 * @throws require_login_exception
2220 * @throws moodle_exception
2222 function require_login($courseorid = null, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) {
2223 global $CFG, $SESSION, $USER, $PAGE, $SITE, $DB, $OUTPUT;
2225 // Must not redirect when byteserving already started.
2226 if (!empty($_SERVER['HTTP_RANGE'])) {
2227 $preventredirect = true;
2230 if (AJAX_SCRIPT) {
2231 // We cannot redirect for AJAX scripts either.
2232 $preventredirect = true;
2235 // Setup global $COURSE, themes, language and locale.
2236 if (!empty($courseorid)) {
2237 if (is_object($courseorid)) {
2238 $course = $courseorid;
2239 } else if ($courseorid == SITEID) {
2240 $course = clone($SITE);
2241 } else {
2242 $course = $DB->get_record('course', array('id' => $courseorid), '*', MUST_EXIST);
2244 if ($cm) {
2245 if ($cm->course != $course->id) {
2246 throw new coding_exception('course and cm parameters in require_login() call do not match!!');
2248 // Make sure we have a $cm from get_fast_modinfo as this contains activity access details.
2249 if (!($cm instanceof cm_info)) {
2250 // Note: nearly all pages call get_fast_modinfo anyway and it does not make any
2251 // db queries so this is not really a performance concern, however it is obviously
2252 // better if you use get_fast_modinfo to get the cm before calling this.
2253 $modinfo = get_fast_modinfo($course);
2254 $cm = $modinfo->get_cm($cm->id);
2257 } else {
2258 // Do not touch global $COURSE via $PAGE->set_course(),
2259 // the reasons is we need to be able to call require_login() at any time!!
2260 $course = $SITE;
2261 if ($cm) {
2262 throw new coding_exception('cm parameter in require_login() requires valid course parameter!');
2266 // If this is an AJAX request and $setwantsurltome is true then we need to override it and set it to false.
2267 // Otherwise the AJAX request URL will be set to $SESSION->wantsurl and events such as self enrolment in the future
2268 // risk leading the user back to the AJAX request URL.
2269 if ($setwantsurltome && defined('AJAX_SCRIPT') && AJAX_SCRIPT) {
2270 $setwantsurltome = false;
2273 // Redirect to the login page if session has expired, only with dbsessions enabled (MDL-35029) to maintain current behaviour.
2274 if ((!isloggedin() or isguestuser()) && !empty($SESSION->has_timed_out) && !empty($CFG->dbsessions)) {
2275 if ($preventredirect) {
2276 throw new require_login_session_timeout_exception();
2277 } else {
2278 if ($setwantsurltome) {
2279 $SESSION->wantsurl = qualified_me();
2281 redirect(get_login_url());
2285 // If the user is not even logged in yet then make sure they are.
2286 if (!isloggedin()) {
2287 if ($autologinguest && !empty($CFG->autologinguests)) {
2288 if (!$guest = get_complete_user_data('id', $CFG->siteguest)) {
2289 // Misconfigured site guest, just redirect to login page.
2290 redirect(get_login_url());
2291 exit; // Never reached.
2293 $lang = isset($SESSION->lang) ? $SESSION->lang : $CFG->lang;
2294 complete_user_login($guest);
2295 $USER->autologinguest = true;
2296 $SESSION->lang = $lang;
2297 } else {
2298 // NOTE: $USER->site check was obsoleted by session test cookie, $USER->confirmed test is in login/index.php.
2299 if ($preventredirect) {
2300 throw new require_login_exception('You are not logged in');
2303 if ($setwantsurltome) {
2304 $SESSION->wantsurl = qualified_me();
2307 // Give auth plugins an opportunity to authenticate or redirect to an external login page
2308 $authsequence = get_enabled_auth_plugins(); // Auths, in sequence.
2309 foreach($authsequence as $authname) {
2310 $authplugin = get_auth_plugin($authname);
2311 $authplugin->pre_loginpage_hook();
2312 if (isloggedin()) {
2313 if ($cm) {
2314 $modinfo = get_fast_modinfo($course);
2315 $cm = $modinfo->get_cm($cm->id);
2317 set_access_log_user();
2318 break;
2322 // If we're still not logged in then go to the login page
2323 if (!isloggedin()) {
2324 redirect(get_login_url());
2325 exit; // Never reached.
2330 // Loginas as redirection if needed.
2331 if ($course->id != SITEID and \core\session\manager::is_loggedinas()) {
2332 if ($USER->loginascontext->contextlevel == CONTEXT_COURSE) {
2333 if ($USER->loginascontext->instanceid != $course->id) {
2334 throw new \moodle_exception('loginasonecourse', '',
2335 $CFG->wwwroot.'/course/view.php?id='.$USER->loginascontext->instanceid);
2340 // Check whether the user should be changing password (but only if it is REALLY them).
2341 if (get_user_preferences('auth_forcepasswordchange') && !\core\session\manager::is_loggedinas()) {
2342 $userauth = get_auth_plugin($USER->auth);
2343 if ($userauth->can_change_password() and !$preventredirect) {
2344 if ($setwantsurltome) {
2345 $SESSION->wantsurl = qualified_me();
2347 if ($changeurl = $userauth->change_password_url()) {
2348 // Use plugin custom url.
2349 redirect($changeurl);
2350 } else {
2351 // Use moodle internal method.
2352 redirect($CFG->wwwroot .'/login/change_password.php');
2354 } else if ($userauth->can_change_password()) {
2355 throw new moodle_exception('forcepasswordchangenotice');
2356 } else {
2357 throw new moodle_exception('nopasswordchangeforced', 'auth');
2361 // Check that the user account is properly set up. If we can't redirect to
2362 // edit their profile and this is not a WS request, perform just the lax check.
2363 // It will allow them to use filepicker on the profile edit page.
2365 if ($preventredirect && !WS_SERVER) {
2366 $usernotfullysetup = user_not_fully_set_up($USER, false);
2367 } else {
2368 $usernotfullysetup = user_not_fully_set_up($USER, true);
2371 if ($usernotfullysetup) {
2372 if ($preventredirect) {
2373 throw new moodle_exception('usernotfullysetup');
2375 if ($setwantsurltome) {
2376 $SESSION->wantsurl = qualified_me();
2378 redirect($CFG->wwwroot .'/user/edit.php?id='. $USER->id .'&amp;course='. SITEID);
2381 // Make sure the USER has a sesskey set up. Used for CSRF protection.
2382 sesskey();
2384 if (\core\session\manager::is_loggedinas()) {
2385 // During a "logged in as" session we should force all content to be cleaned because the
2386 // logged in user will be viewing potentially malicious user generated content.
2387 // See MDL-63786 for more details.
2388 $CFG->forceclean = true;
2391 $afterlogins = get_plugins_with_function('after_require_login', 'lib.php');
2393 // Do not bother admins with any formalities, except for activities pending deletion.
2394 if (is_siteadmin() && !($cm && $cm->deletioninprogress)) {
2395 // Set the global $COURSE.
2396 if ($cm) {
2397 $PAGE->set_cm($cm, $course);
2398 $PAGE->set_pagelayout('incourse');
2399 } else if (!empty($courseorid)) {
2400 $PAGE->set_course($course);
2402 // Set accesstime or the user will appear offline which messes up messaging.
2403 // Do not update access time for webservice or ajax requests.
2404 if (!WS_SERVER && !AJAX_SCRIPT) {
2405 user_accesstime_log($course->id);
2408 foreach ($afterlogins as $plugintype => $plugins) {
2409 foreach ($plugins as $pluginfunction) {
2410 $pluginfunction($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2413 return;
2416 // Scripts have a chance to declare that $USER->policyagreed should not be checked.
2417 // This is mostly for places where users are actually accepting the policies, to avoid the redirect loop.
2418 if (!defined('NO_SITEPOLICY_CHECK')) {
2419 define('NO_SITEPOLICY_CHECK', false);
2422 // Check that the user has agreed to a site policy if there is one - do not test in case of admins.
2423 // Do not test if the script explicitly asked for skipping the site policies check.
2424 // Or if the user auth type is webservice.
2425 if (!$USER->policyagreed && !is_siteadmin() && !NO_SITEPOLICY_CHECK && $USER->auth !== 'webservice') {
2426 $manager = new \core_privacy\local\sitepolicy\manager();
2427 if ($policyurl = $manager->get_redirect_url(isguestuser())) {
2428 if ($preventredirect) {
2429 throw new moodle_exception('sitepolicynotagreed', 'error', '', $policyurl->out());
2431 if ($setwantsurltome) {
2432 $SESSION->wantsurl = qualified_me();
2434 redirect($policyurl);
2438 // Fetch the system context, the course context, and prefetch its child contexts.
2439 $sysctx = context_system::instance();
2440 $coursecontext = context_course::instance($course->id, MUST_EXIST);
2441 if ($cm) {
2442 $cmcontext = context_module::instance($cm->id, MUST_EXIST);
2443 } else {
2444 $cmcontext = null;
2447 // If the site is currently under maintenance, then print a message.
2448 if (!empty($CFG->maintenance_enabled) and !has_capability('moodle/site:maintenanceaccess', $sysctx)) {
2449 if ($preventredirect) {
2450 throw new require_login_exception('Maintenance in progress');
2452 $PAGE->set_context(null);
2453 print_maintenance_message();
2456 // Make sure the course itself is not hidden.
2457 if ($course->id == SITEID) {
2458 // Frontpage can not be hidden.
2459 } else {
2460 if (is_role_switched($course->id)) {
2461 // When switching roles ignore the hidden flag - user had to be in course to do the switch.
2462 } else {
2463 if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
2464 // Originally there was also test of parent category visibility, BUT is was very slow in complex queries
2465 // involving "my courses" now it is also possible to simply hide all courses user is not enrolled in :-).
2466 if ($preventredirect) {
2467 throw new require_login_exception('Course is hidden');
2469 $PAGE->set_context(null);
2470 // We need to override the navigation URL as the course won't have been added to the navigation and thus
2471 // the navigation will mess up when trying to find it.
2472 navigation_node::override_active_url(new moodle_url('/'));
2473 notice(get_string('coursehidden'), $CFG->wwwroot .'/');
2478 // Is the user enrolled?
2479 if ($course->id == SITEID) {
2480 // Everybody is enrolled on the frontpage.
2481 } else {
2482 if (\core\session\manager::is_loggedinas()) {
2483 // Make sure the REAL person can access this course first.
2484 $realuser = \core\session\manager::get_realuser();
2485 if (!is_enrolled($coursecontext, $realuser->id, '', true) and
2486 !is_viewing($coursecontext, $realuser->id) and !is_siteadmin($realuser->id)) {
2487 if ($preventredirect) {
2488 throw new require_login_exception('Invalid course login-as access');
2490 $PAGE->set_context(null);
2491 echo $OUTPUT->header();
2492 notice(get_string('studentnotallowed', '', fullname($USER, true)), $CFG->wwwroot .'/');
2496 $access = false;
2498 if (is_role_switched($course->id)) {
2499 // Ok, user had to be inside this course before the switch.
2500 $access = true;
2502 } else if (is_viewing($coursecontext, $USER)) {
2503 // Ok, no need to mess with enrol.
2504 $access = true;
2506 } else {
2507 if (isset($USER->enrol['enrolled'][$course->id])) {
2508 if ($USER->enrol['enrolled'][$course->id] > time()) {
2509 $access = true;
2510 if (isset($USER->enrol['tempguest'][$course->id])) {
2511 unset($USER->enrol['tempguest'][$course->id]);
2512 remove_temp_course_roles($coursecontext);
2514 } else {
2515 // Expired.
2516 unset($USER->enrol['enrolled'][$course->id]);
2519 if (isset($USER->enrol['tempguest'][$course->id])) {
2520 if ($USER->enrol['tempguest'][$course->id] == 0) {
2521 $access = true;
2522 } else if ($USER->enrol['tempguest'][$course->id] > time()) {
2523 $access = true;
2524 } else {
2525 // Expired.
2526 unset($USER->enrol['tempguest'][$course->id]);
2527 remove_temp_course_roles($coursecontext);
2531 if (!$access) {
2532 // Cache not ok.
2533 $until = enrol_get_enrolment_end($coursecontext->instanceid, $USER->id);
2534 if ($until !== false) {
2535 // Active participants may always access, a timestamp in the future, 0 (always) or false.
2536 if ($until == 0) {
2537 $until = ENROL_MAX_TIMESTAMP;
2539 $USER->enrol['enrolled'][$course->id] = $until;
2540 $access = true;
2542 } else if (core_course_category::can_view_course_info($course)) {
2543 $params = array('courseid' => $course->id, 'status' => ENROL_INSTANCE_ENABLED);
2544 $instances = $DB->get_records('enrol', $params, 'sortorder, id ASC');
2545 $enrols = enrol_get_plugins(true);
2546 // First ask all enabled enrol instances in course if they want to auto enrol user.
2547 foreach ($instances as $instance) {
2548 if (!isset($enrols[$instance->enrol])) {
2549 continue;
2551 // Get a duration for the enrolment, a timestamp in the future, 0 (always) or false.
2552 $until = $enrols[$instance->enrol]->try_autoenrol($instance);
2553 if ($until !== false) {
2554 if ($until == 0) {
2555 $until = ENROL_MAX_TIMESTAMP;
2557 $USER->enrol['enrolled'][$course->id] = $until;
2558 $access = true;
2559 break;
2562 // If not enrolled yet try to gain temporary guest access.
2563 if (!$access) {
2564 foreach ($instances as $instance) {
2565 if (!isset($enrols[$instance->enrol])) {
2566 continue;
2568 // Get a duration for the guest access, a timestamp in the future or false.
2569 $until = $enrols[$instance->enrol]->try_guestaccess($instance);
2570 if ($until !== false and $until > time()) {
2571 $USER->enrol['tempguest'][$course->id] = $until;
2572 $access = true;
2573 break;
2577 } else {
2578 // User is not enrolled and is not allowed to browse courses here.
2579 if ($preventredirect) {
2580 throw new require_login_exception('Course is not available');
2582 $PAGE->set_context(null);
2583 // We need to override the navigation URL as the course won't have been added to the navigation and thus
2584 // the navigation will mess up when trying to find it.
2585 navigation_node::override_active_url(new moodle_url('/'));
2586 notice(get_string('coursehidden'), $CFG->wwwroot .'/');
2591 if (!$access) {
2592 if ($preventredirect) {
2593 throw new require_login_exception('Not enrolled');
2595 if ($setwantsurltome) {
2596 $SESSION->wantsurl = qualified_me();
2598 redirect($CFG->wwwroot .'/enrol/index.php?id='. $course->id);
2602 // Check whether the activity has been scheduled for deletion. If so, then deny access, even for admins.
2603 if ($cm && $cm->deletioninprogress) {
2604 if ($preventredirect) {
2605 throw new moodle_exception('activityisscheduledfordeletion');
2607 require_once($CFG->dirroot . '/course/lib.php');
2608 redirect(course_get_url($course), get_string('activityisscheduledfordeletion', 'error'));
2611 // Check visibility of activity to current user; includes visible flag, conditional availability, etc.
2612 if ($cm && !$cm->uservisible) {
2613 if ($preventredirect) {
2614 throw new require_login_exception('Activity is hidden');
2616 // Get the error message that activity is not available and why (if explanation can be shown to the user).
2617 $PAGE->set_course($course);
2618 $renderer = $PAGE->get_renderer('course');
2619 $message = $renderer->course_section_cm_unavailable_error_message($cm);
2620 redirect(course_get_url($course), $message, null, \core\output\notification::NOTIFY_ERROR);
2623 // Set the global $COURSE.
2624 if ($cm) {
2625 $PAGE->set_cm($cm, $course);
2626 $PAGE->set_pagelayout('incourse');
2627 } else if (!empty($courseorid)) {
2628 $PAGE->set_course($course);
2631 foreach ($afterlogins as $plugintype => $plugins) {
2632 foreach ($plugins as $pluginfunction) {
2633 $pluginfunction($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2637 // Finally access granted, update lastaccess times.
2638 // Do not update access time for webservice or ajax requests.
2639 if (!WS_SERVER && !AJAX_SCRIPT) {
2640 user_accesstime_log($course->id);
2645 * A convenience function for where we must be logged in as admin
2646 * @return void
2648 function require_admin() {
2649 require_login(null, false);
2650 require_capability('moodle/site:config', context_system::instance());
2654 * This function just makes sure a user is logged out.
2656 * @package core_access
2657 * @category access
2659 function require_logout() {
2660 global $USER, $DB;
2662 if (!isloggedin()) {
2663 // This should not happen often, no need for hooks or events here.
2664 \core\session\manager::terminate_current();
2665 return;
2668 // Execute hooks before action.
2669 $authplugins = array();
2670 $authsequence = get_enabled_auth_plugins();
2671 foreach ($authsequence as $authname) {
2672 $authplugins[$authname] = get_auth_plugin($authname);
2673 $authplugins[$authname]->prelogout_hook();
2676 // Store info that gets removed during logout.
2677 $sid = session_id();
2678 $event = \core\event\user_loggedout::create(
2679 array(
2680 'userid' => $USER->id,
2681 'objectid' => $USER->id,
2682 'other' => array('sessionid' => $sid),
2685 if ($session = $DB->get_record('sessions', array('sid'=>$sid))) {
2686 $event->add_record_snapshot('sessions', $session);
2689 // Clone of $USER object to be used by auth plugins.
2690 $user = fullclone($USER);
2692 // Delete session record and drop $_SESSION content.
2693 \core\session\manager::terminate_current();
2695 // Trigger event AFTER action.
2696 $event->trigger();
2698 // Hook to execute auth plugins redirection after event trigger.
2699 foreach ($authplugins as $authplugin) {
2700 $authplugin->postlogout_hook($user);
2705 * Weaker version of require_login()
2707 * This is a weaker version of {@link require_login()} which only requires login
2708 * when called from within a course rather than the site page, unless
2709 * the forcelogin option is turned on.
2710 * @see require_login()
2712 * @package core_access
2713 * @category access
2715 * @param mixed $courseorid The course object or id in question
2716 * @param bool $autologinguest Allow autologin guests if that is wanted
2717 * @param object $cm Course activity module if known
2718 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
2719 * true. Used to avoid (=false) some scripts (file.php...) to set that variable,
2720 * in order to keep redirects working properly. MDL-14495
2721 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
2722 * @return void
2723 * @throws coding_exception
2725 function require_course_login($courseorid, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) {
2726 global $CFG, $PAGE, $SITE;
2727 $issite = ((is_object($courseorid) and $courseorid->id == SITEID)
2728 or (!is_object($courseorid) and $courseorid == SITEID));
2729 if ($issite && !empty($cm) && !($cm instanceof cm_info)) {
2730 // Note: nearly all pages call get_fast_modinfo anyway and it does not make any
2731 // db queries so this is not really a performance concern, however it is obviously
2732 // better if you use get_fast_modinfo to get the cm before calling this.
2733 if (is_object($courseorid)) {
2734 $course = $courseorid;
2735 } else {
2736 $course = clone($SITE);
2738 $modinfo = get_fast_modinfo($course);
2739 $cm = $modinfo->get_cm($cm->id);
2741 if (!empty($CFG->forcelogin)) {
2742 // Login required for both SITE and courses.
2743 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2745 } else if ($issite && !empty($cm) and !$cm->uservisible) {
2746 // Always login for hidden activities.
2747 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2749 } else if (isloggedin() && !isguestuser()) {
2750 // User is already logged in. Make sure the login is complete (user is fully setup, policies agreed).
2751 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2753 } else if ($issite) {
2754 // Login for SITE not required.
2755 // We still need to instatiate PAGE vars properly so that things that rely on it like navigation function correctly.
2756 if (!empty($courseorid)) {
2757 if (is_object($courseorid)) {
2758 $course = $courseorid;
2759 } else {
2760 $course = clone $SITE;
2762 if ($cm) {
2763 if ($cm->course != $course->id) {
2764 throw new coding_exception('course and cm parameters in require_course_login() call do not match!!');
2766 $PAGE->set_cm($cm, $course);
2767 $PAGE->set_pagelayout('incourse');
2768 } else {
2769 $PAGE->set_course($course);
2771 } else {
2772 // If $PAGE->course, and hence $PAGE->context, have not already been set up properly, set them up now.
2773 $PAGE->set_course($PAGE->course);
2775 // Do not update access time for webservice or ajax requests.
2776 if (!WS_SERVER && !AJAX_SCRIPT) {
2777 user_accesstime_log(SITEID);
2779 return;
2781 } else {
2782 // Course login always required.
2783 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2788 * Validates a user key, checking if the key exists, is not expired and the remote ip is correct.
2790 * @param string $keyvalue the key value
2791 * @param string $script unique script identifier
2792 * @param int $instance instance id
2793 * @return stdClass the key entry in the user_private_key table
2794 * @since Moodle 3.2
2795 * @throws moodle_exception
2797 function validate_user_key($keyvalue, $script, $instance) {
2798 global $DB;
2800 if (!$key = $DB->get_record('user_private_key', array('script' => $script, 'value' => $keyvalue, 'instance' => $instance))) {
2801 throw new \moodle_exception('invalidkey');
2804 if (!empty($key->validuntil) and $key->validuntil < time()) {
2805 throw new \moodle_exception('expiredkey');
2808 if ($key->iprestriction) {
2809 $remoteaddr = getremoteaddr(null);
2810 if (empty($remoteaddr) or !address_in_subnet($remoteaddr, $key->iprestriction)) {
2811 throw new \moodle_exception('ipmismatch');
2814 return $key;
2818 * Require key login. Function terminates with error if key not found or incorrect.
2820 * @uses NO_MOODLE_COOKIES
2821 * @uses PARAM_ALPHANUM
2822 * @param string $script unique script identifier
2823 * @param int $instance optional instance id
2824 * @param string $keyvalue The key. If not supplied, this will be fetched from the current session.
2825 * @return int Instance ID
2827 function require_user_key_login($script, $instance = null, $keyvalue = null) {
2828 global $DB;
2830 if (!NO_MOODLE_COOKIES) {
2831 throw new \moodle_exception('sessioncookiesdisable');
2834 // Extra safety.
2835 \core\session\manager::write_close();
2837 if (null === $keyvalue) {
2838 $keyvalue = required_param('key', PARAM_ALPHANUM);
2841 $key = validate_user_key($keyvalue, $script, $instance);
2843 if (!$user = $DB->get_record('user', array('id' => $key->userid))) {
2844 throw new \moodle_exception('invaliduserid');
2847 core_user::require_active_user($user, true, true);
2849 // Emulate normal session.
2850 enrol_check_plugins($user, false);
2851 \core\session\manager::set_user($user);
2853 // Note we are not using normal login.
2854 if (!defined('USER_KEY_LOGIN')) {
2855 define('USER_KEY_LOGIN', true);
2858 // Return instance id - it might be empty.
2859 return $key->instance;
2863 * Creates a new private user access key.
2865 * @param string $script unique target identifier
2866 * @param int $userid
2867 * @param int $instance optional instance id
2868 * @param string $iprestriction optional ip restricted access
2869 * @param int $validuntil key valid only until given data
2870 * @return string access key value
2872 function create_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
2873 global $DB;
2875 $key = new stdClass();
2876 $key->script = $script;
2877 $key->userid = $userid;
2878 $key->instance = $instance;
2879 $key->iprestriction = $iprestriction;
2880 $key->validuntil = $validuntil;
2881 $key->timecreated = time();
2883 // Something long and unique.
2884 $key->value = md5($userid.'_'.time().random_string(40));
2885 while ($DB->record_exists('user_private_key', array('value' => $key->value))) {
2886 // Must be unique.
2887 $key->value = md5($userid.'_'.time().random_string(40));
2889 $DB->insert_record('user_private_key', $key);
2890 return $key->value;
2894 * Delete the user's new private user access keys for a particular script.
2896 * @param string $script unique target identifier
2897 * @param int $userid
2898 * @return void
2900 function delete_user_key($script, $userid) {
2901 global $DB;
2902 $DB->delete_records('user_private_key', array('script' => $script, 'userid' => $userid));
2906 * Gets a private user access key (and creates one if one doesn't exist).
2908 * @param string $script unique target identifier
2909 * @param int $userid
2910 * @param int $instance optional instance id
2911 * @param string $iprestriction optional ip restricted access
2912 * @param int $validuntil key valid only until given date
2913 * @return string access key value
2915 function get_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
2916 global $DB;
2918 if ($key = $DB->get_record('user_private_key', array('script' => $script, 'userid' => $userid,
2919 'instance' => $instance, 'iprestriction' => $iprestriction,
2920 'validuntil' => $validuntil))) {
2921 return $key->value;
2922 } else {
2923 return create_user_key($script, $userid, $instance, $iprestriction, $validuntil);
2929 * Modify the user table by setting the currently logged in user's last login to now.
2931 * @return bool Always returns true
2933 function update_user_login_times() {
2934 global $USER, $DB, $SESSION;
2936 if (isguestuser()) {
2937 // Do not update guest access times/ips for performance.
2938 return true;
2941 if (defined('USER_KEY_LOGIN') && USER_KEY_LOGIN === true) {
2942 // Do not update user login time when using user key login.
2943 return true;
2946 $now = time();
2948 $user = new stdClass();
2949 $user->id = $USER->id;
2951 // Make sure all users that logged in have some firstaccess.
2952 if ($USER->firstaccess == 0) {
2953 $USER->firstaccess = $user->firstaccess = $now;
2956 // Store the previous current as lastlogin.
2957 $USER->lastlogin = $user->lastlogin = $USER->currentlogin;
2959 $USER->currentlogin = $user->currentlogin = $now;
2961 // Function user_accesstime_log() may not update immediately, better do it here.
2962 $USER->lastaccess = $user->lastaccess = $now;
2963 $SESSION->userpreviousip = $USER->lastip;
2964 $USER->lastip = $user->lastip = getremoteaddr();
2966 // Note: do not call user_update_user() here because this is part of the login process,
2967 // the login event means that these fields were updated.
2968 $DB->update_record('user', $user);
2969 return true;
2973 * Determines if a user has completed setting up their account.
2975 * The lax mode (with $strict = false) has been introduced for special cases
2976 * only where we want to skip certain checks intentionally. This is valid in
2977 * certain mnet or ajax scenarios when the user cannot / should not be
2978 * redirected to edit their profile. In most cases, you should perform the
2979 * strict check.
2981 * @param stdClass $user A {@link $USER} object to test for the existence of a valid name and email
2982 * @param bool $strict Be more strict and assert id and custom profile fields set, too
2983 * @return bool
2985 function user_not_fully_set_up($user, $strict = true) {
2986 global $CFG, $SESSION, $USER;
2987 require_once($CFG->dirroot.'/user/profile/lib.php');
2989 // If the user is setup then store this in the session to avoid re-checking.
2990 // Some edge cases are when the users email starts to bounce or the
2991 // configuration for custom fields has changed while they are logged in so
2992 // we re-check this fully every hour for the rare cases it has changed.
2993 if (isset($USER->id) && isset($user->id) && $USER->id === $user->id &&
2994 isset($SESSION->fullysetupstrict) && (time() - $SESSION->fullysetupstrict) < HOURSECS) {
2995 return false;
2998 if (isguestuser($user)) {
2999 return false;
3002 if (empty($user->firstname) or empty($user->lastname) or empty($user->email) or over_bounce_threshold($user)) {
3003 return true;
3006 if ($strict) {
3007 if (empty($user->id)) {
3008 // Strict mode can be used with existing accounts only.
3009 return true;
3011 if (!profile_has_required_custom_fields_set($user->id)) {
3012 return true;
3014 if (isset($USER->id) && isset($user->id) && $USER->id === $user->id) {
3015 $SESSION->fullysetupstrict = time();
3019 return false;
3023 * Check whether the user has exceeded the bounce threshold
3025 * @param stdClass $user A {@link $USER} object
3026 * @return bool true => User has exceeded bounce threshold
3028 function over_bounce_threshold($user) {
3029 global $CFG, $DB;
3031 if (empty($CFG->handlebounces)) {
3032 return false;
3035 if (empty($user->id)) {
3036 // No real (DB) user, nothing to do here.
3037 return false;
3040 // Set sensible defaults.
3041 if (empty($CFG->minbounces)) {
3042 $CFG->minbounces = 10;
3044 if (empty($CFG->bounceratio)) {
3045 $CFG->bounceratio = .20;
3047 $bouncecount = 0;
3048 $sendcount = 0;
3049 if ($bounce = $DB->get_record('user_preferences', array ('userid' => $user->id, 'name' => 'email_bounce_count'))) {
3050 $bouncecount = $bounce->value;
3052 if ($send = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) {
3053 $sendcount = $send->value;
3055 return ($bouncecount >= $CFG->minbounces && $bouncecount/$sendcount >= $CFG->bounceratio);
3059 * Used to increment or reset email sent count
3061 * @param stdClass $user object containing an id
3062 * @param bool $reset will reset the count to 0
3063 * @return void
3065 function set_send_count($user, $reset=false) {
3066 global $DB;
3068 if (empty($user->id)) {
3069 // No real (DB) user, nothing to do here.
3070 return;
3073 if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) {
3074 $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
3075 $DB->update_record('user_preferences', $pref);
3076 } else if (!empty($reset)) {
3077 // If it's not there and we're resetting, don't bother. Make a new one.
3078 $pref = new stdClass();
3079 $pref->name = 'email_send_count';
3080 $pref->value = 1;
3081 $pref->userid = $user->id;
3082 $DB->insert_record('user_preferences', $pref, false);
3087 * Increment or reset user's email bounce count
3089 * @param stdClass $user object containing an id
3090 * @param bool $reset will reset the count to 0
3092 function set_bounce_count($user, $reset=false) {
3093 global $DB;
3095 if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_bounce_count'))) {
3096 $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
3097 $DB->update_record('user_preferences', $pref);
3098 } else if (!empty($reset)) {
3099 // If it's not there and we're resetting, don't bother. Make a new one.
3100 $pref = new stdClass();
3101 $pref->name = 'email_bounce_count';
3102 $pref->value = 1;
3103 $pref->userid = $user->id;
3104 $DB->insert_record('user_preferences', $pref, false);
3109 * Determines if the logged in user is currently moving an activity
3111 * @param int $courseid The id of the course being tested
3112 * @return bool
3114 function ismoving($courseid) {
3115 global $USER;
3117 if (!empty($USER->activitycopy)) {
3118 return ($USER->activitycopycourse == $courseid);
3120 return false;
3124 * Returns a persons full name
3126 * Given an object containing all of the users name values, this function returns a string with the full name of the person.
3127 * The result may depend on system settings or language. 'override' will force the alternativefullnameformat to be used. In
3128 * English, fullname as well as alternativefullnameformat is set to 'firstname lastname' by default. But you could have
3129 * fullname set to 'firstname lastname' and alternativefullnameformat set to 'firstname middlename alternatename lastname'.
3131 * @param stdClass $user A {@link $USER} object to get full name of.
3132 * @param bool $override If true then the alternativefullnameformat format rather than fullnamedisplay format will be used.
3133 * @return string
3135 function fullname($user, $override=false) {
3136 // Note: We do not intend to deprecate this function any time soon as it is too widely used at this time.
3137 // Uses of it should be updated to use the new API and pass updated arguments.
3139 // Return an empty string if there is no user.
3140 if (empty($user)) {
3141 return '';
3144 $options = ['override' => $override];
3145 return core_user::get_fullname($user, null, $options);
3149 * Reduces lines of duplicated code for getting user name fields.
3151 * See also {@link user_picture::unalias()}
3153 * @param object $addtoobject Object to add user name fields to.
3154 * @param object $secondobject Object that contains user name field information.
3155 * @param string $prefix prefix to be added to all fields (including $additionalfields) e.g. authorfirstname.
3156 * @param array $additionalfields Additional fields to be matched with data in the second object.
3157 * The key can be set to the user table field name.
3158 * @return object User name fields.
3160 function username_load_fields_from_object($addtoobject, $secondobject, $prefix = null, $additionalfields = null) {
3161 $fields = [];
3162 foreach (\core_user\fields::get_name_fields() as $field) {
3163 $fields[$field] = $prefix . $field;
3165 if ($additionalfields) {
3166 // Additional fields can specify their own 'alias' such as 'id' => 'userid'. This checks to see if
3167 // the key is a number and then sets the key to the array value.
3168 foreach ($additionalfields as $key => $value) {
3169 if (is_numeric($key)) {
3170 $additionalfields[$value] = $prefix . $value;
3171 unset($additionalfields[$key]);
3172 } else {
3173 $additionalfields[$key] = $prefix . $value;
3176 $fields = array_merge($fields, $additionalfields);
3178 foreach ($fields as $key => $field) {
3179 // Important that we have all of the user name fields present in the object that we are sending back.
3180 $addtoobject->$key = '';
3181 if (isset($secondobject->$field)) {
3182 $addtoobject->$key = $secondobject->$field;
3185 return $addtoobject;
3189 * Returns an array of values in order of occurance in a provided string.
3190 * The key in the result is the character postion in the string.
3192 * @param array $values Values to be found in the string format
3193 * @param string $stringformat The string which may contain values being searched for.
3194 * @return array An array of values in order according to placement in the string format.
3196 function order_in_string($values, $stringformat) {
3197 $valuearray = array();
3198 foreach ($values as $value) {
3199 $pattern = "/$value\b/";
3200 // Using preg_match as strpos() may match values that are similar e.g. firstname and firstnamephonetic.
3201 if (preg_match($pattern, $stringformat)) {
3202 $replacement = "thing";
3203 // Replace the value with something more unique to ensure we get the right position when using strpos().
3204 $newformat = preg_replace($pattern, $replacement, $stringformat);
3205 $position = strpos($newformat, $replacement);
3206 $valuearray[$position] = $value;
3209 ksort($valuearray);
3210 return $valuearray;
3214 * Returns whether a given authentication plugin exists.
3216 * @param string $auth Form of authentication to check for. Defaults to the global setting in {@link $CFG}.
3217 * @return boolean Whether the plugin is available.
3219 function exists_auth_plugin($auth) {
3220 global $CFG;
3222 if (file_exists("{$CFG->dirroot}/auth/$auth/auth.php")) {
3223 return is_readable("{$CFG->dirroot}/auth/$auth/auth.php");
3225 return false;
3229 * Checks if a given plugin is in the list of enabled authentication plugins.
3231 * @param string $auth Authentication plugin.
3232 * @return boolean Whether the plugin is enabled.
3234 function is_enabled_auth($auth) {
3235 if (empty($auth)) {
3236 return false;
3239 $enabled = get_enabled_auth_plugins();
3241 return in_array($auth, $enabled);
3245 * Returns an authentication plugin instance.
3247 * @param string $auth name of authentication plugin
3248 * @return auth_plugin_base An instance of the required authentication plugin.
3250 function get_auth_plugin($auth) {
3251 global $CFG;
3253 // Check the plugin exists first.
3254 if (! exists_auth_plugin($auth)) {
3255 throw new \moodle_exception('authpluginnotfound', 'debug', '', $auth);
3258 // Return auth plugin instance.
3259 require_once("{$CFG->dirroot}/auth/$auth/auth.php");
3260 $class = "auth_plugin_$auth";
3261 return new $class;
3265 * Returns array of active auth plugins.
3267 * @param bool $fix fix $CFG->auth if needed. Only set if logged in as admin.
3268 * @return array
3270 function get_enabled_auth_plugins($fix=false) {
3271 global $CFG;
3273 $default = array('manual', 'nologin');
3275 if (empty($CFG->auth)) {
3276 $auths = array();
3277 } else {
3278 $auths = explode(',', $CFG->auth);
3281 $auths = array_unique($auths);
3282 $oldauthconfig = implode(',', $auths);
3283 foreach ($auths as $k => $authname) {
3284 if (in_array($authname, $default)) {
3285 // The manual and nologin plugin never need to be stored.
3286 unset($auths[$k]);
3287 } else if (!exists_auth_plugin($authname)) {
3288 debugging(get_string('authpluginnotfound', 'debug', $authname));
3289 unset($auths[$k]);
3293 // Ideally only explicit interaction from a human admin should trigger a
3294 // change in auth config, see MDL-70424 for details.
3295 if ($fix) {
3296 $newconfig = implode(',', $auths);
3297 if (!isset($CFG->auth) or $newconfig != $CFG->auth) {
3298 add_to_config_log('auth', $oldauthconfig, $newconfig, 'core');
3299 set_config('auth', $newconfig);
3303 return (array_merge($default, $auths));
3307 * Returns true if an internal authentication method is being used.
3308 * if method not specified then, global default is assumed
3310 * @param string $auth Form of authentication required
3311 * @return bool
3313 function is_internal_auth($auth) {
3314 // Throws error if bad $auth.
3315 $authplugin = get_auth_plugin($auth);
3316 return $authplugin->is_internal();
3320 * Returns true if the user is a 'restored' one.
3322 * Used in the login process to inform the user and allow him/her to reset the password
3324 * @param string $username username to be checked
3325 * @return bool
3327 function is_restored_user($username) {
3328 global $CFG, $DB;
3330 return $DB->record_exists('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id, 'password' => 'restored'));
3334 * Returns an array of user fields
3336 * @return array User field/column names
3338 function get_user_fieldnames() {
3339 global $DB;
3341 $fieldarray = $DB->get_columns('user');
3342 unset($fieldarray['id']);
3343 $fieldarray = array_keys($fieldarray);
3345 return $fieldarray;
3349 * Returns the string of the language for the new user.
3351 * @return string language for the new user
3353 function get_newuser_language() {
3354 global $CFG, $SESSION;
3355 return (!empty($CFG->autolangusercreation) && !empty($SESSION->lang)) ? $SESSION->lang : $CFG->lang;
3359 * Creates a bare-bones user record
3361 * @todo Outline auth types and provide code example
3363 * @param string $username New user's username to add to record
3364 * @param string $password New user's password to add to record
3365 * @param string $auth Form of authentication required
3366 * @return stdClass A complete user object
3368 function create_user_record($username, $password, $auth = 'manual') {
3369 global $CFG, $DB, $SESSION;
3370 require_once($CFG->dirroot.'/user/profile/lib.php');
3371 require_once($CFG->dirroot.'/user/lib.php');
3373 // Just in case check text case.
3374 $username = trim(core_text::strtolower($username));
3376 $authplugin = get_auth_plugin($auth);
3377 $customfields = $authplugin->get_custom_user_profile_fields();
3378 $newuser = new stdClass();
3379 if ($newinfo = $authplugin->get_userinfo($username)) {
3380 $newinfo = truncate_userinfo($newinfo);
3381 foreach ($newinfo as $key => $value) {
3382 if (in_array($key, $authplugin->userfields) || (in_array($key, $customfields))) {
3383 $newuser->$key = $value;
3388 if (!empty($newuser->email)) {
3389 if (email_is_not_allowed($newuser->email)) {
3390 unset($newuser->email);
3394 $newuser->auth = $auth;
3395 $newuser->username = $username;
3397 // Fix for MDL-8480
3398 // user CFG lang for user if $newuser->lang is empty
3399 // or $user->lang is not an installed language.
3400 if (empty($newuser->lang) || !get_string_manager()->translation_exists($newuser->lang)) {
3401 $newuser->lang = get_newuser_language();
3403 $newuser->confirmed = 1;
3404 $newuser->lastip = getremoteaddr();
3405 $newuser->timecreated = time();
3406 $newuser->timemodified = $newuser->timecreated;
3407 $newuser->mnethostid = $CFG->mnet_localhost_id;
3409 $newuser->id = user_create_user($newuser, false, false);
3411 // Save user profile data.
3412 profile_save_data($newuser);
3414 $user = get_complete_user_data('id', $newuser->id);
3415 if (!empty($CFG->{'auth_'.$newuser->auth.'_forcechangepassword'})) {
3416 set_user_preference('auth_forcepasswordchange', 1, $user);
3418 // Set the password.
3419 update_internal_user_password($user, $password);
3421 // Trigger event.
3422 \core\event\user_created::create_from_userid($newuser->id)->trigger();
3424 return $user;
3428 * Will update a local user record from an external source (MNET users can not be updated using this method!).
3430 * @param string $username user's username to update the record
3431 * @return stdClass A complete user object
3433 function update_user_record($username) {
3434 global $DB, $CFG;
3435 // Just in case check text case.
3436 $username = trim(core_text::strtolower($username));
3438 $oldinfo = $DB->get_record('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id), '*', MUST_EXIST);
3439 return update_user_record_by_id($oldinfo->id);
3443 * Will update a local user record from an external source (MNET users can not be updated using this method!).
3445 * @param int $id user id
3446 * @return stdClass A complete user object
3448 function update_user_record_by_id($id) {
3449 global $DB, $CFG;
3450 require_once($CFG->dirroot."/user/profile/lib.php");
3451 require_once($CFG->dirroot.'/user/lib.php');
3453 $params = array('mnethostid' => $CFG->mnet_localhost_id, 'id' => $id, 'deleted' => 0);
3454 $oldinfo = $DB->get_record('user', $params, '*', MUST_EXIST);
3456 $newuser = array();
3457 $userauth = get_auth_plugin($oldinfo->auth);
3459 if ($newinfo = $userauth->get_userinfo($oldinfo->username)) {
3460 $newinfo = truncate_userinfo($newinfo);
3461 $customfields = $userauth->get_custom_user_profile_fields();
3463 foreach ($newinfo as $key => $value) {
3464 $iscustom = in_array($key, $customfields);
3465 if (!$iscustom) {
3466 $key = strtolower($key);
3468 if ((!property_exists($oldinfo, $key) && !$iscustom) or $key === 'username' or $key === 'id'
3469 or $key === 'auth' or $key === 'mnethostid' or $key === 'deleted') {
3470 // Unknown or must not be changed.
3471 continue;
3473 if (empty($userauth->config->{'field_updatelocal_' . $key}) || empty($userauth->config->{'field_lock_' . $key})) {
3474 continue;
3476 $confval = $userauth->config->{'field_updatelocal_' . $key};
3477 $lockval = $userauth->config->{'field_lock_' . $key};
3478 if ($confval === 'onlogin') {
3479 // MDL-4207 Don't overwrite modified user profile values with
3480 // empty LDAP values when 'unlocked if empty' is set. The purpose
3481 // of the setting 'unlocked if empty' is to allow the user to fill
3482 // in a value for the selected field _if LDAP is giving
3483 // nothing_ for this field. Thus it makes sense to let this value
3484 // stand in until LDAP is giving a value for this field.
3485 if (!(empty($value) && $lockval === 'unlockedifempty')) {
3486 if ($iscustom || (in_array($key, $userauth->userfields) &&
3487 ((string)$oldinfo->$key !== (string)$value))) {
3488 $newuser[$key] = (string)$value;
3493 if ($newuser) {
3494 $newuser['id'] = $oldinfo->id;
3495 $newuser['timemodified'] = time();
3496 user_update_user((object) $newuser, false, false);
3498 // Save user profile data.
3499 profile_save_data((object) $newuser);
3501 // Trigger event.
3502 \core\event\user_updated::create_from_userid($newuser['id'])->trigger();
3506 return get_complete_user_data('id', $oldinfo->id);
3510 * Will truncate userinfo as it comes from auth_get_userinfo (from external auth) which may have large fields.
3512 * @param array $info Array of user properties to truncate if needed
3513 * @return array The now truncated information that was passed in
3515 function truncate_userinfo(array $info) {
3516 // Define the limits.
3517 $limit = array(
3518 'username' => 100,
3519 'idnumber' => 255,
3520 'firstname' => 100,
3521 'lastname' => 100,
3522 'email' => 100,
3523 'phone1' => 20,
3524 'phone2' => 20,
3525 'institution' => 255,
3526 'department' => 255,
3527 'address' => 255,
3528 'city' => 120,
3529 'country' => 2,
3532 // Apply where needed.
3533 foreach (array_keys($info) as $key) {
3534 if (!empty($limit[$key])) {
3535 $info[$key] = trim(core_text::substr($info[$key], 0, $limit[$key]));
3539 return $info;
3543 * Marks user deleted in internal user database and notifies the auth plugin.
3544 * Also unenrols user from all roles and does other cleanup.
3546 * Any plugin that needs to purge user data should register the 'user_deleted' event.
3548 * @param stdClass $user full user object before delete
3549 * @return boolean success
3550 * @throws coding_exception if invalid $user parameter detected
3552 function delete_user(stdClass $user) {
3553 global $CFG, $DB, $SESSION;
3554 require_once($CFG->libdir.'/grouplib.php');
3555 require_once($CFG->libdir.'/gradelib.php');
3556 require_once($CFG->dirroot.'/message/lib.php');
3557 require_once($CFG->dirroot.'/user/lib.php');
3559 // Make sure nobody sends bogus record type as parameter.
3560 if (!property_exists($user, 'id') or !property_exists($user, 'username')) {
3561 throw new coding_exception('Invalid $user parameter in delete_user() detected');
3564 // Better not trust the parameter and fetch the latest info this will be very expensive anyway.
3565 if (!$user = $DB->get_record('user', array('id' => $user->id))) {
3566 debugging('Attempt to delete unknown user account.');
3567 return false;
3570 // There must be always exactly one guest record, originally the guest account was identified by username only,
3571 // now we use $CFG->siteguest for performance reasons.
3572 if ($user->username === 'guest' or isguestuser($user)) {
3573 debugging('Guest user account can not be deleted.');
3574 return false;
3577 // Admin can be theoretically from different auth plugin, but we want to prevent deletion of internal accoutns only,
3578 // if anything goes wrong ppl may force somebody to be admin via config.php setting $CFG->siteadmins.
3579 if ($user->auth === 'manual' and is_siteadmin($user)) {
3580 debugging('Local administrator accounts can not be deleted.');
3581 return false;
3583 // Allow plugins to use this user object before we completely delete it.
3584 if ($pluginsfunction = get_plugins_with_function('pre_user_delete')) {
3585 foreach ($pluginsfunction as $plugintype => $plugins) {
3586 foreach ($plugins as $pluginfunction) {
3587 $pluginfunction($user);
3592 // Dispatch the hook for pre user update actions.
3593 $hook = new \core_user\hook\before_user_deleted(
3594 user: $user,
3596 di::get(hook\manager::class)->dispatch($hook);
3598 // Keep user record before updating it, as we have to pass this to user_deleted event.
3599 $olduser = clone $user;
3601 // Keep a copy of user context, we need it for event.
3602 $usercontext = context_user::instance($user->id);
3604 // Delete all grades - backup is kept in grade_grades_history table.
3605 grade_user_delete($user->id);
3607 // TODO: remove from cohorts using standard API here.
3609 // Remove user tags.
3610 core_tag_tag::remove_all_item_tags('core', 'user', $user->id);
3612 // Unconditionally unenrol from all courses.
3613 enrol_user_delete($user);
3615 // Unenrol from all roles in all contexts.
3616 // This might be slow but it is really needed - modules might do some extra cleanup!
3617 role_unassign_all(array('userid' => $user->id));
3619 // Notify the competency subsystem.
3620 \core_competency\api::hook_user_deleted($user->id);
3622 // Now do a brute force cleanup.
3624 // Delete all user events and subscription events.
3625 $DB->delete_records_select('event', 'userid = :userid AND subscriptionid IS NOT NULL', ['userid' => $user->id]);
3627 // Now, delete all calendar subscription from the user.
3628 $DB->delete_records('event_subscriptions', ['userid' => $user->id]);
3630 // Remove from all cohorts.
3631 $DB->delete_records('cohort_members', array('userid' => $user->id));
3633 // Remove from all groups.
3634 $DB->delete_records('groups_members', array('userid' => $user->id));
3636 // Brute force unenrol from all courses.
3637 $DB->delete_records('user_enrolments', array('userid' => $user->id));
3639 // Purge user preferences.
3640 $DB->delete_records('user_preferences', array('userid' => $user->id));
3642 // Purge user extra profile info.
3643 $DB->delete_records('user_info_data', array('userid' => $user->id));
3645 // Purge log of previous password hashes.
3646 $DB->delete_records('user_password_history', array('userid' => $user->id));
3648 // Last course access not necessary either.
3649 $DB->delete_records('user_lastaccess', array('userid' => $user->id));
3650 // Remove all user tokens.
3651 $DB->delete_records('external_tokens', array('userid' => $user->id));
3653 // Unauthorise the user for all services.
3654 $DB->delete_records('external_services_users', array('userid' => $user->id));
3656 // Remove users private keys.
3657 $DB->delete_records('user_private_key', array('userid' => $user->id));
3659 // Remove users customised pages.
3660 $DB->delete_records('my_pages', array('userid' => $user->id, 'private' => 1));
3662 // Remove user's oauth2 refresh tokens, if present.
3663 $DB->delete_records('oauth2_refresh_token', array('userid' => $user->id));
3665 // Delete user from $SESSION->bulk_users.
3666 if (isset($SESSION->bulk_users[$user->id])) {
3667 unset($SESSION->bulk_users[$user->id]);
3670 // Force logout - may fail if file based sessions used, sorry.
3671 \core\session\manager::kill_user_sessions($user->id);
3673 // Generate username from email address, or a fake email.
3674 $delemail = !empty($user->email) ? $user->email : $user->username . '.' . $user->id . '@unknownemail.invalid';
3676 $deltime = time();
3678 // Max username length is 100 chars. Select up to limit - (length of current time + 1 [period character]) from users email.
3679 $delnameprefix = clean_param($delemail, PARAM_USERNAME);
3680 $delnamesuffix = $deltime;
3681 $delnamesuffixlength = 10;
3682 do {
3683 // Workaround for bulk deletes of users with the same email address.
3684 $delname = sprintf(
3685 "%s.%10d",
3686 core_text::substr(
3687 $delnameprefix,
3689 // 100 Character maximum, with a '.' character, and a 10-digit timestamp.
3690 100 - 1 - $delnamesuffixlength,
3692 $delnamesuffix,
3694 $delnamesuffix++;
3696 // No need to use mnethostid here.
3697 } while ($DB->record_exists('user', ['username' => $delname]));
3699 // Mark internal user record as "deleted".
3700 $updateuser = new stdClass();
3701 $updateuser->id = $user->id;
3702 $updateuser->deleted = 1;
3703 $updateuser->username = $delname; // Remember it just in case.
3704 $updateuser->email = md5($user->username);// Store hash of username, useful importing/restoring users.
3705 $updateuser->idnumber = ''; // Clear this field to free it up.
3706 $updateuser->picture = 0;
3707 $updateuser->timemodified = $deltime;
3709 // Don't trigger update event, as user is being deleted.
3710 user_update_user($updateuser, false, false);
3712 // Delete all content associated with the user context, but not the context itself.
3713 $usercontext->delete_content();
3715 // Delete any search data.
3716 \core_search\manager::context_deleted($usercontext);
3718 // Any plugin that needs to cleanup should register this event.
3719 // Trigger event.
3720 $event = \core\event\user_deleted::create(
3721 array(
3722 'objectid' => $user->id,
3723 'relateduserid' => $user->id,
3724 'context' => $usercontext,
3725 'other' => array(
3726 'username' => $user->username,
3727 'email' => $user->email,
3728 'idnumber' => $user->idnumber,
3729 'picture' => $user->picture,
3730 'mnethostid' => $user->mnethostid
3734 $event->add_record_snapshot('user', $olduser);
3735 $event->trigger();
3737 // We will update the user's timemodified, as it will be passed to the user_deleted event, which
3738 // should know about this updated property persisted to the user's table.
3739 $user->timemodified = $updateuser->timemodified;
3741 // Notify auth plugin - do not block the delete even when plugin fails.
3742 $authplugin = get_auth_plugin($user->auth);
3743 $authplugin->user_delete($user);
3745 return true;
3749 * Retrieve the guest user object.
3751 * @return stdClass A {@link $USER} object
3753 function guest_user() {
3754 global $CFG, $DB;
3756 if ($newuser = $DB->get_record('user', array('id' => $CFG->siteguest))) {
3757 $newuser->confirmed = 1;
3758 $newuser->lang = get_newuser_language();
3759 $newuser->lastip = getremoteaddr();
3762 return $newuser;
3766 * Authenticates a user against the chosen authentication mechanism
3768 * Given a username and password, this function looks them
3769 * up using the currently selected authentication mechanism,
3770 * and if the authentication is successful, it returns a
3771 * valid $user object from the 'user' table.
3773 * Uses auth_ functions from the currently active auth module
3775 * After authenticate_user_login() returns success, you will need to
3776 * log that the user has logged in, and call complete_user_login() to set
3777 * the session up.
3779 * Note: this function works only with non-mnet accounts!
3781 * @param string $username User's username (or also email if $CFG->authloginviaemail enabled)
3782 * @param string $password User's password
3783 * @param bool $ignorelockout useful when guessing is prevented by other mechanism such as captcha or SSO
3784 * @param int $failurereason login failure reason, can be used in renderers (it may disclose if account exists)
3785 * @param string|bool $logintoken If this is set to a string it is validated against the login token for the session.
3786 * @param string|bool $loginrecaptcha If this is set to a string it is validated against Google reCaptcha.
3787 * @return stdClass|false A {@link $USER} object or false if error
3789 function authenticate_user_login(
3790 $username,
3791 $password,
3792 $ignorelockout = false,
3793 &$failurereason = null,
3794 $logintoken = false,
3795 string|bool $loginrecaptcha = false,
3797 global $CFG, $DB, $PAGE, $SESSION;
3798 require_once("$CFG->libdir/authlib.php");
3800 if ($user = get_complete_user_data('username', $username, $CFG->mnet_localhost_id)) {
3801 // we have found the user
3803 } else if (!empty($CFG->authloginviaemail)) {
3804 if ($email = clean_param($username, PARAM_EMAIL)) {
3805 $select = "mnethostid = :mnethostid AND LOWER(email) = LOWER(:email) AND deleted = 0";
3806 $params = array('mnethostid' => $CFG->mnet_localhost_id, 'email' => $email);
3807 $users = $DB->get_records_select('user', $select, $params, 'id', 'id', 0, 2);
3808 if (count($users) === 1) {
3809 // Use email for login only if unique.
3810 $user = reset($users);
3811 $user = get_complete_user_data('id', $user->id);
3812 $username = $user->username;
3814 unset($users);
3818 // Make sure this request came from the login form.
3819 if (!\core\session\manager::validate_login_token($logintoken)) {
3820 $failurereason = AUTH_LOGIN_FAILED;
3822 // Trigger login failed event (specifying the ID of the found user, if available).
3823 \core\event\user_login_failed::create([
3824 'userid' => ($user->id ?? 0),
3825 'other' => [
3826 'username' => $username,
3827 'reason' => $failurereason,
3829 ])->trigger();
3831 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Invalid Login Token: $username ".$_SERVER['HTTP_USER_AGENT']);
3832 return false;
3835 // Login reCaptcha.
3836 if (login_captcha_enabled() && !validate_login_captcha($loginrecaptcha)) {
3837 $failurereason = AUTH_LOGIN_FAILED_RECAPTCHA;
3838 // Trigger login failed event (specifying the ID of the found user, if available).
3839 \core\event\user_login_failed::create([
3840 'userid' => ($user->id ?? 0),
3841 'other' => [
3842 'username' => $username,
3843 'reason' => $failurereason,
3845 ])->trigger();
3846 return false;
3849 $authsenabled = get_enabled_auth_plugins();
3851 if ($user) {
3852 // Use manual if auth not set.
3853 $auth = empty($user->auth) ? 'manual' : $user->auth;
3855 if (in_array($user->auth, $authsenabled)) {
3856 $authplugin = get_auth_plugin($user->auth);
3857 $authplugin->pre_user_login_hook($user);
3860 if (!empty($user->suspended)) {
3861 $failurereason = AUTH_LOGIN_SUSPENDED;
3863 // Trigger login failed event.
3864 $event = \core\event\user_login_failed::create(array('userid' => $user->id,
3865 'other' => array('username' => $username, 'reason' => $failurereason)));
3866 $event->trigger();
3867 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Suspended Login: $username ".$_SERVER['HTTP_USER_AGENT']);
3868 return false;
3870 if ($auth=='nologin' or !is_enabled_auth($auth)) {
3871 // Legacy way to suspend user.
3872 $failurereason = AUTH_LOGIN_SUSPENDED;
3874 // Trigger login failed event.
3875 $event = \core\event\user_login_failed::create(array('userid' => $user->id,
3876 'other' => array('username' => $username, 'reason' => $failurereason)));
3877 $event->trigger();
3878 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Disabled Login: $username ".$_SERVER['HTTP_USER_AGENT']);
3879 return false;
3881 $auths = array($auth);
3883 } else {
3884 // Check if there's a deleted record (cheaply), this should not happen because we mangle usernames in delete_user().
3885 if ($DB->get_field('user', 'id', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id, 'deleted' => 1))) {
3886 $failurereason = AUTH_LOGIN_NOUSER;
3888 // Trigger login failed event.
3889 $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
3890 'reason' => $failurereason)));
3891 $event->trigger();
3892 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Deleted Login: $username ".$_SERVER['HTTP_USER_AGENT']);
3893 return false;
3896 // User does not exist.
3897 $auths = $authsenabled;
3898 $user = new stdClass();
3899 $user->id = 0;
3902 if ($ignorelockout) {
3903 // Some other mechanism protects against brute force password guessing, for example login form might include reCAPTCHA
3904 // or this function is called from a SSO script.
3905 } else if ($user->id) {
3906 // Verify login lockout after other ways that may prevent user login.
3907 if (login_is_lockedout($user)) {
3908 $failurereason = AUTH_LOGIN_LOCKOUT;
3910 // Trigger login failed event.
3911 $event = \core\event\user_login_failed::create(array('userid' => $user->id,
3912 'other' => array('username' => $username, 'reason' => $failurereason)));
3913 $event->trigger();
3915 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Login lockout: $username ".$_SERVER['HTTP_USER_AGENT']);
3916 $SESSION->loginerrormsg = get_string('accountlocked', 'admin');
3918 return false;
3920 } else {
3921 // We can not lockout non-existing accounts.
3924 foreach ($auths as $auth) {
3925 $authplugin = get_auth_plugin($auth);
3927 // On auth fail fall through to the next plugin.
3928 if (!$authplugin->user_login($username, $password)) {
3929 continue;
3932 // Before performing login actions, check if user still passes password policy, if admin setting is enabled.
3933 if (!empty($CFG->passwordpolicycheckonlogin)) {
3934 $errmsg = '';
3935 $passed = check_password_policy($password, $errmsg, $user);
3936 if (!$passed) {
3937 // First trigger event for failure.
3938 $failedevent = \core\event\user_password_policy_failed::create_from_user($user);
3939 $failedevent->trigger();
3941 // If able to change password, set flag and move on.
3942 if ($authplugin->can_change_password()) {
3943 // Check if we are on internal change password page, or service is external, don't show notification.
3944 $internalchangeurl = new moodle_url('/login/change_password.php');
3945 if (!($PAGE->has_set_url() && $internalchangeurl->compare($PAGE->url)) && $authplugin->is_internal()) {
3946 \core\notification::error(get_string('passwordpolicynomatch', '', $errmsg));
3948 set_user_preference('auth_forcepasswordchange', 1, $user);
3949 } else if ($authplugin->can_reset_password()) {
3950 // Else force a reset if possible.
3951 \core\notification::error(get_string('forcepasswordresetnotice', '', $errmsg));
3952 redirect(new moodle_url('/login/forgot_password.php'));
3953 } else {
3954 $notifymsg = get_string('forcepasswordresetfailurenotice', '', $errmsg);
3955 // If support page is set, add link for help.
3956 if (!empty($CFG->supportpage)) {
3957 $link = \html_writer::link($CFG->supportpage, $CFG->supportpage);
3958 $link = \html_writer::tag('p', $link);
3959 $notifymsg .= $link;
3962 // If no change or reset is possible, add a notification for user.
3963 \core\notification::error($notifymsg);
3968 // Successful authentication.
3969 if ($user->id) {
3970 // User already exists in database.
3971 if (empty($user->auth)) {
3972 // For some reason auth isn't set yet.
3973 $DB->set_field('user', 'auth', $auth, array('id' => $user->id));
3974 $user->auth = $auth;
3977 // If the existing hash is using an out-of-date algorithm (or the legacy md5 algorithm), then we should update to
3978 // the current hash algorithm while we have access to the user's password.
3979 update_internal_user_password($user, $password);
3981 if ($authplugin->is_synchronised_with_external()) {
3982 // Update user record from external DB.
3983 $user = update_user_record_by_id($user->id);
3985 } else {
3986 // The user is authenticated but user creation may be disabled.
3987 if (!empty($CFG->authpreventaccountcreation)) {
3988 $failurereason = AUTH_LOGIN_UNAUTHORISED;
3990 // Trigger login failed event.
3991 $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
3992 'reason' => $failurereason)));
3993 $event->trigger();
3995 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Unknown user, can not create new accounts: $username ".
3996 $_SERVER['HTTP_USER_AGENT']);
3997 return false;
3998 } else {
3999 $user = create_user_record($username, $password, $auth);
4003 $authplugin->sync_roles($user);
4005 foreach ($authsenabled as $hau) {
4006 $hauth = get_auth_plugin($hau);
4007 $hauth->user_authenticated_hook($user, $username, $password);
4010 if (empty($user->id)) {
4011 $failurereason = AUTH_LOGIN_NOUSER;
4012 // Trigger login failed event.
4013 $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4014 'reason' => $failurereason)));
4015 $event->trigger();
4016 return false;
4019 if (!empty($user->suspended)) {
4020 // Just in case some auth plugin suspended account.
4021 $failurereason = AUTH_LOGIN_SUSPENDED;
4022 // Trigger login failed event.
4023 $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4024 'other' => array('username' => $username, 'reason' => $failurereason)));
4025 $event->trigger();
4026 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Suspended Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4027 return false;
4030 login_attempt_valid($user);
4031 $failurereason = AUTH_LOGIN_OK;
4032 return $user;
4035 // Failed if all the plugins have failed.
4036 if (debugging('', DEBUG_ALL)) {
4037 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Failed Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4040 if ($user->id) {
4041 login_attempt_failed($user);
4042 $failurereason = AUTH_LOGIN_FAILED;
4043 // Trigger login failed event.
4044 $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4045 'other' => array('username' => $username, 'reason' => $failurereason)));
4046 $event->trigger();
4047 } else {
4048 $failurereason = AUTH_LOGIN_NOUSER;
4049 // Trigger login failed event.
4050 $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4051 'reason' => $failurereason)));
4052 $event->trigger();
4055 return false;
4059 * Call to complete the user login process after authenticate_user_login()
4060 * has succeeded. It will setup the $USER variable and other required bits
4061 * and pieces.
4063 * NOTE:
4064 * - It will NOT log anything -- up to the caller to decide what to log.
4065 * - this function does not set any cookies any more!
4067 * @param stdClass $user
4068 * @param array $extrauserinfo
4069 * @return stdClass A {@link $USER} object - BC only, do not use
4071 function complete_user_login($user, array $extrauserinfo = []) {
4072 global $CFG, $DB, $USER, $SESSION;
4074 \core\session\manager::login_user($user);
4076 // Reload preferences from DB.
4077 unset($USER->preference);
4078 check_user_preferences_loaded($USER);
4080 // Update login times.
4081 update_user_login_times();
4083 // Extra session prefs init.
4084 set_login_session_preferences();
4086 // Trigger login event.
4087 $event = \core\event\user_loggedin::create(
4088 array(
4089 'userid' => $USER->id,
4090 'objectid' => $USER->id,
4091 'other' => [
4092 'username' => $USER->username,
4093 'extrauserinfo' => $extrauserinfo
4097 $event->trigger();
4099 // Allow plugins to callback as soon possible after user has completed login.
4100 di::get(\core\hook\manager::class)->dispatch(new \core_user\hook\after_login_completed());
4102 // Check if the user is using a new browser or session (a new MoodleSession cookie is set in that case).
4103 // If the user is accessing from the same IP, ignore everything (most of the time will be a new session in the same browser).
4104 // Skip Web Service requests, CLI scripts, AJAX scripts, and request from the mobile app itself.
4105 $loginip = getremoteaddr();
4106 $isnewip = isset($SESSION->userpreviousip) && $SESSION->userpreviousip != $loginip;
4107 $isvalidenv = (!WS_SERVER && !CLI_SCRIPT && !NO_MOODLE_COOKIES) || PHPUNIT_TEST;
4109 if (!empty($SESSION->isnewsessioncookie) && $isnewip && $isvalidenv && !\core_useragent::is_moodle_app()) {
4111 $logintime = time();
4112 $ismoodleapp = false;
4113 $useragent = \core_useragent::get_user_agent_string();
4115 $sitepreferences = get_message_output_default_preferences();
4116 // Check if new login notification is disabled at system level.
4117 $newlogindisabled = $sitepreferences->moodle_newlogin_disable ?? 0;
4118 // Check if message providers (web, email, mobile) are enabled at system level.
4119 $msgproviderenabled = isset($sitepreferences->message_provider_moodle_newlogin_enabled);
4120 // Get message providers enabled for a user.
4121 $userpreferences = get_user_preferences('message_provider_moodle_newlogin_enabled');
4122 // Check if notification processor plugins (web, email, mobile) are enabled at system level.
4123 $msgprocessorsready = !empty(get_message_processors(true));
4124 // If new login notification is enabled at system level then go for other conditions check.
4125 $newloginenabled = $newlogindisabled ? 0 : ($userpreferences != 'none' && $msgproviderenabled);
4127 if ($newloginenabled && $msgprocessorsready) {
4128 // Schedule adhoc task to send a login notification to the user.
4129 $task = new \core\task\send_login_notifications();
4130 $task->set_userid($USER->id);
4131 $task->set_custom_data(compact('ismoodleapp', 'useragent', 'loginip', 'logintime'));
4132 $task->set_component('core');
4133 \core\task\manager::queue_adhoc_task($task);
4137 // Queue migrating the messaging data, if we need to.
4138 if (!get_user_preferences('core_message_migrate_data', false, $USER->id)) {
4139 // Check if there are any legacy messages to migrate.
4140 if (\core_message\helper::legacy_messages_exist($USER->id)) {
4141 \core_message\task\migrate_message_data::queue_task($USER->id);
4142 } else {
4143 set_user_preference('core_message_migrate_data', true, $USER->id);
4147 if (isguestuser()) {
4148 // No need to continue when user is THE guest.
4149 return $USER;
4152 if (CLI_SCRIPT) {
4153 // We can redirect to password change URL only in browser.
4154 return $USER;
4157 // Select password change url.
4158 $userauth = get_auth_plugin($USER->auth);
4160 // Check whether the user should be changing password.
4161 if (get_user_preferences('auth_forcepasswordchange', false)) {
4162 if ($userauth->can_change_password()) {
4163 if ($changeurl = $userauth->change_password_url()) {
4164 redirect($changeurl);
4165 } else {
4166 require_once($CFG->dirroot . '/login/lib.php');
4167 $SESSION->wantsurl = core_login_get_return_url();
4168 redirect($CFG->wwwroot.'/login/change_password.php');
4170 } else {
4171 throw new \moodle_exception('nopasswordchangeforced', 'auth');
4174 return $USER;
4178 * Check a password hash to see if it was hashed using the legacy hash algorithm (bcrypt).
4180 * @param string $password String to check.
4181 * @return bool True if the $password matches the format of a bcrypt hash.
4183 function password_is_legacy_hash(#[\SensitiveParameter] string $password): bool {
4184 return (bool) preg_match('/^\$2y\$[\d]{2}\$[A-Za-z0-9\.\/]{53}$/', $password);
4188 * Calculate the Shannon entropy of a string.
4190 * @param string $pepper The pepper to calculate the entropy of.
4191 * @return float The Shannon entropy of the string.
4193 function calculate_entropy(#[\SensitiveParameter] string $pepper): float {
4194 // Initialize entropy.
4195 $h = 0;
4197 // Calculate the length of the string.
4198 $size = strlen($pepper);
4200 // For each unique character in the string.
4201 foreach (count_chars($pepper, 1) as $v) {
4202 // Calculate the probability of the character.
4203 $p = $v / $size;
4205 // Add the character's contribution to the total entropy.
4206 // This uses the formula for the entropy of a discrete random variable.
4207 $h -= $p * log($p) / log(2);
4210 // Instead of returning the average entropy per symbol (Shannon entropy),
4211 // we multiply by the length of the string to get total entropy.
4212 return $h * $size;
4216 * Get the available password peppers.
4217 * The latest pepper is checked for minimum entropy as part of this function.
4218 * We only calculate the entropy of the most recent pepper,
4219 * because passwords are always updated to the latest pepper,
4220 * and in the past we may have enforced a lower minimum entropy.
4221 * Also, we allow the latest pepper to be empty, to allow admins to migrate off peppers.
4223 * @return array The password peppers.
4224 * @throws coding_exception If the entropy of the password pepper is less than the recommended minimum.
4226 function get_password_peppers(): array {
4227 global $CFG;
4229 // Get all available peppers.
4230 if (isset($CFG->passwordpeppers) && is_array($CFG->passwordpeppers)) {
4231 // Sort the array in descending order of keys (numerical).
4232 $peppers = $CFG->passwordpeppers;
4233 krsort($peppers, SORT_NUMERIC);
4234 } else {
4235 $peppers = []; // Set an empty array if no peppers are found.
4238 // Check if the entropy of the most recent pepper is less than the minimum.
4239 // Also, we allow the most recent pepper to be empty, to allow admins to migrate off peppers.
4240 $lastpepper = reset($peppers);
4241 if (!empty($peppers) && $lastpepper !== '' && calculate_entropy($lastpepper) < PEPPER_ENTROPY) {
4242 throw new coding_exception(
4243 'password pepper below minimum',
4244 'The entropy of the password pepper is less than the recommended minimum.');
4246 return $peppers;
4250 * Compare password against hash stored in user object to determine if it is valid.
4252 * If necessary it also updates the stored hash to the current format.
4254 * @param stdClass $user (Password property may be updated).
4255 * @param string $password Plain text password.
4256 * @return bool True if password is valid.
4258 function validate_internal_user_password(stdClass $user, #[\SensitiveParameter] string $password): bool {
4260 if (exceeds_password_length($password)) {
4261 // Password cannot be more than MAX_PASSWORD_CHARACTERS characters.
4262 return false;
4265 if ($user->password === AUTH_PASSWORD_NOT_CACHED) {
4266 // Internal password is not used at all, it can not validate.
4267 return false;
4270 $peppers = get_password_peppers(); // Get the array of available peppers.
4271 $islegacy = password_is_legacy_hash($user->password); // Check if the password is a legacy bcrypt hash.
4273 // If the password is a legacy hash, no peppers were used, so verify and update directly.
4274 if ($islegacy && password_verify($password, $user->password)) {
4275 update_internal_user_password($user, $password);
4276 return true;
4279 // If the password is not a legacy hash, iterate through the peppers.
4280 $latestpepper = reset($peppers);
4281 // Add an empty pepper to the beginning of the array. To make it easier to check if the password matches without any pepper.
4282 $peppers = [-1 => ''] + $peppers;
4283 foreach ($peppers as $pepper) {
4284 $pepperedpassword = $password . $pepper;
4286 // If the peppered password is correct, update (if necessary) and return true.
4287 if (password_verify($pepperedpassword, $user->password)) {
4288 // If the pepper used is not the latest one, update the password.
4289 if ($pepper !== $latestpepper) {
4290 update_internal_user_password($user, $password);
4292 return true;
4296 // If no peppered password was correct, the password is wrong.
4297 return false;
4301 * Calculate hash for a plain text password.
4303 * @param string $password Plain text password to be hashed.
4304 * @param bool $fasthash If true, use a low number of rounds when generating the hash
4305 * This is faster to generate but makes the hash less secure.
4306 * It is used when lots of hashes need to be generated quickly.
4307 * @param int $pepperlength Lenght of the peppers
4308 * @return string The hashed password.
4310 * @throws moodle_exception If a problem occurs while generating the hash.
4312 function hash_internal_user_password(#[\SensitiveParameter] string $password, $fasthash = false, $pepperlength = 0): string {
4313 if (exceeds_password_length($password, $pepperlength)) {
4314 // Password cannot be more than MAX_PASSWORD_CHARACTERS.
4315 throw new \moodle_exception(get_string("passwordexceeded", 'error', MAX_PASSWORD_CHARACTERS));
4318 // Set the cost factor to 5000 for fast hashing, otherwise use default cost.
4319 $rounds = $fasthash ? 5000 : 10000;
4321 // First generate a cryptographically suitable salt.
4322 $randombytes = random_bytes(16);
4323 $salt = substr(strtr(base64_encode($randombytes), '+', '.'), 0, 16);
4325 // Now construct the password string with the salt and number of rounds.
4326 // The password string is in the format $algorithm$rounds$salt$hash. ($6 is the SHA512 algorithm).
4327 $generatedhash = crypt($password, implode('$', [
4329 // The SHA512 Algorithm
4330 '6',
4331 "rounds={$rounds}",
4332 $salt,
4334 ]));
4336 if ($generatedhash === false || $generatedhash === null) {
4337 throw new moodle_exception('Failed to generate password hash.');
4340 return $generatedhash;
4344 * Update password hash in user object (if necessary).
4346 * The password is updated if:
4347 * 1. The password has changed (the hash of $user->password is different
4348 * to the hash of $password).
4349 * 2. The existing hash is using an out-of-date algorithm (or the legacy
4350 * md5 algorithm).
4352 * The password is peppered with the latest pepper before hashing,
4353 * if peppers are available.
4354 * Updating the password will modify the $user object and the database
4355 * record to use the current hashing algorithm.
4356 * It will remove Web Services user tokens too.
4358 * @param stdClass $user User object (password property may be updated).
4359 * @param string $password Plain text password.
4360 * @param bool $fasthash If true, use a low cost factor when generating the hash
4361 * This is much faster to generate but makes the hash
4362 * less secure. It is used when lots of hashes need to
4363 * be generated quickly.
4364 * @return bool Always returns true.
4366 function update_internal_user_password(
4367 stdClass $user,
4368 #[\SensitiveParameter] string $password,
4369 bool $fasthash = false
4370 ): bool {
4371 global $CFG, $DB;
4373 // Add the latest password pepper to the password before further processing.
4374 $peppers = get_password_peppers();
4375 if (!empty($peppers)) {
4376 $password = $password . reset($peppers);
4379 // Figure out what the hashed password should be.
4380 if (!isset($user->auth)) {
4381 debugging('User record in update_internal_user_password() must include field auth',
4382 DEBUG_DEVELOPER);
4383 $user->auth = $DB->get_field('user', 'auth', array('id' => $user->id));
4385 $authplugin = get_auth_plugin($user->auth);
4386 if ($authplugin->prevent_local_passwords()) {
4387 $hashedpassword = AUTH_PASSWORD_NOT_CACHED;
4388 } else {
4389 $hashedpassword = hash_internal_user_password($password, $fasthash);
4392 $algorithmchanged = false;
4394 if ($hashedpassword === AUTH_PASSWORD_NOT_CACHED) {
4395 // Password is not cached, update it if not set to AUTH_PASSWORD_NOT_CACHED.
4396 $passwordchanged = ($user->password !== $hashedpassword);
4398 } else if (isset($user->password)) {
4399 // If verification fails then it means the password has changed.
4400 $passwordchanged = !password_verify($password, $user->password);
4401 $algorithmchanged = password_is_legacy_hash($user->password);
4402 } else {
4403 // While creating new user, password in unset in $user object, to avoid
4404 // saving it with user_create()
4405 $passwordchanged = true;
4408 if ($passwordchanged || $algorithmchanged) {
4409 $DB->set_field('user', 'password', $hashedpassword, array('id' => $user->id));
4410 $user->password = $hashedpassword;
4412 // Trigger event.
4413 $user = $DB->get_record('user', array('id' => $user->id));
4414 \core\event\user_password_updated::create_from_user($user)->trigger();
4416 // Remove WS user tokens.
4417 if (!empty($CFG->passwordchangetokendeletion)) {
4418 require_once($CFG->dirroot.'/webservice/lib.php');
4419 webservice::delete_user_ws_tokens($user->id);
4423 return true;
4427 * Get a complete user record, which includes all the info in the user record.
4429 * Intended for setting as $USER session variable
4431 * @param string $field The user field to be checked for a given value.
4432 * @param string $value The value to match for $field.
4433 * @param int $mnethostid
4434 * @param bool $throwexception If true, it will throw an exception when there's no record found or when there are multiple records
4435 * found. Otherwise, it will just return false.
4436 * @return mixed False, or A {@link $USER} object.
4438 function get_complete_user_data($field, $value, $mnethostid = null, $throwexception = false) {
4439 global $CFG, $DB;
4441 if (!$field || !$value) {
4442 return false;
4445 // Change the field to lowercase.
4446 $field = core_text::strtolower($field);
4448 // List of case insensitive fields.
4449 $caseinsensitivefields = ['email'];
4451 // Username input is forced to lowercase and should be case sensitive.
4452 if ($field == 'username') {
4453 $value = core_text::strtolower($value);
4456 // Build the WHERE clause for an SQL query.
4457 $params = array('fieldval' => $value);
4459 // Do a case-insensitive query, if necessary. These are generally very expensive. The performance can be improved on some DBs
4460 // such as MySQL by pre-filtering users with accent-insensitive subselect.
4461 if (in_array($field, $caseinsensitivefields)) {
4462 $fieldselect = $DB->sql_equal($field, ':fieldval', false);
4463 $idsubselect = $DB->sql_equal($field, ':fieldval2', false, false);
4464 $params['fieldval2'] = $value;
4465 } else {
4466 $fieldselect = "$field = :fieldval";
4467 $idsubselect = '';
4469 $constraints = "$fieldselect AND deleted <> 1";
4471 // If we are loading user data based on anything other than id,
4472 // we must also restrict our search based on mnet host.
4473 if ($field != 'id') {
4474 if (empty($mnethostid)) {
4475 // If empty, we restrict to local users.
4476 $mnethostid = $CFG->mnet_localhost_id;
4479 if (!empty($mnethostid)) {
4480 $params['mnethostid'] = $mnethostid;
4481 $constraints .= " AND mnethostid = :mnethostid";
4484 if ($idsubselect) {
4485 $constraints .= " AND id IN (SELECT id FROM {user} WHERE {$idsubselect})";
4488 // Get all the basic user data.
4489 try {
4490 // Make sure that there's only a single record that matches our query.
4491 // For example, when fetching by email, multiple records might match the query as there's no guarantee that email addresses
4492 // are unique. Therefore we can't reliably tell whether the user profile data that we're fetching is the correct one.
4493 $user = $DB->get_record_select('user', $constraints, $params, '*', MUST_EXIST);
4494 } catch (dml_exception $exception) {
4495 if ($throwexception) {
4496 throw $exception;
4497 } else {
4498 // Return false when no records or multiple records were found.
4499 return false;
4503 // Get various settings and preferences.
4505 // Preload preference cache.
4506 check_user_preferences_loaded($user);
4508 // Load course enrolment related stuff.
4509 $user->lastcourseaccess = array(); // During last session.
4510 $user->currentcourseaccess = array(); // During current session.
4511 if ($lastaccesses = $DB->get_records('user_lastaccess', array('userid' => $user->id))) {
4512 foreach ($lastaccesses as $lastaccess) {
4513 $user->lastcourseaccess[$lastaccess->courseid] = $lastaccess->timeaccess;
4517 // Add cohort theme.
4518 if (!empty($CFG->allowcohortthemes)) {
4519 require_once($CFG->dirroot . '/cohort/lib.php');
4520 if ($cohorttheme = cohort_get_user_cohort_theme($user->id)) {
4521 $user->cohorttheme = $cohorttheme;
4525 // Add the custom profile fields to the user record.
4526 $user->profile = array();
4527 if (!isguestuser($user)) {
4528 require_once($CFG->dirroot.'/user/profile/lib.php');
4529 profile_load_custom_fields($user);
4532 // Rewrite some variables if necessary.
4533 if (!empty($user->description)) {
4534 // No need to cart all of it around.
4535 $user->description = true;
4537 if (isguestuser($user)) {
4538 // Guest language always same as site.
4539 $user->lang = get_newuser_language();
4540 // Name always in current language.
4541 $user->firstname = get_string('guestuser');
4542 $user->lastname = ' ';
4545 return $user;
4549 * Validate a password against the configured password policy
4551 * @param string $password the password to be checked against the password policy
4552 * @param string $errmsg the error message to display when the password doesn't comply with the policy.
4553 * @param stdClass $user the user object to perform password validation against. Defaults to null if not provided.
4555 * @return bool true if the password is valid according to the policy. false otherwise.
4557 function check_password_policy($password, &$errmsg, $user = null) {
4558 global $CFG;
4560 if (!empty($CFG->passwordpolicy) && !isguestuser($user)) {
4561 $errmsg = '';
4562 if (core_text::strlen($password) < $CFG->minpasswordlength) {
4563 $errmsg .= '<div>'. get_string('errorminpasswordlength', 'auth', $CFG->minpasswordlength) .'</div>';
4565 if (preg_match_all('/[[:digit:]]/u', $password, $matches) < $CFG->minpassworddigits) {
4566 $errmsg .= '<div>'. get_string('errorminpassworddigits', 'auth', $CFG->minpassworddigits) .'</div>';
4568 if (preg_match_all('/[[:lower:]]/u', $password, $matches) < $CFG->minpasswordlower) {
4569 $errmsg .= '<div>'. get_string('errorminpasswordlower', 'auth', $CFG->minpasswordlower) .'</div>';
4571 if (preg_match_all('/[[:upper:]]/u', $password, $matches) < $CFG->minpasswordupper) {
4572 $errmsg .= '<div>'. get_string('errorminpasswordupper', 'auth', $CFG->minpasswordupper) .'</div>';
4574 if (preg_match_all('/[^[:upper:][:lower:][:digit:]]/u', $password, $matches) < $CFG->minpasswordnonalphanum) {
4575 $errmsg .= '<div>'. get_string('errorminpasswordnonalphanum', 'auth', $CFG->minpasswordnonalphanum) .'</div>';
4577 if (!check_consecutive_identical_characters($password, $CFG->maxconsecutiveidentchars)) {
4578 $errmsg .= '<div>'. get_string('errormaxconsecutiveidentchars', 'auth', $CFG->maxconsecutiveidentchars) .'</div>';
4581 // Fire any additional password policy functions from plugins.
4582 // Plugin functions should output an error message string or empty string for success.
4583 $pluginsfunction = get_plugins_with_function('check_password_policy');
4584 foreach ($pluginsfunction as $plugintype => $plugins) {
4585 foreach ($plugins as $pluginfunction) {
4586 $pluginerr = $pluginfunction($password, $user);
4587 if ($pluginerr) {
4588 $errmsg .= '<div>'. $pluginerr .'</div>';
4594 if ($errmsg == '') {
4595 return true;
4596 } else {
4597 return false;
4603 * When logging in, this function is run to set certain preferences for the current SESSION.
4605 function set_login_session_preferences() {
4606 global $SESSION;
4608 $SESSION->justloggedin = true;
4610 unset($SESSION->lang);
4611 unset($SESSION->forcelang);
4612 unset($SESSION->load_navigation_admin);
4617 * Delete a course, including all related data from the database, and any associated files.
4619 * @param mixed $courseorid The id of the course or course object to delete.
4620 * @param bool $showfeedback Whether to display notifications of each action the function performs.
4621 * @return bool true if all the removals succeeded. false if there were any failures. If this
4622 * method returns false, some of the removals will probably have succeeded, and others
4623 * failed, but you have no way of knowing which.
4625 function delete_course($courseorid, $showfeedback = true) {
4626 global $DB, $CFG;
4628 if (is_object($courseorid)) {
4629 $courseid = $courseorid->id;
4630 $course = $courseorid;
4631 } else {
4632 $courseid = $courseorid;
4633 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
4634 return false;
4637 $context = context_course::instance($courseid);
4639 // Frontpage course can not be deleted!!
4640 if ($courseid == SITEID) {
4641 return false;
4644 // Allow plugins to use this course before we completely delete it.
4645 if ($pluginsfunction = get_plugins_with_function('pre_course_delete')) {
4646 foreach ($pluginsfunction as $plugintype => $plugins) {
4647 foreach ($plugins as $pluginfunction) {
4648 $pluginfunction($course);
4653 // Dispatch the hook for pre course delete actions.
4654 $hook = new \core_course\hook\before_course_deleted(
4655 course: $course,
4657 \core\di::get(\core\hook\manager::class)->dispatch($hook);
4659 // Tell the search manager we are about to delete a course. This prevents us sending updates
4660 // for each individual context being deleted.
4661 \core_search\manager::course_deleting_start($courseid);
4663 $handler = core_course\customfield\course_handler::create();
4664 $handler->delete_instance($courseid);
4666 // Make the course completely empty.
4667 remove_course_contents($courseid, $showfeedback);
4669 // Delete the course and related context instance.
4670 context_helper::delete_instance(CONTEXT_COURSE, $courseid);
4672 $DB->delete_records("course", array("id" => $courseid));
4673 $DB->delete_records("course_format_options", array("courseid" => $courseid));
4675 // Reset all course related caches here.
4676 core_courseformat\base::reset_course_cache($courseid);
4678 // Tell search that we have deleted the course so it can delete course data from the index.
4679 \core_search\manager::course_deleting_finish($courseid);
4681 // Trigger a course deleted event.
4682 $event = \core\event\course_deleted::create(array(
4683 'objectid' => $course->id,
4684 'context' => $context,
4685 'other' => array(
4686 'shortname' => $course->shortname,
4687 'fullname' => $course->fullname,
4688 'idnumber' => $course->idnumber
4691 $event->add_record_snapshot('course', $course);
4692 $event->trigger();
4694 return true;
4698 * Clear a course out completely, deleting all content but don't delete the course itself.
4700 * This function does not verify any permissions.
4702 * Please note this function also deletes all user enrolments,
4703 * enrolment instances and role assignments by default.
4705 * $options:
4706 * - 'keep_roles_and_enrolments' - false by default
4707 * - 'keep_groups_and_groupings' - false by default
4709 * @param int $courseid The id of the course that is being deleted
4710 * @param bool $showfeedback Whether to display notifications of each action the function performs.
4711 * @param array $options extra options
4712 * @return bool true if all the removals succeeded. false if there were any failures. If this
4713 * method returns false, some of the removals will probably have succeeded, and others
4714 * failed, but you have no way of knowing which.
4716 function remove_course_contents($courseid, $showfeedback = true, array $options = null) {
4717 global $CFG, $DB, $OUTPUT;
4719 require_once($CFG->libdir.'/badgeslib.php');
4720 require_once($CFG->libdir.'/completionlib.php');
4721 require_once($CFG->libdir.'/questionlib.php');
4722 require_once($CFG->libdir.'/gradelib.php');
4723 require_once($CFG->dirroot.'/group/lib.php');
4724 require_once($CFG->dirroot.'/comment/lib.php');
4725 require_once($CFG->dirroot.'/rating/lib.php');
4726 require_once($CFG->dirroot.'/notes/lib.php');
4728 // Handle course badges.
4729 badges_handle_course_deletion($courseid);
4731 // NOTE: these concatenated strings are suboptimal, but it is just extra info...
4732 $strdeleted = get_string('deleted').' - ';
4734 // Some crazy wishlist of stuff we should skip during purging of course content.
4735 $options = (array)$options;
4737 $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
4738 $coursecontext = context_course::instance($courseid);
4739 $fs = get_file_storage();
4741 // Delete course completion information, this has to be done before grades and enrols.
4742 $cc = new completion_info($course);
4743 $cc->clear_criteria();
4744 if ($showfeedback) {
4745 echo $OUTPUT->notification($strdeleted.get_string('completion', 'completion'), 'notifysuccess');
4748 // Remove all data from gradebook - this needs to be done before course modules
4749 // because while deleting this information, the system may need to reference
4750 // the course modules that own the grades.
4751 remove_course_grades($courseid, $showfeedback);
4752 remove_grade_letters($coursecontext, $showfeedback);
4754 // Delete course blocks in any all child contexts,
4755 // they may depend on modules so delete them first.
4756 $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2.
4757 foreach ($childcontexts as $childcontext) {
4758 blocks_delete_all_for_context($childcontext->id);
4760 unset($childcontexts);
4761 blocks_delete_all_for_context($coursecontext->id);
4762 if ($showfeedback) {
4763 echo $OUTPUT->notification($strdeleted.get_string('type_block_plural', 'plugin'), 'notifysuccess');
4766 $DB->set_field('course_modules', 'deletioninprogress', '1', ['course' => $courseid]);
4767 rebuild_course_cache($courseid, true);
4769 // Get the list of all modules that are properly installed.
4770 $allmodules = $DB->get_records_menu('modules', array(), '', 'name, id');
4772 // Delete every instance of every module,
4773 // this has to be done before deleting of course level stuff.
4774 $locations = core_component::get_plugin_list('mod');
4775 foreach ($locations as $modname => $moddir) {
4776 if ($modname === 'NEWMODULE') {
4777 continue;
4779 if (array_key_exists($modname, $allmodules)) {
4780 $sql = "SELECT cm.*, m.id AS modinstance, m.name, '$modname' AS modname
4781 FROM {".$modname."} m
4782 LEFT JOIN {course_modules} cm ON cm.instance = m.id AND cm.module = :moduleid
4783 WHERE m.course = :courseid";
4784 $instances = $DB->get_records_sql($sql, array('courseid' => $course->id,
4785 'modulename' => $modname, 'moduleid' => $allmodules[$modname]));
4787 include_once("$moddir/lib.php"); // Shows php warning only if plugin defective.
4788 $moddelete = $modname .'_delete_instance'; // Delete everything connected to an instance.
4790 if ($instances) {
4791 foreach ($instances as $cm) {
4792 if ($cm->id) {
4793 // Delete activity context questions and question categories.
4794 question_delete_activity($cm);
4795 // Notify the competency subsystem.
4796 \core_competency\api::hook_course_module_deleted($cm);
4798 // Delete all tag instances associated with the instance of this module.
4799 core_tag_tag::delete_instances("mod_{$modname}", null, context_module::instance($cm->id)->id);
4800 core_tag_tag::remove_all_item_tags('core', 'course_modules', $cm->id);
4802 if (function_exists($moddelete)) {
4803 // This purges all module data in related tables, extra user prefs, settings, etc.
4804 $moddelete($cm->modinstance);
4805 } else {
4806 // NOTE: we should not allow installation of modules with missing delete support!
4807 debugging("Defective module '$modname' detected when deleting course contents: missing function $moddelete()!");
4808 $DB->delete_records($modname, array('id' => $cm->modinstance));
4811 if ($cm->id) {
4812 // Delete cm and its context - orphaned contexts are purged in cron in case of any race condition.
4813 context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
4814 $DB->delete_records('course_modules_completion', ['coursemoduleid' => $cm->id]);
4815 $DB->delete_records('course_modules_viewed', ['coursemoduleid' => $cm->id]);
4816 $DB->delete_records('course_modules', array('id' => $cm->id));
4817 rebuild_course_cache($cm->course, true);
4821 if ($instances and $showfeedback) {
4822 echo $OUTPUT->notification($strdeleted.get_string('pluginname', $modname), 'notifysuccess');
4824 } else {
4825 // Ooops, this module is not properly installed, force-delete it in the next block.
4829 // We have tried to delete everything the nice way - now let's force-delete any remaining module data.
4831 // Delete completion defaults.
4832 $DB->delete_records("course_completion_defaults", array("course" => $courseid));
4834 // Remove all data from availability and completion tables that is associated
4835 // with course-modules belonging to this course. Note this is done even if the
4836 // features are not enabled now, in case they were enabled previously.
4837 $DB->delete_records_subquery('course_modules_completion', 'coursemoduleid', 'id',
4838 'SELECT id from {course_modules} WHERE course = ?', [$courseid]);
4839 $DB->delete_records_subquery('course_modules_viewed', 'coursemoduleid', 'id',
4840 'SELECT id from {course_modules} WHERE course = ?', [$courseid]);
4842 // Remove course-module data that has not been removed in modules' _delete_instance callbacks.
4843 $cms = $DB->get_records('course_modules', array('course' => $course->id));
4844 $allmodulesbyid = array_flip($allmodules);
4845 foreach ($cms as $cm) {
4846 if (array_key_exists($cm->module, $allmodulesbyid)) {
4847 try {
4848 $DB->delete_records($allmodulesbyid[$cm->module], array('id' => $cm->instance));
4849 } catch (Exception $e) {
4850 // Ignore weird or missing table problems.
4853 context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
4854 $DB->delete_records('course_modules', array('id' => $cm->id));
4855 rebuild_course_cache($cm->course, true);
4858 if ($showfeedback) {
4859 echo $OUTPUT->notification($strdeleted.get_string('type_mod_plural', 'plugin'), 'notifysuccess');
4862 // Delete questions and question categories.
4863 question_delete_course($course);
4864 if ($showfeedback) {
4865 echo $OUTPUT->notification($strdeleted.get_string('questions', 'question'), 'notifysuccess');
4868 // Delete content bank contents.
4869 $cb = new \core_contentbank\contentbank();
4870 $cbdeleted = $cb->delete_contents($coursecontext);
4871 if ($showfeedback && $cbdeleted) {
4872 echo $OUTPUT->notification($strdeleted.get_string('contentbank', 'contentbank'), 'notifysuccess');
4875 // Make sure there are no subcontexts left - all valid blocks and modules should be already gone.
4876 $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2.
4877 foreach ($childcontexts as $childcontext) {
4878 $childcontext->delete();
4880 unset($childcontexts);
4882 // Remove roles and enrolments by default.
4883 if (empty($options['keep_roles_and_enrolments'])) {
4884 // This hack is used in restore when deleting contents of existing course.
4885 // During restore, we should remove only enrolment related data that the user performing the restore has a
4886 // permission to remove.
4887 $userid = $options['userid'] ?? null;
4888 enrol_course_delete($course, $userid);
4889 role_unassign_all(array('contextid' => $coursecontext->id, 'component' => ''), true);
4890 if ($showfeedback) {
4891 echo $OUTPUT->notification($strdeleted.get_string('type_enrol_plural', 'plugin'), 'notifysuccess');
4895 // Delete any groups, removing members and grouping/course links first.
4896 if (empty($options['keep_groups_and_groupings'])) {
4897 groups_delete_groupings($course->id, $showfeedback);
4898 groups_delete_groups($course->id, $showfeedback);
4901 // Filters be gone!
4902 filter_delete_all_for_context($coursecontext->id);
4904 // Notes, you shall not pass!
4905 note_delete_all($course->id);
4907 // Die comments!
4908 comment::delete_comments($coursecontext->id);
4910 // Ratings are history too.
4911 $delopt = new stdclass();
4912 $delopt->contextid = $coursecontext->id;
4913 $rm = new rating_manager();
4914 $rm->delete_ratings($delopt);
4916 // Delete course tags.
4917 core_tag_tag::remove_all_item_tags('core', 'course', $course->id);
4919 // Give the course format the opportunity to remove its obscure data.
4920 $format = course_get_format($course);
4921 $format->delete_format_data();
4923 // Notify the competency subsystem.
4924 \core_competency\api::hook_course_deleted($course);
4926 // Delete calendar events.
4927 $DB->delete_records('event', array('courseid' => $course->id));
4928 $fs->delete_area_files($coursecontext->id, 'calendar');
4930 // Delete all related records in other core tables that may have a courseid
4931 // This array stores the tables that need to be cleared, as
4932 // table_name => column_name that contains the course id.
4933 $tablestoclear = array(
4934 'backup_courses' => 'courseid', // Scheduled backup stuff.
4935 'user_lastaccess' => 'courseid', // User access info.
4937 foreach ($tablestoclear as $table => $col) {
4938 $DB->delete_records($table, array($col => $course->id));
4941 // Delete all course backup files.
4942 $fs->delete_area_files($coursecontext->id, 'backup');
4944 // Cleanup course record - remove links to deleted stuff.
4945 // Do not wipe cacherev, as this course might be reused and we need to ensure that it keeps
4946 // increasing.
4947 $oldcourse = new stdClass();
4948 $oldcourse->id = $course->id;
4949 $oldcourse->summary = '';
4950 $oldcourse->legacyfiles = 0;
4951 if (!empty($options['keep_groups_and_groupings'])) {
4952 $oldcourse->defaultgroupingid = 0;
4954 $DB->update_record('course', $oldcourse);
4956 // Delete course sections.
4957 $DB->delete_records('course_sections', array('course' => $course->id));
4959 // Delete legacy, section and any other course files.
4960 $fs->delete_area_files($coursecontext->id, 'course'); // Files from summary and section.
4962 // Delete all remaining stuff linked to context such as files, comments, ratings, etc.
4963 if (empty($options['keep_roles_and_enrolments']) and empty($options['keep_groups_and_groupings'])) {
4964 // Easy, do not delete the context itself...
4965 $coursecontext->delete_content();
4966 } else {
4967 // Hack alert!!!!
4968 // We can not drop all context stuff because it would bork enrolments and roles,
4969 // there might be also files used by enrol plugins...
4972 // Delete legacy files - just in case some files are still left there after conversion to new file api,
4973 // also some non-standard unsupported plugins may try to store something there.
4974 fulldelete($CFG->dataroot.'/'.$course->id);
4976 // Delete from cache to reduce the cache size especially makes sense in case of bulk course deletion.
4977 course_modinfo::purge_course_cache($courseid);
4979 // Trigger a course content deleted event.
4980 $event = \core\event\course_content_deleted::create(array(
4981 'objectid' => $course->id,
4982 'context' => $coursecontext,
4983 'other' => array('shortname' => $course->shortname,
4984 'fullname' => $course->fullname,
4985 'options' => $options) // Passing this for legacy reasons.
4987 $event->add_record_snapshot('course', $course);
4988 $event->trigger();
4990 return true;
4994 * Change dates in module - used from course reset.
4996 * @param string $modname forum, assignment, etc
4997 * @param array $fields array of date fields from mod table
4998 * @param int $timeshift time difference
4999 * @param int $courseid
5000 * @param int $modid (Optional) passed if specific mod instance in course needs to be updated.
5001 * @return bool success
5003 function shift_course_mod_dates($modname, $fields, $timeshift, $courseid, $modid = 0) {
5004 global $CFG, $DB;
5005 include_once($CFG->dirroot.'/mod/'.$modname.'/lib.php');
5007 $return = true;
5008 $params = array($timeshift, $courseid);
5009 foreach ($fields as $field) {
5010 $updatesql = "UPDATE {".$modname."}
5011 SET $field = $field + ?
5012 WHERE course=? AND $field<>0";
5013 if ($modid) {
5014 $updatesql .= ' AND id=?';
5015 $params[] = $modid;
5017 $return = $DB->execute($updatesql, $params) && $return;
5020 return $return;
5024 * This function will empty a course of user data.
5025 * It will retain the activities and the structure of the course.
5027 * @param object $data an object containing all the settings including courseid (without magic quotes)
5028 * @return array status array of array component, item, error
5030 function reset_course_userdata($data) {
5031 global $CFG, $DB;
5032 require_once($CFG->libdir.'/gradelib.php');
5033 require_once($CFG->libdir.'/completionlib.php');
5034 require_once($CFG->dirroot.'/completion/criteria/completion_criteria_date.php');
5035 require_once($CFG->dirroot.'/group/lib.php');
5037 $data->courseid = $data->id;
5038 $context = context_course::instance($data->courseid);
5040 $eventparams = array(
5041 'context' => $context,
5042 'courseid' => $data->id,
5043 'other' => array(
5044 'reset_options' => (array) $data
5047 $event = \core\event\course_reset_started::create($eventparams);
5048 $event->trigger();
5050 // Calculate the time shift of dates.
5051 if (!empty($data->reset_start_date)) {
5052 // Time part of course startdate should be zero.
5053 $data->timeshift = $data->reset_start_date - usergetmidnight($data->reset_start_date_old);
5054 } else {
5055 $data->timeshift = 0;
5058 // Result array: component, item, error.
5059 $status = array();
5061 // Start the resetting.
5062 $componentstr = get_string('general');
5064 // Move the course start time.
5065 if (!empty($data->reset_start_date) and $data->timeshift) {
5066 // Change course start data.
5067 $DB->set_field('course', 'startdate', $data->reset_start_date, array('id' => $data->courseid));
5068 // Update all course and group events - do not move activity events.
5069 $updatesql = "UPDATE {event}
5070 SET timestart = timestart + ?
5071 WHERE courseid=? AND instance=0";
5072 $DB->execute($updatesql, array($data->timeshift, $data->courseid));
5074 // Update any date activity restrictions.
5075 if ($CFG->enableavailability) {
5076 \availability_date\condition::update_all_dates($data->courseid, $data->timeshift);
5079 // Update completion expected dates.
5080 if ($CFG->enablecompletion) {
5081 $modinfo = get_fast_modinfo($data->courseid);
5082 $changed = false;
5083 foreach ($modinfo->get_cms() as $cm) {
5084 if ($cm->completion && !empty($cm->completionexpected)) {
5085 $DB->set_field('course_modules', 'completionexpected', $cm->completionexpected + $data->timeshift,
5086 array('id' => $cm->id));
5087 $changed = true;
5091 // Clear course cache if changes made.
5092 if ($changed) {
5093 rebuild_course_cache($data->courseid, true);
5096 // Update course date completion criteria.
5097 \completion_criteria_date::update_date($data->courseid, $data->timeshift);
5100 $status[] = array('component' => $componentstr, 'item' => get_string('datechanged'), 'error' => false);
5103 if (!empty($data->reset_end_date)) {
5104 // If the user set a end date value respect it.
5105 $DB->set_field('course', 'enddate', $data->reset_end_date, array('id' => $data->courseid));
5106 } else if ($data->timeshift > 0 && $data->reset_end_date_old) {
5107 // If there is a time shift apply it to the end date as well.
5108 $enddate = $data->reset_end_date_old + $data->timeshift;
5109 $DB->set_field('course', 'enddate', $enddate, array('id' => $data->courseid));
5112 if (!empty($data->reset_events)) {
5113 $DB->delete_records('event', array('courseid' => $data->courseid));
5114 $status[] = array('component' => $componentstr, 'item' => get_string('deleteevents', 'calendar'), 'error' => false);
5117 if (!empty($data->reset_notes)) {
5118 require_once($CFG->dirroot.'/notes/lib.php');
5119 note_delete_all($data->courseid);
5120 $status[] = array('component' => $componentstr, 'item' => get_string('deletenotes', 'notes'), 'error' => false);
5123 if (!empty($data->delete_blog_associations)) {
5124 require_once($CFG->dirroot.'/blog/lib.php');
5125 blog_remove_associations_for_course($data->courseid);
5126 $status[] = array('component' => $componentstr, 'item' => get_string('deleteblogassociations', 'blog'), 'error' => false);
5129 if (!empty($data->reset_completion)) {
5130 // Delete course and activity completion information.
5131 $course = $DB->get_record('course', array('id' => $data->courseid));
5132 $cc = new completion_info($course);
5133 $cc->delete_all_completion_data();
5134 $status[] = array('component' => $componentstr,
5135 'item' => get_string('deletecompletiondata', 'completion'), 'error' => false);
5138 if (!empty($data->reset_competency_ratings)) {
5139 \core_competency\api::hook_course_reset_competency_ratings($data->courseid);
5140 $status[] = array('component' => $componentstr,
5141 'item' => get_string('deletecompetencyratings', 'core_competency'), 'error' => false);
5144 $componentstr = get_string('roles');
5146 if (!empty($data->reset_roles_overrides)) {
5147 $children = $context->get_child_contexts();
5148 foreach ($children as $child) {
5149 $child->delete_capabilities();
5151 $context->delete_capabilities();
5152 $status[] = array('component' => $componentstr, 'item' => get_string('deletecourseoverrides', 'role'), 'error' => false);
5155 if (!empty($data->reset_roles_local)) {
5156 $children = $context->get_child_contexts();
5157 foreach ($children as $child) {
5158 role_unassign_all(array('contextid' => $child->id));
5160 $status[] = array('component' => $componentstr, 'item' => get_string('deletelocalroles', 'role'), 'error' => false);
5163 // First unenrol users - this cleans some of related user data too, such as forum subscriptions, tracking, etc.
5164 $data->unenrolled = array();
5165 if (!empty($data->unenrol_users)) {
5166 $plugins = enrol_get_plugins(true);
5167 $instances = enrol_get_instances($data->courseid, true);
5168 foreach ($instances as $key => $instance) {
5169 if (!isset($plugins[$instance->enrol])) {
5170 unset($instances[$key]);
5171 continue;
5175 $usersroles = enrol_get_course_users_roles($data->courseid);
5176 foreach ($data->unenrol_users as $withroleid) {
5177 if ($withroleid) {
5178 $sql = "SELECT ue.*
5179 FROM {user_enrolments} ue
5180 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5181 JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5182 JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.roleid = :roleid AND ra.userid = ue.userid)";
5183 $params = array('courseid' => $data->courseid, 'roleid' => $withroleid, 'courselevel' => CONTEXT_COURSE);
5185 } else {
5186 // Without any role assigned at course context.
5187 $sql = "SELECT ue.*
5188 FROM {user_enrolments} ue
5189 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5190 JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5191 LEFT JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.userid = ue.userid)
5192 WHERE ra.id IS null";
5193 $params = array('courseid' => $data->courseid, 'courselevel' => CONTEXT_COURSE);
5196 $rs = $DB->get_recordset_sql($sql, $params);
5197 foreach ($rs as $ue) {
5198 if (!isset($instances[$ue->enrolid])) {
5199 continue;
5201 $instance = $instances[$ue->enrolid];
5202 $plugin = $plugins[$instance->enrol];
5203 if (!$plugin->allow_unenrol($instance) and !$plugin->allow_unenrol_user($instance, $ue)) {
5204 continue;
5207 if ($withroleid && count($usersroles[$ue->userid]) > 1) {
5208 // If we don't remove all roles and user has more than one role, just remove this role.
5209 role_unassign($withroleid, $ue->userid, $context->id);
5211 unset($usersroles[$ue->userid][$withroleid]);
5212 } else {
5213 // If we remove all roles or user has only one role, unenrol user from course.
5214 $plugin->unenrol_user($instance, $ue->userid);
5216 $data->unenrolled[$ue->userid] = $ue->userid;
5218 $rs->close();
5221 if (!empty($data->unenrolled)) {
5222 $status[] = array(
5223 'component' => $componentstr,
5224 'item' => get_string('unenrol', 'enrol').' ('.count($data->unenrolled).')',
5225 'error' => false
5229 $componentstr = get_string('groups');
5231 // Remove all group members.
5232 if (!empty($data->reset_groups_members)) {
5233 groups_delete_group_members($data->courseid);
5234 $status[] = array('component' => $componentstr, 'item' => get_string('removegroupsmembers', 'group'), 'error' => false);
5237 // Remove all groups.
5238 if (!empty($data->reset_groups_remove)) {
5239 groups_delete_groups($data->courseid, false);
5240 $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroups', 'group'), 'error' => false);
5243 // Remove all grouping members.
5244 if (!empty($data->reset_groupings_members)) {
5245 groups_delete_groupings_groups($data->courseid, false);
5246 $status[] = array('component' => $componentstr, 'item' => get_string('removegroupingsmembers', 'group'), 'error' => false);
5249 // Remove all groupings.
5250 if (!empty($data->reset_groupings_remove)) {
5251 groups_delete_groupings($data->courseid, false);
5252 $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroupings', 'group'), 'error' => false);
5255 // Look in every instance of every module for data to delete.
5256 $unsupportedmods = array();
5257 if ($allmods = $DB->get_records('modules') ) {
5258 foreach ($allmods as $mod) {
5259 $modname = $mod->name;
5260 $modfile = $CFG->dirroot.'/mod/'. $modname.'/lib.php';
5261 $moddeleteuserdata = $modname.'_reset_userdata'; // Function to delete user data.
5262 if (file_exists($modfile)) {
5263 if (!$DB->count_records($modname, array('course' => $data->courseid))) {
5264 continue; // Skip mods with no instances.
5266 include_once($modfile);
5267 if (function_exists($moddeleteuserdata)) {
5268 $modstatus = $moddeleteuserdata($data);
5269 if (is_array($modstatus)) {
5270 $status = array_merge($status, $modstatus);
5271 } else {
5272 debugging('Module '.$modname.' returned incorrect staus - must be an array!');
5274 } else {
5275 $unsupportedmods[] = $mod;
5277 } else {
5278 debugging('Missing lib.php in '.$modname.' module!');
5280 // Update calendar events for all modules.
5281 course_module_bulk_update_calendar_events($modname, $data->courseid);
5283 // Purge the course cache after resetting course start date. MDL-76936
5284 if ($data->timeshift) {
5285 course_modinfo::purge_course_cache($data->courseid);
5289 // Mention unsupported mods.
5290 if (!empty($unsupportedmods)) {
5291 foreach ($unsupportedmods as $mod) {
5292 $status[] = array(
5293 'component' => get_string('modulenameplural', $mod->name),
5294 'item' => '',
5295 'error' => get_string('resetnotimplemented')
5300 $componentstr = get_string('gradebook', 'grades');
5301 // Reset gradebook,.
5302 if (!empty($data->reset_gradebook_items)) {
5303 remove_course_grades($data->courseid, false);
5304 grade_grab_course_grades($data->courseid);
5305 grade_regrade_final_grades($data->courseid);
5306 $status[] = array('component' => $componentstr, 'item' => get_string('removeallcourseitems', 'grades'), 'error' => false);
5308 } else if (!empty($data->reset_gradebook_grades)) {
5309 grade_course_reset($data->courseid);
5310 $status[] = array('component' => $componentstr, 'item' => get_string('removeallcoursegrades', 'grades'), 'error' => false);
5312 // Reset comments.
5313 if (!empty($data->reset_comments)) {
5314 require_once($CFG->dirroot.'/comment/lib.php');
5315 comment::reset_course_page_comments($context);
5318 $event = \core\event\course_reset_ended::create($eventparams);
5319 $event->trigger();
5321 return $status;
5325 * Generate an email processing address.
5327 * @param int $modid
5328 * @param string $modargs
5329 * @return string Returns email processing address
5331 function generate_email_processing_address($modid, $modargs) {
5332 global $CFG;
5334 $header = $CFG->mailprefix . substr(base64_encode(pack('C', $modid)), 0, 2).$modargs;
5335 return $header . substr(md5($header.get_site_identifier()), 0, 16).'@'.$CFG->maildomain;
5341 * @todo Finish documenting this function
5343 * @param string $modargs
5344 * @param string $body Currently unused
5346 function moodle_process_email($modargs, $body) {
5347 global $DB;
5349 // The first char should be an unencoded letter. We'll take this as an action.
5350 switch ($modargs[0]) {
5351 case 'B': { // Bounce.
5352 list(, $userid) = unpack('V', base64_decode(substr($modargs, 1, 8)));
5353 if ($user = $DB->get_record("user", array('id' => $userid), "id,email")) {
5354 // Check the half md5 of their email.
5355 $md5check = substr(md5($user->email), 0, 16);
5356 if ($md5check == substr($modargs, -16)) {
5357 set_bounce_count($user);
5359 // Else maybe they've already changed it?
5362 break;
5363 // Maybe more later?
5367 // CORRESPONDENCE.
5370 * Get mailer instance, enable buffering, flush buffer or disable buffering.
5372 * @param string $action 'get', 'buffer', 'close' or 'flush'
5373 * @return moodle_phpmailer|null mailer instance if 'get' used or nothing
5375 function get_mailer($action='get') {
5376 global $CFG;
5378 /** @var moodle_phpmailer $mailer */
5379 static $mailer = null;
5380 static $counter = 0;
5382 if (!isset($CFG->smtpmaxbulk)) {
5383 $CFG->smtpmaxbulk = 1;
5386 if ($action == 'get') {
5387 $prevkeepalive = false;
5389 if (isset($mailer) and $mailer->Mailer == 'smtp') {
5390 if ($counter < $CFG->smtpmaxbulk and !$mailer->isError()) {
5391 $counter++;
5392 // Reset the mailer.
5393 $mailer->Priority = 3;
5394 $mailer->CharSet = 'UTF-8'; // Our default.
5395 $mailer->ContentType = "text/plain";
5396 $mailer->Encoding = "8bit";
5397 $mailer->From = "root@localhost";
5398 $mailer->FromName = "Root User";
5399 $mailer->Sender = "";
5400 $mailer->Subject = "";
5401 $mailer->Body = "";
5402 $mailer->AltBody = "";
5403 $mailer->ConfirmReadingTo = "";
5405 $mailer->clearAllRecipients();
5406 $mailer->clearReplyTos();
5407 $mailer->clearAttachments();
5408 $mailer->clearCustomHeaders();
5409 return $mailer;
5412 $prevkeepalive = $mailer->SMTPKeepAlive;
5413 get_mailer('flush');
5416 require_once($CFG->libdir.'/phpmailer/moodle_phpmailer.php');
5417 $mailer = new moodle_phpmailer();
5419 $counter = 1;
5421 if ($CFG->smtphosts == 'qmail') {
5422 // Use Qmail system.
5423 $mailer->isQmail();
5425 } else if (empty($CFG->smtphosts)) {
5426 // Use PHP mail() = sendmail.
5427 $mailer->isMail();
5429 } else {
5430 // Use SMTP directly.
5431 $mailer->isSMTP();
5432 if (!empty($CFG->debugsmtp) && (!empty($CFG->debugdeveloper))) {
5433 $mailer->SMTPDebug = 3;
5435 // Specify main and backup servers.
5436 $mailer->Host = $CFG->smtphosts;
5437 // Specify secure connection protocol.
5438 $mailer->SMTPSecure = $CFG->smtpsecure;
5439 // Use previous keepalive.
5440 $mailer->SMTPKeepAlive = $prevkeepalive;
5442 if ($CFG->smtpuser) {
5443 // Use SMTP authentication.
5444 $mailer->SMTPAuth = true;
5445 $mailer->Username = $CFG->smtpuser;
5446 $mailer->Password = $CFG->smtppass;
5450 return $mailer;
5453 $nothing = null;
5455 // Keep smtp session open after sending.
5456 if ($action == 'buffer') {
5457 if (!empty($CFG->smtpmaxbulk)) {
5458 get_mailer('flush');
5459 $m = get_mailer();
5460 if ($m->Mailer == 'smtp') {
5461 $m->SMTPKeepAlive = true;
5464 return $nothing;
5467 // Close smtp session, but continue buffering.
5468 if ($action == 'flush') {
5469 if (isset($mailer) and $mailer->Mailer == 'smtp') {
5470 if (!empty($mailer->SMTPDebug)) {
5471 echo '<pre>'."\n";
5473 $mailer->SmtpClose();
5474 if (!empty($mailer->SMTPDebug)) {
5475 echo '</pre>';
5478 return $nothing;
5481 // Close smtp session, do not buffer anymore.
5482 if ($action == 'close') {
5483 if (isset($mailer) and $mailer->Mailer == 'smtp') {
5484 get_mailer('flush');
5485 $mailer->SMTPKeepAlive = false;
5487 $mailer = null; // Better force new instance.
5488 return $nothing;
5493 * A helper function to test for email diversion
5495 * @param string $email
5496 * @return bool Returns true if the email should be diverted
5498 function email_should_be_diverted($email) {
5499 global $CFG;
5501 if (empty($CFG->divertallemailsto)) {
5502 return false;
5505 if (empty($CFG->divertallemailsexcept)) {
5506 return true;
5509 $patterns = array_map('trim', preg_split("/[\s,]+/", $CFG->divertallemailsexcept, -1, PREG_SPLIT_NO_EMPTY));
5510 foreach ($patterns as $pattern) {
5511 if (preg_match("/{$pattern}/i", $email)) {
5512 return false;
5516 return true;
5520 * Generate a unique email Message-ID using the moodle domain and install path
5522 * @param string $localpart An optional unique message id prefix.
5523 * @return string The formatted ID ready for appending to the email headers.
5525 function generate_email_messageid($localpart = null) {
5526 global $CFG;
5528 $urlinfo = parse_url($CFG->wwwroot);
5529 $base = '@' . $urlinfo['host'];
5531 // If multiple moodles are on the same domain we want to tell them
5532 // apart so we add the install path to the local part. This means
5533 // that the id local part should never contain a / character so
5534 // we can correctly parse the id to reassemble the wwwroot.
5535 if (isset($urlinfo['path'])) {
5536 $base = $urlinfo['path'] . $base;
5539 if (empty($localpart)) {
5540 $localpart = uniqid('', true);
5543 // Because we may have an option /installpath suffix to the local part
5544 // of the id we need to escape any / chars which are in the $localpart.
5545 $localpart = str_replace('/', '%2F', $localpart);
5547 return '<' . $localpart . $base . '>';
5551 * Send an email to a specified user
5553 * @param stdClass $user A {@link $USER} object
5554 * @param stdClass $from A {@link $USER} object
5555 * @param string $subject plain text subject line of the email
5556 * @param string $messagetext plain text version of the message
5557 * @param string $messagehtml complete html version of the message (optional)
5558 * @param string $attachment a file on the filesystem, either relative to $CFG->dataroot or a full path to a file in one of
5559 * the following directories: $CFG->cachedir, $CFG->dataroot, $CFG->dirroot, $CFG->localcachedir, $CFG->tempdir
5560 * @param string $attachname the name of the file (extension indicates MIME)
5561 * @param bool $usetrueaddress determines whether $from email address should
5562 * be sent out. Will be overruled by user profile setting for maildisplay
5563 * @param string $replyto Email address to reply to
5564 * @param string $replytoname Name of reply to recipient
5565 * @param int $wordwrapwidth custom word wrap width, default 79
5566 * @return bool Returns true if mail was sent OK and false if there was an error.
5568 function email_to_user($user, $from, $subject, $messagetext, $messagehtml = '', $attachment = '', $attachname = '',
5569 $usetrueaddress = true, $replyto = '', $replytoname = '', $wordwrapwidth = 79) {
5571 global $CFG, $PAGE, $SITE;
5573 if (empty($user) or empty($user->id)) {
5574 debugging('Can not send email to null user', DEBUG_DEVELOPER);
5575 return false;
5578 if (empty($user->email)) {
5579 debugging('Can not send email to user without email: '.$user->id, DEBUG_DEVELOPER);
5580 return false;
5583 if (!empty($user->deleted)) {
5584 debugging('Can not send email to deleted user: '.$user->id, DEBUG_DEVELOPER);
5585 return false;
5588 if (defined('BEHAT_SITE_RUNNING')) {
5589 // Fake email sending in behat.
5590 return true;
5593 if (!empty($CFG->noemailever)) {
5594 // Hidden setting for development sites, set in config.php if needed.
5595 debugging('Not sending email due to $CFG->noemailever config setting', DEBUG_NORMAL);
5596 return true;
5599 if (email_should_be_diverted($user->email)) {
5600 $subject = "[DIVERTED {$user->email}] $subject";
5601 $user = clone($user);
5602 $user->email = $CFG->divertallemailsto;
5605 // Skip mail to suspended users.
5606 if ((isset($user->auth) && $user->auth=='nologin') or (isset($user->suspended) && $user->suspended)) {
5607 return true;
5610 if (!validate_email($user->email)) {
5611 // We can not send emails to invalid addresses - it might create security issue or confuse the mailer.
5612 debugging("email_to_user: User $user->id (".fullname($user).") email ($user->email) is invalid! Not sending.");
5613 return false;
5616 if (over_bounce_threshold($user)) {
5617 debugging("email_to_user: User $user->id (".fullname($user).") is over bounce threshold! Not sending.");
5618 return false;
5621 // TLD .invalid is specifically reserved for invalid domain names.
5622 // For More information, see {@link http://tools.ietf.org/html/rfc2606#section-2}.
5623 if (substr($user->email, -8) == '.invalid') {
5624 debugging("email_to_user: User $user->id (".fullname($user).") email domain ($user->email) is invalid! Not sending.");
5625 return true; // This is not an error.
5628 // If the user is a remote mnet user, parse the email text for URL to the
5629 // wwwroot and modify the url to direct the user's browser to login at their
5630 // home site (identity provider - idp) before hitting the link itself.
5631 if (is_mnet_remote_user($user)) {
5632 require_once($CFG->dirroot.'/mnet/lib.php');
5634 $jumpurl = mnet_get_idp_jump_url($user);
5635 $callback = partial('mnet_sso_apply_indirection', $jumpurl);
5637 $messagetext = preg_replace_callback("%($CFG->wwwroot[^[:space:]]*)%",
5638 $callback,
5639 $messagetext);
5640 $messagehtml = preg_replace_callback("%href=[\"'`]($CFG->wwwroot[\w_:\?=#&@/;.~-]*)[\"'`]%",
5641 $callback,
5642 $messagehtml);
5644 $mail = get_mailer();
5646 if (!empty($mail->SMTPDebug)) {
5647 echo '<pre>' . "\n";
5650 $temprecipients = array();
5651 $tempreplyto = array();
5653 // Make sure that we fall back onto some reasonable no-reply address.
5654 $noreplyaddressdefault = 'noreply@' . get_host_from_url($CFG->wwwroot);
5655 $noreplyaddress = empty($CFG->noreplyaddress) ? $noreplyaddressdefault : $CFG->noreplyaddress;
5657 if (!validate_email($noreplyaddress)) {
5658 debugging('email_to_user: Invalid noreply-email '.s($noreplyaddress));
5659 $noreplyaddress = $noreplyaddressdefault;
5662 // Make up an email address for handling bounces.
5663 if (!empty($CFG->handlebounces)) {
5664 $modargs = 'B'.base64_encode(pack('V', $user->id)).substr(md5($user->email), 0, 16);
5665 $mail->Sender = generate_email_processing_address(0, $modargs);
5666 } else {
5667 $mail->Sender = $noreplyaddress;
5670 // Make sure that the explicit replyto is valid, fall back to the implicit one.
5671 if (!empty($replyto) && !validate_email($replyto)) {
5672 debugging('email_to_user: Invalid replyto-email '.s($replyto));
5673 $replyto = $noreplyaddress;
5676 if (is_string($from)) { // So we can pass whatever we want if there is need.
5677 $mail->From = $noreplyaddress;
5678 $mail->FromName = $from;
5679 // Check if using the true address is true, and the email is in the list of allowed domains for sending email,
5680 // and that the senders email setting is either displayed to everyone, or display to only other users that are enrolled
5681 // in a course with the sender.
5682 } else if ($usetrueaddress && can_send_from_real_email_address($from, $user)) {
5683 if (!validate_email($from->email)) {
5684 debugging('email_to_user: Invalid from-email '.s($from->email).' - not sending');
5685 // Better not to use $noreplyaddress in this case.
5686 return false;
5688 $mail->From = $from->email;
5689 $fromdetails = new stdClass();
5690 $fromdetails->name = fullname($from);
5691 $fromdetails->url = preg_replace('#^https?://#', '', $CFG->wwwroot);
5692 $fromdetails->siteshortname = format_string($SITE->shortname);
5693 $fromstring = $fromdetails->name;
5694 if ($CFG->emailfromvia == EMAIL_VIA_ALWAYS) {
5695 $fromstring = get_string('emailvia', 'core', $fromdetails);
5697 $mail->FromName = $fromstring;
5698 if (empty($replyto)) {
5699 $tempreplyto[] = array($from->email, fullname($from));
5701 } else {
5702 $mail->From = $noreplyaddress;
5703 $fromdetails = new stdClass();
5704 $fromdetails->name = fullname($from);
5705 $fromdetails->url = preg_replace('#^https?://#', '', $CFG->wwwroot);
5706 $fromdetails->siteshortname = format_string($SITE->shortname);
5707 $fromstring = $fromdetails->name;
5708 if ($CFG->emailfromvia != EMAIL_VIA_NEVER) {
5709 $fromstring = get_string('emailvia', 'core', $fromdetails);
5711 $mail->FromName = $fromstring;
5712 if (empty($replyto)) {
5713 $tempreplyto[] = array($noreplyaddress, get_string('noreplyname'));
5717 if (!empty($replyto)) {
5718 $tempreplyto[] = array($replyto, $replytoname);
5721 $temprecipients[] = array($user->email, fullname($user));
5723 // Set word wrap.
5724 $mail->WordWrap = $wordwrapwidth;
5726 if (!empty($from->customheaders)) {
5727 // Add custom headers.
5728 if (is_array($from->customheaders)) {
5729 foreach ($from->customheaders as $customheader) {
5730 $mail->addCustomHeader($customheader);
5732 } else {
5733 $mail->addCustomHeader($from->customheaders);
5737 // If the X-PHP-Originating-Script email header is on then also add an additional
5738 // header with details of where exactly in moodle the email was triggered from,
5739 // either a call to message_send() or to email_to_user().
5740 if (ini_get('mail.add_x_header')) {
5742 $stack = debug_backtrace(false);
5743 $origin = $stack[0];
5745 foreach ($stack as $depth => $call) {
5746 if ($call['function'] == 'message_send') {
5747 $origin = $call;
5751 $originheader = $CFG->wwwroot . ' => ' . gethostname() . ':'
5752 . str_replace($CFG->dirroot . '/', '', $origin['file']) . ':' . $origin['line'];
5753 $mail->addCustomHeader('X-Moodle-Originating-Script: ' . $originheader);
5756 if (!empty($CFG->emailheaders)) {
5757 $headers = array_map('trim', explode("\n", $CFG->emailheaders));
5758 foreach ($headers as $header) {
5759 if (!empty($header)) {
5760 $mail->addCustomHeader($header);
5765 if (!empty($from->priority)) {
5766 $mail->Priority = $from->priority;
5769 $renderer = $PAGE->get_renderer('core');
5770 $context = array(
5771 'sitefullname' => $SITE->fullname,
5772 'siteshortname' => $SITE->shortname,
5773 'sitewwwroot' => $CFG->wwwroot,
5774 'subject' => $subject,
5775 'prefix' => $CFG->emailsubjectprefix,
5776 'to' => $user->email,
5777 'toname' => fullname($user),
5778 'from' => $mail->From,
5779 'fromname' => $mail->FromName,
5781 if (!empty($tempreplyto[0])) {
5782 $context['replyto'] = $tempreplyto[0][0];
5783 $context['replytoname'] = $tempreplyto[0][1];
5785 if ($user->id > 0) {
5786 $context['touserid'] = $user->id;
5787 $context['tousername'] = $user->username;
5790 if (!empty($user->mailformat) && $user->mailformat == 1) {
5791 // Only process html templates if the user preferences allow html email.
5793 if (!$messagehtml) {
5794 // If no html has been given, BUT there is an html wrapping template then
5795 // auto convert the text to html and then wrap it.
5796 $messagehtml = trim(text_to_html($messagetext));
5798 $context['body'] = $messagehtml;
5799 $messagehtml = $renderer->render_from_template('core/email_html', $context);
5802 $context['body'] = html_to_text(nl2br($messagetext));
5803 $mail->Subject = $renderer->render_from_template('core/email_subject', $context);
5804 $mail->FromName = $renderer->render_from_template('core/email_fromname', $context);
5805 $messagetext = $renderer->render_from_template('core/email_text', $context);
5807 // Autogenerate a MessageID if it's missing.
5808 if (empty($mail->MessageID)) {
5809 $mail->MessageID = generate_email_messageid();
5812 if ($messagehtml && !empty($user->mailformat) && $user->mailformat == 1) {
5813 // Don't ever send HTML to users who don't want it.
5814 $mail->isHTML(true);
5815 $mail->Encoding = 'quoted-printable';
5816 $mail->Body = $messagehtml;
5817 $mail->AltBody = "\n$messagetext\n";
5818 } else {
5819 $mail->IsHTML(false);
5820 $mail->Body = "\n$messagetext\n";
5823 if ($attachment && $attachname) {
5824 if (preg_match( "~\\.\\.~" , $attachment )) {
5825 // Security check for ".." in dir path.
5826 $supportuser = core_user::get_support_user();
5827 $temprecipients[] = array($supportuser->email, fullname($supportuser, true));
5828 $mail->addStringAttachment('Error in attachment. User attempted to attach a filename with a unsafe name.', 'error.txt', '8bit', 'text/plain');
5829 } else {
5830 require_once($CFG->libdir.'/filelib.php');
5831 $mimetype = mimeinfo('type', $attachname);
5833 // Before doing the comparison, make sure that the paths are correct (Windows uses slashes in the other direction).
5834 // The absolute (real) path is also fetched to ensure that comparisons to allowed paths are compared equally.
5835 $attachpath = str_replace('\\', '/', realpath($attachment));
5837 // Build an array of all filepaths from which attachments can be added (normalised slashes, absolute/real path).
5838 $allowedpaths = array_map(function(string $path): string {
5839 return str_replace('\\', '/', realpath($path));
5840 }, [
5841 $CFG->cachedir,
5842 $CFG->dataroot,
5843 $CFG->dirroot,
5844 $CFG->localcachedir,
5845 $CFG->tempdir,
5846 $CFG->localrequestdir,
5849 // Set addpath to true.
5850 $addpath = true;
5852 // Check if attachment includes one of the allowed paths.
5853 foreach (array_filter($allowedpaths) as $allowedpath) {
5854 // Set addpath to false if the attachment includes one of the allowed paths.
5855 if (strpos($attachpath, $allowedpath) === 0) {
5856 $addpath = false;
5857 break;
5861 // If the attachment is a full path to a file in the multiple allowed paths, use it as is,
5862 // otherwise assume it is a relative path from the dataroot (for backwards compatibility reasons).
5863 if ($addpath == true) {
5864 $attachment = $CFG->dataroot . '/' . $attachment;
5867 $mail->addAttachment($attachment, $attachname, 'base64', $mimetype);
5871 // Check if the email should be sent in an other charset then the default UTF-8.
5872 if ((!empty($CFG->sitemailcharset) || !empty($CFG->allowusermailcharset))) {
5874 // Use the defined site mail charset or eventually the one preferred by the recipient.
5875 $charset = $CFG->sitemailcharset;
5876 if (!empty($CFG->allowusermailcharset)) {
5877 if ($useremailcharset = get_user_preferences('mailcharset', '0', $user->id)) {
5878 $charset = $useremailcharset;
5882 // Convert all the necessary strings if the charset is supported.
5883 $charsets = get_list_of_charsets();
5884 unset($charsets['UTF-8']);
5885 if (in_array($charset, $charsets)) {
5886 $mail->CharSet = $charset;
5887 $mail->FromName = core_text::convert($mail->FromName, 'utf-8', strtolower($charset));
5888 $mail->Subject = core_text::convert($mail->Subject, 'utf-8', strtolower($charset));
5889 $mail->Body = core_text::convert($mail->Body, 'utf-8', strtolower($charset));
5890 $mail->AltBody = core_text::convert($mail->AltBody, 'utf-8', strtolower($charset));
5892 foreach ($temprecipients as $key => $values) {
5893 $temprecipients[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset));
5895 foreach ($tempreplyto as $key => $values) {
5896 $tempreplyto[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset));
5901 foreach ($temprecipients as $values) {
5902 $mail->addAddress($values[0], $values[1]);
5904 foreach ($tempreplyto as $values) {
5905 $mail->addReplyTo($values[0], $values[1]);
5908 if (!empty($CFG->emaildkimselector)) {
5909 $domain = substr(strrchr($mail->From, "@"), 1);
5910 $pempath = "{$CFG->dataroot}/dkim/{$domain}/{$CFG->emaildkimselector}.private";
5911 if (file_exists($pempath)) {
5912 $mail->DKIM_domain = $domain;
5913 $mail->DKIM_private = $pempath;
5914 $mail->DKIM_selector = $CFG->emaildkimselector;
5915 $mail->DKIM_identity = $mail->From;
5916 } else {
5917 debugging("Email DKIM selector chosen due to {$mail->From} but no certificate found at $pempath", DEBUG_DEVELOPER);
5921 if ($mail->send()) {
5922 set_send_count($user);
5923 if (!empty($mail->SMTPDebug)) {
5924 echo '</pre>';
5926 return true;
5927 } else {
5928 // Trigger event for failing to send email.
5929 $event = \core\event\email_failed::create(array(
5930 'context' => context_system::instance(),
5931 'userid' => $from->id,
5932 'relateduserid' => $user->id,
5933 'other' => array(
5934 'subject' => $subject,
5935 'message' => $messagetext,
5936 'errorinfo' => $mail->ErrorInfo
5939 $event->trigger();
5940 if (CLI_SCRIPT) {
5941 mtrace('Error: lib/moodlelib.php email_to_user(): '.$mail->ErrorInfo);
5943 if (!empty($mail->SMTPDebug)) {
5944 echo '</pre>';
5946 return false;
5951 * Check to see if a user's real email address should be used for the "From" field.
5953 * @param object $from The user object for the user we are sending the email from.
5954 * @param object $user The user object that we are sending the email to.
5955 * @param array $unused No longer used.
5956 * @return bool Returns true if we can use the from user's email adress in the "From" field.
5958 function can_send_from_real_email_address($from, $user, $unused = null) {
5959 global $CFG;
5960 if (!isset($CFG->allowedemaildomains) || empty(trim($CFG->allowedemaildomains))) {
5961 return false;
5963 $alloweddomains = array_map('trim', explode("\n", $CFG->allowedemaildomains));
5964 // Email is in the list of allowed domains for sending email,
5965 // and the senders email setting is either displayed to everyone, or display to only other users that are enrolled
5966 // in a course with the sender.
5967 if (\core\ip_utils::is_domain_in_allowed_list(substr($from->email, strpos($from->email, '@') + 1), $alloweddomains)
5968 && ($from->maildisplay == core_user::MAILDISPLAY_EVERYONE
5969 || ($from->maildisplay == core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY
5970 && enrol_get_shared_courses($user, $from, false, true)))) {
5971 return true;
5973 return false;
5977 * Generate a signoff for emails based on support settings
5979 * @return string
5981 function generate_email_signoff() {
5982 global $CFG, $OUTPUT;
5984 $signoff = "\n";
5985 if (!empty($CFG->supportname)) {
5986 $signoff .= $CFG->supportname."\n";
5989 $supportemail = $OUTPUT->supportemail(['class' => 'font-weight-bold']);
5991 if ($supportemail) {
5992 $signoff .= "\n" . $supportemail . "\n";
5995 return $signoff;
5999 * Sets specified user's password and send the new password to the user via email.
6001 * @param stdClass $user A {@link $USER} object
6002 * @param bool $fasthash If true, use a low cost factor when generating the hash for speed.
6003 * @return bool|string Returns "true" if mail was sent OK and "false" if there was an error
6005 function setnew_password_and_mail($user, $fasthash = false) {
6006 global $CFG, $DB;
6008 // We try to send the mail in language the user understands,
6009 // unfortunately the filter_string() does not support alternative langs yet
6010 // so multilang will not work properly for site->fullname.
6011 $lang = empty($user->lang) ? get_newuser_language() : $user->lang;
6013 $site = get_site();
6015 $supportuser = core_user::get_support_user();
6017 $newpassword = generate_password();
6019 update_internal_user_password($user, $newpassword, $fasthash);
6021 $a = new stdClass();
6022 $a->firstname = fullname($user, true);
6023 $a->sitename = format_string($site->fullname);
6024 $a->username = $user->username;
6025 $a->newpassword = $newpassword;
6026 $a->link = $CFG->wwwroot .'/login/?lang='.$lang;
6027 $a->signoff = generate_email_signoff();
6029 $message = (string)new lang_string('newusernewpasswordtext', '', $a, $lang);
6031 $subject = format_string($site->fullname) .': '. (string)new lang_string('newusernewpasswordsubj', '', $a, $lang);
6033 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6034 return email_to_user($user, $supportuser, $subject, $message);
6039 * Resets specified user's password and send the new password to the user via email.
6041 * @param stdClass $user A {@link $USER} object
6042 * @return bool Returns true if mail was sent OK and false if there was an error.
6044 function reset_password_and_mail($user) {
6045 global $CFG;
6047 $site = get_site();
6048 $supportuser = core_user::get_support_user();
6050 $userauth = get_auth_plugin($user->auth);
6051 if (!$userauth->can_reset_password() or !is_enabled_auth($user->auth)) {
6052 trigger_error("Attempt to reset user password for user $user->username with Auth $user->auth.");
6053 return false;
6056 $newpassword = generate_password();
6058 if (!$userauth->user_update_password($user, $newpassword)) {
6059 throw new \moodle_exception("cannotsetpassword");
6062 $a = new stdClass();
6063 $a->firstname = $user->firstname;
6064 $a->lastname = $user->lastname;
6065 $a->sitename = format_string($site->fullname);
6066 $a->username = $user->username;
6067 $a->newpassword = $newpassword;
6068 $a->link = $CFG->wwwroot .'/login/change_password.php';
6069 $a->signoff = generate_email_signoff();
6071 $message = get_string('newpasswordtext', '', $a);
6073 $subject = format_string($site->fullname) .': '. get_string('changedpassword');
6075 unset_user_preference('create_password', $user); // Prevent cron from generating the password.
6077 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6078 return email_to_user($user, $supportuser, $subject, $message);
6082 * Send email to specified user with confirmation text and activation link.
6084 * @param stdClass $user A {@link $USER} object
6085 * @param string $confirmationurl user confirmation URL
6086 * @return bool Returns true if mail was sent OK and false if there was an error.
6088 function send_confirmation_email($user, $confirmationurl = null) {
6089 global $CFG;
6091 $site = get_site();
6092 $supportuser = core_user::get_support_user();
6094 $data = new stdClass();
6095 $data->sitename = format_string($site->fullname);
6096 $data->admin = generate_email_signoff();
6098 $subject = get_string('emailconfirmationsubject', '', format_string($site->fullname));
6100 if (empty($confirmationurl)) {
6101 $confirmationurl = '/login/confirm.php';
6104 $confirmationurl = new moodle_url($confirmationurl);
6105 // Remove data parameter just in case it was included in the confirmation so we can add it manually later.
6106 $confirmationurl->remove_params('data');
6107 $confirmationpath = $confirmationurl->out(false);
6109 // We need to custom encode the username to include trailing dots in the link.
6110 // Because of this custom encoding we can't use moodle_url directly.
6111 // Determine if a query string is present in the confirmation url.
6112 $hasquerystring = strpos($confirmationpath, '?') !== false;
6113 // Perform normal url encoding of the username first.
6114 $username = urlencode($user->username);
6115 // Prevent problems with trailing dots not being included as part of link in some mail clients.
6116 $username = str_replace('.', '%2E', $username);
6118 $data->link = $confirmationpath . ( $hasquerystring ? '&' : '?') . 'data='. $user->secret .'/'. $username;
6120 $message = get_string('emailconfirmation', '', $data);
6121 $messagehtml = text_to_html(get_string('emailconfirmation', '', $data), false, false, true);
6123 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6124 return email_to_user($user, $supportuser, $subject, $message, $messagehtml);
6128 * Sends a password change confirmation email.
6130 * @param stdClass $user A {@link $USER} object
6131 * @param stdClass $resetrecord An object tracking metadata regarding password reset request
6132 * @return bool Returns true if mail was sent OK and false if there was an error.
6134 function send_password_change_confirmation_email($user, $resetrecord) {
6135 global $CFG;
6137 $site = get_site();
6138 $supportuser = core_user::get_support_user();
6139 $pwresetmins = isset($CFG->pwresettime) ? floor($CFG->pwresettime / MINSECS) : 30;
6141 $data = new stdClass();
6142 $data->firstname = $user->firstname;
6143 $data->lastname = $user->lastname;
6144 $data->username = $user->username;
6145 $data->sitename = format_string($site->fullname);
6146 $data->link = $CFG->wwwroot .'/login/forgot_password.php?token='. $resetrecord->token;
6147 $data->admin = generate_email_signoff();
6148 $data->resetminutes = $pwresetmins;
6150 $message = get_string('emailresetconfirmation', '', $data);
6151 $subject = get_string('emailresetconfirmationsubject', '', format_string($site->fullname));
6153 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6154 return email_to_user($user, $supportuser, $subject, $message);
6159 * Sends an email containing information on how to change your password.
6161 * @param stdClass $user A {@link $USER} object
6162 * @return bool Returns true if mail was sent OK and false if there was an error.
6164 function send_password_change_info($user) {
6165 $site = get_site();
6166 $supportuser = core_user::get_support_user();
6168 $data = new stdClass();
6169 $data->firstname = $user->firstname;
6170 $data->lastname = $user->lastname;
6171 $data->username = $user->username;
6172 $data->sitename = format_string($site->fullname);
6173 $data->admin = generate_email_signoff();
6175 if (!is_enabled_auth($user->auth)) {
6176 $message = get_string('emailpasswordchangeinfodisabled', '', $data);
6177 $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname));
6178 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6179 return email_to_user($user, $supportuser, $subject, $message);
6182 $userauth = get_auth_plugin($user->auth);
6183 ['subject' => $subject, 'message' => $message] = $userauth->get_password_change_info($user);
6185 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6186 return email_to_user($user, $supportuser, $subject, $message);
6190 * Check that an email is allowed. It returns an error message if there was a problem.
6192 * @param string $email Content of email
6193 * @return string|false
6195 function email_is_not_allowed($email) {
6196 global $CFG;
6198 // Comparing lowercase domains.
6199 $email = strtolower($email);
6200 if (!empty($CFG->allowemailaddresses)) {
6201 $allowed = explode(' ', strtolower($CFG->allowemailaddresses));
6202 foreach ($allowed as $allowedpattern) {
6203 $allowedpattern = trim($allowedpattern);
6204 if (!$allowedpattern) {
6205 continue;
6207 if (strpos($allowedpattern, '.') === 0) {
6208 if (strpos(strrev($email), strrev($allowedpattern)) === 0) {
6209 // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com".
6210 return false;
6213 } else if (strpos(strrev($email), strrev('@'.$allowedpattern)) === 0) {
6214 return false;
6217 return get_string('emailonlyallowed', '', $CFG->allowemailaddresses);
6219 } else if (!empty($CFG->denyemailaddresses)) {
6220 $denied = explode(' ', strtolower($CFG->denyemailaddresses));
6221 foreach ($denied as $deniedpattern) {
6222 $deniedpattern = trim($deniedpattern);
6223 if (!$deniedpattern) {
6224 continue;
6226 if (strpos($deniedpattern, '.') === 0) {
6227 if (strpos(strrev($email), strrev($deniedpattern)) === 0) {
6228 // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com".
6229 return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
6232 } else if (strpos(strrev($email), strrev('@'.$deniedpattern)) === 0) {
6233 return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
6238 return false;
6241 // FILE HANDLING.
6244 * Returns local file storage instance
6246 * @return ?file_storage
6248 function get_file_storage($reset = false) {
6249 global $CFG;
6251 static $fs = null;
6253 if ($reset) {
6254 $fs = null;
6255 return;
6258 if ($fs) {
6259 return $fs;
6262 require_once("$CFG->libdir/filelib.php");
6264 $fs = new file_storage();
6266 return $fs;
6270 * Returns local file storage instance
6272 * @return file_browser
6274 function get_file_browser() {
6275 global $CFG;
6277 static $fb = null;
6279 if ($fb) {
6280 return $fb;
6283 require_once("$CFG->libdir/filelib.php");
6285 $fb = new file_browser();
6287 return $fb;
6291 * Returns file packer
6293 * @param string $mimetype default application/zip
6294 * @return file_packer|false
6296 function get_file_packer($mimetype='application/zip') {
6297 global $CFG;
6299 static $fp = array();
6301 if (isset($fp[$mimetype])) {
6302 return $fp[$mimetype];
6305 switch ($mimetype) {
6306 case 'application/zip':
6307 case 'application/vnd.moodle.profiling':
6308 $classname = 'zip_packer';
6309 break;
6311 case 'application/x-gzip' :
6312 $classname = 'tgz_packer';
6313 break;
6315 case 'application/vnd.moodle.backup':
6316 $classname = 'mbz_packer';
6317 break;
6319 default:
6320 return false;
6323 require_once("$CFG->libdir/filestorage/$classname.php");
6324 $fp[$mimetype] = new $classname();
6326 return $fp[$mimetype];
6330 * Returns current name of file on disk if it exists.
6332 * @param string $newfile File to be verified
6333 * @return string Current name of file on disk if true
6335 function valid_uploaded_file($newfile) {
6336 if (empty($newfile)) {
6337 return '';
6339 if (is_uploaded_file($newfile['tmp_name']) and $newfile['size'] > 0) {
6340 return $newfile['tmp_name'];
6341 } else {
6342 return '';
6347 * Returns the maximum size for uploading files.
6349 * There are seven possible upload limits:
6350 * 1. in Apache using LimitRequestBody (no way of checking or changing this)
6351 * 2. in php.ini for 'upload_max_filesize' (can not be changed inside PHP)
6352 * 3. in .htaccess for 'upload_max_filesize' (can not be changed inside PHP)
6353 * 4. in php.ini for 'post_max_size' (can not be changed inside PHP)
6354 * 5. by the Moodle admin in $CFG->maxbytes
6355 * 6. by the teacher in the current course $course->maxbytes
6356 * 7. by the teacher for the current module, eg $assignment->maxbytes
6358 * These last two are passed to this function as arguments (in bytes).
6359 * Anything defined as 0 is ignored.
6360 * The smallest of all the non-zero numbers is returned.
6362 * @todo Finish documenting this function
6364 * @param int $sitebytes Set maximum size
6365 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6366 * @param int $modulebytes Current module ->maxbytes (in bytes)
6367 * @param bool $unused This parameter has been deprecated and is not used any more.
6368 * @return int The maximum size for uploading files.
6370 function get_max_upload_file_size($sitebytes=0, $coursebytes=0, $modulebytes=0, $unused = false) {
6372 if (! $filesize = ini_get('upload_max_filesize')) {
6373 $filesize = '5M';
6375 $minimumsize = get_real_size($filesize);
6377 if ($postsize = ini_get('post_max_size')) {
6378 $postsize = get_real_size($postsize);
6379 if ($postsize < $minimumsize) {
6380 $minimumsize = $postsize;
6384 if (($sitebytes > 0) and ($sitebytes < $minimumsize)) {
6385 $minimumsize = $sitebytes;
6388 if (($coursebytes > 0) and ($coursebytes < $minimumsize)) {
6389 $minimumsize = $coursebytes;
6392 if (($modulebytes > 0) and ($modulebytes < $minimumsize)) {
6393 $minimumsize = $modulebytes;
6396 return $minimumsize;
6400 * Returns the maximum size for uploading files for the current user
6402 * This function takes in account {@link get_max_upload_file_size()} the user's capabilities
6404 * @param context $context The context in which to check user capabilities
6405 * @param int $sitebytes Set maximum size
6406 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6407 * @param int $modulebytes Current module ->maxbytes (in bytes)
6408 * @param stdClass|int|null $user The user
6409 * @param bool $unused This parameter has been deprecated and is not used any more.
6410 * @return int The maximum size for uploading files.
6412 function get_user_max_upload_file_size($context, $sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $user = null,
6413 $unused = false) {
6414 global $USER;
6416 if (empty($user)) {
6417 $user = $USER;
6420 if (has_capability('moodle/course:ignorefilesizelimits', $context, $user)) {
6421 return USER_CAN_IGNORE_FILE_SIZE_LIMITS;
6424 return get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes);
6428 * Returns an array of possible sizes in local language
6430 * Related to {@link get_max_upload_file_size()} - this function returns an
6431 * array of possible sizes in an array, translated to the
6432 * local language.
6434 * The list of options will go up to the minimum of $sitebytes, $coursebytes or $modulebytes.
6436 * If $coursebytes or $sitebytes is not 0, an option will be included for "Course/Site upload limit (X)"
6437 * with the value set to 0. This option will be the first in the list.
6439 * @uses SORT_NUMERIC
6440 * @param int $sitebytes Set maximum size
6441 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6442 * @param int $modulebytes Current module ->maxbytes (in bytes)
6443 * @param int|array $custombytes custom upload size/s which will be added to list,
6444 * Only value/s smaller then maxsize will be added to list.
6445 * @return array
6447 function get_max_upload_sizes($sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $custombytes = null) {
6448 global $CFG;
6450 if (!$maxsize = get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes)) {
6451 return array();
6454 if ($sitebytes == 0) {
6455 // Will get the minimum of upload_max_filesize or post_max_size.
6456 $sitebytes = get_max_upload_file_size();
6459 $filesize = array();
6460 $sizelist = array(10240, 51200, 102400, 512000, 1048576, 2097152,
6461 5242880, 10485760, 20971520, 52428800, 104857600,
6462 262144000, 524288000, 786432000, 1073741824,
6463 2147483648, 4294967296, 8589934592);
6465 // If custombytes is given and is valid then add it to the list.
6466 if (is_number($custombytes) and $custombytes > 0) {
6467 $custombytes = (int)$custombytes;
6468 if (!in_array($custombytes, $sizelist)) {
6469 $sizelist[] = $custombytes;
6471 } else if (is_array($custombytes)) {
6472 $sizelist = array_unique(array_merge($sizelist, $custombytes));
6475 // Allow maxbytes to be selected if it falls outside the above boundaries.
6476 if (isset($CFG->maxbytes) && !in_array(get_real_size($CFG->maxbytes), $sizelist)) {
6477 // Note: get_real_size() is used in order to prevent problems with invalid values.
6478 $sizelist[] = get_real_size($CFG->maxbytes);
6481 foreach ($sizelist as $sizebytes) {
6482 if ($sizebytes < $maxsize && $sizebytes > 0) {
6483 $filesize[(string)intval($sizebytes)] = display_size($sizebytes, 0);
6487 $limitlevel = '';
6488 $displaysize = '';
6489 if ($modulebytes &&
6490 (($modulebytes < $coursebytes || $coursebytes == 0) &&
6491 ($modulebytes < $sitebytes || $sitebytes == 0))) {
6492 $limitlevel = get_string('activity', 'core');
6493 $displaysize = display_size($modulebytes, 0);
6494 $filesize[$modulebytes] = $displaysize; // Make sure the limit is also included in the list.
6496 } else if ($coursebytes && ($coursebytes < $sitebytes || $sitebytes == 0)) {
6497 $limitlevel = get_string('course', 'core');
6498 $displaysize = display_size($coursebytes, 0);
6499 $filesize[$coursebytes] = $displaysize; // Make sure the limit is also included in the list.
6501 } else if ($sitebytes) {
6502 $limitlevel = get_string('site', 'core');
6503 $displaysize = display_size($sitebytes, 0);
6504 $filesize[$sitebytes] = $displaysize; // Make sure the limit is also included in the list.
6507 krsort($filesize, SORT_NUMERIC);
6508 if ($limitlevel) {
6509 $params = (object) array('contextname' => $limitlevel, 'displaysize' => $displaysize);
6510 $filesize = array('0' => get_string('uploadlimitwithsize', 'core', $params)) + $filesize;
6513 return $filesize;
6517 * Returns an array with all the filenames in all subdirectories, relative to the given rootdir.
6519 * If excludefiles is defined, then that file/directory is ignored
6520 * If getdirs is true, then (sub)directories are included in the output
6521 * If getfiles is true, then files are included in the output
6522 * (at least one of these must be true!)
6524 * @todo Finish documenting this function. Add examples of $excludefile usage.
6526 * @param string $rootdir A given root directory to start from
6527 * @param string|array $excludefiles If defined then the specified file/directory is ignored
6528 * @param bool $descend If true then subdirectories are recursed as well
6529 * @param bool $getdirs If true then (sub)directories are included in the output
6530 * @param bool $getfiles If true then files are included in the output
6531 * @return array An array with all the filenames in all subdirectories, relative to the given rootdir
6533 function get_directory_list($rootdir, $excludefiles='', $descend=true, $getdirs=false, $getfiles=true) {
6535 $dirs = array();
6537 if (!$getdirs and !$getfiles) { // Nothing to show.
6538 return $dirs;
6541 if (!is_dir($rootdir)) { // Must be a directory.
6542 return $dirs;
6545 if (!$dir = opendir($rootdir)) { // Can't open it for some reason.
6546 return $dirs;
6549 if (!is_array($excludefiles)) {
6550 $excludefiles = array($excludefiles);
6553 while (false !== ($file = readdir($dir))) {
6554 $firstchar = substr($file, 0, 1);
6555 if ($firstchar == '.' or $file == 'CVS' or in_array($file, $excludefiles)) {
6556 continue;
6558 $fullfile = $rootdir .'/'. $file;
6559 if (filetype($fullfile) == 'dir') {
6560 if ($getdirs) {
6561 $dirs[] = $file;
6563 if ($descend) {
6564 $subdirs = get_directory_list($fullfile, $excludefiles, $descend, $getdirs, $getfiles);
6565 foreach ($subdirs as $subdir) {
6566 $dirs[] = $file .'/'. $subdir;
6569 } else if ($getfiles) {
6570 $dirs[] = $file;
6573 closedir($dir);
6575 asort($dirs);
6577 return $dirs;
6582 * Adds up all the files in a directory and works out the size.
6584 * @param string $rootdir The directory to start from
6585 * @param string $excludefile A file to exclude when summing directory size
6586 * @return int The summed size of all files and subfiles within the root directory
6588 function get_directory_size($rootdir, $excludefile='') {
6589 global $CFG;
6591 // Do it this way if we can, it's much faster.
6592 if (!empty($CFG->pathtodu) && is_executable(trim($CFG->pathtodu))) {
6593 $command = trim($CFG->pathtodu).' -sk '.escapeshellarg($rootdir);
6594 $output = null;
6595 $return = null;
6596 exec($command, $output, $return);
6597 if (is_array($output)) {
6598 // We told it to return k.
6599 return get_real_size(intval($output[0]).'k');
6603 if (!is_dir($rootdir)) {
6604 // Must be a directory.
6605 return 0;
6608 if (!$dir = @opendir($rootdir)) {
6609 // Can't open it for some reason.
6610 return 0;
6613 $size = 0;
6615 while (false !== ($file = readdir($dir))) {
6616 $firstchar = substr($file, 0, 1);
6617 if ($firstchar == '.' or $file == 'CVS' or $file == $excludefile) {
6618 continue;
6620 $fullfile = $rootdir .'/'. $file;
6621 if (filetype($fullfile) == 'dir') {
6622 $size += get_directory_size($fullfile, $excludefile);
6623 } else {
6624 $size += filesize($fullfile);
6627 closedir($dir);
6629 return $size;
6633 * Converts bytes into display form
6635 * @param int $size The size to convert to human readable form
6636 * @param int $decimalplaces If specified, uses fixed number of decimal places
6637 * @param string $fixedunits If specified, uses fixed units (e.g. 'KB')
6638 * @return string Display version of size
6640 function display_size($size, int $decimalplaces = 1, string $fixedunits = ''): string {
6642 static $units;
6644 if ($size === USER_CAN_IGNORE_FILE_SIZE_LIMITS) {
6645 return get_string('unlimited');
6648 if (empty($units)) {
6649 $units[] = get_string('sizeb');
6650 $units[] = get_string('sizekb');
6651 $units[] = get_string('sizemb');
6652 $units[] = get_string('sizegb');
6653 $units[] = get_string('sizetb');
6654 $units[] = get_string('sizepb');
6657 switch ($fixedunits) {
6658 case 'PB' :
6659 $magnitude = 5;
6660 break;
6661 case 'TB' :
6662 $magnitude = 4;
6663 break;
6664 case 'GB' :
6665 $magnitude = 3;
6666 break;
6667 case 'MB' :
6668 $magnitude = 2;
6669 break;
6670 case 'KB' :
6671 $magnitude = 1;
6672 break;
6673 case 'B' :
6674 $magnitude = 0;
6675 break;
6676 case '':
6677 $magnitude = floor(log($size, 1024));
6678 $magnitude = max(0, min(5, $magnitude));
6679 break;
6680 default:
6681 throw new coding_exception('Unknown fixed units value: ' . $fixedunits);
6684 // Special case for magnitude 0 (bytes) - never use decimal places.
6685 $nbsp = "\xc2\xa0";
6686 if ($magnitude === 0) {
6687 return round($size) . $nbsp . $units[$magnitude];
6690 // Convert to specified units.
6691 $sizeinunit = $size / 1024 ** $magnitude;
6693 // Fixed decimal places.
6694 return sprintf('%.' . $decimalplaces . 'f', $sizeinunit) . $nbsp . $units[$magnitude];
6698 * Cleans a given filename by removing suspicious or troublesome characters
6700 * @see clean_param()
6701 * @param string $string file name
6702 * @return string cleaned file name
6704 function clean_filename($string) {
6705 return clean_param($string, PARAM_FILE);
6708 // STRING TRANSLATION.
6711 * Returns the code for the current language
6713 * @category string
6714 * @return string
6716 function current_language() {
6717 global $CFG, $PAGE, $SESSION, $USER;
6719 if (!empty($SESSION->forcelang)) {
6720 // Allows overriding course-forced language (useful for admins to check
6721 // issues in courses whose language they don't understand).
6722 // Also used by some code to temporarily get language-related information in a
6723 // specific language (see force_current_language()).
6724 $return = $SESSION->forcelang;
6726 } else if (!empty($PAGE->cm->lang)) {
6727 // Activity language, if set.
6728 $return = $PAGE->cm->lang;
6730 } else if (!empty($PAGE->course->id) && $PAGE->course->id != SITEID && !empty($PAGE->course->lang)) {
6731 // Course language can override all other settings for this page.
6732 $return = $PAGE->course->lang;
6734 } else if (!empty($SESSION->lang)) {
6735 // Session language can override other settings.
6736 $return = $SESSION->lang;
6738 } else if (!empty($USER->lang)) {
6739 $return = $USER->lang;
6741 } else if (isset($CFG->lang)) {
6742 $return = $CFG->lang;
6744 } else {
6745 $return = 'en';
6748 // Just in case this slipped in from somewhere by accident.
6749 $return = str_replace('_utf8', '', $return);
6751 return $return;
6755 * Fix the current language to the given language code.
6757 * @param string $lang The language code to use.
6758 * @return void
6760 function fix_current_language(string $lang): void {
6761 global $CFG, $COURSE, $SESSION, $USER;
6763 if (!get_string_manager()->translation_exists($lang)) {
6764 throw new coding_exception("The language pack for $lang is not available");
6767 $fixglobal = '';
6768 $fixlang = 'lang';
6769 if (!empty($SESSION->forcelang)) {
6770 $fixglobal = $SESSION;
6771 $fixlang = 'forcelang';
6772 } else if (!empty($COURSE->id) && $COURSE->id != SITEID && !empty($COURSE->lang)) {
6773 $fixglobal = $COURSE;
6774 } else if (!empty($SESSION->lang)) {
6775 $fixglobal = $SESSION;
6776 } else if (!empty($USER->lang)) {
6777 $fixglobal = $USER;
6778 } else if (isset($CFG->lang)) {
6779 set_config('lang', $lang);
6782 if ($fixglobal) {
6783 $fixglobal->$fixlang = $lang;
6788 * Returns parent language of current active language if defined
6790 * @category string
6791 * @param string $lang null means current language
6792 * @return string
6794 function get_parent_language($lang=null) {
6796 $parentlang = get_string_manager()->get_string('parentlanguage', 'langconfig', null, $lang);
6798 if ($parentlang === 'en') {
6799 $parentlang = '';
6802 return $parentlang;
6806 * Force the current language to get strings and dates localised in the given language.
6808 * After calling this function, all strings will be provided in the given language
6809 * until this function is called again, or equivalent code is run.
6811 * @param string $language
6812 * @return string previous $SESSION->forcelang value
6814 function force_current_language($language) {
6815 global $SESSION;
6816 $sessionforcelang = isset($SESSION->forcelang) ? $SESSION->forcelang : '';
6817 if ($language !== $sessionforcelang) {
6818 // Setting forcelang to null or an empty string disables its effect.
6819 if (empty($language) || get_string_manager()->translation_exists($language, false)) {
6820 $SESSION->forcelang = $language;
6821 moodle_setlocale();
6824 return $sessionforcelang;
6828 * Returns current string_manager instance.
6830 * The param $forcereload is needed for CLI installer only where the string_manager instance
6831 * must be replaced during the install.php script life time.
6833 * @category string
6834 * @param bool $forcereload shall the singleton be released and new instance created instead?
6835 * @return core_string_manager
6837 function get_string_manager($forcereload=false) {
6838 global $CFG;
6840 static $singleton = null;
6842 if ($forcereload) {
6843 $singleton = null;
6845 if ($singleton === null) {
6846 if (empty($CFG->early_install_lang)) {
6848 $transaliases = array();
6849 if (empty($CFG->langlist)) {
6850 $translist = array();
6851 } else {
6852 $translist = explode(',', $CFG->langlist);
6853 $translist = array_map('trim', $translist);
6854 // Each language in the $CFG->langlist can has an "alias" that would substitute the default language name.
6855 foreach ($translist as $i => $value) {
6856 $parts = preg_split('/\s*\|\s*/', $value, 2);
6857 if (count($parts) == 2) {
6858 $transaliases[$parts[0]] = $parts[1];
6859 $translist[$i] = $parts[0];
6864 if (!empty($CFG->config_php_settings['customstringmanager'])) {
6865 $classname = $CFG->config_php_settings['customstringmanager'];
6867 if (class_exists($classname)) {
6868 $implements = class_implements($classname);
6870 if (isset($implements['core_string_manager'])) {
6871 $singleton = new $classname($CFG->langotherroot, $CFG->langlocalroot, $translist, $transaliases);
6872 return $singleton;
6874 } else {
6875 debugging('Unable to instantiate custom string manager: class '.$classname.
6876 ' does not implement the core_string_manager interface.');
6879 } else {
6880 debugging('Unable to instantiate custom string manager: class '.$classname.' can not be found.');
6884 $singleton = new core_string_manager_standard($CFG->langotherroot, $CFG->langlocalroot, $translist, $transaliases);
6886 } else {
6887 $singleton = new core_string_manager_install();
6891 return $singleton;
6895 * Returns a localized string.
6897 * Returns the translated string specified by $identifier as
6898 * for $module. Uses the same format files as STphp.
6899 * $a is an object, string or number that can be used
6900 * within translation strings
6902 * eg 'hello {$a->firstname} {$a->lastname}'
6903 * or 'hello {$a}'
6905 * If you would like to directly echo the localized string use
6906 * the function {@link print_string()}
6908 * Example usage of this function involves finding the string you would
6909 * like a local equivalent of and using its identifier and module information
6910 * to retrieve it.<br/>
6911 * If you open moodle/lang/en/moodle.php and look near line 278
6912 * you will find a string to prompt a user for their word for 'course'
6913 * <code>
6914 * $string['course'] = 'Course';
6915 * </code>
6916 * So if you want to display the string 'Course'
6917 * in any language that supports it on your site
6918 * you just need to use the identifier 'course'
6919 * <code>
6920 * $mystring = '<strong>'. get_string('course') .'</strong>';
6921 * or
6922 * </code>
6923 * If the string you want is in another file you'd take a slightly
6924 * different approach. Looking in moodle/lang/en/calendar.php you find
6925 * around line 75:
6926 * <code>
6927 * $string['typecourse'] = 'Course event';
6928 * </code>
6929 * If you want to display the string "Course event" in any language
6930 * supported you would use the identifier 'typecourse' and the module 'calendar'
6931 * (because it is in the file calendar.php):
6932 * <code>
6933 * $mystring = '<h1>'. get_string('typecourse', 'calendar') .'</h1>';
6934 * </code>
6936 * As a last resort, should the identifier fail to map to a string
6937 * the returned string will be [[ $identifier ]]
6939 * In Moodle 2.3 there is a new argument to this function $lazyload.
6940 * Setting $lazyload to true causes get_string to return a lang_string object
6941 * rather than the string itself. The fetching of the string is then put off until
6942 * the string object is first used. The object can be used by calling it's out
6943 * method or by casting the object to a string, either directly e.g.
6944 * (string)$stringobject
6945 * or indirectly by using the string within another string or echoing it out e.g.
6946 * echo $stringobject
6947 * return "<p>{$stringobject}</p>";
6948 * It is worth noting that using $lazyload and attempting to use the string as an
6949 * array key will cause a fatal error as objects cannot be used as array keys.
6950 * But you should never do that anyway!
6951 * For more information {@link lang_string}
6953 * @category string
6954 * @param string $identifier The key identifier for the localized string
6955 * @param string $component The module where the key identifier is stored,
6956 * usually expressed as the filename in the language pack without the
6957 * .php on the end but can also be written as mod/forum or grade/export/xls.
6958 * If none is specified then moodle.php is used.
6959 * @param string|object|array|int $a An object, string or number that can be used
6960 * within translation strings
6961 * @param bool $lazyload If set to true a string object is returned instead of
6962 * the string itself. The string then isn't calculated until it is first used.
6963 * @return string The localized string.
6964 * @throws coding_exception
6966 function get_string($identifier, $component = '', $a = null, $lazyload = false) {
6967 global $CFG;
6969 // If the lazy load argument has been supplied return a lang_string object
6970 // instead.
6971 // We need to make sure it is true (and a bool) as you will see below there
6972 // used to be a forth argument at one point.
6973 if ($lazyload === true) {
6974 return new lang_string($identifier, $component, $a);
6977 if ($CFG->debugdeveloper && clean_param($identifier, PARAM_STRINGID) === '') {
6978 throw new coding_exception('Invalid string identifier. The identifier cannot be empty. Please fix your get_string() call.', DEBUG_DEVELOPER);
6981 // There is now a forth argument again, this time it is a boolean however so
6982 // we can still check for the old extralocations parameter.
6983 if (!is_bool($lazyload) && !empty($lazyload)) {
6984 debugging('extralocations parameter in get_string() is not supported any more, please use standard lang locations only.');
6987 if (strpos((string)$component, '/') !== false) {
6988 debugging('The module name you passed to get_string is the deprecated format ' .
6989 'like mod/mymod or block/myblock. The correct form looks like mymod, or block_myblock.' , DEBUG_DEVELOPER);
6990 $componentpath = explode('/', $component);
6992 switch ($componentpath[0]) {
6993 case 'mod':
6994 $component = $componentpath[1];
6995 break;
6996 case 'blocks':
6997 case 'block':
6998 $component = 'block_'.$componentpath[1];
6999 break;
7000 case 'enrol':
7001 $component = 'enrol_'.$componentpath[1];
7002 break;
7003 case 'format':
7004 $component = 'format_'.$componentpath[1];
7005 break;
7006 case 'grade':
7007 $component = 'grade'.$componentpath[1].'_'.$componentpath[2];
7008 break;
7012 $result = get_string_manager()->get_string($identifier, $component, $a);
7014 // Debugging feature lets you display string identifier and component.
7015 if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
7016 $result .= ' {' . $identifier . '/' . $component . '}';
7018 return $result;
7022 * Converts an array of strings to their localized value.
7024 * @param array $array An array of strings
7025 * @param string $component The language module that these strings can be found in.
7026 * @return stdClass translated strings.
7028 function get_strings($array, $component = '') {
7029 $string = new stdClass;
7030 foreach ($array as $item) {
7031 $string->$item = get_string($item, $component);
7033 return $string;
7037 * Prints out a translated string.
7039 * Prints out a translated string using the return value from the {@link get_string()} function.
7041 * Example usage of this function when the string is in the moodle.php file:<br/>
7042 * <code>
7043 * echo '<strong>';
7044 * print_string('course');
7045 * echo '</strong>';
7046 * </code>
7048 * Example usage of this function when the string is not in the moodle.php file:<br/>
7049 * <code>
7050 * echo '<h1>';
7051 * print_string('typecourse', 'calendar');
7052 * echo '</h1>';
7053 * </code>
7055 * @category string
7056 * @param string $identifier The key identifier for the localized string
7057 * @param string $component The module where the key identifier is stored. If none is specified then moodle.php is used.
7058 * @param string|object|array $a An object, string or number that can be used within translation strings
7060 function print_string($identifier, $component = '', $a = null) {
7061 echo get_string($identifier, $component, $a);
7065 * Returns a list of charset codes
7067 * Returns a list of charset codes. It's hardcoded, so they should be added manually
7068 * (checking that such charset is supported by the texlib library!)
7070 * @return array And associative array with contents in the form of charset => charset
7072 function get_list_of_charsets() {
7074 $charsets = array(
7075 'EUC-JP' => 'EUC-JP',
7076 'ISO-2022-JP'=> 'ISO-2022-JP',
7077 'ISO-8859-1' => 'ISO-8859-1',
7078 'SHIFT-JIS' => 'SHIFT-JIS',
7079 'GB2312' => 'GB2312',
7080 'GB18030' => 'GB18030', // GB18030 not supported by typo and mbstring.
7081 'UTF-8' => 'UTF-8');
7083 asort($charsets);
7085 return $charsets;
7089 * Returns a list of valid and compatible themes
7091 * @return array
7093 function get_list_of_themes() {
7094 global $CFG;
7096 $themes = array();
7098 if (!empty($CFG->themelist)) { // Use admin's list of themes.
7099 $themelist = explode(',', $CFG->themelist);
7100 } else {
7101 $themelist = array_keys(core_component::get_plugin_list("theme"));
7104 foreach ($themelist as $key => $themename) {
7105 $theme = theme_config::load($themename);
7106 $themes[$themename] = $theme;
7109 core_collator::asort_objects_by_method($themes, 'get_theme_name');
7111 return $themes;
7115 * Factory function for emoticon_manager
7117 * @return emoticon_manager singleton
7119 function get_emoticon_manager() {
7120 static $singleton = null;
7122 if (is_null($singleton)) {
7123 $singleton = new emoticon_manager();
7126 return $singleton;
7130 * Provides core support for plugins that have to deal with emoticons (like HTML editor or emoticon filter).
7132 * Whenever this manager mentiones 'emoticon object', the following data
7133 * structure is expected: stdClass with properties text, imagename, imagecomponent,
7134 * altidentifier and altcomponent
7136 * @see admin_setting_emoticons
7138 * @copyright 2010 David Mudrak
7139 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
7141 class emoticon_manager {
7144 * Returns the currently enabled emoticons
7146 * @param boolean $selectable - If true, only return emoticons that should be selectable from a list.
7147 * @return array of emoticon objects
7149 public function get_emoticons($selectable = false) {
7150 global $CFG;
7151 $notselectable = ['martin', 'egg'];
7153 if (empty($CFG->emoticons)) {
7154 return array();
7157 $emoticons = $this->decode_stored_config($CFG->emoticons);
7159 if (!is_array($emoticons)) {
7160 // Something is wrong with the format of stored setting.
7161 debugging('Invalid format of emoticons setting, please resave the emoticons settings form', DEBUG_NORMAL);
7162 return array();
7164 if ($selectable) {
7165 foreach ($emoticons as $index => $emote) {
7166 if (in_array($emote->altidentifier, $notselectable)) {
7167 // Skip this one.
7168 unset($emoticons[$index]);
7173 return $emoticons;
7177 * Converts emoticon object into renderable pix_emoticon object
7179 * @param stdClass $emoticon emoticon object
7180 * @param array $attributes explicit HTML attributes to set
7181 * @return pix_emoticon
7183 public function prepare_renderable_emoticon(stdClass $emoticon, array $attributes = array()) {
7184 $stringmanager = get_string_manager();
7185 if ($stringmanager->string_exists($emoticon->altidentifier, $emoticon->altcomponent)) {
7186 $alt = get_string($emoticon->altidentifier, $emoticon->altcomponent);
7187 } else {
7188 $alt = s($emoticon->text);
7190 return new pix_emoticon($emoticon->imagename, $alt, $emoticon->imagecomponent, $attributes);
7194 * Encodes the array of emoticon objects into a string storable in config table
7196 * @see self::decode_stored_config()
7197 * @param array $emoticons array of emtocion objects
7198 * @return string
7200 public function encode_stored_config(array $emoticons) {
7201 return json_encode($emoticons);
7205 * Decodes the string into an array of emoticon objects
7207 * @see self::encode_stored_config()
7208 * @param string $encoded
7209 * @return array|null
7211 public function decode_stored_config($encoded) {
7212 $decoded = json_decode($encoded);
7213 if (!is_array($decoded)) {
7214 return null;
7216 return $decoded;
7220 * Returns default set of emoticons supported by Moodle
7222 * @return array of sdtClasses
7224 public function default_emoticons() {
7225 return array(
7226 $this->prepare_emoticon_object(":-)", 's/smiley', 'smiley'),
7227 $this->prepare_emoticon_object(":)", 's/smiley', 'smiley'),
7228 $this->prepare_emoticon_object(":-D", 's/biggrin', 'biggrin'),
7229 $this->prepare_emoticon_object(";-)", 's/wink', 'wink'),
7230 $this->prepare_emoticon_object(":-/", 's/mixed', 'mixed'),
7231 $this->prepare_emoticon_object("V-.", 's/thoughtful', 'thoughtful'),
7232 $this->prepare_emoticon_object(":-P", 's/tongueout', 'tongueout'),
7233 $this->prepare_emoticon_object(":-p", 's/tongueout', 'tongueout'),
7234 $this->prepare_emoticon_object("B-)", 's/cool', 'cool'),
7235 $this->prepare_emoticon_object("^-)", 's/approve', 'approve'),
7236 $this->prepare_emoticon_object("8-)", 's/wideeyes', 'wideeyes'),
7237 $this->prepare_emoticon_object(":o)", 's/clown', 'clown'),
7238 $this->prepare_emoticon_object(":-(", 's/sad', 'sad'),
7239 $this->prepare_emoticon_object(":(", 's/sad', 'sad'),
7240 $this->prepare_emoticon_object("8-.", 's/shy', 'shy'),
7241 $this->prepare_emoticon_object(":-I", 's/blush', 'blush'),
7242 $this->prepare_emoticon_object(":-X", 's/kiss', 'kiss'),
7243 $this->prepare_emoticon_object("8-o", 's/surprise', 'surprise'),
7244 $this->prepare_emoticon_object("P-|", 's/blackeye', 'blackeye'),
7245 $this->prepare_emoticon_object("8-[", 's/angry', 'angry'),
7246 $this->prepare_emoticon_object("(grr)", 's/angry', 'angry'),
7247 $this->prepare_emoticon_object("xx-P", 's/dead', 'dead'),
7248 $this->prepare_emoticon_object("|-.", 's/sleepy', 'sleepy'),
7249 $this->prepare_emoticon_object("}-]", 's/evil', 'evil'),
7250 $this->prepare_emoticon_object("(h)", 's/heart', 'heart'),
7251 $this->prepare_emoticon_object("(heart)", 's/heart', 'heart'),
7252 $this->prepare_emoticon_object("(y)", 's/yes', 'yes', 'core'),
7253 $this->prepare_emoticon_object("(n)", 's/no', 'no', 'core'),
7254 $this->prepare_emoticon_object("(martin)", 's/martin', 'martin'),
7255 $this->prepare_emoticon_object("( )", 's/egg', 'egg'),
7260 * Helper method preparing the stdClass with the emoticon properties
7262 * @param string|array $text or array of strings
7263 * @param string $imagename to be used by {@link pix_emoticon}
7264 * @param string $altidentifier alternative string identifier, null for no alt
7265 * @param string $altcomponent where the alternative string is defined
7266 * @param string $imagecomponent to be used by {@link pix_emoticon}
7267 * @return stdClass
7269 protected function prepare_emoticon_object($text, $imagename, $altidentifier = null,
7270 $altcomponent = 'core_pix', $imagecomponent = 'core') {
7271 return (object)array(
7272 'text' => $text,
7273 'imagename' => $imagename,
7274 'imagecomponent' => $imagecomponent,
7275 'altidentifier' => $altidentifier,
7276 'altcomponent' => $altcomponent,
7281 // ENCRYPTION.
7284 * rc4encrypt
7286 * @param string $data Data to encrypt.
7287 * @return string The now encrypted data.
7289 function rc4encrypt($data) {
7290 return endecrypt(get_site_identifier(), $data, '');
7294 * rc4decrypt
7296 * @param string $data Data to decrypt.
7297 * @return string The now decrypted data.
7299 function rc4decrypt($data) {
7300 return endecrypt(get_site_identifier(), $data, 'de');
7304 * Based on a class by Mukul Sabharwal [mukulsabharwal @ yahoo.com]
7306 * @todo Finish documenting this function
7308 * @param string $pwd The password to use when encrypting or decrypting
7309 * @param string $data The data to be decrypted/encrypted
7310 * @param string $case Either 'de' for decrypt or '' for encrypt
7311 * @return string
7313 function endecrypt ($pwd, $data, $case) {
7315 if ($case == 'de') {
7316 $data = urldecode($data);
7319 $key[] = '';
7320 $box[] = '';
7321 $pwdlength = strlen($pwd);
7323 for ($i = 0; $i <= 255; $i++) {
7324 $key[$i] = ord(substr($pwd, ($i % $pwdlength), 1));
7325 $box[$i] = $i;
7328 $x = 0;
7330 for ($i = 0; $i <= 255; $i++) {
7331 $x = ($x + $box[$i] + $key[$i]) % 256;
7332 $tempswap = $box[$i];
7333 $box[$i] = $box[$x];
7334 $box[$x] = $tempswap;
7337 $cipher = '';
7339 $a = 0;
7340 $j = 0;
7342 for ($i = 0; $i < strlen($data); $i++) {
7343 $a = ($a + 1) % 256;
7344 $j = ($j + $box[$a]) % 256;
7345 $temp = $box[$a];
7346 $box[$a] = $box[$j];
7347 $box[$j] = $temp;
7348 $k = $box[(($box[$a] + $box[$j]) % 256)];
7349 $cipherby = ord(substr($data, $i, 1)) ^ $k;
7350 $cipher .= chr($cipherby);
7353 if ($case == 'de') {
7354 $cipher = urldecode(urlencode($cipher));
7355 } else {
7356 $cipher = urlencode($cipher);
7359 return $cipher;
7362 // ENVIRONMENT CHECKING.
7365 * This method validates a plug name. It is much faster than calling clean_param.
7367 * @param string $name a string that might be a plugin name.
7368 * @return bool if this string is a valid plugin name.
7370 function is_valid_plugin_name($name) {
7371 // This does not work for 'mod', bad luck, use any other type.
7372 return core_component::is_valid_plugin_name('tool', $name);
7376 * Get a list of all the plugins of a given type that define a certain API function
7377 * in a certain file. The plugin component names and function names are returned.
7379 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
7380 * @param string $function the part of the name of the function after the
7381 * frankenstyle prefix. e.g 'hook' if you are looking for functions with
7382 * names like report_courselist_hook.
7383 * @param string $file the name of file within the plugin that defines the
7384 * function. Defaults to lib.php.
7385 * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
7386 * and the function names as values (e.g. 'report_courselist_hook', 'forum_hook').
7388 function get_plugin_list_with_function($plugintype, $function, $file = 'lib.php') {
7389 global $CFG;
7391 // We don't include here as all plugin types files would be included.
7392 $plugins = get_plugins_with_function($function, $file, false);
7394 if (empty($plugins[$plugintype])) {
7395 return array();
7398 $allplugins = core_component::get_plugin_list($plugintype);
7400 // Reformat the array and include the files.
7401 $pluginfunctions = array();
7402 foreach ($plugins[$plugintype] as $pluginname => $functionname) {
7404 // Check that it has not been removed and the file is still available.
7405 if (!empty($allplugins[$pluginname])) {
7407 $filepath = $allplugins[$pluginname] . DIRECTORY_SEPARATOR . $file;
7408 if (file_exists($filepath)) {
7409 include_once($filepath);
7411 // Now that the file is loaded, we must verify the function still exists.
7412 if (function_exists($functionname)) {
7413 $pluginfunctions[$plugintype . '_' . $pluginname] = $functionname;
7414 } else {
7415 // Invalidate the cache for next run.
7416 \cache_helper::invalidate_by_definition('core', 'plugin_functions');
7422 return $pluginfunctions;
7426 * Get a list of all the plugins that define a certain API function in a certain file.
7428 * @param string $function the part of the name of the function after the
7429 * frankenstyle prefix. e.g 'hook' if you are looking for functions with
7430 * names like report_courselist_hook.
7431 * @param string $file the name of file within the plugin that defines the
7432 * function. Defaults to lib.php.
7433 * @param bool $include Whether to include the files that contain the functions or not.
7434 * @param bool $migratedtohook if true this is a deprecated lib.php callback, if hook callback is present then do nothing
7435 * @return array with [plugintype][plugin] = functionname
7437 function get_plugins_with_function($function, $file = 'lib.php', $include = true, bool $migratedtohook = false) {
7438 global $CFG;
7440 if (during_initial_install() || isset($CFG->upgraderunning)) {
7441 // API functions _must not_ be called during an installation or upgrade.
7442 return [];
7445 $plugincallback = $function;
7446 $filtermigrated = function($plugincallback, $pluginfunctions): array {
7447 foreach ($pluginfunctions as $plugintype => $plugins) {
7448 foreach ($plugins as $plugin => $unusedfunction) {
7449 $component = $plugintype . '_' . $plugin;
7450 if ($hooks = di::get(hook\manager::class)->get_hooks_deprecating_plugin_callback($plugincallback)) {
7451 if (di::get(hook\manager::class)->is_deprecating_hook_present($component, $plugincallback)) {
7452 // Ignore the old callback, it is there only for older Moodle versions.
7453 unset($pluginfunctions[$plugintype][$plugin]);
7454 } else {
7455 $hookmessage = count($hooks) == 1 ? reset($hooks) : 'one of ' . implode(', ', $hooks);
7456 debugging(
7457 "Callback $plugincallback in $component component should be migrated to new " .
7458 "hook callback for $hookmessage",
7459 DEBUG_DEVELOPER
7465 return $pluginfunctions;
7468 $cache = \cache::make('core', 'plugin_functions');
7470 // Including both although I doubt that we will find two functions definitions with the same name.
7471 // Clean the filename as cache_helper::hash_key only allows a-zA-Z0-9_.
7472 $pluginfunctions = false;
7473 if (!empty($CFG->allversionshash)) {
7474 $key = $CFG->allversionshash . '_' . $function . '_' . clean_param($file, PARAM_ALPHA);
7475 $pluginfunctions = $cache->get($key);
7477 $dirty = false;
7479 // Use the plugin manager to check that plugins are currently installed.
7480 $pluginmanager = \core_plugin_manager::instance();
7482 if ($pluginfunctions !== false) {
7484 // Checking that the files are still available.
7485 foreach ($pluginfunctions as $plugintype => $plugins) {
7487 $allplugins = \core_component::get_plugin_list($plugintype);
7488 $installedplugins = $pluginmanager->get_installed_plugins($plugintype);
7489 foreach ($plugins as $plugin => $function) {
7490 if (!isset($installedplugins[$plugin])) {
7491 // Plugin code is still present on disk but it is not installed.
7492 $dirty = true;
7493 break 2;
7496 // Cache might be out of sync with the codebase, skip the plugin if it is not available.
7497 if (empty($allplugins[$plugin])) {
7498 $dirty = true;
7499 break 2;
7502 $fileexists = file_exists($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file);
7503 if ($include && $fileexists) {
7504 // Include the files if it was requested.
7505 include_once($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file);
7506 } else if (!$fileexists) {
7507 // If the file is not available any more it should not be returned.
7508 $dirty = true;
7509 break 2;
7512 // Check if the function still exists in the file.
7513 if ($include && !function_exists($function)) {
7514 $dirty = true;
7515 break 2;
7520 // If the cache is dirty, we should fall through and let it rebuild.
7521 if (!$dirty) {
7522 if ($migratedtohook && $file === 'lib.php') {
7523 $pluginfunctions = $filtermigrated($plugincallback, $pluginfunctions);
7525 return $pluginfunctions;
7529 $pluginfunctions = array();
7531 // To fill the cached. Also, everything should continue working with cache disabled.
7532 $plugintypes = \core_component::get_plugin_types();
7533 foreach ($plugintypes as $plugintype => $unused) {
7535 // We need to include files here.
7536 $pluginswithfile = \core_component::get_plugin_list_with_file($plugintype, $file, true);
7537 $installedplugins = $pluginmanager->get_installed_plugins($plugintype);
7538 foreach ($pluginswithfile as $plugin => $notused) {
7540 if (!isset($installedplugins[$plugin])) {
7541 continue;
7544 $fullfunction = $plugintype . '_' . $plugin . '_' . $function;
7546 $pluginfunction = false;
7547 if (function_exists($fullfunction)) {
7548 // Function exists with standard name. Store, indexed by frankenstyle name of plugin.
7549 $pluginfunction = $fullfunction;
7551 } else if ($plugintype === 'mod') {
7552 // For modules, we also allow plugin without full frankenstyle but just starting with the module name.
7553 $shortfunction = $plugin . '_' . $function;
7554 if (function_exists($shortfunction)) {
7555 $pluginfunction = $shortfunction;
7559 if ($pluginfunction) {
7560 if (empty($pluginfunctions[$plugintype])) {
7561 $pluginfunctions[$plugintype] = array();
7563 $pluginfunctions[$plugintype][$plugin] = $pluginfunction;
7568 if (!empty($CFG->allversionshash)) {
7569 $cache->set($key, $pluginfunctions);
7572 if ($migratedtohook && $file === 'lib.php') {
7573 $pluginfunctions = $filtermigrated($plugincallback, $pluginfunctions);
7576 return $pluginfunctions;
7581 * Lists plugin-like directories within specified directory
7583 * This function was originally used for standard Moodle plugins, please use
7584 * new core_component::get_plugin_list() now.
7586 * This function is used for general directory listing and backwards compatility.
7588 * @param string $directory relative directory from root
7589 * @param string $exclude dir name to exclude from the list (defaults to none)
7590 * @param string $basedir full path to the base dir where $plugin resides (defaults to $CFG->dirroot)
7591 * @return array Sorted array of directory names found under the requested parameters
7593 function get_list_of_plugins($directory='mod', $exclude='', $basedir='') {
7594 global $CFG;
7596 $plugins = array();
7598 if (empty($basedir)) {
7599 $basedir = $CFG->dirroot .'/'. $directory;
7601 } else {
7602 $basedir = $basedir .'/'. $directory;
7605 if ($CFG->debugdeveloper and empty($exclude)) {
7606 // Make sure devs do not use this to list normal plugins,
7607 // this is intended for general directories that are not plugins!
7609 $subtypes = core_component::get_plugin_types();
7610 if (in_array($basedir, $subtypes)) {
7611 debugging('get_list_of_plugins() should not be used to list real plugins, use core_component::get_plugin_list() instead!', DEBUG_DEVELOPER);
7613 unset($subtypes);
7616 $ignorelist = array_flip(array_filter([
7617 'CVS',
7618 '_vti_cnf',
7619 'amd',
7620 'classes',
7621 'simpletest',
7622 'tests',
7623 'templates',
7624 'yui',
7625 $exclude,
7626 ]));
7628 if (file_exists($basedir) && filetype($basedir) == 'dir') {
7629 if (!$dirhandle = opendir($basedir)) {
7630 debugging("Directory permission error for plugin ({$directory}). Directory exists but cannot be read.", DEBUG_DEVELOPER);
7631 return array();
7633 while (false !== ($dir = readdir($dirhandle))) {
7634 if (strpos($dir, '.') === 0) {
7635 // Ignore directories starting with .
7636 // These are treated as hidden directories.
7637 continue;
7639 if (array_key_exists($dir, $ignorelist)) {
7640 // This directory features on the ignore list.
7641 continue;
7643 if (filetype($basedir .'/'. $dir) != 'dir') {
7644 continue;
7646 $plugins[] = $dir;
7648 closedir($dirhandle);
7650 if ($plugins) {
7651 asort($plugins);
7653 return $plugins;
7657 * Invoke plugin's callback functions
7659 * @param string $type plugin type e.g. 'mod'
7660 * @param string $name plugin name
7661 * @param string $feature feature name
7662 * @param string $action feature's action
7663 * @param array $params parameters of callback function, should be an array
7664 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
7665 * @param bool $migratedtohook if true this is a deprecated callback, if hook callback is present then do nothing
7666 * @return mixed
7668 * @todo Decide about to deprecate and drop plugin_callback() - MDL-30743
7670 function plugin_callback($type, $name, $feature, $action, $params = null, $default = null, bool $migratedtohook = false) {
7671 return component_callback($type . '_' . $name, $feature . '_' . $action, (array) $params, $default, $migratedtohook);
7675 * Invoke component's callback functions
7677 * @param string $component frankenstyle component name, e.g. 'mod_quiz'
7678 * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
7679 * @param array $params parameters of callback function
7680 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
7681 * @param bool $migratedtohook if true this is a deprecated callback, if hook callback is present then do nothing
7682 * @return mixed
7684 function component_callback($component, $function, array $params = array(), $default = null, bool $migratedtohook = false) {
7685 $functionname = component_callback_exists($component, $function);
7687 if ($functionname) {
7688 if ($migratedtohook) {
7689 $hookmanager = di::get(hook\manager::class);
7690 if ($hooks = $hookmanager->get_hooks_deprecating_plugin_callback($function)) {
7691 if ($hookmanager->is_deprecating_hook_present($component, $function)) {
7692 // Do not call the old lib.php callback,
7693 // it is there for compatibility with older Moodle versions only.
7694 return null;
7695 } else {
7696 $hookmessage = count($hooks) == 1 ? reset($hooks) : 'one of ' . implode(', ', $hooks);
7697 debugging(
7698 "Callback $function in $component component should be migrated to new hook callback for $hookmessage",
7699 DEBUG_DEVELOPER);
7704 // Function exists, so just return function result.
7705 $ret = call_user_func_array($functionname, $params);
7706 if (is_null($ret)) {
7707 return $default;
7708 } else {
7709 return $ret;
7712 return $default;
7716 * Determine if a component callback exists and return the function name to call. Note that this
7717 * function will include the required library files so that the functioname returned can be
7718 * called directly.
7720 * @param string $component frankenstyle component name, e.g. 'mod_quiz'
7721 * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
7722 * @return mixed Complete function name to call if the callback exists or false if it doesn't.
7723 * @throws coding_exception if invalid component specfied
7725 function component_callback_exists($component, $function) {
7726 global $CFG; // This is needed for the inclusions.
7728 $cleancomponent = clean_param($component, PARAM_COMPONENT);
7729 if (empty($cleancomponent)) {
7730 throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
7732 $component = $cleancomponent;
7734 list($type, $name) = core_component::normalize_component($component);
7735 $component = $type . '_' . $name;
7737 $oldfunction = $name.'_'.$function;
7738 $function = $component.'_'.$function;
7740 $dir = core_component::get_component_directory($component);
7741 if (empty($dir)) {
7742 throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
7745 // Load library and look for function.
7746 if (file_exists($dir.'/lib.php')) {
7747 require_once($dir.'/lib.php');
7750 if (!function_exists($function) and function_exists($oldfunction)) {
7751 if ($type !== 'mod' and $type !== 'core') {
7752 debugging("Please use new function name $function instead of legacy $oldfunction", DEBUG_DEVELOPER);
7754 $function = $oldfunction;
7757 if (function_exists($function)) {
7758 return $function;
7760 return false;
7764 * Call the specified callback method on the provided class.
7766 * If the callback returns null, then the default value is returned instead.
7767 * If the class does not exist, then the default value is returned.
7769 * @param string $classname The name of the class to call upon.
7770 * @param string $methodname The name of the staticically defined method on the class.
7771 * @param array $params The arguments to pass into the method.
7772 * @param mixed $default The default value.
7773 * @param bool $migratedtohook True if the callback has been migrated to a hook.
7774 * @return mixed The return value.
7776 function component_class_callback($classname, $methodname, array $params, $default = null, bool $migratedtohook = false) {
7777 if (!class_exists($classname)) {
7778 return $default;
7781 if (!method_exists($classname, $methodname)) {
7782 return $default;
7785 $fullfunction = $classname . '::' . $methodname;
7787 if ($migratedtohook) {
7788 $functionparts = explode('\\', trim($fullfunction, '\\'));
7789 $component = $functionparts[0];
7790 $callback = end($functionparts);
7791 $hookmanager = di::get(hook\manager::class);
7792 if ($hooks = $hookmanager->get_hooks_deprecating_plugin_callback($callback)) {
7793 if ($hookmanager->is_deprecating_hook_present($component, $callback)) {
7794 // Do not call the old class callback,
7795 // it is there for compatibility with older Moodle versions only.
7796 return null;
7797 } else {
7798 $hookmessage = count($hooks) == 1 ? reset($hooks) : 'one of ' . implode(', ', $hooks);
7799 debugging("Callback $callback in $component component should be migrated to new hook callback for $hookmessage",
7800 DEBUG_DEVELOPER);
7805 $result = call_user_func_array($fullfunction, $params);
7807 if (null === $result) {
7808 return $default;
7809 } else {
7810 return $result;
7815 * Checks whether a plugin supports a specified feature.
7817 * @param string $type Plugin type e.g. 'mod'
7818 * @param string $name Plugin name e.g. 'forum'
7819 * @param string $feature Feature code (FEATURE_xx constant)
7820 * @param mixed $default default value if feature support unknown
7821 * @return mixed Feature result (false if not supported, null if feature is unknown,
7822 * otherwise usually true but may have other feature-specific value such as array)
7823 * @throws coding_exception
7825 function plugin_supports($type, $name, $feature, $default = null) {
7826 global $CFG;
7828 if ($type === 'mod' and $name === 'NEWMODULE') {
7829 // Somebody forgot to rename the module template.
7830 return false;
7833 $component = clean_param($type . '_' . $name, PARAM_COMPONENT);
7834 if (empty($component)) {
7835 throw new coding_exception('Invalid component used in plugin_supports():' . $type . '_' . $name);
7838 $function = null;
7840 if ($type === 'mod') {
7841 // We need this special case because we support subplugins in modules,
7842 // otherwise it would end up in infinite loop.
7843 if (file_exists("$CFG->dirroot/mod/$name/lib.php")) {
7844 include_once("$CFG->dirroot/mod/$name/lib.php");
7845 $function = $component.'_supports';
7846 if (!function_exists($function)) {
7847 // Legacy non-frankenstyle function name.
7848 $function = $name.'_supports';
7852 } else {
7853 if (!$path = core_component::get_plugin_directory($type, $name)) {
7854 // Non existent plugin type.
7855 return false;
7857 if (file_exists("$path/lib.php")) {
7858 include_once("$path/lib.php");
7859 $function = $component.'_supports';
7863 if ($function and function_exists($function)) {
7864 $supports = $function($feature);
7865 if (is_null($supports)) {
7866 // Plugin does not know - use default.
7867 return $default;
7868 } else {
7869 return $supports;
7873 // Plugin does not care, so use default.
7874 return $default;
7878 * Returns true if the current version of PHP is greater that the specified one.
7880 * @todo Check PHP version being required here is it too low?
7882 * @param string $version The version of php being tested.
7883 * @return bool
7885 function check_php_version($version='5.2.4') {
7886 return (version_compare(phpversion(), $version) >= 0);
7890 * Determine if moodle installation requires update.
7892 * Checks version numbers of main code and all plugins to see
7893 * if there are any mismatches.
7895 * @param bool $checkupgradeflag check the outagelessupgrade flag to see if an upgrade is running.
7896 * @return bool
7898 function moodle_needs_upgrading($checkupgradeflag = true) {
7899 global $CFG, $DB;
7901 // Say no if there is already an upgrade running.
7902 if ($checkupgradeflag) {
7903 $lock = $DB->get_field('config', 'value', ['name' => 'outagelessupgrade']);
7904 $currentprocessrunningupgrade = (defined('CLI_UPGRADE_RUNNING') && CLI_UPGRADE_RUNNING);
7905 // If we ARE locked, but this PHP process is NOT the process running the upgrade,
7906 // We should always return false.
7907 // This means the upgrade is running from CLI somewhere, or about to.
7908 if (!empty($lock) && !$currentprocessrunningupgrade) {
7909 return false;
7913 if (empty($CFG->version)) {
7914 return true;
7917 // There is no need to purge plugininfo caches here because
7918 // these caches are not used during upgrade and they are purged after
7919 // every upgrade.
7921 if (empty($CFG->allversionshash)) {
7922 return true;
7925 $hash = core_component::get_all_versions_hash();
7927 return ($hash !== $CFG->allversionshash);
7931 * Returns the major version of this site
7933 * Moodle version numbers consist of three numbers separated by a dot, for
7934 * example 1.9.11 or 2.0.2. The first two numbers, like 1.9 or 2.0, represent so
7935 * called major version. This function extracts the major version from either
7936 * $CFG->release (default) or eventually from the $release variable defined in
7937 * the main version.php.
7939 * @param bool $fromdisk should the version if source code files be used
7940 * @return string|false the major version like '2.3', false if could not be determined
7942 function moodle_major_version($fromdisk = false) {
7943 global $CFG;
7945 if ($fromdisk) {
7946 $release = null;
7947 require($CFG->dirroot.'/version.php');
7948 if (empty($release)) {
7949 return false;
7952 } else {
7953 if (empty($CFG->release)) {
7954 return false;
7956 $release = $CFG->release;
7959 if (preg_match('/^[0-9]+\.[0-9]+/', $release, $matches)) {
7960 return $matches[0];
7961 } else {
7962 return false;
7966 // MISCELLANEOUS.
7969 * Gets the system locale
7971 * @return string Retuns the current locale.
7973 function moodle_getlocale() {
7974 global $CFG;
7976 // Fetch the correct locale based on ostype.
7977 if ($CFG->ostype == 'WINDOWS') {
7978 $stringtofetch = 'localewin';
7979 } else {
7980 $stringtofetch = 'locale';
7983 if (!empty($CFG->locale)) { // Override locale for all language packs.
7984 return $CFG->locale;
7987 return get_string($stringtofetch, 'langconfig');
7991 * Sets the system locale
7993 * @category string
7994 * @param string $locale Can be used to force a locale
7996 function moodle_setlocale($locale='') {
7997 global $CFG;
7999 static $currentlocale = ''; // Last locale caching.
8001 $oldlocale = $currentlocale;
8003 // The priority is the same as in get_string() - parameter, config, course, session, user, global language.
8004 if (!empty($locale)) {
8005 $currentlocale = $locale;
8006 } else {
8007 $currentlocale = moodle_getlocale();
8010 // Do nothing if locale already set up.
8011 if ($oldlocale == $currentlocale) {
8012 return;
8015 // Due to some strange BUG we cannot set the LC_TIME directly, so we fetch current values,
8016 // set LC_ALL and then set values again. Just wondering why we cannot set LC_ALL only??? - stronk7
8017 // Some day, numeric, monetary and other categories should be set too, I think. :-/.
8019 // Get current values.
8020 $monetary= setlocale (LC_MONETARY, 0);
8021 $numeric = setlocale (LC_NUMERIC, 0);
8022 $ctype = setlocale (LC_CTYPE, 0);
8023 if ($CFG->ostype != 'WINDOWS') {
8024 $messages= setlocale (LC_MESSAGES, 0);
8026 // Set locale to all.
8027 $result = setlocale (LC_ALL, $currentlocale);
8028 // If setting of locale fails try the other utf8 or utf-8 variant,
8029 // some operating systems support both (Debian), others just one (OSX).
8030 if ($result === false) {
8031 if (stripos($currentlocale, '.UTF-8') !== false) {
8032 $newlocale = str_ireplace('.UTF-8', '.UTF8', $currentlocale);
8033 setlocale (LC_ALL, $newlocale);
8034 } else if (stripos($currentlocale, '.UTF8') !== false) {
8035 $newlocale = str_ireplace('.UTF8', '.UTF-8', $currentlocale);
8036 setlocale (LC_ALL, $newlocale);
8039 // Set old values.
8040 setlocale (LC_MONETARY, $monetary);
8041 setlocale (LC_NUMERIC, $numeric);
8042 if ($CFG->ostype != 'WINDOWS') {
8043 setlocale (LC_MESSAGES, $messages);
8045 if ($currentlocale == 'tr_TR' or $currentlocale == 'tr_TR.UTF-8') {
8046 // To workaround a well-known PHP problem with Turkish letter Ii.
8047 setlocale (LC_CTYPE, $ctype);
8052 * Count words in a string.
8054 * Words are defined as things between whitespace.
8056 * @category string
8057 * @param string $string The text to be searched for words. May be HTML.
8058 * @param int|null $format
8059 * @return int The count of words in the specified string
8061 function count_words($string, $format = null) {
8062 // Before stripping tags, add a space after the close tag of anything that is not obviously inline.
8063 // Also, br is a special case because it definitely delimits a word, but has no close tag.
8064 $string = preg_replace('~
8065 ( # Capture the tag we match.
8066 </ # Start of close tag.
8067 (?! # Do not match any of these specific close tag names.
8068 a> | b> | del> | em> | i> |
8069 ins> | s> | small> | span> |
8070 strong> | sub> | sup> | u>
8072 \w+ # But, apart from those execptions, match any tag name.
8073 > # End of close tag.
8075 <br> | <br\s*/> # Special cases that are not close tags.
8077 ~x', '$1 ', $string); // Add a space after the close tag.
8078 if ($format !== null && $format != FORMAT_PLAIN) {
8079 // Match the usual text cleaning before display.
8080 // Ideally we should apply multilang filter only here, other filters might add extra text.
8081 $string = format_text($string, $format, ['filter' => false, 'noclean' => false, 'para' => false]);
8083 // Now remove HTML tags.
8084 $string = strip_tags($string);
8085 // Decode HTML entities.
8086 $string = html_entity_decode($string, ENT_COMPAT);
8088 // Now, the word count is the number of blocks of characters separated
8089 // by any sort of space. That seems to be the definition used by all other systems.
8090 // To be precise about what is considered to separate words:
8091 // * Anything that Unicode considers a 'Separator'
8092 // * Anything that Unicode considers a 'Control character'
8093 // * An em- or en- dash.
8094 return count(preg_split('~[\p{Z}\p{Cc}—–]+~u', $string, -1, PREG_SPLIT_NO_EMPTY));
8098 * Count letters in a string.
8100 * Letters are defined as chars not in tags and different from whitespace.
8102 * @category string
8103 * @param string $string The text to be searched for letters. May be HTML.
8104 * @param int|null $format
8105 * @return int The count of letters in the specified text.
8107 function count_letters($string, $format = null) {
8108 if ($format !== null && $format != FORMAT_PLAIN) {
8109 // Match the usual text cleaning before display.
8110 // Ideally we should apply multilang filter only here, other filters might add extra text.
8111 $string = format_text($string, $format, ['filter' => false, 'noclean' => false, 'para' => false]);
8113 $string = strip_tags($string); // Tags are out now.
8114 $string = html_entity_decode($string, ENT_COMPAT);
8115 $string = preg_replace('/[[:space:]]*/', '', $string); // Whitespace are out now.
8117 return core_text::strlen($string);
8121 * Generate and return a random string of the specified length.
8123 * @param int $length The length of the string to be created.
8124 * @return string
8126 function random_string($length=15) {
8127 $randombytes = random_bytes($length);
8128 $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
8129 $pool .= 'abcdefghijklmnopqrstuvwxyz';
8130 $pool .= '0123456789';
8131 $poollen = strlen($pool);
8132 $string = '';
8133 for ($i = 0; $i < $length; $i++) {
8134 $rand = ord($randombytes[$i]);
8135 $string .= substr($pool, ($rand%($poollen)), 1);
8137 return $string;
8141 * Generate a complex random string (useful for md5 salts)
8143 * This function is based on the above {@link random_string()} however it uses a
8144 * larger pool of characters and generates a string between 24 and 32 characters
8146 * @param int $length Optional if set generates a string to exactly this length
8147 * @return string
8149 function complex_random_string($length=null) {
8150 $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
8151 $pool .= '`~!@#%^&*()_+-=[];,./<>?:{} ';
8152 $poollen = strlen($pool);
8153 if ($length===null) {
8154 $length = floor(rand(24, 32));
8156 $randombytes = random_bytes($length);
8157 $string = '';
8158 for ($i = 0; $i < $length; $i++) {
8159 $rand = ord($randombytes[$i]);
8160 $string .= $pool[($rand%$poollen)];
8162 return $string;
8166 * Given some text (which may contain HTML) and an ideal length,
8167 * this function truncates the text neatly on a word boundary if possible
8169 * @category string
8170 * @param string $text text to be shortened
8171 * @param int $ideal ideal string length
8172 * @param boolean $exact if false, $text will not be cut mid-word
8173 * @param string $ending The string to append if the passed string is truncated
8174 * @return string $truncate shortened string
8176 function shorten_text($text, $ideal=30, $exact = false, $ending='...') {
8177 // If the plain text is shorter than the maximum length, return the whole text.
8178 if (core_text::strlen(preg_replace('/<.*?>/', '', $text)) <= $ideal) {
8179 return $text;
8182 // Splits on HTML tags. Each open/close/empty tag will be the first thing
8183 // and only tag in its 'line'.
8184 preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER);
8186 $totallength = core_text::strlen($ending);
8187 $truncate = '';
8189 // This array stores information about open and close tags and their position
8190 // in the truncated string. Each item in the array is an object with fields
8191 // ->open (true if open), ->tag (tag name in lower case), and ->pos
8192 // (byte position in truncated text).
8193 $tagdetails = array();
8195 foreach ($lines as $linematchings) {
8196 // If there is any html-tag in this line, handle it and add it (uncounted) to the output.
8197 if (!empty($linematchings[1])) {
8198 // If it's an "empty element" with or without xhtml-conform closing slash (f.e. <br/>).
8199 if (!preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $linematchings[1])) {
8200 if (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $linematchings[1], $tagmatchings)) {
8201 // Record closing tag.
8202 $tagdetails[] = (object) array(
8203 'open' => false,
8204 'tag' => core_text::strtolower($tagmatchings[1]),
8205 'pos' => core_text::strlen($truncate),
8208 } else if (preg_match('/^<\s*([^\s>!]+).*?>$/s', $linematchings[1], $tagmatchings)) {
8209 // Record opening tag.
8210 $tagdetails[] = (object) array(
8211 'open' => true,
8212 'tag' => core_text::strtolower($tagmatchings[1]),
8213 'pos' => core_text::strlen($truncate),
8215 } else if (preg_match('/^<!--\[if\s.*?\]>$/s', $linematchings[1], $tagmatchings)) {
8216 $tagdetails[] = (object) array(
8217 'open' => true,
8218 'tag' => core_text::strtolower('if'),
8219 'pos' => core_text::strlen($truncate),
8221 } else if (preg_match('/^<!--<!\[endif\]-->$/s', $linematchings[1], $tagmatchings)) {
8222 $tagdetails[] = (object) array(
8223 'open' => false,
8224 'tag' => core_text::strtolower('if'),
8225 'pos' => core_text::strlen($truncate),
8229 // Add html-tag to $truncate'd text.
8230 $truncate .= $linematchings[1];
8233 // Calculate the length of the plain text part of the line; handle entities as one character.
8234 $contentlength = core_text::strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', ' ', $linematchings[2]));
8235 if ($totallength + $contentlength > $ideal) {
8236 // The number of characters which are left.
8237 $left = $ideal - $totallength;
8238 $entitieslength = 0;
8239 // Search for html entities.
8240 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)) {
8241 // Calculate the real length of all entities in the legal range.
8242 foreach ($entities[0] as $entity) {
8243 if ($entity[1]+1-$entitieslength <= $left) {
8244 $left--;
8245 $entitieslength += core_text::strlen($entity[0]);
8246 } else {
8247 // No more characters left.
8248 break;
8252 $breakpos = $left + $entitieslength;
8254 // If the words shouldn't be cut in the middle...
8255 if (!$exact) {
8256 // Search the last occurence of a space.
8257 for (; $breakpos > 0; $breakpos--) {
8258 if ($char = core_text::substr($linematchings[2], $breakpos, 1)) {
8259 if ($char === '.' or $char === ' ') {
8260 $breakpos += 1;
8261 break;
8262 } else if (strlen($char) > 2) {
8263 // Chinese/Japanese/Korean text can be truncated at any UTF-8 character boundary.
8264 $breakpos += 1;
8265 break;
8270 if ($breakpos == 0) {
8271 // This deals with the test_shorten_text_no_spaces case.
8272 $breakpos = $left + $entitieslength;
8273 } else if ($breakpos > $left + $entitieslength) {
8274 // This deals with the previous for loop breaking on the first char.
8275 $breakpos = $left + $entitieslength;
8278 $truncate .= core_text::substr($linematchings[2], 0, $breakpos);
8279 // Maximum length is reached, so get off the loop.
8280 break;
8281 } else {
8282 $truncate .= $linematchings[2];
8283 $totallength += $contentlength;
8286 // If the maximum length is reached, get off the loop.
8287 if ($totallength >= $ideal) {
8288 break;
8292 // Add the defined ending to the text.
8293 $truncate .= $ending;
8295 // Now calculate the list of open html tags based on the truncate position.
8296 $opentags = array();
8297 foreach ($tagdetails as $taginfo) {
8298 if ($taginfo->open) {
8299 // Add tag to the beginning of $opentags list.
8300 array_unshift($opentags, $taginfo->tag);
8301 } else {
8302 // Can have multiple exact same open tags, close the last one.
8303 $pos = array_search($taginfo->tag, array_reverse($opentags, true));
8304 if ($pos !== false) {
8305 unset($opentags[$pos]);
8310 // Close all unclosed html-tags.
8311 foreach ($opentags as $tag) {
8312 if ($tag === 'if') {
8313 $truncate .= '<!--<![endif]-->';
8314 } else {
8315 $truncate .= '</' . $tag . '>';
8319 return $truncate;
8323 * Shortens a given filename by removing characters positioned after the ideal string length.
8324 * When the filename is too long, the file cannot be created on the filesystem due to exceeding max byte size.
8325 * Limiting the filename to a certain size (considering multibyte characters) will prevent this.
8327 * @param string $filename file name
8328 * @param int $length ideal string length
8329 * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness.
8330 * @return string $shortened shortened file name
8332 function shorten_filename($filename, $length = MAX_FILENAME_SIZE, $includehash = false) {
8333 $shortened = $filename;
8334 // Extract a part of the filename if it's char size exceeds the ideal string length.
8335 if (core_text::strlen($filename) > $length) {
8336 // Exclude extension if present in filename.
8337 $mimetypes = get_mimetypes_array();
8338 $extension = pathinfo($filename, PATHINFO_EXTENSION);
8339 if ($extension && !empty($mimetypes[$extension])) {
8340 $basename = pathinfo($filename, PATHINFO_FILENAME);
8341 $hash = empty($includehash) ? '' : ' - ' . substr(sha1($basename), 0, 10);
8342 $shortened = core_text::substr($basename, 0, $length - strlen($hash)) . $hash;
8343 $shortened .= '.' . $extension;
8344 } else {
8345 $hash = empty($includehash) ? '' : ' - ' . substr(sha1($filename), 0, 10);
8346 $shortened = core_text::substr($filename, 0, $length - strlen($hash)) . $hash;
8349 return $shortened;
8353 * Shortens a given array of filenames by removing characters positioned after the ideal string length.
8355 * @param array $path The paths to reduce the length.
8356 * @param int $length Ideal string length
8357 * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness.
8358 * @return array $result Shortened paths in array.
8360 function shorten_filenames(array $path, $length = MAX_FILENAME_SIZE, $includehash = false) {
8361 $result = null;
8363 $result = array_reduce($path, function($carry, $singlepath) use ($length, $includehash) {
8364 $carry[] = shorten_filename($singlepath, $length, $includehash);
8365 return $carry;
8366 }, []);
8368 return $result;
8372 * Given dates in seconds, how many weeks is the date from startdate
8373 * The first week is 1, the second 2 etc ...
8375 * @param int $startdate Timestamp for the start date
8376 * @param int $thedate Timestamp for the end date
8377 * @return string
8379 function getweek ($startdate, $thedate) {
8380 if ($thedate < $startdate) {
8381 return 0;
8384 return floor(($thedate - $startdate) / WEEKSECS) + 1;
8388 * Returns a randomly generated password of length $maxlen. inspired by
8390 * {@link http://www.phpbuilder.com/columns/jesus19990502.php3} and
8391 * {@link http://es2.php.net/manual/en/function.str-shuffle.php#73254}
8393 * @param int $maxlen The maximum size of the password being generated.
8394 * @return string
8396 function generate_password($maxlen=10) {
8397 global $CFG;
8399 if (empty($CFG->passwordpolicy)) {
8400 $fillers = PASSWORD_DIGITS;
8401 $wordlist = file($CFG->wordlist);
8402 $word1 = trim($wordlist[rand(0, count($wordlist) - 1)]);
8403 $word2 = trim($wordlist[rand(0, count($wordlist) - 1)]);
8404 $filler1 = $fillers[rand(0, strlen($fillers) - 1)];
8405 $password = $word1 . $filler1 . $word2;
8406 } else {
8407 $minlen = !empty($CFG->minpasswordlength) ? $CFG->minpasswordlength : 0;
8408 $digits = $CFG->minpassworddigits;
8409 $lower = $CFG->minpasswordlower;
8410 $upper = $CFG->minpasswordupper;
8411 $nonalphanum = $CFG->minpasswordnonalphanum;
8412 $total = $lower + $upper + $digits + $nonalphanum;
8413 // Var minlength should be the greater one of the two ( $minlen and $total ).
8414 $minlen = $minlen < $total ? $total : $minlen;
8415 // Var maxlen can never be smaller than minlen.
8416 $maxlen = $minlen > $maxlen ? $minlen : $maxlen;
8417 $additional = $maxlen - $total;
8419 // Make sure we have enough characters to fulfill
8420 // complexity requirements.
8421 $passworddigits = PASSWORD_DIGITS;
8422 while ($digits > strlen($passworddigits)) {
8423 $passworddigits .= PASSWORD_DIGITS;
8425 $passwordlower = PASSWORD_LOWER;
8426 while ($lower > strlen($passwordlower)) {
8427 $passwordlower .= PASSWORD_LOWER;
8429 $passwordupper = PASSWORD_UPPER;
8430 while ($upper > strlen($passwordupper)) {
8431 $passwordupper .= PASSWORD_UPPER;
8433 $passwordnonalphanum = PASSWORD_NONALPHANUM;
8434 while ($nonalphanum > strlen($passwordnonalphanum)) {
8435 $passwordnonalphanum .= PASSWORD_NONALPHANUM;
8438 // Now mix and shuffle it all.
8439 $password = str_shuffle (substr(str_shuffle ($passwordlower), 0, $lower) .
8440 substr(str_shuffle ($passwordupper), 0, $upper) .
8441 substr(str_shuffle ($passworddigits), 0, $digits) .
8442 substr(str_shuffle ($passwordnonalphanum), 0 , $nonalphanum) .
8443 substr(str_shuffle ($passwordlower .
8444 $passwordupper .
8445 $passworddigits .
8446 $passwordnonalphanum), 0 , $additional));
8449 return substr ($password, 0, $maxlen);
8453 * Given a float, prints it nicely.
8454 * Localized floats must not be used in calculations!
8456 * The stripzeros feature is intended for making numbers look nicer in small
8457 * areas where it is not necessary to indicate the degree of accuracy by showing
8458 * ending zeros. If you turn it on with $decimalpoints set to 3, for example,
8459 * then it will display '5.4' instead of '5.400' or '5' instead of '5.000'.
8461 * @param float $float The float to print
8462 * @param int $decimalpoints The number of decimal places to print. -1 is a special value for auto detect (full precision).
8463 * @param bool $localized use localized decimal separator
8464 * @param bool $stripzeros If true, removes final zeros after decimal point. It will be ignored and the trailing zeros after
8465 * the decimal point are always striped if $decimalpoints is -1.
8466 * @return string locale float
8468 function format_float($float, $decimalpoints=1, $localized=true, $stripzeros=false) {
8469 if (is_null($float)) {
8470 return '';
8472 if ($localized) {
8473 $separator = get_string('decsep', 'langconfig');
8474 } else {
8475 $separator = '.';
8477 if ($decimalpoints == -1) {
8478 // The following counts the number of decimals.
8479 // It is safe as both floatval() and round() functions have same behaviour when non-numeric values are provided.
8480 $floatval = floatval($float);
8481 for ($decimalpoints = 0; $floatval != round($float, $decimalpoints); $decimalpoints++);
8484 $result = number_format($float, $decimalpoints, $separator, '');
8485 if ($stripzeros && $decimalpoints > 0) {
8486 // Remove zeros and final dot if not needed.
8487 // However, only do this if there is a decimal point!
8488 $result = preg_replace('~(' . preg_quote($separator, '~') . ')?0+$~', '', $result);
8490 return $result;
8494 * Converts locale specific floating point/comma number back to standard PHP float value
8495 * Do NOT try to do any math operations before this conversion on any user submitted floats!
8497 * @param string $localefloat locale aware float representation
8498 * @param bool $strict If true, then check the input and return false if it is not a valid number.
8499 * @return mixed float|bool - false or the parsed float.
8501 function unformat_float($localefloat, $strict = false) {
8502 $localefloat = trim((string)$localefloat);
8504 if ($localefloat == '') {
8505 return null;
8508 $localefloat = str_replace(' ', '', $localefloat); // No spaces - those might be used as thousand separators.
8509 $localefloat = str_replace(get_string('decsep', 'langconfig'), '.', $localefloat);
8511 if ($strict && !is_numeric($localefloat)) {
8512 return false;
8515 return (float)$localefloat;
8519 * Given a simple array, this shuffles it up just like shuffle()
8520 * Unlike PHP's shuffle() this function works on any machine.
8522 * @param array $array The array to be rearranged
8523 * @return array
8525 function swapshuffle($array) {
8527 $last = count($array) - 1;
8528 for ($i = 0; $i <= $last; $i++) {
8529 $from = rand(0, $last);
8530 $curr = $array[$i];
8531 $array[$i] = $array[$from];
8532 $array[$from] = $curr;
8534 return $array;
8538 * Like {@link swapshuffle()}, but works on associative arrays
8540 * @param array $array The associative array to be rearranged
8541 * @return array
8543 function swapshuffle_assoc($array) {
8545 $newarray = array();
8546 $newkeys = swapshuffle(array_keys($array));
8548 foreach ($newkeys as $newkey) {
8549 $newarray[$newkey] = $array[$newkey];
8551 return $newarray;
8555 * Given an arbitrary array, and a number of draws,
8556 * this function returns an array with that amount
8557 * of items. The indexes are retained.
8559 * @todo Finish documenting this function
8561 * @param array $array
8562 * @param int $draws
8563 * @return array
8565 function draw_rand_array($array, $draws) {
8567 $return = array();
8569 $last = count($array);
8571 if ($draws > $last) {
8572 $draws = $last;
8575 while ($draws > 0) {
8576 $last--;
8578 $keys = array_keys($array);
8579 $rand = rand(0, $last);
8581 $return[$keys[$rand]] = $array[$keys[$rand]];
8582 unset($array[$keys[$rand]]);
8584 $draws--;
8587 return $return;
8591 * Calculate the difference between two microtimes
8593 * @param string $a The first Microtime
8594 * @param string $b The second Microtime
8595 * @return string
8597 function microtime_diff($a, $b) {
8598 list($adec, $asec) = explode(' ', $a);
8599 list($bdec, $bsec) = explode(' ', $b);
8600 return $bsec - $asec + $bdec - $adec;
8604 * Given a list (eg a,b,c,d,e) this function returns
8605 * an array of 1->a, 2->b, 3->c etc
8607 * @param string $list The string to explode into array bits
8608 * @param string $separator The separator used within the list string
8609 * @return array The now assembled array
8611 function make_menu_from_list($list, $separator=',') {
8613 $array = array_reverse(explode($separator, $list), true);
8614 foreach ($array as $key => $item) {
8615 $outarray[$key+1] = trim($item);
8617 return $outarray;
8621 * Creates an array that represents all the current grades that
8622 * can be chosen using the given grading type.
8624 * Negative numbers
8625 * are scales, zero is no grade, and positive numbers are maximum
8626 * grades.
8628 * @todo Finish documenting this function or better deprecated this completely!
8630 * @param int $gradingtype
8631 * @return array
8633 function make_grades_menu($gradingtype) {
8634 global $DB;
8636 $grades = array();
8637 if ($gradingtype < 0) {
8638 if ($scale = $DB->get_record('scale', array('id'=> (-$gradingtype)))) {
8639 return make_menu_from_list($scale->scale);
8641 } else if ($gradingtype > 0) {
8642 for ($i=$gradingtype; $i>=0; $i--) {
8643 $grades[$i] = $i .' / '. $gradingtype;
8645 return $grades;
8647 return $grades;
8651 * make_unique_id_code
8653 * @todo Finish documenting this function
8655 * @uses $_SERVER
8656 * @param string $extra Extra string to append to the end of the code
8657 * @return string
8659 function make_unique_id_code($extra = '') {
8661 $hostname = 'unknownhost';
8662 if (!empty($_SERVER['HTTP_HOST'])) {
8663 $hostname = $_SERVER['HTTP_HOST'];
8664 } else if (!empty($_ENV['HTTP_HOST'])) {
8665 $hostname = $_ENV['HTTP_HOST'];
8666 } else if (!empty($_SERVER['SERVER_NAME'])) {
8667 $hostname = $_SERVER['SERVER_NAME'];
8668 } else if (!empty($_ENV['SERVER_NAME'])) {
8669 $hostname = $_ENV['SERVER_NAME'];
8672 $date = gmdate("ymdHis");
8674 $random = random_string(6);
8676 if ($extra) {
8677 return $hostname .'+'. $date .'+'. $random .'+'. $extra;
8678 } else {
8679 return $hostname .'+'. $date .'+'. $random;
8685 * Function to check the passed address is within the passed subnet
8687 * The parameter is a comma separated string of subnet definitions.
8688 * Subnet strings can be in one of three formats:
8689 * 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn (number of bits in net mask)
8690 * 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)
8691 * 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx. (incomplete address, a bit non-technical ;-)
8692 * Code for type 1 modified from user posted comments by mediator at
8693 * {@link http://au.php.net/manual/en/function.ip2long.php}
8695 * @param string $addr The address you are checking
8696 * @param string $subnetstr The string of subnet addresses
8697 * @param bool $checkallzeros The state to whether check for 0.0.0.0
8698 * @return bool
8700 function address_in_subnet($addr, $subnetstr, $checkallzeros = false) {
8702 if ($addr == '0.0.0.0' && !$checkallzeros) {
8703 return false;
8705 $subnets = explode(',', $subnetstr);
8706 $found = false;
8707 $addr = trim($addr);
8708 $addr = cleanremoteaddr($addr, false); // Normalise.
8709 if ($addr === null) {
8710 return false;
8712 $addrparts = explode(':', $addr);
8714 $ipv6 = strpos($addr, ':');
8716 foreach ($subnets as $subnet) {
8717 $subnet = trim($subnet);
8718 if ($subnet === '') {
8719 continue;
8722 if (strpos($subnet, '/') !== false) {
8723 // 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn.
8724 list($ip, $mask) = explode('/', $subnet);
8725 $mask = trim($mask);
8726 if (!is_number($mask)) {
8727 continue; // Incorect mask number, eh?
8729 $ip = cleanremoteaddr($ip, false); // Normalise.
8730 if ($ip === null) {
8731 continue;
8733 if (strpos($ip, ':') !== false) {
8734 // IPv6.
8735 if (!$ipv6) {
8736 continue;
8738 if ($mask > 128 or $mask < 0) {
8739 continue; // Nonsense.
8741 if ($mask == 0) {
8742 return true; // Any address.
8744 if ($mask == 128) {
8745 if ($ip === $addr) {
8746 return true;
8748 continue;
8750 $ipparts = explode(':', $ip);
8751 $modulo = $mask % 16;
8752 $ipnet = array_slice($ipparts, 0, ($mask-$modulo)/16);
8753 $addrnet = array_slice($addrparts, 0, ($mask-$modulo)/16);
8754 if (implode(':', $ipnet) === implode(':', $addrnet)) {
8755 if ($modulo == 0) {
8756 return true;
8758 $pos = ($mask-$modulo)/16;
8759 $ipnet = hexdec($ipparts[$pos]);
8760 $addrnet = hexdec($addrparts[$pos]);
8761 $mask = 0xffff << (16 - $modulo);
8762 if (($addrnet & $mask) == ($ipnet & $mask)) {
8763 return true;
8767 } else {
8768 // IPv4.
8769 if ($ipv6) {
8770 continue;
8772 if ($mask > 32 or $mask < 0) {
8773 continue; // Nonsense.
8775 if ($mask == 0) {
8776 return true;
8778 if ($mask == 32) {
8779 if ($ip === $addr) {
8780 return true;
8782 continue;
8784 $mask = 0xffffffff << (32 - $mask);
8785 if (((ip2long($addr) & $mask) == (ip2long($ip) & $mask))) {
8786 return true;
8790 } else if (strpos($subnet, '-') !== false) {
8791 // 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.
8792 $parts = explode('-', $subnet);
8793 if (count($parts) != 2) {
8794 continue;
8797 if (strpos($subnet, ':') !== false) {
8798 // IPv6.
8799 if (!$ipv6) {
8800 continue;
8802 $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise.
8803 if ($ipstart === null) {
8804 continue;
8806 $ipparts = explode(':', $ipstart);
8807 $start = hexdec(array_pop($ipparts));
8808 $ipparts[] = trim($parts[1]);
8809 $ipend = cleanremoteaddr(implode(':', $ipparts), false); // Normalise.
8810 if ($ipend === null) {
8811 continue;
8813 $ipparts[7] = '';
8814 $ipnet = implode(':', $ipparts);
8815 if (strpos($addr, $ipnet) !== 0) {
8816 continue;
8818 $ipparts = explode(':', $ipend);
8819 $end = hexdec($ipparts[7]);
8821 $addrend = hexdec($addrparts[7]);
8823 if (($addrend >= $start) and ($addrend <= $end)) {
8824 return true;
8827 } else {
8828 // IPv4.
8829 if ($ipv6) {
8830 continue;
8832 $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise.
8833 if ($ipstart === null) {
8834 continue;
8836 $ipparts = explode('.', $ipstart);
8837 $ipparts[3] = trim($parts[1]);
8838 $ipend = cleanremoteaddr(implode('.', $ipparts), false); // Normalise.
8839 if ($ipend === null) {
8840 continue;
8843 if ((ip2long($addr) >= ip2long($ipstart)) and (ip2long($addr) <= ip2long($ipend))) {
8844 return true;
8848 } else {
8849 // 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx.
8850 if (strpos($subnet, ':') !== false) {
8851 // IPv6.
8852 if (!$ipv6) {
8853 continue;
8855 $parts = explode(':', $subnet);
8856 $count = count($parts);
8857 if ($parts[$count-1] === '') {
8858 unset($parts[$count-1]); // Trim trailing :'s.
8859 $count--;
8860 $subnet = implode('.', $parts);
8862 $isip = cleanremoteaddr($subnet, false); // Normalise.
8863 if ($isip !== null) {
8864 if ($isip === $addr) {
8865 return true;
8867 continue;
8868 } else if ($count > 8) {
8869 continue;
8871 $zeros = array_fill(0, 8-$count, '0');
8872 $subnet = $subnet.':'.implode(':', $zeros).'/'.($count*16);
8873 if (address_in_subnet($addr, $subnet)) {
8874 return true;
8877 } else {
8878 // IPv4.
8879 if ($ipv6) {
8880 continue;
8882 $parts = explode('.', $subnet);
8883 $count = count($parts);
8884 if ($parts[$count-1] === '') {
8885 unset($parts[$count-1]); // Trim trailing .
8886 $count--;
8887 $subnet = implode('.', $parts);
8889 if ($count == 4) {
8890 $subnet = cleanremoteaddr($subnet, false); // Normalise.
8891 if ($subnet === $addr) {
8892 return true;
8894 continue;
8895 } else if ($count > 4) {
8896 continue;
8898 $zeros = array_fill(0, 4-$count, '0');
8899 $subnet = $subnet.'.'.implode('.', $zeros).'/'.($count*8);
8900 if (address_in_subnet($addr, $subnet)) {
8901 return true;
8907 return false;
8911 * For outputting debugging info
8913 * @param string $string The string to write
8914 * @param string $eol The end of line char(s) to use
8915 * @param string $sleep Period to make the application sleep
8916 * This ensures any messages have time to display before redirect
8918 function mtrace($string, $eol="\n", $sleep=0) {
8919 global $CFG;
8921 if (isset($CFG->mtrace_wrapper) && function_exists($CFG->mtrace_wrapper)) {
8922 $fn = $CFG->mtrace_wrapper;
8923 $fn($string, $eol);
8924 return;
8925 } else if (defined('STDOUT') && !PHPUNIT_TEST && !defined('BEHAT_TEST')) {
8926 // We must explicitly call the add_line function here.
8927 // Uses of fwrite to STDOUT are not picked up by ob_start.
8928 if ($output = \core\task\logmanager::add_line("{$string}{$eol}")) {
8929 fwrite(STDOUT, $output);
8931 } else {
8932 echo $string . $eol;
8935 // Flush again.
8936 flush();
8938 // Delay to keep message on user's screen in case of subsequent redirect.
8939 if ($sleep) {
8940 sleep($sleep);
8945 * Helper to {@see mtrace()} an exception or throwable, including all relevant information.
8947 * @param Throwable $e the error to ouptput.
8949 function mtrace_exception(Throwable $e): void {
8950 $info = get_exception_info($e);
8952 $message = $info->message;
8953 if ($info->debuginfo) {
8954 $message .= "\n\n" . $info->debuginfo;
8956 if ($info->backtrace) {
8957 $message .= "\n\n" . format_backtrace($info->backtrace, true);
8960 mtrace($message);
8964 * Replace 1 or more slashes or backslashes to 1 slash
8966 * @param string $path The path to strip
8967 * @return string the path with double slashes removed
8969 function cleardoubleslashes ($path) {
8970 return preg_replace('/(\/|\\\){1,}/', '/', $path);
8974 * Is the current ip in a given list?
8976 * @param string $list
8977 * @return bool
8979 function remoteip_in_list($list) {
8980 $clientip = getremoteaddr(null);
8982 if (!$clientip) {
8983 // Ensure access on cli.
8984 return true;
8986 return \core\ip_utils::is_ip_in_subnet_list($clientip, $list);
8990 * Returns most reliable client address
8992 * @param string $default If an address can't be determined, then return this
8993 * @return string The remote IP address
8995 function getremoteaddr($default='0.0.0.0') {
8996 global $CFG;
8998 if (!isset($CFG->getremoteaddrconf)) {
8999 // This will happen, for example, before just after the upgrade, as the
9000 // user is redirected to the admin screen.
9001 $variablestoskip = GETREMOTEADDR_SKIP_DEFAULT;
9002 } else {
9003 $variablestoskip = $CFG->getremoteaddrconf;
9005 if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_CLIENT_IP)) {
9006 if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
9007 $address = cleanremoteaddr($_SERVER['HTTP_CLIENT_IP']);
9008 return $address ? $address : $default;
9011 if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR)) {
9012 if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
9013 $forwardedaddresses = explode(",", $_SERVER['HTTP_X_FORWARDED_FOR']);
9015 $forwardedaddresses = array_filter($forwardedaddresses, function($ip) {
9016 global $CFG;
9017 return !\core\ip_utils::is_ip_in_subnet_list($ip, $CFG->reverseproxyignore ?? '', ',');
9020 // Multiple proxies can append values to this header including an
9021 // untrusted original request header so we must only trust the last ip.
9022 $address = end($forwardedaddresses);
9024 if (substr_count($address, ":") > 1) {
9025 // Remove port and brackets from IPv6.
9026 if (preg_match("/\[(.*)\]:/", $address, $matches)) {
9027 $address = $matches[1];
9029 } else {
9030 // Remove port from IPv4.
9031 if (substr_count($address, ":") == 1) {
9032 $parts = explode(":", $address);
9033 $address = $parts[0];
9037 $address = cleanremoteaddr($address);
9038 return $address ? $address : $default;
9041 if (!empty($_SERVER['REMOTE_ADDR'])) {
9042 $address = cleanremoteaddr($_SERVER['REMOTE_ADDR']);
9043 return $address ? $address : $default;
9044 } else {
9045 return $default;
9050 * Cleans an ip address. Internal addresses are now allowed.
9051 * (Originally local addresses were not allowed.)
9053 * @param string $addr IPv4 or IPv6 address
9054 * @param bool $compress use IPv6 address compression
9055 * @return string normalised ip address string, null if error
9057 function cleanremoteaddr($addr, $compress=false) {
9058 $addr = trim($addr);
9060 if (strpos($addr, ':') !== false) {
9061 // Can be only IPv6.
9062 $parts = explode(':', $addr);
9063 $count = count($parts);
9065 if (strpos($parts[$count-1], '.') !== false) {
9066 // Legacy ipv4 notation.
9067 $last = array_pop($parts);
9068 $ipv4 = cleanremoteaddr($last, true);
9069 if ($ipv4 === null) {
9070 return null;
9072 $bits = explode('.', $ipv4);
9073 $parts[] = dechex($bits[0]).dechex($bits[1]);
9074 $parts[] = dechex($bits[2]).dechex($bits[3]);
9075 $count = count($parts);
9076 $addr = implode(':', $parts);
9079 if ($count < 3 or $count > 8) {
9080 return null; // Severly malformed.
9083 if ($count != 8) {
9084 if (strpos($addr, '::') === false) {
9085 return null; // Malformed.
9087 // Uncompress.
9088 $insertat = array_search('', $parts, true);
9089 $missing = array_fill(0, 1 + 8 - $count, '0');
9090 array_splice($parts, $insertat, 1, $missing);
9091 foreach ($parts as $key => $part) {
9092 if ($part === '') {
9093 $parts[$key] = '0';
9098 $adr = implode(':', $parts);
9099 if (!preg_match('/^([0-9a-f]{1,4})(:[0-9a-f]{1,4})*$/i', $adr)) {
9100 return null; // Incorrect format - sorry.
9103 // Normalise 0s and case.
9104 $parts = array_map('hexdec', $parts);
9105 $parts = array_map('dechex', $parts);
9107 $result = implode(':', $parts);
9109 if (!$compress) {
9110 return $result;
9113 if ($result === '0:0:0:0:0:0:0:0') {
9114 return '::'; // All addresses.
9117 $compressed = preg_replace('/(:0)+:0$/', '::', $result, 1);
9118 if ($compressed !== $result) {
9119 return $compressed;
9122 $compressed = preg_replace('/^(0:){2,7}/', '::', $result, 1);
9123 if ($compressed !== $result) {
9124 return $compressed;
9127 $compressed = preg_replace('/(:0){2,6}:/', '::', $result, 1);
9128 if ($compressed !== $result) {
9129 return $compressed;
9132 return $result;
9135 // First get all things that look like IPv4 addresses.
9136 $parts = array();
9137 if (!preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $addr, $parts)) {
9138 return null;
9140 unset($parts[0]);
9142 foreach ($parts as $key => $match) {
9143 if ($match > 255) {
9144 return null;
9146 $parts[$key] = (int)$match; // Normalise 0s.
9149 return implode('.', $parts);
9154 * Is IP address a public address?
9156 * @param string $ip The ip to check
9157 * @return bool true if the ip is public
9159 function ip_is_public($ip) {
9160 return (bool) filter_var($ip, FILTER_VALIDATE_IP, (FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE));
9164 * This function will make a complete copy of anything it's given,
9165 * regardless of whether it's an object or not.
9167 * @param mixed $thing Something you want cloned
9168 * @return mixed What ever it is you passed it
9170 function fullclone($thing) {
9171 return unserialize(serialize($thing));
9175 * Used to make sure that $min <= $value <= $max
9177 * Make sure that value is between min, and max
9179 * @param int $min The minimum value
9180 * @param int $value The value to check
9181 * @param int $max The maximum value
9182 * @return int
9184 function bounded_number($min, $value, $max) {
9185 if ($value < $min) {
9186 return $min;
9188 if ($value > $max) {
9189 return $max;
9191 return $value;
9195 * Check if there is a nested array within the passed array
9197 * @param array $array
9198 * @return bool true if there is a nested array false otherwise
9200 function array_is_nested($array) {
9201 foreach ($array as $value) {
9202 if (is_array($value)) {
9203 return true;
9206 return false;
9210 * get_performance_info() pairs up with init_performance_info()
9211 * loaded in setup.php. Returns an array with 'html' and 'txt'
9212 * values ready for use, and each of the individual stats provided
9213 * separately as well.
9215 * @return array
9217 function get_performance_info() {
9218 global $CFG, $PERF, $DB, $PAGE;
9220 $info = array();
9221 $info['txt'] = me() . ' '; // Holds log-friendly representation.
9223 $info['html'] = '';
9224 if (!empty($CFG->themedesignermode)) {
9225 // Attempt to avoid devs debugging peformance issues, when its caused by css building and so on.
9226 $info['html'] .= '<p><strong>Warning: Theme designer mode is enabled.</strong></p>';
9228 $info['html'] .= '<ul class="list-unstyled row mx-md-0">'; // Holds userfriendly HTML representation.
9230 $info['realtime'] = microtime_diff($PERF->starttime, microtime());
9232 $info['html'] .= '<li class="timeused col-sm-4">'.$info['realtime'].' secs</li> ';
9233 $info['txt'] .= 'time: '.$info['realtime'].'s ';
9235 // GET/POST (or NULL if $_SERVER['REQUEST_METHOD'] is undefined) is useful for txt logged information.
9236 $info['txt'] .= 'method: ' . ($_SERVER['REQUEST_METHOD'] ?? "NULL") . ' ';
9238 if (function_exists('memory_get_usage')) {
9239 $info['memory_total'] = memory_get_usage();
9240 $info['memory_growth'] = memory_get_usage() - $PERF->startmemory;
9241 $info['html'] .= '<li class="memoryused col-sm-4">RAM: '.display_size($info['memory_total']).'</li> ';
9242 $info['txt'] .= 'memory_total: '.$info['memory_total'].'B (' . display_size($info['memory_total']).') memory_growth: '.
9243 $info['memory_growth'].'B ('.display_size($info['memory_growth']).') ';
9246 if (function_exists('memory_get_peak_usage')) {
9247 $info['memory_peak'] = memory_get_peak_usage();
9248 $info['html'] .= '<li class="memoryused col-sm-4">RAM peak: '.display_size($info['memory_peak']).'</li> ';
9249 $info['txt'] .= 'memory_peak: '.$info['memory_peak'].'B (' . display_size($info['memory_peak']).') ';
9252 $info['html'] .= '</ul><ul class="list-unstyled row mx-md-0">';
9253 $inc = get_included_files();
9254 $info['includecount'] = count($inc);
9255 $info['html'] .= '<li class="included col-sm-4">Included '.$info['includecount'].' files</li> ';
9256 $info['txt'] .= 'includecount: '.$info['includecount'].' ';
9258 if (!empty($CFG->early_install_lang) or empty($PAGE)) {
9259 // We can not track more performance before installation or before PAGE init, sorry.
9260 return $info;
9263 $filtermanager = filter_manager::instance();
9264 if (method_exists($filtermanager, 'get_performance_summary')) {
9265 list($filterinfo, $nicenames) = $filtermanager->get_performance_summary();
9266 $info = array_merge($filterinfo, $info);
9267 foreach ($filterinfo as $key => $value) {
9268 $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> ";
9269 $info['txt'] .= "$key: $value ";
9273 $stringmanager = get_string_manager();
9274 if (method_exists($stringmanager, 'get_performance_summary')) {
9275 list($filterinfo, $nicenames) = $stringmanager->get_performance_summary();
9276 $info = array_merge($filterinfo, $info);
9277 foreach ($filterinfo as $key => $value) {
9278 $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> ";
9279 $info['txt'] .= "$key: $value ";
9283 $info['dbqueries'] = $DB->perf_get_reads().'/'.$DB->perf_get_writes();
9284 $info['html'] .= '<li class="dbqueries col-sm-4">DB reads/writes: '.$info['dbqueries'].'</li> ';
9285 $info['txt'] .= 'db reads/writes: '.$info['dbqueries'].' ';
9287 if ($DB->want_read_slave()) {
9288 $info['dbreads_slave'] = $DB->perf_get_reads_slave();
9289 $info['html'] .= '<li class="dbqueries col-sm-4">DB reads from slave: '.$info['dbreads_slave'].'</li> ';
9290 $info['txt'] .= 'db reads from slave: '.$info['dbreads_slave'].' ';
9293 $info['dbtime'] = round($DB->perf_get_queries_time(), 5);
9294 $info['html'] .= '<li class="dbtime col-sm-4">DB queries time: '.$info['dbtime'].' secs</li> ';
9295 $info['txt'] .= 'db queries time: ' . $info['dbtime'] . 's ';
9297 if (function_exists('posix_times')) {
9298 $ptimes = posix_times();
9299 if (is_array($ptimes)) {
9300 foreach ($ptimes as $key => $val) {
9301 $info[$key] = $ptimes[$key] - $PERF->startposixtimes[$key];
9303 $info['html'] .= "<li class=\"posixtimes col-sm-4\">ticks: $info[ticks] user: $info[utime]";
9304 $info['html'] .= "sys: $info[stime] cuser: $info[cutime] csys: $info[cstime]</li> ";
9305 $info['txt'] .= "ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime] ";
9309 // Grab the load average for the last minute.
9310 // /proc will only work under some linux configurations
9311 // while uptime is there under MacOSX/Darwin and other unices.
9312 if (is_readable('/proc/loadavg') && $loadavg = @file('/proc/loadavg')) {
9313 list($serverload) = explode(' ', $loadavg[0]);
9314 unset($loadavg);
9315 } else if ( function_exists('is_executable') && is_executable('/usr/bin/uptime') && $loadavg = `/usr/bin/uptime` ) {
9316 if (preg_match('/load averages?: (\d+[\.,:]\d+)/', $loadavg, $matches)) {
9317 $serverload = $matches[1];
9318 } else {
9319 trigger_error('Could not parse uptime output!');
9322 if (!empty($serverload)) {
9323 $info['serverload'] = $serverload;
9324 $info['html'] .= '<li class="serverload col-sm-4">Load average: '.$info['serverload'].'</li> ';
9325 $info['txt'] .= "serverload: {$info['serverload']} ";
9328 // Display size of session if session started.
9329 if ($si = \core\session\manager::get_performance_info()) {
9330 $info['sessionsize'] = $si['size'];
9331 $info['html'] .= "<li class=\"serverload col-sm-4\">" . $si['html'] . "</li>";
9332 $info['txt'] .= $si['txt'];
9335 // Display time waiting for session if applicable.
9336 if (!empty($PERF->sessionlock['wait'])) {
9337 $sessionwait = number_format($PERF->sessionlock['wait'], 3) . ' secs';
9338 $info['html'] .= html_writer::tag('li', 'Session wait: ' . $sessionwait, [
9339 'class' => 'sessionwait col-sm-4'
9341 $info['txt'] .= 'sessionwait: ' . $sessionwait . ' ';
9344 $info['html'] .= '</ul>';
9345 $html = '';
9346 if ($stats = cache_helper::get_stats()) {
9348 $table = new html_table();
9349 $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered';
9350 $table->head = ['Mode', 'Cache item', 'Static', 'H', 'M', get_string('mappingprimary', 'cache'), 'H', 'M', 'S', 'I/O'];
9351 $table->data = [];
9352 $table->align = ['left', 'left', 'left', 'right', 'right', 'left', 'right', 'right', 'right', 'right'];
9354 $text = 'Caches used (hits/misses/sets): ';
9355 $hits = 0;
9356 $misses = 0;
9357 $sets = 0;
9358 $maxstores = 0;
9360 // We want to align static caches into their own column.
9361 $hasstatic = false;
9362 foreach ($stats as $definition => $details) {
9363 $numstores = count($details['stores']);
9364 $first = key($details['stores']);
9365 if ($first !== cache_store::STATIC_ACCEL) {
9366 $numstores++; // Add a blank space for the missing static store.
9368 $maxstores = max($maxstores, $numstores);
9371 $storec = 0;
9373 while ($storec++ < ($maxstores - 2)) {
9374 if ($storec == ($maxstores - 2)) {
9375 $table->head[] = get_string('mappingfinal', 'cache');
9376 } else {
9377 $table->head[] = "Store $storec";
9379 $table->align[] = 'left';
9380 $table->align[] = 'right';
9381 $table->align[] = 'right';
9382 $table->align[] = 'right';
9383 $table->align[] = 'right';
9384 $table->head[] = 'H';
9385 $table->head[] = 'M';
9386 $table->head[] = 'S';
9387 $table->head[] = 'I/O';
9390 ksort($stats);
9392 foreach ($stats as $definition => $details) {
9393 switch ($details['mode']) {
9394 case cache_store::MODE_APPLICATION:
9395 $modeclass = 'application';
9396 $mode = ' <span title="application cache">App</span>';
9397 break;
9398 case cache_store::MODE_SESSION:
9399 $modeclass = 'session';
9400 $mode = ' <span title="session cache">Ses</span>';
9401 break;
9402 case cache_store::MODE_REQUEST:
9403 $modeclass = 'request';
9404 $mode = ' <span title="request cache">Req</span>';
9405 break;
9407 $row = [$mode, $definition];
9409 $text .= "$definition {";
9411 $storec = 0;
9412 foreach ($details['stores'] as $store => $data) {
9414 if ($storec == 0 && $store !== cache_store::STATIC_ACCEL) {
9415 $row[] = '';
9416 $row[] = '';
9417 $row[] = '';
9418 $storec++;
9421 $hits += $data['hits'];
9422 $misses += $data['misses'];
9423 $sets += $data['sets'];
9424 if ($data['hits'] == 0 and $data['misses'] > 0) {
9425 $cachestoreclass = 'nohits bg-danger';
9426 } else if ($data['hits'] < $data['misses']) {
9427 $cachestoreclass = 'lowhits bg-warning text-dark';
9428 } else {
9429 $cachestoreclass = 'hihits';
9431 $text .= "$store($data[hits]/$data[misses]/$data[sets]) ";
9432 $cell = new html_table_cell($store);
9433 $cell->attributes = ['class' => $cachestoreclass];
9434 $row[] = $cell;
9435 $cell = new html_table_cell($data['hits']);
9436 $cell->attributes = ['class' => $cachestoreclass];
9437 $row[] = $cell;
9438 $cell = new html_table_cell($data['misses']);
9439 $cell->attributes = ['class' => $cachestoreclass];
9440 $row[] = $cell;
9442 if ($store !== cache_store::STATIC_ACCEL) {
9443 // The static cache is never set.
9444 $cell = new html_table_cell($data['sets']);
9445 $cell->attributes = ['class' => $cachestoreclass];
9446 $row[] = $cell;
9448 if ($data['hits'] || $data['sets']) {
9449 if ($data['iobytes'] === cache_store::IO_BYTES_NOT_SUPPORTED) {
9450 $size = '-';
9451 } else {
9452 $size = display_size($data['iobytes'], 1, 'KB');
9453 if ($data['iobytes'] >= 10 * 1024) {
9454 $cachestoreclass = ' bg-warning text-dark';
9457 } else {
9458 $size = '';
9460 $cell = new html_table_cell($size);
9461 $cell->attributes = ['class' => $cachestoreclass];
9462 $row[] = $cell;
9464 $storec++;
9466 while ($storec++ < $maxstores) {
9467 $row[] = '';
9468 $row[] = '';
9469 $row[] = '';
9470 $row[] = '';
9471 $row[] = '';
9473 $text .= '} ';
9475 $table->data[] = $row;
9478 $html .= html_writer::table($table);
9480 // Now lets also show sub totals for each cache store.
9481 $storetotals = [];
9482 $storetotal = ['hits' => 0, 'misses' => 0, 'sets' => 0, 'iobytes' => 0];
9483 foreach ($stats as $definition => $details) {
9484 foreach ($details['stores'] as $store => $data) {
9485 if (!array_key_exists($store, $storetotals)) {
9486 $storetotals[$store] = ['hits' => 0, 'misses' => 0, 'sets' => 0, 'iobytes' => 0];
9488 $storetotals[$store]['class'] = $data['class'];
9489 $storetotals[$store]['hits'] += $data['hits'];
9490 $storetotals[$store]['misses'] += $data['misses'];
9491 $storetotals[$store]['sets'] += $data['sets'];
9492 $storetotal['hits'] += $data['hits'];
9493 $storetotal['misses'] += $data['misses'];
9494 $storetotal['sets'] += $data['sets'];
9495 if ($data['iobytes'] !== cache_store::IO_BYTES_NOT_SUPPORTED) {
9496 $storetotals[$store]['iobytes'] += $data['iobytes'];
9497 $storetotal['iobytes'] += $data['iobytes'];
9502 $table = new html_table();
9503 $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered';
9504 $table->head = [get_string('storename', 'cache'), get_string('type_cachestore', 'plugin'), 'H', 'M', 'S', 'I/O'];
9505 $table->data = [];
9506 $table->align = ['left', 'left', 'right', 'right', 'right', 'right'];
9508 ksort($storetotals);
9510 foreach ($storetotals as $store => $data) {
9511 $row = [];
9512 if ($data['hits'] == 0 and $data['misses'] > 0) {
9513 $cachestoreclass = 'nohits bg-danger';
9514 } else if ($data['hits'] < $data['misses']) {
9515 $cachestoreclass = 'lowhits bg-warning text-dark';
9516 } else {
9517 $cachestoreclass = 'hihits';
9519 $cell = new html_table_cell($store);
9520 $cell->attributes = ['class' => $cachestoreclass];
9521 $row[] = $cell;
9522 $cell = new html_table_cell($data['class']);
9523 $cell->attributes = ['class' => $cachestoreclass];
9524 $row[] = $cell;
9525 $cell = new html_table_cell($data['hits']);
9526 $cell->attributes = ['class' => $cachestoreclass];
9527 $row[] = $cell;
9528 $cell = new html_table_cell($data['misses']);
9529 $cell->attributes = ['class' => $cachestoreclass];
9530 $row[] = $cell;
9531 $cell = new html_table_cell($data['sets']);
9532 $cell->attributes = ['class' => $cachestoreclass];
9533 $row[] = $cell;
9534 if ($data['hits'] || $data['sets']) {
9535 if ($data['iobytes']) {
9536 $size = display_size($data['iobytes'], 1, 'KB');
9537 } else {
9538 $size = '-';
9540 } else {
9541 $size = '';
9543 $cell = new html_table_cell($size);
9544 $cell->attributes = ['class' => $cachestoreclass];
9545 $row[] = $cell;
9546 $table->data[] = $row;
9548 if (!empty($storetotal['iobytes'])) {
9549 $size = display_size($storetotal['iobytes'], 1, 'KB');
9550 } else if (!empty($storetotal['hits']) || !empty($storetotal['sets'])) {
9551 $size = '-';
9552 } else {
9553 $size = '';
9555 $row = [
9556 get_string('total'),
9558 $storetotal['hits'],
9559 $storetotal['misses'],
9560 $storetotal['sets'],
9561 $size,
9563 $table->data[] = $row;
9565 $html .= html_writer::table($table);
9567 $info['cachesused'] = "$hits / $misses / $sets";
9568 $info['html'] .= $html;
9569 $info['txt'] .= $text.'. ';
9570 } else {
9571 $info['cachesused'] = '0 / 0 / 0';
9572 $info['html'] .= '<div class="cachesused">Caches used (hits/misses/sets): 0/0/0</div>';
9573 $info['txt'] .= 'Caches used (hits/misses/sets): 0/0/0 ';
9576 // Display lock information if any.
9577 if (!empty($PERF->locks)) {
9578 $table = new html_table();
9579 $table->attributes['class'] = 'locktimings table table-dark table-sm w-auto table-bordered';
9580 $table->head = ['Lock', 'Waited (s)', 'Obtained', 'Held for (s)'];
9581 $table->align = ['left', 'right', 'center', 'right'];
9582 $table->data = [];
9583 $text = 'Locks (waited/obtained/held):';
9584 foreach ($PERF->locks as $locktiming) {
9585 $row = [];
9586 $row[] = s($locktiming->type . '/' . $locktiming->resource);
9587 $text .= ' ' . $locktiming->type . '/' . $locktiming->resource . ' (';
9589 // The time we had to wait to get the lock.
9590 $roundedtime = number_format($locktiming->wait, 1);
9591 $cell = new html_table_cell($roundedtime);
9592 if ($locktiming->wait > 0.5) {
9593 $cell->attributes = ['class' => 'bg-warning text-dark'];
9595 $row[] = $cell;
9596 $text .= $roundedtime . '/';
9598 // Show a tick or cross for success.
9599 $row[] = $locktiming->success ? '&#x2713;' : '&#x274c;';
9600 $text .= ($locktiming->success ? 'y' : 'n') . '/';
9602 // If applicable, show how long we held the lock before releasing it.
9603 if (property_exists($locktiming, 'held')) {
9604 $roundedtime = number_format($locktiming->held, 1);
9605 $cell = new html_table_cell($roundedtime);
9606 if ($locktiming->held > 0.5) {
9607 $cell->attributes = ['class' => 'bg-warning text-dark'];
9609 $row[] = $cell;
9610 $text .= $roundedtime;
9611 } else {
9612 $row[] = '-';
9613 $text .= '-';
9615 $text .= ')';
9617 $table->data[] = $row;
9619 $info['html'] .= html_writer::table($table);
9620 $info['txt'] .= $text . '. ';
9623 $info['html'] = '<div class="performanceinfo siteinfo container-fluid px-md-0 overflow-auto pt-3">'.$info['html'].'</div>';
9624 return $info;
9628 * Renames a file or directory to a unique name within the same directory.
9630 * This function is designed to avoid any potential race conditions, and select an unused name.
9632 * @param string $filepath Original filepath
9633 * @param string $prefix Prefix to use for the temporary name
9634 * @return string|bool New file path or false if failed
9635 * @since Moodle 3.10
9637 function rename_to_unused_name(string $filepath, string $prefix = '_temp_') {
9638 $dir = dirname($filepath);
9639 $basename = $dir . '/' . $prefix;
9640 $limit = 0;
9641 while ($limit < 100) {
9642 // Select a new name based on a random number.
9643 $newfilepath = $basename . md5(mt_rand());
9645 // Attempt a rename to that new name.
9646 if (@rename($filepath, $newfilepath)) {
9647 return $newfilepath;
9650 // The first time, do some sanity checks, maybe it is failing for a good reason and there
9651 // is no point trying 100 times if so.
9652 if ($limit === 0 && (!file_exists($filepath) || !is_writable($dir))) {
9653 return false;
9655 $limit++;
9657 return false;
9661 * Delete directory or only its content
9663 * @param string $dir directory path
9664 * @param bool $contentonly
9665 * @return bool success, true also if dir does not exist
9667 function remove_dir($dir, $contentonly=false) {
9668 if (!is_dir($dir)) {
9669 // Nothing to do.
9670 return true;
9673 if (!$contentonly) {
9674 // Start by renaming the directory; this will guarantee that other processes don't write to it
9675 // while it is in the process of being deleted.
9676 $tempdir = rename_to_unused_name($dir);
9677 if ($tempdir) {
9678 // If the rename was successful then delete the $tempdir instead.
9679 $dir = $tempdir;
9681 // If the rename fails, we will continue through and attempt to delete the directory
9682 // without renaming it since that is likely to at least delete most of the files.
9685 if (!$handle = opendir($dir)) {
9686 return false;
9688 $result = true;
9689 while (false!==($item = readdir($handle))) {
9690 if ($item != '.' && $item != '..') {
9691 if (is_dir($dir.'/'.$item)) {
9692 $result = remove_dir($dir.'/'.$item) && $result;
9693 } else {
9694 $result = unlink($dir.'/'.$item) && $result;
9698 closedir($handle);
9699 if ($contentonly) {
9700 clearstatcache(); // Make sure file stat cache is properly invalidated.
9701 return $result;
9703 $result = rmdir($dir); // If anything left the result will be false, no need for && $result.
9704 clearstatcache(); // Make sure file stat cache is properly invalidated.
9705 return $result;
9709 * Detect if an object or a class contains a given property
9710 * will take an actual object or the name of a class
9712 * @param mixed $obj Name of class or real object to test
9713 * @param string $property name of property to find
9714 * @return bool true if property exists
9716 function object_property_exists( $obj, $property ) {
9717 if (is_string( $obj )) {
9718 $properties = get_class_vars( $obj );
9719 } else {
9720 $properties = get_object_vars( $obj );
9722 return array_key_exists( $property, $properties );
9726 * Converts an object into an associative array
9728 * This function converts an object into an associative array by iterating
9729 * over its public properties. Because this function uses the foreach
9730 * construct, Iterators are respected. It works recursively on arrays of objects.
9731 * Arrays and simple values are returned as is.
9733 * If class has magic properties, it can implement IteratorAggregate
9734 * and return all available properties in getIterator()
9736 * @param mixed $var
9737 * @return array
9739 function convert_to_array($var) {
9740 $result = array();
9742 // Loop over elements/properties.
9743 foreach ($var as $key => $value) {
9744 // Recursively convert objects.
9745 if (is_object($value) || is_array($value)) {
9746 $result[$key] = convert_to_array($value);
9747 } else {
9748 // Simple values are untouched.
9749 $result[$key] = $value;
9752 return $result;
9756 * Detect a custom script replacement in the data directory that will
9757 * replace an existing moodle script
9759 * @return string|bool full path name if a custom script exists, false if no custom script exists
9761 function custom_script_path() {
9762 global $CFG, $SCRIPT;
9764 if ($SCRIPT === null) {
9765 // Probably some weird external script.
9766 return false;
9769 $scriptpath = $CFG->customscripts . $SCRIPT;
9771 // Check the custom script exists.
9772 if (file_exists($scriptpath) and is_file($scriptpath)) {
9773 return $scriptpath;
9774 } else {
9775 return false;
9780 * Returns whether or not the user object is a remote MNET user. This function
9781 * is in moodlelib because it does not rely on loading any of the MNET code.
9783 * @param object $user A valid user object
9784 * @return bool True if the user is from a remote Moodle.
9786 function is_mnet_remote_user($user) {
9787 global $CFG;
9789 if (!isset($CFG->mnet_localhost_id)) {
9790 include_once($CFG->dirroot . '/mnet/lib.php');
9791 $env = new mnet_environment();
9792 $env->init();
9793 unset($env);
9796 return (!empty($user->mnethostid) && $user->mnethostid != $CFG->mnet_localhost_id);
9800 * This function will search for browser prefereed languages, setting Moodle
9801 * to use the best one available if $SESSION->lang is undefined
9803 function setup_lang_from_browser() {
9804 global $CFG, $SESSION, $USER;
9806 if (!empty($SESSION->lang) or !empty($USER->lang) or empty($CFG->autolang)) {
9807 // Lang is defined in session or user profile, nothing to do.
9808 return;
9811 if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { // There isn't list of browser langs, nothing to do.
9812 return;
9815 // Extract and clean langs from headers.
9816 $rawlangs = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
9817 $rawlangs = str_replace('-', '_', $rawlangs); // We are using underscores.
9818 $rawlangs = explode(',', $rawlangs); // Convert to array.
9819 $langs = array();
9821 $order = 1.0;
9822 foreach ($rawlangs as $lang) {
9823 if (strpos($lang, ';') === false) {
9824 $langs[(string)$order] = $lang;
9825 $order = $order-0.01;
9826 } else {
9827 $parts = explode(';', $lang);
9828 $pos = strpos($parts[1], '=');
9829 $langs[substr($parts[1], $pos+1)] = $parts[0];
9832 krsort($langs, SORT_NUMERIC);
9834 // Look for such langs under standard locations.
9835 foreach ($langs as $lang) {
9836 // Clean it properly for include.
9837 $lang = strtolower(clean_param($lang, PARAM_SAFEDIR));
9838 if (get_string_manager()->translation_exists($lang, false)) {
9839 // If the translation for this language exists then try to set it
9840 // for the rest of the session, if this is a read only session then
9841 // we can only set it temporarily in $CFG.
9842 if (defined('READ_ONLY_SESSION') && !empty($CFG->enable_read_only_sessions)) {
9843 $CFG->lang = $lang;
9844 } else {
9845 $SESSION->lang = $lang;
9847 // We have finished. Go out.
9848 break;
9851 return;
9855 * Check if $url matches anything in proxybypass list
9857 * Any errors just result in the proxy being used (least bad)
9859 * @param string $url url to check
9860 * @return boolean true if we should bypass the proxy
9862 function is_proxybypass( $url ) {
9863 global $CFG;
9865 // Sanity check.
9866 if (empty($CFG->proxyhost) or empty($CFG->proxybypass)) {
9867 return false;
9870 // Get the host part out of the url.
9871 if (!$host = parse_url( $url, PHP_URL_HOST )) {
9872 return false;
9875 // Get the possible bypass hosts into an array.
9876 $matches = explode( ',', $CFG->proxybypass );
9878 // Check for a exact match on the IP or in the domains.
9879 $isdomaininallowedlist = \core\ip_utils::is_domain_in_allowed_list($host, $matches);
9880 $isipinsubnetlist = \core\ip_utils::is_ip_in_subnet_list($host, $CFG->proxybypass, ',');
9882 if ($isdomaininallowedlist || $isipinsubnetlist) {
9883 return true;
9886 // Nothing matched.
9887 return false;
9891 * Check if the passed navigation is of the new style
9893 * @param mixed $navigation
9894 * @return bool true for yes false for no
9896 function is_newnav($navigation) {
9897 if (is_array($navigation) && !empty($navigation['newnav'])) {
9898 return true;
9899 } else {
9900 return false;
9905 * Checks whether the given variable name is defined as a variable within the given object.
9907 * This will NOT work with stdClass objects, which have no class variables.
9909 * @param string $var The variable name
9910 * @param object $object The object to check
9911 * @return boolean
9913 function in_object_vars($var, $object) {
9914 $classvars = get_class_vars(get_class($object));
9915 $classvars = array_keys($classvars);
9916 return in_array($var, $classvars);
9920 * Returns an array without repeated objects.
9921 * This function is similar to array_unique, but for arrays that have objects as values
9923 * @param array $array
9924 * @param bool $keepkeyassoc
9925 * @return array
9927 function object_array_unique($array, $keepkeyassoc = true) {
9928 $duplicatekeys = array();
9929 $tmp = array();
9931 foreach ($array as $key => $val) {
9932 // Convert objects to arrays, in_array() does not support objects.
9933 if (is_object($val)) {
9934 $val = (array)$val;
9937 if (!in_array($val, $tmp)) {
9938 $tmp[] = $val;
9939 } else {
9940 $duplicatekeys[] = $key;
9944 foreach ($duplicatekeys as $key) {
9945 unset($array[$key]);
9948 return $keepkeyassoc ? $array : array_values($array);
9952 * Is a userid the primary administrator?
9954 * @param int $userid int id of user to check
9955 * @return boolean
9957 function is_primary_admin($userid) {
9958 $primaryadmin = get_admin();
9960 if ($userid == $primaryadmin->id) {
9961 return true;
9962 } else {
9963 return false;
9968 * Returns the site identifier
9970 * @return string $CFG->siteidentifier, first making sure it is properly initialised.
9972 function get_site_identifier() {
9973 global $CFG;
9974 // Check to see if it is missing. If so, initialise it.
9975 if (empty($CFG->siteidentifier)) {
9976 set_config('siteidentifier', random_string(32) . $_SERVER['HTTP_HOST']);
9978 // Return it.
9979 return $CFG->siteidentifier;
9983 * Check whether the given password has no more than the specified
9984 * number of consecutive identical characters.
9986 * @param string $password password to be checked against the password policy
9987 * @param integer $maxchars maximum number of consecutive identical characters
9988 * @return bool
9990 function check_consecutive_identical_characters($password, $maxchars) {
9992 if ($maxchars < 1) {
9993 return true; // Zero 0 is to disable this check.
9995 if (strlen($password) <= $maxchars) {
9996 return true; // Too short to fail this test.
9999 $previouschar = '';
10000 $consecutivecount = 1;
10001 foreach (str_split($password) as $char) {
10002 if ($char != $previouschar) {
10003 $consecutivecount = 1;
10004 } else {
10005 $consecutivecount++;
10006 if ($consecutivecount > $maxchars) {
10007 return false; // Check failed already.
10011 $previouschar = $char;
10014 return true;
10018 * Helper function to do partial function binding.
10019 * so we can use it for preg_replace_callback, for example
10020 * this works with php functions, user functions, static methods and class methods
10021 * it returns you a callback that you can pass on like so:
10023 * $callback = partial('somefunction', $arg1, $arg2);
10024 * or
10025 * $callback = partial(array('someclass', 'somestaticmethod'), $arg1, $arg2);
10026 * or even
10027 * $obj = new someclass();
10028 * $callback = partial(array($obj, 'somemethod'), $arg1, $arg2);
10030 * and then the arguments that are passed through at calltime are appended to the argument list.
10032 * @param mixed $function a php callback
10033 * @param mixed $arg1,... $argv arguments to partially bind with
10034 * @return array Array callback
10036 function partial() {
10037 if (!class_exists('partial')) {
10039 * Used to manage function binding.
10040 * @copyright 2009 Penny Leach
10041 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
10043 class partial{
10044 /** @var array */
10045 public $values = array();
10046 /** @var string The function to call as a callback. */
10047 public $func;
10049 * Constructor
10050 * @param string $func
10051 * @param array $args
10053 public function __construct($func, $args) {
10054 $this->values = $args;
10055 $this->func = $func;
10058 * Calls the callback function.
10059 * @return mixed
10061 public function method() {
10062 $args = func_get_args();
10063 return call_user_func_array($this->func, array_merge($this->values, $args));
10067 $args = func_get_args();
10068 $func = array_shift($args);
10069 $p = new partial($func, $args);
10070 return array($p, 'method');
10074 * helper function to load up and initialise the mnet environment
10075 * this must be called before you use mnet functions.
10077 * @return mnet_environment the equivalent of old $MNET global
10079 function get_mnet_environment() {
10080 global $CFG;
10081 require_once($CFG->dirroot . '/mnet/lib.php');
10082 static $instance = null;
10083 if (empty($instance)) {
10084 $instance = new mnet_environment();
10085 $instance->init();
10087 return $instance;
10091 * during xmlrpc server code execution, any code wishing to access
10092 * information about the remote peer must use this to get it.
10094 * @return mnet_remote_client|false the equivalent of old $MNETREMOTE_CLIENT global
10096 function get_mnet_remote_client() {
10097 if (!defined('MNET_SERVER')) {
10098 debugging(get_string('notinxmlrpcserver', 'mnet'));
10099 return false;
10101 global $MNET_REMOTE_CLIENT;
10102 if (isset($MNET_REMOTE_CLIENT)) {
10103 return $MNET_REMOTE_CLIENT;
10105 return false;
10109 * during the xmlrpc server code execution, this will be called
10110 * to setup the object returned by {@link get_mnet_remote_client}
10112 * @param mnet_remote_client $client the client to set up
10113 * @throws moodle_exception
10115 function set_mnet_remote_client($client) {
10116 if (!defined('MNET_SERVER')) {
10117 throw new moodle_exception('notinxmlrpcserver', 'mnet');
10119 global $MNET_REMOTE_CLIENT;
10120 $MNET_REMOTE_CLIENT = $client;
10124 * return the jump url for a given remote user
10125 * this is used for rewriting forum post links in emails, etc
10127 * @param stdclass $user the user to get the idp url for
10129 function mnet_get_idp_jump_url($user) {
10130 global $CFG;
10132 static $mnetjumps = array();
10133 if (!array_key_exists($user->mnethostid, $mnetjumps)) {
10134 $idp = mnet_get_peer_host($user->mnethostid);
10135 $idpjumppath = mnet_get_app_jumppath($idp->applicationid);
10136 $mnetjumps[$user->mnethostid] = $idp->wwwroot . $idpjumppath . '?hostwwwroot=' . $CFG->wwwroot . '&wantsurl=';
10138 return $mnetjumps[$user->mnethostid];
10142 * Gets the homepage to use for the current user
10144 * @return int One of HOMEPAGE_*
10146 function get_home_page() {
10147 global $CFG;
10149 if (isloggedin() && !isguestuser() && !empty($CFG->defaulthomepage)) {
10150 // If dashboard is disabled, home will be set to default page.
10151 $defaultpage = get_default_home_page();
10152 if ($CFG->defaulthomepage == HOMEPAGE_MY) {
10153 if (!empty($CFG->enabledashboard)) {
10154 return HOMEPAGE_MY;
10155 } else {
10156 return $defaultpage;
10158 } else if ($CFG->defaulthomepage == HOMEPAGE_MYCOURSES) {
10159 return HOMEPAGE_MYCOURSES;
10160 } else {
10161 $userhomepage = (int) get_user_preferences('user_home_page_preference', $defaultpage);
10162 if (empty($CFG->enabledashboard) && $userhomepage == HOMEPAGE_MY) {
10163 // If the user was using the dashboard but it's disabled, return the default home page.
10164 $userhomepage = $defaultpage;
10166 return $userhomepage;
10169 return HOMEPAGE_SITE;
10173 * Returns the default home page to display if current one is not defined or can't be applied.
10174 * The default behaviour is to return Dashboard if it's enabled or My courses page if it isn't.
10176 * @return int The default home page.
10178 function get_default_home_page(): int {
10179 global $CFG;
10181 return (!isset($CFG->enabledashboard) || $CFG->enabledashboard) ? HOMEPAGE_MY : HOMEPAGE_MYCOURSES;
10185 * Gets the name of a course to be displayed when showing a list of courses.
10186 * By default this is just $course->fullname but user can configure it. The
10187 * result of this function should be passed through print_string.
10188 * @param stdClass|core_course_list_element $course Moodle course object
10189 * @return string Display name of course (either fullname or short + fullname)
10191 function get_course_display_name_for_list($course) {
10192 global $CFG;
10193 if (!empty($CFG->courselistshortnames)) {
10194 if (!($course instanceof stdClass)) {
10195 $course = (object)convert_to_array($course);
10197 return get_string('courseextendednamedisplay', '', $course);
10198 } else {
10199 return $course->fullname;
10204 * Safe analogue of unserialize() that can only parse arrays
10206 * Arrays may contain only integers or strings as both keys and values. Nested arrays are allowed.
10208 * @param string $expression
10209 * @return array|bool either parsed array or false if parsing was impossible.
10211 function unserialize_array($expression) {
10213 // Check the expression is an array.
10214 if (!preg_match('/^a:(\d+):/', $expression)) {
10215 return false;
10218 $values = (array) unserialize_object($expression);
10220 // Callback that returns true if the given value is an unserialized object, executes recursively.
10221 $invalidvaluecallback = static function($value) use (&$invalidvaluecallback): bool {
10222 if (is_array($value)) {
10223 return (bool) array_filter($value, $invalidvaluecallback);
10225 return ($value instanceof stdClass) || ($value instanceof __PHP_Incomplete_Class);
10228 // Iterate over the result to ensure there are no stray objects.
10229 if (array_filter($values, $invalidvaluecallback)) {
10230 return false;
10233 return $values;
10237 * Safe method for unserializing given input that is expected to contain only a serialized instance of an stdClass object
10239 * If any class type other than stdClass is included in the input string, it will not be instantiated and will be cast to an
10240 * stdClass object. The initial cast to array, then back to object is to ensure we are always returning the correct type,
10241 * otherwise we would return an instances of {@see __PHP_Incomplete_class} for malformed strings
10243 * @param string $input
10244 * @return stdClass
10246 function unserialize_object(string $input): stdClass {
10247 $instance = (array) unserialize($input, ['allowed_classes' => [stdClass::class]]);
10248 return (object) $instance;
10252 * The lang_string class
10254 * This special class is used to create an object representation of a string request.
10255 * It is special because processing doesn't occur until the object is first used.
10256 * The class was created especially to aid performance in areas where strings were
10257 * required to be generated but were not necessarily used.
10258 * As an example the admin tree when generated uses over 1500 strings, of which
10259 * normally only 1/3 are ever actually printed at any time.
10260 * The performance advantage is achieved by not actually processing strings that
10261 * arn't being used, as such reducing the processing required for the page.
10263 * How to use the lang_string class?
10264 * There are two methods of using the lang_string class, first through the
10265 * forth argument of the get_string function, and secondly directly.
10266 * The following are examples of both.
10267 * 1. Through get_string calls e.g.
10268 * $string = get_string($identifier, $component, $a, true);
10269 * $string = get_string('yes', 'moodle', null, true);
10270 * 2. Direct instantiation
10271 * $string = new lang_string($identifier, $component, $a, $lang);
10272 * $string = new lang_string('yes');
10274 * How do I use a lang_string object?
10275 * The lang_string object makes use of a magic __toString method so that you
10276 * are able to use the object exactly as you would use a string in most cases.
10277 * This means you are able to collect it into a variable and then directly
10278 * echo it, or concatenate it into another string, or similar.
10279 * The other thing you can do is manually get the string by calling the
10280 * lang_strings out method e.g.
10281 * $string = new lang_string('yes');
10282 * $string->out();
10283 * Also worth noting is that the out method can take one argument, $lang which
10284 * allows the developer to change the language on the fly.
10286 * When should I use a lang_string object?
10287 * The lang_string object is designed to be used in any situation where a
10288 * string may not be needed, but needs to be generated.
10289 * The admin tree is a good example of where lang_string objects should be
10290 * used.
10291 * A more practical example would be any class that requries strings that may
10292 * not be printed (after all classes get renderer by renderers and who knows
10293 * what they will do ;))
10295 * When should I not use a lang_string object?
10296 * Don't use lang_strings when you are going to use a string immediately.
10297 * There is no need as it will be processed immediately and there will be no
10298 * advantage, and in fact perhaps a negative hit as a class has to be
10299 * instantiated for a lang_string object, however get_string won't require
10300 * that.
10302 * Limitations:
10303 * 1. You cannot use a lang_string object as an array offset. Doing so will
10304 * result in PHP throwing an error. (You can use it as an object property!)
10306 * @package core
10307 * @category string
10308 * @copyright 2011 Sam Hemelryk
10309 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
10311 class lang_string {
10313 /** @var string The strings identifier */
10314 protected $identifier;
10315 /** @var string The strings component. Default '' */
10316 protected $component = '';
10317 /** @var array|stdClass Any arguments required for the string. Default null */
10318 protected $a = null;
10319 /** @var string The language to use when processing the string. Default null */
10320 protected $lang = null;
10322 /** @var string The processed string (once processed) */
10323 protected $string = null;
10326 * A special boolean. If set to true then the object has been woken up and
10327 * cannot be regenerated. If this is set then $this->string MUST be used.
10328 * @var bool
10330 protected $forcedstring = false;
10333 * Constructs a lang_string object
10335 * This function should do as little processing as possible to ensure the best
10336 * performance for strings that won't be used.
10338 * @param string $identifier The strings identifier
10339 * @param string $component The strings component
10340 * @param stdClass|array|mixed $a Any arguments the string requires
10341 * @param string $lang The language to use when processing the string.
10342 * @throws coding_exception
10344 public function __construct($identifier, $component = '', $a = null, $lang = null) {
10345 if (empty($component)) {
10346 $component = 'moodle';
10349 $this->identifier = $identifier;
10350 $this->component = $component;
10351 $this->lang = $lang;
10353 // We MUST duplicate $a to ensure that it if it changes by reference those
10354 // changes are not carried across.
10355 // To do this we always ensure $a or its properties/values are strings
10356 // and that any properties/values that arn't convertable are forgotten.
10357 if ($a !== null) {
10358 if (is_scalar($a)) {
10359 $this->a = $a;
10360 } else if ($a instanceof lang_string) {
10361 $this->a = $a->out();
10362 } else if (is_object($a) or is_array($a)) {
10363 $a = (array)$a;
10364 $this->a = array();
10365 foreach ($a as $key => $value) {
10366 // Make sure conversion errors don't get displayed (results in '').
10367 if (is_array($value)) {
10368 $this->a[$key] = '';
10369 } else if (is_object($value)) {
10370 if (method_exists($value, '__toString')) {
10371 $this->a[$key] = $value->__toString();
10372 } else {
10373 $this->a[$key] = '';
10375 } else {
10376 $this->a[$key] = (string)$value;
10382 if (debugging(false, DEBUG_DEVELOPER)) {
10383 if (clean_param($this->identifier, PARAM_STRINGID) == '') {
10384 throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please check your string definition');
10386 if (!empty($this->component) && clean_param($this->component, PARAM_COMPONENT) == '') {
10387 throw new coding_exception('Invalid string compontent. Please check your string definition');
10389 if (!get_string_manager()->string_exists($this->identifier, $this->component)) {
10390 debugging('String does not exist. Please check your string definition for '.$this->identifier.'/'.$this->component, DEBUG_DEVELOPER);
10396 * Processes the string.
10398 * This function actually processes the string, stores it in the string property
10399 * and then returns it.
10400 * You will notice that this function is VERY similar to the get_string method.
10401 * That is because it is pretty much doing the same thing.
10402 * However as this function is an upgrade it isn't as tolerant to backwards
10403 * compatibility.
10405 * @return string
10406 * @throws coding_exception
10408 protected function get_string() {
10409 global $CFG;
10411 // Check if we need to process the string.
10412 if ($this->string === null) {
10413 // Check the quality of the identifier.
10414 if ($CFG->debugdeveloper && clean_param($this->identifier, PARAM_STRINGID) === '') {
10415 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);
10418 // Process the string.
10419 $this->string = get_string_manager()->get_string($this->identifier, $this->component, $this->a, $this->lang);
10420 // Debugging feature lets you display string identifier and component.
10421 if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
10422 $this->string .= ' {' . $this->identifier . '/' . $this->component . '}';
10425 // Return the string.
10426 return $this->string;
10430 * Returns the string
10432 * @param string $lang The langauge to use when processing the string
10433 * @return string
10435 public function out($lang = null) {
10436 if ($lang !== null && $lang != $this->lang && ($this->lang == null && $lang != current_language())) {
10437 if ($this->forcedstring) {
10438 debugging('lang_string objects that have been used cannot be printed in another language. ('.$this->lang.' used)', DEBUG_DEVELOPER);
10439 return $this->get_string();
10441 $translatedstring = new lang_string($this->identifier, $this->component, $this->a, $lang);
10442 return $translatedstring->out();
10444 return $this->get_string();
10448 * Magic __toString method for printing a string
10450 * @return string
10452 public function __toString() {
10453 return $this->get_string();
10457 * Magic __set_state method used for var_export
10459 * @param array $array
10460 * @return self
10462 public static function __set_state(array $array): self {
10463 $tmp = new lang_string($array['identifier'], $array['component'], $array['a'], $array['lang']);
10464 $tmp->string = $array['string'];
10465 $tmp->forcedstring = $array['forcedstring'];
10466 return $tmp;
10470 * Prepares the lang_string for sleep and stores only the forcedstring and
10471 * string properties... the string cannot be regenerated so we need to ensure
10472 * it is generated for this.
10474 * @return array
10476 public function __sleep() {
10477 $this->get_string();
10478 $this->forcedstring = true;
10479 return array('forcedstring', 'string', 'lang');
10483 * Returns the identifier.
10485 * @return string
10487 public function get_identifier() {
10488 return $this->identifier;
10492 * Returns the component.
10494 * @return string
10496 public function get_component() {
10497 return $this->component;
10502 * Get human readable name describing the given callable.
10504 * This performs syntax check only to see if the given param looks like a valid function, method or closure.
10505 * It does not check if the callable actually exists.
10507 * @param callable|string|array $callable
10508 * @return string|bool Human readable name of callable, or false if not a valid callable.
10510 function get_callable_name($callable) {
10512 if (!is_callable($callable, true, $name)) {
10513 return false;
10515 } else {
10516 return $name;
10521 * Tries to guess if $CFG->wwwroot is publicly accessible or not.
10522 * Never put your faith on this function and rely on its accuracy as there might be false positives.
10523 * It just performs some simple checks, and mainly is used for places where we want to hide some options
10524 * such as site registration when $CFG->wwwroot is not publicly accessible.
10525 * Good thing is there is no false negative.
10526 * Note that it's possible to force the result of this check by specifying $CFG->site_is_public in config.php
10528 * @return bool
10530 function site_is_public() {
10531 global $CFG;
10533 // Return early if site admin has forced this setting.
10534 if (isset($CFG->site_is_public)) {
10535 return (bool)$CFG->site_is_public;
10538 $host = parse_url($CFG->wwwroot, PHP_URL_HOST);
10540 if ($host === 'localhost' || preg_match('|^127\.\d+\.\d+\.\d+$|', $host)) {
10541 $ispublic = false;
10542 } else if (\core\ip_utils::is_ip_address($host) && !ip_is_public($host)) {
10543 $ispublic = false;
10544 } else if (($address = \core\ip_utils::get_ip_address($host)) && !ip_is_public($address)) {
10545 $ispublic = false;
10546 } else {
10547 $ispublic = true;
10550 return $ispublic;
10554 * Validates user's password length.
10556 * @param string $password
10557 * @param int $pepperlength The length of the used peppers
10558 * @return bool
10560 function exceeds_password_length(string $password, int $pepperlength = 0): bool {
10561 return (strlen($password) > (MAX_PASSWORD_CHARACTERS + $pepperlength));
10565 * A helper to replace PHP 8.3 usage of array_keys with two args.
10567 * There is an indication that this will become a new method in PHP 8.4, but that has not happened yet.
10568 * Therefore this non-polyfill has been created with a different naming convention.
10569 * In the future it can be deprecated if a core PHP method is created.
10571 * https://wiki.php.net/rfc/deprecate_functions_with_overloaded_signatures#array_keys
10573 * @param array $array
10574 * @param mixed $filter The value to filter on
10575 * @param bool $strict Whether to apply a strit test with the filter
10576 * @return array
10578 function moodle_array_keys_filter(array $array, mixed $filter, bool $strict = false): array {
10579 return array_keys(array_filter(
10580 $array,
10581 function($value, $key) use ($filter, $strict): bool {
10582 if ($strict) {
10583 return $value === $filter;
10585 return $value == $filter;
10587 ARRAY_FILTER_USE_BOTH,