MDL-70483 forum: Final deprecation of get_forum_discussions_paginated
[moodle.git] / lib / moodlelib.php
blob89cb766244be02718f97e8f5e09f3154e067c5e9
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 /**
18 * moodlelib.php - Moodle main library
20 * Main library file of miscellaneous general-purpose Moodle functions.
21 * Other main libraries:
22 * - weblib.php - functions that produce web output
23 * - datalib.php - functions that access the database
25 * @package core
26 * @subpackage lib
27 * @copyright 1999 onwards Martin Dougiamas http://dougiamas.com
28 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
31 defined('MOODLE_INTERNAL') || die();
33 // CONSTANTS (Encased in phpdoc proper comments).
35 // Date and time constants.
36 /**
37 * Time constant - the number of seconds in a year
39 define('YEARSECS', 31536000);
41 /**
42 * Time constant - the number of seconds in a week
44 define('WEEKSECS', 604800);
46 /**
47 * Time constant - the number of seconds in a day
49 define('DAYSECS', 86400);
51 /**
52 * Time constant - the number of seconds in an hour
54 define('HOURSECS', 3600);
56 /**
57 * Time constant - the number of seconds in a minute
59 define('MINSECS', 60);
61 /**
62 * Time constant - the number of minutes in a day
64 define('DAYMINS', 1440);
66 /**
67 * Time constant - the number of minutes in an hour
69 define('HOURMINS', 60);
71 // Parameter constants - every call to optional_param(), required_param()
72 // or clean_param() should have a specified type of parameter.
74 // We currently include \core\param manually here to avoid broken upgrades.
75 // This may change after the next LTS release as LTS releases require the previous LTS release.
76 require_once(__DIR__ . '/classes/deprecation.php');
77 require_once(__DIR__ . '/classes/deprecated.php');
78 require_once(__DIR__ . '/classes/param.php');
80 /**
81 * PARAM_ALPHA - contains only English ascii letters [a-zA-Z].
83 define('PARAM_ALPHA', \core\param::ALPHA->value);
85 /**
86 * PARAM_ALPHAEXT the same contents as PARAM_ALPHA (English ascii letters [a-zA-Z]) plus the chars in quotes: "_-" allowed
87 * NOTE: originally this allowed "/" too, please use PARAM_SAFEPATH if "/" needed
89 define('PARAM_ALPHAEXT', \core\param::ALPHAEXT->value);
91 /**
92 * PARAM_ALPHANUM - expected numbers 0-9 and English ascii letters [a-zA-Z] only.
94 define('PARAM_ALPHANUM', \core\param::ALPHANUM->value);
96 /**
97 * PARAM_ALPHANUMEXT - expected numbers 0-9, letters (English ascii letters [a-zA-Z]) and _- only.
99 define('PARAM_ALPHANUMEXT', \core\param::ALPHANUMEXT->value);
102 * PARAM_AUTH - actually checks to make sure the string is a valid auth plugin
104 define('PARAM_AUTH', \core\param::AUTH->value);
107 * PARAM_BASE64 - Base 64 encoded format
109 define('PARAM_BASE64', \core\param::BASE64->value);
112 * PARAM_BOOL - converts input into 0 or 1, use for switches in forms and urls.
114 define('PARAM_BOOL', \core\param::BOOL->value);
117 * PARAM_CAPABILITY - A capability name, like 'moodle/role:manage'. Actually
118 * checked against the list of capabilities in the database.
120 define('PARAM_CAPABILITY', \core\param::CAPABILITY->value);
123 * PARAM_CLEANHTML - cleans submitted HTML code. Note that you almost never want
124 * to use this. The normal mode of operation is to use PARAM_RAW when receiving
125 * the input (required/optional_param or formslib) and then sanitise the HTML
126 * using format_text on output. This is for the rare cases when you want to
127 * sanitise the HTML on input. This cleaning may also fix xhtml strictness.
129 define('PARAM_CLEANHTML', \core\param::CLEANHTML->value);
132 * PARAM_EMAIL - an email address following the RFC
134 define('PARAM_EMAIL', \core\param::EMAIL->value);
137 * PARAM_FILE - safe file name, all dangerous chars are stripped, protects against XSS, SQL injections and directory traversals
139 define('PARAM_FILE', \core\param::FILE->value);
142 * PARAM_FLOAT - a real/floating point number.
144 * Note that you should not use PARAM_FLOAT for numbers typed in by the user.
145 * It does not work for languages that use , as a decimal separator.
146 * Use PARAM_LOCALISEDFLOAT instead.
148 define('PARAM_FLOAT', \core\param::FLOAT->value);
151 * PARAM_LOCALISEDFLOAT - a localised real/floating point number.
152 * This is preferred over PARAM_FLOAT for numbers typed in by the user.
153 * Cleans localised numbers to computer readable numbers; false for invalid numbers.
155 define('PARAM_LOCALISEDFLOAT', \core\param::LOCALISEDFLOAT->value);
158 * PARAM_HOST - expected fully qualified domain name (FQDN) or an IPv4 dotted quad (IP address)
160 define('PARAM_HOST', \core\param::HOST->value);
163 * PARAM_INT - integers only, use when expecting only numbers.
165 define('PARAM_INT', \core\param::INT->value);
168 * PARAM_LANG - checks to see if the string is a valid installed language in the current site.
170 define('PARAM_LANG', \core\param::LANG->value);
173 * PARAM_LOCALURL - expected properly formatted URL as well as one that refers to the local server itself. (NOT orthogonal to the
174 * others! Implies PARAM_URL!)
176 define('PARAM_LOCALURL', \core\param::LOCALURL->value);
179 * PARAM_NOTAGS - all html tags are stripped from the text. Do not abuse this type.
181 define('PARAM_NOTAGS', \core\param::NOTAGS->value);
184 * PARAM_PATH - safe relative path name, all dangerous chars are stripped, protects against XSS, SQL injections and directory
185 * traversals note: the leading slash is not removed, window drive letter is not allowed
187 define('PARAM_PATH', \core\param::PATH->value);
190 * PARAM_PEM - Privacy Enhanced Mail format
192 define('PARAM_PEM', \core\param::PEM->value);
195 * PARAM_PERMISSION - A permission, one of CAP_INHERIT, CAP_ALLOW, CAP_PREVENT or CAP_PROHIBIT.
197 define('PARAM_PERMISSION', \core\param::PERMISSION->value);
200 * PARAM_RAW specifies a parameter that is not cleaned/processed in any way except the discarding of the invalid utf-8 characters
202 define('PARAM_RAW', \core\param::RAW->value);
205 * PARAM_RAW_TRIMMED like PARAM_RAW but leading and trailing whitespace is stripped.
207 define('PARAM_RAW_TRIMMED', \core\param::RAW_TRIMMED->value);
210 * PARAM_SAFEDIR - safe directory name, suitable for include() and require()
212 define('PARAM_SAFEDIR', \core\param::SAFEDIR->value);
215 * PARAM_SAFEPATH - several PARAM_SAFEDIR joined by "/", suitable for include() and require(), plugin paths
216 * and other references to Moodle code files.
218 * This is NOT intended to be used for absolute paths or any user uploaded files.
220 define('PARAM_SAFEPATH', \core\param::SAFEPATH->value);
223 * PARAM_SEQUENCE - expects a sequence of numbers like 8 to 1,5,6,4,6,8,9. Numbers and comma only.
225 define('PARAM_SEQUENCE', \core\param::SEQUENCE->value);
228 * PARAM_TAG - one tag (interests, blogs, etc.) - mostly international characters and space, <> not supported
230 define('PARAM_TAG', \core\param::TAG->value);
233 * PARAM_TAGLIST - list of tags separated by commas (interests, blogs, etc.)
235 define('PARAM_TAGLIST', \core\param::TAGLIST->value);
238 * PARAM_TEXT - general plain text compatible with multilang filter, no other html tags. Please note '<', or '>' are allowed here.
240 define('PARAM_TEXT', \core\param::TEXT->value);
243 * PARAM_THEME - Checks to see if the string is a valid theme name in the current site
245 define('PARAM_THEME', \core\param::THEME->value);
248 * PARAM_URL - expected properly formatted URL. Please note that domain part is required, http://localhost/ is not accepted but
249 * http://localhost.localdomain/ is ok.
251 define('PARAM_URL', \core\param::URL->value);
254 * PARAM_USERNAME - Clean username to only contains allowed characters. This is to be used ONLY when manually creating user
255 * accounts, do NOT use when syncing with external systems!!
257 define('PARAM_USERNAME', \core\param::USERNAME->value);
260 * PARAM_STRINGID - used to check if the given string is valid string identifier for get_string()
262 define('PARAM_STRINGID', \core\param::STRINGID->value);
264 // DEPRECATED PARAM TYPES OR ALIASES - DO NOT USE FOR NEW CODE.
266 * PARAM_CLEAN - obsoleted, please use a more specific type of parameter.
267 * It was one of the first types, that is why it is abused so much ;-)
268 * @deprecated since 2.0
270 define('PARAM_CLEAN', \core\param::CLEAN->value);
273 * PARAM_INTEGER - deprecated alias for PARAM_INT
274 * @deprecated since 2.0
276 define('PARAM_INTEGER', \core\param::INT->value);
279 * PARAM_NUMBER - deprecated alias of PARAM_FLOAT
280 * @deprecated since 2.0
282 define('PARAM_NUMBER', \core\param::FLOAT->value);
285 * PARAM_ACTION - deprecated alias for PARAM_ALPHANUMEXT, use for various actions in forms and urls
286 * NOTE: originally alias for PARAM_APLHA
287 * @deprecated since 2.0
289 define('PARAM_ACTION', \core\param::ALPHANUMEXT->value);
292 * PARAM_FORMAT - deprecated alias for PARAM_ALPHANUMEXT, use for names of plugins, formats, etc.
293 * NOTE: originally alias for PARAM_APLHA
294 * @deprecated since 2.0
296 define('PARAM_FORMAT', \core\param::ALPHANUMEXT->value);
299 * PARAM_MULTILANG - deprecated alias of PARAM_TEXT.
300 * @deprecated since 2.0
302 define('PARAM_MULTILANG', \core\param::TEXT->value);
305 * PARAM_TIMEZONE - expected timezone. Timezone can be int +-(0-13) or float +-(0.5-12.5) or
306 * string separated by '/' and can have '-' &/ '_' (eg. America/North_Dakota/New_Salem
307 * America/Port-au-Prince)
309 define('PARAM_TIMEZONE', \core\param::TIMEZONE->value);
312 * PARAM_CLEANFILE - deprecated alias of PARAM_FILE; originally was removing regional chars too
313 * @deprecated since 2.0
315 define('PARAM_CLEANFILE', \core\param::CLEANFILE->value);
318 * PARAM_COMPONENT is used for full component names (aka frankenstyle) such as 'mod_forum', 'core_rating', 'auth_ldap'.
319 * Short legacy subsystem names and module names are accepted too ex: 'forum', 'rating', 'user'.
320 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
321 * NOTE: numbers and underscores are strongly discouraged in plugin names!
323 define('PARAM_COMPONENT', \core\param::COMPONENT->value);
326 * PARAM_AREA is a name of area used when addressing files, comments, ratings, etc.
327 * It is usually used together with context id and component.
328 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
330 define('PARAM_AREA', \core\param::AREA->value);
333 * PARAM_PLUGIN is used for plugin names such as 'forum', 'glossary', 'ldap', 'paypal', 'completionstatus'.
334 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
335 * NOTE: numbers and underscores are strongly discouraged in plugin names! Underscores are forbidden in module names.
337 define('PARAM_PLUGIN', \core\param::PLUGIN->value);
340 // Web Services.
343 * VALUE_REQUIRED - if the parameter is not supplied, there is an error
345 define('VALUE_REQUIRED', 1);
348 * VALUE_OPTIONAL - if the parameter is not supplied, then the param has no value
350 define('VALUE_OPTIONAL', 2);
353 * VALUE_DEFAULT - if the parameter is not supplied, then the default value is used
355 define('VALUE_DEFAULT', 0);
358 * NULL_NOT_ALLOWED - the parameter can not be set to null in the database
360 define('NULL_NOT_ALLOWED', false);
363 * NULL_ALLOWED - the parameter can be set to null in the database
365 define('NULL_ALLOWED', true);
367 // Page types.
370 * PAGE_COURSE_VIEW is a definition of a page type. For more information on the page class see moodle/lib/pagelib.php.
372 define('PAGE_COURSE_VIEW', 'course-view');
374 /** Get remote addr constant */
375 define('GETREMOTEADDR_SKIP_HTTP_CLIENT_IP', '1');
376 /** Get remote addr constant */
377 define('GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR', '2');
379 * GETREMOTEADDR_SKIP_DEFAULT defines the default behavior remote IP address validation.
381 define('GETREMOTEADDR_SKIP_DEFAULT', GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR|GETREMOTEADDR_SKIP_HTTP_CLIENT_IP);
383 // Blog access level constant declaration.
384 define ('BLOG_USER_LEVEL', 1);
385 define ('BLOG_GROUP_LEVEL', 2);
386 define ('BLOG_COURSE_LEVEL', 3);
387 define ('BLOG_SITE_LEVEL', 4);
388 define ('BLOG_GLOBAL_LEVEL', 5);
391 // Tag constants.
393 * To prevent problems with multibytes strings,Flag updating in nav not working on the review page. this should not exceed the
394 * length of "varchar(255) / 3 (bytes / utf-8 character) = 85".
395 * TODO: this is not correct, varchar(255) are 255 unicode chars ;-)
397 * @todo define(TAG_MAX_LENGTH) this is not correct, varchar(255) are 255 unicode chars ;-)
399 define('TAG_MAX_LENGTH', 50);
401 // Password policy constants.
402 define ('PASSWORD_LOWER', 'abcdefghijklmnopqrstuvwxyz');
403 define ('PASSWORD_UPPER', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
404 define ('PASSWORD_DIGITS', '0123456789');
405 define ('PASSWORD_NONALPHANUM', '.,;:!?_-+/*@#&$');
408 * Required password pepper entropy.
410 define ('PEPPER_ENTROPY', 112);
412 // Feature constants.
413 // Used for plugin_supports() to report features that are, or are not, supported by a module.
415 /** True if module can provide a grade */
416 define('FEATURE_GRADE_HAS_GRADE', 'grade_has_grade');
417 /** True if module supports outcomes */
418 define('FEATURE_GRADE_OUTCOMES', 'outcomes');
419 /** True if module supports advanced grading methods */
420 define('FEATURE_ADVANCED_GRADING', 'grade_advanced_grading');
421 /** True if module controls the grade visibility over the gradebook */
422 define('FEATURE_CONTROLS_GRADE_VISIBILITY', 'controlsgradevisbility');
423 /** True if module supports plagiarism plugins */
424 define('FEATURE_PLAGIARISM', 'plagiarism');
426 /** True if module has code to track whether somebody viewed it */
427 define('FEATURE_COMPLETION_TRACKS_VIEWS', 'completion_tracks_views');
428 /** True if module has custom completion rules */
429 define('FEATURE_COMPLETION_HAS_RULES', 'completion_has_rules');
431 /** True if module has no 'view' page (like label) */
432 define('FEATURE_NO_VIEW_LINK', 'viewlink');
433 /** True (which is default) if the module wants support for setting the ID number for grade calculation purposes. */
434 define('FEATURE_IDNUMBER', 'idnumber');
435 /** True if module supports groups */
436 define('FEATURE_GROUPS', 'groups');
437 /** True if module supports groupings */
438 define('FEATURE_GROUPINGS', 'groupings');
440 * True if module supports groupmembersonly (which no longer exists)
441 * @deprecated Since Moodle 2.8
443 define('FEATURE_GROUPMEMBERSONLY', 'groupmembersonly');
445 /** Type of module */
446 define('FEATURE_MOD_ARCHETYPE', 'mod_archetype');
447 /** True if module supports intro editor */
448 define('FEATURE_MOD_INTRO', 'mod_intro');
449 /** True if module has default completion */
450 define('FEATURE_MODEDIT_DEFAULT_COMPLETION', 'modedit_default_completion');
452 define('FEATURE_COMMENT', 'comment');
454 define('FEATURE_RATE', 'rate');
455 /** True if module supports backup/restore of moodle2 format */
456 define('FEATURE_BACKUP_MOODLE2', 'backup_moodle2');
458 /** True if module can show description on course main page */
459 define('FEATURE_SHOW_DESCRIPTION', 'showdescription');
461 /** True if module uses the question bank */
462 define('FEATURE_USES_QUESTIONS', 'usesquestions');
465 * Maximum filename char size
467 define('MAX_FILENAME_SIZE', 100);
469 /** Unspecified module archetype */
470 define('MOD_ARCHETYPE_OTHER', 0);
471 /** Resource-like type module */
472 define('MOD_ARCHETYPE_RESOURCE', 1);
473 /** Assignment module archetype */
474 define('MOD_ARCHETYPE_ASSIGNMENT', 2);
475 /** System (not user-addable) module archetype */
476 define('MOD_ARCHETYPE_SYSTEM', 3);
478 /** Type of module */
479 define('FEATURE_MOD_PURPOSE', 'mod_purpose');
480 /** Module purpose administration */
481 define('MOD_PURPOSE_ADMINISTRATION', 'administration');
482 /** Module purpose assessment */
483 define('MOD_PURPOSE_ASSESSMENT', 'assessment');
484 /** Module purpose communication */
485 define('MOD_PURPOSE_COLLABORATION', 'collaboration');
486 /** Module purpose communication */
487 define('MOD_PURPOSE_COMMUNICATION', 'communication');
488 /** Module purpose content */
489 define('MOD_PURPOSE_CONTENT', 'content');
490 /** Module purpose interface */
491 define('MOD_PURPOSE_INTERFACE', 'interface');
492 /** Module purpose other */
493 define('MOD_PURPOSE_OTHER', 'other');
496 * Security token used for allowing access
497 * from external application such as web services.
498 * Scripts do not use any session, performance is relatively
499 * low because we need to load access info in each request.
500 * Scripts are executed in parallel.
502 define('EXTERNAL_TOKEN_PERMANENT', 0);
505 * Security token used for allowing access
506 * of embedded applications, the code is executed in the
507 * active user session. Token is invalidated after user logs out.
508 * Scripts are executed serially - normal session locking is used.
510 define('EXTERNAL_TOKEN_EMBEDDED', 1);
513 * The home page should be the site home
515 define('HOMEPAGE_SITE', 0);
517 * The home page should be the users my page
519 define('HOMEPAGE_MY', 1);
521 * The home page can be chosen by the user
523 define('HOMEPAGE_USER', 2);
525 * The home page should be the users my courses page
527 define('HOMEPAGE_MYCOURSES', 3);
530 * URL of the Moodle sites registration portal.
532 defined('HUB_MOODLEORGHUBURL') || define('HUB_MOODLEORGHUBURL', 'https://stats.moodle.org');
535 * URL of the statistic server public key.
537 defined('HUB_STATSPUBLICKEY') || define('HUB_STATSPUBLICKEY', 'https://moodle.org/static/statspubkey.pem');
540 * Moodle mobile app service name
542 define('MOODLE_OFFICIAL_MOBILE_SERVICE', 'moodle_mobile_app');
545 * Indicates the user has the capabilities required to ignore activity and course file size restrictions
547 define('USER_CAN_IGNORE_FILE_SIZE_LIMITS', -1);
550 * Course display settings: display all sections on one page.
552 define('COURSE_DISPLAY_SINGLEPAGE', 0);
554 * Course display settings: split pages into a page per section.
556 define('COURSE_DISPLAY_MULTIPAGE', 1);
559 * Authentication constant: String used in password field when password is not stored.
561 define('AUTH_PASSWORD_NOT_CACHED', 'not cached');
564 * Email from header to never include via information.
566 define('EMAIL_VIA_NEVER', 0);
569 * Email from header to always include via information.
571 define('EMAIL_VIA_ALWAYS', 1);
574 * Email from header to only include via information if the address is no-reply.
576 define('EMAIL_VIA_NO_REPLY_ONLY', 2);
579 * Contact site support form/link disabled.
581 define('CONTACT_SUPPORT_DISABLED', 0);
584 * Contact site support form/link only available to authenticated users.
586 define('CONTACT_SUPPORT_AUTHENTICATED', 1);
589 * Contact site support form/link available to anyone visiting the site.
591 define('CONTACT_SUPPORT_ANYONE', 2);
594 * Maximum number of characters for password.
596 define('MAX_PASSWORD_CHARACTERS', 128);
598 // PARAMETER HANDLING.
601 * Returns a particular value for the named variable, taken from
602 * POST or GET. If the parameter doesn't exist then an error is
603 * thrown because we require this variable.
605 * This function should be used to initialise all required values
606 * in a script that are based on parameters. Usually it will be
607 * used like this:
608 * $id = required_param('id', PARAM_INT);
610 * Please note the $type parameter is now required and the value can not be array.
612 * @param string $parname the name of the page parameter we want
613 * @param string $type expected type of parameter
614 * @return mixed
615 * @throws coding_exception
617 function required_param($parname, $type) {
618 return \core\param::from_type($type)->required_param($parname);
622 * Returns a particular array value for the named variable, taken from
623 * POST or GET. If the parameter doesn't exist then an error is
624 * thrown because we require this variable.
626 * This function should be used to initialise all required values
627 * in a script that are based on parameters. Usually it will be
628 * used like this:
629 * $ids = required_param_array('ids', PARAM_INT);
631 * Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
633 * @param string $parname the name of the page parameter we want
634 * @param string $type expected type of parameter
635 * @return array
636 * @throws coding_exception
638 function required_param_array($parname, $type) {
639 return \core\param::from_type($type)->required_param_array($parname);
643 * Returns a particular value for the named variable, taken from
644 * POST or GET, otherwise returning a given default.
646 * This function should be used to initialise all optional values
647 * in a script that are based on parameters. Usually it will be
648 * used like this:
649 * $name = optional_param('name', 'Fred', PARAM_TEXT);
651 * Please note the $type parameter is now required and the value can not be array.
653 * @param string $parname the name of the page parameter we want
654 * @param mixed $default the default value to return if nothing is found
655 * @param string $type expected type of parameter
656 * @return mixed
657 * @throws coding_exception
659 function optional_param($parname, $default, $type) {
660 return \core\param::from_type($type)->optional_param(
661 paramname: $parname,
662 default: $default,
667 * Returns a particular array value for the named variable, taken from
668 * POST or GET, otherwise returning a given default.
670 * This function should be used to initialise all optional values
671 * in a script that are based on parameters. Usually it will be
672 * used like this:
673 * $ids = optional_param('id', array(), PARAM_INT);
675 * Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported
677 * @param string $parname the name of the page parameter we want
678 * @param mixed $default the default value to return if nothing is found
679 * @param string $type expected type of parameter
680 * @return array
681 * @throws coding_exception
683 function optional_param_array($parname, $default, $type) {
684 return \core\param::from_type($type)->optional_param_array(
685 paramname: $parname,
686 default: $default,
691 * Strict validation of parameter values, the values are only converted
692 * to requested PHP type. Internally it is using clean_param, the values
693 * before and after cleaning must be equal - otherwise
694 * an invalid_parameter_exception is thrown.
695 * Objects and classes are not accepted.
697 * @param mixed $param
698 * @param string $type PARAM_ constant
699 * @param bool $allownull are nulls valid value?
700 * @param string $debuginfo optional debug information
701 * @return mixed the $param value converted to PHP type
702 * @throws invalid_parameter_exception if $param is not of given type
704 function validate_param($param, $type, $allownull = NULL_NOT_ALLOWED, $debuginfo = '') {
705 return \core\param::from_type($type)->validate_param(
706 param: $param,
707 allownull: $allownull,
708 debuginfo: $debuginfo,
713 * Makes sure array contains only the allowed types, this function does not validate array key names!
715 * <code>
716 * $options = clean_param($options, PARAM_INT);
717 * </code>
719 * @param array|null $param the variable array we are cleaning
720 * @param string $type expected format of param after cleaning.
721 * @param bool $recursive clean recursive arrays
722 * @return array
723 * @throws coding_exception
725 function clean_param_array(?array $param, $type, $recursive = false) {
726 return \core\param::from_type($type)->clean_param_array(
727 param: $param,
728 recursive: $recursive,
733 * Used by {@link optional_param()} and {@link required_param()} to
734 * clean the variables and/or cast to specific types, based on
735 * an options field.
736 * <code>
737 * $course->format = clean_param($course->format, PARAM_ALPHA);
738 * $selectedgradeitem = clean_param($selectedgradeitem, PARAM_INT);
739 * </code>
741 * @param mixed $param the variable we are cleaning
742 * @param string $type expected format of param after cleaning.
743 * @return mixed
744 * @throws coding_exception
746 function clean_param($param, $type) {
747 return \core\param::from_type($type)->clean($param);
751 * Whether the PARAM_* type is compatible in RTL.
753 * Being compatible with RTL means that the data they contain can flow
754 * from right-to-left or left-to-right without compromising the user experience.
756 * Take URLs for example, they are not RTL compatible as they should always
757 * flow from the left to the right. This also applies to numbers, email addresses,
758 * configuration snippets, base64 strings, etc...
760 * This function tries to best guess which parameters can contain localised strings.
762 * @param string $paramtype Constant PARAM_*.
763 * @return bool
765 function is_rtl_compatible($paramtype) {
766 return $paramtype == PARAM_TEXT || $paramtype == PARAM_NOTAGS;
770 * Makes sure the data is using valid utf8, invalid characters are discarded.
772 * Note: this function is not intended for full objects with methods and private properties.
774 * @param mixed $value
775 * @return mixed with proper utf-8 encoding
777 function fix_utf8($value) {
778 if (is_null($value) or $value === '') {
779 return $value;
781 } else if (is_string($value)) {
782 if ((string)(int)$value === $value) {
783 // Shortcut.
784 return $value;
787 // Remove null bytes or invalid Unicode sequences from value.
788 $value = str_replace(["\0", "\xef\xbf\xbe", "\xef\xbf\xbf"], '', $value);
790 // Note: this duplicates min_fix_utf8() intentionally.
791 static $buggyiconv = null;
792 if ($buggyiconv === null) {
793 $buggyiconv = (!function_exists('iconv') or @iconv('UTF-8', 'UTF-8//IGNORE', '100'.chr(130).'€') !== '100€');
796 if ($buggyiconv) {
797 if (function_exists('mb_convert_encoding')) {
798 $subst = mb_substitute_character();
799 mb_substitute_character('none');
800 $result = mb_convert_encoding($value, 'utf-8', 'utf-8');
801 mb_substitute_character($subst);
803 } else {
804 // Warn admins on admin/index.php page.
805 $result = $value;
808 } else {
809 $result = @iconv('UTF-8', 'UTF-8//IGNORE', $value);
812 return $result;
814 } else if (is_array($value)) {
815 foreach ($value as $k => $v) {
816 $value[$k] = fix_utf8($v);
818 return $value;
820 } else if (is_object($value)) {
821 // Do not modify original.
822 $value = clone($value);
823 foreach ($value as $k => $v) {
824 $value->$k = fix_utf8($v);
826 return $value;
828 } else {
829 // This is some other type, no utf-8 here.
830 return $value;
835 * Return true if given value is integer or string with integer value
837 * @param mixed $value String or Int
838 * @return bool true if number, false if not
840 function is_number($value) {
841 if (is_int($value)) {
842 return true;
843 } else if (is_string($value)) {
844 return ((string)(int)$value) === $value;
845 } else {
846 return false;
851 * Returns host part from url.
853 * @param string $url full url
854 * @return string host, null if not found
856 function get_host_from_url($url) {
857 preg_match('|^[a-z]+://([a-zA-Z0-9-.]+)|i', $url, $matches);
858 if ($matches) {
859 return $matches[1];
861 return null;
865 * Tests whether anything was returned by text editor
867 * This function is useful for testing whether something you got back from
868 * the HTML editor actually contains anything. Sometimes the HTML editor
869 * appear to be empty, but actually you get back a <br> tag or something.
871 * @param string $string a string containing HTML.
872 * @return boolean does the string contain any actual content - that is text,
873 * images, objects, etc.
875 function html_is_blank($string) {
876 return trim(strip_tags((string)$string, '<img><object><applet><input><select><textarea><hr>')) == '';
880 * Set a key in global configuration
882 * Set a key/value pair in both this session's {@link $CFG} global variable
883 * and in the 'config' database table for future sessions.
885 * Can also be used to update keys for plugin-scoped configs in config_plugin table.
886 * In that case it doesn't affect $CFG.
888 * A NULL value will delete the entry.
890 * NOTE: this function is called from lib/db/upgrade.php
892 * @param string $name the key to set
893 * @param string $value the value to set (without magic quotes)
894 * @param string $plugin (optional) the plugin scope, default null
895 * @return bool true or exception
897 function set_config($name, $value, $plugin = null) {
898 global $CFG, $DB;
900 // Redirect to appropriate handler when value is null.
901 if ($value === null) {
902 return unset_config($name, $plugin);
905 // Set variables determining conditions and where to store the new config.
906 // Plugin config goes to {config_plugins}, core config goes to {config}.
907 $iscore = empty($plugin);
908 if ($iscore) {
909 // If it's for core config.
910 $table = 'config';
911 $conditions = ['name' => $name];
912 $invalidatecachekey = 'core';
913 } else {
914 // If it's a plugin.
915 $table = 'config_plugins';
916 $conditions = ['name' => $name, 'plugin' => $plugin];
917 $invalidatecachekey = $plugin;
920 // DB handling - checks for existing config, updating or inserting only if necessary.
921 $invalidatecache = true;
922 $inserted = false;
923 $record = $DB->get_record($table, $conditions, 'id, value');
924 if ($record === false) {
925 // Inserts a new config record.
926 $config = new stdClass();
927 $config->name = $name;
928 $config->value = $value;
929 if (!$iscore) {
930 $config->plugin = $plugin;
932 $inserted = $DB->insert_record($table, $config, false);
933 } else if ($invalidatecache = ($record->value !== $value)) {
934 // Record exists - Check and only set new value if it has changed.
935 $DB->set_field($table, 'value', $value, ['id' => $record->id]);
938 if ($iscore && !isset($CFG->config_php_settings[$name])) {
939 // So it's defined for this invocation at least.
940 // Settings from db are always strings.
941 $CFG->$name = (string) $value;
944 // When setting config during a Behat test (in the CLI script, not in the web browser
945 // requests), remember which ones are set so that we can clear them later.
946 if ($iscore && $inserted && defined('BEHAT_TEST')) {
947 $CFG->behat_cli_added_config[$name] = true;
950 // Update siteidentifier cache, if required.
951 if ($iscore && $name === 'siteidentifier') {
952 cache_helper::update_site_identifier($value);
955 // Invalidate cache, if required.
956 if ($invalidatecache) {
957 cache_helper::invalidate_by_definition('core', 'config', [], $invalidatecachekey);
960 return true;
964 * Get configuration values from the global config table
965 * or the config_plugins table.
967 * If called with one parameter, it will load all the config
968 * variables for one plugin, and return them as an object.
970 * If called with 2 parameters it will return a string single
971 * value or false if the value is not found.
973 * NOTE: this function is called from lib/db/upgrade.php
975 * @param string $plugin full component name
976 * @param string $name default null
977 * @return mixed hash-like object or single value, return false no config found
978 * @throws dml_exception
980 function get_config($plugin, $name = null) {
981 global $CFG, $DB;
983 if ($plugin === 'moodle' || $plugin === 'core' || empty($plugin)) {
984 $forced =& $CFG->config_php_settings;
985 $iscore = true;
986 $plugin = 'core';
987 } else {
988 if (array_key_exists($plugin, $CFG->forced_plugin_settings)) {
989 $forced =& $CFG->forced_plugin_settings[$plugin];
990 } else {
991 $forced = array();
993 $iscore = false;
996 if (!isset($CFG->siteidentifier)) {
997 try {
998 // This may throw an exception during installation, which is how we detect the
999 // need to install the database. For more details see {@see initialise_cfg()}.
1000 $CFG->siteidentifier = $DB->get_field('config', 'value', array('name' => 'siteidentifier'));
1001 } catch (dml_exception $ex) {
1002 // Set siteidentifier to false. We don't want to trip this continually.
1003 $siteidentifier = false;
1004 throw $ex;
1008 if (!empty($name)) {
1009 if (array_key_exists($name, $forced)) {
1010 return (string)$forced[$name];
1011 } else if ($name === 'siteidentifier' && $plugin == 'core') {
1012 return $CFG->siteidentifier;
1016 $cache = cache::make('core', 'config');
1017 $result = $cache->get($plugin);
1018 if ($result === false) {
1019 // The user is after a recordset.
1020 if (!$iscore) {
1021 $result = $DB->get_records_menu('config_plugins', array('plugin' => $plugin), '', 'name,value');
1022 } else {
1023 // This part is not really used any more, but anyway...
1024 $result = $DB->get_records_menu('config', array(), '', 'name,value');;
1026 $cache->set($plugin, $result);
1029 if (!empty($name)) {
1030 if (array_key_exists($name, $result)) {
1031 return $result[$name];
1033 return false;
1036 if ($plugin === 'core') {
1037 $result['siteidentifier'] = $CFG->siteidentifier;
1040 foreach ($forced as $key => $value) {
1041 if (is_null($value) or is_array($value) or is_object($value)) {
1042 // We do not want any extra mess here, just real settings that could be saved in db.
1043 unset($result[$key]);
1044 } else {
1045 // Convert to string as if it went through the DB.
1046 $result[$key] = (string)$value;
1050 return (object)$result;
1054 * Removes a key from global configuration.
1056 * NOTE: this function is called from lib/db/upgrade.php
1058 * @param string $name the key to set
1059 * @param string $plugin (optional) the plugin scope
1060 * @return boolean whether the operation succeeded.
1062 function unset_config($name, $plugin=null) {
1063 global $CFG, $DB;
1065 if (empty($plugin)) {
1066 unset($CFG->$name);
1067 $DB->delete_records('config', array('name' => $name));
1068 cache_helper::invalidate_by_definition('core', 'config', array(), 'core');
1069 } else {
1070 $DB->delete_records('config_plugins', array('name' => $name, 'plugin' => $plugin));
1071 cache_helper::invalidate_by_definition('core', 'config', array(), $plugin);
1074 return true;
1078 * Remove all the config variables for a given plugin.
1080 * NOTE: this function is called from lib/db/upgrade.php
1082 * @param string $plugin a plugin, for example 'quiz' or 'qtype_multichoice';
1083 * @return boolean whether the operation succeeded.
1085 function unset_all_config_for_plugin($plugin) {
1086 global $DB;
1087 // Delete from the obvious config_plugins first.
1088 $DB->delete_records('config_plugins', array('plugin' => $plugin));
1089 // Next delete any suspect settings from config.
1090 $like = $DB->sql_like('name', '?', true, true, false, '|');
1091 $params = array($DB->sql_like_escape($plugin.'_', '|') . '%');
1092 $DB->delete_records_select('config', $like, $params);
1093 // Finally clear both the plugin cache and the core cache (suspect settings now removed from core).
1094 cache_helper::invalidate_by_definition('core', 'config', array(), array('core', $plugin));
1096 return true;
1100 * Use this function to get a list of users from a config setting of type admin_setting_users_with_capability.
1102 * All users are verified if they still have the necessary capability.
1104 * @param string $value the value of the config setting.
1105 * @param string $capability the capability - must match the one passed to the admin_setting_users_with_capability constructor.
1106 * @param bool $includeadmins include administrators.
1107 * @return array of user objects.
1109 function get_users_from_config($value, $capability, $includeadmins = true) {
1110 if (empty($value) or $value === '$@NONE@$') {
1111 return array();
1114 // We have to make sure that users still have the necessary capability,
1115 // it should be faster to fetch them all first and then test if they are present
1116 // instead of validating them one-by-one.
1117 $users = get_users_by_capability(context_system::instance(), $capability);
1118 if ($includeadmins) {
1119 $admins = get_admins();
1120 foreach ($admins as $admin) {
1121 $users[$admin->id] = $admin;
1125 if ($value === '$@ALL@$') {
1126 return $users;
1129 $result = array(); // Result in correct order.
1130 $allowed = explode(',', $value);
1131 foreach ($allowed as $uid) {
1132 if (isset($users[$uid])) {
1133 $user = $users[$uid];
1134 $result[$user->id] = $user;
1138 return $result;
1143 * Invalidates browser caches and cached data in temp.
1145 * @return void
1147 function purge_all_caches() {
1148 purge_caches();
1152 * Selectively invalidate different types of cache.
1154 * Purges the cache areas specified. By default, this will purge all caches but can selectively purge specific
1155 * areas alone or in combination.
1157 * @param bool[] $options Specific parts of the cache to purge. Valid options are:
1158 * 'muc' Purge MUC caches?
1159 * 'theme' Purge theme cache?
1160 * 'lang' Purge language string cache?
1161 * 'js' Purge javascript cache?
1162 * 'filter' Purge text filter cache?
1163 * 'other' Purge all other caches?
1165 function purge_caches($options = []) {
1166 $defaults = array_fill_keys(['muc', 'theme', 'lang', 'js', 'template', 'filter', 'other'], false);
1167 if (empty(array_filter($options))) {
1168 $options = array_fill_keys(array_keys($defaults), true); // Set all options to true.
1169 } else {
1170 $options = array_merge($defaults, array_intersect_key($options, $defaults)); // Override defaults with specified options.
1172 if ($options['muc']) {
1173 cache_helper::purge_all();
1175 if ($options['theme']) {
1176 theme_reset_all_caches();
1178 if ($options['lang']) {
1179 get_string_manager()->reset_caches();
1181 if ($options['js']) {
1182 js_reset_all_caches();
1184 if ($options['template']) {
1185 template_reset_all_caches();
1187 if ($options['filter']) {
1188 reset_text_filters_cache();
1190 if ($options['other']) {
1191 purge_other_caches();
1196 * Purge all non-MUC caches not otherwise purged in purge_caches.
1198 * IMPORTANT - If you are adding anything here to do with the cache directory you should also have a look at
1199 * {@link phpunit_util::reset_dataroot()}
1201 function purge_other_caches() {
1202 global $DB, $CFG;
1203 if (class_exists('core_plugin_manager')) {
1204 core_plugin_manager::reset_caches();
1207 // Bump up cacherev field for all courses.
1208 try {
1209 increment_revision_number('course', 'cacherev', '');
1210 } catch (moodle_exception $e) {
1211 // Ignore exception since this function is also called before upgrade script when field course.cacherev does not exist yet.
1214 $DB->reset_caches();
1216 // Purge all other caches: rss, simplepie, etc.
1217 clearstatcache();
1218 remove_dir($CFG->cachedir.'', true);
1220 // Make sure cache dir is writable, throws exception if not.
1221 make_cache_directory('');
1223 // This is the only place where we purge local caches, we are only adding files there.
1224 // The $CFG->localcachedirpurged flag forces local directories to be purged on cluster nodes.
1225 remove_dir($CFG->localcachedir, true);
1226 set_config('localcachedirpurged', time());
1227 make_localcache_directory('', true);
1228 \core\task\manager::clear_static_caches();
1232 * Get volatile flags
1234 * @param string $type
1235 * @param int $changedsince default null
1236 * @return array records array
1238 function get_cache_flags($type, $changedsince = null) {
1239 global $DB;
1241 $params = array('type' => $type, 'expiry' => time());
1242 $sqlwhere = "flagtype = :type AND expiry >= :expiry";
1243 if ($changedsince !== null) {
1244 $params['changedsince'] = $changedsince;
1245 $sqlwhere .= " AND timemodified > :changedsince";
1247 $cf = array();
1248 if ($flags = $DB->get_records_select('cache_flags', $sqlwhere, $params, '', 'name,value')) {
1249 foreach ($flags as $flag) {
1250 $cf[$flag->name] = $flag->value;
1253 return $cf;
1257 * Get volatile flags
1259 * @param string $type
1260 * @param string $name
1261 * @param int $changedsince default null
1262 * @return string|false The cache flag value or false
1264 function get_cache_flag($type, $name, $changedsince=null) {
1265 global $DB;
1267 $params = array('type' => $type, 'name' => $name, 'expiry' => time());
1269 $sqlwhere = "flagtype = :type AND name = :name AND expiry >= :expiry";
1270 if ($changedsince !== null) {
1271 $params['changedsince'] = $changedsince;
1272 $sqlwhere .= " AND timemodified > :changedsince";
1275 return $DB->get_field_select('cache_flags', 'value', $sqlwhere, $params);
1279 * Set a volatile flag
1281 * @param string $type the "type" namespace for the key
1282 * @param string $name the key to set
1283 * @param string $value the value to set (without magic quotes) - null will remove the flag
1284 * @param int $expiry (optional) epoch indicating expiry - defaults to now()+ 24hs
1285 * @return bool Always returns true
1287 function set_cache_flag($type, $name, $value, $expiry = null) {
1288 global $DB;
1290 $timemodified = time();
1291 if ($expiry === null || $expiry < $timemodified) {
1292 $expiry = $timemodified + 24 * 60 * 60;
1293 } else {
1294 $expiry = (int)$expiry;
1297 if ($value === null) {
1298 unset_cache_flag($type, $name);
1299 return true;
1302 if ($f = $DB->get_record('cache_flags', array('name' => $name, 'flagtype' => $type), '*', IGNORE_MULTIPLE)) {
1303 // This is a potential problem in DEBUG_DEVELOPER.
1304 if ($f->value == $value and $f->expiry == $expiry and $f->timemodified == $timemodified) {
1305 return true; // No need to update.
1307 $f->value = $value;
1308 $f->expiry = $expiry;
1309 $f->timemodified = $timemodified;
1310 $DB->update_record('cache_flags', $f);
1311 } else {
1312 $f = new stdClass();
1313 $f->flagtype = $type;
1314 $f->name = $name;
1315 $f->value = $value;
1316 $f->expiry = $expiry;
1317 $f->timemodified = $timemodified;
1318 $DB->insert_record('cache_flags', $f);
1320 return true;
1324 * Removes a single volatile flag
1326 * @param string $type the "type" namespace for the key
1327 * @param string $name the key to set
1328 * @return bool
1330 function unset_cache_flag($type, $name) {
1331 global $DB;
1332 $DB->delete_records('cache_flags', array('name' => $name, 'flagtype' => $type));
1333 return true;
1337 * Garbage-collect volatile flags
1339 * @return bool Always returns true
1341 function gc_cache_flags() {
1342 global $DB;
1343 $DB->delete_records_select('cache_flags', 'expiry < ?', array(time()));
1344 return true;
1347 // USER PREFERENCE API.
1350 * Refresh user preference cache. This is used most often for $USER
1351 * object that is stored in session, but it also helps with performance in cron script.
1353 * Preferences for each user are loaded on first use on every page, then again after the timeout expires.
1355 * @package core
1356 * @category preference
1357 * @access public
1358 * @param stdClass $user User object. Preferences are preloaded into 'preference' property
1359 * @param int $cachelifetime Cache life time on the current page (in seconds)
1360 * @throws coding_exception
1361 * @return null
1363 function check_user_preferences_loaded(stdClass $user, $cachelifetime = 120) {
1364 global $DB;
1365 // Static cache, we need to check on each page load, not only every 2 minutes.
1366 static $loadedusers = array();
1368 if (!isset($user->id)) {
1369 throw new coding_exception('Invalid $user parameter in check_user_preferences_loaded() call, missing id field');
1372 if (empty($user->id) or isguestuser($user->id)) {
1373 // No permanent storage for not-logged-in users and guest.
1374 if (!isset($user->preference)) {
1375 $user->preference = array();
1377 return;
1380 $timenow = time();
1382 if (isset($loadedusers[$user->id]) and isset($user->preference) and isset($user->preference['_lastloaded'])) {
1383 // Already loaded at least once on this page. Are we up to date?
1384 if ($user->preference['_lastloaded'] + $cachelifetime > $timenow) {
1385 // No need to reload - we are on the same page and we loaded prefs just a moment ago.
1386 return;
1388 } else if (!get_cache_flag('userpreferenceschanged', $user->id, $user->preference['_lastloaded'])) {
1389 // No change since the lastcheck on this page.
1390 $user->preference['_lastloaded'] = $timenow;
1391 return;
1395 // OK, so we have to reload all preferences.
1396 $loadedusers[$user->id] = true;
1397 $user->preference = $DB->get_records_menu('user_preferences', array('userid' => $user->id), '', 'name,value'); // All values.
1398 $user->preference['_lastloaded'] = $timenow;
1402 * Called from set/unset_user_preferences, so that the prefs can be correctly reloaded in different sessions.
1404 * NOTE: internal function, do not call from other code.
1406 * @package core
1407 * @access private
1408 * @param integer $userid the user whose prefs were changed.
1410 function mark_user_preferences_changed($userid) {
1411 global $CFG;
1413 if (empty($userid) or isguestuser($userid)) {
1414 // No cache flags for guest and not-logged-in users.
1415 return;
1418 set_cache_flag('userpreferenceschanged', $userid, 1, time() + $CFG->sessiontimeout);
1422 * Sets a preference for the specified user.
1424 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1426 * When additional validation/permission check is needed it is better to use {@see useredit_update_user_preference()}
1428 * @package core
1429 * @category preference
1430 * @access public
1431 * @param string $name The key to set as preference for the specified user
1432 * @param string $value The value to set for the $name key in the specified user's
1433 * record, null means delete current value.
1434 * @param stdClass|int|null $user A moodle user object or id, null means current user
1435 * @throws coding_exception
1436 * @return bool Always true or exception
1438 function set_user_preference($name, $value, $user = null) {
1439 global $USER, $DB;
1441 if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
1442 throw new coding_exception('Invalid preference name in set_user_preference() call');
1445 if (is_null($value)) {
1446 // Null means delete current.
1447 return unset_user_preference($name, $user);
1448 } else if (is_object($value)) {
1449 throw new coding_exception('Invalid value in set_user_preference() call, objects are not allowed');
1450 } else if (is_array($value)) {
1451 throw new coding_exception('Invalid value in set_user_preference() call, arrays are not allowed');
1453 // Value column maximum length is 1333 characters.
1454 $value = (string)$value;
1455 if (core_text::strlen($value) > 1333) {
1456 throw new coding_exception('Invalid value in set_user_preference() call, value is is too long for the value column');
1459 if (is_null($user)) {
1460 $user = $USER;
1461 } else if (isset($user->id)) {
1462 // It is a valid object.
1463 } else if (is_numeric($user)) {
1464 $user = (object)array('id' => (int)$user);
1465 } else {
1466 throw new coding_exception('Invalid $user parameter in set_user_preference() call');
1469 check_user_preferences_loaded($user);
1471 if (empty($user->id) or isguestuser($user->id)) {
1472 // No permanent storage for not-logged-in users and guest.
1473 $user->preference[$name] = $value;
1474 return true;
1477 if ($preference = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => $name))) {
1478 if ($preference->value === $value and isset($user->preference[$name]) and $user->preference[$name] === $value) {
1479 // Preference already set to this value.
1480 return true;
1482 $DB->set_field('user_preferences', 'value', $value, array('id' => $preference->id));
1484 } else {
1485 $preference = new stdClass();
1486 $preference->userid = $user->id;
1487 $preference->name = $name;
1488 $preference->value = $value;
1489 $DB->insert_record('user_preferences', $preference);
1492 // Update value in cache.
1493 $user->preference[$name] = $value;
1494 // Update the $USER in case where we've not a direct reference to $USER.
1495 if ($user !== $USER && $user->id == $USER->id) {
1496 $USER->preference[$name] = $value;
1499 // Set reload flag for other sessions.
1500 mark_user_preferences_changed($user->id);
1502 return true;
1506 * Sets a whole array of preferences for the current user
1508 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1510 * @package core
1511 * @category preference
1512 * @access public
1513 * @param array $prefarray An array of key/value pairs to be set
1514 * @param stdClass|int|null $user A moodle user object or id, null means current user
1515 * @return bool Always true or exception
1517 function set_user_preferences(array $prefarray, $user = null) {
1518 foreach ($prefarray as $name => $value) {
1519 set_user_preference($name, $value, $user);
1521 return true;
1525 * Unsets a preference completely by deleting it from the database
1527 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1529 * @package core
1530 * @category preference
1531 * @access public
1532 * @param string $name The key to unset as preference for the specified user
1533 * @param stdClass|int|null $user A moodle user object or id, null means current user
1534 * @throws coding_exception
1535 * @return bool Always true or exception
1537 function unset_user_preference($name, $user = null) {
1538 global $USER, $DB;
1540 if (empty($name) or is_numeric($name) or $name === '_lastloaded') {
1541 throw new coding_exception('Invalid preference name in unset_user_preference() call');
1544 if (is_null($user)) {
1545 $user = $USER;
1546 } else if (isset($user->id)) {
1547 // It is a valid object.
1548 } else if (is_numeric($user)) {
1549 $user = (object)array('id' => (int)$user);
1550 } else {
1551 throw new coding_exception('Invalid $user parameter in unset_user_preference() call');
1554 check_user_preferences_loaded($user);
1556 if (empty($user->id) or isguestuser($user->id)) {
1557 // No permanent storage for not-logged-in user and guest.
1558 unset($user->preference[$name]);
1559 return true;
1562 // Delete from DB.
1563 $DB->delete_records('user_preferences', array('userid' => $user->id, 'name' => $name));
1565 // Delete the preference from cache.
1566 unset($user->preference[$name]);
1567 // Update the $USER in case where we've not a direct reference to $USER.
1568 if ($user !== $USER && $user->id == $USER->id) {
1569 unset($USER->preference[$name]);
1572 // Set reload flag for other sessions.
1573 mark_user_preferences_changed($user->id);
1575 return true;
1579 * Used to fetch user preference(s)
1581 * If no arguments are supplied this function will return
1582 * all of the current user preferences as an array.
1584 * If a name is specified then this function
1585 * attempts to return that particular preference value. If
1586 * none is found, then the optional value $default is returned,
1587 * otherwise null.
1589 * If a $user object is submitted it's 'preference' property is used for the preferences cache.
1591 * @package core
1592 * @category preference
1593 * @access public
1594 * @param string $name Name of the key to use in finding a preference value
1595 * @param mixed|null $default Value to be returned if the $name key is not set in the user preferences
1596 * @param stdClass|int|null $user A moodle user object or id, null means current user
1597 * @throws coding_exception
1598 * @return string|mixed|null A string containing the value of a single preference. An
1599 * array with all of the preferences or null
1601 function get_user_preferences($name = null, $default = null, $user = null) {
1602 global $USER;
1604 if (is_null($name)) {
1605 // All prefs.
1606 } else if (is_numeric($name) or $name === '_lastloaded') {
1607 throw new coding_exception('Invalid preference name in get_user_preferences() call');
1610 if (is_null($user)) {
1611 $user = $USER;
1612 } else if (isset($user->id)) {
1613 // Is a valid object.
1614 } else if (is_numeric($user)) {
1615 if ($USER->id == $user) {
1616 $user = $USER;
1617 } else {
1618 $user = (object)array('id' => (int)$user);
1620 } else {
1621 throw new coding_exception('Invalid $user parameter in get_user_preferences() call');
1624 check_user_preferences_loaded($user);
1626 if (empty($name)) {
1627 // All values.
1628 return $user->preference;
1629 } else if (isset($user->preference[$name])) {
1630 // The single string value.
1631 return $user->preference[$name];
1632 } else {
1633 // Default value (null if not specified).
1634 return $default;
1638 // FUNCTIONS FOR HANDLING TIME.
1641 * Given Gregorian date parts in user time produce a GMT timestamp.
1643 * @package core
1644 * @category time
1645 * @param int $year The year part to create timestamp of
1646 * @param int $month The month part to create timestamp of
1647 * @param int $day The day part to create timestamp of
1648 * @param int $hour The hour part to create timestamp of
1649 * @param int $minute The minute part to create timestamp of
1650 * @param int $second The second part to create timestamp of
1651 * @param int|float|string $timezone Timezone modifier, used to calculate GMT time offset.
1652 * if 99 then default user's timezone is used {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
1653 * @param bool $applydst Toggle Daylight Saving Time, default true, will be
1654 * applied only if timezone is 99 or string.
1655 * @return int GMT timestamp
1657 function make_timestamp($year, $month=1, $day=1, $hour=0, $minute=0, $second=0, $timezone=99, $applydst=true) {
1658 $date = new DateTime('now', core_date::get_user_timezone_object($timezone));
1659 $date->setDate((int)$year, (int)$month, (int)$day);
1660 $date->setTime((int)$hour, (int)$minute, (int)$second);
1662 $time = $date->getTimestamp();
1664 if ($time === false) {
1665 throw new coding_exception('getTimestamp() returned false, please ensure you have passed correct values.'.
1666 ' This can fail if year is more than 2038 and OS is 32 bit windows');
1669 // Moodle BC DST stuff.
1670 if (!$applydst) {
1671 $time += dst_offset_on($time, $timezone);
1674 return $time;
1679 * Format a date/time (seconds) as weeks, days, hours etc as needed
1681 * Given an amount of time in seconds, returns string
1682 * formatted nicely as years, days, hours etc as needed
1684 * @package core
1685 * @category time
1686 * @uses MINSECS
1687 * @uses HOURSECS
1688 * @uses DAYSECS
1689 * @uses YEARSECS
1690 * @param int $totalsecs Time in seconds
1691 * @param stdClass $str Should be a time object
1692 * @return string A nicely formatted date/time string
1694 function format_time($totalsecs, $str = null) {
1696 $totalsecs = abs($totalsecs);
1698 if (!$str) {
1699 // Create the str structure the slow way.
1700 $str = new stdClass();
1701 $str->day = get_string('day');
1702 $str->days = get_string('days');
1703 $str->hour = get_string('hour');
1704 $str->hours = get_string('hours');
1705 $str->min = get_string('min');
1706 $str->mins = get_string('mins');
1707 $str->sec = get_string('sec');
1708 $str->secs = get_string('secs');
1709 $str->year = get_string('year');
1710 $str->years = get_string('years');
1713 $years = floor($totalsecs/YEARSECS);
1714 $remainder = $totalsecs - ($years*YEARSECS);
1715 $days = floor($remainder/DAYSECS);
1716 $remainder = $totalsecs - ($days*DAYSECS);
1717 $hours = floor($remainder/HOURSECS);
1718 $remainder = $remainder - ($hours*HOURSECS);
1719 $mins = floor($remainder/MINSECS);
1720 $secs = $remainder - ($mins*MINSECS);
1722 $ss = ($secs == 1) ? $str->sec : $str->secs;
1723 $sm = ($mins == 1) ? $str->min : $str->mins;
1724 $sh = ($hours == 1) ? $str->hour : $str->hours;
1725 $sd = ($days == 1) ? $str->day : $str->days;
1726 $sy = ($years == 1) ? $str->year : $str->years;
1728 $oyears = '';
1729 $odays = '';
1730 $ohours = '';
1731 $omins = '';
1732 $osecs = '';
1734 if ($years) {
1735 $oyears = $years .' '. $sy;
1737 if ($days) {
1738 $odays = $days .' '. $sd;
1740 if ($hours) {
1741 $ohours = $hours .' '. $sh;
1743 if ($mins) {
1744 $omins = $mins .' '. $sm;
1746 if ($secs) {
1747 $osecs = $secs .' '. $ss;
1750 if ($years) {
1751 return trim($oyears .' '. $odays);
1753 if ($days) {
1754 return trim($odays .' '. $ohours);
1756 if ($hours) {
1757 return trim($ohours .' '. $omins);
1759 if ($mins) {
1760 return trim($omins .' '. $osecs);
1762 if ($secs) {
1763 return $osecs;
1765 return get_string('now');
1769 * Returns a formatted string that represents a date in user time.
1771 * @package core
1772 * @category time
1773 * @param int $date the timestamp in UTC, as obtained from the database.
1774 * @param string $format strftime format. You should probably get this using
1775 * get_string('strftime...', 'langconfig');
1776 * @param int|float|string $timezone by default, uses the user's time zone. if numeric and
1777 * not 99 then daylight saving will not be added.
1778 * {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
1779 * @param bool $fixday If true (default) then the leading zero from %d is removed.
1780 * If false then the leading zero is maintained.
1781 * @param bool $fixhour If true (default) then the leading zero from %I is removed.
1782 * @return string the formatted date/time.
1784 function userdate($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) {
1785 $calendartype = \core_calendar\type_factory::get_calendar_instance();
1786 return $calendartype->timestamp_to_date_string($date, $format, $timezone, $fixday, $fixhour);
1790 * Returns a html "time" tag with both the exact user date with timezone information
1791 * as a datetime attribute in the W3C format, and the user readable date and time as text.
1793 * @package core
1794 * @category time
1795 * @param int $date the timestamp in UTC, as obtained from the database.
1796 * @param string $format strftime format. You should probably get this using
1797 * get_string('strftime...', 'langconfig');
1798 * @param int|float|string $timezone by default, uses the user's time zone. if numeric and
1799 * not 99 then daylight saving will not be added.
1800 * {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
1801 * @param bool $fixday If true (default) then the leading zero from %d is removed.
1802 * If false then the leading zero is maintained.
1803 * @param bool $fixhour If true (default) then the leading zero from %I is removed.
1804 * @return string the formatted date/time.
1806 function userdate_htmltime($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) {
1807 $userdatestr = userdate($date, $format, $timezone, $fixday, $fixhour);
1808 if (CLI_SCRIPT && !PHPUNIT_TEST) {
1809 return $userdatestr;
1811 $machinedate = new DateTime();
1812 $machinedate->setTimestamp(intval($date));
1813 $machinedate->setTimezone(core_date::get_user_timezone_object());
1815 return html_writer::tag('time', $userdatestr, ['datetime' => $machinedate->format(DateTime::W3C)]);
1819 * Returns a formatted date ensuring it is UTF-8.
1821 * If we are running under Windows convert to Windows encoding and then back to UTF-8
1822 * (because it's impossible to specify UTF-8 to fetch locale info in Win32).
1824 * @param int $date the timestamp - since Moodle 2.9 this is a real UTC timestamp
1825 * @param string $format strftime format.
1826 * @param int|float|string $tz the user timezone
1827 * @return string the formatted date/time.
1828 * @since Moodle 2.3.3
1830 function date_format_string($date, $format, $tz = 99) {
1832 date_default_timezone_set(core_date::get_user_timezone($tz));
1834 if (date('A', 0) === date('A', HOURSECS * 18)) {
1835 $datearray = getdate($date);
1836 $format = str_replace([
1837 '%P',
1838 '%p',
1839 ], [
1840 $datearray['hours'] < 12 ? get_string('am', 'langconfig') : get_string('pm', 'langconfig'),
1841 $datearray['hours'] < 12 ? get_string('amcaps', 'langconfig') : get_string('pmcaps', 'langconfig'),
1842 ], $format);
1845 $datestring = core_date::strftime($format, $date);
1846 core_date::set_default_server_timezone();
1848 return $datestring;
1852 * Given a $time timestamp in GMT (seconds since epoch),
1853 * returns an array that represents the Gregorian date in user time
1855 * @package core
1856 * @category time
1857 * @param int $time Timestamp in GMT
1858 * @param float|int|string $timezone user timezone
1859 * @return array An array that represents the date in user time
1861 function usergetdate($time, $timezone=99) {
1862 if ($time === null) {
1863 // PHP8 and PHP7 return different results when getdate(null) is called.
1864 // Display warning and cast to 0 to make sure the usergetdate() behaves consistently on all versions of PHP.
1865 // In the future versions of Moodle we may consider adding a strict typehint.
1866 debugging('usergetdate() expects parameter $time to be int, null given', DEBUG_DEVELOPER);
1867 $time = 0;
1870 date_default_timezone_set(core_date::get_user_timezone($timezone));
1871 $result = getdate($time);
1872 core_date::set_default_server_timezone();
1874 return $result;
1878 * Given a GMT timestamp (seconds since epoch), offsets it by
1879 * the timezone. eg 3pm in India is 3pm GMT - 7 * 3600 seconds
1881 * NOTE: this function does not include DST properly,
1882 * you should use the PHP date stuff instead!
1884 * @package core
1885 * @category time
1886 * @param int $date Timestamp in GMT
1887 * @param float|int|string $timezone user timezone
1888 * @return int
1890 function usertime($date, $timezone=99) {
1891 $userdate = new DateTime('@' . $date);
1892 $userdate->setTimezone(core_date::get_user_timezone_object($timezone));
1893 $dst = dst_offset_on($date, $timezone);
1895 return $date - $userdate->getOffset() + $dst;
1899 * Get a formatted string representation of an interval between two unix timestamps.
1901 * E.g.
1902 * $intervalstring = get_time_interval_string(12345600, 12345660);
1903 * Will produce the string:
1904 * '0d 0h 1m'
1906 * @param int $time1 unix timestamp
1907 * @param int $time2 unix timestamp
1908 * @param string $format string (can be lang string) containing format chars: https://www.php.net/manual/en/dateinterval.format.php.
1909 * @param bool $dropzeroes If format is not provided and this is set to true, do not include zero time units.
1910 * e.g. a duration of 3 days and 2 hours will be displayed as '3d 2h' instead of '3d 2h 0s'
1911 * @param bool $fullformat If format is not provided and this is set to true, display time units in full format.
1912 * e.g. instead of showing "3d", "3 days" will be returned.
1913 * @return string the formatted string describing the time difference, e.g. '10d 11h 45m'.
1915 function get_time_interval_string(int $time1, int $time2, string $format = '',
1916 bool $dropzeroes = false, bool $fullformat = false): string {
1917 $dtdate = new DateTime();
1918 $dtdate->setTimeStamp($time1);
1919 $dtdate2 = new DateTime();
1920 $dtdate2->setTimeStamp($time2);
1921 $interval = $dtdate2->diff($dtdate);
1923 if (empty(trim($format))) {
1924 // Default to this key.
1925 $formatkey = 'dateintervaldayhrmin';
1927 if ($dropzeroes) {
1928 $units = [
1929 'y' => 'yr',
1930 'm' => 'mo',
1931 'd' => 'day',
1932 'h' => 'hr',
1933 'i' => 'min',
1934 's' => 'sec',
1936 $formatunits = [];
1937 foreach ($units as $key => $unit) {
1938 if (empty($interval->$key)) {
1939 continue;
1941 $formatunits[] = $unit;
1943 if (!empty($formatunits)) {
1944 $formatkey = 'dateinterval' . implode("", $formatunits);
1948 if ($fullformat) {
1949 $formatkey .= 'full';
1951 $format = get_string($formatkey, 'langconfig');
1953 return $interval->format($format);
1957 * Given a time, return the GMT timestamp of the most recent midnight
1958 * for the current user.
1960 * @package core
1961 * @category time
1962 * @param int $date Timestamp in GMT
1963 * @param float|int|string $timezone user timezone
1964 * @return int Returns a GMT timestamp
1966 function usergetmidnight($date, $timezone=99) {
1968 $userdate = usergetdate($date, $timezone);
1970 // Time of midnight of this user's day, in GMT.
1971 return make_timestamp($userdate['year'], $userdate['mon'], $userdate['mday'], 0, 0, 0, $timezone);
1976 * Returns a string that prints the user's timezone
1978 * @package core
1979 * @category time
1980 * @param float|int|string $timezone user timezone
1981 * @return string
1983 function usertimezone($timezone=99) {
1984 $tz = core_date::get_user_timezone($timezone);
1985 return core_date::get_localised_timezone($tz);
1989 * Returns a float or a string which denotes the user's timezone
1990 * 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)
1991 * means that for this timezone there are also DST rules to be taken into account
1992 * Checks various settings and picks the most dominant of those which have a value
1994 * @package core
1995 * @category time
1996 * @param float|int|string $tz timezone to calculate GMT time offset before
1997 * calculating user timezone, 99 is default user timezone
1998 * {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
1999 * @return float|string
2001 function get_user_timezone($tz = 99) {
2002 global $USER, $CFG;
2004 $timezones = array(
2005 $tz,
2006 isset($CFG->forcetimezone) ? $CFG->forcetimezone : 99,
2007 isset($USER->timezone) ? $USER->timezone : 99,
2008 isset($CFG->timezone) ? $CFG->timezone : 99,
2011 $tz = 99;
2013 // Loop while $tz is, empty but not zero, or 99, and there is another timezone is the array.
2014 foreach ($timezones as $nextvalue) {
2015 if ((empty($tz) && !is_numeric($tz)) || $tz == 99) {
2016 $tz = $nextvalue;
2019 return is_numeric($tz) ? (float) $tz : $tz;
2023 * Calculates the Daylight Saving Offset for a given date/time (timestamp)
2024 * - Note: Daylight saving only works for string timezones and not for float.
2026 * @package core
2027 * @category time
2028 * @param int $time must NOT be compensated at all, it has to be a pure timestamp
2029 * @param int|float|string $strtimezone user timezone
2030 * @return int
2032 function dst_offset_on($time, $strtimezone = null) {
2033 $tz = core_date::get_user_timezone($strtimezone);
2034 $date = new DateTime('@' . $time);
2035 $date->setTimezone(new DateTimeZone($tz));
2036 if ($date->format('I') == '1') {
2037 if ($tz === 'Australia/Lord_Howe') {
2038 return 1800;
2040 return 3600;
2042 return 0;
2046 * Calculates when the day appears in specific month
2048 * @package core
2049 * @category time
2050 * @param int $startday starting day of the month
2051 * @param int $weekday The day when week starts (normally taken from user preferences)
2052 * @param int $month The month whose day is sought
2053 * @param int $year The year of the month whose day is sought
2054 * @return int
2056 function find_day_in_month($startday, $weekday, $month, $year) {
2057 $calendartype = \core_calendar\type_factory::get_calendar_instance();
2059 $daysinmonth = days_in_month($month, $year);
2060 $daysinweek = count($calendartype->get_weekdays());
2062 if ($weekday == -1) {
2063 // Don't care about weekday, so return:
2064 // abs($startday) if $startday != -1
2065 // $daysinmonth otherwise.
2066 return ($startday == -1) ? $daysinmonth : abs($startday);
2069 // From now on we 're looking for a specific weekday.
2070 // Give "end of month" its actual value, since we know it.
2071 if ($startday == -1) {
2072 $startday = -1 * $daysinmonth;
2075 // Starting from day $startday, the sign is the direction.
2076 if ($startday < 1) {
2077 $startday = abs($startday);
2078 $lastmonthweekday = dayofweek($daysinmonth, $month, $year);
2080 // This is the last such weekday of the month.
2081 $lastinmonth = $daysinmonth + $weekday - $lastmonthweekday;
2082 if ($lastinmonth > $daysinmonth) {
2083 $lastinmonth -= $daysinweek;
2086 // Find the first such weekday <= $startday.
2087 while ($lastinmonth > $startday) {
2088 $lastinmonth -= $daysinweek;
2091 return $lastinmonth;
2092 } else {
2093 $indexweekday = dayofweek($startday, $month, $year);
2095 $diff = $weekday - $indexweekday;
2096 if ($diff < 0) {
2097 $diff += $daysinweek;
2100 // This is the first such weekday of the month equal to or after $startday.
2101 $firstfromindex = $startday + $diff;
2103 return $firstfromindex;
2108 * Calculate the number of days in a given month
2110 * @package core
2111 * @category time
2112 * @param int $month The month whose day count is sought
2113 * @param int $year The year of the month whose day count is sought
2114 * @return int
2116 function days_in_month($month, $year) {
2117 $calendartype = \core_calendar\type_factory::get_calendar_instance();
2118 return $calendartype->get_num_days_in_month($year, $month);
2122 * Calculate the position in the week of a specific calendar day
2124 * @package core
2125 * @category time
2126 * @param int $day The day of the date whose position in the week is sought
2127 * @param int $month The month of the date whose position in the week is sought
2128 * @param int $year The year of the date whose position in the week is sought
2129 * @return int
2131 function dayofweek($day, $month, $year) {
2132 $calendartype = \core_calendar\type_factory::get_calendar_instance();
2133 return $calendartype->get_weekday($year, $month, $day);
2136 // USER AUTHENTICATION AND LOGIN.
2139 * Returns full login url.
2141 * Any form submissions for authentication to this URL must include username,
2142 * password as well as a logintoken generated by \core\session\manager::get_login_token().
2144 * @return string login url
2146 function get_login_url() {
2147 global $CFG;
2149 return "$CFG->wwwroot/login/index.php";
2153 * This function checks that the current user is logged in and has the
2154 * required privileges
2156 * This function checks that the current user is logged in, and optionally
2157 * whether they are allowed to be in a particular course and view a particular
2158 * course module.
2159 * If they are not logged in, then it redirects them to the site login unless
2160 * $autologinguest is set and {@link $CFG}->autologinguests is set to 1 in which
2161 * case they are automatically logged in as guests.
2162 * If $courseid is given and the user is not enrolled in that course then the
2163 * user is redirected to the course enrolment page.
2164 * If $cm is given and the course module is hidden and the user is not a teacher
2165 * in the course then the user is redirected to the course home page.
2167 * When $cm parameter specified, this function sets page layout to 'module'.
2168 * You need to change it manually later if some other layout needed.
2170 * @package core_access
2171 * @category access
2173 * @param mixed $courseorid id of the course or course object
2174 * @param bool $autologinguest default true
2175 * @param object $cm course module object
2176 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
2177 * true. Used to avoid (=false) some scripts (file.php...) to set that variable,
2178 * in order to keep redirects working properly. MDL-14495
2179 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
2180 * @return mixed Void, exit, and die depending on path
2181 * @throws coding_exception
2182 * @throws require_login_exception
2183 * @throws moodle_exception
2185 function require_login($courseorid = null, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) {
2186 global $CFG, $SESSION, $USER, $PAGE, $SITE, $DB, $OUTPUT;
2188 // Must not redirect when byteserving already started.
2189 if (!empty($_SERVER['HTTP_RANGE'])) {
2190 $preventredirect = true;
2193 if (AJAX_SCRIPT) {
2194 // We cannot redirect for AJAX scripts either.
2195 $preventredirect = true;
2198 // Setup global $COURSE, themes, language and locale.
2199 if (!empty($courseorid)) {
2200 if (is_object($courseorid)) {
2201 $course = $courseorid;
2202 } else if ($courseorid == SITEID) {
2203 $course = clone($SITE);
2204 } else {
2205 $course = $DB->get_record('course', array('id' => $courseorid), '*', MUST_EXIST);
2207 if ($cm) {
2208 if ($cm->course != $course->id) {
2209 throw new coding_exception('course and cm parameters in require_login() call do not match!!');
2211 // Make sure we have a $cm from get_fast_modinfo as this contains activity access details.
2212 if (!($cm instanceof cm_info)) {
2213 // Note: nearly all pages call get_fast_modinfo anyway and it does not make any
2214 // db queries so this is not really a performance concern, however it is obviously
2215 // better if you use get_fast_modinfo to get the cm before calling this.
2216 $modinfo = get_fast_modinfo($course);
2217 $cm = $modinfo->get_cm($cm->id);
2220 } else {
2221 // Do not touch global $COURSE via $PAGE->set_course(),
2222 // the reasons is we need to be able to call require_login() at any time!!
2223 $course = $SITE;
2224 if ($cm) {
2225 throw new coding_exception('cm parameter in require_login() requires valid course parameter!');
2229 // If this is an AJAX request and $setwantsurltome is true then we need to override it and set it to false.
2230 // Otherwise the AJAX request URL will be set to $SESSION->wantsurl and events such as self enrolment in the future
2231 // risk leading the user back to the AJAX request URL.
2232 if ($setwantsurltome && defined('AJAX_SCRIPT') && AJAX_SCRIPT) {
2233 $setwantsurltome = false;
2236 // Redirect to the login page if session has expired, only with dbsessions enabled (MDL-35029) to maintain current behaviour.
2237 if ((!isloggedin() or isguestuser()) && !empty($SESSION->has_timed_out) && !empty($CFG->dbsessions)) {
2238 if ($preventredirect) {
2239 throw new require_login_session_timeout_exception();
2240 } else {
2241 if ($setwantsurltome) {
2242 $SESSION->wantsurl = qualified_me();
2244 redirect(get_login_url());
2248 // If the user is not even logged in yet then make sure they are.
2249 if (!isloggedin()) {
2250 if ($autologinguest && !empty($CFG->autologinguests)) {
2251 if (!$guest = get_complete_user_data('id', $CFG->siteguest)) {
2252 // Misconfigured site guest, just redirect to login page.
2253 redirect(get_login_url());
2254 exit; // Never reached.
2256 $lang = isset($SESSION->lang) ? $SESSION->lang : $CFG->lang;
2257 complete_user_login($guest);
2258 $USER->autologinguest = true;
2259 $SESSION->lang = $lang;
2260 } else {
2261 // NOTE: $USER->site check was obsoleted by session test cookie, $USER->confirmed test is in login/index.php.
2262 if ($preventredirect) {
2263 throw new require_login_exception('You are not logged in');
2266 if ($setwantsurltome) {
2267 $SESSION->wantsurl = qualified_me();
2270 // Give auth plugins an opportunity to authenticate or redirect to an external login page
2271 $authsequence = get_enabled_auth_plugins(); // Auths, in sequence.
2272 foreach($authsequence as $authname) {
2273 $authplugin = get_auth_plugin($authname);
2274 $authplugin->pre_loginpage_hook();
2275 if (isloggedin()) {
2276 if ($cm) {
2277 $modinfo = get_fast_modinfo($course);
2278 $cm = $modinfo->get_cm($cm->id);
2280 set_access_log_user();
2281 break;
2285 // If we're still not logged in then go to the login page
2286 if (!isloggedin()) {
2287 redirect(get_login_url());
2288 exit; // Never reached.
2293 // Loginas as redirection if needed.
2294 if ($course->id != SITEID and \core\session\manager::is_loggedinas()) {
2295 if ($USER->loginascontext->contextlevel == CONTEXT_COURSE) {
2296 if ($USER->loginascontext->instanceid != $course->id) {
2297 throw new \moodle_exception('loginasonecourse', '',
2298 $CFG->wwwroot.'/course/view.php?id='.$USER->loginascontext->instanceid);
2303 // Check whether the user should be changing password (but only if it is REALLY them).
2304 if (get_user_preferences('auth_forcepasswordchange') && !\core\session\manager::is_loggedinas()) {
2305 $userauth = get_auth_plugin($USER->auth);
2306 if ($userauth->can_change_password() and !$preventredirect) {
2307 if ($setwantsurltome) {
2308 $SESSION->wantsurl = qualified_me();
2310 if ($changeurl = $userauth->change_password_url()) {
2311 // Use plugin custom url.
2312 redirect($changeurl);
2313 } else {
2314 // Use moodle internal method.
2315 redirect($CFG->wwwroot .'/login/change_password.php');
2317 } else if ($userauth->can_change_password()) {
2318 throw new moodle_exception('forcepasswordchangenotice');
2319 } else {
2320 throw new moodle_exception('nopasswordchangeforced', 'auth');
2324 // Check that the user account is properly set up. If we can't redirect to
2325 // edit their profile and this is not a WS request, perform just the lax check.
2326 // It will allow them to use filepicker on the profile edit page.
2328 if ($preventredirect && !WS_SERVER) {
2329 $usernotfullysetup = user_not_fully_set_up($USER, false);
2330 } else {
2331 $usernotfullysetup = user_not_fully_set_up($USER, true);
2334 if ($usernotfullysetup) {
2335 if ($preventredirect) {
2336 throw new moodle_exception('usernotfullysetup');
2338 if ($setwantsurltome) {
2339 $SESSION->wantsurl = qualified_me();
2341 redirect($CFG->wwwroot .'/user/edit.php?id='. $USER->id .'&amp;course='. SITEID);
2344 // Make sure the USER has a sesskey set up. Used for CSRF protection.
2345 sesskey();
2347 if (\core\session\manager::is_loggedinas()) {
2348 // During a "logged in as" session we should force all content to be cleaned because the
2349 // logged in user will be viewing potentially malicious user generated content.
2350 // See MDL-63786 for more details.
2351 $CFG->forceclean = true;
2354 $afterlogins = get_plugins_with_function('after_require_login', 'lib.php');
2356 // Do not bother admins with any formalities, except for activities pending deletion.
2357 if (is_siteadmin() && !($cm && $cm->deletioninprogress)) {
2358 // Set the global $COURSE.
2359 if ($cm) {
2360 $PAGE->set_cm($cm, $course);
2361 $PAGE->set_pagelayout('incourse');
2362 } else if (!empty($courseorid)) {
2363 $PAGE->set_course($course);
2365 // Set accesstime or the user will appear offline which messes up messaging.
2366 // Do not update access time for webservice or ajax requests.
2367 if (!WS_SERVER && !AJAX_SCRIPT) {
2368 user_accesstime_log($course->id);
2371 foreach ($afterlogins as $plugintype => $plugins) {
2372 foreach ($plugins as $pluginfunction) {
2373 $pluginfunction($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2376 return;
2379 // Scripts have a chance to declare that $USER->policyagreed should not be checked.
2380 // This is mostly for places where users are actually accepting the policies, to avoid the redirect loop.
2381 if (!defined('NO_SITEPOLICY_CHECK')) {
2382 define('NO_SITEPOLICY_CHECK', false);
2385 // Check that the user has agreed to a site policy if there is one - do not test in case of admins.
2386 // Do not test if the script explicitly asked for skipping the site policies check.
2387 // Or if the user auth type is webservice.
2388 if (!$USER->policyagreed && !is_siteadmin() && !NO_SITEPOLICY_CHECK && $USER->auth !== 'webservice') {
2389 $manager = new \core_privacy\local\sitepolicy\manager();
2390 if ($policyurl = $manager->get_redirect_url(isguestuser())) {
2391 if ($preventredirect) {
2392 throw new moodle_exception('sitepolicynotagreed', 'error', '', $policyurl->out());
2394 if ($setwantsurltome) {
2395 $SESSION->wantsurl = qualified_me();
2397 redirect($policyurl);
2401 // Fetch the system context, the course context, and prefetch its child contexts.
2402 $sysctx = context_system::instance();
2403 $coursecontext = context_course::instance($course->id, MUST_EXIST);
2404 if ($cm) {
2405 $cmcontext = context_module::instance($cm->id, MUST_EXIST);
2406 } else {
2407 $cmcontext = null;
2410 // If the site is currently under maintenance, then print a message.
2411 if (!empty($CFG->maintenance_enabled) and !has_capability('moodle/site:maintenanceaccess', $sysctx)) {
2412 if ($preventredirect) {
2413 throw new require_login_exception('Maintenance in progress');
2415 $PAGE->set_context(null);
2416 print_maintenance_message();
2419 // Make sure the course itself is not hidden.
2420 if ($course->id == SITEID) {
2421 // Frontpage can not be hidden.
2422 } else {
2423 if (is_role_switched($course->id)) {
2424 // When switching roles ignore the hidden flag - user had to be in course to do the switch.
2425 } else {
2426 if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
2427 // Originally there was also test of parent category visibility, BUT is was very slow in complex queries
2428 // involving "my courses" now it is also possible to simply hide all courses user is not enrolled in :-).
2429 if ($preventredirect) {
2430 throw new require_login_exception('Course is hidden');
2432 $PAGE->set_context(null);
2433 // We need to override the navigation URL as the course won't have been added to the navigation and thus
2434 // the navigation will mess up when trying to find it.
2435 navigation_node::override_active_url(new moodle_url('/'));
2436 notice(get_string('coursehidden'), $CFG->wwwroot .'/');
2441 // Is the user enrolled?
2442 if ($course->id == SITEID) {
2443 // Everybody is enrolled on the frontpage.
2444 } else {
2445 if (\core\session\manager::is_loggedinas()) {
2446 // Make sure the REAL person can access this course first.
2447 $realuser = \core\session\manager::get_realuser();
2448 if (!is_enrolled($coursecontext, $realuser->id, '', true) and
2449 !is_viewing($coursecontext, $realuser->id) and !is_siteadmin($realuser->id)) {
2450 if ($preventredirect) {
2451 throw new require_login_exception('Invalid course login-as access');
2453 $PAGE->set_context(null);
2454 echo $OUTPUT->header();
2455 notice(get_string('studentnotallowed', '', fullname($USER, true)), $CFG->wwwroot .'/');
2459 $access = false;
2461 if (is_role_switched($course->id)) {
2462 // Ok, user had to be inside this course before the switch.
2463 $access = true;
2465 } else if (is_viewing($coursecontext, $USER)) {
2466 // Ok, no need to mess with enrol.
2467 $access = true;
2469 } else {
2470 if (isset($USER->enrol['enrolled'][$course->id])) {
2471 if ($USER->enrol['enrolled'][$course->id] > time()) {
2472 $access = true;
2473 if (isset($USER->enrol['tempguest'][$course->id])) {
2474 unset($USER->enrol['tempguest'][$course->id]);
2475 remove_temp_course_roles($coursecontext);
2477 } else {
2478 // Expired.
2479 unset($USER->enrol['enrolled'][$course->id]);
2482 if (isset($USER->enrol['tempguest'][$course->id])) {
2483 if ($USER->enrol['tempguest'][$course->id] == 0) {
2484 $access = true;
2485 } else if ($USER->enrol['tempguest'][$course->id] > time()) {
2486 $access = true;
2487 } else {
2488 // Expired.
2489 unset($USER->enrol['tempguest'][$course->id]);
2490 remove_temp_course_roles($coursecontext);
2494 if (!$access) {
2495 // Cache not ok.
2496 $until = enrol_get_enrolment_end($coursecontext->instanceid, $USER->id);
2497 if ($until !== false) {
2498 // Active participants may always access, a timestamp in the future, 0 (always) or false.
2499 if ($until == 0) {
2500 $until = ENROL_MAX_TIMESTAMP;
2502 $USER->enrol['enrolled'][$course->id] = $until;
2503 $access = true;
2505 } else if (core_course_category::can_view_course_info($course)) {
2506 $params = array('courseid' => $course->id, 'status' => ENROL_INSTANCE_ENABLED);
2507 $instances = $DB->get_records('enrol', $params, 'sortorder, id ASC');
2508 $enrols = enrol_get_plugins(true);
2509 // First ask all enabled enrol instances in course if they want to auto enrol user.
2510 foreach ($instances as $instance) {
2511 if (!isset($enrols[$instance->enrol])) {
2512 continue;
2514 // Get a duration for the enrolment, a timestamp in the future, 0 (always) or false.
2515 $until = $enrols[$instance->enrol]->try_autoenrol($instance);
2516 if ($until !== false) {
2517 if ($until == 0) {
2518 $until = ENROL_MAX_TIMESTAMP;
2520 $USER->enrol['enrolled'][$course->id] = $until;
2521 $access = true;
2522 break;
2525 // If not enrolled yet try to gain temporary guest access.
2526 if (!$access) {
2527 foreach ($instances as $instance) {
2528 if (!isset($enrols[$instance->enrol])) {
2529 continue;
2531 // Get a duration for the guest access, a timestamp in the future or false.
2532 $until = $enrols[$instance->enrol]->try_guestaccess($instance);
2533 if ($until !== false and $until > time()) {
2534 $USER->enrol['tempguest'][$course->id] = $until;
2535 $access = true;
2536 break;
2540 } else {
2541 // User is not enrolled and is not allowed to browse courses here.
2542 if ($preventredirect) {
2543 throw new require_login_exception('Course is not available');
2545 $PAGE->set_context(null);
2546 // We need to override the navigation URL as the course won't have been added to the navigation and thus
2547 // the navigation will mess up when trying to find it.
2548 navigation_node::override_active_url(new moodle_url('/'));
2549 notice(get_string('coursehidden'), $CFG->wwwroot .'/');
2554 if (!$access) {
2555 if ($preventredirect) {
2556 throw new require_login_exception('Not enrolled');
2558 if ($setwantsurltome) {
2559 $SESSION->wantsurl = qualified_me();
2561 redirect($CFG->wwwroot .'/enrol/index.php?id='. $course->id);
2565 // Check whether the activity has been scheduled for deletion. If so, then deny access, even for admins.
2566 if ($cm && $cm->deletioninprogress) {
2567 if ($preventredirect) {
2568 throw new moodle_exception('activityisscheduledfordeletion');
2570 require_once($CFG->dirroot . '/course/lib.php');
2571 redirect(course_get_url($course), get_string('activityisscheduledfordeletion', 'error'));
2574 // Check visibility of activity to current user; includes visible flag, conditional availability, etc.
2575 if ($cm && !$cm->uservisible) {
2576 if ($preventredirect) {
2577 throw new require_login_exception('Activity is hidden');
2579 // Get the error message that activity is not available and why (if explanation can be shown to the user).
2580 $PAGE->set_course($course);
2581 $renderer = $PAGE->get_renderer('course');
2582 $message = $renderer->course_section_cm_unavailable_error_message($cm);
2583 redirect(course_get_url($course), $message, null, \core\output\notification::NOTIFY_ERROR);
2586 // Set the global $COURSE.
2587 if ($cm) {
2588 $PAGE->set_cm($cm, $course);
2589 $PAGE->set_pagelayout('incourse');
2590 } else if (!empty($courseorid)) {
2591 $PAGE->set_course($course);
2594 foreach ($afterlogins as $plugintype => $plugins) {
2595 foreach ($plugins as $pluginfunction) {
2596 $pluginfunction($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2600 // Finally access granted, update lastaccess times.
2601 // Do not update access time for webservice or ajax requests.
2602 if (!WS_SERVER && !AJAX_SCRIPT) {
2603 user_accesstime_log($course->id);
2608 * A convenience function for where we must be logged in as admin
2609 * @return void
2611 function require_admin() {
2612 require_login(null, false);
2613 require_capability('moodle/site:config', context_system::instance());
2617 * This function just makes sure a user is logged out.
2619 * @package core_access
2620 * @category access
2622 function require_logout() {
2623 global $USER, $DB;
2625 if (!isloggedin()) {
2626 // This should not happen often, no need for hooks or events here.
2627 \core\session\manager::terminate_current();
2628 return;
2631 // Execute hooks before action.
2632 $authplugins = array();
2633 $authsequence = get_enabled_auth_plugins();
2634 foreach ($authsequence as $authname) {
2635 $authplugins[$authname] = get_auth_plugin($authname);
2636 $authplugins[$authname]->prelogout_hook();
2639 // Store info that gets removed during logout.
2640 $sid = session_id();
2641 $event = \core\event\user_loggedout::create(
2642 array(
2643 'userid' => $USER->id,
2644 'objectid' => $USER->id,
2645 'other' => array('sessionid' => $sid),
2648 if ($session = $DB->get_record('sessions', array('sid'=>$sid))) {
2649 $event->add_record_snapshot('sessions', $session);
2652 // Clone of $USER object to be used by auth plugins.
2653 $user = fullclone($USER);
2655 // Delete session record and drop $_SESSION content.
2656 \core\session\manager::terminate_current();
2658 // Trigger event AFTER action.
2659 $event->trigger();
2661 // Hook to execute auth plugins redirection after event trigger.
2662 foreach ($authplugins as $authplugin) {
2663 $authplugin->postlogout_hook($user);
2668 * Weaker version of require_login()
2670 * This is a weaker version of {@link require_login()} which only requires login
2671 * when called from within a course rather than the site page, unless
2672 * the forcelogin option is turned on.
2673 * @see require_login()
2675 * @package core_access
2676 * @category access
2678 * @param mixed $courseorid The course object or id in question
2679 * @param bool $autologinguest Allow autologin guests if that is wanted
2680 * @param object $cm Course activity module if known
2681 * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to
2682 * true. Used to avoid (=false) some scripts (file.php...) to set that variable,
2683 * in order to keep redirects working properly. MDL-14495
2684 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions
2685 * @return void
2686 * @throws coding_exception
2688 function require_course_login($courseorid, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) {
2689 global $CFG, $PAGE, $SITE;
2690 $issite = ((is_object($courseorid) and $courseorid->id == SITEID)
2691 or (!is_object($courseorid) and $courseorid == SITEID));
2692 if ($issite && !empty($cm) && !($cm instanceof cm_info)) {
2693 // Note: nearly all pages call get_fast_modinfo anyway and it does not make any
2694 // db queries so this is not really a performance concern, however it is obviously
2695 // better if you use get_fast_modinfo to get the cm before calling this.
2696 if (is_object($courseorid)) {
2697 $course = $courseorid;
2698 } else {
2699 $course = clone($SITE);
2701 $modinfo = get_fast_modinfo($course);
2702 $cm = $modinfo->get_cm($cm->id);
2704 if (!empty($CFG->forcelogin)) {
2705 // Login required for both SITE and courses.
2706 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2708 } else if ($issite && !empty($cm) and !$cm->uservisible) {
2709 // Always login for hidden activities.
2710 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2712 } else if (isloggedin() && !isguestuser()) {
2713 // User is already logged in. Make sure the login is complete (user is fully setup, policies agreed).
2714 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2716 } else if ($issite) {
2717 // Login for SITE not required.
2718 // We still need to instatiate PAGE vars properly so that things that rely on it like navigation function correctly.
2719 if (!empty($courseorid)) {
2720 if (is_object($courseorid)) {
2721 $course = $courseorid;
2722 } else {
2723 $course = clone $SITE;
2725 if ($cm) {
2726 if ($cm->course != $course->id) {
2727 throw new coding_exception('course and cm parameters in require_course_login() call do not match!!');
2729 $PAGE->set_cm($cm, $course);
2730 $PAGE->set_pagelayout('incourse');
2731 } else {
2732 $PAGE->set_course($course);
2734 } else {
2735 // If $PAGE->course, and hence $PAGE->context, have not already been set up properly, set them up now.
2736 $PAGE->set_course($PAGE->course);
2738 // Do not update access time for webservice or ajax requests.
2739 if (!WS_SERVER && !AJAX_SCRIPT) {
2740 user_accesstime_log(SITEID);
2742 return;
2744 } else {
2745 // Course login always required.
2746 require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect);
2751 * Validates a user key, checking if the key exists, is not expired and the remote ip is correct.
2753 * @param string $keyvalue the key value
2754 * @param string $script unique script identifier
2755 * @param int $instance instance id
2756 * @return stdClass the key entry in the user_private_key table
2757 * @since Moodle 3.2
2758 * @throws moodle_exception
2760 function validate_user_key($keyvalue, $script, $instance) {
2761 global $DB;
2763 if (!$key = $DB->get_record('user_private_key', array('script' => $script, 'value' => $keyvalue, 'instance' => $instance))) {
2764 throw new \moodle_exception('invalidkey');
2767 if (!empty($key->validuntil) and $key->validuntil < time()) {
2768 throw new \moodle_exception('expiredkey');
2771 if ($key->iprestriction) {
2772 $remoteaddr = getremoteaddr(null);
2773 if (empty($remoteaddr) or !address_in_subnet($remoteaddr, $key->iprestriction)) {
2774 throw new \moodle_exception('ipmismatch');
2777 return $key;
2781 * Require key login. Function terminates with error if key not found or incorrect.
2783 * @uses NO_MOODLE_COOKIES
2784 * @uses PARAM_ALPHANUM
2785 * @param string $script unique script identifier
2786 * @param int $instance optional instance id
2787 * @param string $keyvalue The key. If not supplied, this will be fetched from the current session.
2788 * @return int Instance ID
2790 function require_user_key_login($script, $instance = null, $keyvalue = null) {
2791 global $DB;
2793 if (!NO_MOODLE_COOKIES) {
2794 throw new \moodle_exception('sessioncookiesdisable');
2797 // Extra safety.
2798 \core\session\manager::write_close();
2800 if (null === $keyvalue) {
2801 $keyvalue = required_param('key', PARAM_ALPHANUM);
2804 $key = validate_user_key($keyvalue, $script, $instance);
2806 if (!$user = $DB->get_record('user', array('id' => $key->userid))) {
2807 throw new \moodle_exception('invaliduserid');
2810 core_user::require_active_user($user, true, true);
2812 // Emulate normal session.
2813 enrol_check_plugins($user, false);
2814 \core\session\manager::set_user($user);
2816 // Note we are not using normal login.
2817 if (!defined('USER_KEY_LOGIN')) {
2818 define('USER_KEY_LOGIN', true);
2821 // Return instance id - it might be empty.
2822 return $key->instance;
2826 * Creates a new private user access key.
2828 * @param string $script unique target identifier
2829 * @param int $userid
2830 * @param int $instance optional instance id
2831 * @param string $iprestriction optional ip restricted access
2832 * @param int $validuntil key valid only until given data
2833 * @return string access key value
2835 function create_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
2836 global $DB;
2838 $key = new stdClass();
2839 $key->script = $script;
2840 $key->userid = $userid;
2841 $key->instance = $instance;
2842 $key->iprestriction = $iprestriction;
2843 $key->validuntil = $validuntil;
2844 $key->timecreated = time();
2846 // Something long and unique.
2847 $key->value = md5($userid.'_'.time().random_string(40));
2848 while ($DB->record_exists('user_private_key', array('value' => $key->value))) {
2849 // Must be unique.
2850 $key->value = md5($userid.'_'.time().random_string(40));
2852 $DB->insert_record('user_private_key', $key);
2853 return $key->value;
2857 * Delete the user's new private user access keys for a particular script.
2859 * @param string $script unique target identifier
2860 * @param int $userid
2861 * @return void
2863 function delete_user_key($script, $userid) {
2864 global $DB;
2865 $DB->delete_records('user_private_key', array('script' => $script, 'userid' => $userid));
2869 * Gets a private user access key (and creates one if one doesn't exist).
2871 * @param string $script unique target identifier
2872 * @param int $userid
2873 * @param int $instance optional instance id
2874 * @param string $iprestriction optional ip restricted access
2875 * @param int $validuntil key valid only until given date
2876 * @return string access key value
2878 function get_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) {
2879 global $DB;
2881 if ($key = $DB->get_record('user_private_key', array('script' => $script, 'userid' => $userid,
2882 'instance' => $instance, 'iprestriction' => $iprestriction,
2883 'validuntil' => $validuntil))) {
2884 return $key->value;
2885 } else {
2886 return create_user_key($script, $userid, $instance, $iprestriction, $validuntil);
2892 * Modify the user table by setting the currently logged in user's last login to now.
2894 * @return bool Always returns true
2896 function update_user_login_times() {
2897 global $USER, $DB, $SESSION;
2899 if (isguestuser()) {
2900 // Do not update guest access times/ips for performance.
2901 return true;
2904 if (defined('USER_KEY_LOGIN') && USER_KEY_LOGIN === true) {
2905 // Do not update user login time when using user key login.
2906 return true;
2909 $now = time();
2911 $user = new stdClass();
2912 $user->id = $USER->id;
2914 // Make sure all users that logged in have some firstaccess.
2915 if ($USER->firstaccess == 0) {
2916 $USER->firstaccess = $user->firstaccess = $now;
2919 // Store the previous current as lastlogin.
2920 $USER->lastlogin = $user->lastlogin = $USER->currentlogin;
2922 $USER->currentlogin = $user->currentlogin = $now;
2924 // Function user_accesstime_log() may not update immediately, better do it here.
2925 $USER->lastaccess = $user->lastaccess = $now;
2926 $SESSION->userpreviousip = $USER->lastip;
2927 $USER->lastip = $user->lastip = getremoteaddr();
2929 // Note: do not call user_update_user() here because this is part of the login process,
2930 // the login event means that these fields were updated.
2931 $DB->update_record('user', $user);
2932 return true;
2936 * Determines if a user has completed setting up their account.
2938 * The lax mode (with $strict = false) has been introduced for special cases
2939 * only where we want to skip certain checks intentionally. This is valid in
2940 * certain mnet or ajax scenarios when the user cannot / should not be
2941 * redirected to edit their profile. In most cases, you should perform the
2942 * strict check.
2944 * @param stdClass $user A {@link $USER} object to test for the existence of a valid name and email
2945 * @param bool $strict Be more strict and assert id and custom profile fields set, too
2946 * @return bool
2948 function user_not_fully_set_up($user, $strict = true) {
2949 global $CFG, $SESSION, $USER;
2950 require_once($CFG->dirroot.'/user/profile/lib.php');
2952 // If the user is setup then store this in the session to avoid re-checking.
2953 // Some edge cases are when the users email starts to bounce or the
2954 // configuration for custom fields has changed while they are logged in so
2955 // we re-check this fully every hour for the rare cases it has changed.
2956 if (isset($USER->id) && isset($user->id) && $USER->id === $user->id &&
2957 isset($SESSION->fullysetupstrict) && (time() - $SESSION->fullysetupstrict) < HOURSECS) {
2958 return false;
2961 if (isguestuser($user)) {
2962 return false;
2965 if (empty($user->firstname) or empty($user->lastname) or empty($user->email) or over_bounce_threshold($user)) {
2966 return true;
2969 if ($strict) {
2970 if (empty($user->id)) {
2971 // Strict mode can be used with existing accounts only.
2972 return true;
2974 if (!profile_has_required_custom_fields_set($user->id)) {
2975 return true;
2977 if (isset($USER->id) && isset($user->id) && $USER->id === $user->id) {
2978 $SESSION->fullysetupstrict = time();
2982 return false;
2986 * Check whether the user has exceeded the bounce threshold
2988 * @param stdClass $user A {@link $USER} object
2989 * @return bool true => User has exceeded bounce threshold
2991 function over_bounce_threshold($user) {
2992 global $CFG, $DB;
2994 if (empty($CFG->handlebounces)) {
2995 return false;
2998 if (empty($user->id)) {
2999 // No real (DB) user, nothing to do here.
3000 return false;
3003 // Set sensible defaults.
3004 if (empty($CFG->minbounces)) {
3005 $CFG->minbounces = 10;
3007 if (empty($CFG->bounceratio)) {
3008 $CFG->bounceratio = .20;
3010 $bouncecount = 0;
3011 $sendcount = 0;
3012 if ($bounce = $DB->get_record('user_preferences', array ('userid' => $user->id, 'name' => 'email_bounce_count'))) {
3013 $bouncecount = $bounce->value;
3015 if ($send = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) {
3016 $sendcount = $send->value;
3018 return ($bouncecount >= $CFG->minbounces && $bouncecount/$sendcount >= $CFG->bounceratio);
3022 * Used to increment or reset email sent count
3024 * @param stdClass $user object containing an id
3025 * @param bool $reset will reset the count to 0
3026 * @return void
3028 function set_send_count($user, $reset=false) {
3029 global $DB;
3031 if (empty($user->id)) {
3032 // No real (DB) user, nothing to do here.
3033 return;
3036 if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) {
3037 $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
3038 $DB->update_record('user_preferences', $pref);
3039 } else if (!empty($reset)) {
3040 // If it's not there and we're resetting, don't bother. Make a new one.
3041 $pref = new stdClass();
3042 $pref->name = 'email_send_count';
3043 $pref->value = 1;
3044 $pref->userid = $user->id;
3045 $DB->insert_record('user_preferences', $pref, false);
3050 * Increment or reset user's email bounce count
3052 * @param stdClass $user object containing an id
3053 * @param bool $reset will reset the count to 0
3055 function set_bounce_count($user, $reset=false) {
3056 global $DB;
3058 if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_bounce_count'))) {
3059 $pref->value = (!empty($reset)) ? 0 : $pref->value+1;
3060 $DB->update_record('user_preferences', $pref);
3061 } else if (!empty($reset)) {
3062 // If it's not there and we're resetting, don't bother. Make a new one.
3063 $pref = new stdClass();
3064 $pref->name = 'email_bounce_count';
3065 $pref->value = 1;
3066 $pref->userid = $user->id;
3067 $DB->insert_record('user_preferences', $pref, false);
3072 * Determines if the logged in user is currently moving an activity
3074 * @param int $courseid The id of the course being tested
3075 * @return bool
3077 function ismoving($courseid) {
3078 global $USER;
3080 if (!empty($USER->activitycopy)) {
3081 return ($USER->activitycopycourse == $courseid);
3083 return false;
3087 * Returns a persons full name
3089 * Given an object containing all of the users name values, this function returns a string with the full name of the person.
3090 * The result may depend on system settings or language. 'override' will force the alternativefullnameformat to be used. In
3091 * English, fullname as well as alternativefullnameformat is set to 'firstname lastname' by default. But you could have
3092 * fullname set to 'firstname lastname' and alternativefullnameformat set to 'firstname middlename alternatename lastname'.
3094 * @param stdClass $user A {@link $USER} object to get full name of.
3095 * @param bool $override If true then the alternativefullnameformat format rather than fullnamedisplay format will be used.
3096 * @return string
3098 function fullname($user, $override=false) {
3099 // Note: We do not intend to deprecate this function any time soon as it is too widely used at this time.
3100 // Uses of it should be updated to use the new API and pass updated arguments.
3102 // Return an empty string if there is no user.
3103 if (empty($user)) {
3104 return '';
3107 $options = ['override' => $override];
3108 return core_user::get_fullname($user, null, $options);
3112 * Reduces lines of duplicated code for getting user name fields.
3114 * See also {@link user_picture::unalias()}
3116 * @param object $addtoobject Object to add user name fields to.
3117 * @param object $secondobject Object that contains user name field information.
3118 * @param string $prefix prefix to be added to all fields (including $additionalfields) e.g. authorfirstname.
3119 * @param array $additionalfields Additional fields to be matched with data in the second object.
3120 * The key can be set to the user table field name.
3121 * @return object User name fields.
3123 function username_load_fields_from_object($addtoobject, $secondobject, $prefix = null, $additionalfields = null) {
3124 $fields = [];
3125 foreach (\core_user\fields::get_name_fields() as $field) {
3126 $fields[$field] = $prefix . $field;
3128 if ($additionalfields) {
3129 // Additional fields can specify their own 'alias' such as 'id' => 'userid'. This checks to see if
3130 // the key is a number and then sets the key to the array value.
3131 foreach ($additionalfields as $key => $value) {
3132 if (is_numeric($key)) {
3133 $additionalfields[$value] = $prefix . $value;
3134 unset($additionalfields[$key]);
3135 } else {
3136 $additionalfields[$key] = $prefix . $value;
3139 $fields = array_merge($fields, $additionalfields);
3141 foreach ($fields as $key => $field) {
3142 // Important that we have all of the user name fields present in the object that we are sending back.
3143 $addtoobject->$key = '';
3144 if (isset($secondobject->$field)) {
3145 $addtoobject->$key = $secondobject->$field;
3148 return $addtoobject;
3152 * Returns an array of values in order of occurance in a provided string.
3153 * The key in the result is the character postion in the string.
3155 * @param array $values Values to be found in the string format
3156 * @param string $stringformat The string which may contain values being searched for.
3157 * @return array An array of values in order according to placement in the string format.
3159 function order_in_string($values, $stringformat) {
3160 $valuearray = array();
3161 foreach ($values as $value) {
3162 $pattern = "/$value\b/";
3163 // Using preg_match as strpos() may match values that are similar e.g. firstname and firstnamephonetic.
3164 if (preg_match($pattern, $stringformat)) {
3165 $replacement = "thing";
3166 // Replace the value with something more unique to ensure we get the right position when using strpos().
3167 $newformat = preg_replace($pattern, $replacement, $stringformat);
3168 $position = strpos($newformat, $replacement);
3169 $valuearray[$position] = $value;
3172 ksort($valuearray);
3173 return $valuearray;
3177 * Returns whether a given authentication plugin exists.
3179 * @param string $auth Form of authentication to check for. Defaults to the global setting in {@link $CFG}.
3180 * @return boolean Whether the plugin is available.
3182 function exists_auth_plugin($auth) {
3183 global $CFG;
3185 if (file_exists("{$CFG->dirroot}/auth/$auth/auth.php")) {
3186 return is_readable("{$CFG->dirroot}/auth/$auth/auth.php");
3188 return false;
3192 * Checks if a given plugin is in the list of enabled authentication plugins.
3194 * @param string $auth Authentication plugin.
3195 * @return boolean Whether the plugin is enabled.
3197 function is_enabled_auth($auth) {
3198 if (empty($auth)) {
3199 return false;
3202 $enabled = get_enabled_auth_plugins();
3204 return in_array($auth, $enabled);
3208 * Returns an authentication plugin instance.
3210 * @param string $auth name of authentication plugin
3211 * @return auth_plugin_base An instance of the required authentication plugin.
3213 function get_auth_plugin($auth) {
3214 global $CFG;
3216 // Check the plugin exists first.
3217 if (! exists_auth_plugin($auth)) {
3218 throw new \moodle_exception('authpluginnotfound', 'debug', '', $auth);
3221 // Return auth plugin instance.
3222 require_once("{$CFG->dirroot}/auth/$auth/auth.php");
3223 $class = "auth_plugin_$auth";
3224 return new $class;
3228 * Returns array of active auth plugins.
3230 * @param bool $fix fix $CFG->auth if needed. Only set if logged in as admin.
3231 * @return array
3233 function get_enabled_auth_plugins($fix=false) {
3234 global $CFG;
3236 $default = array('manual', 'nologin');
3238 if (empty($CFG->auth)) {
3239 $auths = array();
3240 } else {
3241 $auths = explode(',', $CFG->auth);
3244 $auths = array_unique($auths);
3245 $oldauthconfig = implode(',', $auths);
3246 foreach ($auths as $k => $authname) {
3247 if (in_array($authname, $default)) {
3248 // The manual and nologin plugin never need to be stored.
3249 unset($auths[$k]);
3250 } else if (!exists_auth_plugin($authname)) {
3251 debugging(get_string('authpluginnotfound', 'debug', $authname));
3252 unset($auths[$k]);
3256 // Ideally only explicit interaction from a human admin should trigger a
3257 // change in auth config, see MDL-70424 for details.
3258 if ($fix) {
3259 $newconfig = implode(',', $auths);
3260 if (!isset($CFG->auth) or $newconfig != $CFG->auth) {
3261 add_to_config_log('auth', $oldauthconfig, $newconfig, 'core');
3262 set_config('auth', $newconfig);
3266 return (array_merge($default, $auths));
3270 * Returns true if an internal authentication method is being used.
3271 * if method not specified then, global default is assumed
3273 * @param string $auth Form of authentication required
3274 * @return bool
3276 function is_internal_auth($auth) {
3277 // Throws error if bad $auth.
3278 $authplugin = get_auth_plugin($auth);
3279 return $authplugin->is_internal();
3283 * Returns true if the user is a 'restored' one.
3285 * Used in the login process to inform the user and allow him/her to reset the password
3287 * @param string $username username to be checked
3288 * @return bool
3290 function is_restored_user($username) {
3291 global $CFG, $DB;
3293 return $DB->record_exists('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id, 'password' => 'restored'));
3297 * Returns an array of user fields
3299 * @return array User field/column names
3301 function get_user_fieldnames() {
3302 global $DB;
3304 $fieldarray = $DB->get_columns('user');
3305 unset($fieldarray['id']);
3306 $fieldarray = array_keys($fieldarray);
3308 return $fieldarray;
3312 * Returns the string of the language for the new user.
3314 * @return string language for the new user
3316 function get_newuser_language() {
3317 global $CFG, $SESSION;
3318 return (!empty($CFG->autolangusercreation) && !empty($SESSION->lang)) ? $SESSION->lang : $CFG->lang;
3322 * Creates a bare-bones user record
3324 * @todo Outline auth types and provide code example
3326 * @param string $username New user's username to add to record
3327 * @param string $password New user's password to add to record
3328 * @param string $auth Form of authentication required
3329 * @return stdClass A complete user object
3331 function create_user_record($username, $password, $auth = 'manual') {
3332 global $CFG, $DB, $SESSION;
3333 require_once($CFG->dirroot.'/user/profile/lib.php');
3334 require_once($CFG->dirroot.'/user/lib.php');
3336 // Just in case check text case.
3337 $username = trim(core_text::strtolower($username));
3339 $authplugin = get_auth_plugin($auth);
3340 $customfields = $authplugin->get_custom_user_profile_fields();
3341 $newuser = new stdClass();
3342 if ($newinfo = $authplugin->get_userinfo($username)) {
3343 $newinfo = truncate_userinfo($newinfo);
3344 foreach ($newinfo as $key => $value) {
3345 if (in_array($key, $authplugin->userfields) || (in_array($key, $customfields))) {
3346 $newuser->$key = $value;
3351 if (!empty($newuser->email)) {
3352 if (email_is_not_allowed($newuser->email)) {
3353 unset($newuser->email);
3357 $newuser->auth = $auth;
3358 $newuser->username = $username;
3360 // Fix for MDL-8480
3361 // user CFG lang for user if $newuser->lang is empty
3362 // or $user->lang is not an installed language.
3363 if (empty($newuser->lang) || !get_string_manager()->translation_exists($newuser->lang)) {
3364 $newuser->lang = get_newuser_language();
3366 $newuser->confirmed = 1;
3367 $newuser->lastip = getremoteaddr();
3368 $newuser->timecreated = time();
3369 $newuser->timemodified = $newuser->timecreated;
3370 $newuser->mnethostid = $CFG->mnet_localhost_id;
3372 $newuser->id = user_create_user($newuser, false, false);
3374 // Save user profile data.
3375 profile_save_data($newuser);
3377 $user = get_complete_user_data('id', $newuser->id);
3378 if (!empty($CFG->{'auth_'.$newuser->auth.'_forcechangepassword'})) {
3379 set_user_preference('auth_forcepasswordchange', 1, $user);
3381 // Set the password.
3382 update_internal_user_password($user, $password);
3384 // Trigger event.
3385 \core\event\user_created::create_from_userid($newuser->id)->trigger();
3387 return $user;
3391 * Will update a local user record from an external source (MNET users can not be updated using this method!).
3393 * @param string $username user's username to update the record
3394 * @return stdClass A complete user object
3396 function update_user_record($username) {
3397 global $DB, $CFG;
3398 // Just in case check text case.
3399 $username = trim(core_text::strtolower($username));
3401 $oldinfo = $DB->get_record('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id), '*', MUST_EXIST);
3402 return update_user_record_by_id($oldinfo->id);
3406 * Will update a local user record from an external source (MNET users can not be updated using this method!).
3408 * @param int $id user id
3409 * @return stdClass A complete user object
3411 function update_user_record_by_id($id) {
3412 global $DB, $CFG;
3413 require_once($CFG->dirroot."/user/profile/lib.php");
3414 require_once($CFG->dirroot.'/user/lib.php');
3416 $params = array('mnethostid' => $CFG->mnet_localhost_id, 'id' => $id, 'deleted' => 0);
3417 $oldinfo = $DB->get_record('user', $params, '*', MUST_EXIST);
3419 $newuser = array();
3420 $userauth = get_auth_plugin($oldinfo->auth);
3422 if ($newinfo = $userauth->get_userinfo($oldinfo->username)) {
3423 $newinfo = truncate_userinfo($newinfo);
3424 $customfields = $userauth->get_custom_user_profile_fields();
3426 foreach ($newinfo as $key => $value) {
3427 $iscustom = in_array($key, $customfields);
3428 if (!$iscustom) {
3429 $key = strtolower($key);
3431 if ((!property_exists($oldinfo, $key) && !$iscustom) or $key === 'username' or $key === 'id'
3432 or $key === 'auth' or $key === 'mnethostid' or $key === 'deleted') {
3433 // Unknown or must not be changed.
3434 continue;
3436 if (empty($userauth->config->{'field_updatelocal_' . $key}) || empty($userauth->config->{'field_lock_' . $key})) {
3437 continue;
3439 $confval = $userauth->config->{'field_updatelocal_' . $key};
3440 $lockval = $userauth->config->{'field_lock_' . $key};
3441 if ($confval === 'onlogin') {
3442 // MDL-4207 Don't overwrite modified user profile values with
3443 // empty LDAP values when 'unlocked if empty' is set. The purpose
3444 // of the setting 'unlocked if empty' is to allow the user to fill
3445 // in a value for the selected field _if LDAP is giving
3446 // nothing_ for this field. Thus it makes sense to let this value
3447 // stand in until LDAP is giving a value for this field.
3448 if (!(empty($value) && $lockval === 'unlockedifempty')) {
3449 if ($iscustom || (in_array($key, $userauth->userfields) &&
3450 ((string)$oldinfo->$key !== (string)$value))) {
3451 $newuser[$key] = (string)$value;
3456 if ($newuser) {
3457 $newuser['id'] = $oldinfo->id;
3458 $newuser['timemodified'] = time();
3459 user_update_user((object) $newuser, false, false);
3461 // Save user profile data.
3462 profile_save_data((object) $newuser);
3464 // Trigger event.
3465 \core\event\user_updated::create_from_userid($newuser['id'])->trigger();
3469 return get_complete_user_data('id', $oldinfo->id);
3473 * Will truncate userinfo as it comes from auth_get_userinfo (from external auth) which may have large fields.
3475 * @param array $info Array of user properties to truncate if needed
3476 * @return array The now truncated information that was passed in
3478 function truncate_userinfo(array $info) {
3479 // Define the limits.
3480 $limit = array(
3481 'username' => 100,
3482 'idnumber' => 255,
3483 'firstname' => 100,
3484 'lastname' => 100,
3485 'email' => 100,
3486 'phone1' => 20,
3487 'phone2' => 20,
3488 'institution' => 255,
3489 'department' => 255,
3490 'address' => 255,
3491 'city' => 120,
3492 'country' => 2,
3495 // Apply where needed.
3496 foreach (array_keys($info) as $key) {
3497 if (!empty($limit[$key])) {
3498 $info[$key] = trim(core_text::substr($info[$key], 0, $limit[$key]));
3502 return $info;
3506 * Marks user deleted in internal user database and notifies the auth plugin.
3507 * Also unenrols user from all roles and does other cleanup.
3509 * Any plugin that needs to purge user data should register the 'user_deleted' event.
3511 * @param stdClass $user full user object before delete
3512 * @return boolean success
3513 * @throws coding_exception if invalid $user parameter detected
3515 function delete_user(stdClass $user) {
3516 global $CFG, $DB, $SESSION;
3517 require_once($CFG->libdir.'/grouplib.php');
3518 require_once($CFG->libdir.'/gradelib.php');
3519 require_once($CFG->dirroot.'/message/lib.php');
3520 require_once($CFG->dirroot.'/user/lib.php');
3522 // Make sure nobody sends bogus record type as parameter.
3523 if (!property_exists($user, 'id') or !property_exists($user, 'username')) {
3524 throw new coding_exception('Invalid $user parameter in delete_user() detected');
3527 // Better not trust the parameter and fetch the latest info this will be very expensive anyway.
3528 if (!$user = $DB->get_record('user', array('id' => $user->id))) {
3529 debugging('Attempt to delete unknown user account.');
3530 return false;
3533 // There must be always exactly one guest record, originally the guest account was identified by username only,
3534 // now we use $CFG->siteguest for performance reasons.
3535 if ($user->username === 'guest' or isguestuser($user)) {
3536 debugging('Guest user account can not be deleted.');
3537 return false;
3540 // Admin can be theoretically from different auth plugin, but we want to prevent deletion of internal accoutns only,
3541 // if anything goes wrong ppl may force somebody to be admin via config.php setting $CFG->siteadmins.
3542 if ($user->auth === 'manual' and is_siteadmin($user)) {
3543 debugging('Local administrator accounts can not be deleted.');
3544 return false;
3547 // Allow plugins to use this user object before we completely delete it.
3548 if ($pluginsfunction = get_plugins_with_function('pre_user_delete')) {
3549 foreach ($pluginsfunction as $plugintype => $plugins) {
3550 foreach ($plugins as $pluginfunction) {
3551 $pluginfunction($user);
3556 // Keep user record before updating it, as we have to pass this to user_deleted event.
3557 $olduser = clone $user;
3559 // Keep a copy of user context, we need it for event.
3560 $usercontext = context_user::instance($user->id);
3562 // Remove user from communication rooms immediately.
3563 if (core_communication\api::is_available()) {
3564 foreach (enrol_get_users_courses($user->id) as $course) {
3565 $communication = \core_communication\processor::load_by_instance(
3566 context: \core\context\course::instance($course->id),
3567 component: 'core_course',
3568 instancetype: 'coursecommunication',
3569 instanceid: $course->id,
3571 if ($communication !== null) {
3572 $communication->get_room_user_provider()->remove_members_from_room([$user->id]);
3573 $communication->delete_instance_user_mapping([$user->id]);
3578 // Delete all grades - backup is kept in grade_grades_history table.
3579 grade_user_delete($user->id);
3581 // TODO: remove from cohorts using standard API here.
3583 // Remove user tags.
3584 core_tag_tag::remove_all_item_tags('core', 'user', $user->id);
3586 // Unconditionally unenrol from all courses.
3587 enrol_user_delete($user);
3589 // Unenrol from all roles in all contexts.
3590 // This might be slow but it is really needed - modules might do some extra cleanup!
3591 role_unassign_all(array('userid' => $user->id));
3593 // Notify the competency subsystem.
3594 \core_competency\api::hook_user_deleted($user->id);
3596 // Now do a brute force cleanup.
3598 // Delete all user events and subscription events.
3599 $DB->delete_records_select('event', 'userid = :userid AND subscriptionid IS NOT NULL', ['userid' => $user->id]);
3601 // Now, delete all calendar subscription from the user.
3602 $DB->delete_records('event_subscriptions', ['userid' => $user->id]);
3604 // Remove from all cohorts.
3605 $DB->delete_records('cohort_members', array('userid' => $user->id));
3607 // Remove from all groups.
3608 $DB->delete_records('groups_members', array('userid' => $user->id));
3610 // Brute force unenrol from all courses.
3611 $DB->delete_records('user_enrolments', array('userid' => $user->id));
3613 // Purge user preferences.
3614 $DB->delete_records('user_preferences', array('userid' => $user->id));
3616 // Purge user extra profile info.
3617 $DB->delete_records('user_info_data', array('userid' => $user->id));
3619 // Purge log of previous password hashes.
3620 $DB->delete_records('user_password_history', array('userid' => $user->id));
3622 // Last course access not necessary either.
3623 $DB->delete_records('user_lastaccess', array('userid' => $user->id));
3624 // Remove all user tokens.
3625 $DB->delete_records('external_tokens', array('userid' => $user->id));
3627 // Unauthorise the user for all services.
3628 $DB->delete_records('external_services_users', array('userid' => $user->id));
3630 // Remove users private keys.
3631 $DB->delete_records('user_private_key', array('userid' => $user->id));
3633 // Remove users customised pages.
3634 $DB->delete_records('my_pages', array('userid' => $user->id, 'private' => 1));
3636 // Remove user's oauth2 refresh tokens, if present.
3637 $DB->delete_records('oauth2_refresh_token', array('userid' => $user->id));
3639 // Delete user from $SESSION->bulk_users.
3640 if (isset($SESSION->bulk_users[$user->id])) {
3641 unset($SESSION->bulk_users[$user->id]);
3644 // Force logout - may fail if file based sessions used, sorry.
3645 \core\session\manager::kill_user_sessions($user->id);
3647 // Generate username from email address, or a fake email.
3648 $delemail = !empty($user->email) ? $user->email : $user->username . '.' . $user->id . '@unknownemail.invalid';
3650 $deltime = time();
3651 $deltimelength = core_text::strlen((string) $deltime);
3653 // Max username length is 100 chars. Select up to limit - (length of current time + 1 [period character]) from users email.
3654 $delname = clean_param($delemail, PARAM_USERNAME);
3655 $delname = core_text::substr($delname, 0, 100 - ($deltimelength + 1)) . ".{$deltime}";
3657 // Workaround for bulk deletes of users with the same email address.
3658 while ($DB->record_exists('user', array('username' => $delname))) { // No need to use mnethostid here.
3659 $delname++;
3662 // Mark internal user record as "deleted".
3663 $updateuser = new stdClass();
3664 $updateuser->id = $user->id;
3665 $updateuser->deleted = 1;
3666 $updateuser->username = $delname; // Remember it just in case.
3667 $updateuser->email = md5($user->username);// Store hash of username, useful importing/restoring users.
3668 $updateuser->idnumber = ''; // Clear this field to free it up.
3669 $updateuser->picture = 0;
3670 $updateuser->timemodified = $deltime;
3672 // Don't trigger update event, as user is being deleted.
3673 user_update_user($updateuser, false, false);
3675 // Delete all content associated with the user context, but not the context itself.
3676 $usercontext->delete_content();
3678 // Delete any search data.
3679 \core_search\manager::context_deleted($usercontext);
3681 // Any plugin that needs to cleanup should register this event.
3682 // Trigger event.
3683 $event = \core\event\user_deleted::create(
3684 array(
3685 'objectid' => $user->id,
3686 'relateduserid' => $user->id,
3687 'context' => $usercontext,
3688 'other' => array(
3689 'username' => $user->username,
3690 'email' => $user->email,
3691 'idnumber' => $user->idnumber,
3692 'picture' => $user->picture,
3693 'mnethostid' => $user->mnethostid
3697 $event->add_record_snapshot('user', $olduser);
3698 $event->trigger();
3700 // We will update the user's timemodified, as it will be passed to the user_deleted event, which
3701 // should know about this updated property persisted to the user's table.
3702 $user->timemodified = $updateuser->timemodified;
3704 // Notify auth plugin - do not block the delete even when plugin fails.
3705 $authplugin = get_auth_plugin($user->auth);
3706 $authplugin->user_delete($user);
3708 return true;
3712 * Retrieve the guest user object.
3714 * @return stdClass A {@link $USER} object
3716 function guest_user() {
3717 global $CFG, $DB;
3719 if ($newuser = $DB->get_record('user', array('id' => $CFG->siteguest))) {
3720 $newuser->confirmed = 1;
3721 $newuser->lang = get_newuser_language();
3722 $newuser->lastip = getremoteaddr();
3725 return $newuser;
3729 * Authenticates a user against the chosen authentication mechanism
3731 * Given a username and password, this function looks them
3732 * up using the currently selected authentication mechanism,
3733 * and if the authentication is successful, it returns a
3734 * valid $user object from the 'user' table.
3736 * Uses auth_ functions from the currently active auth module
3738 * After authenticate_user_login() returns success, you will need to
3739 * log that the user has logged in, and call complete_user_login() to set
3740 * the session up.
3742 * Note: this function works only with non-mnet accounts!
3744 * @param string $username User's username (or also email if $CFG->authloginviaemail enabled)
3745 * @param string $password User's password
3746 * @param bool $ignorelockout useful when guessing is prevented by other mechanism such as captcha or SSO
3747 * @param int $failurereason login failure reason, can be used in renderers (it may disclose if account exists)
3748 * @param string|bool $logintoken If this is set to a string it is validated against the login token for the session.
3749 * @param string|bool $loginrecaptcha If this is set to a string it is validated against Google reCaptcha.
3750 * @return stdClass|false A {@link $USER} object or false if error
3752 function authenticate_user_login(
3753 $username,
3754 $password,
3755 $ignorelockout = false,
3756 &$failurereason = null,
3757 $logintoken = false,
3758 string|bool $loginrecaptcha = false,
3760 global $CFG, $DB, $PAGE, $SESSION;
3761 require_once("$CFG->libdir/authlib.php");
3763 if ($user = get_complete_user_data('username', $username, $CFG->mnet_localhost_id)) {
3764 // we have found the user
3766 } else if (!empty($CFG->authloginviaemail)) {
3767 if ($email = clean_param($username, PARAM_EMAIL)) {
3768 $select = "mnethostid = :mnethostid AND LOWER(email) = LOWER(:email) AND deleted = 0";
3769 $params = array('mnethostid' => $CFG->mnet_localhost_id, 'email' => $email);
3770 $users = $DB->get_records_select('user', $select, $params, 'id', 'id', 0, 2);
3771 if (count($users) === 1) {
3772 // Use email for login only if unique.
3773 $user = reset($users);
3774 $user = get_complete_user_data('id', $user->id);
3775 $username = $user->username;
3777 unset($users);
3781 // Make sure this request came from the login form.
3782 if (!\core\session\manager::validate_login_token($logintoken)) {
3783 $failurereason = AUTH_LOGIN_FAILED;
3785 // Trigger login failed event (specifying the ID of the found user, if available).
3786 \core\event\user_login_failed::create([
3787 'userid' => ($user->id ?? 0),
3788 'other' => [
3789 'username' => $username,
3790 'reason' => $failurereason,
3792 ])->trigger();
3794 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Invalid Login Token: $username ".$_SERVER['HTTP_USER_AGENT']);
3795 return false;
3798 // Login reCaptcha.
3799 if (login_captcha_enabled() && !validate_login_captcha($loginrecaptcha)) {
3800 $failurereason = AUTH_LOGIN_FAILED_RECAPTCHA;
3801 // Trigger login failed event (specifying the ID of the found user, if available).
3802 \core\event\user_login_failed::create([
3803 'userid' => ($user->id ?? 0),
3804 'other' => [
3805 'username' => $username,
3806 'reason' => $failurereason,
3808 ])->trigger();
3809 return false;
3812 $authsenabled = get_enabled_auth_plugins();
3814 if ($user) {
3815 // Use manual if auth not set.
3816 $auth = empty($user->auth) ? 'manual' : $user->auth;
3818 if (in_array($user->auth, $authsenabled)) {
3819 $authplugin = get_auth_plugin($user->auth);
3820 $authplugin->pre_user_login_hook($user);
3823 if (!empty($user->suspended)) {
3824 $failurereason = AUTH_LOGIN_SUSPENDED;
3826 // Trigger login failed event.
3827 $event = \core\event\user_login_failed::create(array('userid' => $user->id,
3828 'other' => array('username' => $username, 'reason' => $failurereason)));
3829 $event->trigger();
3830 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Suspended Login: $username ".$_SERVER['HTTP_USER_AGENT']);
3831 return false;
3833 if ($auth=='nologin' or !is_enabled_auth($auth)) {
3834 // Legacy way to suspend user.
3835 $failurereason = AUTH_LOGIN_SUSPENDED;
3837 // Trigger login failed event.
3838 $event = \core\event\user_login_failed::create(array('userid' => $user->id,
3839 'other' => array('username' => $username, 'reason' => $failurereason)));
3840 $event->trigger();
3841 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Disabled Login: $username ".$_SERVER['HTTP_USER_AGENT']);
3842 return false;
3844 $auths = array($auth);
3846 } else {
3847 // Check if there's a deleted record (cheaply), this should not happen because we mangle usernames in delete_user().
3848 if ($DB->get_field('user', 'id', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id, 'deleted' => 1))) {
3849 $failurereason = AUTH_LOGIN_NOUSER;
3851 // Trigger login failed event.
3852 $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
3853 'reason' => $failurereason)));
3854 $event->trigger();
3855 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Deleted Login: $username ".$_SERVER['HTTP_USER_AGENT']);
3856 return false;
3859 // User does not exist.
3860 $auths = $authsenabled;
3861 $user = new stdClass();
3862 $user->id = 0;
3865 if ($ignorelockout) {
3866 // Some other mechanism protects against brute force password guessing, for example login form might include reCAPTCHA
3867 // or this function is called from a SSO script.
3868 } else if ($user->id) {
3869 // Verify login lockout after other ways that may prevent user login.
3870 if (login_is_lockedout($user)) {
3871 $failurereason = AUTH_LOGIN_LOCKOUT;
3873 // Trigger login failed event.
3874 $event = \core\event\user_login_failed::create(array('userid' => $user->id,
3875 'other' => array('username' => $username, 'reason' => $failurereason)));
3876 $event->trigger();
3878 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Login lockout: $username ".$_SERVER['HTTP_USER_AGENT']);
3879 $SESSION->loginerrormsg = get_string('accountlocked', 'admin');
3881 return false;
3883 } else {
3884 // We can not lockout non-existing accounts.
3887 foreach ($auths as $auth) {
3888 $authplugin = get_auth_plugin($auth);
3890 // On auth fail fall through to the next plugin.
3891 if (!$authplugin->user_login($username, $password)) {
3892 continue;
3895 // Before performing login actions, check if user still passes password policy, if admin setting is enabled.
3896 if (!empty($CFG->passwordpolicycheckonlogin)) {
3897 $errmsg = '';
3898 $passed = check_password_policy($password, $errmsg, $user);
3899 if (!$passed) {
3900 // First trigger event for failure.
3901 $failedevent = \core\event\user_password_policy_failed::create_from_user($user);
3902 $failedevent->trigger();
3904 // If able to change password, set flag and move on.
3905 if ($authplugin->can_change_password()) {
3906 // Check if we are on internal change password page, or service is external, don't show notification.
3907 $internalchangeurl = new moodle_url('/login/change_password.php');
3908 if (!($PAGE->has_set_url() && $internalchangeurl->compare($PAGE->url)) && $authplugin->is_internal()) {
3909 \core\notification::error(get_string('passwordpolicynomatch', '', $errmsg));
3911 set_user_preference('auth_forcepasswordchange', 1, $user);
3912 } else if ($authplugin->can_reset_password()) {
3913 // Else force a reset if possible.
3914 \core\notification::error(get_string('forcepasswordresetnotice', '', $errmsg));
3915 redirect(new moodle_url('/login/forgot_password.php'));
3916 } else {
3917 $notifymsg = get_string('forcepasswordresetfailurenotice', '', $errmsg);
3918 // If support page is set, add link for help.
3919 if (!empty($CFG->supportpage)) {
3920 $link = \html_writer::link($CFG->supportpage, $CFG->supportpage);
3921 $link = \html_writer::tag('p', $link);
3922 $notifymsg .= $link;
3925 // If no change or reset is possible, add a notification for user.
3926 \core\notification::error($notifymsg);
3931 // Successful authentication.
3932 if ($user->id) {
3933 // User already exists in database.
3934 if (empty($user->auth)) {
3935 // For some reason auth isn't set yet.
3936 $DB->set_field('user', 'auth', $auth, array('id' => $user->id));
3937 $user->auth = $auth;
3940 // If the existing hash is using an out-of-date algorithm (or the legacy md5 algorithm), then we should update to
3941 // the current hash algorithm while we have access to the user's password.
3942 update_internal_user_password($user, $password);
3944 if ($authplugin->is_synchronised_with_external()) {
3945 // Update user record from external DB.
3946 $user = update_user_record_by_id($user->id);
3948 } else {
3949 // The user is authenticated but user creation may be disabled.
3950 if (!empty($CFG->authpreventaccountcreation)) {
3951 $failurereason = AUTH_LOGIN_UNAUTHORISED;
3953 // Trigger login failed event.
3954 $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
3955 'reason' => $failurereason)));
3956 $event->trigger();
3958 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Unknown user, can not create new accounts: $username ".
3959 $_SERVER['HTTP_USER_AGENT']);
3960 return false;
3961 } else {
3962 $user = create_user_record($username, $password, $auth);
3966 $authplugin->sync_roles($user);
3968 foreach ($authsenabled as $hau) {
3969 $hauth = get_auth_plugin($hau);
3970 $hauth->user_authenticated_hook($user, $username, $password);
3973 if (empty($user->id)) {
3974 $failurereason = AUTH_LOGIN_NOUSER;
3975 // Trigger login failed event.
3976 $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
3977 'reason' => $failurereason)));
3978 $event->trigger();
3979 return false;
3982 if (!empty($user->suspended)) {
3983 // Just in case some auth plugin suspended account.
3984 $failurereason = AUTH_LOGIN_SUSPENDED;
3985 // Trigger login failed event.
3986 $event = \core\event\user_login_failed::create(array('userid' => $user->id,
3987 'other' => array('username' => $username, 'reason' => $failurereason)));
3988 $event->trigger();
3989 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Suspended Login: $username ".$_SERVER['HTTP_USER_AGENT']);
3990 return false;
3993 login_attempt_valid($user);
3994 $failurereason = AUTH_LOGIN_OK;
3995 return $user;
3998 // Failed if all the plugins have failed.
3999 if (debugging('', DEBUG_ALL)) {
4000 error_log('[client '.getremoteaddr()."] $CFG->wwwroot Failed Login: $username ".$_SERVER['HTTP_USER_AGENT']);
4003 if ($user->id) {
4004 login_attempt_failed($user);
4005 $failurereason = AUTH_LOGIN_FAILED;
4006 // Trigger login failed event.
4007 $event = \core\event\user_login_failed::create(array('userid' => $user->id,
4008 'other' => array('username' => $username, 'reason' => $failurereason)));
4009 $event->trigger();
4010 } else {
4011 $failurereason = AUTH_LOGIN_NOUSER;
4012 // Trigger login failed event.
4013 $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
4014 'reason' => $failurereason)));
4015 $event->trigger();
4018 return false;
4022 * Call to complete the user login process after authenticate_user_login()
4023 * has succeeded. It will setup the $USER variable and other required bits
4024 * and pieces.
4026 * NOTE:
4027 * - It will NOT log anything -- up to the caller to decide what to log.
4028 * - this function does not set any cookies any more!
4030 * @param stdClass $user
4031 * @param array $extrauserinfo
4032 * @return stdClass A {@link $USER} object - BC only, do not use
4034 function complete_user_login($user, array $extrauserinfo = []) {
4035 global $CFG, $DB, $USER, $SESSION;
4037 \core\session\manager::login_user($user);
4039 // Reload preferences from DB.
4040 unset($USER->preference);
4041 check_user_preferences_loaded($USER);
4043 // Update login times.
4044 update_user_login_times();
4046 // Extra session prefs init.
4047 set_login_session_preferences();
4049 // Trigger login event.
4050 $event = \core\event\user_loggedin::create(
4051 array(
4052 'userid' => $USER->id,
4053 'objectid' => $USER->id,
4054 'other' => [
4055 'username' => $USER->username,
4056 'extrauserinfo' => $extrauserinfo
4060 $event->trigger();
4062 // Check if the user is using a new browser or session (a new MoodleSession cookie is set in that case).
4063 // If the user is accessing from the same IP, ignore everything (most of the time will be a new session in the same browser).
4064 // Skip Web Service requests, CLI scripts, AJAX scripts, and request from the mobile app itself.
4065 $loginip = getremoteaddr();
4066 $isnewip = isset($SESSION->userpreviousip) && $SESSION->userpreviousip != $loginip;
4067 $isvalidenv = (!WS_SERVER && !CLI_SCRIPT && !NO_MOODLE_COOKIES) || PHPUNIT_TEST;
4069 if (!empty($SESSION->isnewsessioncookie) && $isnewip && $isvalidenv && !\core_useragent::is_moodle_app()) {
4071 $logintime = time();
4072 $ismoodleapp = false;
4073 $useragent = \core_useragent::get_user_agent_string();
4075 $sitepreferences = get_message_output_default_preferences();
4076 // Check if new login notification is disabled at system level.
4077 $newlogindisabled = $sitepreferences->moodle_newlogin_disable ?? 0;
4078 // Check if message providers (web, email, mobile) are enabled at system level.
4079 $msgproviderenabled = isset($sitepreferences->message_provider_moodle_newlogin_enabled);
4080 // Get message providers enabled for a user.
4081 $userpreferences = get_user_preferences('message_provider_moodle_newlogin_enabled');
4082 // Check if notification processor plugins (web, email, mobile) are enabled at system level.
4083 $msgprocessorsready = !empty(get_message_processors(true));
4084 // If new login notification is enabled at system level then go for other conditions check.
4085 $newloginenabled = $newlogindisabled ? 0 : ($userpreferences != 'none' && $msgproviderenabled);
4087 if ($newloginenabled && $msgprocessorsready) {
4088 // Schedule adhoc task to send a login notification to the user.
4089 $task = new \core\task\send_login_notifications();
4090 $task->set_userid($USER->id);
4091 $task->set_custom_data(compact('ismoodleapp', 'useragent', 'loginip', 'logintime'));
4092 $task->set_component('core');
4093 \core\task\manager::queue_adhoc_task($task);
4097 // Queue migrating the messaging data, if we need to.
4098 if (!get_user_preferences('core_message_migrate_data', false, $USER->id)) {
4099 // Check if there are any legacy messages to migrate.
4100 if (\core_message\helper::legacy_messages_exist($USER->id)) {
4101 \core_message\task\migrate_message_data::queue_task($USER->id);
4102 } else {
4103 set_user_preference('core_message_migrate_data', true, $USER->id);
4107 if (isguestuser()) {
4108 // No need to continue when user is THE guest.
4109 return $USER;
4112 if (CLI_SCRIPT) {
4113 // We can redirect to password change URL only in browser.
4114 return $USER;
4117 // Select password change url.
4118 $userauth = get_auth_plugin($USER->auth);
4120 // Check whether the user should be changing password.
4121 if (get_user_preferences('auth_forcepasswordchange', false)) {
4122 if ($userauth->can_change_password()) {
4123 if ($changeurl = $userauth->change_password_url()) {
4124 redirect($changeurl);
4125 } else {
4126 require_once($CFG->dirroot . '/login/lib.php');
4127 $SESSION->wantsurl = core_login_get_return_url();
4128 redirect($CFG->wwwroot.'/login/change_password.php');
4130 } else {
4131 throw new \moodle_exception('nopasswordchangeforced', 'auth');
4134 return $USER;
4138 * Check a password hash to see if it was hashed using the legacy hash algorithm (bcrypt).
4140 * @param string $password String to check.
4141 * @return bool True if the $password matches the format of a bcrypt hash.
4143 function password_is_legacy_hash(#[\SensitiveParameter] string $password): bool {
4144 return (bool) preg_match('/^\$2y\$[\d]{2}\$[A-Za-z0-9\.\/]{53}$/', $password);
4148 * Calculate the Shannon entropy of a string.
4150 * @param string $pepper The pepper to calculate the entropy of.
4151 * @return float The Shannon entropy of the string.
4153 function calculate_entropy(#[\SensitiveParameter] string $pepper): float {
4154 // Initialize entropy.
4155 $h = 0;
4157 // Calculate the length of the string.
4158 $size = strlen($pepper);
4160 // For each unique character in the string.
4161 foreach (count_chars($pepper, 1) as $v) {
4162 // Calculate the probability of the character.
4163 $p = $v / $size;
4165 // Add the character's contribution to the total entropy.
4166 // This uses the formula for the entropy of a discrete random variable.
4167 $h -= $p * log($p) / log(2);
4170 // Instead of returning the average entropy per symbol (Shannon entropy),
4171 // we multiply by the length of the string to get total entropy.
4172 return $h * $size;
4176 * Get the available password peppers.
4177 * The latest pepper is checked for minimum entropy as part of this function.
4178 * We only calculate the entropy of the most recent pepper,
4179 * because passwords are always updated to the latest pepper,
4180 * and in the past we may have enforced a lower minimum entropy.
4181 * Also, we allow the latest pepper to be empty, to allow admins to migrate off peppers.
4183 * @return array The password peppers.
4184 * @throws coding_exception If the entropy of the password pepper is less than the recommended minimum.
4186 function get_password_peppers(): array {
4187 global $CFG;
4189 // Get all available peppers.
4190 if (isset($CFG->passwordpeppers) && is_array($CFG->passwordpeppers)) {
4191 // Sort the array in descending order of keys (numerical).
4192 $peppers = $CFG->passwordpeppers;
4193 krsort($peppers, SORT_NUMERIC);
4194 } else {
4195 $peppers = []; // Set an empty array if no peppers are found.
4198 // Check if the entropy of the most recent pepper is less than the minimum.
4199 // Also, we allow the most recent pepper to be empty, to allow admins to migrate off peppers.
4200 $lastpepper = reset($peppers);
4201 if (!empty($peppers) && $lastpepper !== '' && calculate_entropy($lastpepper) < PEPPER_ENTROPY) {
4202 throw new coding_exception(
4203 'password pepper below minimum',
4204 'The entropy of the password pepper is less than the recommended minimum.');
4206 return $peppers;
4210 * Compare password against hash stored in user object to determine if it is valid.
4212 * If necessary it also updates the stored hash to the current format.
4214 * @param stdClass $user (Password property may be updated).
4215 * @param string $password Plain text password.
4216 * @return bool True if password is valid.
4218 function validate_internal_user_password(stdClass $user, #[\SensitiveParameter] string $password): bool {
4220 if (exceeds_password_length($password)) {
4221 // Password cannot be more than MAX_PASSWORD_CHARACTERS characters.
4222 return false;
4225 if ($user->password === AUTH_PASSWORD_NOT_CACHED) {
4226 // Internal password is not used at all, it can not validate.
4227 return false;
4230 $peppers = get_password_peppers(); // Get the array of available peppers.
4231 $islegacy = password_is_legacy_hash($user->password); // Check if the password is a legacy bcrypt hash.
4233 // If the password is a legacy hash, no peppers were used, so verify and update directly.
4234 if ($islegacy && password_verify($password, $user->password)) {
4235 update_internal_user_password($user, $password);
4236 return true;
4239 // If the password is not a legacy hash, iterate through the peppers.
4240 $latestpepper = reset($peppers);
4241 // Add an empty pepper to the beginning of the array. To make it easier to check if the password matches without any pepper.
4242 $peppers = [-1 => ''] + $peppers;
4243 foreach ($peppers as $pepper) {
4244 $pepperedpassword = $password . $pepper;
4246 // If the peppered password is correct, update (if necessary) and return true.
4247 if (password_verify($pepperedpassword, $user->password)) {
4248 // If the pepper used is not the latest one, update the password.
4249 if ($pepper !== $latestpepper) {
4250 update_internal_user_password($user, $password);
4252 return true;
4256 // If no peppered password was correct, the password is wrong.
4257 return false;
4261 * Calculate hash for a plain text password.
4263 * @param string $password Plain text password to be hashed.
4264 * @param bool $fasthash If true, use a low number of rounds when generating the hash
4265 * This is faster to generate but makes the hash less secure.
4266 * It is used when lots of hashes need to be generated quickly.
4267 * @param int $pepperlength Lenght of the peppers
4268 * @return string The hashed password.
4270 * @throws moodle_exception If a problem occurs while generating the hash.
4272 function hash_internal_user_password(#[\SensitiveParameter] string $password, $fasthash = false, $pepperlength = 0): string {
4273 if (exceeds_password_length($password, $pepperlength)) {
4274 // Password cannot be more than MAX_PASSWORD_CHARACTERS.
4275 throw new \moodle_exception(get_string("passwordexceeded", 'error', MAX_PASSWORD_CHARACTERS));
4278 // Set the cost factor to 5000 for fast hashing, otherwise use default cost.
4279 $rounds = $fasthash ? 5000 : 10000;
4281 // First generate a cryptographically suitable salt.
4282 $randombytes = random_bytes(16);
4283 $salt = substr(strtr(base64_encode($randombytes), '+', '.'), 0, 16);
4285 // Now construct the password string with the salt and number of rounds.
4286 // The password string is in the format $algorithm$rounds$salt$hash. ($6 is the SHA512 algorithm).
4287 $generatedhash = crypt($password, implode('$', [
4289 // The SHA512 Algorithm
4290 '6',
4291 "rounds={$rounds}",
4292 $salt,
4294 ]));
4296 if ($generatedhash === false || $generatedhash === null) {
4297 throw new moodle_exception('Failed to generate password hash.');
4300 return $generatedhash;
4304 * Update password hash in user object (if necessary).
4306 * The password is updated if:
4307 * 1. The password has changed (the hash of $user->password is different
4308 * to the hash of $password).
4309 * 2. The existing hash is using an out-of-date algorithm (or the legacy
4310 * md5 algorithm).
4312 * The password is peppered with the latest pepper before hashing,
4313 * if peppers are available.
4314 * Updating the password will modify the $user object and the database
4315 * record to use the current hashing algorithm.
4316 * It will remove Web Services user tokens too.
4318 * @param stdClass $user User object (password property may be updated).
4319 * @param string $password Plain text password.
4320 * @param bool $fasthash If true, use a low cost factor when generating the hash
4321 * This is much faster to generate but makes the hash
4322 * less secure. It is used when lots of hashes need to
4323 * be generated quickly.
4324 * @return bool Always returns true.
4326 function update_internal_user_password(
4327 stdClass $user,
4328 #[\SensitiveParameter] string $password,
4329 bool $fasthash = false
4330 ): bool {
4331 global $CFG, $DB;
4333 // Add the latest password pepper to the password before further processing.
4334 $peppers = get_password_peppers();
4335 if (!empty($peppers)) {
4336 $password = $password . reset($peppers);
4339 // Figure out what the hashed password should be.
4340 if (!isset($user->auth)) {
4341 debugging('User record in update_internal_user_password() must include field auth',
4342 DEBUG_DEVELOPER);
4343 $user->auth = $DB->get_field('user', 'auth', array('id' => $user->id));
4345 $authplugin = get_auth_plugin($user->auth);
4346 if ($authplugin->prevent_local_passwords()) {
4347 $hashedpassword = AUTH_PASSWORD_NOT_CACHED;
4348 } else {
4349 $hashedpassword = hash_internal_user_password($password, $fasthash);
4352 $algorithmchanged = false;
4354 if ($hashedpassword === AUTH_PASSWORD_NOT_CACHED) {
4355 // Password is not cached, update it if not set to AUTH_PASSWORD_NOT_CACHED.
4356 $passwordchanged = ($user->password !== $hashedpassword);
4358 } else if (isset($user->password)) {
4359 // If verification fails then it means the password has changed.
4360 $passwordchanged = !password_verify($password, $user->password);
4361 $algorithmchanged = password_is_legacy_hash($user->password);
4362 } else {
4363 // While creating new user, password in unset in $user object, to avoid
4364 // saving it with user_create()
4365 $passwordchanged = true;
4368 if ($passwordchanged || $algorithmchanged) {
4369 $DB->set_field('user', 'password', $hashedpassword, array('id' => $user->id));
4370 $user->password = $hashedpassword;
4372 // Trigger event.
4373 $user = $DB->get_record('user', array('id' => $user->id));
4374 \core\event\user_password_updated::create_from_user($user)->trigger();
4376 // Remove WS user tokens.
4377 if (!empty($CFG->passwordchangetokendeletion)) {
4378 require_once($CFG->dirroot.'/webservice/lib.php');
4379 webservice::delete_user_ws_tokens($user->id);
4383 return true;
4387 * Get a complete user record, which includes all the info in the user record.
4389 * Intended for setting as $USER session variable
4391 * @param string $field The user field to be checked for a given value.
4392 * @param string $value The value to match for $field.
4393 * @param int $mnethostid
4394 * @param bool $throwexception If true, it will throw an exception when there's no record found or when there are multiple records
4395 * found. Otherwise, it will just return false.
4396 * @return mixed False, or A {@link $USER} object.
4398 function get_complete_user_data($field, $value, $mnethostid = null, $throwexception = false) {
4399 global $CFG, $DB;
4401 if (!$field || !$value) {
4402 return false;
4405 // Change the field to lowercase.
4406 $field = core_text::strtolower($field);
4408 // List of case insensitive fields.
4409 $caseinsensitivefields = ['email'];
4411 // Username input is forced to lowercase and should be case sensitive.
4412 if ($field == 'username') {
4413 $value = core_text::strtolower($value);
4416 // Build the WHERE clause for an SQL query.
4417 $params = array('fieldval' => $value);
4419 // Do a case-insensitive query, if necessary. These are generally very expensive. The performance can be improved on some DBs
4420 // such as MySQL by pre-filtering users with accent-insensitive subselect.
4421 if (in_array($field, $caseinsensitivefields)) {
4422 $fieldselect = $DB->sql_equal($field, ':fieldval', false);
4423 $idsubselect = $DB->sql_equal($field, ':fieldval2', false, false);
4424 $params['fieldval2'] = $value;
4425 } else {
4426 $fieldselect = "$field = :fieldval";
4427 $idsubselect = '';
4429 $constraints = "$fieldselect AND deleted <> 1";
4431 // If we are loading user data based on anything other than id,
4432 // we must also restrict our search based on mnet host.
4433 if ($field != 'id') {
4434 if (empty($mnethostid)) {
4435 // If empty, we restrict to local users.
4436 $mnethostid = $CFG->mnet_localhost_id;
4439 if (!empty($mnethostid)) {
4440 $params['mnethostid'] = $mnethostid;
4441 $constraints .= " AND mnethostid = :mnethostid";
4444 if ($idsubselect) {
4445 $constraints .= " AND id IN (SELECT id FROM {user} WHERE {$idsubselect})";
4448 // Get all the basic user data.
4449 try {
4450 // Make sure that there's only a single record that matches our query.
4451 // For example, when fetching by email, multiple records might match the query as there's no guarantee that email addresses
4452 // are unique. Therefore we can't reliably tell whether the user profile data that we're fetching is the correct one.
4453 $user = $DB->get_record_select('user', $constraints, $params, '*', MUST_EXIST);
4454 } catch (dml_exception $exception) {
4455 if ($throwexception) {
4456 throw $exception;
4457 } else {
4458 // Return false when no records or multiple records were found.
4459 return false;
4463 // Get various settings and preferences.
4465 // Preload preference cache.
4466 check_user_preferences_loaded($user);
4468 // Load course enrolment related stuff.
4469 $user->lastcourseaccess = array(); // During last session.
4470 $user->currentcourseaccess = array(); // During current session.
4471 if ($lastaccesses = $DB->get_records('user_lastaccess', array('userid' => $user->id))) {
4472 foreach ($lastaccesses as $lastaccess) {
4473 $user->lastcourseaccess[$lastaccess->courseid] = $lastaccess->timeaccess;
4477 // Add cohort theme.
4478 if (!empty($CFG->allowcohortthemes)) {
4479 require_once($CFG->dirroot . '/cohort/lib.php');
4480 if ($cohorttheme = cohort_get_user_cohort_theme($user->id)) {
4481 $user->cohorttheme = $cohorttheme;
4485 // Add the custom profile fields to the user record.
4486 $user->profile = array();
4487 if (!isguestuser($user)) {
4488 require_once($CFG->dirroot.'/user/profile/lib.php');
4489 profile_load_custom_fields($user);
4492 // Rewrite some variables if necessary.
4493 if (!empty($user->description)) {
4494 // No need to cart all of it around.
4495 $user->description = true;
4497 if (isguestuser($user)) {
4498 // Guest language always same as site.
4499 $user->lang = get_newuser_language();
4500 // Name always in current language.
4501 $user->firstname = get_string('guestuser');
4502 $user->lastname = ' ';
4505 return $user;
4509 * Validate a password against the configured password policy
4511 * @param string $password the password to be checked against the password policy
4512 * @param string $errmsg the error message to display when the password doesn't comply with the policy.
4513 * @param stdClass $user the user object to perform password validation against. Defaults to null if not provided.
4515 * @return bool true if the password is valid according to the policy. false otherwise.
4517 function check_password_policy($password, &$errmsg, $user = null) {
4518 global $CFG;
4520 if (!empty($CFG->passwordpolicy) && !isguestuser($user)) {
4521 $errmsg = '';
4522 if (core_text::strlen($password) < $CFG->minpasswordlength) {
4523 $errmsg .= '<div>'. get_string('errorminpasswordlength', 'auth', $CFG->minpasswordlength) .'</div>';
4525 if (preg_match_all('/[[:digit:]]/u', $password, $matches) < $CFG->minpassworddigits) {
4526 $errmsg .= '<div>'. get_string('errorminpassworddigits', 'auth', $CFG->minpassworddigits) .'</div>';
4528 if (preg_match_all('/[[:lower:]]/u', $password, $matches) < $CFG->minpasswordlower) {
4529 $errmsg .= '<div>'. get_string('errorminpasswordlower', 'auth', $CFG->minpasswordlower) .'</div>';
4531 if (preg_match_all('/[[:upper:]]/u', $password, $matches) < $CFG->minpasswordupper) {
4532 $errmsg .= '<div>'. get_string('errorminpasswordupper', 'auth', $CFG->minpasswordupper) .'</div>';
4534 if (preg_match_all('/[^[:upper:][:lower:][:digit:]]/u', $password, $matches) < $CFG->minpasswordnonalphanum) {
4535 $errmsg .= '<div>'. get_string('errorminpasswordnonalphanum', 'auth', $CFG->minpasswordnonalphanum) .'</div>';
4537 if (!check_consecutive_identical_characters($password, $CFG->maxconsecutiveidentchars)) {
4538 $errmsg .= '<div>'. get_string('errormaxconsecutiveidentchars', 'auth', $CFG->maxconsecutiveidentchars) .'</div>';
4541 // Fire any additional password policy functions from plugins.
4542 // Plugin functions should output an error message string or empty string for success.
4543 $pluginsfunction = get_plugins_with_function('check_password_policy');
4544 foreach ($pluginsfunction as $plugintype => $plugins) {
4545 foreach ($plugins as $pluginfunction) {
4546 $pluginerr = $pluginfunction($password, $user);
4547 if ($pluginerr) {
4548 $errmsg .= '<div>'. $pluginerr .'</div>';
4554 if ($errmsg == '') {
4555 return true;
4556 } else {
4557 return false;
4563 * When logging in, this function is run to set certain preferences for the current SESSION.
4565 function set_login_session_preferences() {
4566 global $SESSION;
4568 $SESSION->justloggedin = true;
4570 unset($SESSION->lang);
4571 unset($SESSION->forcelang);
4572 unset($SESSION->load_navigation_admin);
4577 * Delete a course, including all related data from the database, and any associated files.
4579 * @param mixed $courseorid The id of the course or course object to delete.
4580 * @param bool $showfeedback Whether to display notifications of each action the function performs.
4581 * @return bool true if all the removals succeeded. false if there were any failures. If this
4582 * method returns false, some of the removals will probably have succeeded, and others
4583 * failed, but you have no way of knowing which.
4585 function delete_course($courseorid, $showfeedback = true) {
4586 global $DB, $CFG;
4588 if (is_object($courseorid)) {
4589 $courseid = $courseorid->id;
4590 $course = $courseorid;
4591 } else {
4592 $courseid = $courseorid;
4593 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
4594 return false;
4597 $context = context_course::instance($courseid);
4599 // Frontpage course can not be deleted!!
4600 if ($courseid == SITEID) {
4601 return false;
4604 // Allow plugins to use this course before we completely delete it.
4605 if ($pluginsfunction = get_plugins_with_function('pre_course_delete')) {
4606 foreach ($pluginsfunction as $plugintype => $plugins) {
4607 foreach ($plugins as $pluginfunction) {
4608 $pluginfunction($course);
4613 // Tell the search manager we are about to delete a course. This prevents us sending updates
4614 // for each individual context being deleted.
4615 \core_search\manager::course_deleting_start($courseid);
4617 $handler = core_course\customfield\course_handler::create();
4618 $handler->delete_instance($courseid);
4620 // Make the course completely empty.
4621 remove_course_contents($courseid, $showfeedback);
4623 // Communication provider delete associated information.
4624 $communication = \core_communication\api::load_by_instance(
4625 $context,
4626 'core_course',
4627 'coursecommunication',
4628 $course->id
4631 // Delete the course and related context instance.
4632 context_helper::delete_instance(CONTEXT_COURSE, $courseid);
4634 // Update communication room membership of enrolled users.
4635 require_once($CFG->libdir . '/enrollib.php');
4636 $courseusers = enrol_get_course_users($courseid);
4637 $enrolledusers = [];
4639 foreach ($courseusers as $user) {
4640 $enrolledusers[] = $user->id;
4643 $communication->remove_members_from_room($enrolledusers);
4645 $communication->delete_room();
4647 $DB->delete_records("course", array("id" => $courseid));
4648 $DB->delete_records("course_format_options", array("courseid" => $courseid));
4650 // Reset all course related caches here.
4651 core_courseformat\base::reset_course_cache($courseid);
4653 // Tell search that we have deleted the course so it can delete course data from the index.
4654 \core_search\manager::course_deleting_finish($courseid);
4656 // Trigger a course deleted event.
4657 $event = \core\event\course_deleted::create(array(
4658 'objectid' => $course->id,
4659 'context' => $context,
4660 'other' => array(
4661 'shortname' => $course->shortname,
4662 'fullname' => $course->fullname,
4663 'idnumber' => $course->idnumber
4666 $event->add_record_snapshot('course', $course);
4667 $event->trigger();
4669 return true;
4673 * Clear a course out completely, deleting all content but don't delete the course itself.
4675 * This function does not verify any permissions.
4677 * Please note this function also deletes all user enrolments,
4678 * enrolment instances and role assignments by default.
4680 * $options:
4681 * - 'keep_roles_and_enrolments' - false by default
4682 * - 'keep_groups_and_groupings' - false by default
4684 * @param int $courseid The id of the course that is being deleted
4685 * @param bool $showfeedback Whether to display notifications of each action the function performs.
4686 * @param array $options extra options
4687 * @return bool true if all the removals succeeded. false if there were any failures. If this
4688 * method returns false, some of the removals will probably have succeeded, and others
4689 * failed, but you have no way of knowing which.
4691 function remove_course_contents($courseid, $showfeedback = true, array $options = null) {
4692 global $CFG, $DB, $OUTPUT;
4694 require_once($CFG->libdir.'/badgeslib.php');
4695 require_once($CFG->libdir.'/completionlib.php');
4696 require_once($CFG->libdir.'/questionlib.php');
4697 require_once($CFG->libdir.'/gradelib.php');
4698 require_once($CFG->dirroot.'/group/lib.php');
4699 require_once($CFG->dirroot.'/comment/lib.php');
4700 require_once($CFG->dirroot.'/rating/lib.php');
4701 require_once($CFG->dirroot.'/notes/lib.php');
4703 // Handle course badges.
4704 badges_handle_course_deletion($courseid);
4706 // NOTE: these concatenated strings are suboptimal, but it is just extra info...
4707 $strdeleted = get_string('deleted').' - ';
4709 // Some crazy wishlist of stuff we should skip during purging of course content.
4710 $options = (array)$options;
4712 $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
4713 $coursecontext = context_course::instance($courseid);
4714 $fs = get_file_storage();
4716 // Delete course completion information, this has to be done before grades and enrols.
4717 $cc = new completion_info($course);
4718 $cc->clear_criteria();
4719 if ($showfeedback) {
4720 echo $OUTPUT->notification($strdeleted.get_string('completion', 'completion'), 'notifysuccess');
4723 // Remove all data from gradebook - this needs to be done before course modules
4724 // because while deleting this information, the system may need to reference
4725 // the course modules that own the grades.
4726 remove_course_grades($courseid, $showfeedback);
4727 remove_grade_letters($coursecontext, $showfeedback);
4729 // Delete course blocks in any all child contexts,
4730 // they may depend on modules so delete them first.
4731 $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2.
4732 foreach ($childcontexts as $childcontext) {
4733 blocks_delete_all_for_context($childcontext->id);
4735 unset($childcontexts);
4736 blocks_delete_all_for_context($coursecontext->id);
4737 if ($showfeedback) {
4738 echo $OUTPUT->notification($strdeleted.get_string('type_block_plural', 'plugin'), 'notifysuccess');
4741 $DB->set_field('course_modules', 'deletioninprogress', '1', ['course' => $courseid]);
4742 rebuild_course_cache($courseid, true);
4744 // Get the list of all modules that are properly installed.
4745 $allmodules = $DB->get_records_menu('modules', array(), '', 'name, id');
4747 // Delete every instance of every module,
4748 // this has to be done before deleting of course level stuff.
4749 $locations = core_component::get_plugin_list('mod');
4750 foreach ($locations as $modname => $moddir) {
4751 if ($modname === 'NEWMODULE') {
4752 continue;
4754 if (array_key_exists($modname, $allmodules)) {
4755 $sql = "SELECT cm.*, m.id AS modinstance, m.name, '$modname' AS modname
4756 FROM {".$modname."} m
4757 LEFT JOIN {course_modules} cm ON cm.instance = m.id AND cm.module = :moduleid
4758 WHERE m.course = :courseid";
4759 $instances = $DB->get_records_sql($sql, array('courseid' => $course->id,
4760 'modulename' => $modname, 'moduleid' => $allmodules[$modname]));
4762 include_once("$moddir/lib.php"); // Shows php warning only if plugin defective.
4763 $moddelete = $modname .'_delete_instance'; // Delete everything connected to an instance.
4765 if ($instances) {
4766 foreach ($instances as $cm) {
4767 if ($cm->id) {
4768 // Delete activity context questions and question categories.
4769 question_delete_activity($cm);
4770 // Notify the competency subsystem.
4771 \core_competency\api::hook_course_module_deleted($cm);
4773 // Delete all tag instances associated with the instance of this module.
4774 core_tag_tag::delete_instances("mod_{$modname}", null, context_module::instance($cm->id)->id);
4775 core_tag_tag::remove_all_item_tags('core', 'course_modules', $cm->id);
4777 if (function_exists($moddelete)) {
4778 // This purges all module data in related tables, extra user prefs, settings, etc.
4779 $moddelete($cm->modinstance);
4780 } else {
4781 // NOTE: we should not allow installation of modules with missing delete support!
4782 debugging("Defective module '$modname' detected when deleting course contents: missing function $moddelete()!");
4783 $DB->delete_records($modname, array('id' => $cm->modinstance));
4786 if ($cm->id) {
4787 // Delete cm and its context - orphaned contexts are purged in cron in case of any race condition.
4788 context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
4789 $DB->delete_records('course_modules_completion', ['coursemoduleid' => $cm->id]);
4790 $DB->delete_records('course_modules_viewed', ['coursemoduleid' => $cm->id]);
4791 $DB->delete_records('course_modules', array('id' => $cm->id));
4792 rebuild_course_cache($cm->course, true);
4796 if ($instances and $showfeedback) {
4797 echo $OUTPUT->notification($strdeleted.get_string('pluginname', $modname), 'notifysuccess');
4799 } else {
4800 // Ooops, this module is not properly installed, force-delete it in the next block.
4804 // We have tried to delete everything the nice way - now let's force-delete any remaining module data.
4806 // Delete completion defaults.
4807 $DB->delete_records("course_completion_defaults", array("course" => $courseid));
4809 // Remove all data from availability and completion tables that is associated
4810 // with course-modules belonging to this course. Note this is done even if the
4811 // features are not enabled now, in case they were enabled previously.
4812 $DB->delete_records_subquery('course_modules_completion', 'coursemoduleid', 'id',
4813 'SELECT id from {course_modules} WHERE course = ?', [$courseid]);
4814 $DB->delete_records_subquery('course_modules_viewed', 'coursemoduleid', 'id',
4815 'SELECT id from {course_modules} WHERE course = ?', [$courseid]);
4817 // Remove course-module data that has not been removed in modules' _delete_instance callbacks.
4818 $cms = $DB->get_records('course_modules', array('course' => $course->id));
4819 $allmodulesbyid = array_flip($allmodules);
4820 foreach ($cms as $cm) {
4821 if (array_key_exists($cm->module, $allmodulesbyid)) {
4822 try {
4823 $DB->delete_records($allmodulesbyid[$cm->module], array('id' => $cm->instance));
4824 } catch (Exception $e) {
4825 // Ignore weird or missing table problems.
4828 context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
4829 $DB->delete_records('course_modules', array('id' => $cm->id));
4830 rebuild_course_cache($cm->course, true);
4833 if ($showfeedback) {
4834 echo $OUTPUT->notification($strdeleted.get_string('type_mod_plural', 'plugin'), 'notifysuccess');
4837 // Delete questions and question categories.
4838 question_delete_course($course);
4839 if ($showfeedback) {
4840 echo $OUTPUT->notification($strdeleted.get_string('questions', 'question'), 'notifysuccess');
4843 // Delete content bank contents.
4844 $cb = new \core_contentbank\contentbank();
4845 $cbdeleted = $cb->delete_contents($coursecontext);
4846 if ($showfeedback && $cbdeleted) {
4847 echo $OUTPUT->notification($strdeleted.get_string('contentbank', 'contentbank'), 'notifysuccess');
4850 // Make sure there are no subcontexts left - all valid blocks and modules should be already gone.
4851 $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2.
4852 foreach ($childcontexts as $childcontext) {
4853 $childcontext->delete();
4855 unset($childcontexts);
4857 // Remove roles and enrolments by default.
4858 if (empty($options['keep_roles_and_enrolments'])) {
4859 // This hack is used in restore when deleting contents of existing course.
4860 // During restore, we should remove only enrolment related data that the user performing the restore has a
4861 // permission to remove.
4862 $userid = $options['userid'] ?? null;
4863 enrol_course_delete($course, $userid);
4864 role_unassign_all(array('contextid' => $coursecontext->id, 'component' => ''), true);
4865 if ($showfeedback) {
4866 echo $OUTPUT->notification($strdeleted.get_string('type_enrol_plural', 'plugin'), 'notifysuccess');
4870 // Delete any groups, removing members and grouping/course links first.
4871 if (empty($options['keep_groups_and_groupings'])) {
4872 groups_delete_groupings($course->id, $showfeedback);
4873 groups_delete_groups($course->id, $showfeedback);
4876 // Filters be gone!
4877 filter_delete_all_for_context($coursecontext->id);
4879 // Notes, you shall not pass!
4880 note_delete_all($course->id);
4882 // Die comments!
4883 comment::delete_comments($coursecontext->id);
4885 // Ratings are history too.
4886 $delopt = new stdclass();
4887 $delopt->contextid = $coursecontext->id;
4888 $rm = new rating_manager();
4889 $rm->delete_ratings($delopt);
4891 // Delete course tags.
4892 core_tag_tag::remove_all_item_tags('core', 'course', $course->id);
4894 // Give the course format the opportunity to remove its obscure data.
4895 $format = course_get_format($course);
4896 $format->delete_format_data();
4898 // Notify the competency subsystem.
4899 \core_competency\api::hook_course_deleted($course);
4901 // Delete calendar events.
4902 $DB->delete_records('event', array('courseid' => $course->id));
4903 $fs->delete_area_files($coursecontext->id, 'calendar');
4905 // Delete all related records in other core tables that may have a courseid
4906 // This array stores the tables that need to be cleared, as
4907 // table_name => column_name that contains the course id.
4908 $tablestoclear = array(
4909 'backup_courses' => 'courseid', // Scheduled backup stuff.
4910 'user_lastaccess' => 'courseid', // User access info.
4912 foreach ($tablestoclear as $table => $col) {
4913 $DB->delete_records($table, array($col => $course->id));
4916 // Delete all course backup files.
4917 $fs->delete_area_files($coursecontext->id, 'backup');
4919 // Cleanup course record - remove links to deleted stuff.
4920 // Do not wipe cacherev, as this course might be reused and we need to ensure that it keeps
4921 // increasing.
4922 $oldcourse = new stdClass();
4923 $oldcourse->id = $course->id;
4924 $oldcourse->summary = '';
4925 $oldcourse->legacyfiles = 0;
4926 if (!empty($options['keep_groups_and_groupings'])) {
4927 $oldcourse->defaultgroupingid = 0;
4929 $DB->update_record('course', $oldcourse);
4931 // Delete course sections.
4932 $DB->delete_records('course_sections', array('course' => $course->id));
4934 // Delete legacy, section and any other course files.
4935 $fs->delete_area_files($coursecontext->id, 'course'); // Files from summary and section.
4937 // Delete all remaining stuff linked to context such as files, comments, ratings, etc.
4938 if (empty($options['keep_roles_and_enrolments']) and empty($options['keep_groups_and_groupings'])) {
4939 // Easy, do not delete the context itself...
4940 $coursecontext->delete_content();
4941 } else {
4942 // Hack alert!!!!
4943 // We can not drop all context stuff because it would bork enrolments and roles,
4944 // there might be also files used by enrol plugins...
4947 // Delete legacy files - just in case some files are still left there after conversion to new file api,
4948 // also some non-standard unsupported plugins may try to store something there.
4949 fulldelete($CFG->dataroot.'/'.$course->id);
4951 // Delete from cache to reduce the cache size especially makes sense in case of bulk course deletion.
4952 course_modinfo::purge_course_cache($courseid);
4954 // Trigger a course content deleted event.
4955 $event = \core\event\course_content_deleted::create(array(
4956 'objectid' => $course->id,
4957 'context' => $coursecontext,
4958 'other' => array('shortname' => $course->shortname,
4959 'fullname' => $course->fullname,
4960 'options' => $options) // Passing this for legacy reasons.
4962 $event->add_record_snapshot('course', $course);
4963 $event->trigger();
4965 return true;
4969 * Change dates in module - used from course reset.
4971 * @param string $modname forum, assignment, etc
4972 * @param array $fields array of date fields from mod table
4973 * @param int $timeshift time difference
4974 * @param int $courseid
4975 * @param int $modid (Optional) passed if specific mod instance in course needs to be updated.
4976 * @return bool success
4978 function shift_course_mod_dates($modname, $fields, $timeshift, $courseid, $modid = 0) {
4979 global $CFG, $DB;
4980 include_once($CFG->dirroot.'/mod/'.$modname.'/lib.php');
4982 $return = true;
4983 $params = array($timeshift, $courseid);
4984 foreach ($fields as $field) {
4985 $updatesql = "UPDATE {".$modname."}
4986 SET $field = $field + ?
4987 WHERE course=? AND $field<>0";
4988 if ($modid) {
4989 $updatesql .= ' AND id=?';
4990 $params[] = $modid;
4992 $return = $DB->execute($updatesql, $params) && $return;
4995 return $return;
4999 * This function will empty a course of user data.
5000 * It will retain the activities and the structure of the course.
5002 * @param object $data an object containing all the settings including courseid (without magic quotes)
5003 * @return array status array of array component, item, error
5005 function reset_course_userdata($data) {
5006 global $CFG, $DB;
5007 require_once($CFG->libdir.'/gradelib.php');
5008 require_once($CFG->libdir.'/completionlib.php');
5009 require_once($CFG->dirroot.'/completion/criteria/completion_criteria_date.php');
5010 require_once($CFG->dirroot.'/group/lib.php');
5012 $data->courseid = $data->id;
5013 $context = context_course::instance($data->courseid);
5015 $eventparams = array(
5016 'context' => $context,
5017 'courseid' => $data->id,
5018 'other' => array(
5019 'reset_options' => (array) $data
5022 $event = \core\event\course_reset_started::create($eventparams);
5023 $event->trigger();
5025 // Calculate the time shift of dates.
5026 if (!empty($data->reset_start_date)) {
5027 // Time part of course startdate should be zero.
5028 $data->timeshift = $data->reset_start_date - usergetmidnight($data->reset_start_date_old);
5029 } else {
5030 $data->timeshift = 0;
5033 // Result array: component, item, error.
5034 $status = array();
5036 // Start the resetting.
5037 $componentstr = get_string('general');
5039 // Move the course start time.
5040 if (!empty($data->reset_start_date) and $data->timeshift) {
5041 // Change course start data.
5042 $DB->set_field('course', 'startdate', $data->reset_start_date, array('id' => $data->courseid));
5043 // Update all course and group events - do not move activity events.
5044 $updatesql = "UPDATE {event}
5045 SET timestart = timestart + ?
5046 WHERE courseid=? AND instance=0";
5047 $DB->execute($updatesql, array($data->timeshift, $data->courseid));
5049 // Update any date activity restrictions.
5050 if ($CFG->enableavailability) {
5051 \availability_date\condition::update_all_dates($data->courseid, $data->timeshift);
5054 // Update completion expected dates.
5055 if ($CFG->enablecompletion) {
5056 $modinfo = get_fast_modinfo($data->courseid);
5057 $changed = false;
5058 foreach ($modinfo->get_cms() as $cm) {
5059 if ($cm->completion && !empty($cm->completionexpected)) {
5060 $DB->set_field('course_modules', 'completionexpected', $cm->completionexpected + $data->timeshift,
5061 array('id' => $cm->id));
5062 $changed = true;
5066 // Clear course cache if changes made.
5067 if ($changed) {
5068 rebuild_course_cache($data->courseid, true);
5071 // Update course date completion criteria.
5072 \completion_criteria_date::update_date($data->courseid, $data->timeshift);
5075 $status[] = array('component' => $componentstr, 'item' => get_string('datechanged'), 'error' => false);
5078 if (!empty($data->reset_end_date)) {
5079 // If the user set a end date value respect it.
5080 $DB->set_field('course', 'enddate', $data->reset_end_date, array('id' => $data->courseid));
5081 } else if ($data->timeshift > 0 && $data->reset_end_date_old) {
5082 // If there is a time shift apply it to the end date as well.
5083 $enddate = $data->reset_end_date_old + $data->timeshift;
5084 $DB->set_field('course', 'enddate', $enddate, array('id' => $data->courseid));
5087 if (!empty($data->reset_events)) {
5088 $DB->delete_records('event', array('courseid' => $data->courseid));
5089 $status[] = array('component' => $componentstr, 'item' => get_string('deleteevents', 'calendar'), 'error' => false);
5092 if (!empty($data->reset_notes)) {
5093 require_once($CFG->dirroot.'/notes/lib.php');
5094 note_delete_all($data->courseid);
5095 $status[] = array('component' => $componentstr, 'item' => get_string('deletenotes', 'notes'), 'error' => false);
5098 if (!empty($data->delete_blog_associations)) {
5099 require_once($CFG->dirroot.'/blog/lib.php');
5100 blog_remove_associations_for_course($data->courseid);
5101 $status[] = array('component' => $componentstr, 'item' => get_string('deleteblogassociations', 'blog'), 'error' => false);
5104 if (!empty($data->reset_completion)) {
5105 // Delete course and activity completion information.
5106 $course = $DB->get_record('course', array('id' => $data->courseid));
5107 $cc = new completion_info($course);
5108 $cc->delete_all_completion_data();
5109 $status[] = array('component' => $componentstr,
5110 'item' => get_string('deletecompletiondata', 'completion'), 'error' => false);
5113 if (!empty($data->reset_competency_ratings)) {
5114 \core_competency\api::hook_course_reset_competency_ratings($data->courseid);
5115 $status[] = array('component' => $componentstr,
5116 'item' => get_string('deletecompetencyratings', 'core_competency'), 'error' => false);
5119 $componentstr = get_string('roles');
5121 if (!empty($data->reset_roles_overrides)) {
5122 $children = $context->get_child_contexts();
5123 foreach ($children as $child) {
5124 $child->delete_capabilities();
5126 $context->delete_capabilities();
5127 $status[] = array('component' => $componentstr, 'item' => get_string('deletecourseoverrides', 'role'), 'error' => false);
5130 if (!empty($data->reset_roles_local)) {
5131 $children = $context->get_child_contexts();
5132 foreach ($children as $child) {
5133 role_unassign_all(array('contextid' => $child->id));
5135 $status[] = array('component' => $componentstr, 'item' => get_string('deletelocalroles', 'role'), 'error' => false);
5138 // First unenrol users - this cleans some of related user data too, such as forum subscriptions, tracking, etc.
5139 $data->unenrolled = array();
5140 if (!empty($data->unenrol_users)) {
5141 $plugins = enrol_get_plugins(true);
5142 $instances = enrol_get_instances($data->courseid, true);
5143 foreach ($instances as $key => $instance) {
5144 if (!isset($plugins[$instance->enrol])) {
5145 unset($instances[$key]);
5146 continue;
5150 $usersroles = enrol_get_course_users_roles($data->courseid);
5151 foreach ($data->unenrol_users as $withroleid) {
5152 if ($withroleid) {
5153 $sql = "SELECT ue.*
5154 FROM {user_enrolments} ue
5155 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5156 JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5157 JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.roleid = :roleid AND ra.userid = ue.userid)";
5158 $params = array('courseid' => $data->courseid, 'roleid' => $withroleid, 'courselevel' => CONTEXT_COURSE);
5160 } else {
5161 // Without any role assigned at course context.
5162 $sql = "SELECT ue.*
5163 FROM {user_enrolments} ue
5164 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
5165 JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid)
5166 LEFT JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.userid = ue.userid)
5167 WHERE ra.id IS null";
5168 $params = array('courseid' => $data->courseid, 'courselevel' => CONTEXT_COURSE);
5171 $rs = $DB->get_recordset_sql($sql, $params);
5172 foreach ($rs as $ue) {
5173 if (!isset($instances[$ue->enrolid])) {
5174 continue;
5176 $instance = $instances[$ue->enrolid];
5177 $plugin = $plugins[$instance->enrol];
5178 if (!$plugin->allow_unenrol($instance) and !$plugin->allow_unenrol_user($instance, $ue)) {
5179 continue;
5182 if ($withroleid && count($usersroles[$ue->userid]) > 1) {
5183 // If we don't remove all roles and user has more than one role, just remove this role.
5184 role_unassign($withroleid, $ue->userid, $context->id);
5186 unset($usersroles[$ue->userid][$withroleid]);
5187 } else {
5188 // If we remove all roles or user has only one role, unenrol user from course.
5189 $plugin->unenrol_user($instance, $ue->userid);
5191 $data->unenrolled[$ue->userid] = $ue->userid;
5193 $rs->close();
5196 if (!empty($data->unenrolled)) {
5197 $status[] = array(
5198 'component' => $componentstr,
5199 'item' => get_string('unenrol', 'enrol').' ('.count($data->unenrolled).')',
5200 'error' => false
5204 $componentstr = get_string('groups');
5206 // Remove all group members.
5207 if (!empty($data->reset_groups_members)) {
5208 groups_delete_group_members($data->courseid);
5209 $status[] = array('component' => $componentstr, 'item' => get_string('removegroupsmembers', 'group'), 'error' => false);
5212 // Remove all groups.
5213 if (!empty($data->reset_groups_remove)) {
5214 groups_delete_groups($data->courseid, false);
5215 $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroups', 'group'), 'error' => false);
5218 // Remove all grouping members.
5219 if (!empty($data->reset_groupings_members)) {
5220 groups_delete_groupings_groups($data->courseid, false);
5221 $status[] = array('component' => $componentstr, 'item' => get_string('removegroupingsmembers', 'group'), 'error' => false);
5224 // Remove all groupings.
5225 if (!empty($data->reset_groupings_remove)) {
5226 groups_delete_groupings($data->courseid, false);
5227 $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroupings', 'group'), 'error' => false);
5230 // Look in every instance of every module for data to delete.
5231 $unsupportedmods = array();
5232 if ($allmods = $DB->get_records('modules') ) {
5233 foreach ($allmods as $mod) {
5234 $modname = $mod->name;
5235 $modfile = $CFG->dirroot.'/mod/'. $modname.'/lib.php';
5236 $moddeleteuserdata = $modname.'_reset_userdata'; // Function to delete user data.
5237 if (file_exists($modfile)) {
5238 if (!$DB->count_records($modname, array('course' => $data->courseid))) {
5239 continue; // Skip mods with no instances.
5241 include_once($modfile);
5242 if (function_exists($moddeleteuserdata)) {
5243 $modstatus = $moddeleteuserdata($data);
5244 if (is_array($modstatus)) {
5245 $status = array_merge($status, $modstatus);
5246 } else {
5247 debugging('Module '.$modname.' returned incorrect staus - must be an array!');
5249 } else {
5250 $unsupportedmods[] = $mod;
5252 } else {
5253 debugging('Missing lib.php in '.$modname.' module!');
5255 // Update calendar events for all modules.
5256 course_module_bulk_update_calendar_events($modname, $data->courseid);
5258 // Purge the course cache after resetting course start date. MDL-76936
5259 if ($data->timeshift) {
5260 course_modinfo::purge_course_cache($data->courseid);
5264 // Mention unsupported mods.
5265 if (!empty($unsupportedmods)) {
5266 foreach ($unsupportedmods as $mod) {
5267 $status[] = array(
5268 'component' => get_string('modulenameplural', $mod->name),
5269 'item' => '',
5270 'error' => get_string('resetnotimplemented')
5275 $componentstr = get_string('gradebook', 'grades');
5276 // Reset gradebook,.
5277 if (!empty($data->reset_gradebook_items)) {
5278 remove_course_grades($data->courseid, false);
5279 grade_grab_course_grades($data->courseid);
5280 grade_regrade_final_grades($data->courseid);
5281 $status[] = array('component' => $componentstr, 'item' => get_string('removeallcourseitems', 'grades'), 'error' => false);
5283 } else if (!empty($data->reset_gradebook_grades)) {
5284 grade_course_reset($data->courseid);
5285 $status[] = array('component' => $componentstr, 'item' => get_string('removeallcoursegrades', 'grades'), 'error' => false);
5287 // Reset comments.
5288 if (!empty($data->reset_comments)) {
5289 require_once($CFG->dirroot.'/comment/lib.php');
5290 comment::reset_course_page_comments($context);
5293 $event = \core\event\course_reset_ended::create($eventparams);
5294 $event->trigger();
5296 return $status;
5300 * Generate an email processing address.
5302 * @param int $modid
5303 * @param string $modargs
5304 * @return string Returns email processing address
5306 function generate_email_processing_address($modid, $modargs) {
5307 global $CFG;
5309 $header = $CFG->mailprefix . substr(base64_encode(pack('C', $modid)), 0, 2).$modargs;
5310 return $header . substr(md5($header.get_site_identifier()), 0, 16).'@'.$CFG->maildomain;
5316 * @todo Finish documenting this function
5318 * @param string $modargs
5319 * @param string $body Currently unused
5321 function moodle_process_email($modargs, $body) {
5322 global $DB;
5324 // The first char should be an unencoded letter. We'll take this as an action.
5325 switch ($modargs[0]) {
5326 case 'B': { // Bounce.
5327 list(, $userid) = unpack('V', base64_decode(substr($modargs, 1, 8)));
5328 if ($user = $DB->get_record("user", array('id' => $userid), "id,email")) {
5329 // Check the half md5 of their email.
5330 $md5check = substr(md5($user->email), 0, 16);
5331 if ($md5check == substr($modargs, -16)) {
5332 set_bounce_count($user);
5334 // Else maybe they've already changed it?
5337 break;
5338 // Maybe more later?
5342 // CORRESPONDENCE.
5345 * Get mailer instance, enable buffering, flush buffer or disable buffering.
5347 * @param string $action 'get', 'buffer', 'close' or 'flush'
5348 * @return moodle_phpmailer|null mailer instance if 'get' used or nothing
5350 function get_mailer($action='get') {
5351 global $CFG;
5353 /** @var moodle_phpmailer $mailer */
5354 static $mailer = null;
5355 static $counter = 0;
5357 if (!isset($CFG->smtpmaxbulk)) {
5358 $CFG->smtpmaxbulk = 1;
5361 if ($action == 'get') {
5362 $prevkeepalive = false;
5364 if (isset($mailer) and $mailer->Mailer == 'smtp') {
5365 if ($counter < $CFG->smtpmaxbulk and !$mailer->isError()) {
5366 $counter++;
5367 // Reset the mailer.
5368 $mailer->Priority = 3;
5369 $mailer->CharSet = 'UTF-8'; // Our default.
5370 $mailer->ContentType = "text/plain";
5371 $mailer->Encoding = "8bit";
5372 $mailer->From = "root@localhost";
5373 $mailer->FromName = "Root User";
5374 $mailer->Sender = "";
5375 $mailer->Subject = "";
5376 $mailer->Body = "";
5377 $mailer->AltBody = "";
5378 $mailer->ConfirmReadingTo = "";
5380 $mailer->clearAllRecipients();
5381 $mailer->clearReplyTos();
5382 $mailer->clearAttachments();
5383 $mailer->clearCustomHeaders();
5384 return $mailer;
5387 $prevkeepalive = $mailer->SMTPKeepAlive;
5388 get_mailer('flush');
5391 require_once($CFG->libdir.'/phpmailer/moodle_phpmailer.php');
5392 $mailer = new moodle_phpmailer();
5394 $counter = 1;
5396 if ($CFG->smtphosts == 'qmail') {
5397 // Use Qmail system.
5398 $mailer->isQmail();
5400 } else if (empty($CFG->smtphosts)) {
5401 // Use PHP mail() = sendmail.
5402 $mailer->isMail();
5404 } else {
5405 // Use SMTP directly.
5406 $mailer->isSMTP();
5407 if (!empty($CFG->debugsmtp) && (!empty($CFG->debugdeveloper))) {
5408 $mailer->SMTPDebug = 3;
5410 // Specify main and backup servers.
5411 $mailer->Host = $CFG->smtphosts;
5412 // Specify secure connection protocol.
5413 $mailer->SMTPSecure = $CFG->smtpsecure;
5414 // Use previous keepalive.
5415 $mailer->SMTPKeepAlive = $prevkeepalive;
5417 if ($CFG->smtpuser) {
5418 // Use SMTP authentication.
5419 $mailer->SMTPAuth = true;
5420 $mailer->Username = $CFG->smtpuser;
5421 $mailer->Password = $CFG->smtppass;
5425 return $mailer;
5428 $nothing = null;
5430 // Keep smtp session open after sending.
5431 if ($action == 'buffer') {
5432 if (!empty($CFG->smtpmaxbulk)) {
5433 get_mailer('flush');
5434 $m = get_mailer();
5435 if ($m->Mailer == 'smtp') {
5436 $m->SMTPKeepAlive = true;
5439 return $nothing;
5442 // Close smtp session, but continue buffering.
5443 if ($action == 'flush') {
5444 if (isset($mailer) and $mailer->Mailer == 'smtp') {
5445 if (!empty($mailer->SMTPDebug)) {
5446 echo '<pre>'."\n";
5448 $mailer->SmtpClose();
5449 if (!empty($mailer->SMTPDebug)) {
5450 echo '</pre>';
5453 return $nothing;
5456 // Close smtp session, do not buffer anymore.
5457 if ($action == 'close') {
5458 if (isset($mailer) and $mailer->Mailer == 'smtp') {
5459 get_mailer('flush');
5460 $mailer->SMTPKeepAlive = false;
5462 $mailer = null; // Better force new instance.
5463 return $nothing;
5468 * A helper function to test for email diversion
5470 * @param string $email
5471 * @return bool Returns true if the email should be diverted
5473 function email_should_be_diverted($email) {
5474 global $CFG;
5476 if (empty($CFG->divertallemailsto)) {
5477 return false;
5480 if (empty($CFG->divertallemailsexcept)) {
5481 return true;
5484 $patterns = array_map('trim', preg_split("/[\s,]+/", $CFG->divertallemailsexcept, -1, PREG_SPLIT_NO_EMPTY));
5485 foreach ($patterns as $pattern) {
5486 if (preg_match("/{$pattern}/i", $email)) {
5487 return false;
5491 return true;
5495 * Generate a unique email Message-ID using the moodle domain and install path
5497 * @param string $localpart An optional unique message id prefix.
5498 * @return string The formatted ID ready for appending to the email headers.
5500 function generate_email_messageid($localpart = null) {
5501 global $CFG;
5503 $urlinfo = parse_url($CFG->wwwroot);
5504 $base = '@' . $urlinfo['host'];
5506 // If multiple moodles are on the same domain we want to tell them
5507 // apart so we add the install path to the local part. This means
5508 // that the id local part should never contain a / character so
5509 // we can correctly parse the id to reassemble the wwwroot.
5510 if (isset($urlinfo['path'])) {
5511 $base = $urlinfo['path'] . $base;
5514 if (empty($localpart)) {
5515 $localpart = uniqid('', true);
5518 // Because we may have an option /installpath suffix to the local part
5519 // of the id we need to escape any / chars which are in the $localpart.
5520 $localpart = str_replace('/', '%2F', $localpart);
5522 return '<' . $localpart . $base . '>';
5526 * Send an email to a specified user
5528 * @param stdClass $user A {@link $USER} object
5529 * @param stdClass $from A {@link $USER} object
5530 * @param string $subject plain text subject line of the email
5531 * @param string $messagetext plain text version of the message
5532 * @param string $messagehtml complete html version of the message (optional)
5533 * @param string $attachment a file on the filesystem, either relative to $CFG->dataroot or a full path to a file in one of
5534 * the following directories: $CFG->cachedir, $CFG->dataroot, $CFG->dirroot, $CFG->localcachedir, $CFG->tempdir
5535 * @param string $attachname the name of the file (extension indicates MIME)
5536 * @param bool $usetrueaddress determines whether $from email address should
5537 * be sent out. Will be overruled by user profile setting for maildisplay
5538 * @param string $replyto Email address to reply to
5539 * @param string $replytoname Name of reply to recipient
5540 * @param int $wordwrapwidth custom word wrap width, default 79
5541 * @return bool Returns true if mail was sent OK and false if there was an error.
5543 function email_to_user($user, $from, $subject, $messagetext, $messagehtml = '', $attachment = '', $attachname = '',
5544 $usetrueaddress = true, $replyto = '', $replytoname = '', $wordwrapwidth = 79) {
5546 global $CFG, $PAGE, $SITE;
5548 if (empty($user) or empty($user->id)) {
5549 debugging('Can not send email to null user', DEBUG_DEVELOPER);
5550 return false;
5553 if (empty($user->email)) {
5554 debugging('Can not send email to user without email: '.$user->id, DEBUG_DEVELOPER);
5555 return false;
5558 if (!empty($user->deleted)) {
5559 debugging('Can not send email to deleted user: '.$user->id, DEBUG_DEVELOPER);
5560 return false;
5563 if (defined('BEHAT_SITE_RUNNING')) {
5564 // Fake email sending in behat.
5565 return true;
5568 if (!empty($CFG->noemailever)) {
5569 // Hidden setting for development sites, set in config.php if needed.
5570 debugging('Not sending email due to $CFG->noemailever config setting', DEBUG_NORMAL);
5571 return true;
5574 if (email_should_be_diverted($user->email)) {
5575 $subject = "[DIVERTED {$user->email}] $subject";
5576 $user = clone($user);
5577 $user->email = $CFG->divertallemailsto;
5580 // Skip mail to suspended users.
5581 if ((isset($user->auth) && $user->auth=='nologin') or (isset($user->suspended) && $user->suspended)) {
5582 return true;
5585 if (!validate_email($user->email)) {
5586 // We can not send emails to invalid addresses - it might create security issue or confuse the mailer.
5587 debugging("email_to_user: User $user->id (".fullname($user).") email ($user->email) is invalid! Not sending.");
5588 return false;
5591 if (over_bounce_threshold($user)) {
5592 debugging("email_to_user: User $user->id (".fullname($user).") is over bounce threshold! Not sending.");
5593 return false;
5596 // TLD .invalid is specifically reserved for invalid domain names.
5597 // For More information, see {@link http://tools.ietf.org/html/rfc2606#section-2}.
5598 if (substr($user->email, -8) == '.invalid') {
5599 debugging("email_to_user: User $user->id (".fullname($user).") email domain ($user->email) is invalid! Not sending.");
5600 return true; // This is not an error.
5603 // If the user is a remote mnet user, parse the email text for URL to the
5604 // wwwroot and modify the url to direct the user's browser to login at their
5605 // home site (identity provider - idp) before hitting the link itself.
5606 if (is_mnet_remote_user($user)) {
5607 require_once($CFG->dirroot.'/mnet/lib.php');
5609 $jumpurl = mnet_get_idp_jump_url($user);
5610 $callback = partial('mnet_sso_apply_indirection', $jumpurl);
5612 $messagetext = preg_replace_callback("%($CFG->wwwroot[^[:space:]]*)%",
5613 $callback,
5614 $messagetext);
5615 $messagehtml = preg_replace_callback("%href=[\"'`]($CFG->wwwroot[\w_:\?=#&@/;.~-]*)[\"'`]%",
5616 $callback,
5617 $messagehtml);
5619 $mail = get_mailer();
5621 if (!empty($mail->SMTPDebug)) {
5622 echo '<pre>' . "\n";
5625 $temprecipients = array();
5626 $tempreplyto = array();
5628 // Make sure that we fall back onto some reasonable no-reply address.
5629 $noreplyaddressdefault = 'noreply@' . get_host_from_url($CFG->wwwroot);
5630 $noreplyaddress = empty($CFG->noreplyaddress) ? $noreplyaddressdefault : $CFG->noreplyaddress;
5632 if (!validate_email($noreplyaddress)) {
5633 debugging('email_to_user: Invalid noreply-email '.s($noreplyaddress));
5634 $noreplyaddress = $noreplyaddressdefault;
5637 // Make up an email address for handling bounces.
5638 if (!empty($CFG->handlebounces)) {
5639 $modargs = 'B'.base64_encode(pack('V', $user->id)).substr(md5($user->email), 0, 16);
5640 $mail->Sender = generate_email_processing_address(0, $modargs);
5641 } else {
5642 $mail->Sender = $noreplyaddress;
5645 // Make sure that the explicit replyto is valid, fall back to the implicit one.
5646 if (!empty($replyto) && !validate_email($replyto)) {
5647 debugging('email_to_user: Invalid replyto-email '.s($replyto));
5648 $replyto = $noreplyaddress;
5651 if (is_string($from)) { // So we can pass whatever we want if there is need.
5652 $mail->From = $noreplyaddress;
5653 $mail->FromName = $from;
5654 // Check if using the true address is true, and the email is in the list of allowed domains for sending email,
5655 // and that the senders email setting is either displayed to everyone, or display to only other users that are enrolled
5656 // in a course with the sender.
5657 } else if ($usetrueaddress && can_send_from_real_email_address($from, $user)) {
5658 if (!validate_email($from->email)) {
5659 debugging('email_to_user: Invalid from-email '.s($from->email).' - not sending');
5660 // Better not to use $noreplyaddress in this case.
5661 return false;
5663 $mail->From = $from->email;
5664 $fromdetails = new stdClass();
5665 $fromdetails->name = fullname($from);
5666 $fromdetails->url = preg_replace('#^https?://#', '', $CFG->wwwroot);
5667 $fromdetails->siteshortname = format_string($SITE->shortname);
5668 $fromstring = $fromdetails->name;
5669 if ($CFG->emailfromvia == EMAIL_VIA_ALWAYS) {
5670 $fromstring = get_string('emailvia', 'core', $fromdetails);
5672 $mail->FromName = $fromstring;
5673 if (empty($replyto)) {
5674 $tempreplyto[] = array($from->email, fullname($from));
5676 } else {
5677 $mail->From = $noreplyaddress;
5678 $fromdetails = new stdClass();
5679 $fromdetails->name = fullname($from);
5680 $fromdetails->url = preg_replace('#^https?://#', '', $CFG->wwwroot);
5681 $fromdetails->siteshortname = format_string($SITE->shortname);
5682 $fromstring = $fromdetails->name;
5683 if ($CFG->emailfromvia != EMAIL_VIA_NEVER) {
5684 $fromstring = get_string('emailvia', 'core', $fromdetails);
5686 $mail->FromName = $fromstring;
5687 if (empty($replyto)) {
5688 $tempreplyto[] = array($noreplyaddress, get_string('noreplyname'));
5692 if (!empty($replyto)) {
5693 $tempreplyto[] = array($replyto, $replytoname);
5696 $temprecipients[] = array($user->email, fullname($user));
5698 // Set word wrap.
5699 $mail->WordWrap = $wordwrapwidth;
5701 if (!empty($from->customheaders)) {
5702 // Add custom headers.
5703 if (is_array($from->customheaders)) {
5704 foreach ($from->customheaders as $customheader) {
5705 $mail->addCustomHeader($customheader);
5707 } else {
5708 $mail->addCustomHeader($from->customheaders);
5712 // If the X-PHP-Originating-Script email header is on then also add an additional
5713 // header with details of where exactly in moodle the email was triggered from,
5714 // either a call to message_send() or to email_to_user().
5715 if (ini_get('mail.add_x_header')) {
5717 $stack = debug_backtrace(false);
5718 $origin = $stack[0];
5720 foreach ($stack as $depth => $call) {
5721 if ($call['function'] == 'message_send') {
5722 $origin = $call;
5726 $originheader = $CFG->wwwroot . ' => ' . gethostname() . ':'
5727 . str_replace($CFG->dirroot . '/', '', $origin['file']) . ':' . $origin['line'];
5728 $mail->addCustomHeader('X-Moodle-Originating-Script: ' . $originheader);
5731 if (!empty($CFG->emailheaders)) {
5732 $headers = array_map('trim', explode("\n", $CFG->emailheaders));
5733 foreach ($headers as $header) {
5734 if (!empty($header)) {
5735 $mail->addCustomHeader($header);
5740 if (!empty($from->priority)) {
5741 $mail->Priority = $from->priority;
5744 $renderer = $PAGE->get_renderer('core');
5745 $context = array(
5746 'sitefullname' => $SITE->fullname,
5747 'siteshortname' => $SITE->shortname,
5748 'sitewwwroot' => $CFG->wwwroot,
5749 'subject' => $subject,
5750 'prefix' => $CFG->emailsubjectprefix,
5751 'to' => $user->email,
5752 'toname' => fullname($user),
5753 'from' => $mail->From,
5754 'fromname' => $mail->FromName,
5756 if (!empty($tempreplyto[0])) {
5757 $context['replyto'] = $tempreplyto[0][0];
5758 $context['replytoname'] = $tempreplyto[0][1];
5760 if ($user->id > 0) {
5761 $context['touserid'] = $user->id;
5762 $context['tousername'] = $user->username;
5765 if (!empty($user->mailformat) && $user->mailformat == 1) {
5766 // Only process html templates if the user preferences allow html email.
5768 if (!$messagehtml) {
5769 // If no html has been given, BUT there is an html wrapping template then
5770 // auto convert the text to html and then wrap it.
5771 $messagehtml = trim(text_to_html($messagetext));
5773 $context['body'] = $messagehtml;
5774 $messagehtml = $renderer->render_from_template('core/email_html', $context);
5777 $context['body'] = html_to_text(nl2br($messagetext));
5778 $mail->Subject = $renderer->render_from_template('core/email_subject', $context);
5779 $mail->FromName = $renderer->render_from_template('core/email_fromname', $context);
5780 $messagetext = $renderer->render_from_template('core/email_text', $context);
5782 // Autogenerate a MessageID if it's missing.
5783 if (empty($mail->MessageID)) {
5784 $mail->MessageID = generate_email_messageid();
5787 if ($messagehtml && !empty($user->mailformat) && $user->mailformat == 1) {
5788 // Don't ever send HTML to users who don't want it.
5789 $mail->isHTML(true);
5790 $mail->Encoding = 'quoted-printable';
5791 $mail->Body = $messagehtml;
5792 $mail->AltBody = "\n$messagetext\n";
5793 } else {
5794 $mail->IsHTML(false);
5795 $mail->Body = "\n$messagetext\n";
5798 if ($attachment && $attachname) {
5799 if (preg_match( "~\\.\\.~" , $attachment )) {
5800 // Security check for ".." in dir path.
5801 $supportuser = core_user::get_support_user();
5802 $temprecipients[] = array($supportuser->email, fullname($supportuser, true));
5803 $mail->addStringAttachment('Error in attachment. User attempted to attach a filename with a unsafe name.', 'error.txt', '8bit', 'text/plain');
5804 } else {
5805 require_once($CFG->libdir.'/filelib.php');
5806 $mimetype = mimeinfo('type', $attachname);
5808 // Before doing the comparison, make sure that the paths are correct (Windows uses slashes in the other direction).
5809 // The absolute (real) path is also fetched to ensure that comparisons to allowed paths are compared equally.
5810 $attachpath = str_replace('\\', '/', realpath($attachment));
5812 // Build an array of all filepaths from which attachments can be added (normalised slashes, absolute/real path).
5813 $allowedpaths = array_map(function(string $path): string {
5814 return str_replace('\\', '/', realpath($path));
5815 }, [
5816 $CFG->cachedir,
5817 $CFG->dataroot,
5818 $CFG->dirroot,
5819 $CFG->localcachedir,
5820 $CFG->tempdir,
5821 $CFG->localrequestdir,
5824 // Set addpath to true.
5825 $addpath = true;
5827 // Check if attachment includes one of the allowed paths.
5828 foreach (array_filter($allowedpaths) as $allowedpath) {
5829 // Set addpath to false if the attachment includes one of the allowed paths.
5830 if (strpos($attachpath, $allowedpath) === 0) {
5831 $addpath = false;
5832 break;
5836 // If the attachment is a full path to a file in the multiple allowed paths, use it as is,
5837 // otherwise assume it is a relative path from the dataroot (for backwards compatibility reasons).
5838 if ($addpath == true) {
5839 $attachment = $CFG->dataroot . '/' . $attachment;
5842 $mail->addAttachment($attachment, $attachname, 'base64', $mimetype);
5846 // Check if the email should be sent in an other charset then the default UTF-8.
5847 if ((!empty($CFG->sitemailcharset) || !empty($CFG->allowusermailcharset))) {
5849 // Use the defined site mail charset or eventually the one preferred by the recipient.
5850 $charset = $CFG->sitemailcharset;
5851 if (!empty($CFG->allowusermailcharset)) {
5852 if ($useremailcharset = get_user_preferences('mailcharset', '0', $user->id)) {
5853 $charset = $useremailcharset;
5857 // Convert all the necessary strings if the charset is supported.
5858 $charsets = get_list_of_charsets();
5859 unset($charsets['UTF-8']);
5860 if (in_array($charset, $charsets)) {
5861 $mail->CharSet = $charset;
5862 $mail->FromName = core_text::convert($mail->FromName, 'utf-8', strtolower($charset));
5863 $mail->Subject = core_text::convert($mail->Subject, 'utf-8', strtolower($charset));
5864 $mail->Body = core_text::convert($mail->Body, 'utf-8', strtolower($charset));
5865 $mail->AltBody = core_text::convert($mail->AltBody, 'utf-8', strtolower($charset));
5867 foreach ($temprecipients as $key => $values) {
5868 $temprecipients[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset));
5870 foreach ($tempreplyto as $key => $values) {
5871 $tempreplyto[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset));
5876 foreach ($temprecipients as $values) {
5877 $mail->addAddress($values[0], $values[1]);
5879 foreach ($tempreplyto as $values) {
5880 $mail->addReplyTo($values[0], $values[1]);
5883 if (!empty($CFG->emaildkimselector)) {
5884 $domain = substr(strrchr($mail->From, "@"), 1);
5885 $pempath = "{$CFG->dataroot}/dkim/{$domain}/{$CFG->emaildkimselector}.private";
5886 if (file_exists($pempath)) {
5887 $mail->DKIM_domain = $domain;
5888 $mail->DKIM_private = $pempath;
5889 $mail->DKIM_selector = $CFG->emaildkimselector;
5890 $mail->DKIM_identity = $mail->From;
5891 } else {
5892 debugging("Email DKIM selector chosen due to {$mail->From} but no certificate found at $pempath", DEBUG_DEVELOPER);
5896 if ($mail->send()) {
5897 set_send_count($user);
5898 if (!empty($mail->SMTPDebug)) {
5899 echo '</pre>';
5901 return true;
5902 } else {
5903 // Trigger event for failing to send email.
5904 $event = \core\event\email_failed::create(array(
5905 'context' => context_system::instance(),
5906 'userid' => $from->id,
5907 'relateduserid' => $user->id,
5908 'other' => array(
5909 'subject' => $subject,
5910 'message' => $messagetext,
5911 'errorinfo' => $mail->ErrorInfo
5914 $event->trigger();
5915 if (CLI_SCRIPT) {
5916 mtrace('Error: lib/moodlelib.php email_to_user(): '.$mail->ErrorInfo);
5918 if (!empty($mail->SMTPDebug)) {
5919 echo '</pre>';
5921 return false;
5926 * Check to see if a user's real email address should be used for the "From" field.
5928 * @param object $from The user object for the user we are sending the email from.
5929 * @param object $user The user object that we are sending the email to.
5930 * @param array $unused No longer used.
5931 * @return bool Returns true if we can use the from user's email adress in the "From" field.
5933 function can_send_from_real_email_address($from, $user, $unused = null) {
5934 global $CFG;
5935 if (!isset($CFG->allowedemaildomains) || empty(trim($CFG->allowedemaildomains))) {
5936 return false;
5938 $alloweddomains = array_map('trim', explode("\n", $CFG->allowedemaildomains));
5939 // Email is in the list of allowed domains for sending email,
5940 // and the senders email setting is either displayed to everyone, or display to only other users that are enrolled
5941 // in a course with the sender.
5942 if (\core\ip_utils::is_domain_in_allowed_list(substr($from->email, strpos($from->email, '@') + 1), $alloweddomains)
5943 && ($from->maildisplay == core_user::MAILDISPLAY_EVERYONE
5944 || ($from->maildisplay == core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY
5945 && enrol_get_shared_courses($user, $from, false, true)))) {
5946 return true;
5948 return false;
5952 * Generate a signoff for emails based on support settings
5954 * @return string
5956 function generate_email_signoff() {
5957 global $CFG, $OUTPUT;
5959 $signoff = "\n";
5960 if (!empty($CFG->supportname)) {
5961 $signoff .= $CFG->supportname."\n";
5964 $supportemail = $OUTPUT->supportemail(['class' => 'font-weight-bold']);
5966 if ($supportemail) {
5967 $signoff .= "\n" . $supportemail . "\n";
5970 return $signoff;
5974 * Sets specified user's password and send the new password to the user via email.
5976 * @param stdClass $user A {@link $USER} object
5977 * @param bool $fasthash If true, use a low cost factor when generating the hash for speed.
5978 * @return bool|string Returns "true" if mail was sent OK and "false" if there was an error
5980 function setnew_password_and_mail($user, $fasthash = false) {
5981 global $CFG, $DB;
5983 // We try to send the mail in language the user understands,
5984 // unfortunately the filter_string() does not support alternative langs yet
5985 // so multilang will not work properly for site->fullname.
5986 $lang = empty($user->lang) ? get_newuser_language() : $user->lang;
5988 $site = get_site();
5990 $supportuser = core_user::get_support_user();
5992 $newpassword = generate_password();
5994 update_internal_user_password($user, $newpassword, $fasthash);
5996 $a = new stdClass();
5997 $a->firstname = fullname($user, true);
5998 $a->sitename = format_string($site->fullname);
5999 $a->username = $user->username;
6000 $a->newpassword = $newpassword;
6001 $a->link = $CFG->wwwroot .'/login/?lang='.$lang;
6002 $a->signoff = generate_email_signoff();
6004 $message = (string)new lang_string('newusernewpasswordtext', '', $a, $lang);
6006 $subject = format_string($site->fullname) .': '. (string)new lang_string('newusernewpasswordsubj', '', $a, $lang);
6008 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6009 return email_to_user($user, $supportuser, $subject, $message);
6014 * Resets specified user's password and send the new password to the user via email.
6016 * @param stdClass $user A {@link $USER} object
6017 * @return bool Returns true if mail was sent OK and false if there was an error.
6019 function reset_password_and_mail($user) {
6020 global $CFG;
6022 $site = get_site();
6023 $supportuser = core_user::get_support_user();
6025 $userauth = get_auth_plugin($user->auth);
6026 if (!$userauth->can_reset_password() or !is_enabled_auth($user->auth)) {
6027 trigger_error("Attempt to reset user password for user $user->username with Auth $user->auth.");
6028 return false;
6031 $newpassword = generate_password();
6033 if (!$userauth->user_update_password($user, $newpassword)) {
6034 throw new \moodle_exception("cannotsetpassword");
6037 $a = new stdClass();
6038 $a->firstname = $user->firstname;
6039 $a->lastname = $user->lastname;
6040 $a->sitename = format_string($site->fullname);
6041 $a->username = $user->username;
6042 $a->newpassword = $newpassword;
6043 $a->link = $CFG->wwwroot .'/login/change_password.php';
6044 $a->signoff = generate_email_signoff();
6046 $message = get_string('newpasswordtext', '', $a);
6048 $subject = format_string($site->fullname) .': '. get_string('changedpassword');
6050 unset_user_preference('create_password', $user); // Prevent cron from generating the password.
6052 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6053 return email_to_user($user, $supportuser, $subject, $message);
6057 * Send email to specified user with confirmation text and activation link.
6059 * @param stdClass $user A {@link $USER} object
6060 * @param string $confirmationurl user confirmation URL
6061 * @return bool Returns true if mail was sent OK and false if there was an error.
6063 function send_confirmation_email($user, $confirmationurl = null) {
6064 global $CFG;
6066 $site = get_site();
6067 $supportuser = core_user::get_support_user();
6069 $data = new stdClass();
6070 $data->sitename = format_string($site->fullname);
6071 $data->admin = generate_email_signoff();
6073 $subject = get_string('emailconfirmationsubject', '', format_string($site->fullname));
6075 if (empty($confirmationurl)) {
6076 $confirmationurl = '/login/confirm.php';
6079 $confirmationurl = new moodle_url($confirmationurl);
6080 // Remove data parameter just in case it was included in the confirmation so we can add it manually later.
6081 $confirmationurl->remove_params('data');
6082 $confirmationpath = $confirmationurl->out(false);
6084 // We need to custom encode the username to include trailing dots in the link.
6085 // Because of this custom encoding we can't use moodle_url directly.
6086 // Determine if a query string is present in the confirmation url.
6087 $hasquerystring = strpos($confirmationpath, '?') !== false;
6088 // Perform normal url encoding of the username first.
6089 $username = urlencode($user->username);
6090 // Prevent problems with trailing dots not being included as part of link in some mail clients.
6091 $username = str_replace('.', '%2E', $username);
6093 $data->link = $confirmationpath . ( $hasquerystring ? '&' : '?') . 'data='. $user->secret .'/'. $username;
6095 $message = get_string('emailconfirmation', '', $data);
6096 $messagehtml = text_to_html(get_string('emailconfirmation', '', $data), false, false, true);
6098 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6099 return email_to_user($user, $supportuser, $subject, $message, $messagehtml);
6103 * Sends a password change confirmation email.
6105 * @param stdClass $user A {@link $USER} object
6106 * @param stdClass $resetrecord An object tracking metadata regarding password reset request
6107 * @return bool Returns true if mail was sent OK and false if there was an error.
6109 function send_password_change_confirmation_email($user, $resetrecord) {
6110 global $CFG;
6112 $site = get_site();
6113 $supportuser = core_user::get_support_user();
6114 $pwresetmins = isset($CFG->pwresettime) ? floor($CFG->pwresettime / MINSECS) : 30;
6116 $data = new stdClass();
6117 $data->firstname = $user->firstname;
6118 $data->lastname = $user->lastname;
6119 $data->username = $user->username;
6120 $data->sitename = format_string($site->fullname);
6121 $data->link = $CFG->wwwroot .'/login/forgot_password.php?token='. $resetrecord->token;
6122 $data->admin = generate_email_signoff();
6123 $data->resetminutes = $pwresetmins;
6125 $message = get_string('emailresetconfirmation', '', $data);
6126 $subject = get_string('emailresetconfirmationsubject', '', format_string($site->fullname));
6128 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6129 return email_to_user($user, $supportuser, $subject, $message);
6134 * Sends an email containing information on how to change your password.
6136 * @param stdClass $user A {@link $USER} object
6137 * @return bool Returns true if mail was sent OK and false if there was an error.
6139 function send_password_change_info($user) {
6140 $site = get_site();
6141 $supportuser = core_user::get_support_user();
6143 $data = new stdClass();
6144 $data->firstname = $user->firstname;
6145 $data->lastname = $user->lastname;
6146 $data->username = $user->username;
6147 $data->sitename = format_string($site->fullname);
6148 $data->admin = generate_email_signoff();
6150 if (!is_enabled_auth($user->auth)) {
6151 $message = get_string('emailpasswordchangeinfodisabled', '', $data);
6152 $subject = get_string('emailpasswordchangeinfosubject', '', 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);
6157 $userauth = get_auth_plugin($user->auth);
6158 ['subject' => $subject, 'message' => $message] = $userauth->get_password_change_info($user);
6160 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
6161 return email_to_user($user, $supportuser, $subject, $message);
6165 * Check that an email is allowed. It returns an error message if there was a problem.
6167 * @param string $email Content of email
6168 * @return string|false
6170 function email_is_not_allowed($email) {
6171 global $CFG;
6173 // Comparing lowercase domains.
6174 $email = strtolower($email);
6175 if (!empty($CFG->allowemailaddresses)) {
6176 $allowed = explode(' ', strtolower($CFG->allowemailaddresses));
6177 foreach ($allowed as $allowedpattern) {
6178 $allowedpattern = trim($allowedpattern);
6179 if (!$allowedpattern) {
6180 continue;
6182 if (strpos($allowedpattern, '.') === 0) {
6183 if (strpos(strrev($email), strrev($allowedpattern)) === 0) {
6184 // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com".
6185 return false;
6188 } else if (strpos(strrev($email), strrev('@'.$allowedpattern)) === 0) {
6189 return false;
6192 return get_string('emailonlyallowed', '', $CFG->allowemailaddresses);
6194 } else if (!empty($CFG->denyemailaddresses)) {
6195 $denied = explode(' ', strtolower($CFG->denyemailaddresses));
6196 foreach ($denied as $deniedpattern) {
6197 $deniedpattern = trim($deniedpattern);
6198 if (!$deniedpattern) {
6199 continue;
6201 if (strpos($deniedpattern, '.') === 0) {
6202 if (strpos(strrev($email), strrev($deniedpattern)) === 0) {
6203 // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com".
6204 return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
6207 } else if (strpos(strrev($email), strrev('@'.$deniedpattern)) === 0) {
6208 return get_string('emailnotallowed', '', $CFG->denyemailaddresses);
6213 return false;
6216 // FILE HANDLING.
6219 * Returns local file storage instance
6221 * @return file_storage
6223 function get_file_storage($reset = false) {
6224 global $CFG;
6226 static $fs = null;
6228 if ($reset) {
6229 $fs = null;
6230 return;
6233 if ($fs) {
6234 return $fs;
6237 require_once("$CFG->libdir/filelib.php");
6239 $fs = new file_storage();
6241 return $fs;
6245 * Returns local file storage instance
6247 * @return file_browser
6249 function get_file_browser() {
6250 global $CFG;
6252 static $fb = null;
6254 if ($fb) {
6255 return $fb;
6258 require_once("$CFG->libdir/filelib.php");
6260 $fb = new file_browser();
6262 return $fb;
6266 * Returns file packer
6268 * @param string $mimetype default application/zip
6269 * @return file_packer
6271 function get_file_packer($mimetype='application/zip') {
6272 global $CFG;
6274 static $fp = array();
6276 if (isset($fp[$mimetype])) {
6277 return $fp[$mimetype];
6280 switch ($mimetype) {
6281 case 'application/zip':
6282 case 'application/vnd.moodle.profiling':
6283 $classname = 'zip_packer';
6284 break;
6286 case 'application/x-gzip' :
6287 $classname = 'tgz_packer';
6288 break;
6290 case 'application/vnd.moodle.backup':
6291 $classname = 'mbz_packer';
6292 break;
6294 default:
6295 return false;
6298 require_once("$CFG->libdir/filestorage/$classname.php");
6299 $fp[$mimetype] = new $classname();
6301 return $fp[$mimetype];
6305 * Returns current name of file on disk if it exists.
6307 * @param string $newfile File to be verified
6308 * @return string Current name of file on disk if true
6310 function valid_uploaded_file($newfile) {
6311 if (empty($newfile)) {
6312 return '';
6314 if (is_uploaded_file($newfile['tmp_name']) and $newfile['size'] > 0) {
6315 return $newfile['tmp_name'];
6316 } else {
6317 return '';
6322 * Returns the maximum size for uploading files.
6324 * There are seven possible upload limits:
6325 * 1. in Apache using LimitRequestBody (no way of checking or changing this)
6326 * 2. in php.ini for 'upload_max_filesize' (can not be changed inside PHP)
6327 * 3. in .htaccess for 'upload_max_filesize' (can not be changed inside PHP)
6328 * 4. in php.ini for 'post_max_size' (can not be changed inside PHP)
6329 * 5. by the Moodle admin in $CFG->maxbytes
6330 * 6. by the teacher in the current course $course->maxbytes
6331 * 7. by the teacher for the current module, eg $assignment->maxbytes
6333 * These last two are passed to this function as arguments (in bytes).
6334 * Anything defined as 0 is ignored.
6335 * The smallest of all the non-zero numbers is returned.
6337 * @todo Finish documenting this function
6339 * @param int $sitebytes Set maximum size
6340 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6341 * @param int $modulebytes Current module ->maxbytes (in bytes)
6342 * @param bool $unused This parameter has been deprecated and is not used any more.
6343 * @return int The maximum size for uploading files.
6345 function get_max_upload_file_size($sitebytes=0, $coursebytes=0, $modulebytes=0, $unused = false) {
6347 if (! $filesize = ini_get('upload_max_filesize')) {
6348 $filesize = '5M';
6350 $minimumsize = get_real_size($filesize);
6352 if ($postsize = ini_get('post_max_size')) {
6353 $postsize = get_real_size($postsize);
6354 if ($postsize < $minimumsize) {
6355 $minimumsize = $postsize;
6359 if (($sitebytes > 0) and ($sitebytes < $minimumsize)) {
6360 $minimumsize = $sitebytes;
6363 if (($coursebytes > 0) and ($coursebytes < $minimumsize)) {
6364 $minimumsize = $coursebytes;
6367 if (($modulebytes > 0) and ($modulebytes < $minimumsize)) {
6368 $minimumsize = $modulebytes;
6371 return $minimumsize;
6375 * Returns the maximum size for uploading files for the current user
6377 * This function takes in account {@link get_max_upload_file_size()} the user's capabilities
6379 * @param context $context The context in which to check user capabilities
6380 * @param int $sitebytes Set maximum size
6381 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6382 * @param int $modulebytes Current module ->maxbytes (in bytes)
6383 * @param stdClass $user The user
6384 * @param bool $unused This parameter has been deprecated and is not used any more.
6385 * @return int The maximum size for uploading files.
6387 function get_user_max_upload_file_size($context, $sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $user = null,
6388 $unused = false) {
6389 global $USER;
6391 if (empty($user)) {
6392 $user = $USER;
6395 if (has_capability('moodle/course:ignorefilesizelimits', $context, $user)) {
6396 return USER_CAN_IGNORE_FILE_SIZE_LIMITS;
6399 return get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes);
6403 * Returns an array of possible sizes in local language
6405 * Related to {@link get_max_upload_file_size()} - this function returns an
6406 * array of possible sizes in an array, translated to the
6407 * local language.
6409 * The list of options will go up to the minimum of $sitebytes, $coursebytes or $modulebytes.
6411 * If $coursebytes or $sitebytes is not 0, an option will be included for "Course/Site upload limit (X)"
6412 * with the value set to 0. This option will be the first in the list.
6414 * @uses SORT_NUMERIC
6415 * @param int $sitebytes Set maximum size
6416 * @param int $coursebytes Current course $course->maxbytes (in bytes)
6417 * @param int $modulebytes Current module ->maxbytes (in bytes)
6418 * @param int|array $custombytes custom upload size/s which will be added to list,
6419 * Only value/s smaller then maxsize will be added to list.
6420 * @return array
6422 function get_max_upload_sizes($sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $custombytes = null) {
6423 global $CFG;
6425 if (!$maxsize = get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes)) {
6426 return array();
6429 if ($sitebytes == 0) {
6430 // Will get the minimum of upload_max_filesize or post_max_size.
6431 $sitebytes = get_max_upload_file_size();
6434 $filesize = array();
6435 $sizelist = array(10240, 51200, 102400, 512000, 1048576, 2097152,
6436 5242880, 10485760, 20971520, 52428800, 104857600,
6437 262144000, 524288000, 786432000, 1073741824,
6438 2147483648, 4294967296, 8589934592);
6440 // If custombytes is given and is valid then add it to the list.
6441 if (is_number($custombytes) and $custombytes > 0) {
6442 $custombytes = (int)$custombytes;
6443 if (!in_array($custombytes, $sizelist)) {
6444 $sizelist[] = $custombytes;
6446 } else if (is_array($custombytes)) {
6447 $sizelist = array_unique(array_merge($sizelist, $custombytes));
6450 // Allow maxbytes to be selected if it falls outside the above boundaries.
6451 if (isset($CFG->maxbytes) && !in_array(get_real_size($CFG->maxbytes), $sizelist)) {
6452 // Note: get_real_size() is used in order to prevent problems with invalid values.
6453 $sizelist[] = get_real_size($CFG->maxbytes);
6456 foreach ($sizelist as $sizebytes) {
6457 if ($sizebytes < $maxsize && $sizebytes > 0) {
6458 $filesize[(string)intval($sizebytes)] = display_size($sizebytes, 0);
6462 $limitlevel = '';
6463 $displaysize = '';
6464 if ($modulebytes &&
6465 (($modulebytes < $coursebytes || $coursebytes == 0) &&
6466 ($modulebytes < $sitebytes || $sitebytes == 0))) {
6467 $limitlevel = get_string('activity', 'core');
6468 $displaysize = display_size($modulebytes, 0);
6469 $filesize[$modulebytes] = $displaysize; // Make sure the limit is also included in the list.
6471 } else if ($coursebytes && ($coursebytes < $sitebytes || $sitebytes == 0)) {
6472 $limitlevel = get_string('course', 'core');
6473 $displaysize = display_size($coursebytes, 0);
6474 $filesize[$coursebytes] = $displaysize; // Make sure the limit is also included in the list.
6476 } else if ($sitebytes) {
6477 $limitlevel = get_string('site', 'core');
6478 $displaysize = display_size($sitebytes, 0);
6479 $filesize[$sitebytes] = $displaysize; // Make sure the limit is also included in the list.
6482 krsort($filesize, SORT_NUMERIC);
6483 if ($limitlevel) {
6484 $params = (object) array('contextname' => $limitlevel, 'displaysize' => $displaysize);
6485 $filesize = array('0' => get_string('uploadlimitwithsize', 'core', $params)) + $filesize;
6488 return $filesize;
6492 * Returns an array with all the filenames in all subdirectories, relative to the given rootdir.
6494 * If excludefiles is defined, then that file/directory is ignored
6495 * If getdirs is true, then (sub)directories are included in the output
6496 * If getfiles is true, then files are included in the output
6497 * (at least one of these must be true!)
6499 * @todo Finish documenting this function. Add examples of $excludefile usage.
6501 * @param string $rootdir A given root directory to start from
6502 * @param string|array $excludefiles If defined then the specified file/directory is ignored
6503 * @param bool $descend If true then subdirectories are recursed as well
6504 * @param bool $getdirs If true then (sub)directories are included in the output
6505 * @param bool $getfiles If true then files are included in the output
6506 * @return array An array with all the filenames in all subdirectories, relative to the given rootdir
6508 function get_directory_list($rootdir, $excludefiles='', $descend=true, $getdirs=false, $getfiles=true) {
6510 $dirs = array();
6512 if (!$getdirs and !$getfiles) { // Nothing to show.
6513 return $dirs;
6516 if (!is_dir($rootdir)) { // Must be a directory.
6517 return $dirs;
6520 if (!$dir = opendir($rootdir)) { // Can't open it for some reason.
6521 return $dirs;
6524 if (!is_array($excludefiles)) {
6525 $excludefiles = array($excludefiles);
6528 while (false !== ($file = readdir($dir))) {
6529 $firstchar = substr($file, 0, 1);
6530 if ($firstchar == '.' or $file == 'CVS' or in_array($file, $excludefiles)) {
6531 continue;
6533 $fullfile = $rootdir .'/'. $file;
6534 if (filetype($fullfile) == 'dir') {
6535 if ($getdirs) {
6536 $dirs[] = $file;
6538 if ($descend) {
6539 $subdirs = get_directory_list($fullfile, $excludefiles, $descend, $getdirs, $getfiles);
6540 foreach ($subdirs as $subdir) {
6541 $dirs[] = $file .'/'. $subdir;
6544 } else if ($getfiles) {
6545 $dirs[] = $file;
6548 closedir($dir);
6550 asort($dirs);
6552 return $dirs;
6557 * Adds up all the files in a directory and works out the size.
6559 * @param string $rootdir The directory to start from
6560 * @param string $excludefile A file to exclude when summing directory size
6561 * @return int The summed size of all files and subfiles within the root directory
6563 function get_directory_size($rootdir, $excludefile='') {
6564 global $CFG;
6566 // Do it this way if we can, it's much faster.
6567 if (!empty($CFG->pathtodu) && is_executable(trim($CFG->pathtodu))) {
6568 $command = trim($CFG->pathtodu).' -sk '.escapeshellarg($rootdir);
6569 $output = null;
6570 $return = null;
6571 exec($command, $output, $return);
6572 if (is_array($output)) {
6573 // We told it to return k.
6574 return get_real_size(intval($output[0]).'k');
6578 if (!is_dir($rootdir)) {
6579 // Must be a directory.
6580 return 0;
6583 if (!$dir = @opendir($rootdir)) {
6584 // Can't open it for some reason.
6585 return 0;
6588 $size = 0;
6590 while (false !== ($file = readdir($dir))) {
6591 $firstchar = substr($file, 0, 1);
6592 if ($firstchar == '.' or $file == 'CVS' or $file == $excludefile) {
6593 continue;
6595 $fullfile = $rootdir .'/'. $file;
6596 if (filetype($fullfile) == 'dir') {
6597 $size += get_directory_size($fullfile, $excludefile);
6598 } else {
6599 $size += filesize($fullfile);
6602 closedir($dir);
6604 return $size;
6608 * Converts bytes into display form
6610 * @param int $size The size to convert to human readable form
6611 * @param int $decimalplaces If specified, uses fixed number of decimal places
6612 * @param string $fixedunits If specified, uses fixed units (e.g. 'KB')
6613 * @return string Display version of size
6615 function display_size($size, int $decimalplaces = 1, string $fixedunits = ''): string {
6617 static $units;
6619 if ($size === USER_CAN_IGNORE_FILE_SIZE_LIMITS) {
6620 return get_string('unlimited');
6623 if (empty($units)) {
6624 $units[] = get_string('sizeb');
6625 $units[] = get_string('sizekb');
6626 $units[] = get_string('sizemb');
6627 $units[] = get_string('sizegb');
6628 $units[] = get_string('sizetb');
6629 $units[] = get_string('sizepb');
6632 switch ($fixedunits) {
6633 case 'PB' :
6634 $magnitude = 5;
6635 break;
6636 case 'TB' :
6637 $magnitude = 4;
6638 break;
6639 case 'GB' :
6640 $magnitude = 3;
6641 break;
6642 case 'MB' :
6643 $magnitude = 2;
6644 break;
6645 case 'KB' :
6646 $magnitude = 1;
6647 break;
6648 case 'B' :
6649 $magnitude = 0;
6650 break;
6651 case '':
6652 $magnitude = floor(log($size, 1024));
6653 $magnitude = max(0, min(5, $magnitude));
6654 break;
6655 default:
6656 throw new coding_exception('Unknown fixed units value: ' . $fixedunits);
6659 // Special case for magnitude 0 (bytes) - never use decimal places.
6660 $nbsp = "\xc2\xa0";
6661 if ($magnitude === 0) {
6662 return round($size) . $nbsp . $units[$magnitude];
6665 // Convert to specified units.
6666 $sizeinunit = $size / 1024 ** $magnitude;
6668 // Fixed decimal places.
6669 return sprintf('%.' . $decimalplaces . 'f', $sizeinunit) . $nbsp . $units[$magnitude];
6673 * Cleans a given filename by removing suspicious or troublesome characters
6675 * @see clean_param()
6676 * @param string $string file name
6677 * @return string cleaned file name
6679 function clean_filename($string) {
6680 return clean_param($string, PARAM_FILE);
6683 // STRING TRANSLATION.
6686 * Returns the code for the current language
6688 * @category string
6689 * @return string
6691 function current_language() {
6692 global $CFG, $PAGE, $SESSION, $USER;
6694 if (!empty($SESSION->forcelang)) {
6695 // Allows overriding course-forced language (useful for admins to check
6696 // issues in courses whose language they don't understand).
6697 // Also used by some code to temporarily get language-related information in a
6698 // specific language (see force_current_language()).
6699 $return = $SESSION->forcelang;
6701 } else if (!empty($PAGE->cm->lang)) {
6702 // Activity language, if set.
6703 $return = $PAGE->cm->lang;
6705 } else if (!empty($PAGE->course->id) && $PAGE->course->id != SITEID && !empty($PAGE->course->lang)) {
6706 // Course language can override all other settings for this page.
6707 $return = $PAGE->course->lang;
6709 } else if (!empty($SESSION->lang)) {
6710 // Session language can override other settings.
6711 $return = $SESSION->lang;
6713 } else if (!empty($USER->lang)) {
6714 $return = $USER->lang;
6716 } else if (isset($CFG->lang)) {
6717 $return = $CFG->lang;
6719 } else {
6720 $return = 'en';
6723 // Just in case this slipped in from somewhere by accident.
6724 $return = str_replace('_utf8', '', $return);
6726 return $return;
6730 * Fix the current language to the given language code.
6732 * @param string $lang The language code to use.
6733 * @return void
6735 function fix_current_language(string $lang): void {
6736 global $CFG, $COURSE, $SESSION, $USER;
6738 if (!get_string_manager()->translation_exists($lang)) {
6739 throw new coding_exception("The language pack for $lang is not available");
6742 $fixglobal = '';
6743 $fixlang = 'lang';
6744 if (!empty($SESSION->forcelang)) {
6745 $fixglobal = $SESSION;
6746 $fixlang = 'forcelang';
6747 } else if (!empty($COURSE->id) && $COURSE->id != SITEID && !empty($COURSE->lang)) {
6748 $fixglobal = $COURSE;
6749 } else if (!empty($SESSION->lang)) {
6750 $fixglobal = $SESSION;
6751 } else if (!empty($USER->lang)) {
6752 $fixglobal = $USER;
6753 } else if (isset($CFG->lang)) {
6754 set_config('lang', $lang);
6757 if ($fixglobal) {
6758 $fixglobal->$fixlang = $lang;
6763 * Returns parent language of current active language if defined
6765 * @category string
6766 * @param string $lang null means current language
6767 * @return string
6769 function get_parent_language($lang=null) {
6771 $parentlang = get_string_manager()->get_string('parentlanguage', 'langconfig', null, $lang);
6773 if ($parentlang === 'en') {
6774 $parentlang = '';
6777 return $parentlang;
6781 * Force the current language to get strings and dates localised in the given language.
6783 * After calling this function, all strings will be provided in the given language
6784 * until this function is called again, or equivalent code is run.
6786 * @param string $language
6787 * @return string previous $SESSION->forcelang value
6789 function force_current_language($language) {
6790 global $SESSION;
6791 $sessionforcelang = isset($SESSION->forcelang) ? $SESSION->forcelang : '';
6792 if ($language !== $sessionforcelang) {
6793 // Setting forcelang to null or an empty string disables its effect.
6794 if (empty($language) || get_string_manager()->translation_exists($language, false)) {
6795 $SESSION->forcelang = $language;
6796 moodle_setlocale();
6799 return $sessionforcelang;
6803 * Returns current string_manager instance.
6805 * The param $forcereload is needed for CLI installer only where the string_manager instance
6806 * must be replaced during the install.php script life time.
6808 * @category string
6809 * @param bool $forcereload shall the singleton be released and new instance created instead?
6810 * @return core_string_manager
6812 function get_string_manager($forcereload=false) {
6813 global $CFG;
6815 static $singleton = null;
6817 if ($forcereload) {
6818 $singleton = null;
6820 if ($singleton === null) {
6821 if (empty($CFG->early_install_lang)) {
6823 $transaliases = array();
6824 if (empty($CFG->langlist)) {
6825 $translist = array();
6826 } else {
6827 $translist = explode(',', $CFG->langlist);
6828 $translist = array_map('trim', $translist);
6829 // Each language in the $CFG->langlist can has an "alias" that would substitute the default language name.
6830 foreach ($translist as $i => $value) {
6831 $parts = preg_split('/\s*\|\s*/', $value, 2);
6832 if (count($parts) == 2) {
6833 $transaliases[$parts[0]] = $parts[1];
6834 $translist[$i] = $parts[0];
6839 if (!empty($CFG->config_php_settings['customstringmanager'])) {
6840 $classname = $CFG->config_php_settings['customstringmanager'];
6842 if (class_exists($classname)) {
6843 $implements = class_implements($classname);
6845 if (isset($implements['core_string_manager'])) {
6846 $singleton = new $classname($CFG->langotherroot, $CFG->langlocalroot, $translist, $transaliases);
6847 return $singleton;
6849 } else {
6850 debugging('Unable to instantiate custom string manager: class '.$classname.
6851 ' does not implement the core_string_manager interface.');
6854 } else {
6855 debugging('Unable to instantiate custom string manager: class '.$classname.' can not be found.');
6859 $singleton = new core_string_manager_standard($CFG->langotherroot, $CFG->langlocalroot, $translist, $transaliases);
6861 } else {
6862 $singleton = new core_string_manager_install();
6866 return $singleton;
6870 * Returns a localized string.
6872 * Returns the translated string specified by $identifier as
6873 * for $module. Uses the same format files as STphp.
6874 * $a is an object, string or number that can be used
6875 * within translation strings
6877 * eg 'hello {$a->firstname} {$a->lastname}'
6878 * or 'hello {$a}'
6880 * If you would like to directly echo the localized string use
6881 * the function {@link print_string()}
6883 * Example usage of this function involves finding the string you would
6884 * like a local equivalent of and using its identifier and module information
6885 * to retrieve it.<br/>
6886 * If you open moodle/lang/en/moodle.php and look near line 278
6887 * you will find a string to prompt a user for their word for 'course'
6888 * <code>
6889 * $string['course'] = 'Course';
6890 * </code>
6891 * So if you want to display the string 'Course'
6892 * in any language that supports it on your site
6893 * you just need to use the identifier 'course'
6894 * <code>
6895 * $mystring = '<strong>'. get_string('course') .'</strong>';
6896 * or
6897 * </code>
6898 * If the string you want is in another file you'd take a slightly
6899 * different approach. Looking in moodle/lang/en/calendar.php you find
6900 * around line 75:
6901 * <code>
6902 * $string['typecourse'] = 'Course event';
6903 * </code>
6904 * If you want to display the string "Course event" in any language
6905 * supported you would use the identifier 'typecourse' and the module 'calendar'
6906 * (because it is in the file calendar.php):
6907 * <code>
6908 * $mystring = '<h1>'. get_string('typecourse', 'calendar') .'</h1>';
6909 * </code>
6911 * As a last resort, should the identifier fail to map to a string
6912 * the returned string will be [[ $identifier ]]
6914 * In Moodle 2.3 there is a new argument to this function $lazyload.
6915 * Setting $lazyload to true causes get_string to return a lang_string object
6916 * rather than the string itself. The fetching of the string is then put off until
6917 * the string object is first used. The object can be used by calling it's out
6918 * method or by casting the object to a string, either directly e.g.
6919 * (string)$stringobject
6920 * or indirectly by using the string within another string or echoing it out e.g.
6921 * echo $stringobject
6922 * return "<p>{$stringobject}</p>";
6923 * It is worth noting that using $lazyload and attempting to use the string as an
6924 * array key will cause a fatal error as objects cannot be used as array keys.
6925 * But you should never do that anyway!
6926 * For more information {@link lang_string}
6928 * @category string
6929 * @param string $identifier The key identifier for the localized string
6930 * @param string $component The module where the key identifier is stored,
6931 * usually expressed as the filename in the language pack without the
6932 * .php on the end but can also be written as mod/forum or grade/export/xls.
6933 * If none is specified then moodle.php is used.
6934 * @param string|object|array|int $a An object, string or number that can be used
6935 * within translation strings
6936 * @param bool $lazyload If set to true a string object is returned instead of
6937 * the string itself. The string then isn't calculated until it is first used.
6938 * @return string The localized string.
6939 * @throws coding_exception
6941 function get_string($identifier, $component = '', $a = null, $lazyload = false) {
6942 global $CFG;
6944 // If the lazy load argument has been supplied return a lang_string object
6945 // instead.
6946 // We need to make sure it is true (and a bool) as you will see below there
6947 // used to be a forth argument at one point.
6948 if ($lazyload === true) {
6949 return new lang_string($identifier, $component, $a);
6952 if ($CFG->debugdeveloper && clean_param($identifier, PARAM_STRINGID) === '') {
6953 throw new coding_exception('Invalid string identifier. The identifier cannot be empty. Please fix your get_string() call.', DEBUG_DEVELOPER);
6956 // There is now a forth argument again, this time it is a boolean however so
6957 // we can still check for the old extralocations parameter.
6958 if (!is_bool($lazyload) && !empty($lazyload)) {
6959 debugging('extralocations parameter in get_string() is not supported any more, please use standard lang locations only.');
6962 if (strpos((string)$component, '/') !== false) {
6963 debugging('The module name you passed to get_string is the deprecated format ' .
6964 'like mod/mymod or block/myblock. The correct form looks like mymod, or block_myblock.' , DEBUG_DEVELOPER);
6965 $componentpath = explode('/', $component);
6967 switch ($componentpath[0]) {
6968 case 'mod':
6969 $component = $componentpath[1];
6970 break;
6971 case 'blocks':
6972 case 'block':
6973 $component = 'block_'.$componentpath[1];
6974 break;
6975 case 'enrol':
6976 $component = 'enrol_'.$componentpath[1];
6977 break;
6978 case 'format':
6979 $component = 'format_'.$componentpath[1];
6980 break;
6981 case 'grade':
6982 $component = 'grade'.$componentpath[1].'_'.$componentpath[2];
6983 break;
6987 $result = get_string_manager()->get_string($identifier, $component, $a);
6989 // Debugging feature lets you display string identifier and component.
6990 if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
6991 $result .= ' {' . $identifier . '/' . $component . '}';
6993 return $result;
6997 * Converts an array of strings to their localized value.
6999 * @param array $array An array of strings
7000 * @param string $component The language module that these strings can be found in.
7001 * @return stdClass translated strings.
7003 function get_strings($array, $component = '') {
7004 $string = new stdClass;
7005 foreach ($array as $item) {
7006 $string->$item = get_string($item, $component);
7008 return $string;
7012 * Prints out a translated string.
7014 * Prints out a translated string using the return value from the {@link get_string()} function.
7016 * Example usage of this function when the string is in the moodle.php file:<br/>
7017 * <code>
7018 * echo '<strong>';
7019 * print_string('course');
7020 * echo '</strong>';
7021 * </code>
7023 * Example usage of this function when the string is not in the moodle.php file:<br/>
7024 * <code>
7025 * echo '<h1>';
7026 * print_string('typecourse', 'calendar');
7027 * echo '</h1>';
7028 * </code>
7030 * @category string
7031 * @param string $identifier The key identifier for the localized string
7032 * @param string $component The module where the key identifier is stored. If none is specified then moodle.php is used.
7033 * @param string|object|array $a An object, string or number that can be used within translation strings
7035 function print_string($identifier, $component = '', $a = null) {
7036 echo get_string($identifier, $component, $a);
7040 * Returns a list of charset codes
7042 * Returns a list of charset codes. It's hardcoded, so they should be added manually
7043 * (checking that such charset is supported by the texlib library!)
7045 * @return array And associative array with contents in the form of charset => charset
7047 function get_list_of_charsets() {
7049 $charsets = array(
7050 'EUC-JP' => 'EUC-JP',
7051 'ISO-2022-JP'=> 'ISO-2022-JP',
7052 'ISO-8859-1' => 'ISO-8859-1',
7053 'SHIFT-JIS' => 'SHIFT-JIS',
7054 'GB2312' => 'GB2312',
7055 'GB18030' => 'GB18030', // GB18030 not supported by typo and mbstring.
7056 'UTF-8' => 'UTF-8');
7058 asort($charsets);
7060 return $charsets;
7064 * Returns a list of valid and compatible themes
7066 * @return array
7068 function get_list_of_themes() {
7069 global $CFG;
7071 $themes = array();
7073 if (!empty($CFG->themelist)) { // Use admin's list of themes.
7074 $themelist = explode(',', $CFG->themelist);
7075 } else {
7076 $themelist = array_keys(core_component::get_plugin_list("theme"));
7079 foreach ($themelist as $key => $themename) {
7080 $theme = theme_config::load($themename);
7081 $themes[$themename] = $theme;
7084 core_collator::asort_objects_by_method($themes, 'get_theme_name');
7086 return $themes;
7090 * Factory function for emoticon_manager
7092 * @return emoticon_manager singleton
7094 function get_emoticon_manager() {
7095 static $singleton = null;
7097 if (is_null($singleton)) {
7098 $singleton = new emoticon_manager();
7101 return $singleton;
7105 * Provides core support for plugins that have to deal with emoticons (like HTML editor or emoticon filter).
7107 * Whenever this manager mentiones 'emoticon object', the following data
7108 * structure is expected: stdClass with properties text, imagename, imagecomponent,
7109 * altidentifier and altcomponent
7111 * @see admin_setting_emoticons
7113 * @copyright 2010 David Mudrak
7114 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
7116 class emoticon_manager {
7119 * Returns the currently enabled emoticons
7121 * @param boolean $selectable - If true, only return emoticons that should be selectable from a list.
7122 * @return array of emoticon objects
7124 public function get_emoticons($selectable = false) {
7125 global $CFG;
7126 $notselectable = ['martin', 'egg'];
7128 if (empty($CFG->emoticons)) {
7129 return array();
7132 $emoticons = $this->decode_stored_config($CFG->emoticons);
7134 if (!is_array($emoticons)) {
7135 // Something is wrong with the format of stored setting.
7136 debugging('Invalid format of emoticons setting, please resave the emoticons settings form', DEBUG_NORMAL);
7137 return array();
7139 if ($selectable) {
7140 foreach ($emoticons as $index => $emote) {
7141 if (in_array($emote->altidentifier, $notselectable)) {
7142 // Skip this one.
7143 unset($emoticons[$index]);
7148 return $emoticons;
7152 * Converts emoticon object into renderable pix_emoticon object
7154 * @param stdClass $emoticon emoticon object
7155 * @param array $attributes explicit HTML attributes to set
7156 * @return pix_emoticon
7158 public function prepare_renderable_emoticon(stdClass $emoticon, array $attributes = array()) {
7159 $stringmanager = get_string_manager();
7160 if ($stringmanager->string_exists($emoticon->altidentifier, $emoticon->altcomponent)) {
7161 $alt = get_string($emoticon->altidentifier, $emoticon->altcomponent);
7162 } else {
7163 $alt = s($emoticon->text);
7165 return new pix_emoticon($emoticon->imagename, $alt, $emoticon->imagecomponent, $attributes);
7169 * Encodes the array of emoticon objects into a string storable in config table
7171 * @see self::decode_stored_config()
7172 * @param array $emoticons array of emtocion objects
7173 * @return string
7175 public function encode_stored_config(array $emoticons) {
7176 return json_encode($emoticons);
7180 * Decodes the string into an array of emoticon objects
7182 * @see self::encode_stored_config()
7183 * @param string $encoded
7184 * @return array|null
7186 public function decode_stored_config($encoded) {
7187 $decoded = json_decode($encoded);
7188 if (!is_array($decoded)) {
7189 return null;
7191 return $decoded;
7195 * Returns default set of emoticons supported by Moodle
7197 * @return array of sdtClasses
7199 public function default_emoticons() {
7200 return array(
7201 $this->prepare_emoticon_object(":-)", 's/smiley', 'smiley'),
7202 $this->prepare_emoticon_object(":)", 's/smiley', 'smiley'),
7203 $this->prepare_emoticon_object(":-D", 's/biggrin', 'biggrin'),
7204 $this->prepare_emoticon_object(";-)", 's/wink', 'wink'),
7205 $this->prepare_emoticon_object(":-/", 's/mixed', 'mixed'),
7206 $this->prepare_emoticon_object("V-.", 's/thoughtful', 'thoughtful'),
7207 $this->prepare_emoticon_object(":-P", 's/tongueout', 'tongueout'),
7208 $this->prepare_emoticon_object(":-p", 's/tongueout', 'tongueout'),
7209 $this->prepare_emoticon_object("B-)", 's/cool', 'cool'),
7210 $this->prepare_emoticon_object("^-)", 's/approve', 'approve'),
7211 $this->prepare_emoticon_object("8-)", 's/wideeyes', 'wideeyes'),
7212 $this->prepare_emoticon_object(":o)", 's/clown', 'clown'),
7213 $this->prepare_emoticon_object(":-(", 's/sad', 'sad'),
7214 $this->prepare_emoticon_object(":(", 's/sad', 'sad'),
7215 $this->prepare_emoticon_object("8-.", 's/shy', 'shy'),
7216 $this->prepare_emoticon_object(":-I", 's/blush', 'blush'),
7217 $this->prepare_emoticon_object(":-X", 's/kiss', 'kiss'),
7218 $this->prepare_emoticon_object("8-o", 's/surprise', 'surprise'),
7219 $this->prepare_emoticon_object("P-|", 's/blackeye', 'blackeye'),
7220 $this->prepare_emoticon_object("8-[", 's/angry', 'angry'),
7221 $this->prepare_emoticon_object("(grr)", 's/angry', 'angry'),
7222 $this->prepare_emoticon_object("xx-P", 's/dead', 'dead'),
7223 $this->prepare_emoticon_object("|-.", 's/sleepy', 'sleepy'),
7224 $this->prepare_emoticon_object("}-]", 's/evil', 'evil'),
7225 $this->prepare_emoticon_object("(h)", 's/heart', 'heart'),
7226 $this->prepare_emoticon_object("(heart)", 's/heart', 'heart'),
7227 $this->prepare_emoticon_object("(y)", 's/yes', 'yes', 'core'),
7228 $this->prepare_emoticon_object("(n)", 's/no', 'no', 'core'),
7229 $this->prepare_emoticon_object("(martin)", 's/martin', 'martin'),
7230 $this->prepare_emoticon_object("( )", 's/egg', 'egg'),
7235 * Helper method preparing the stdClass with the emoticon properties
7237 * @param string|array $text or array of strings
7238 * @param string $imagename to be used by {@link pix_emoticon}
7239 * @param string $altidentifier alternative string identifier, null for no alt
7240 * @param string $altcomponent where the alternative string is defined
7241 * @param string $imagecomponent to be used by {@link pix_emoticon}
7242 * @return stdClass
7244 protected function prepare_emoticon_object($text, $imagename, $altidentifier = null,
7245 $altcomponent = 'core_pix', $imagecomponent = 'core') {
7246 return (object)array(
7247 'text' => $text,
7248 'imagename' => $imagename,
7249 'imagecomponent' => $imagecomponent,
7250 'altidentifier' => $altidentifier,
7251 'altcomponent' => $altcomponent,
7256 // ENCRYPTION.
7259 * rc4encrypt
7261 * @param string $data Data to encrypt.
7262 * @return string The now encrypted data.
7264 function rc4encrypt($data) {
7265 return endecrypt(get_site_identifier(), $data, '');
7269 * rc4decrypt
7271 * @param string $data Data to decrypt.
7272 * @return string The now decrypted data.
7274 function rc4decrypt($data) {
7275 return endecrypt(get_site_identifier(), $data, 'de');
7279 * Based on a class by Mukul Sabharwal [mukulsabharwal @ yahoo.com]
7281 * @todo Finish documenting this function
7283 * @param string $pwd The password to use when encrypting or decrypting
7284 * @param string $data The data to be decrypted/encrypted
7285 * @param string $case Either 'de' for decrypt or '' for encrypt
7286 * @return string
7288 function endecrypt ($pwd, $data, $case) {
7290 if ($case == 'de') {
7291 $data = urldecode($data);
7294 $key[] = '';
7295 $box[] = '';
7296 $pwdlength = strlen($pwd);
7298 for ($i = 0; $i <= 255; $i++) {
7299 $key[$i] = ord(substr($pwd, ($i % $pwdlength), 1));
7300 $box[$i] = $i;
7303 $x = 0;
7305 for ($i = 0; $i <= 255; $i++) {
7306 $x = ($x + $box[$i] + $key[$i]) % 256;
7307 $tempswap = $box[$i];
7308 $box[$i] = $box[$x];
7309 $box[$x] = $tempswap;
7312 $cipher = '';
7314 $a = 0;
7315 $j = 0;
7317 for ($i = 0; $i < strlen($data); $i++) {
7318 $a = ($a + 1) % 256;
7319 $j = ($j + $box[$a]) % 256;
7320 $temp = $box[$a];
7321 $box[$a] = $box[$j];
7322 $box[$j] = $temp;
7323 $k = $box[(($box[$a] + $box[$j]) % 256)];
7324 $cipherby = ord(substr($data, $i, 1)) ^ $k;
7325 $cipher .= chr($cipherby);
7328 if ($case == 'de') {
7329 $cipher = urldecode(urlencode($cipher));
7330 } else {
7331 $cipher = urlencode($cipher);
7334 return $cipher;
7337 // ENVIRONMENT CHECKING.
7340 * This method validates a plug name. It is much faster than calling clean_param.
7342 * @param string $name a string that might be a plugin name.
7343 * @return bool if this string is a valid plugin name.
7345 function is_valid_plugin_name($name) {
7346 // This does not work for 'mod', bad luck, use any other type.
7347 return core_component::is_valid_plugin_name('tool', $name);
7351 * Get a list of all the plugins of a given type that define a certain API function
7352 * in a certain file. The plugin component names and function names are returned.
7354 * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
7355 * @param string $function the part of the name of the function after the
7356 * frankenstyle prefix. e.g 'hook' if you are looking for functions with
7357 * names like report_courselist_hook.
7358 * @param string $file the name of file within the plugin that defines the
7359 * function. Defaults to lib.php.
7360 * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
7361 * and the function names as values (e.g. 'report_courselist_hook', 'forum_hook').
7363 function get_plugin_list_with_function($plugintype, $function, $file = 'lib.php') {
7364 global $CFG;
7366 // We don't include here as all plugin types files would be included.
7367 $plugins = get_plugins_with_function($function, $file, false);
7369 if (empty($plugins[$plugintype])) {
7370 return array();
7373 $allplugins = core_component::get_plugin_list($plugintype);
7375 // Reformat the array and include the files.
7376 $pluginfunctions = array();
7377 foreach ($plugins[$plugintype] as $pluginname => $functionname) {
7379 // Check that it has not been removed and the file is still available.
7380 if (!empty($allplugins[$pluginname])) {
7382 $filepath = $allplugins[$pluginname] . DIRECTORY_SEPARATOR . $file;
7383 if (file_exists($filepath)) {
7384 include_once($filepath);
7386 // Now that the file is loaded, we must verify the function still exists.
7387 if (function_exists($functionname)) {
7388 $pluginfunctions[$plugintype . '_' . $pluginname] = $functionname;
7389 } else {
7390 // Invalidate the cache for next run.
7391 \cache_helper::invalidate_by_definition('core', 'plugin_functions');
7397 return $pluginfunctions;
7401 * Get a list of all the plugins that define a certain API function in a certain file.
7403 * @param string $function the part of the name of the function after the
7404 * frankenstyle prefix. e.g 'hook' if you are looking for functions with
7405 * names like report_courselist_hook.
7406 * @param string $file the name of file within the plugin that defines the
7407 * function. Defaults to lib.php.
7408 * @param bool $include Whether to include the files that contain the functions or not.
7409 * @param bool $migratedtohook if true this is a deprecated lib.php callback, if hook callback is present then do nothing
7410 * @return array with [plugintype][plugin] = functionname
7412 function get_plugins_with_function($function, $file = 'lib.php', $include = true, bool $migratedtohook = false) {
7413 global $CFG;
7415 if (during_initial_install() || isset($CFG->upgraderunning)) {
7416 // API functions _must not_ be called during an installation or upgrade.
7417 return [];
7420 $plugincallback = $function;
7421 $filtermigrated = function($plugincallback, $pluginfunctions): array {
7422 foreach ($pluginfunctions as $plugintype => $plugins) {
7423 foreach ($plugins as $plugin => $unusedfunction) {
7424 $component = $plugintype . '_' . $plugin;
7425 if (\core\hook\manager::get_instance()->is_deprecated_plugin_callback($plugincallback)) {
7426 if (\core\hook\manager::get_instance()->is_deprecating_hook_present($component, $plugincallback)) {
7427 // Ignore the old callback, it is there only for older Moodle versions.
7428 unset($pluginfunctions[$plugintype][$plugin]);
7429 } else {
7430 debugging("Callback $plugincallback in $component component should be migrated to new hook callback",
7431 DEBUG_DEVELOPER);
7436 return $pluginfunctions;
7439 $cache = \cache::make('core', 'plugin_functions');
7441 // Including both although I doubt that we will find two functions definitions with the same name.
7442 // Clean the filename as cache_helper::hash_key only allows a-zA-Z0-9_.
7443 $pluginfunctions = false;
7444 if (!empty($CFG->allversionshash)) {
7445 $key = $CFG->allversionshash . '_' . $function . '_' . clean_param($file, PARAM_ALPHA);
7446 $pluginfunctions = $cache->get($key);
7448 $dirty = false;
7450 // Use the plugin manager to check that plugins are currently installed.
7451 $pluginmanager = \core_plugin_manager::instance();
7453 if ($pluginfunctions !== false) {
7455 // Checking that the files are still available.
7456 foreach ($pluginfunctions as $plugintype => $plugins) {
7458 $allplugins = \core_component::get_plugin_list($plugintype);
7459 $installedplugins = $pluginmanager->get_installed_plugins($plugintype);
7460 foreach ($plugins as $plugin => $function) {
7461 if (!isset($installedplugins[$plugin])) {
7462 // Plugin code is still present on disk but it is not installed.
7463 $dirty = true;
7464 break 2;
7467 // Cache might be out of sync with the codebase, skip the plugin if it is not available.
7468 if (empty($allplugins[$plugin])) {
7469 $dirty = true;
7470 break 2;
7473 $fileexists = file_exists($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file);
7474 if ($include && $fileexists) {
7475 // Include the files if it was requested.
7476 include_once($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file);
7477 } else if (!$fileexists) {
7478 // If the file is not available any more it should not be returned.
7479 $dirty = true;
7480 break 2;
7483 // Check if the function still exists in the file.
7484 if ($include && !function_exists($function)) {
7485 $dirty = true;
7486 break 2;
7491 // If the cache is dirty, we should fall through and let it rebuild.
7492 if (!$dirty) {
7493 if ($migratedtohook && $file === 'lib.php') {
7494 $pluginfunctions = $filtermigrated($plugincallback, $pluginfunctions);
7496 return $pluginfunctions;
7500 $pluginfunctions = array();
7502 // To fill the cached. Also, everything should continue working with cache disabled.
7503 $plugintypes = \core_component::get_plugin_types();
7504 foreach ($plugintypes as $plugintype => $unused) {
7506 // We need to include files here.
7507 $pluginswithfile = \core_component::get_plugin_list_with_file($plugintype, $file, true);
7508 $installedplugins = $pluginmanager->get_installed_plugins($plugintype);
7509 foreach ($pluginswithfile as $plugin => $notused) {
7511 if (!isset($installedplugins[$plugin])) {
7512 continue;
7515 $fullfunction = $plugintype . '_' . $plugin . '_' . $function;
7517 $pluginfunction = false;
7518 if (function_exists($fullfunction)) {
7519 // Function exists with standard name. Store, indexed by frankenstyle name of plugin.
7520 $pluginfunction = $fullfunction;
7522 } else if ($plugintype === 'mod') {
7523 // For modules, we also allow plugin without full frankenstyle but just starting with the module name.
7524 $shortfunction = $plugin . '_' . $function;
7525 if (function_exists($shortfunction)) {
7526 $pluginfunction = $shortfunction;
7530 if ($pluginfunction) {
7531 if (empty($pluginfunctions[$plugintype])) {
7532 $pluginfunctions[$plugintype] = array();
7534 $pluginfunctions[$plugintype][$plugin] = $pluginfunction;
7539 if (!empty($CFG->allversionshash)) {
7540 $cache->set($key, $pluginfunctions);
7543 if ($migratedtohook && $file === 'lib.php') {
7544 $pluginfunctions = $filtermigrated($plugincallback, $pluginfunctions);
7547 return $pluginfunctions;
7552 * Lists plugin-like directories within specified directory
7554 * This function was originally used for standard Moodle plugins, please use
7555 * new core_component::get_plugin_list() now.
7557 * This function is used for general directory listing and backwards compatility.
7559 * @param string $directory relative directory from root
7560 * @param string $exclude dir name to exclude from the list (defaults to none)
7561 * @param string $basedir full path to the base dir where $plugin resides (defaults to $CFG->dirroot)
7562 * @return array Sorted array of directory names found under the requested parameters
7564 function get_list_of_plugins($directory='mod', $exclude='', $basedir='') {
7565 global $CFG;
7567 $plugins = array();
7569 if (empty($basedir)) {
7570 $basedir = $CFG->dirroot .'/'. $directory;
7572 } else {
7573 $basedir = $basedir .'/'. $directory;
7576 if ($CFG->debugdeveloper and empty($exclude)) {
7577 // Make sure devs do not use this to list normal plugins,
7578 // this is intended for general directories that are not plugins!
7580 $subtypes = core_component::get_plugin_types();
7581 if (in_array($basedir, $subtypes)) {
7582 debugging('get_list_of_plugins() should not be used to list real plugins, use core_component::get_plugin_list() instead!', DEBUG_DEVELOPER);
7584 unset($subtypes);
7587 $ignorelist = array_flip(array_filter([
7588 'CVS',
7589 '_vti_cnf',
7590 'amd',
7591 'classes',
7592 'simpletest',
7593 'tests',
7594 'templates',
7595 'yui',
7596 $exclude,
7597 ]));
7599 if (file_exists($basedir) && filetype($basedir) == 'dir') {
7600 if (!$dirhandle = opendir($basedir)) {
7601 debugging("Directory permission error for plugin ({$directory}). Directory exists but cannot be read.", DEBUG_DEVELOPER);
7602 return array();
7604 while (false !== ($dir = readdir($dirhandle))) {
7605 if (strpos($dir, '.') === 0) {
7606 // Ignore directories starting with .
7607 // These are treated as hidden directories.
7608 continue;
7610 if (array_key_exists($dir, $ignorelist)) {
7611 // This directory features on the ignore list.
7612 continue;
7614 if (filetype($basedir .'/'. $dir) != 'dir') {
7615 continue;
7617 $plugins[] = $dir;
7619 closedir($dirhandle);
7621 if ($plugins) {
7622 asort($plugins);
7624 return $plugins;
7628 * Invoke plugin's callback functions
7630 * @param string $type plugin type e.g. 'mod'
7631 * @param string $name plugin name
7632 * @param string $feature feature name
7633 * @param string $action feature's action
7634 * @param array $params parameters of callback function, should be an array
7635 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
7636 * @param bool $migratedtohook if true this is a deprecated callback, if hook callback is present then do nothing
7637 * @return mixed
7639 * @todo Decide about to deprecate and drop plugin_callback() - MDL-30743
7641 function plugin_callback($type, $name, $feature, $action, $params = null, $default = null, bool $migratedtohook = false) {
7642 return component_callback($type . '_' . $name, $feature . '_' . $action, (array) $params, $default, $migratedtohook);
7646 * Invoke component's callback functions
7648 * @param string $component frankenstyle component name, e.g. 'mod_quiz'
7649 * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
7650 * @param array $params parameters of callback function
7651 * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
7652 * @param bool $migratedtohook if true this is a deprecated callback, if hook callback is present then do nothing
7653 * @return mixed
7655 function component_callback($component, $function, array $params = array(), $default = null, bool $migratedtohook = false) {
7656 $functionname = component_callback_exists($component, $function);
7658 if ($functionname) {
7659 if ($migratedtohook) {
7660 if (\core\hook\manager::get_instance()->is_deprecated_plugin_callback($function)) {
7661 if (\core\hook\manager::get_instance()->is_deprecating_hook_present($component, $function)) {
7662 // Do not call the old lib.php callback,
7663 // it is there for compatibility with older Moodle versions only.
7664 return null;
7665 } else {
7666 debugging("Callback $function in $component component should be migrated to new hook callback",
7667 DEBUG_DEVELOPER);
7672 // Function exists, so just return function result.
7673 $ret = call_user_func_array($functionname, $params);
7674 if (is_null($ret)) {
7675 return $default;
7676 } else {
7677 return $ret;
7680 return $default;
7684 * Determine if a component callback exists and return the function name to call. Note that this
7685 * function will include the required library files so that the functioname returned can be
7686 * called directly.
7688 * @param string $component frankenstyle component name, e.g. 'mod_quiz'
7689 * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron'
7690 * @return mixed Complete function name to call if the callback exists or false if it doesn't.
7691 * @throws coding_exception if invalid component specfied
7693 function component_callback_exists($component, $function) {
7694 global $CFG; // This is needed for the inclusions.
7696 $cleancomponent = clean_param($component, PARAM_COMPONENT);
7697 if (empty($cleancomponent)) {
7698 throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
7700 $component = $cleancomponent;
7702 list($type, $name) = core_component::normalize_component($component);
7703 $component = $type . '_' . $name;
7705 $oldfunction = $name.'_'.$function;
7706 $function = $component.'_'.$function;
7708 $dir = core_component::get_component_directory($component);
7709 if (empty($dir)) {
7710 throw new coding_exception('Invalid component used in plugin/component_callback():' . $component);
7713 // Load library and look for function.
7714 if (file_exists($dir.'/lib.php')) {
7715 require_once($dir.'/lib.php');
7718 if (!function_exists($function) and function_exists($oldfunction)) {
7719 if ($type !== 'mod' and $type !== 'core') {
7720 debugging("Please use new function name $function instead of legacy $oldfunction", DEBUG_DEVELOPER);
7722 $function = $oldfunction;
7725 if (function_exists($function)) {
7726 return $function;
7728 return false;
7732 * Call the specified callback method on the provided class.
7734 * If the callback returns null, then the default value is returned instead.
7735 * If the class does not exist, then the default value is returned.
7737 * @param string $classname The name of the class to call upon.
7738 * @param string $methodname The name of the staticically defined method on the class.
7739 * @param array $params The arguments to pass into the method.
7740 * @param mixed $default The default value.
7741 * @return mixed The return value.
7743 function component_class_callback($classname, $methodname, array $params, $default = null) {
7744 if (!class_exists($classname)) {
7745 return $default;
7748 if (!method_exists($classname, $methodname)) {
7749 return $default;
7752 $fullfunction = $classname . '::' . $methodname;
7753 $result = call_user_func_array($fullfunction, $params);
7755 if (null === $result) {
7756 return $default;
7757 } else {
7758 return $result;
7763 * Checks whether a plugin supports a specified feature.
7765 * @param string $type Plugin type e.g. 'mod'
7766 * @param string $name Plugin name e.g. 'forum'
7767 * @param string $feature Feature code (FEATURE_xx constant)
7768 * @param mixed $default default value if feature support unknown
7769 * @return mixed Feature result (false if not supported, null if feature is unknown,
7770 * otherwise usually true but may have other feature-specific value such as array)
7771 * @throws coding_exception
7773 function plugin_supports($type, $name, $feature, $default = null) {
7774 global $CFG;
7776 if ($type === 'mod' and $name === 'NEWMODULE') {
7777 // Somebody forgot to rename the module template.
7778 return false;
7781 $component = clean_param($type . '_' . $name, PARAM_COMPONENT);
7782 if (empty($component)) {
7783 throw new coding_exception('Invalid component used in plugin_supports():' . $type . '_' . $name);
7786 $function = null;
7788 if ($type === 'mod') {
7789 // We need this special case because we support subplugins in modules,
7790 // otherwise it would end up in infinite loop.
7791 if (file_exists("$CFG->dirroot/mod/$name/lib.php")) {
7792 include_once("$CFG->dirroot/mod/$name/lib.php");
7793 $function = $component.'_supports';
7794 if (!function_exists($function)) {
7795 // Legacy non-frankenstyle function name.
7796 $function = $name.'_supports';
7800 } else {
7801 if (!$path = core_component::get_plugin_directory($type, $name)) {
7802 // Non existent plugin type.
7803 return false;
7805 if (file_exists("$path/lib.php")) {
7806 include_once("$path/lib.php");
7807 $function = $component.'_supports';
7811 if ($function and function_exists($function)) {
7812 $supports = $function($feature);
7813 if (is_null($supports)) {
7814 // Plugin does not know - use default.
7815 return $default;
7816 } else {
7817 return $supports;
7821 // Plugin does not care, so use default.
7822 return $default;
7826 * Returns true if the current version of PHP is greater that the specified one.
7828 * @todo Check PHP version being required here is it too low?
7830 * @param string $version The version of php being tested.
7831 * @return bool
7833 function check_php_version($version='5.2.4') {
7834 return (version_compare(phpversion(), $version) >= 0);
7838 * Determine if moodle installation requires update.
7840 * Checks version numbers of main code and all plugins to see
7841 * if there are any mismatches.
7843 * @param bool $checkupgradeflag check the outagelessupgrade flag to see if an upgrade is running.
7844 * @return bool
7846 function moodle_needs_upgrading($checkupgradeflag = true) {
7847 global $CFG, $DB;
7849 // Say no if there is already an upgrade running.
7850 if ($checkupgradeflag) {
7851 $lock = $DB->get_field('config', 'value', ['name' => 'outagelessupgrade']);
7852 $currentprocessrunningupgrade = (defined('CLI_UPGRADE_RUNNING') && CLI_UPGRADE_RUNNING);
7853 // If we ARE locked, but this PHP process is NOT the process running the upgrade,
7854 // We should always return false.
7855 // This means the upgrade is running from CLI somewhere, or about to.
7856 if (!empty($lock) && !$currentprocessrunningupgrade) {
7857 return false;
7861 if (empty($CFG->version)) {
7862 return true;
7865 // There is no need to purge plugininfo caches here because
7866 // these caches are not used during upgrade and they are purged after
7867 // every upgrade.
7869 if (empty($CFG->allversionshash)) {
7870 return true;
7873 $hash = core_component::get_all_versions_hash();
7875 return ($hash !== $CFG->allversionshash);
7879 * Returns the major version of this site
7881 * Moodle version numbers consist of three numbers separated by a dot, for
7882 * example 1.9.11 or 2.0.2. The first two numbers, like 1.9 or 2.0, represent so
7883 * called major version. This function extracts the major version from either
7884 * $CFG->release (default) or eventually from the $release variable defined in
7885 * the main version.php.
7887 * @param bool $fromdisk should the version if source code files be used
7888 * @return string|false the major version like '2.3', false if could not be determined
7890 function moodle_major_version($fromdisk = false) {
7891 global $CFG;
7893 if ($fromdisk) {
7894 $release = null;
7895 require($CFG->dirroot.'/version.php');
7896 if (empty($release)) {
7897 return false;
7900 } else {
7901 if (empty($CFG->release)) {
7902 return false;
7904 $release = $CFG->release;
7907 if (preg_match('/^[0-9]+\.[0-9]+/', $release, $matches)) {
7908 return $matches[0];
7909 } else {
7910 return false;
7914 // MISCELLANEOUS.
7917 * Gets the system locale
7919 * @return string Retuns the current locale.
7921 function moodle_getlocale() {
7922 global $CFG;
7924 // Fetch the correct locale based on ostype.
7925 if ($CFG->ostype == 'WINDOWS') {
7926 $stringtofetch = 'localewin';
7927 } else {
7928 $stringtofetch = 'locale';
7931 if (!empty($CFG->locale)) { // Override locale for all language packs.
7932 return $CFG->locale;
7935 return get_string($stringtofetch, 'langconfig');
7939 * Sets the system locale
7941 * @category string
7942 * @param string $locale Can be used to force a locale
7944 function moodle_setlocale($locale='') {
7945 global $CFG;
7947 static $currentlocale = ''; // Last locale caching.
7949 $oldlocale = $currentlocale;
7951 // The priority is the same as in get_string() - parameter, config, course, session, user, global language.
7952 if (!empty($locale)) {
7953 $currentlocale = $locale;
7954 } else {
7955 $currentlocale = moodle_getlocale();
7958 // Do nothing if locale already set up.
7959 if ($oldlocale == $currentlocale) {
7960 return;
7963 // Due to some strange BUG we cannot set the LC_TIME directly, so we fetch current values,
7964 // set LC_ALL and then set values again. Just wondering why we cannot set LC_ALL only??? - stronk7
7965 // Some day, numeric, monetary and other categories should be set too, I think. :-/.
7967 // Get current values.
7968 $monetary= setlocale (LC_MONETARY, 0);
7969 $numeric = setlocale (LC_NUMERIC, 0);
7970 $ctype = setlocale (LC_CTYPE, 0);
7971 if ($CFG->ostype != 'WINDOWS') {
7972 $messages= setlocale (LC_MESSAGES, 0);
7974 // Set locale to all.
7975 $result = setlocale (LC_ALL, $currentlocale);
7976 // If setting of locale fails try the other utf8 or utf-8 variant,
7977 // some operating systems support both (Debian), others just one (OSX).
7978 if ($result === false) {
7979 if (stripos($currentlocale, '.UTF-8') !== false) {
7980 $newlocale = str_ireplace('.UTF-8', '.UTF8', $currentlocale);
7981 setlocale (LC_ALL, $newlocale);
7982 } else if (stripos($currentlocale, '.UTF8') !== false) {
7983 $newlocale = str_ireplace('.UTF8', '.UTF-8', $currentlocale);
7984 setlocale (LC_ALL, $newlocale);
7987 // Set old values.
7988 setlocale (LC_MONETARY, $monetary);
7989 setlocale (LC_NUMERIC, $numeric);
7990 if ($CFG->ostype != 'WINDOWS') {
7991 setlocale (LC_MESSAGES, $messages);
7993 if ($currentlocale == 'tr_TR' or $currentlocale == 'tr_TR.UTF-8') {
7994 // To workaround a well-known PHP problem with Turkish letter Ii.
7995 setlocale (LC_CTYPE, $ctype);
8000 * Count words in a string.
8002 * Words are defined as things between whitespace.
8004 * @category string
8005 * @param string $string The text to be searched for words. May be HTML.
8006 * @param int|null $format
8007 * @return int The count of words in the specified string
8009 function count_words($string, $format = null) {
8010 // Before stripping tags, add a space after the close tag of anything that is not obviously inline.
8011 // Also, br is a special case because it definitely delimits a word, but has no close tag.
8012 $string = preg_replace('~
8013 ( # Capture the tag we match.
8014 </ # Start of close tag.
8015 (?! # Do not match any of these specific close tag names.
8016 a> | b> | del> | em> | i> |
8017 ins> | s> | small> | span> |
8018 strong> | sub> | sup> | u>
8020 \w+ # But, apart from those execptions, match any tag name.
8021 > # End of close tag.
8023 <br> | <br\s*/> # Special cases that are not close tags.
8025 ~x', '$1 ', $string); // Add a space after the close tag.
8026 if ($format !== null && $format != FORMAT_PLAIN) {
8027 // Match the usual text cleaning before display.
8028 // Ideally we should apply multilang filter only here, other filters might add extra text.
8029 $string = format_text($string, $format, ['filter' => false, 'noclean' => false, 'para' => false]);
8031 // Now remove HTML tags.
8032 $string = strip_tags($string);
8033 // Decode HTML entities.
8034 $string = html_entity_decode($string, ENT_COMPAT);
8036 // Now, the word count is the number of blocks of characters separated
8037 // by any sort of space. That seems to be the definition used by all other systems.
8038 // To be precise about what is considered to separate words:
8039 // * Anything that Unicode considers a 'Separator'
8040 // * Anything that Unicode considers a 'Control character'
8041 // * An em- or en- dash.
8042 return count(preg_split('~[\p{Z}\p{Cc}—–]+~u', $string, -1, PREG_SPLIT_NO_EMPTY));
8046 * Count letters in a string.
8048 * Letters are defined as chars not in tags and different from whitespace.
8050 * @category string
8051 * @param string $string The text to be searched for letters. May be HTML.
8052 * @param int|null $format
8053 * @return int The count of letters in the specified text.
8055 function count_letters($string, $format = null) {
8056 if ($format !== null && $format != FORMAT_PLAIN) {
8057 // Match the usual text cleaning before display.
8058 // Ideally we should apply multilang filter only here, other filters might add extra text.
8059 $string = format_text($string, $format, ['filter' => false, 'noclean' => false, 'para' => false]);
8061 $string = strip_tags($string); // Tags are out now.
8062 $string = html_entity_decode($string, ENT_COMPAT);
8063 $string = preg_replace('/[[:space:]]*/', '', $string); // Whitespace are out now.
8065 return core_text::strlen($string);
8069 * Generate and return a random string of the specified length.
8071 * @param int $length The length of the string to be created.
8072 * @return string
8074 function random_string($length=15) {
8075 $randombytes = random_bytes($length);
8076 $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
8077 $pool .= 'abcdefghijklmnopqrstuvwxyz';
8078 $pool .= '0123456789';
8079 $poollen = strlen($pool);
8080 $string = '';
8081 for ($i = 0; $i < $length; $i++) {
8082 $rand = ord($randombytes[$i]);
8083 $string .= substr($pool, ($rand%($poollen)), 1);
8085 return $string;
8089 * Generate a complex random string (useful for md5 salts)
8091 * This function is based on the above {@link random_string()} however it uses a
8092 * larger pool of characters and generates a string between 24 and 32 characters
8094 * @param int $length Optional if set generates a string to exactly this length
8095 * @return string
8097 function complex_random_string($length=null) {
8098 $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
8099 $pool .= '`~!@#%^&*()_+-=[];,./<>?:{} ';
8100 $poollen = strlen($pool);
8101 if ($length===null) {
8102 $length = floor(rand(24, 32));
8104 $randombytes = random_bytes($length);
8105 $string = '';
8106 for ($i = 0; $i < $length; $i++) {
8107 $rand = ord($randombytes[$i]);
8108 $string .= $pool[($rand%$poollen)];
8110 return $string;
8114 * Given some text (which may contain HTML) and an ideal length,
8115 * this function truncates the text neatly on a word boundary if possible
8117 * @category string
8118 * @param string $text text to be shortened
8119 * @param int $ideal ideal string length
8120 * @param boolean $exact if false, $text will not be cut mid-word
8121 * @param string $ending The string to append if the passed string is truncated
8122 * @return string $truncate shortened string
8124 function shorten_text($text, $ideal=30, $exact = false, $ending='...') {
8125 // If the plain text is shorter than the maximum length, return the whole text.
8126 if (core_text::strlen(preg_replace('/<.*?>/', '', $text)) <= $ideal) {
8127 return $text;
8130 // Splits on HTML tags. Each open/close/empty tag will be the first thing
8131 // and only tag in its 'line'.
8132 preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER);
8134 $totallength = core_text::strlen($ending);
8135 $truncate = '';
8137 // This array stores information about open and close tags and their position
8138 // in the truncated string. Each item in the array is an object with fields
8139 // ->open (true if open), ->tag (tag name in lower case), and ->pos
8140 // (byte position in truncated text).
8141 $tagdetails = array();
8143 foreach ($lines as $linematchings) {
8144 // If there is any html-tag in this line, handle it and add it (uncounted) to the output.
8145 if (!empty($linematchings[1])) {
8146 // If it's an "empty element" with or without xhtml-conform closing slash (f.e. <br/>).
8147 if (!preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $linematchings[1])) {
8148 if (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $linematchings[1], $tagmatchings)) {
8149 // Record closing tag.
8150 $tagdetails[] = (object) array(
8151 'open' => false,
8152 'tag' => core_text::strtolower($tagmatchings[1]),
8153 'pos' => core_text::strlen($truncate),
8156 } else if (preg_match('/^<\s*([^\s>!]+).*?>$/s', $linematchings[1], $tagmatchings)) {
8157 // Record opening tag.
8158 $tagdetails[] = (object) array(
8159 'open' => true,
8160 'tag' => core_text::strtolower($tagmatchings[1]),
8161 'pos' => core_text::strlen($truncate),
8163 } else if (preg_match('/^<!--\[if\s.*?\]>$/s', $linematchings[1], $tagmatchings)) {
8164 $tagdetails[] = (object) array(
8165 'open' => true,
8166 'tag' => core_text::strtolower('if'),
8167 'pos' => core_text::strlen($truncate),
8169 } else if (preg_match('/^<!--<!\[endif\]-->$/s', $linematchings[1], $tagmatchings)) {
8170 $tagdetails[] = (object) array(
8171 'open' => false,
8172 'tag' => core_text::strtolower('if'),
8173 'pos' => core_text::strlen($truncate),
8177 // Add html-tag to $truncate'd text.
8178 $truncate .= $linematchings[1];
8181 // Calculate the length of the plain text part of the line; handle entities as one character.
8182 $contentlength = core_text::strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', ' ', $linematchings[2]));
8183 if ($totallength + $contentlength > $ideal) {
8184 // The number of characters which are left.
8185 $left = $ideal - $totallength;
8186 $entitieslength = 0;
8187 // Search for html entities.
8188 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)) {
8189 // Calculate the real length of all entities in the legal range.
8190 foreach ($entities[0] as $entity) {
8191 if ($entity[1]+1-$entitieslength <= $left) {
8192 $left--;
8193 $entitieslength += core_text::strlen($entity[0]);
8194 } else {
8195 // No more characters left.
8196 break;
8200 $breakpos = $left + $entitieslength;
8202 // If the words shouldn't be cut in the middle...
8203 if (!$exact) {
8204 // Search the last occurence of a space.
8205 for (; $breakpos > 0; $breakpos--) {
8206 if ($char = core_text::substr($linematchings[2], $breakpos, 1)) {
8207 if ($char === '.' or $char === ' ') {
8208 $breakpos += 1;
8209 break;
8210 } else if (strlen($char) > 2) {
8211 // Chinese/Japanese/Korean text can be truncated at any UTF-8 character boundary.
8212 $breakpos += 1;
8213 break;
8218 if ($breakpos == 0) {
8219 // This deals with the test_shorten_text_no_spaces case.
8220 $breakpos = $left + $entitieslength;
8221 } else if ($breakpos > $left + $entitieslength) {
8222 // This deals with the previous for loop breaking on the first char.
8223 $breakpos = $left + $entitieslength;
8226 $truncate .= core_text::substr($linematchings[2], 0, $breakpos);
8227 // Maximum length is reached, so get off the loop.
8228 break;
8229 } else {
8230 $truncate .= $linematchings[2];
8231 $totallength += $contentlength;
8234 // If the maximum length is reached, get off the loop.
8235 if ($totallength >= $ideal) {
8236 break;
8240 // Add the defined ending to the text.
8241 $truncate .= $ending;
8243 // Now calculate the list of open html tags based on the truncate position.
8244 $opentags = array();
8245 foreach ($tagdetails as $taginfo) {
8246 if ($taginfo->open) {
8247 // Add tag to the beginning of $opentags list.
8248 array_unshift($opentags, $taginfo->tag);
8249 } else {
8250 // Can have multiple exact same open tags, close the last one.
8251 $pos = array_search($taginfo->tag, array_reverse($opentags, true));
8252 if ($pos !== false) {
8253 unset($opentags[$pos]);
8258 // Close all unclosed html-tags.
8259 foreach ($opentags as $tag) {
8260 if ($tag === 'if') {
8261 $truncate .= '<!--<![endif]-->';
8262 } else {
8263 $truncate .= '</' . $tag . '>';
8267 return $truncate;
8271 * Shortens a given filename by removing characters positioned after the ideal string length.
8272 * When the filename is too long, the file cannot be created on the filesystem due to exceeding max byte size.
8273 * Limiting the filename to a certain size (considering multibyte characters) will prevent this.
8275 * @param string $filename file name
8276 * @param int $length ideal string length
8277 * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness.
8278 * @return string $shortened shortened file name
8280 function shorten_filename($filename, $length = MAX_FILENAME_SIZE, $includehash = false) {
8281 $shortened = $filename;
8282 // Extract a part of the filename if it's char size exceeds the ideal string length.
8283 if (core_text::strlen($filename) > $length) {
8284 // Exclude extension if present in filename.
8285 $mimetypes = get_mimetypes_array();
8286 $extension = pathinfo($filename, PATHINFO_EXTENSION);
8287 if ($extension && !empty($mimetypes[$extension])) {
8288 $basename = pathinfo($filename, PATHINFO_FILENAME);
8289 $hash = empty($includehash) ? '' : ' - ' . substr(sha1($basename), 0, 10);
8290 $shortened = core_text::substr($basename, 0, $length - strlen($hash)) . $hash;
8291 $shortened .= '.' . $extension;
8292 } else {
8293 $hash = empty($includehash) ? '' : ' - ' . substr(sha1($filename), 0, 10);
8294 $shortened = core_text::substr($filename, 0, $length - strlen($hash)) . $hash;
8297 return $shortened;
8301 * Shortens a given array of filenames by removing characters positioned after the ideal string length.
8303 * @param array $path The paths to reduce the length.
8304 * @param int $length Ideal string length
8305 * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness.
8306 * @return array $result Shortened paths in array.
8308 function shorten_filenames(array $path, $length = MAX_FILENAME_SIZE, $includehash = false) {
8309 $result = null;
8311 $result = array_reduce($path, function($carry, $singlepath) use ($length, $includehash) {
8312 $carry[] = shorten_filename($singlepath, $length, $includehash);
8313 return $carry;
8314 }, []);
8316 return $result;
8320 * Given dates in seconds, how many weeks is the date from startdate
8321 * The first week is 1, the second 2 etc ...
8323 * @param int $startdate Timestamp for the start date
8324 * @param int $thedate Timestamp for the end date
8325 * @return string
8327 function getweek ($startdate, $thedate) {
8328 if ($thedate < $startdate) {
8329 return 0;
8332 return floor(($thedate - $startdate) / WEEKSECS) + 1;
8336 * Returns a randomly generated password of length $maxlen. inspired by
8338 * {@link http://www.phpbuilder.com/columns/jesus19990502.php3} and
8339 * {@link http://es2.php.net/manual/en/function.str-shuffle.php#73254}
8341 * @param int $maxlen The maximum size of the password being generated.
8342 * @return string
8344 function generate_password($maxlen=10) {
8345 global $CFG;
8347 if (empty($CFG->passwordpolicy)) {
8348 $fillers = PASSWORD_DIGITS;
8349 $wordlist = file($CFG->wordlist);
8350 $word1 = trim($wordlist[rand(0, count($wordlist) - 1)]);
8351 $word2 = trim($wordlist[rand(0, count($wordlist) - 1)]);
8352 $filler1 = $fillers[rand(0, strlen($fillers) - 1)];
8353 $password = $word1 . $filler1 . $word2;
8354 } else {
8355 $minlen = !empty($CFG->minpasswordlength) ? $CFG->minpasswordlength : 0;
8356 $digits = $CFG->minpassworddigits;
8357 $lower = $CFG->minpasswordlower;
8358 $upper = $CFG->minpasswordupper;
8359 $nonalphanum = $CFG->minpasswordnonalphanum;
8360 $total = $lower + $upper + $digits + $nonalphanum;
8361 // Var minlength should be the greater one of the two ( $minlen and $total ).
8362 $minlen = $minlen < $total ? $total : $minlen;
8363 // Var maxlen can never be smaller than minlen.
8364 $maxlen = $minlen > $maxlen ? $minlen : $maxlen;
8365 $additional = $maxlen - $total;
8367 // Make sure we have enough characters to fulfill
8368 // complexity requirements.
8369 $passworddigits = PASSWORD_DIGITS;
8370 while ($digits > strlen($passworddigits)) {
8371 $passworddigits .= PASSWORD_DIGITS;
8373 $passwordlower = PASSWORD_LOWER;
8374 while ($lower > strlen($passwordlower)) {
8375 $passwordlower .= PASSWORD_LOWER;
8377 $passwordupper = PASSWORD_UPPER;
8378 while ($upper > strlen($passwordupper)) {
8379 $passwordupper .= PASSWORD_UPPER;
8381 $passwordnonalphanum = PASSWORD_NONALPHANUM;
8382 while ($nonalphanum > strlen($passwordnonalphanum)) {
8383 $passwordnonalphanum .= PASSWORD_NONALPHANUM;
8386 // Now mix and shuffle it all.
8387 $password = str_shuffle (substr(str_shuffle ($passwordlower), 0, $lower) .
8388 substr(str_shuffle ($passwordupper), 0, $upper) .
8389 substr(str_shuffle ($passworddigits), 0, $digits) .
8390 substr(str_shuffle ($passwordnonalphanum), 0 , $nonalphanum) .
8391 substr(str_shuffle ($passwordlower .
8392 $passwordupper .
8393 $passworddigits .
8394 $passwordnonalphanum), 0 , $additional));
8397 return substr ($password, 0, $maxlen);
8401 * Given a float, prints it nicely.
8402 * Localized floats must not be used in calculations!
8404 * The stripzeros feature is intended for making numbers look nicer in small
8405 * areas where it is not necessary to indicate the degree of accuracy by showing
8406 * ending zeros. If you turn it on with $decimalpoints set to 3, for example,
8407 * then it will display '5.4' instead of '5.400' or '5' instead of '5.000'.
8409 * @param float $float The float to print
8410 * @param int $decimalpoints The number of decimal places to print. -1 is a special value for auto detect (full precision).
8411 * @param bool $localized use localized decimal separator
8412 * @param bool $stripzeros If true, removes final zeros after decimal point. It will be ignored and the trailing zeros after
8413 * the decimal point are always striped if $decimalpoints is -1.
8414 * @return string locale float
8416 function format_float($float, $decimalpoints=1, $localized=true, $stripzeros=false) {
8417 if (is_null($float)) {
8418 return '';
8420 if ($localized) {
8421 $separator = get_string('decsep', 'langconfig');
8422 } else {
8423 $separator = '.';
8425 if ($decimalpoints == -1) {
8426 // The following counts the number of decimals.
8427 // It is safe as both floatval() and round() functions have same behaviour when non-numeric values are provided.
8428 $floatval = floatval($float);
8429 for ($decimalpoints = 0; $floatval != round($float, $decimalpoints); $decimalpoints++);
8432 $result = number_format($float, $decimalpoints, $separator, '');
8433 if ($stripzeros && $decimalpoints > 0) {
8434 // Remove zeros and final dot if not needed.
8435 // However, only do this if there is a decimal point!
8436 $result = preg_replace('~(' . preg_quote($separator, '~') . ')?0+$~', '', $result);
8438 return $result;
8442 * Converts locale specific floating point/comma number back to standard PHP float value
8443 * Do NOT try to do any math operations before this conversion on any user submitted floats!
8445 * @param string $localefloat locale aware float representation
8446 * @param bool $strict If true, then check the input and return false if it is not a valid number.
8447 * @return mixed float|bool - false or the parsed float.
8449 function unformat_float($localefloat, $strict = false) {
8450 $localefloat = trim((string)$localefloat);
8452 if ($localefloat == '') {
8453 return null;
8456 $localefloat = str_replace(' ', '', $localefloat); // No spaces - those might be used as thousand separators.
8457 $localefloat = str_replace(get_string('decsep', 'langconfig'), '.', $localefloat);
8459 if ($strict && !is_numeric($localefloat)) {
8460 return false;
8463 return (float)$localefloat;
8467 * Given a simple array, this shuffles it up just like shuffle()
8468 * Unlike PHP's shuffle() this function works on any machine.
8470 * @param array $array The array to be rearranged
8471 * @return array
8473 function swapshuffle($array) {
8475 $last = count($array) - 1;
8476 for ($i = 0; $i <= $last; $i++) {
8477 $from = rand(0, $last);
8478 $curr = $array[$i];
8479 $array[$i] = $array[$from];
8480 $array[$from] = $curr;
8482 return $array;
8486 * Like {@link swapshuffle()}, but works on associative arrays
8488 * @param array $array The associative array to be rearranged
8489 * @return array
8491 function swapshuffle_assoc($array) {
8493 $newarray = array();
8494 $newkeys = swapshuffle(array_keys($array));
8496 foreach ($newkeys as $newkey) {
8497 $newarray[$newkey] = $array[$newkey];
8499 return $newarray;
8503 * Given an arbitrary array, and a number of draws,
8504 * this function returns an array with that amount
8505 * of items. The indexes are retained.
8507 * @todo Finish documenting this function
8509 * @param array $array
8510 * @param int $draws
8511 * @return array
8513 function draw_rand_array($array, $draws) {
8515 $return = array();
8517 $last = count($array);
8519 if ($draws > $last) {
8520 $draws = $last;
8523 while ($draws > 0) {
8524 $last--;
8526 $keys = array_keys($array);
8527 $rand = rand(0, $last);
8529 $return[$keys[$rand]] = $array[$keys[$rand]];
8530 unset($array[$keys[$rand]]);
8532 $draws--;
8535 return $return;
8539 * Calculate the difference between two microtimes
8541 * @param string $a The first Microtime
8542 * @param string $b The second Microtime
8543 * @return string
8545 function microtime_diff($a, $b) {
8546 list($adec, $asec) = explode(' ', $a);
8547 list($bdec, $bsec) = explode(' ', $b);
8548 return $bsec - $asec + $bdec - $adec;
8552 * Given a list (eg a,b,c,d,e) this function returns
8553 * an array of 1->a, 2->b, 3->c etc
8555 * @param string $list The string to explode into array bits
8556 * @param string $separator The separator used within the list string
8557 * @return array The now assembled array
8559 function make_menu_from_list($list, $separator=',') {
8561 $array = array_reverse(explode($separator, $list), true);
8562 foreach ($array as $key => $item) {
8563 $outarray[$key+1] = trim($item);
8565 return $outarray;
8569 * Creates an array that represents all the current grades that
8570 * can be chosen using the given grading type.
8572 * Negative numbers
8573 * are scales, zero is no grade, and positive numbers are maximum
8574 * grades.
8576 * @todo Finish documenting this function or better deprecated this completely!
8578 * @param int $gradingtype
8579 * @return array
8581 function make_grades_menu($gradingtype) {
8582 global $DB;
8584 $grades = array();
8585 if ($gradingtype < 0) {
8586 if ($scale = $DB->get_record('scale', array('id'=> (-$gradingtype)))) {
8587 return make_menu_from_list($scale->scale);
8589 } else if ($gradingtype > 0) {
8590 for ($i=$gradingtype; $i>=0; $i--) {
8591 $grades[$i] = $i .' / '. $gradingtype;
8593 return $grades;
8595 return $grades;
8599 * make_unique_id_code
8601 * @todo Finish documenting this function
8603 * @uses $_SERVER
8604 * @param string $extra Extra string to append to the end of the code
8605 * @return string
8607 function make_unique_id_code($extra = '') {
8609 $hostname = 'unknownhost';
8610 if (!empty($_SERVER['HTTP_HOST'])) {
8611 $hostname = $_SERVER['HTTP_HOST'];
8612 } else if (!empty($_ENV['HTTP_HOST'])) {
8613 $hostname = $_ENV['HTTP_HOST'];
8614 } else if (!empty($_SERVER['SERVER_NAME'])) {
8615 $hostname = $_SERVER['SERVER_NAME'];
8616 } else if (!empty($_ENV['SERVER_NAME'])) {
8617 $hostname = $_ENV['SERVER_NAME'];
8620 $date = gmdate("ymdHis");
8622 $random = random_string(6);
8624 if ($extra) {
8625 return $hostname .'+'. $date .'+'. $random .'+'. $extra;
8626 } else {
8627 return $hostname .'+'. $date .'+'. $random;
8633 * Function to check the passed address is within the passed subnet
8635 * The parameter is a comma separated string of subnet definitions.
8636 * Subnet strings can be in one of three formats:
8637 * 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn (number of bits in net mask)
8638 * 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)
8639 * 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx. (incomplete address, a bit non-technical ;-)
8640 * Code for type 1 modified from user posted comments by mediator at
8641 * {@link http://au.php.net/manual/en/function.ip2long.php}
8643 * @param string $addr The address you are checking
8644 * @param string $subnetstr The string of subnet addresses
8645 * @param bool $checkallzeros The state to whether check for 0.0.0.0
8646 * @return bool
8648 function address_in_subnet($addr, $subnetstr, $checkallzeros = false) {
8650 if ($addr == '0.0.0.0' && !$checkallzeros) {
8651 return false;
8653 $subnets = explode(',', $subnetstr);
8654 $found = false;
8655 $addr = trim($addr);
8656 $addr = cleanremoteaddr($addr, false); // Normalise.
8657 if ($addr === null) {
8658 return false;
8660 $addrparts = explode(':', $addr);
8662 $ipv6 = strpos($addr, ':');
8664 foreach ($subnets as $subnet) {
8665 $subnet = trim($subnet);
8666 if ($subnet === '') {
8667 continue;
8670 if (strpos($subnet, '/') !== false) {
8671 // 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn.
8672 list($ip, $mask) = explode('/', $subnet);
8673 $mask = trim($mask);
8674 if (!is_number($mask)) {
8675 continue; // Incorect mask number, eh?
8677 $ip = cleanremoteaddr($ip, false); // Normalise.
8678 if ($ip === null) {
8679 continue;
8681 if (strpos($ip, ':') !== false) {
8682 // IPv6.
8683 if (!$ipv6) {
8684 continue;
8686 if ($mask > 128 or $mask < 0) {
8687 continue; // Nonsense.
8689 if ($mask == 0) {
8690 return true; // Any address.
8692 if ($mask == 128) {
8693 if ($ip === $addr) {
8694 return true;
8696 continue;
8698 $ipparts = explode(':', $ip);
8699 $modulo = $mask % 16;
8700 $ipnet = array_slice($ipparts, 0, ($mask-$modulo)/16);
8701 $addrnet = array_slice($addrparts, 0, ($mask-$modulo)/16);
8702 if (implode(':', $ipnet) === implode(':', $addrnet)) {
8703 if ($modulo == 0) {
8704 return true;
8706 $pos = ($mask-$modulo)/16;
8707 $ipnet = hexdec($ipparts[$pos]);
8708 $addrnet = hexdec($addrparts[$pos]);
8709 $mask = 0xffff << (16 - $modulo);
8710 if (($addrnet & $mask) == ($ipnet & $mask)) {
8711 return true;
8715 } else {
8716 // IPv4.
8717 if ($ipv6) {
8718 continue;
8720 if ($mask > 32 or $mask < 0) {
8721 continue; // Nonsense.
8723 if ($mask == 0) {
8724 return true;
8726 if ($mask == 32) {
8727 if ($ip === $addr) {
8728 return true;
8730 continue;
8732 $mask = 0xffffffff << (32 - $mask);
8733 if (((ip2long($addr) & $mask) == (ip2long($ip) & $mask))) {
8734 return true;
8738 } else if (strpos($subnet, '-') !== false) {
8739 // 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.
8740 $parts = explode('-', $subnet);
8741 if (count($parts) != 2) {
8742 continue;
8745 if (strpos($subnet, ':') !== false) {
8746 // IPv6.
8747 if (!$ipv6) {
8748 continue;
8750 $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise.
8751 if ($ipstart === null) {
8752 continue;
8754 $ipparts = explode(':', $ipstart);
8755 $start = hexdec(array_pop($ipparts));
8756 $ipparts[] = trim($parts[1]);
8757 $ipend = cleanremoteaddr(implode(':', $ipparts), false); // Normalise.
8758 if ($ipend === null) {
8759 continue;
8761 $ipparts[7] = '';
8762 $ipnet = implode(':', $ipparts);
8763 if (strpos($addr, $ipnet) !== 0) {
8764 continue;
8766 $ipparts = explode(':', $ipend);
8767 $end = hexdec($ipparts[7]);
8769 $addrend = hexdec($addrparts[7]);
8771 if (($addrend >= $start) and ($addrend <= $end)) {
8772 return true;
8775 } else {
8776 // IPv4.
8777 if ($ipv6) {
8778 continue;
8780 $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise.
8781 if ($ipstart === null) {
8782 continue;
8784 $ipparts = explode('.', $ipstart);
8785 $ipparts[3] = trim($parts[1]);
8786 $ipend = cleanremoteaddr(implode('.', $ipparts), false); // Normalise.
8787 if ($ipend === null) {
8788 continue;
8791 if ((ip2long($addr) >= ip2long($ipstart)) and (ip2long($addr) <= ip2long($ipend))) {
8792 return true;
8796 } else {
8797 // 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx.
8798 if (strpos($subnet, ':') !== false) {
8799 // IPv6.
8800 if (!$ipv6) {
8801 continue;
8803 $parts = explode(':', $subnet);
8804 $count = count($parts);
8805 if ($parts[$count-1] === '') {
8806 unset($parts[$count-1]); // Trim trailing :'s.
8807 $count--;
8808 $subnet = implode('.', $parts);
8810 $isip = cleanremoteaddr($subnet, false); // Normalise.
8811 if ($isip !== null) {
8812 if ($isip === $addr) {
8813 return true;
8815 continue;
8816 } else if ($count > 8) {
8817 continue;
8819 $zeros = array_fill(0, 8-$count, '0');
8820 $subnet = $subnet.':'.implode(':', $zeros).'/'.($count*16);
8821 if (address_in_subnet($addr, $subnet)) {
8822 return true;
8825 } else {
8826 // IPv4.
8827 if ($ipv6) {
8828 continue;
8830 $parts = explode('.', $subnet);
8831 $count = count($parts);
8832 if ($parts[$count-1] === '') {
8833 unset($parts[$count-1]); // Trim trailing .
8834 $count--;
8835 $subnet = implode('.', $parts);
8837 if ($count == 4) {
8838 $subnet = cleanremoteaddr($subnet, false); // Normalise.
8839 if ($subnet === $addr) {
8840 return true;
8842 continue;
8843 } else if ($count > 4) {
8844 continue;
8846 $zeros = array_fill(0, 4-$count, '0');
8847 $subnet = $subnet.'.'.implode('.', $zeros).'/'.($count*8);
8848 if (address_in_subnet($addr, $subnet)) {
8849 return true;
8855 return false;
8859 * For outputting debugging info
8861 * @param string $string The string to write
8862 * @param string $eol The end of line char(s) to use
8863 * @param string $sleep Period to make the application sleep
8864 * This ensures any messages have time to display before redirect
8866 function mtrace($string, $eol="\n", $sleep=0) {
8867 global $CFG;
8869 if (isset($CFG->mtrace_wrapper) && function_exists($CFG->mtrace_wrapper)) {
8870 $fn = $CFG->mtrace_wrapper;
8871 $fn($string, $eol);
8872 return;
8873 } else if (defined('STDOUT') && !PHPUNIT_TEST && !defined('BEHAT_TEST')) {
8874 // We must explicitly call the add_line function here.
8875 // Uses of fwrite to STDOUT are not picked up by ob_start.
8876 if ($output = \core\task\logmanager::add_line("{$string}{$eol}")) {
8877 fwrite(STDOUT, $output);
8879 } else {
8880 echo $string . $eol;
8883 // Flush again.
8884 flush();
8886 // Delay to keep message on user's screen in case of subsequent redirect.
8887 if ($sleep) {
8888 sleep($sleep);
8893 * Helper to {@see mtrace()} an exception or throwable, including all relevant information.
8895 * @param Throwable $e the error to ouptput.
8897 function mtrace_exception(Throwable $e): void {
8898 $info = get_exception_info($e);
8900 $message = $info->message;
8901 if ($info->debuginfo) {
8902 $message .= "\n\n" . $info->debuginfo;
8904 if ($info->backtrace) {
8905 $message .= "\n\n" . format_backtrace($info->backtrace, true);
8908 mtrace($message);
8912 * Replace 1 or more slashes or backslashes to 1 slash
8914 * @param string $path The path to strip
8915 * @return string the path with double slashes removed
8917 function cleardoubleslashes ($path) {
8918 return preg_replace('/(\/|\\\){1,}/', '/', $path);
8922 * Is the current ip in a given list?
8924 * @param string $list
8925 * @return bool
8927 function remoteip_in_list($list) {
8928 $clientip = getremoteaddr(null);
8930 if (!$clientip) {
8931 // Ensure access on cli.
8932 return true;
8934 return \core\ip_utils::is_ip_in_subnet_list($clientip, $list);
8938 * Returns most reliable client address
8940 * @param string $default If an address can't be determined, then return this
8941 * @return string The remote IP address
8943 function getremoteaddr($default='0.0.0.0') {
8944 global $CFG;
8946 if (!isset($CFG->getremoteaddrconf)) {
8947 // This will happen, for example, before just after the upgrade, as the
8948 // user is redirected to the admin screen.
8949 $variablestoskip = GETREMOTEADDR_SKIP_DEFAULT;
8950 } else {
8951 $variablestoskip = $CFG->getremoteaddrconf;
8953 if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_CLIENT_IP)) {
8954 if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
8955 $address = cleanremoteaddr($_SERVER['HTTP_CLIENT_IP']);
8956 return $address ? $address : $default;
8959 if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR)) {
8960 if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
8961 $forwardedaddresses = explode(",", $_SERVER['HTTP_X_FORWARDED_FOR']);
8963 $forwardedaddresses = array_filter($forwardedaddresses, function($ip) {
8964 global $CFG;
8965 return !\core\ip_utils::is_ip_in_subnet_list($ip, $CFG->reverseproxyignore ?? '', ',');
8968 // Multiple proxies can append values to this header including an
8969 // untrusted original request header so we must only trust the last ip.
8970 $address = end($forwardedaddresses);
8972 if (substr_count($address, ":") > 1) {
8973 // Remove port and brackets from IPv6.
8974 if (preg_match("/\[(.*)\]:/", $address, $matches)) {
8975 $address = $matches[1];
8977 } else {
8978 // Remove port from IPv4.
8979 if (substr_count($address, ":") == 1) {
8980 $parts = explode(":", $address);
8981 $address = $parts[0];
8985 $address = cleanremoteaddr($address);
8986 return $address ? $address : $default;
8989 if (!empty($_SERVER['REMOTE_ADDR'])) {
8990 $address = cleanremoteaddr($_SERVER['REMOTE_ADDR']);
8991 return $address ? $address : $default;
8992 } else {
8993 return $default;
8998 * Cleans an ip address. Internal addresses are now allowed.
8999 * (Originally local addresses were not allowed.)
9001 * @param string $addr IPv4 or IPv6 address
9002 * @param bool $compress use IPv6 address compression
9003 * @return string normalised ip address string, null if error
9005 function cleanremoteaddr($addr, $compress=false) {
9006 $addr = trim($addr);
9008 if (strpos($addr, ':') !== false) {
9009 // Can be only IPv6.
9010 $parts = explode(':', $addr);
9011 $count = count($parts);
9013 if (strpos($parts[$count-1], '.') !== false) {
9014 // Legacy ipv4 notation.
9015 $last = array_pop($parts);
9016 $ipv4 = cleanremoteaddr($last, true);
9017 if ($ipv4 === null) {
9018 return null;
9020 $bits = explode('.', $ipv4);
9021 $parts[] = dechex($bits[0]).dechex($bits[1]);
9022 $parts[] = dechex($bits[2]).dechex($bits[3]);
9023 $count = count($parts);
9024 $addr = implode(':', $parts);
9027 if ($count < 3 or $count > 8) {
9028 return null; // Severly malformed.
9031 if ($count != 8) {
9032 if (strpos($addr, '::') === false) {
9033 return null; // Malformed.
9035 // Uncompress.
9036 $insertat = array_search('', $parts, true);
9037 $missing = array_fill(0, 1 + 8 - $count, '0');
9038 array_splice($parts, $insertat, 1, $missing);
9039 foreach ($parts as $key => $part) {
9040 if ($part === '') {
9041 $parts[$key] = '0';
9046 $adr = implode(':', $parts);
9047 if (!preg_match('/^([0-9a-f]{1,4})(:[0-9a-f]{1,4})*$/i', $adr)) {
9048 return null; // Incorrect format - sorry.
9051 // Normalise 0s and case.
9052 $parts = array_map('hexdec', $parts);
9053 $parts = array_map('dechex', $parts);
9055 $result = implode(':', $parts);
9057 if (!$compress) {
9058 return $result;
9061 if ($result === '0:0:0:0:0:0:0:0') {
9062 return '::'; // All addresses.
9065 $compressed = preg_replace('/(:0)+:0$/', '::', $result, 1);
9066 if ($compressed !== $result) {
9067 return $compressed;
9070 $compressed = preg_replace('/^(0:){2,7}/', '::', $result, 1);
9071 if ($compressed !== $result) {
9072 return $compressed;
9075 $compressed = preg_replace('/(:0){2,6}:/', '::', $result, 1);
9076 if ($compressed !== $result) {
9077 return $compressed;
9080 return $result;
9083 // First get all things that look like IPv4 addresses.
9084 $parts = array();
9085 if (!preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $addr, $parts)) {
9086 return null;
9088 unset($parts[0]);
9090 foreach ($parts as $key => $match) {
9091 if ($match > 255) {
9092 return null;
9094 $parts[$key] = (int)$match; // Normalise 0s.
9097 return implode('.', $parts);
9102 * Is IP address a public address?
9104 * @param string $ip The ip to check
9105 * @return bool true if the ip is public
9107 function ip_is_public($ip) {
9108 return (bool) filter_var($ip, FILTER_VALIDATE_IP, (FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE));
9112 * This function will make a complete copy of anything it's given,
9113 * regardless of whether it's an object or not.
9115 * @param mixed $thing Something you want cloned
9116 * @return mixed What ever it is you passed it
9118 function fullclone($thing) {
9119 return unserialize(serialize($thing));
9123 * Used to make sure that $min <= $value <= $max
9125 * Make sure that value is between min, and max
9127 * @param int $min The minimum value
9128 * @param int $value The value to check
9129 * @param int $max The maximum value
9130 * @return int
9132 function bounded_number($min, $value, $max) {
9133 if ($value < $min) {
9134 return $min;
9136 if ($value > $max) {
9137 return $max;
9139 return $value;
9143 * Check if there is a nested array within the passed array
9145 * @param array $array
9146 * @return bool true if there is a nested array false otherwise
9148 function array_is_nested($array) {
9149 foreach ($array as $value) {
9150 if (is_array($value)) {
9151 return true;
9154 return false;
9158 * get_performance_info() pairs up with init_performance_info()
9159 * loaded in setup.php. Returns an array with 'html' and 'txt'
9160 * values ready for use, and each of the individual stats provided
9161 * separately as well.
9163 * @return array
9165 function get_performance_info() {
9166 global $CFG, $PERF, $DB, $PAGE;
9168 $info = array();
9169 $info['txt'] = me() . ' '; // Holds log-friendly representation.
9171 $info['html'] = '';
9172 if (!empty($CFG->themedesignermode)) {
9173 // Attempt to avoid devs debugging peformance issues, when its caused by css building and so on.
9174 $info['html'] .= '<p><strong>Warning: Theme designer mode is enabled.</strong></p>';
9176 $info['html'] .= '<ul class="list-unstyled row mx-md-0">'; // Holds userfriendly HTML representation.
9178 $info['realtime'] = microtime_diff($PERF->starttime, microtime());
9180 $info['html'] .= '<li class="timeused col-sm-4">'.$info['realtime'].' secs</li> ';
9181 $info['txt'] .= 'time: '.$info['realtime'].'s ';
9183 // GET/POST (or NULL if $_SERVER['REQUEST_METHOD'] is undefined) is useful for txt logged information.
9184 $info['txt'] .= 'method: ' . ($_SERVER['REQUEST_METHOD'] ?? "NULL") . ' ';
9186 if (function_exists('memory_get_usage')) {
9187 $info['memory_total'] = memory_get_usage();
9188 $info['memory_growth'] = memory_get_usage() - $PERF->startmemory;
9189 $info['html'] .= '<li class="memoryused col-sm-4">RAM: '.display_size($info['memory_total']).'</li> ';
9190 $info['txt'] .= 'memory_total: '.$info['memory_total'].'B (' . display_size($info['memory_total']).') memory_growth: '.
9191 $info['memory_growth'].'B ('.display_size($info['memory_growth']).') ';
9194 if (function_exists('memory_get_peak_usage')) {
9195 $info['memory_peak'] = memory_get_peak_usage();
9196 $info['html'] .= '<li class="memoryused col-sm-4">RAM peak: '.display_size($info['memory_peak']).'</li> ';
9197 $info['txt'] .= 'memory_peak: '.$info['memory_peak'].'B (' . display_size($info['memory_peak']).') ';
9200 $info['html'] .= '</ul><ul class="list-unstyled row mx-md-0">';
9201 $inc = get_included_files();
9202 $info['includecount'] = count($inc);
9203 $info['html'] .= '<li class="included col-sm-4">Included '.$info['includecount'].' files</li> ';
9204 $info['txt'] .= 'includecount: '.$info['includecount'].' ';
9206 if (!empty($CFG->early_install_lang) or empty($PAGE)) {
9207 // We can not track more performance before installation or before PAGE init, sorry.
9208 return $info;
9211 $filtermanager = filter_manager::instance();
9212 if (method_exists($filtermanager, 'get_performance_summary')) {
9213 list($filterinfo, $nicenames) = $filtermanager->get_performance_summary();
9214 $info = array_merge($filterinfo, $info);
9215 foreach ($filterinfo as $key => $value) {
9216 $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> ";
9217 $info['txt'] .= "$key: $value ";
9221 $stringmanager = get_string_manager();
9222 if (method_exists($stringmanager, 'get_performance_summary')) {
9223 list($filterinfo, $nicenames) = $stringmanager->get_performance_summary();
9224 $info = array_merge($filterinfo, $info);
9225 foreach ($filterinfo as $key => $value) {
9226 $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> ";
9227 $info['txt'] .= "$key: $value ";
9231 $info['dbqueries'] = $DB->perf_get_reads().'/'.$DB->perf_get_writes();
9232 $info['html'] .= '<li class="dbqueries col-sm-4">DB reads/writes: '.$info['dbqueries'].'</li> ';
9233 $info['txt'] .= 'db reads/writes: '.$info['dbqueries'].' ';
9235 if ($DB->want_read_slave()) {
9236 $info['dbreads_slave'] = $DB->perf_get_reads_slave();
9237 $info['html'] .= '<li class="dbqueries col-sm-4">DB reads from slave: '.$info['dbreads_slave'].'</li> ';
9238 $info['txt'] .= 'db reads from slave: '.$info['dbreads_slave'].' ';
9241 $info['dbtime'] = round($DB->perf_get_queries_time(), 5);
9242 $info['html'] .= '<li class="dbtime col-sm-4">DB queries time: '.$info['dbtime'].' secs</li> ';
9243 $info['txt'] .= 'db queries time: ' . $info['dbtime'] . 's ';
9245 if (function_exists('posix_times')) {
9246 $ptimes = posix_times();
9247 if (is_array($ptimes)) {
9248 foreach ($ptimes as $key => $val) {
9249 $info[$key] = $ptimes[$key] - $PERF->startposixtimes[$key];
9251 $info['html'] .= "<li class=\"posixtimes col-sm-4\">ticks: $info[ticks] user: $info[utime]";
9252 $info['html'] .= "sys: $info[stime] cuser: $info[cutime] csys: $info[cstime]</li> ";
9253 $info['txt'] .= "ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime] ";
9257 // Grab the load average for the last minute.
9258 // /proc will only work under some linux configurations
9259 // while uptime is there under MacOSX/Darwin and other unices.
9260 if (is_readable('/proc/loadavg') && $loadavg = @file('/proc/loadavg')) {
9261 list($serverload) = explode(' ', $loadavg[0]);
9262 unset($loadavg);
9263 } else if ( function_exists('is_executable') && is_executable('/usr/bin/uptime') && $loadavg = `/usr/bin/uptime` ) {
9264 if (preg_match('/load averages?: (\d+[\.,:]\d+)/', $loadavg, $matches)) {
9265 $serverload = $matches[1];
9266 } else {
9267 trigger_error('Could not parse uptime output!');
9270 if (!empty($serverload)) {
9271 $info['serverload'] = $serverload;
9272 $info['html'] .= '<li class="serverload col-sm-4">Load average: '.$info['serverload'].'</li> ';
9273 $info['txt'] .= "serverload: {$info['serverload']} ";
9276 // Display size of session if session started.
9277 if ($si = \core\session\manager::get_performance_info()) {
9278 $info['sessionsize'] = $si['size'];
9279 $info['html'] .= "<li class=\"serverload col-sm-4\">" . $si['html'] . "</li>";
9280 $info['txt'] .= $si['txt'];
9283 // Display time waiting for session if applicable.
9284 if (!empty($PERF->sessionlock['wait'])) {
9285 $sessionwait = number_format($PERF->sessionlock['wait'], 3) . ' secs';
9286 $info['html'] .= html_writer::tag('li', 'Session wait: ' . $sessionwait, [
9287 'class' => 'sessionwait col-sm-4'
9289 $info['txt'] .= 'sessionwait: ' . $sessionwait . ' ';
9292 $info['html'] .= '</ul>';
9293 $html = '';
9294 if ($stats = cache_helper::get_stats()) {
9296 $table = new html_table();
9297 $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered';
9298 $table->head = ['Mode', 'Cache item', 'Static', 'H', 'M', get_string('mappingprimary', 'cache'), 'H', 'M', 'S', 'I/O'];
9299 $table->data = [];
9300 $table->align = ['left', 'left', 'left', 'right', 'right', 'left', 'right', 'right', 'right', 'right'];
9302 $text = 'Caches used (hits/misses/sets): ';
9303 $hits = 0;
9304 $misses = 0;
9305 $sets = 0;
9306 $maxstores = 0;
9308 // We want to align static caches into their own column.
9309 $hasstatic = false;
9310 foreach ($stats as $definition => $details) {
9311 $numstores = count($details['stores']);
9312 $first = key($details['stores']);
9313 if ($first !== cache_store::STATIC_ACCEL) {
9314 $numstores++; // Add a blank space for the missing static store.
9316 $maxstores = max($maxstores, $numstores);
9319 $storec = 0;
9321 while ($storec++ < ($maxstores - 2)) {
9322 if ($storec == ($maxstores - 2)) {
9323 $table->head[] = get_string('mappingfinal', 'cache');
9324 } else {
9325 $table->head[] = "Store $storec";
9327 $table->align[] = 'left';
9328 $table->align[] = 'right';
9329 $table->align[] = 'right';
9330 $table->align[] = 'right';
9331 $table->align[] = 'right';
9332 $table->head[] = 'H';
9333 $table->head[] = 'M';
9334 $table->head[] = 'S';
9335 $table->head[] = 'I/O';
9338 ksort($stats);
9340 foreach ($stats as $definition => $details) {
9341 switch ($details['mode']) {
9342 case cache_store::MODE_APPLICATION:
9343 $modeclass = 'application';
9344 $mode = ' <span title="application cache">App</span>';
9345 break;
9346 case cache_store::MODE_SESSION:
9347 $modeclass = 'session';
9348 $mode = ' <span title="session cache">Ses</span>';
9349 break;
9350 case cache_store::MODE_REQUEST:
9351 $modeclass = 'request';
9352 $mode = ' <span title="request cache">Req</span>';
9353 break;
9355 $row = [$mode, $definition];
9357 $text .= "$definition {";
9359 $storec = 0;
9360 foreach ($details['stores'] as $store => $data) {
9362 if ($storec == 0 && $store !== cache_store::STATIC_ACCEL) {
9363 $row[] = '';
9364 $row[] = '';
9365 $row[] = '';
9366 $storec++;
9369 $hits += $data['hits'];
9370 $misses += $data['misses'];
9371 $sets += $data['sets'];
9372 if ($data['hits'] == 0 and $data['misses'] > 0) {
9373 $cachestoreclass = 'nohits bg-danger';
9374 } else if ($data['hits'] < $data['misses']) {
9375 $cachestoreclass = 'lowhits bg-warning text-dark';
9376 } else {
9377 $cachestoreclass = 'hihits';
9379 $text .= "$store($data[hits]/$data[misses]/$data[sets]) ";
9380 $cell = new html_table_cell($store);
9381 $cell->attributes = ['class' => $cachestoreclass];
9382 $row[] = $cell;
9383 $cell = new html_table_cell($data['hits']);
9384 $cell->attributes = ['class' => $cachestoreclass];
9385 $row[] = $cell;
9386 $cell = new html_table_cell($data['misses']);
9387 $cell->attributes = ['class' => $cachestoreclass];
9388 $row[] = $cell;
9390 if ($store !== cache_store::STATIC_ACCEL) {
9391 // The static cache is never set.
9392 $cell = new html_table_cell($data['sets']);
9393 $cell->attributes = ['class' => $cachestoreclass];
9394 $row[] = $cell;
9396 if ($data['hits'] || $data['sets']) {
9397 if ($data['iobytes'] === cache_store::IO_BYTES_NOT_SUPPORTED) {
9398 $size = '-';
9399 } else {
9400 $size = display_size($data['iobytes'], 1, 'KB');
9401 if ($data['iobytes'] >= 10 * 1024) {
9402 $cachestoreclass = ' bg-warning text-dark';
9405 } else {
9406 $size = '';
9408 $cell = new html_table_cell($size);
9409 $cell->attributes = ['class' => $cachestoreclass];
9410 $row[] = $cell;
9412 $storec++;
9414 while ($storec++ < $maxstores) {
9415 $row[] = '';
9416 $row[] = '';
9417 $row[] = '';
9418 $row[] = '';
9419 $row[] = '';
9421 $text .= '} ';
9423 $table->data[] = $row;
9426 $html .= html_writer::table($table);
9428 // Now lets also show sub totals for each cache store.
9429 $storetotals = [];
9430 $storetotal = ['hits' => 0, 'misses' => 0, 'sets' => 0, 'iobytes' => 0];
9431 foreach ($stats as $definition => $details) {
9432 foreach ($details['stores'] as $store => $data) {
9433 if (!array_key_exists($store, $storetotals)) {
9434 $storetotals[$store] = ['hits' => 0, 'misses' => 0, 'sets' => 0, 'iobytes' => 0];
9436 $storetotals[$store]['class'] = $data['class'];
9437 $storetotals[$store]['hits'] += $data['hits'];
9438 $storetotals[$store]['misses'] += $data['misses'];
9439 $storetotals[$store]['sets'] += $data['sets'];
9440 $storetotal['hits'] += $data['hits'];
9441 $storetotal['misses'] += $data['misses'];
9442 $storetotal['sets'] += $data['sets'];
9443 if ($data['iobytes'] !== cache_store::IO_BYTES_NOT_SUPPORTED) {
9444 $storetotals[$store]['iobytes'] += $data['iobytes'];
9445 $storetotal['iobytes'] += $data['iobytes'];
9450 $table = new html_table();
9451 $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered';
9452 $table->head = [get_string('storename', 'cache'), get_string('type_cachestore', 'plugin'), 'H', 'M', 'S', 'I/O'];
9453 $table->data = [];
9454 $table->align = ['left', 'left', 'right', 'right', 'right', 'right'];
9456 ksort($storetotals);
9458 foreach ($storetotals as $store => $data) {
9459 $row = [];
9460 if ($data['hits'] == 0 and $data['misses'] > 0) {
9461 $cachestoreclass = 'nohits bg-danger';
9462 } else if ($data['hits'] < $data['misses']) {
9463 $cachestoreclass = 'lowhits bg-warning text-dark';
9464 } else {
9465 $cachestoreclass = 'hihits';
9467 $cell = new html_table_cell($store);
9468 $cell->attributes = ['class' => $cachestoreclass];
9469 $row[] = $cell;
9470 $cell = new html_table_cell($data['class']);
9471 $cell->attributes = ['class' => $cachestoreclass];
9472 $row[] = $cell;
9473 $cell = new html_table_cell($data['hits']);
9474 $cell->attributes = ['class' => $cachestoreclass];
9475 $row[] = $cell;
9476 $cell = new html_table_cell($data['misses']);
9477 $cell->attributes = ['class' => $cachestoreclass];
9478 $row[] = $cell;
9479 $cell = new html_table_cell($data['sets']);
9480 $cell->attributes = ['class' => $cachestoreclass];
9481 $row[] = $cell;
9482 if ($data['hits'] || $data['sets']) {
9483 if ($data['iobytes']) {
9484 $size = display_size($data['iobytes'], 1, 'KB');
9485 } else {
9486 $size = '-';
9488 } else {
9489 $size = '';
9491 $cell = new html_table_cell($size);
9492 $cell->attributes = ['class' => $cachestoreclass];
9493 $row[] = $cell;
9494 $table->data[] = $row;
9496 if (!empty($storetotal['iobytes'])) {
9497 $size = display_size($storetotal['iobytes'], 1, 'KB');
9498 } else if (!empty($storetotal['hits']) || !empty($storetotal['sets'])) {
9499 $size = '-';
9500 } else {
9501 $size = '';
9503 $row = [
9504 get_string('total'),
9506 $storetotal['hits'],
9507 $storetotal['misses'],
9508 $storetotal['sets'],
9509 $size,
9511 $table->data[] = $row;
9513 $html .= html_writer::table($table);
9515 $info['cachesused'] = "$hits / $misses / $sets";
9516 $info['html'] .= $html;
9517 $info['txt'] .= $text.'. ';
9518 } else {
9519 $info['cachesused'] = '0 / 0 / 0';
9520 $info['html'] .= '<div class="cachesused">Caches used (hits/misses/sets): 0/0/0</div>';
9521 $info['txt'] .= 'Caches used (hits/misses/sets): 0/0/0 ';
9524 // Display lock information if any.
9525 if (!empty($PERF->locks)) {
9526 $table = new html_table();
9527 $table->attributes['class'] = 'locktimings table table-dark table-sm w-auto table-bordered';
9528 $table->head = ['Lock', 'Waited (s)', 'Obtained', 'Held for (s)'];
9529 $table->align = ['left', 'right', 'center', 'right'];
9530 $table->data = [];
9531 $text = 'Locks (waited/obtained/held):';
9532 foreach ($PERF->locks as $locktiming) {
9533 $row = [];
9534 $row[] = s($locktiming->type . '/' . $locktiming->resource);
9535 $text .= ' ' . $locktiming->type . '/' . $locktiming->resource . ' (';
9537 // The time we had to wait to get the lock.
9538 $roundedtime = number_format($locktiming->wait, 1);
9539 $cell = new html_table_cell($roundedtime);
9540 if ($locktiming->wait > 0.5) {
9541 $cell->attributes = ['class' => 'bg-warning text-dark'];
9543 $row[] = $cell;
9544 $text .= $roundedtime . '/';
9546 // Show a tick or cross for success.
9547 $row[] = $locktiming->success ? '&#x2713;' : '&#x274c;';
9548 $text .= ($locktiming->success ? 'y' : 'n') . '/';
9550 // If applicable, show how long we held the lock before releasing it.
9551 if (property_exists($locktiming, 'held')) {
9552 $roundedtime = number_format($locktiming->held, 1);
9553 $cell = new html_table_cell($roundedtime);
9554 if ($locktiming->held > 0.5) {
9555 $cell->attributes = ['class' => 'bg-warning text-dark'];
9557 $row[] = $cell;
9558 $text .= $roundedtime;
9559 } else {
9560 $row[] = '-';
9561 $text .= '-';
9563 $text .= ')';
9565 $table->data[] = $row;
9567 $info['html'] .= html_writer::table($table);
9568 $info['txt'] .= $text . '. ';
9571 $info['html'] = '<div class="performanceinfo siteinfo container-fluid px-md-0 overflow-auto pt-3">'.$info['html'].'</div>';
9572 return $info;
9576 * Renames a file or directory to a unique name within the same directory.
9578 * This function is designed to avoid any potential race conditions, and select an unused name.
9580 * @param string $filepath Original filepath
9581 * @param string $prefix Prefix to use for the temporary name
9582 * @return string|bool New file path or false if failed
9583 * @since Moodle 3.10
9585 function rename_to_unused_name(string $filepath, string $prefix = '_temp_') {
9586 $dir = dirname($filepath);
9587 $basename = $dir . '/' . $prefix;
9588 $limit = 0;
9589 while ($limit < 100) {
9590 // Select a new name based on a random number.
9591 $newfilepath = $basename . md5(mt_rand());
9593 // Attempt a rename to that new name.
9594 if (@rename($filepath, $newfilepath)) {
9595 return $newfilepath;
9598 // The first time, do some sanity checks, maybe it is failing for a good reason and there
9599 // is no point trying 100 times if so.
9600 if ($limit === 0 && (!file_exists($filepath) || !is_writable($dir))) {
9601 return false;
9603 $limit++;
9605 return false;
9609 * Delete directory or only its content
9611 * @param string $dir directory path
9612 * @param bool $contentonly
9613 * @return bool success, true also if dir does not exist
9615 function remove_dir($dir, $contentonly=false) {
9616 if (!is_dir($dir)) {
9617 // Nothing to do.
9618 return true;
9621 if (!$contentonly) {
9622 // Start by renaming the directory; this will guarantee that other processes don't write to it
9623 // while it is in the process of being deleted.
9624 $tempdir = rename_to_unused_name($dir);
9625 if ($tempdir) {
9626 // If the rename was successful then delete the $tempdir instead.
9627 $dir = $tempdir;
9629 // If the rename fails, we will continue through and attempt to delete the directory
9630 // without renaming it since that is likely to at least delete most of the files.
9633 if (!$handle = opendir($dir)) {
9634 return false;
9636 $result = true;
9637 while (false!==($item = readdir($handle))) {
9638 if ($item != '.' && $item != '..') {
9639 if (is_dir($dir.'/'.$item)) {
9640 $result = remove_dir($dir.'/'.$item) && $result;
9641 } else {
9642 $result = unlink($dir.'/'.$item) && $result;
9646 closedir($handle);
9647 if ($contentonly) {
9648 clearstatcache(); // Make sure file stat cache is properly invalidated.
9649 return $result;
9651 $result = rmdir($dir); // If anything left the result will be false, no need for && $result.
9652 clearstatcache(); // Make sure file stat cache is properly invalidated.
9653 return $result;
9657 * Detect if an object or a class contains a given property
9658 * will take an actual object or the name of a class
9660 * @param mixed $obj Name of class or real object to test
9661 * @param string $property name of property to find
9662 * @return bool true if property exists
9664 function object_property_exists( $obj, $property ) {
9665 if (is_string( $obj )) {
9666 $properties = get_class_vars( $obj );
9667 } else {
9668 $properties = get_object_vars( $obj );
9670 return array_key_exists( $property, $properties );
9674 * Converts an object into an associative array
9676 * This function converts an object into an associative array by iterating
9677 * over its public properties. Because this function uses the foreach
9678 * construct, Iterators are respected. It works recursively on arrays of objects.
9679 * Arrays and simple values are returned as is.
9681 * If class has magic properties, it can implement IteratorAggregate
9682 * and return all available properties in getIterator()
9684 * @param mixed $var
9685 * @return array
9687 function convert_to_array($var) {
9688 $result = array();
9690 // Loop over elements/properties.
9691 foreach ($var as $key => $value) {
9692 // Recursively convert objects.
9693 if (is_object($value) || is_array($value)) {
9694 $result[$key] = convert_to_array($value);
9695 } else {
9696 // Simple values are untouched.
9697 $result[$key] = $value;
9700 return $result;
9704 * Detect a custom script replacement in the data directory that will
9705 * replace an existing moodle script
9707 * @return string|bool full path name if a custom script exists, false if no custom script exists
9709 function custom_script_path() {
9710 global $CFG, $SCRIPT;
9712 if ($SCRIPT === null) {
9713 // Probably some weird external script.
9714 return false;
9717 $scriptpath = $CFG->customscripts . $SCRIPT;
9719 // Check the custom script exists.
9720 if (file_exists($scriptpath) and is_file($scriptpath)) {
9721 return $scriptpath;
9722 } else {
9723 return false;
9728 * Returns whether or not the user object is a remote MNET user. This function
9729 * is in moodlelib because it does not rely on loading any of the MNET code.
9731 * @param object $user A valid user object
9732 * @return bool True if the user is from a remote Moodle.
9734 function is_mnet_remote_user($user) {
9735 global $CFG;
9737 if (!isset($CFG->mnet_localhost_id)) {
9738 include_once($CFG->dirroot . '/mnet/lib.php');
9739 $env = new mnet_environment();
9740 $env->init();
9741 unset($env);
9744 return (!empty($user->mnethostid) && $user->mnethostid != $CFG->mnet_localhost_id);
9748 * This function will search for browser prefereed languages, setting Moodle
9749 * to use the best one available if $SESSION->lang is undefined
9751 function setup_lang_from_browser() {
9752 global $CFG, $SESSION, $USER;
9754 if (!empty($SESSION->lang) or !empty($USER->lang) or empty($CFG->autolang)) {
9755 // Lang is defined in session or user profile, nothing to do.
9756 return;
9759 if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { // There isn't list of browser langs, nothing to do.
9760 return;
9763 // Extract and clean langs from headers.
9764 $rawlangs = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
9765 $rawlangs = str_replace('-', '_', $rawlangs); // We are using underscores.
9766 $rawlangs = explode(',', $rawlangs); // Convert to array.
9767 $langs = array();
9769 $order = 1.0;
9770 foreach ($rawlangs as $lang) {
9771 if (strpos($lang, ';') === false) {
9772 $langs[(string)$order] = $lang;
9773 $order = $order-0.01;
9774 } else {
9775 $parts = explode(';', $lang);
9776 $pos = strpos($parts[1], '=');
9777 $langs[substr($parts[1], $pos+1)] = $parts[0];
9780 krsort($langs, SORT_NUMERIC);
9782 // Look for such langs under standard locations.
9783 foreach ($langs as $lang) {
9784 // Clean it properly for include.
9785 $lang = strtolower(clean_param($lang, PARAM_SAFEDIR));
9786 if (get_string_manager()->translation_exists($lang, false)) {
9787 // If the translation for this language exists then try to set it
9788 // for the rest of the session, if this is a read only session then
9789 // we can only set it temporarily in $CFG.
9790 if (defined('READ_ONLY_SESSION') && !empty($CFG->enable_read_only_sessions)) {
9791 $CFG->lang = $lang;
9792 } else {
9793 $SESSION->lang = $lang;
9795 // We have finished. Go out.
9796 break;
9799 return;
9803 * Check if $url matches anything in proxybypass list
9805 * Any errors just result in the proxy being used (least bad)
9807 * @param string $url url to check
9808 * @return boolean true if we should bypass the proxy
9810 function is_proxybypass( $url ) {
9811 global $CFG;
9813 // Sanity check.
9814 if (empty($CFG->proxyhost) or empty($CFG->proxybypass)) {
9815 return false;
9818 // Get the host part out of the url.
9819 if (!$host = parse_url( $url, PHP_URL_HOST )) {
9820 return false;
9823 // Get the possible bypass hosts into an array.
9824 $matches = explode( ',', $CFG->proxybypass );
9826 // Check for a exact match on the IP or in the domains.
9827 $isdomaininallowedlist = \core\ip_utils::is_domain_in_allowed_list($host, $matches);
9828 $isipinsubnetlist = \core\ip_utils::is_ip_in_subnet_list($host, $CFG->proxybypass, ',');
9830 if ($isdomaininallowedlist || $isipinsubnetlist) {
9831 return true;
9834 // Nothing matched.
9835 return false;
9839 * Check if the passed navigation is of the new style
9841 * @param mixed $navigation
9842 * @return bool true for yes false for no
9844 function is_newnav($navigation) {
9845 if (is_array($navigation) && !empty($navigation['newnav'])) {
9846 return true;
9847 } else {
9848 return false;
9853 * Checks whether the given variable name is defined as a variable within the given object.
9855 * This will NOT work with stdClass objects, which have no class variables.
9857 * @param string $var The variable name
9858 * @param object $object The object to check
9859 * @return boolean
9861 function in_object_vars($var, $object) {
9862 $classvars = get_class_vars(get_class($object));
9863 $classvars = array_keys($classvars);
9864 return in_array($var, $classvars);
9868 * Returns an array without repeated objects.
9869 * This function is similar to array_unique, but for arrays that have objects as values
9871 * @param array $array
9872 * @param bool $keepkeyassoc
9873 * @return array
9875 function object_array_unique($array, $keepkeyassoc = true) {
9876 $duplicatekeys = array();
9877 $tmp = array();
9879 foreach ($array as $key => $val) {
9880 // Convert objects to arrays, in_array() does not support objects.
9881 if (is_object($val)) {
9882 $val = (array)$val;
9885 if (!in_array($val, $tmp)) {
9886 $tmp[] = $val;
9887 } else {
9888 $duplicatekeys[] = $key;
9892 foreach ($duplicatekeys as $key) {
9893 unset($array[$key]);
9896 return $keepkeyassoc ? $array : array_values($array);
9900 * Is a userid the primary administrator?
9902 * @param int $userid int id of user to check
9903 * @return boolean
9905 function is_primary_admin($userid) {
9906 $primaryadmin = get_admin();
9908 if ($userid == $primaryadmin->id) {
9909 return true;
9910 } else {
9911 return false;
9916 * Returns the site identifier
9918 * @return string $CFG->siteidentifier, first making sure it is properly initialised.
9920 function get_site_identifier() {
9921 global $CFG;
9922 // Check to see if it is missing. If so, initialise it.
9923 if (empty($CFG->siteidentifier)) {
9924 set_config('siteidentifier', random_string(32) . $_SERVER['HTTP_HOST']);
9926 // Return it.
9927 return $CFG->siteidentifier;
9931 * Check whether the given password has no more than the specified
9932 * number of consecutive identical characters.
9934 * @param string $password password to be checked against the password policy
9935 * @param integer $maxchars maximum number of consecutive identical characters
9936 * @return bool
9938 function check_consecutive_identical_characters($password, $maxchars) {
9940 if ($maxchars < 1) {
9941 return true; // Zero 0 is to disable this check.
9943 if (strlen($password) <= $maxchars) {
9944 return true; // Too short to fail this test.
9947 $previouschar = '';
9948 $consecutivecount = 1;
9949 foreach (str_split($password) as $char) {
9950 if ($char != $previouschar) {
9951 $consecutivecount = 1;
9952 } else {
9953 $consecutivecount++;
9954 if ($consecutivecount > $maxchars) {
9955 return false; // Check failed already.
9959 $previouschar = $char;
9962 return true;
9966 * Helper function to do partial function binding.
9967 * so we can use it for preg_replace_callback, for example
9968 * this works with php functions, user functions, static methods and class methods
9969 * it returns you a callback that you can pass on like so:
9971 * $callback = partial('somefunction', $arg1, $arg2);
9972 * or
9973 * $callback = partial(array('someclass', 'somestaticmethod'), $arg1, $arg2);
9974 * or even
9975 * $obj = new someclass();
9976 * $callback = partial(array($obj, 'somemethod'), $arg1, $arg2);
9978 * and then the arguments that are passed through at calltime are appended to the argument list.
9980 * @param mixed $function a php callback
9981 * @param mixed $arg1,... $argv arguments to partially bind with
9982 * @return array Array callback
9984 function partial() {
9985 if (!class_exists('partial')) {
9987 * Used to manage function binding.
9988 * @copyright 2009 Penny Leach
9989 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
9991 class partial{
9992 /** @var array */
9993 public $values = array();
9994 /** @var string The function to call as a callback. */
9995 public $func;
9997 * Constructor
9998 * @param string $func
9999 * @param array $args
10001 public function __construct($func, $args) {
10002 $this->values = $args;
10003 $this->func = $func;
10006 * Calls the callback function.
10007 * @return mixed
10009 public function method() {
10010 $args = func_get_args();
10011 return call_user_func_array($this->func, array_merge($this->values, $args));
10015 $args = func_get_args();
10016 $func = array_shift($args);
10017 $p = new partial($func, $args);
10018 return array($p, 'method');
10022 * helper function to load up and initialise the mnet environment
10023 * this must be called before you use mnet functions.
10025 * @return mnet_environment the equivalent of old $MNET global
10027 function get_mnet_environment() {
10028 global $CFG;
10029 require_once($CFG->dirroot . '/mnet/lib.php');
10030 static $instance = null;
10031 if (empty($instance)) {
10032 $instance = new mnet_environment();
10033 $instance->init();
10035 return $instance;
10039 * during xmlrpc server code execution, any code wishing to access
10040 * information about the remote peer must use this to get it.
10042 * @return mnet_remote_client the equivalent of old $MNETREMOTE_CLIENT global
10044 function get_mnet_remote_client() {
10045 if (!defined('MNET_SERVER')) {
10046 debugging(get_string('notinxmlrpcserver', 'mnet'));
10047 return false;
10049 global $MNET_REMOTE_CLIENT;
10050 if (isset($MNET_REMOTE_CLIENT)) {
10051 return $MNET_REMOTE_CLIENT;
10053 return false;
10057 * during the xmlrpc server code execution, this will be called
10058 * to setup the object returned by {@link get_mnet_remote_client}
10060 * @param mnet_remote_client $client the client to set up
10061 * @throws moodle_exception
10063 function set_mnet_remote_client($client) {
10064 if (!defined('MNET_SERVER')) {
10065 throw new moodle_exception('notinxmlrpcserver', 'mnet');
10067 global $MNET_REMOTE_CLIENT;
10068 $MNET_REMOTE_CLIENT = $client;
10072 * return the jump url for a given remote user
10073 * this is used for rewriting forum post links in emails, etc
10075 * @param stdclass $user the user to get the idp url for
10077 function mnet_get_idp_jump_url($user) {
10078 global $CFG;
10080 static $mnetjumps = array();
10081 if (!array_key_exists($user->mnethostid, $mnetjumps)) {
10082 $idp = mnet_get_peer_host($user->mnethostid);
10083 $idpjumppath = mnet_get_app_jumppath($idp->applicationid);
10084 $mnetjumps[$user->mnethostid] = $idp->wwwroot . $idpjumppath . '?hostwwwroot=' . $CFG->wwwroot . '&wantsurl=';
10086 return $mnetjumps[$user->mnethostid];
10090 * Gets the homepage to use for the current user
10092 * @return int One of HOMEPAGE_*
10094 function get_home_page() {
10095 global $CFG;
10097 if (isloggedin() && !isguestuser() && !empty($CFG->defaulthomepage)) {
10098 // If dashboard is disabled, home will be set to default page.
10099 $defaultpage = get_default_home_page();
10100 if ($CFG->defaulthomepage == HOMEPAGE_MY) {
10101 if (!empty($CFG->enabledashboard)) {
10102 return HOMEPAGE_MY;
10103 } else {
10104 return $defaultpage;
10106 } else if ($CFG->defaulthomepage == HOMEPAGE_MYCOURSES) {
10107 return HOMEPAGE_MYCOURSES;
10108 } else {
10109 $userhomepage = (int) get_user_preferences('user_home_page_preference', $defaultpage);
10110 if (empty($CFG->enabledashboard) && $userhomepage == HOMEPAGE_MY) {
10111 // If the user was using the dashboard but it's disabled, return the default home page.
10112 $userhomepage = $defaultpage;
10114 return $userhomepage;
10117 return HOMEPAGE_SITE;
10121 * Returns the default home page to display if current one is not defined or can't be applied.
10122 * The default behaviour is to return Dashboard if it's enabled or My courses page if it isn't.
10124 * @return int The default home page.
10126 function get_default_home_page(): int {
10127 global $CFG;
10129 return (!isset($CFG->enabledashboard) || $CFG->enabledashboard) ? HOMEPAGE_MY : HOMEPAGE_MYCOURSES;
10133 * Gets the name of a course to be displayed when showing a list of courses.
10134 * By default this is just $course->fullname but user can configure it. The
10135 * result of this function should be passed through print_string.
10136 * @param stdClass|core_course_list_element $course Moodle course object
10137 * @return string Display name of course (either fullname or short + fullname)
10139 function get_course_display_name_for_list($course) {
10140 global $CFG;
10141 if (!empty($CFG->courselistshortnames)) {
10142 if (!($course instanceof stdClass)) {
10143 $course = (object)convert_to_array($course);
10145 return get_string('courseextendednamedisplay', '', $course);
10146 } else {
10147 return $course->fullname;
10152 * Safe analogue of unserialize() that can only parse arrays
10154 * Arrays may contain only integers or strings as both keys and values. Nested arrays are allowed.
10156 * @param string $expression
10157 * @return array|bool either parsed array or false if parsing was impossible.
10159 function unserialize_array($expression) {
10161 // Check the expression is an array.
10162 if (!preg_match('/^a:(\d+):/', $expression)) {
10163 return false;
10166 $values = (array) unserialize_object($expression);
10168 // Callback that returns true if the given value is an unserialized object, executes recursively.
10169 $invalidvaluecallback = static function($value) use (&$invalidvaluecallback): bool {
10170 if (is_array($value)) {
10171 return (bool) array_filter($value, $invalidvaluecallback);
10173 return ($value instanceof stdClass) || ($value instanceof __PHP_Incomplete_Class);
10176 // Iterate over the result to ensure there are no stray objects.
10177 if (array_filter($values, $invalidvaluecallback)) {
10178 return false;
10181 return $values;
10185 * Safe method for unserializing given input that is expected to contain only a serialized instance of an stdClass object
10187 * If any class type other than stdClass is included in the input string, it will not be instantiated and will be cast to an
10188 * stdClass object. The initial cast to array, then back to object is to ensure we are always returning the correct type,
10189 * otherwise we would return an instances of {@see __PHP_Incomplete_class} for malformed strings
10191 * @param string $input
10192 * @return stdClass
10194 function unserialize_object(string $input): stdClass {
10195 $instance = (array) unserialize($input, ['allowed_classes' => [stdClass::class]]);
10196 return (object) $instance;
10200 * The lang_string class
10202 * This special class is used to create an object representation of a string request.
10203 * It is special because processing doesn't occur until the object is first used.
10204 * The class was created especially to aid performance in areas where strings were
10205 * required to be generated but were not necessarily used.
10206 * As an example the admin tree when generated uses over 1500 strings, of which
10207 * normally only 1/3 are ever actually printed at any time.
10208 * The performance advantage is achieved by not actually processing strings that
10209 * arn't being used, as such reducing the processing required for the page.
10211 * How to use the lang_string class?
10212 * There are two methods of using the lang_string class, first through the
10213 * forth argument of the get_string function, and secondly directly.
10214 * The following are examples of both.
10215 * 1. Through get_string calls e.g.
10216 * $string = get_string($identifier, $component, $a, true);
10217 * $string = get_string('yes', 'moodle', null, true);
10218 * 2. Direct instantiation
10219 * $string = new lang_string($identifier, $component, $a, $lang);
10220 * $string = new lang_string('yes');
10222 * How do I use a lang_string object?
10223 * The lang_string object makes use of a magic __toString method so that you
10224 * are able to use the object exactly as you would use a string in most cases.
10225 * This means you are able to collect it into a variable and then directly
10226 * echo it, or concatenate it into another string, or similar.
10227 * The other thing you can do is manually get the string by calling the
10228 * lang_strings out method e.g.
10229 * $string = new lang_string('yes');
10230 * $string->out();
10231 * Also worth noting is that the out method can take one argument, $lang which
10232 * allows the developer to change the language on the fly.
10234 * When should I use a lang_string object?
10235 * The lang_string object is designed to be used in any situation where a
10236 * string may not be needed, but needs to be generated.
10237 * The admin tree is a good example of where lang_string objects should be
10238 * used.
10239 * A more practical example would be any class that requries strings that may
10240 * not be printed (after all classes get renderer by renderers and who knows
10241 * what they will do ;))
10243 * When should I not use a lang_string object?
10244 * Don't use lang_strings when you are going to use a string immediately.
10245 * There is no need as it will be processed immediately and there will be no
10246 * advantage, and in fact perhaps a negative hit as a class has to be
10247 * instantiated for a lang_string object, however get_string won't require
10248 * that.
10250 * Limitations:
10251 * 1. You cannot use a lang_string object as an array offset. Doing so will
10252 * result in PHP throwing an error. (You can use it as an object property!)
10254 * @package core
10255 * @category string
10256 * @copyright 2011 Sam Hemelryk
10257 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
10259 class lang_string {
10261 /** @var string The strings identifier */
10262 protected $identifier;
10263 /** @var string The strings component. Default '' */
10264 protected $component = '';
10265 /** @var array|stdClass Any arguments required for the string. Default null */
10266 protected $a = null;
10267 /** @var string The language to use when processing the string. Default null */
10268 protected $lang = null;
10270 /** @var string The processed string (once processed) */
10271 protected $string = null;
10274 * A special boolean. If set to true then the object has been woken up and
10275 * cannot be regenerated. If this is set then $this->string MUST be used.
10276 * @var bool
10278 protected $forcedstring = false;
10281 * Constructs a lang_string object
10283 * This function should do as little processing as possible to ensure the best
10284 * performance for strings that won't be used.
10286 * @param string $identifier The strings identifier
10287 * @param string $component The strings component
10288 * @param stdClass|array|mixed $a Any arguments the string requires
10289 * @param string $lang The language to use when processing the string.
10290 * @throws coding_exception
10292 public function __construct($identifier, $component = '', $a = null, $lang = null) {
10293 if (empty($component)) {
10294 $component = 'moodle';
10297 $this->identifier = $identifier;
10298 $this->component = $component;
10299 $this->lang = $lang;
10301 // We MUST duplicate $a to ensure that it if it changes by reference those
10302 // changes are not carried across.
10303 // To do this we always ensure $a or its properties/values are strings
10304 // and that any properties/values that arn't convertable are forgotten.
10305 if ($a !== null) {
10306 if (is_scalar($a)) {
10307 $this->a = $a;
10308 } else if ($a instanceof lang_string) {
10309 $this->a = $a->out();
10310 } else if (is_object($a) or is_array($a)) {
10311 $a = (array)$a;
10312 $this->a = array();
10313 foreach ($a as $key => $value) {
10314 // Make sure conversion errors don't get displayed (results in '').
10315 if (is_array($value)) {
10316 $this->a[$key] = '';
10317 } else if (is_object($value)) {
10318 if (method_exists($value, '__toString')) {
10319 $this->a[$key] = $value->__toString();
10320 } else {
10321 $this->a[$key] = '';
10323 } else {
10324 $this->a[$key] = (string)$value;
10330 if (debugging(false, DEBUG_DEVELOPER)) {
10331 if (clean_param($this->identifier, PARAM_STRINGID) == '') {
10332 throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please check your string definition');
10334 if (!empty($this->component) && clean_param($this->component, PARAM_COMPONENT) == '') {
10335 throw new coding_exception('Invalid string compontent. Please check your string definition');
10337 if (!get_string_manager()->string_exists($this->identifier, $this->component)) {
10338 debugging('String does not exist. Please check your string definition for '.$this->identifier.'/'.$this->component, DEBUG_DEVELOPER);
10344 * Processes the string.
10346 * This function actually processes the string, stores it in the string property
10347 * and then returns it.
10348 * You will notice that this function is VERY similar to the get_string method.
10349 * That is because it is pretty much doing the same thing.
10350 * However as this function is an upgrade it isn't as tolerant to backwards
10351 * compatibility.
10353 * @return string
10354 * @throws coding_exception
10356 protected function get_string() {
10357 global $CFG;
10359 // Check if we need to process the string.
10360 if ($this->string === null) {
10361 // Check the quality of the identifier.
10362 if ($CFG->debugdeveloper && clean_param($this->identifier, PARAM_STRINGID) === '') {
10363 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);
10366 // Process the string.
10367 $this->string = get_string_manager()->get_string($this->identifier, $this->component, $this->a, $this->lang);
10368 // Debugging feature lets you display string identifier and component.
10369 if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
10370 $this->string .= ' {' . $this->identifier . '/' . $this->component . '}';
10373 // Return the string.
10374 return $this->string;
10378 * Returns the string
10380 * @param string $lang The langauge to use when processing the string
10381 * @return string
10383 public function out($lang = null) {
10384 if ($lang !== null && $lang != $this->lang && ($this->lang == null && $lang != current_language())) {
10385 if ($this->forcedstring) {
10386 debugging('lang_string objects that have been used cannot be printed in another language. ('.$this->lang.' used)', DEBUG_DEVELOPER);
10387 return $this->get_string();
10389 $translatedstring = new lang_string($this->identifier, $this->component, $this->a, $lang);
10390 return $translatedstring->out();
10392 return $this->get_string();
10396 * Magic __toString method for printing a string
10398 * @return string
10400 public function __toString() {
10401 return $this->get_string();
10405 * Magic __set_state method used for var_export
10407 * @param array $array
10408 * @return self
10410 public static function __set_state(array $array): self {
10411 $tmp = new lang_string($array['identifier'], $array['component'], $array['a'], $array['lang']);
10412 $tmp->string = $array['string'];
10413 $tmp->forcedstring = $array['forcedstring'];
10414 return $tmp;
10418 * Prepares the lang_string for sleep and stores only the forcedstring and
10419 * string properties... the string cannot be regenerated so we need to ensure
10420 * it is generated for this.
10422 * @return string
10424 public function __sleep() {
10425 $this->get_string();
10426 $this->forcedstring = true;
10427 return array('forcedstring', 'string', 'lang');
10431 * Returns the identifier.
10433 * @return string
10435 public function get_identifier() {
10436 return $this->identifier;
10440 * Returns the component.
10442 * @return string
10444 public function get_component() {
10445 return $this->component;
10450 * Get human readable name describing the given callable.
10452 * This performs syntax check only to see if the given param looks like a valid function, method or closure.
10453 * It does not check if the callable actually exists.
10455 * @param callable|string|array $callable
10456 * @return string|bool Human readable name of callable, or false if not a valid callable.
10458 function get_callable_name($callable) {
10460 if (!is_callable($callable, true, $name)) {
10461 return false;
10463 } else {
10464 return $name;
10469 * Tries to guess if $CFG->wwwroot is publicly accessible or not.
10470 * Never put your faith on this function and rely on its accuracy as there might be false positives.
10471 * It just performs some simple checks, and mainly is used for places where we want to hide some options
10472 * such as site registration when $CFG->wwwroot is not publicly accessible.
10473 * Good thing is there is no false negative.
10474 * Note that it's possible to force the result of this check by specifying $CFG->site_is_public in config.php
10476 * @return bool
10478 function site_is_public() {
10479 global $CFG;
10481 // Return early if site admin has forced this setting.
10482 if (isset($CFG->site_is_public)) {
10483 return (bool)$CFG->site_is_public;
10486 $host = parse_url($CFG->wwwroot, PHP_URL_HOST);
10488 if ($host === 'localhost' || preg_match('|^127\.\d+\.\d+\.\d+$|', $host)) {
10489 $ispublic = false;
10490 } else if (\core\ip_utils::is_ip_address($host) && !ip_is_public($host)) {
10491 $ispublic = false;
10492 } else if (($address = \core\ip_utils::get_ip_address($host)) && !ip_is_public($address)) {
10493 $ispublic = false;
10494 } else {
10495 $ispublic = true;
10498 return $ispublic;
10502 * Validates user's password length.
10504 * @param string $password
10505 * @param int $pepperlength The length of the used peppers
10506 * @return bool
10508 function exceeds_password_length(string $password, int $pepperlength = 0): bool {
10509 return (strlen($password) > (MAX_PASSWORD_CHARACTERS + $pepperlength));
10513 * A helper to replace PHP 8.3 usage of array_keys with two args.
10515 * There is an indication that this will become a new method in PHP 8.4, but that has not happened yet.
10516 * Therefore this non-polyfill has been created with a different naming convention.
10517 * In the future it can be deprecated if a core PHP method is created.
10519 * https://wiki.php.net/rfc/deprecate_functions_with_overloaded_signatures#array_keys
10521 * @param array $array
10522 * @param mixed $filter The value to filter on
10523 * @param bool $strict Whether to apply a strit test with the filter
10524 * @return array
10526 function moodle_array_keys_filter(array $array, mixed $filter, bool $strict = false): array {
10527 return array_keys(array_filter(
10528 $array,
10529 function($value, $key) use ($filter, $strict): bool {
10530 if ($strict) {
10531 return $value === $filter;
10533 return $value == $filter;
10535 ARRAY_FILTER_USE_BOTH,