2 // This file is part of Moodle - http://moodle.org/
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
18 * moodlelib.php - Moodle main library
20 * Main library file of miscellaneous general-purpose Moodle functions.
21 * Other main libraries:
22 * - weblib.php - functions that produce web output
23 * - datalib.php - functions that access the database
27 * @copyright 1999 onwards Martin Dougiamas http://dougiamas.com
28 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
34 defined('MOODLE_INTERNAL') ||
die();
36 // CONSTANTS (Encased in phpdoc proper comments).
38 // Date and time constants.
40 * Time constant - the number of seconds in a year
42 define('YEARSECS', 31536000);
45 * Time constant - the number of seconds in a week
47 define('WEEKSECS', 604800);
50 * Time constant - the number of seconds in a day
52 define('DAYSECS', 86400);
55 * Time constant - the number of seconds in an hour
57 define('HOURSECS', 3600);
60 * Time constant - the number of seconds in a minute
62 define('MINSECS', 60);
65 * Time constant - the number of minutes in a day
67 define('DAYMINS', 1440);
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');
83 * PARAM_ALPHA - contains only English ascii letters [a-zA-Z].
85 define('PARAM_ALPHA', \core\param
::ALPHA
->value
);
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
);
94 * PARAM_ALPHANUM - expected numbers 0-9 and English ascii letters [a-zA-Z] only.
96 define('PARAM_ALPHANUM', \core\param
::ALPHANUM
->value
);
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
);
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);
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);
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
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
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
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
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
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
685 * @throws coding_exception
687 function optional_param($parname, $default, $type) {
688 return \core\param
::from_type($type)->optional_param(
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
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
709 * @throws coding_exception
711 function optional_param_array($parname, $default, $type) {
712 return \core\param
::from_type($type)->optional_param_array(
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(
735 allownull
: $allownull,
736 debuginfo
: $debuginfo,
741 * Makes sure array contains only the allowed types, this function does not validate array key names!
744 * $options = clean_param($options, PARAM_INT);
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
751 * @throws coding_exception
753 function clean_param_array(?
array $param, $type, $recursive = false) {
754 return \core\param
::from_type($type)->clean_param_array(
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
765 * $course->format = clean_param($course->format, PARAM_ALPHA);
766 * $selectedgradeitem = clean_param($selectedgradeitem, PARAM_INT);
769 * @param mixed $param the variable we are cleaning
770 * @param string $type expected format of param after cleaning.
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_*.
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 === '') {
809 } else if (is_string($value)) {
810 if ((string)(int)$value === $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€');
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);
832 // Warn admins on admin/index.php page.
837 $result = @iconv
('UTF-8', 'UTF-8//IGNORE', $value);
842 } else if (is_array($value)) {
843 foreach ($value as $k => $v) {
844 $value[$k] = fix_utf8($v);
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);
857 // This is some other type, no utf-8 here.
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)) {
871 } else if (is_string($value)) {
872 return ((string)(int)$value) === $value;
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);
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) {
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);
938 // If it's for core config.
940 $conditions = ['name' => $name];
941 $invalidatecachekey = 'core';
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;
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;
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);
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) {
1012 if ($plugin === 'moodle' ||
$plugin === 'core' ||
empty($plugin)) {
1013 $forced =& $CFG->config_php_settings
;
1017 if (array_key_exists($plugin, $CFG->forced_plugin_settings
)) {
1018 $forced =& $CFG->forced_plugin_settings
[$plugin];
1025 if (!isset($CFG->siteidentifier
)) {
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;
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.
1050 $result = $DB->get_records_menu('config_plugins', array('plugin' => $plugin), '', 'name,value');
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];
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]);
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) {
1094 if (empty($plugin)) {
1096 $DB->delete_records('config', array('name' => $name));
1097 cache_helper
::invalidate_by_definition('core', 'config', array(), 'core');
1099 $DB->delete_records('config_plugins', array('name' => $name, 'plugin' => $plugin));
1100 cache_helper
::invalidate_by_definition('core', 'config', array(), $plugin);
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) {
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));
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@$') {
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@$') {
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;
1172 * Invalidates browser caches and cached data in temp.
1176 function purge_all_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.
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) {
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() {
1240 if (class_exists('core_plugin_manager')) {
1241 core_plugin_manager
::reset_caches();
1244 // Bump up cacherev field for all courses.
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.
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) {
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";
1285 if ($flags = $DB->get_records_select('cache_flags', $sqlwhere, $params, '', 'name,value')) {
1286 foreach ($flags as $flag) {
1287 $cf[$flag->name
] = $flag->value
;
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) {
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) {
1327 $timemodified = time();
1328 if ($expiry === null ||
$expiry < $timemodified) {
1329 $expiry = $timemodified +
24 * 60 * 60;
1331 $expiry = (int)$expiry;
1334 if ($value === null) {
1335 unset_cache_flag($type, $name);
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.
1345 $f->expiry
= $expiry;
1346 $f->timemodified
= $timemodified;
1347 $DB->update_record('cache_flags', $f);
1349 $f = new stdClass();
1350 $f->flagtype
= $type;
1353 $f->expiry
= $expiry;
1354 $f->timemodified
= $timemodified;
1355 $DB->insert_record('cache_flags', $f);
1361 * Removes a single volatile flag
1363 * @param string $type the "type" namespace for the key
1364 * @param string $name the key to set
1367 function unset_cache_flag($type, $name) {
1369 $DB->delete_records('cache_flags', array('name' => $name, 'flagtype' => $type));
1374 * Garbage-collect volatile flags
1376 * @return bool Always returns true
1378 function gc_cache_flags() {
1380 $DB->delete_records_select('cache_flags', 'expiry < ?', array(time()));
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.
1393 * @category preference
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
1400 function check_user_preferences_loaded(stdClass
$user, $cachelifetime = 120) {
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();
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.
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;
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.
1445 * @param integer $userid the user whose prefs were changed.
1447 function mark_user_preferences_changed($userid) {
1450 if (empty($userid) or isguestuser($userid)) {
1451 // No cache flags for guest and not-logged-in users.
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()}
1466 * @category preference
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) {
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)) {
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);
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;
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.
1519 $DB->set_field('user_preferences', 'value', $value, array('id' => $preference->id
));
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
);
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.
1548 * @category preference
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);
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.
1567 * @category preference
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) {
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)) {
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);
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]);
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
);
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,
1626 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1629 * @category preference
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) {
1641 if (is_null($name)) {
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)) {
1649 } else if (isset($user->id
)) {
1650 // Is a valid object.
1651 } else if (is_numeric($user)) {
1652 if ($USER->id
== $user) {
1655 $user = (object)array('id' => (int)$user);
1658 throw new coding_exception('Invalid $user parameter in get_user_preferences() call');
1661 check_user_preferences_loaded($user);
1665 return $user->preference
;
1666 } else if (isset($user->preference
[$name])) {
1667 // The single string value.
1668 return $user->preference
[$name];
1670 // Default value (null if not specified).
1675 // FUNCTIONS FOR HANDLING TIME.
1678 * Given Gregorian date parts in user time produce a GMT timestamp.
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.
1708 $time +
= dst_offset_on($time, $timezone);
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
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);
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
;
1772 $oyears = $years .' '. $sy;
1775 $odays = $days .' '. $sd;
1778 $ohours = $hours .' '. $sh;
1781 $omins = $mins .' '. $sm;
1784 $osecs = $secs .' '. $ss;
1788 return trim($oyears .' '. $odays);
1791 return trim($odays .' '. $ohours);
1794 return trim($ohours .' '. $omins);
1797 return trim($omins .' '. $osecs);
1802 return get_string('now');
1806 * Returns a formatted string that represents a date in user 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.
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([
1877 $datearray['hours'] < 12 ?
get_string('am', 'langconfig') : get_string('pm', 'langconfig'),
1878 $datearray['hours'] < 12 ?
get_string('amcaps', 'langconfig') : get_string('pmcaps', 'langconfig'),
1882 $datestring = core_date
::strftime($format, $date);
1883 core_date
::set_default_server_timezone();
1889 * Given a $time timestamp in GMT (seconds since epoch),
1890 * returns an array that represents the Gregorian date in user 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
);
1907 date_default_timezone_set(core_date
::get_user_timezone($timezone));
1908 $result = getdate($time);
1909 core_date
::set_default_server_timezone();
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!
1923 * @param int $date Timestamp in GMT
1924 * @param float|int|string $timezone user timezone
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.
1939 * $intervalstring = get_time_interval_string(12345600, 12345660);
1940 * Will produce the string:
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';
1974 foreach ($units as $key => $unit) {
1975 if (empty($interval->$key)) {
1978 $formatunits[] = $unit;
1980 if (!empty($formatunits)) {
1981 $formatkey = 'dateinterval' . implode("", $formatunits);
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.
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
2017 * @param float|int|string $timezone user timezone
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
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) {
2043 isset($CFG->forcetimezone
) ?
$CFG->forcetimezone
: 99,
2044 isset($USER->timezone
) ?
$USER->timezone
: 99,
2045 isset($CFG->timezone
) ?
$CFG->timezone
: 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) {
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.
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
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') {
2083 * Calculates when the day appears in specific month
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
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;
2130 $indexweekday = dayofweek($startday, $month, $year);
2132 $diff = $weekday - $indexweekday;
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
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
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
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
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() {
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
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
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;
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);
2242 $course = $DB->get_record('course', array('id' => $courseorid), '*', MUST_EXIST
);
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
);
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!!
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();
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;
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();
2314 $modinfo = get_fast_modinfo($course);
2315 $cm = $modinfo->get_cm($cm->id
);
2317 set_access_log_user();
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);
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');
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);
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
.'&course='. SITEID
);
2381 // Make sure the USER has a sesskey set up. Used for CSRF protection.
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.
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);
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
);
2442 $cmcontext = context_module
::instance($cm->id
, MUST_EXIST
);
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.
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.
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.
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
.'/');
2498 if (is_role_switched($course->id
)) {
2499 // Ok, user had to be inside this course before the switch.
2502 } else if (is_viewing($coursecontext, $USER)) {
2503 // Ok, no need to mess with enrol.
2507 if (isset($USER->enrol
['enrolled'][$course->id
])) {
2508 if ($USER->enrol
['enrolled'][$course->id
] > time()) {
2510 if (isset($USER->enrol
['tempguest'][$course->id
])) {
2511 unset($USER->enrol
['tempguest'][$course->id
]);
2512 remove_temp_course_roles($coursecontext);
2516 unset($USER->enrol
['enrolled'][$course->id
]);
2519 if (isset($USER->enrol
['tempguest'][$course->id
])) {
2520 if ($USER->enrol
['tempguest'][$course->id
] == 0) {
2522 } else if ($USER->enrol
['tempguest'][$course->id
] > time()) {
2526 unset($USER->enrol
['tempguest'][$course->id
]);
2527 remove_temp_course_roles($coursecontext);
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.
2537 $until = ENROL_MAX_TIMESTAMP
;
2539 $USER->enrol
['enrolled'][$course->id
] = $until;
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
])) {
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) {
2555 $until = ENROL_MAX_TIMESTAMP
;
2557 $USER->enrol
['enrolled'][$course->id
] = $until;
2562 // If not enrolled yet try to gain temporary guest access.
2564 foreach ($instances as $instance) {
2565 if (!isset($enrols[$instance->enrol
])) {
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;
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
.'/');
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.
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
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
2659 function require_logout() {
2662 if (!isloggedin()) {
2663 // This should not happen often, no need for hooks or events here.
2664 \core\session\manager
::terminate_current();
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(
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.
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
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
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;
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;
2760 $course = clone $SITE;
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');
2769 $PAGE->set_course($course);
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
);
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
2795 * @throws moodle_exception
2797 function validate_user_key($keyvalue, $script, $instance) {
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');
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) {
2830 if (!NO_MOODLE_COOKIES
) {
2831 throw new \
moodle_exception('sessioncookiesdisable');
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) {
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
))) {
2887 $key->value
= md5($userid.'_'.time().random_string(40));
2889 $DB->insert_record('user_private_key', $key);
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
2900 function delete_user_key($script, $userid) {
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) {
2918 if ($key = $DB->get_record('user_private_key', array('script' => $script, 'userid' => $userid,
2919 'instance' => $instance, 'iprestriction' => $iprestriction,
2920 'validuntil' => $validuntil))) {
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.
2941 if (defined('USER_KEY_LOGIN') && USER_KEY_LOGIN
=== true) {
2942 // Do not update user login time when using user key login.
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);
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
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
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
) {
2998 if (isguestuser($user)) {
3002 if (empty($user->firstname
) or empty($user->lastname
) or empty($user->email
) or over_bounce_threshold($user)) {
3007 if (empty($user->id
)) {
3008 // Strict mode can be used with existing accounts only.
3011 if (!profile_has_required_custom_fields_set($user->id
)) {
3014 if (isset($USER->id
) && isset($user->id
) && $USER->id
=== $user->id
) {
3015 $SESSION->fullysetupstrict
= time();
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) {
3031 if (empty($CFG->handlebounces
)) {
3035 if (empty($user->id
)) {
3036 // No real (DB) user, nothing to do here.
3040 // Set sensible defaults.
3041 if (empty($CFG->minbounces
)) {
3042 $CFG->minbounces
= 10;
3044 if (empty($CFG->bounceratio
)) {
3045 $CFG->bounceratio
= .20;
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
3065 function set_send_count($user, $reset=false) {
3068 if (empty($user->id
)) {
3069 // No real (DB) user, nothing to do here.
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';
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) {
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';
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
3114 function ismoving($courseid) {
3117 if (!empty($USER->activitycopy
)) {
3118 return ($USER->activitycopycourse
== $courseid);
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.
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.
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) {
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]);
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;
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) {
3222 if (file_exists("{$CFG->dirroot}/auth/$auth/auth.php")) {
3223 return is_readable("{$CFG->dirroot}/auth/$auth/auth.php");
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) {
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) {
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";
3265 * Returns array of active auth plugins.
3267 * @param bool $fix fix $CFG->auth if needed. Only set if logged in as admin.
3270 function get_enabled_auth_plugins($fix=false) {
3273 $default = array('manual', 'nologin');
3275 if (empty($CFG->auth
)) {
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.
3287 } else if (!exists_auth_plugin($authname)) {
3288 debugging(get_string('authpluginnotfound', 'debug', $authname));
3293 // Ideally only explicit interaction from a human admin should trigger a
3294 // change in auth config, see MDL-70424 for details.
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
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
3327 function is_restored_user($username) {
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() {
3341 $fieldarray = $DB->get_columns('user');
3342 unset($fieldarray['id']);
3343 $fieldarray = array_keys($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;
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);
3422 \core\event\user_created
::create_from_userid($newuser->id
)->trigger();
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) {
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) {
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
);
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);
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.
3473 if (empty($userauth->config
->{'field_updatelocal_' . $key}) ||
empty($userauth->config
->{'field_lock_' . $key})) {
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;
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);
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.
3525 'institution' => 255,
3526 'department' => 255,
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]));
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.');
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.');
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.');
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
(
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';
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;
3683 // Workaround for bulk deletes of users with the same email address.
3689 // 100 Character maximum, with a '.' character, and a 10-digit timestamp.
3690 100 - 1 - $delnamesuffixlength,
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.
3720 $event = \core\event\user_deleted
::create(
3722 'objectid' => $user->id
,
3723 'relateduserid' => $user->id
,
3724 'context' => $usercontext,
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);
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);
3749 * Retrieve the guest user object.
3751 * @return stdClass A {@link $USER} object
3753 function guest_user() {
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();
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
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(
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
;
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),
3826 'username' => $username,
3827 'reason' => $failurereason,
3831 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Invalid Login Token: $username ".$_SERVER['HTTP_USER_AGENT']);
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),
3842 'username' => $username,
3843 'reason' => $failurereason,
3849 $authsenabled = get_enabled_auth_plugins();
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)));
3867 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Suspended Login: $username ".$_SERVER['HTTP_USER_AGENT']);
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)));
3878 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Disabled Login: $username ".$_SERVER['HTTP_USER_AGENT']);
3881 $auths = array($auth);
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)));
3892 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Deleted Login: $username ".$_SERVER['HTTP_USER_AGENT']);
3896 // User does not exist.
3897 $auths = $authsenabled;
3898 $user = new stdClass();
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)));
3915 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Login lockout: $username ".$_SERVER['HTTP_USER_AGENT']);
3916 $SESSION->loginerrormsg
= get_string('accountlocked', 'admin');
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)) {
3932 // Before performing login actions, check if user still passes password policy, if admin setting is enabled.
3933 if (!empty($CFG->passwordpolicycheckonlogin
)) {
3935 $passed = check_password_policy($password, $errmsg, $user);
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'));
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.
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
);
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)));
3995 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Unknown user, can not create new accounts: $username ".
3996 $_SERVER['HTTP_USER_AGENT']);
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)));
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)));
4026 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Suspended Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4030 login_attempt_valid($user);
4031 $failurereason = AUTH_LOGIN_OK
;
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']);
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)));
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)));
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
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(
4089 'userid' => $USER->id
,
4090 'objectid' => $USER->id
,
4092 'username' => $USER->username
,
4093 'extrauserinfo' => $extrauserinfo
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
);
4143 set_user_preference('core_message_migrate_data', true, $USER->id
);
4147 if (isguestuser()) {
4148 // No need to continue when user is THE guest.
4153 // We can redirect to password change URL only in browser.
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);
4166 require_once($CFG->dirroot
. '/login/lib.php');
4167 $SESSION->wantsurl
= core_login_get_return_url();
4168 redirect($CFG->wwwroot
.'/login/change_password.php');
4171 throw new \
moodle_exception('nopasswordchangeforced', 'auth');
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.
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.
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.
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 {
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
);
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.');
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.
4265 if ($user->password
=== AUTH_PASSWORD_NOT_CACHED
) {
4266 // Internal password is not used at all, it can not validate.
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);
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);
4296 // If no peppered password was correct, the password is wrong.
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
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
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(
4368 #[\SensitiveParameter] string $password,
4369 bool $fasthash = false
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',
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
;
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
);
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;
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
);
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) {
4441 if (!$field ||
!$value) {
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;
4466 $fieldselect = "$field = :fieldval";
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";
4485 $constraints .= " AND id IN (SELECT id FROM {user} WHERE {$idsubselect})";
4488 // Get all the basic user data.
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) {
4498 // Return false when no records or multiple records were found.
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
= ' ';
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) {
4560 if (!empty($CFG->passwordpolicy
) && !isguestuser($user)) {
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);
4588 $errmsg .= '<div>'. $pluginerr .'</div>';
4594 if ($errmsg == '') {
4603 * When logging in, this function is run to set certain preferences for the current SESSION.
4605 function set_login_session_preferences() {
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) {
4628 if (is_object($courseorid)) {
4629 $courseid = $courseorid->id
;
4630 $course = $courseorid;
4632 $courseid = $courseorid;
4633 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
4637 $context = context_course
::instance($courseid);
4639 // Frontpage course can not be deleted!!
4640 if ($courseid == SITEID
) {
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
(
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,
4686 'shortname' => $course->shortname
,
4687 'fullname' => $course->fullname
,
4688 'idnumber' => $course->idnumber
4691 $event->add_record_snapshot('course', $course);
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.
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') {
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.
4791 foreach ($instances as $cm) {
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
);
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
));
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');
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)) {
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);
4902 filter_delete_all_for_context($coursecontext->id
);
4904 // Notes, you shall not pass!
4905 note_delete_all($course->id
);
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
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();
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);
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) {
5005 include_once($CFG->dirroot
.'/mod/'.$modname.'/lib.php');
5008 $params = array($timeshift, $courseid);
5009 foreach ($fields as $field) {
5010 $updatesql = "UPDATE {".$modname."}
5011 SET $field = $field + ?
5012 WHERE course=? AND $field<>0";
5014 $updatesql .= ' AND id=?';
5017 $return = $DB->execute($updatesql, $params) && $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) {
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
,
5044 'reset_options' => (array) $data
5047 $event = \core\event\course_reset_started
::create($eventparams);
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
);
5055 $data->timeshift
= 0;
5058 // Result array: component, item, error.
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
);
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
));
5091 // Clear course cache if changes made.
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]);
5175 $usersroles = enrol_get_course_users_roles($data->courseid
);
5176 foreach ($data->unenrol_users
as $withroleid) {
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
);
5186 // Without any role assigned at course context.
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
])) {
5201 $instance = $instances[$ue->enrolid
];
5202 $plugin = $plugins[$instance->enrol
];
5203 if (!$plugin->allow_unenrol($instance) and !$plugin->allow_unenrol_user($instance, $ue)) {
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]);
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
;
5221 if (!empty($data->unenrolled
)) {
5223 'component' => $componentstr,
5224 'item' => get_string('unenrol', 'enrol').' ('.count($data->unenrolled
).')',
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);
5272 debugging('Module '.$modname.' returned incorrect staus - must be an array!');
5275 $unsupportedmods[] = $mod;
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) {
5293 'component' => get_string('modulenameplural', $mod->name
),
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);
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);
5325 * Generate an email processing address.
5328 * @param string $modargs
5329 * @return string Returns email processing address
5331 function generate_email_processing_address($modid, $modargs) {
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) {
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?
5363 // Maybe more later?
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') {
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()) {
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
= "";
5402 $mailer->AltBody
= "";
5403 $mailer->ConfirmReadingTo
= "";
5405 $mailer->clearAllRecipients();
5406 $mailer->clearReplyTos();
5407 $mailer->clearAttachments();
5408 $mailer->clearCustomHeaders();
5412 $prevkeepalive = $mailer->SMTPKeepAlive
;
5413 get_mailer('flush');
5416 require_once($CFG->libdir
.'/phpmailer/moodle_phpmailer.php');
5417 $mailer = new moodle_phpmailer();
5421 if ($CFG->smtphosts
== 'qmail') {
5422 // Use Qmail system.
5425 } else if (empty($CFG->smtphosts
)) {
5426 // Use PHP mail() = sendmail.
5430 // Use SMTP directly.
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
;
5455 // Keep smtp session open after sending.
5456 if ($action == 'buffer') {
5457 if (!empty($CFG->smtpmaxbulk
)) {
5458 get_mailer('flush');
5460 if ($m->Mailer
== 'smtp') {
5461 $m->SMTPKeepAlive
= true;
5467 // Close smtp session, but continue buffering.
5468 if ($action == 'flush') {
5469 if (isset($mailer) and $mailer->Mailer
== 'smtp') {
5470 if (!empty($mailer->SMTPDebug
)) {
5473 $mailer->SmtpClose();
5474 if (!empty($mailer->SMTPDebug
)) {
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.
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) {
5501 if (empty($CFG->divertallemailsto
)) {
5505 if (empty($CFG->divertallemailsexcept
)) {
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)) {
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) {
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
);
5578 if (empty($user->email
)) {
5579 debugging('Can not send email to user without email: '.$user->id
, DEBUG_DEVELOPER
);
5583 if (!empty($user->deleted
)) {
5584 debugging('Can not send email to deleted user: '.$user->id
, DEBUG_DEVELOPER
);
5588 if (defined('BEHAT_SITE_RUNNING')) {
5589 // Fake email sending in behat.
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
);
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
)) {
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.");
5616 if (over_bounce_threshold($user)) {
5617 debugging("email_to_user: User $user->id (".fullname($user).") is over bounce threshold! Not sending.");
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:]]*)%",
5640 $messagehtml = preg_replace_callback("%href=[\"'`]($CFG->wwwroot[\w_:\?=#&@/;.~-]*)[\"'`]%",
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);
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.
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));
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));
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);
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') {
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');
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";
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');
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));
5844 $CFG->localcachedir
,
5846 $CFG->localrequestdir
,
5849 // Set addpath to 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) {
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
;
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
)) {
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
,
5934 'subject' => $subject,
5935 'message' => $messagetext,
5936 'errorinfo' => $mail->ErrorInfo
5941 mtrace('Error: lib/moodlelib.php email_to_user(): '.$mail->ErrorInfo
);
5943 if (!empty($mail->SMTPDebug
)) {
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) {
5960 if (!isset($CFG->allowedemaildomains
) ||
empty(trim($CFG->allowedemaildomains
))) {
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)))) {
5977 * Generate a signoff for emails based on support settings
5981 function generate_email_signoff() {
5982 global $CFG, $OUTPUT;
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";
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) {
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
;
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) {
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.");
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) {
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) {
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) {
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) {
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) {
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".
6213 } else if (strpos(strrev($email), strrev('@'.$allowedpattern)) === 0) {
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) {
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
);
6244 * Returns local file storage instance
6246 * @return ?file_storage
6248 function get_file_storage($reset = false) {
6262 require_once("$CFG->libdir/filelib.php");
6264 $fs = new file_storage();
6270 * Returns local file storage instance
6272 * @return file_browser
6274 function get_file_browser() {
6283 require_once("$CFG->libdir/filelib.php");
6285 $fb = new file_browser();
6291 * Returns file packer
6293 * @param string $mimetype default application/zip
6294 * @return file_packer|false
6296 function get_file_packer($mimetype='application/zip') {
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';
6311 case 'application/x-gzip' :
6312 $classname = 'tgz_packer';
6315 case 'application/vnd.moodle.backup':
6316 $classname = 'mbz_packer';
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)) {
6339 if (is_uploaded_file($newfile['tmp_name']) and $newfile['size'] > 0) {
6340 return $newfile['tmp_name'];
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')) {
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,
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
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.
6447 function get_max_upload_sizes($sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $custombytes = null) {
6450 if (!$maxsize = get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes)) {
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);
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
);
6509 $params = (object) array('contextname' => $limitlevel, 'displaysize' => $displaysize);
6510 $filesize = array('0' => get_string('uploadlimitwithsize', 'core', $params)) +
$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) {
6537 if (!$getdirs and !$getfiles) { // Nothing to show.
6541 if (!is_dir($rootdir)) { // Must be a directory.
6545 if (!$dir = opendir($rootdir)) { // Can't open it for some reason.
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)) {
6558 $fullfile = $rootdir .'/'. $file;
6559 if (filetype($fullfile) == 'dir') {
6564 $subdirs = get_directory_list($fullfile, $excludefiles, $descend, $getdirs, $getfiles);
6565 foreach ($subdirs as $subdir) {
6566 $dirs[] = $file .'/'. $subdir;
6569 } else if ($getfiles) {
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='') {
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);
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.
6608 if (!$dir = @opendir
($rootdir)) {
6609 // Can't open it for some reason.
6615 while (false !== ($file = readdir($dir))) {
6616 $firstchar = substr($file, 0, 1);
6617 if ($firstchar == '.' or $file == 'CVS' or $file == $excludefile) {
6620 $fullfile = $rootdir .'/'. $file;
6621 if (filetype($fullfile) == 'dir') {
6622 $size +
= get_directory_size($fullfile, $excludefile);
6624 $size +
= filesize($fullfile);
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 {
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) {
6677 $magnitude = floor(log($size, 1024));
6678 $magnitude = max(0, min(5, $magnitude));
6681 throw new coding_exception('Unknown fixed units value: ' . $fixedunits);
6684 // Special case for magnitude 0 (bytes) - never use decimal places.
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
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
;
6748 // Just in case this slipped in from somewhere by accident.
6749 $return = str_replace('_utf8', '', $return);
6755 * Fix the current language to the given language code.
6757 * @param string $lang The language code to use.
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");
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
)) {
6778 } else if (isset($CFG->lang
)) {
6779 set_config('lang', $lang);
6783 $fixglobal->$fixlang = $lang;
6788 * Returns parent language of current active language if defined
6791 * @param string $lang null means current language
6794 function get_parent_language($lang=null) {
6796 $parentlang = get_string_manager()->get_string('parentlanguage', 'langconfig', null, $lang);
6798 if ($parentlang === 'en') {
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) {
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;
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.
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) {
6840 static $singleton = null;
6845 if ($singleton === null) {
6846 if (empty($CFG->early_install_lang
)) {
6848 $transaliases = array();
6849 if (empty($CFG->langlist
)) {
6850 $translist = array();
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);
6875 debugging('Unable to instantiate custom string manager: class '.$classname.
6876 ' does not implement the core_string_manager interface.');
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);
6887 $singleton = new core_string_manager_install();
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}'
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'
6914 * $string['course'] = 'Course';
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'
6920 * $mystring = '<strong>'. get_string('course') .'</strong>';
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
6927 * $string['typecourse'] = 'Course event';
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):
6933 * $mystring = '<h1>'. get_string('typecourse', 'calendar') .'</h1>';
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}
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) {
6969 // If the lazy load argument has been supplied return a lang_string object
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]) {
6994 $component = $componentpath[1];
6998 $component = 'block_'.$componentpath[1];
7001 $component = 'enrol_'.$componentpath[1];
7004 $component = 'format_'.$componentpath[1];
7007 $component = 'grade'.$componentpath[1].'_'.$componentpath[2];
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 . '}';
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);
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/>
7044 * print_string('course');
7048 * Example usage of this function when the string is not in the moodle.php file:<br/>
7051 * print_string('typecourse', 'calendar');
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() {
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');
7089 * Returns a list of valid and compatible themes
7093 function get_list_of_themes() {
7098 if (!empty($CFG->themelist
)) { // Use admin's list of themes.
7099 $themelist = explode(',', $CFG->themelist
);
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');
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();
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) {
7151 $notselectable = ['martin', 'egg'];
7153 if (empty($CFG->emoticons
)) {
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
);
7165 foreach ($emoticons as $index => $emote) {
7166 if (in_array($emote->altidentifier
, $notselectable)) {
7168 unset($emoticons[$index]);
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
);
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
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)) {
7220 * Returns default set of emoticons supported by Moodle
7222 * @return array of sdtClasses
7224 public function default_emoticons() {
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}
7269 protected function prepare_emoticon_object($text, $imagename, $altidentifier = null,
7270 $altcomponent = 'core_pix', $imagecomponent = 'core') {
7271 return (object)array(
7273 'imagename' => $imagename,
7274 'imagecomponent' => $imagecomponent,
7275 'altidentifier' => $altidentifier,
7276 'altcomponent' => $altcomponent,
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, '');
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
7313 function endecrypt ($pwd, $data, $case) {
7315 if ($case == 'de') {
7316 $data = urldecode($data);
7321 $pwdlength = strlen($pwd);
7323 for ($i = 0; $i <= 255; $i++
) {
7324 $key[$i] = ord(substr($pwd, ($i %
$pwdlength), 1));
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;
7342 for ($i = 0; $i < strlen($data); $i++
) {
7343 $a = ($a +
1) %
256;
7344 $j = ($j +
$box[$a]) %
256;
7346 $box[$a] = $box[$j];
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));
7356 $cipher = urlencode($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') {
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])) {
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;
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) {
7440 if (during_initial_install() ||
isset($CFG->upgraderunning
)) {
7441 // API functions _must not_ be called during an installation or upgrade.
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]);
7455 $hookmessage = count($hooks) == 1 ?
reset($hooks) : 'one of ' . implode(', ', $hooks);
7457 "Callback $plugincallback in $component component should be migrated to new " .
7458 "hook callback for $hookmessage",
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);
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.
7496 // Cache might be out of sync with the codebase, skip the plugin if it is not available.
7497 if (empty($allplugins[$plugin])) {
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.
7512 // Check if the function still exists in the file.
7513 if ($include && !function_exists($function)) {
7520 // If the cache is dirty, we should fall through and let it rebuild.
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])) {
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='') {
7598 if (empty($basedir)) {
7599 $basedir = $CFG->dirroot
.'/'. $directory;
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
);
7616 $ignorelist = array_flip(array_filter([
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
);
7633 while (false !== ($dir = readdir($dirhandle))) {
7634 if (strpos($dir, '.') === 0) {
7635 // Ignore directories starting with .
7636 // These are treated as hidden directories.
7639 if (array_key_exists($dir, $ignorelist)) {
7640 // This directory features on the ignore list.
7643 if (filetype($basedir .'/'. $dir) != 'dir') {
7648 closedir($dirhandle);
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
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
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.
7696 $hookmessage = count($hooks) == 1 ?
reset($hooks) : 'one of ' . implode(', ', $hooks);
7698 "Callback $function in $component component should be migrated to new hook callback for $hookmessage",
7704 // Function exists, so just return function result.
7705 $ret = call_user_func_array($functionname, $params);
7706 if (is_null($ret)) {
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
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);
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)) {
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)) {
7781 if (!method_exists($classname, $methodname)) {
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.
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",
7805 $result = call_user_func_array($fullfunction, $params);
7807 if (null === $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) {
7828 if ($type === 'mod' and $name === 'NEWMODULE') {
7829 // Somebody forgot to rename the module template.
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);
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';
7853 if (!$path = core_component
::get_plugin_directory($type, $name)) {
7854 // Non existent plugin type.
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.
7873 // Plugin does not care, so use 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.
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.
7898 function moodle_needs_upgrading($checkupgradeflag = true) {
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) {
7913 if (empty($CFG->version
)) {
7917 // There is no need to purge plugininfo caches here because
7918 // these caches are not used during upgrade and they are purged after
7921 if (empty($CFG->allversionshash
)) {
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) {
7947 require($CFG->dirroot
.'/version.php');
7948 if (empty($release)) {
7953 if (empty($CFG->release
)) {
7956 $release = $CFG->release
;
7959 if (preg_match('/^[0-9]+\.[0-9]+/', $release, $matches)) {
7969 * Gets the system locale
7971 * @return string Retuns the current locale.
7973 function moodle_getlocale() {
7976 // Fetch the correct locale based on ostype.
7977 if ($CFG->ostype
== 'WINDOWS') {
7978 $stringtofetch = 'localewin';
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
7994 * @param string $locale Can be used to force a locale
7996 function moodle_setlocale($locale='') {
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;
8007 $currentlocale = moodle_getlocale();
8010 // Do nothing if locale already set up.
8011 if ($oldlocale == $currentlocale) {
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);
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.
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.
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.
8126 function random_string($length=15) {
8127 $randombytes = random_bytes($length);
8128 $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
8129 $pool .= 'abcdefghijklmnopqrstuvwxyz';
8130 $pool .= '0123456789';
8131 $poollen = strlen($pool);
8133 for ($i = 0; $i < $length; $i++
) {
8134 $rand = ord($randombytes[$i]);
8135 $string .= substr($pool, ($rand%
($poollen)), 1);
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
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);
8158 for ($i = 0; $i < $length; $i++
) {
8159 $rand = ord($randombytes[$i]);
8160 $string .= $pool[($rand%
$poollen)];
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
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) {
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);
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(
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(
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(
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(
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) {
8245 $entitieslength +
= core_text
::strlen($entity[0]);
8247 // No more characters left.
8252 $breakpos = $left +
$entitieslength;
8254 // If the words shouldn't be cut in the middle...
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 === ' ') {
8262 } else if (strlen($char) > 2) {
8263 // Chinese/Japanese/Korean text can be truncated at any UTF-8 character boundary.
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.
8282 $truncate .= $linematchings[2];
8283 $totallength +
= $contentlength;
8286 // If the maximum length is reached, get off the loop.
8287 if ($totallength >= $ideal) {
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
);
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]-->';
8315 $truncate .= '</' . $tag . '>';
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;
8345 $hash = empty($includehash) ?
'' : ' - ' . substr(sha1($filename), 0, 10);
8346 $shortened = core_text
::substr($filename, 0, $length - strlen($hash)) . $hash;
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) {
8363 $result = array_reduce($path, function($carry, $singlepath) use ($length, $includehash) {
8364 $carry[] = shorten_filename($singlepath, $length, $includehash);
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
8379 function getweek ($startdate, $thedate) {
8380 if ($thedate < $startdate) {
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.
8396 function generate_password($maxlen=10) {
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;
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 .
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)) {
8473 $separator = get_string('decsep', 'langconfig');
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);
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 == '') {
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)) {
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
8525 function swapshuffle($array) {
8527 $last = count($array) - 1;
8528 for ($i = 0; $i <= $last; $i++
) {
8529 $from = rand(0, $last);
8531 $array[$i] = $array[$from];
8532 $array[$from] = $curr;
8538 * Like {@link swapshuffle()}, but works on associative arrays
8540 * @param array $array The associative array to be rearranged
8543 function swapshuffle_assoc($array) {
8545 $newarray = array();
8546 $newkeys = swapshuffle(array_keys($array));
8548 foreach ($newkeys as $newkey) {
8549 $newarray[$newkey] = $array[$newkey];
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
8565 function draw_rand_array($array, $draws) {
8569 $last = count($array);
8571 if ($draws > $last) {
8575 while ($draws > 0) {
8578 $keys = array_keys($array);
8579 $rand = rand(0, $last);
8581 $return[$keys[$rand]] = $array[$keys[$rand]];
8582 unset($array[$keys[$rand]]);
8591 * Calculate the difference between two microtimes
8593 * @param string $a The first Microtime
8594 * @param string $b The second Microtime
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);
8621 * Creates an array that represents all the current grades that
8622 * can be chosen using the given grading type.
8625 * are scales, zero is no grade, and positive numbers are maximum
8628 * @todo Finish documenting this function or better deprecated this completely!
8630 * @param int $gradingtype
8633 function make_grades_menu($gradingtype) {
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;
8651 * make_unique_id_code
8653 * @todo Finish documenting this function
8656 * @param string $extra Extra string to append to the end of the code
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);
8677 return $hostname .'+'. $date .'+'. $random .'+'. $extra;
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
8700 function address_in_subnet($addr, $subnetstr, $checkallzeros = false) {
8702 if ($addr == '0.0.0.0' && !$checkallzeros) {
8705 $subnets = explode(',', $subnetstr);
8707 $addr = trim($addr);
8708 $addr = cleanremoteaddr($addr, false); // Normalise.
8709 if ($addr === null) {
8712 $addrparts = explode(':', $addr);
8714 $ipv6 = strpos($addr, ':');
8716 foreach ($subnets as $subnet) {
8717 $subnet = trim($subnet);
8718 if ($subnet === '') {
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.
8733 if (strpos($ip, ':') !== false) {
8738 if ($mask > 128 or $mask < 0) {
8739 continue; // Nonsense.
8742 return true; // Any address.
8745 if ($ip === $addr) {
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)) {
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)) {
8772 if ($mask > 32 or $mask < 0) {
8773 continue; // Nonsense.
8779 if ($ip === $addr) {
8784 $mask = 0xffffffff << (32 - $mask);
8785 if (((ip2long($addr) & $mask) == (ip2long($ip) & $mask))) {
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) {
8797 if (strpos($subnet, ':') !== false) {
8802 $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise.
8803 if ($ipstart === null) {
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) {
8814 $ipnet = implode(':', $ipparts);
8815 if (strpos($addr, $ipnet) !== 0) {
8818 $ipparts = explode(':', $ipend);
8819 $end = hexdec($ipparts[7]);
8821 $addrend = hexdec($addrparts[7]);
8823 if (($addrend >= $start) and ($addrend <= $end)) {
8832 $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise.
8833 if ($ipstart === null) {
8836 $ipparts = explode('.', $ipstart);
8837 $ipparts[3] = trim($parts[1]);
8838 $ipend = cleanremoteaddr(implode('.', $ipparts), false); // Normalise.
8839 if ($ipend === null) {
8843 if ((ip2long($addr) >= ip2long($ipstart)) and (ip2long($addr) <= ip2long($ipend))) {
8849 // 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx.
8850 if (strpos($subnet, ':') !== false) {
8855 $parts = explode(':', $subnet);
8856 $count = count($parts);
8857 if ($parts[$count-1] === '') {
8858 unset($parts[$count-1]); // Trim trailing :'s.
8860 $subnet = implode('.', $parts);
8862 $isip = cleanremoteaddr($subnet, false); // Normalise.
8863 if ($isip !== null) {
8864 if ($isip === $addr) {
8868 } else if ($count > 8) {
8871 $zeros = array_fill(0, 8-$count, '0');
8872 $subnet = $subnet.':'.implode(':', $zeros).'/'.($count*16);
8873 if (address_in_subnet($addr, $subnet)) {
8882 $parts = explode('.', $subnet);
8883 $count = count($parts);
8884 if ($parts[$count-1] === '') {
8885 unset($parts[$count-1]); // Trim trailing .
8887 $subnet = implode('.', $parts);
8890 $subnet = cleanremoteaddr($subnet, false); // Normalise.
8891 if ($subnet === $addr) {
8895 } else if ($count > 4) {
8898 $zeros = array_fill(0, 4-$count, '0');
8899 $subnet = $subnet.'.'.implode('.', $zeros).'/'.($count*8);
8900 if (address_in_subnet($addr, $subnet)) {
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) {
8921 if (isset($CFG->mtrace_wrapper
) && function_exists($CFG->mtrace_wrapper
)) {
8922 $fn = $CFG->mtrace_wrapper
;
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);
8932 echo $string . $eol;
8938 // Delay to keep message on user's screen in case of subsequent redirect.
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);
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
8979 function remoteip_in_list($list) {
8980 $clientip = getremoteaddr(null);
8983 // Ensure access on cli.
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') {
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
;
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) {
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];
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;
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) {
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.
9084 if (strpos($addr, '::') === false) {
9085 return null; // Malformed.
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) {
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);
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) {
9122 $compressed = preg_replace('/^(0:){2,7}/', '::', $result, 1);
9123 if ($compressed !== $result) {
9127 $compressed = preg_replace('/(:0){2,6}:/', '::', $result, 1);
9128 if ($compressed !== $result) {
9135 // First get all things that look like IPv4 addresses.
9137 if (!preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $addr, $parts)) {
9142 foreach ($parts as $key => $match) {
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
9184 function bounded_number($min, $value, $max) {
9185 if ($value < $min) {
9188 if ($value > $max) {
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)) {
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.
9217 function get_performance_info() {
9218 global $CFG, $PERF, $DB, $PAGE;
9221 $info['txt'] = me() . ' '; // Holds log-friendly representation.
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.
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]);
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];
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>';
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'];
9352 $table->align
= ['left', 'left', 'left', 'right', 'right', 'left', 'right', 'right', 'right', 'right'];
9354 $text = 'Caches used (hits/misses/sets): ';
9360 // We want to align static caches into their own column.
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);
9373 while ($storec++
< ($maxstores - 2)) {
9374 if ($storec == ($maxstores - 2)) {
9375 $table->head
[] = get_string('mappingfinal', 'cache');
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';
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>';
9398 case cache_store
::MODE_SESSION
:
9399 $modeclass = 'session';
9400 $mode = ' <span title="session cache">Ses</span>';
9402 case cache_store
::MODE_REQUEST
:
9403 $modeclass = 'request';
9404 $mode = ' <span title="request cache">Req</span>';
9407 $row = [$mode, $definition];
9409 $text .= "$definition {";
9412 foreach ($details['stores'] as $store => $data) {
9414 if ($storec == 0 && $store !== cache_store
::STATIC_ACCEL
) {
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';
9429 $cachestoreclass = 'hihits';
9431 $text .= "$store($data[hits]/$data[misses]/$data[sets]) ";
9432 $cell = new html_table_cell($store);
9433 $cell->attributes
= ['class' => $cachestoreclass];
9435 $cell = new html_table_cell($data['hits']);
9436 $cell->attributes
= ['class' => $cachestoreclass];
9438 $cell = new html_table_cell($data['misses']);
9439 $cell->attributes
= ['class' => $cachestoreclass];
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];
9448 if ($data['hits'] ||
$data['sets']) {
9449 if ($data['iobytes'] === cache_store
::IO_BYTES_NOT_SUPPORTED
) {
9452 $size = display_size($data['iobytes'], 1, 'KB');
9453 if ($data['iobytes'] >= 10 * 1024) {
9454 $cachestoreclass = ' bg-warning text-dark';
9460 $cell = new html_table_cell($size);
9461 $cell->attributes
= ['class' => $cachestoreclass];
9466 while ($storec++
< $maxstores) {
9475 $table->data
[] = $row;
9478 $html .= html_writer
::table($table);
9480 // Now lets also show sub totals for each cache store.
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'];
9506 $table->align
= ['left', 'left', 'right', 'right', 'right', 'right'];
9508 ksort($storetotals);
9510 foreach ($storetotals as $store => $data) {
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';
9517 $cachestoreclass = 'hihits';
9519 $cell = new html_table_cell($store);
9520 $cell->attributes
= ['class' => $cachestoreclass];
9522 $cell = new html_table_cell($data['class']);
9523 $cell->attributes
= ['class' => $cachestoreclass];
9525 $cell = new html_table_cell($data['hits']);
9526 $cell->attributes
= ['class' => $cachestoreclass];
9528 $cell = new html_table_cell($data['misses']);
9529 $cell->attributes
= ['class' => $cachestoreclass];
9531 $cell = new html_table_cell($data['sets']);
9532 $cell->attributes
= ['class' => $cachestoreclass];
9534 if ($data['hits'] ||
$data['sets']) {
9535 if ($data['iobytes']) {
9536 $size = display_size($data['iobytes'], 1, 'KB');
9543 $cell = new html_table_cell($size);
9544 $cell->attributes
= ['class' => $cachestoreclass];
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'])) {
9556 get_string('total'),
9558 $storetotal['hits'],
9559 $storetotal['misses'],
9560 $storetotal['sets'],
9563 $table->data
[] = $row;
9565 $html .= html_writer
::table($table);
9567 $info['cachesused'] = "$hits / $misses / $sets";
9568 $info['html'] .= $html;
9569 $info['txt'] .= $text.'. ';
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'];
9583 $text = 'Locks (waited/obtained/held):';
9584 foreach ($PERF->locks
as $locktiming) {
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'];
9596 $text .= $roundedtime . '/';
9598 // Show a tick or cross for success.
9599 $row[] = $locktiming->success ?
'✓' : '❌';
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'];
9610 $text .= $roundedtime;
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>';
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;
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))) {
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)) {
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);
9678 // If the rename was successful then delete the $tempdir instead.
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)) {
9689 while (false!==($item = readdir($handle))) {
9690 if ($item != '.' && $item != '..') {
9691 if (is_dir($dir.'/'.$item)) {
9692 $result = remove_dir($dir.'/'.$item) && $result;
9694 $result = unlink($dir.'/'.$item) && $result;
9700 clearstatcache(); // Make sure file stat cache is properly invalidated.
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.
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 );
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()
9739 function convert_to_array($var) {
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);
9748 // Simple values are untouched.
9749 $result[$key] = $value;
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.
9769 $scriptpath = $CFG->customscripts
. $SCRIPT;
9771 // Check the custom script exists.
9772 if (file_exists($scriptpath) and is_file($scriptpath)) {
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) {
9789 if (!isset($CFG->mnet_localhost_id
)) {
9790 include_once($CFG->dirroot
. '/mnet/lib.php');
9791 $env = new mnet_environment();
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.
9811 if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { // There isn't list of browser langs, nothing to do.
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.
9822 foreach ($rawlangs as $lang) {
9823 if (strpos($lang, ';') === false) {
9824 $langs[(string)$order] = $lang;
9825 $order = $order-0.01;
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
)) {
9845 $SESSION->lang
= $lang;
9847 // We have finished. Go out.
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 ) {
9866 if (empty($CFG->proxyhost
) or empty($CFG->proxybypass
)) {
9870 // Get the host part out of the url.
9871 if (!$host = parse_url( $url, PHP_URL_HOST
)) {
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) {
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'])) {
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
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
9927 function object_array_unique($array, $keepkeyassoc = true) {
9928 $duplicatekeys = array();
9931 foreach ($array as $key => $val) {
9932 // Convert objects to arrays, in_array() does not support objects.
9933 if (is_object($val)) {
9937 if (!in_array($val, $tmp)) {
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
9957 function is_primary_admin($userid) {
9958 $primaryadmin = get_admin();
9960 if ($userid == $primaryadmin->id
) {
9968 * Returns the site identifier
9970 * @return string $CFG->siteidentifier, first making sure it is properly initialised.
9972 function get_site_identifier() {
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']);
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
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.
10000 $consecutivecount = 1;
10001 foreach (str_split($password) as $char) {
10002 if ($char != $previouschar) {
10003 $consecutivecount = 1;
10005 $consecutivecount++
;
10006 if ($consecutivecount > $maxchars) {
10007 return false; // Check failed already.
10011 $previouschar = $char;
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);
10025 * $callback = partial(array('someclass', 'somestaticmethod'), $arg1, $arg2);
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
10045 public $values = array();
10046 /** @var string The function to call as a callback. */
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.
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() {
10081 require_once($CFG->dirroot
. '/mnet/lib.php');
10082 static $instance = null;
10083 if (empty($instance)) {
10084 $instance = new mnet_environment();
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'));
10101 global $MNET_REMOTE_CLIENT;
10102 if (isset($MNET_REMOTE_CLIENT)) {
10103 return $MNET_REMOTE_CLIENT;
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) {
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() {
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
;
10156 return $defaultpage;
10158 } else if ($CFG->defaulthomepage
== HOMEPAGE_MYCOURSES
) {
10159 return HOMEPAGE_MYCOURSES
;
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 {
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) {
10193 if (!empty($CFG->courselistshortnames
)) {
10194 if (!($course instanceof stdClass
)) {
10195 $course = (object)convert_to_array($course);
10197 return get_string('courseextendednamedisplay', '', $course);
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)) {
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)) {
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
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');
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
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
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!)
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.
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.
10358 if (is_scalar($a)) {
10360 } else if ($a instanceof lang_string
) {
10361 $this->a
= $a->out();
10362 } else if (is_object($a) or is_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();
10373 $this->a
[$key] = '';
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
10406 * @throws coding_exception
10408 protected function get_string() {
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
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
10452 public function __toString() {
10453 return $this->get_string();
10457 * Magic __set_state method used for var_export
10459 * @param array $array
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'];
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.
10476 public function __sleep() {
10477 $this->get_string();
10478 $this->forcedstring
= true;
10479 return array('forcedstring', 'string', 'lang');
10483 * Returns the identifier.
10487 public function get_identifier() {
10488 return $this->identifier
;
10492 * Returns the component.
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)) {
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
10530 function site_is_public() {
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)) {
10542 } else if (\core\ip_utils
::is_ip_address($host) && !ip_is_public($host)) {
10544 } else if (($address = \core\ip_utils
::get_ip_address($host)) && !ip_is_public($address)) {
10554 * Validates user's password length.
10556 * @param string $password
10557 * @param int $pepperlength The length of the used peppers
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
10578 function moodle_array_keys_filter(array $array, mixed $filter, bool $strict = false): array {
10579 return array_keys(array_filter(
10581 function($value, $key) use ($filter, $strict): bool {
10583 return $value === $filter;
10585 return $value == $filter;
10587 ARRAY_FILTER_USE_BOTH
,